import {Fragment} from 'react'; import styled from '@emotion/styled'; import moment from 'moment'; import CommitLink from 'sentry/components/commitLink'; import Duration from 'sentry/components/duration'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import PullRequestLink from 'sentry/components/pullRequestLink'; import Version from 'sentry/components/version'; import {t, tct, tn} from 'sentry/locale'; import TeamStore from 'sentry/stores/teamStore'; import { GroupActivity, GroupActivityAssigned, GroupActivitySetIgnored, GroupActivityType, Organization, Project, User, } from 'sentry/types'; type Props = { activity: GroupActivity; author: React.ReactNode; orgSlug: Organization['slug']; projectId: Project['id']; }; function GroupActivityItem({activity, orgSlug, projectId, author}: Props) { const issuesLink = `/organizations/${orgSlug}/issues/`; function getIgnoredMessage(data: GroupActivitySetIgnored['data']) { if (data.ignoreDuration) { return tct('[author] ignored this issue for [duration]', { author, duration: , }); } if (data.ignoreCount && data.ignoreWindow) { return tct( '[author] ignored this issue until it happens [count] time(s) in [duration]', { author, count: data.ignoreCount, duration: , } ); } if (data.ignoreCount) { return tct('[author] ignored this issue until it happens [count] time(s)', { author, count: data.ignoreCount, }); } if (data.ignoreUserCount && data.ignoreUserWindow) { return tct( '[author] ignored this issue until it affects [count] user(s) in [duration]', { author, count: data.ignoreUserCount, duration: , } ); } if (data.ignoreUserCount) { return tct('[author] ignored this issue until it affects [count] user(s)', { author, count: data.ignoreUserCount, }); } return tct('[author] ignored this issue', {author}); } function getAssignedMessage(data: GroupActivityAssigned['data']) { let assignee: string | User | undefined = undefined; if (data.assigneeType === 'team') { const team = TeamStore.getById(data.assignee); assignee = team ? `#${team.slug}` : ''; } else if (activity.user && data.assignee === activity.user.id) { assignee = t('themselves'); } else if (data.assigneeType === 'user' && data.assigneeEmail) { assignee = data.assigneeEmail; } else { assignee = t('an unknown user'); } const isAutoAssigned = ['projectOwnership', 'codeowners'].includes( data.integration as string ); const integrationName: Record< NonNullable, string > = { msteams: t('Microsoft Teams'), slack: t('Slack'), projectOwnership: t('Ownership Rule'), codeowners: t('Codeowners Rule'), }; return (
{tct('[author] [action] this issue to [assignee]', { action: isAutoAssigned ? t('auto-assigned') : t('assigned'), author, assignee, })}
{data.integration && ( {t('Assigned via %s', integrationName[data.integration])} {data.rule && ( : {data.rule} )} )}
); } function renderContent() { switch (activity.type) { case GroupActivityType.NOTE: return tct('[author] left a comment', {author}); case GroupActivityType.SET_RESOLVED: return tct('[author] marked this issue as resolved', {author}); case GroupActivityType.SET_RESOLVED_BY_AGE: return tct('[author] marked this issue as resolved due to inactivity', { author, }); case GroupActivityType.SET_RESOLVED_IN_RELEASE: const {current_release_version, version} = activity.data; if (current_release_version) { return tct( '[author] marked this issue as resolved in releases greater than [version]', { author, version: ( ), } ); } return version ? tct('[author] marked this issue as resolved in [version]', { author, version: ( ), }) : tct('[author] marked this issue as resolved in the upcoming release', { author, }); case GroupActivityType.SET_RESOLVED_IN_COMMIT: const deployedReleases = (activity.data.commit?.releases || []) .filter(r => r.dateReleased !== null) .sort( (a, b) => moment(a.dateReleased).valueOf() - moment(b.dateReleased).valueOf() ); if (deployedReleases.length === 1) { return tct( '[author] marked this issue as resolved in [version] [break]' + 'This commit was released in [release]', { author, version: ( ), break:
, release: ( ), } ); } if (deployedReleases.length > 1) { return tct( '[author] marked this issue as resolved in [version] [break]' + 'This commit was released in [release] and ' + (deployedReleases.length - 1) + ' others', { author, version: ( ), break:
, release: ( ), } ); } return tct('[author] marked this issue as resolved in [version]', { author, version: ( ), }); case GroupActivityType.SET_RESOLVED_IN_PULL_REQUEST: { const {data} = activity; const {pullRequest} = data; return tct('[author] marked this issue as resolved in [version]', { author, version: ( ), }); } case GroupActivityType.SET_UNRESOLVED: return tct('[author] marked this issue as unresolved', {author}); case GroupActivityType.SET_IGNORED: { const {data} = activity; return getIgnoredMessage(data); } case GroupActivityType.SET_PUBLIC: return tct('[author] made this issue public', {author}); case GroupActivityType.SET_PRIVATE: return tct('[author] made this issue private', {author}); case GroupActivityType.SET_REGRESSION: { const {data} = activity; return data.version ? tct('[author] marked this issue as a regression in [version]', { author, version: ( ), }) : tct('[author] marked this issue as a regression', {author}); } case GroupActivityType.CREATE_ISSUE: { const {data} = activity; return tct('[author] created an issue on [provider] titled [title]', { author, provider: data.provider, title: {data.title}, }); } case GroupActivityType.UNMERGE_SOURCE: { const {data} = activity; const {destination, fingerprints} = data; return tn( '%2$s migrated %1$s fingerprint to %3$s', '%2$s migrated %1$s fingerprints to %3$s', fingerprints.length, author, destination ? ( {destination.shortId} ) : ( t('a group') ) ); } case GroupActivityType.UNMERGE_DESTINATION: { const {data} = activity; const {source, fingerprints} = data; return tn( '%2$s migrated %1$s fingerprint from %3$s', '%2$s migrated %1$s fingerprints from %3$s', fingerprints.length, author, source ? ( {source.shortId} ) : ( t('a group') ) ); } case GroupActivityType.FIRST_SEEN: return tct('[author] first saw this issue', {author}); case GroupActivityType.ASSIGNED: { const {data} = activity; return getAssignedMessage(data); } case GroupActivityType.UNASSIGNED: return tct('[author] unassigned this issue', {author}); case GroupActivityType.MERGE: return tn( '%2$s merged %1$s issue into this issue', '%2$s merged %1$s issues into this issue', activity.data.issues.length, author ); case GroupActivityType.REPROCESS: { const {data} = activity; const {oldGroupId, eventCount} = data; return tct('[author] reprocessed the events in this issue. [new-events]', { author, ['new-events']: ( {tn('See %s new event', 'See %s new events', eventCount)} ), }); } case GroupActivityType.MARK_REVIEWED: { return tct('[author] marked this issue as reviewed', { author, }); } default: return ''; // should never hit (?) } } return {renderContent()}; } export default GroupActivityItem; const CodeWrapper = styled('div')` overflow-wrap: anywhere; font-size: ${p => p.theme.fontSizeSmall}; `; const StyledRuleSpan = styled('span')` font-family: ${p => p.theme.text.familyMono}; `;