import {Fragment, useCallback, useState} from 'react'; import styled from '@emotion/styled'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {NoteBody} from 'sentry/components/activity/note/body'; import {NoteInputWithStorage} from 'sentry/components/activity/note/inputWithStorage'; import {Button} from 'sentry/components/button'; import {Flex} from 'sentry/components/container/flex'; import useMutateActivity from 'sentry/components/feedback/useMutateActivity'; import Timeline from 'sentry/components/timeline'; import TimeSince from 'sentry/components/timeSince'; import {Tooltip} from 'sentry/components/tooltip'; import {IconEllipsis} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import GroupStore from 'sentry/stores/groupStore'; import {space} from 'sentry/styles/space'; import textStyles from 'sentry/styles/text'; import type {NoteType} from 'sentry/types/alerts'; import type {Group, GroupActivity} from 'sentry/types/group'; import {GroupActivityType} from 'sentry/types/group'; import type {Team} from 'sentry/types/organization'; import type {User} from 'sentry/types/user'; import {trackAnalytics} from 'sentry/utils/analytics'; import {uniqueId} from 'sentry/utils/guid'; import useOrganization from 'sentry/utils/useOrganization'; import {useTeamsById} from 'sentry/utils/useTeamsById'; import {useUser} from 'sentry/utils/useUser'; import {groupActivityTypeIconMapping} from 'sentry/views/issueDetails/streamline/groupActivityIcons'; import getGroupActivityItem from 'sentry/views/issueDetails/streamline/groupActivityItem'; import {NoteDropdown} from 'sentry/views/issueDetails/streamline/noteDropdown'; import {SidebarSectionTitle} from 'sentry/views/issueDetails/streamline/sidebar'; function TimelineItem({ item, handleDelete, group, teams, }: { group: Group; handleDelete: (item: GroupActivity) => void; item: GroupActivity; teams: Team[]; }) { const organization = useOrganization(); const authorName = item.user ? item.user.name : 'Sentry'; const {title, message} = getGroupActivityItem( item, organization, group.project, {authorName}, teams ); const iconMapping = groupActivityTypeIconMapping[item.type]; const Icon = iconMapping?.componentFunction ? iconMapping.componentFunction(item.data) : iconMapping?.Component ?? null; return ( {title} {item.type === GroupActivityType.NOTE && ( handleDelete(item)} user={item.user} /> )} } timestamp={} icon={ Icon && ( ) } > {typeof message === 'string' ? ( ) : ( message )} ); } export default function StreamlinedActivitySection({group}: {group: Group}) { const organization = useOrganization(); const {teams} = useTeamsById(); const [showAll, setShowAll] = useState(false); const [inputId, setInputId] = useState(uniqueId()); const activeUser = useUser(); const projectSlugs = group?.project ? [group.project.slug] : []; const noteProps = { minHeight: 140, group, projectSlugs, placeholder: t('Add a comment\u2026'), }; const mutators = useMutateActivity({ organization, group, }); const handleDelete = useCallback( (item: GroupActivity) => { const restore = group.activity.find(activity => activity.id === item.id); const index = GroupStore.removeActivity(group.id, item.id); if (index === -1 || restore === undefined) { addErrorMessage(t('Failed to delete comment')); return; } mutators.handleDelete( item.id, group.activity.filter(a => a.id !== item.id), { onError: () => { addErrorMessage(t('Failed to delete comment')); }, onSuccess: () => { trackAnalytics('issue_details.comment_deleted', {organization}); addSuccessMessage(t('Comment removed')); }, } ); }, [group.activity, mutators, group.id, organization] ); const handleCreate = useCallback( (n: NoteType, _me: User) => { mutators.handleCreate(n, group.activity, { onError: err => { const errMessage = err.responseJSON?.detail ? tct('Error: [msg]', {msg: err.responseJSON?.detail as string}) : t('Unable to post comment'); addErrorMessage(errMessage); }, onSuccess: data => { GroupStore.addActivity(group.id, data); trackAnalytics('issue_details.comment_created', {organization}); addSuccessMessage(t('Comment posted')); }, }); }, [group.activity, mutators, group.id, organization] ); return (
{t('Activity')} {showAll && ( setShowAll(false)} analyticsEventKey="issue_details.activity_collapsed" analyticsEventName="Issue Details: Activity Collapsed" analyticsParams={{num_activities: group.activity.length}} > {t('Collapse')} )} { handleCreate(n, activeUser); setInputId(uniqueId()); }} source="issue-details" {...noteProps} /> {(group.activity.length < 5 || showAll) && group.activity.map(item => { return ( ); })} {!showAll && group.activity.length >= 5 && ( {group.activity.slice(0, 2).map(item => { return ( ); })} setShowAll(true)} borderless size="zero" analyticsEventKey="issue_details.activity_expanded" analyticsEventName="Issue Details: Activity Expanded" analyticsParams={{ num_activities_hidden: group.activity.length - 3, }} > {t('%s activities hidden', group.activity.length - 3)} } icon={} /> )}
); } const TitleTooltip = styled(Tooltip)` justify-self: start; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; const TitleDropdown = styled(NoteDropdown)` font-weight: normal; `; const ActivityTimelineItem = styled(Timeline.Item)` align-items: center; grid-template-columns: 22px minmax(50px, 1fr) auto; `; const Timestamp = styled(TimeSince)` font-size: ${p => p.theme.fontSizeSmall}; white-space: nowrap; `; const TextButton = styled(Button)` font-weight: ${p => p.theme.fontWeightNormal}; font-size: ${p => p.theme.fontSizeSmall}; color: ${p => p.theme.subText}; `; const RotatedEllipsisIcon = styled(IconEllipsis)` transform: rotate(90deg) translateY(1px); `; const NoteWrapper = styled('div')` ${textStyles} `;