diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx index 360df2840..44c049c0a 100644 --- a/src/components/NotificationRow.tsx +++ b/src/components/NotificationRow.tsx @@ -53,11 +53,8 @@ export const NotificationRow: React.FC = ({ }; const reason = formatReason(notification.reason); - const NotificationIcon = getNotificationTypeIcon( - notification.subject.type, - notification.subject.state, - ); - const iconColor = getNotificationTypeIconColor(notification.subject.state); + const NotificationIcon = getNotificationTypeIcon(notification.subject); + const iconColor = getNotificationTypeIconColor(notification.subject); const realIconColor = settings ? (settings.colors && iconColor) || '' : iconColor; diff --git a/src/typesGithub.ts b/src/typesGithub.ts index bbe02f134..0f6a087e6 100644 --- a/src/typesGithub.ts +++ b/src/typesGithub.ts @@ -37,6 +37,21 @@ export type StateType = IssueStateType | PullRequestStateType; export type ViewerSubscription = 'IGNORED' | 'SUBSCRIBED' | 'UNSUBSCRIBED'; +export type CheckSuiteStatus = + | 'action_required' + | 'cancelled' + | 'completed' + | 'failure' + | 'in_progress' + | 'pending' + | 'queued' + | 'requested' + | 'skipped' + | 'stale' + | 'success' + | 'timed_out' + | 'waiting'; + export interface Notification { id: string; unread: boolean; diff --git a/src/utils/__snapshots__/github-api.test.ts.snap b/src/utils/__snapshots__/github-api.test.ts.snap index 758f59fc6..67b2749bd 100644 --- a/src/utils/__snapshots__/github-api.test.ts.snap +++ b/src/utils/__snapshots__/github-api.test.ts.snap @@ -1,122 +1,132 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`./utils/github-api.ts should format the notification color 1`] = `"text-red-500"`; - -exports[`./utils/github-api.ts should format the notification color 2`] = `"text-purple-500"`; - -exports[`./utils/github-api.ts should format the notification color 3`] = `"text-gray-600"`; - -exports[`./utils/github-api.ts should format the notification color 4`] = `"text-purple-500"`; - -exports[`./utils/github-api.ts should format the notification color 5`] = `"text-gray-300"`; - -exports[`./utils/github-api.ts should format the notification color 6`] = `"text-green-500"`; - -exports[`./utils/github-api.ts should format the notification color 7`] = `"text-green-500"`; - -exports[`./utils/github-api.ts should format the notification color 8`] = `"text-gray-300"`; - -exports[`./utils/github-api.ts should format the notification reason 1`] = ` +exports[`formatReason should format the notification reason 1`] = ` { "description": "You were assigned to the issue.", "type": "Assign", } `; -exports[`./utils/github-api.ts should format the notification reason 2`] = ` +exports[`formatReason should format the notification reason 2`] = ` { "description": "You created the thread.", "type": "Author", } `; -exports[`./utils/github-api.ts should format the notification reason 3`] = ` +exports[`formatReason should format the notification reason 3`] = ` { "description": "A GitHub Actions workflow run was triggered for your repository", "type": "Workflow Run", } `; -exports[`./utils/github-api.ts should format the notification reason 4`] = ` +exports[`formatReason should format the notification reason 4`] = ` { "description": "You commented on the thread.", "type": "Comment", } `; -exports[`./utils/github-api.ts should format the notification reason 5`] = ` +exports[`formatReason should format the notification reason 5`] = ` { "description": "You accepted an invitation to contribute to the repository.", "type": "Invitation", } `; -exports[`./utils/github-api.ts should format the notification reason 6`] = ` +exports[`formatReason should format the notification reason 6`] = ` { "description": "You subscribed to the thread (via an issue or pull request).", "type": "Manual", } `; -exports[`./utils/github-api.ts should format the notification reason 7`] = ` +exports[`formatReason should format the notification reason 7`] = ` { "description": "Organization members have requested to enable a feature such as Draft Pull Requests or CoPilot.", "type": "Member Feature Requested", } `; -exports[`./utils/github-api.ts should format the notification reason 8`] = ` +exports[`formatReason should format the notification reason 8`] = ` { "description": "You were specifically @mentioned in the content.", "type": "Mention", } `; -exports[`./utils/github-api.ts should format the notification reason 9`] = ` +exports[`formatReason should format the notification reason 9`] = ` { "description": "You, or a team you're a member of, were requested to review a pull request.", "type": "Review Requested", } `; -exports[`./utils/github-api.ts should format the notification reason 10`] = ` +exports[`formatReason should format the notification reason 10`] = ` { "description": "You were credited for contributing to a security advisory.", "type": "Security Advisory Credit", } `; -exports[`./utils/github-api.ts should format the notification reason 11`] = ` +exports[`formatReason should format the notification reason 11`] = ` { "description": "GitHub discovered a security vulnerability in your repository.", "type": "Security Alert", } `; -exports[`./utils/github-api.ts should format the notification reason 12`] = ` +exports[`formatReason should format the notification reason 12`] = ` { "description": "You changed the thread state (for example, closing an issue or merging a pull request).", "type": "State Change", } `; -exports[`./utils/github-api.ts should format the notification reason 13`] = ` +exports[`formatReason should format the notification reason 13`] = ` { "description": "You're watching the repository.", "type": "Subscribed", } `; -exports[`./utils/github-api.ts should format the notification reason 14`] = ` +exports[`formatReason should format the notification reason 14`] = ` { "description": "You were on a team that was mentioned.", "type": "Team Mention", } `; -exports[`./utils/github-api.ts should format the notification reason 15`] = ` +exports[`formatReason should format the notification reason 15`] = ` { "description": "The reason for this notification is not supported by the app.", "type": "Unknown", } `; + +exports[`getNotificationTypeIconColor should format the notification color for check suite 1`] = `"text-gray-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for check suite 2`] = `"text-red-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for check suite 3`] = `"text-gray-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for check suite 4`] = `"text-green-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for check suite 5`] = `"text-gray-300"`; + +exports[`getNotificationTypeIconColor should format the notification color for state 1`] = `"text-red-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for state 2`] = `"text-purple-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for state 3`] = `"text-gray-600"`; + +exports[`getNotificationTypeIconColor should format the notification color for state 4`] = `"text-purple-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for state 5`] = `"text-gray-300"`; + +exports[`getNotificationTypeIconColor should format the notification color for state 6`] = `"text-green-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for state 7`] = `"text-green-500"`; + +exports[`getNotificationTypeIconColor should format the notification color for state 8`] = `"text-gray-300"`; diff --git a/src/utils/github-api.test.ts b/src/utils/github-api.test.ts index 5ce254034..e9cc4d756 100644 --- a/src/utils/github-api.test.ts +++ b/src/utils/github-api.test.ts @@ -3,9 +3,9 @@ import { getNotificationTypeIcon, getNotificationTypeIconColor, } from './github-api'; -import { Reason, StateType, SubjectType } from '../typesGithub'; +import { Reason, StateType, Subject, SubjectType } from '../typesGithub'; -describe('./utils/github-api.ts', () => { +describe('formatReason', () => { it('should format the notification reason', () => { expect(formatReason('assign')).toMatchSnapshot(); expect(formatReason('author')).toMatchSnapshot(); @@ -23,62 +23,228 @@ describe('./utils/github-api.ts', () => { expect(formatReason('team_mention')).toMatchSnapshot(); expect(formatReason('something_else_unknown' as Reason)).toMatchSnapshot(); }); +}); +describe('getNotificationTypeIcon', () => { it('should get the notification type icon', () => { - expect(getNotificationTypeIcon('CheckSuite').displayName).toBe('SyncIcon'); - expect(getNotificationTypeIcon('Commit').displayName).toBe('GitCommitIcon'); - expect(getNotificationTypeIcon('Discussion').displayName).toBe( - 'CommentDiscussionIcon', - ); - expect(getNotificationTypeIcon('Issue').displayName).toBe( - 'IssueOpenedIcon', - ); - expect(getNotificationTypeIcon('Issue', 'draft').displayName).toBe( - 'IssueDraftIcon', - ); - expect(getNotificationTypeIcon('Issue', 'closed').displayName).toBe( - 'IssueClosedIcon', - ); - expect(getNotificationTypeIcon('Issue', 'completed').displayName).toBe( - 'IssueClosedIcon', - ); - expect(getNotificationTypeIcon('Issue', 'reopened').displayName).toBe( - 'IssueReopenedIcon', - ); - expect(getNotificationTypeIcon('PullRequest').displayName).toBe( - 'GitPullRequestIcon', - ); - expect(getNotificationTypeIcon('PullRequest', 'draft').displayName).toBe( - 'GitPullRequestDraftIcon', - ); - expect(getNotificationTypeIcon('PullRequest', 'closed').displayName).toBe( - 'GitPullRequestClosedIcon', - ); - expect(getNotificationTypeIcon('PullRequest', 'merged').displayName).toBe( - 'GitMergeIcon', - ); - expect(getNotificationTypeIcon('Release').displayName).toBe('TagIcon'); - expect(getNotificationTypeIcon('RepositoryInvitation').displayName).toBe( - 'MailIcon', - ); expect( - getNotificationTypeIcon('RepositoryVulnerabilityAlert').displayName, + getNotificationTypeIcon(createSubjectMock({ type: 'CheckSuite' })) + .displayName, + ).toBe('SyncIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'CheckSuite', + title: 'Workflow cancelled for main branch', + }), + ).displayName, + ).toBe('StopIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'CheckSuite', + title: 'Workflow failed for main branch', + }), + ).displayName, + ).toBe('XIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'CheckSuite', + title: 'Workflow skipped for main branch', + }), + ).displayName, + ).toBe('SkipIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'CheckSuite', + title: 'Workflow succeeded for main branch', + }), + ).displayName, + ).toBe('CheckIcon'); + expect( + getNotificationTypeIcon(createSubjectMock({ type: 'Commit' })) + .displayName, + ).toBe('GitCommitIcon'); + expect( + getNotificationTypeIcon(createSubjectMock({ type: 'Discussion' })) + .displayName, + ).toBe('CommentDiscussionIcon'); + expect( + getNotificationTypeIcon(createSubjectMock({ type: 'Issue' })).displayName, + ).toBe('IssueOpenedIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ type: 'Issue', state: 'draft' }), + ).displayName, + ).toBe('IssueDraftIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'Issue', + state: 'closed', + }), + ).displayName, + ).toBe('IssueClosedIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'Issue', + state: 'completed', + }), + ).displayName, + ).toBe('IssueClosedIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'Issue', + state: 'reopened', + }), + ).displayName, + ).toBe('IssueReopenedIcon'); + expect( + getNotificationTypeIcon(createSubjectMock({ type: 'PullRequest' })) + .displayName, + ).toBe('GitPullRequestIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'PullRequest', + state: 'draft', + }), + ).displayName, + ).toBe('GitPullRequestDraftIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'PullRequest', + state: 'closed', + }), + ).displayName, + ).toBe('GitPullRequestClosedIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'PullRequest', + state: 'merged', + }), + ).displayName, + ).toBe('GitMergeIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'Release', + }), + ).displayName, + ).toBe('TagIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'RepositoryInvitation', + }), + ).displayName, + ).toBe('MailIcon'); + expect( + getNotificationTypeIcon( + createSubjectMock({ + type: 'RepositoryVulnerabilityAlert', + }), + ).displayName, ).toBe('AlertIcon'); - expect(getNotificationTypeIcon('Unknown' as SubjectType).displayName).toBe( + expect(getNotificationTypeIcon(createSubjectMock({})).displayName).toBe( 'QuestionIcon', ); }); +}); + +describe('getNotificationTypeIconColor', () => { + it('should format the notification color for check suite', () => { + expect( + getNotificationTypeIconColor( + createSubjectMock({ + type: 'CheckSuite', + title: 'Workflow cancelled for main branch', + }), + ), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor( + createSubjectMock({ + type: 'CheckSuite', + title: 'Workflow failed for main branch', + }), + ), + ).toMatchSnapshot(); - it('should format the notification color', () => { - expect(getNotificationTypeIconColor('closed')).toMatchSnapshot(); - expect(getNotificationTypeIconColor('completed')).toMatchSnapshot(); - expect(getNotificationTypeIconColor('draft')).toMatchSnapshot(); - expect(getNotificationTypeIconColor('merged')).toMatchSnapshot(); - expect(getNotificationTypeIconColor('not_planned')).toMatchSnapshot(); - expect(getNotificationTypeIconColor('open')).toMatchSnapshot(); - expect(getNotificationTypeIconColor('reopened')).toMatchSnapshot(); - expect( - getNotificationTypeIconColor('something_else_unknown' as StateType), + expect( + getNotificationTypeIconColor( + createSubjectMock({ + type: 'CheckSuite', + title: 'Workflow skipped for main branch', + }), + ), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor( + createSubjectMock({ + type: 'CheckSuite', + title: 'Workflow succeeded for main branch', + }), + ), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor( + createSubjectMock({ + type: 'CheckSuite', + title: 'unknown state', + }), + ), + ).toMatchSnapshot(); + }); + + it('should format the notification color for state', () => { + expect( + getNotificationTypeIconColor(createSubjectMock({ state: 'closed' })), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor(createSubjectMock({ state: 'completed' })), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor(createSubjectMock({ state: 'draft' })), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor(createSubjectMock({ state: 'merged' })), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor(createSubjectMock({ state: 'not_planned' })), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor(createSubjectMock({ state: 'open' })), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor(createSubjectMock({ state: 'reopened' })), + ).toMatchSnapshot(); + expect( + getNotificationTypeIconColor( + createSubjectMock({ + state: 'something_else_unknown' as StateType, + }), + ), ).toMatchSnapshot(); }); }); + +function createSubjectMock(mocks: { + title?: string; + type?: SubjectType; + state?: StateType; +}): Subject { + return { + title: mocks.title ?? 'Mock Subject', + type: mocks.type ?? ('Unknown' as SubjectType), + state: mocks.state ?? ('Unknown' as StateType), + url: null, + latest_comment_url: null, + }; +} diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts index 10811d5b2..eff854549 100644 --- a/src/utils/github-api.ts +++ b/src/utils/github-api.ts @@ -1,5 +1,6 @@ import { AlertIcon, + CheckIcon, CommentDiscussionIcon, GitCommitIcon, GitMergeIcon, @@ -13,10 +14,13 @@ import { MailIcon, OcticonProps, QuestionIcon, + SkipIcon, + StopIcon, SyncIcon, TagIcon, + XIcon, } from '@primer/octicons-react'; -import { Reason, StateType, SubjectType } from '../typesGithub'; +import { CheckSuiteStatus, Reason, Subject } from '../typesGithub'; // prettier-ignore const DESCRIPTIONS = { @@ -77,18 +81,30 @@ export function formatReason(reason: Reason): { } export function getNotificationTypeIcon( - type: SubjectType, - state?: StateType, + subject: Subject, ): React.FC { - switch (type) { + switch (subject.type) { case 'CheckSuite': - return SyncIcon; + const checkSuiteState = inferCheckSuiteStatus(subject.title); + + switch (checkSuiteState) { + case 'cancelled': + return StopIcon; + case 'failure': + return XIcon; + case 'skipped': + return SkipIcon; + case 'success': + return CheckIcon; + default: + return SyncIcon; + } case 'Commit': return GitCommitIcon; case 'Discussion': return CommentDiscussionIcon; case 'Issue': - switch (state) { + switch (subject.state) { case 'draft': return IssueDraftIcon; case 'closed': @@ -100,7 +116,7 @@ export function getNotificationTypeIcon( return IssueOpenedIcon; } case 'PullRequest': - switch (state) { + switch (subject.state) { case 'draft': return GitPullRequestDraftIcon; case 'closed': @@ -121,8 +137,25 @@ export function getNotificationTypeIcon( } } -export function getNotificationTypeIconColor(state: StateType): string { - switch (state) { +export function getNotificationTypeIconColor(subject: Subject): string { + if (subject.type === 'CheckSuite') { + const checkSuiteState = inferCheckSuiteStatus(subject.title); + + switch (checkSuiteState) { + case 'cancelled': + return 'text-gray-500'; + case 'failure': + return 'text-red-500'; + case 'skipped': + return 'text-gray-500'; + case 'success': + return 'text-green-500'; + default: + return 'text-gray-300'; + } + } + + switch (subject.state) { case 'closed': return 'text-red-500'; case 'completed': @@ -141,3 +174,27 @@ export function getNotificationTypeIconColor(state: StateType): string { return 'text-gray-300'; } } + +export function inferCheckSuiteStatus(title: string): CheckSuiteStatus { + if (title) { + const lowerTitle = title.toLowerCase(); + + if (lowerTitle.includes('cancelled for')) { + return 'cancelled'; + } + + if (lowerTitle.includes('failed for')) { + return 'failure'; + } + + if (lowerTitle.includes('skipped for')) { + return 'skipped'; + } + + if (lowerTitle.includes('succeeded for')) { + return 'success'; + } + } + + return null; +}