diff --git a/src/components/NotificationRow.test.tsx b/src/components/NotificationRow.test.tsx index af7b1627d..71be711e6 100644 --- a/src/components/NotificationRow.test.tsx +++ b/src/components/NotificationRow.test.tsx @@ -117,6 +117,25 @@ describe('components/NotificationRow.tsx', () => { }); }); + describe('notification labels', () => { + it('should render labels metric when available', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2024').valueOf()); + + const mockNotification = mockSingleNotification; + mockNotification.subject.labels = ['enhancement', 'good-first-issue']; + + const props = { + notification: mockNotification, + hostname: 'github.com', + }; + + const tree = render(); + expect(tree).toMatchSnapshot(); + }); + }); + describe('linked issues/prs', () => { it('should render when linked to one issue/pr', async () => { jest diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx index d9cd9b9f1..1a5af4379 100644 --- a/src/components/NotificationRow.tsx +++ b/src/components/NotificationRow.tsx @@ -5,6 +5,7 @@ import { FeedPersonIcon, IssueClosedIcon, ReadIcon, + TagIcon, } from '@primer/octicons-react'; import { type FC, @@ -92,6 +93,10 @@ export const NotificationRow: FC = ({ notification, hostname }) => { notification.subject.comments > 1 ? 'comments' : 'comment' }`; + const labelsPillDescription = notification.subject.labels + ?.map((label) => `🏷️ ${label}`) + .join('\n'); + const linkedIssuesPillDescription = `Linked to ${ notification.subject.linkedIssues?.length > 1 ? 'issues' : 'issue' } ${notification.subject?.linkedIssues?.join(', ')}`; @@ -201,6 +206,18 @@ export const NotificationRow: FC = ({ notification, hostname }) => { )} + {notification.subject?.labels?.length > 0 && ( + + + + )} diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap index 6479765ad..c2cac717f 100644 --- a/src/components/__snapshots__/NotificationRow.test.tsx.snap +++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap @@ -186,6 +186,34 @@ exports[`components/NotificationRow.tsx linked issues/prs should render when lin 2 + + + @@ -442,6 +470,34 @@ exports[`components/NotificationRow.tsx linked issues/prs should render when lin 2 + + + @@ -755,6 +811,34 @@ exports[`components/NotificationRow.tsx linked issues/prs should render when lin 2 + + + @@ -1011,6 +1095,601 @@ exports[`components/NotificationRow.tsx linked issues/prs should render when lin 2 + + + + + + + +
+ + + +
+ + , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`components/NotificationRow.tsx notification labels should render labels metric when available 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+
+ + + + +
+
+
+ I am a robot and this is a test! +
+
+ + + + + + + Updated + + + over 6 years ago + + + + + + + + + + + + + + + +
+
+
+ + + +
+
+
+ , + "container":
+
+
+ + + + +
+
+
+ I am a robot and this is a test! +
+
+ + + + + + + Updated + + + over 6 years ago + + + + + + + + + + + + +
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts index 7d55d3325..cf33a3b79 100644 --- a/src/hooks/useNotifications.test.ts +++ b/src/hooks/useNotifications.test.ts @@ -225,6 +225,7 @@ describe('hooks/useNotifications.ts', () => { }, ], }, + labels: null, }, ], }, @@ -237,6 +238,7 @@ describe('hooks/useNotifications.ts', () => { state: 'closed', merged: true, user: mockNotificationUser, + labels: [], }); nock('https://api.github.com') .get('/repos/gitify-app/notifications-test/issues/3/comments') @@ -249,6 +251,7 @@ describe('hooks/useNotifications.ts', () => { state: 'closed', merged: false, user: mockNotificationUser, + labels: [], }); nock('https://api.github.com') .get('/repos/gitify-app/notifications-test/pulls/4/reviews') diff --git a/src/typesGitHub.ts b/src/typesGitHub.ts index 46f0ee943..316baee7c 100644 --- a/src/typesGitHub.ts +++ b/src/typesGitHub.ts @@ -266,6 +266,7 @@ export interface GitifySubject { reviews?: GitifyPullRequestReview[]; linkedIssues?: string[]; comments?: number; + labels?: string[]; } export interface PullRequest { @@ -287,6 +288,7 @@ export interface PullRequest { closed_at: string | null; merged_at: string | null; merge_commit_sha: string | null; + labels: Labels[]; draft: boolean; commits_url: string; review_comments_url: string; @@ -311,6 +313,16 @@ export interface GitifyPullRequestReview { users: string[]; } +export interface Labels { + id: number; + node_id: string; + url: string; + name: string; + color: string; + default: boolean; + description: string; +} + export interface PullRequestReview { id: number; node_id: string; @@ -416,6 +428,7 @@ export interface Issue { author_association: string; body: string; state_reason: IssueStateReasonType | null; + labels: Labels[]; } export interface IssueOrPullRequestComment { @@ -463,6 +476,15 @@ export interface Discussion { url: string; author: DiscussionAuthor; comments: DiscussionComments; + labels: DiscussionLabels | null; +} + +export interface DiscussionLabels { + nodes: DiscussionLabel[]; +} + +export interface DiscussionLabel { + name: string; } export interface DiscussionComments { diff --git a/src/utils/api/__mocks__/response-mocks.ts b/src/utils/api/__mocks__/response-mocks.ts index 603a58fb3..06e438f37 100644 --- a/src/utils/api/__mocks__/response-mocks.ts +++ b/src/utils/api/__mocks__/response-mocks.ts @@ -6,6 +6,7 @@ import type { Discussion, DiscussionAuthor, DiscussionComments, + DiscussionLabels, GraphQLSearch, Notification, Repository, @@ -388,6 +389,14 @@ export const mockDiscussionComments: DiscussionComments = { totalCount: 2, }; +export const mockDiscussionLabels: DiscussionLabels = { + nodes: [ + { + name: 'enhancement', + }, + ], +}; + export const mockGraphQLResponse: GraphQLSearch = { data: { search: { @@ -404,6 +413,7 @@ export const mockGraphQLResponse: GraphQLSearch = { type: 'User', }, comments: mockDiscussionComments, + labels: mockDiscussionLabels, }, ], }, diff --git a/src/utils/api/graphql/discussions.ts b/src/utils/api/graphql/discussions.ts index 7472f10a0..ab34a9001 100644 --- a/src/utils/api/graphql/discussions.ts +++ b/src/utils/api/graphql/discussions.ts @@ -49,6 +49,11 @@ export const QUERY_SEARCH_DISCUSSIONS = gql` } } } + labels { + nodes { + name + } + } } } } diff --git a/src/utils/subject.test.ts b/src/utils/subject.test.ts index 9ea16cad2..8ecc5c1e8 100644 --- a/src/utils/subject.test.ts +++ b/src/utils/subject.test.ts @@ -227,6 +227,7 @@ describe('utils/subject.ts', () => { type: mockDiscussionAuthor.type, }, comments: 0, + labels: [], }); }); @@ -252,6 +253,7 @@ describe('utils/subject.ts', () => { type: mockDiscussionAuthor.type, }, comments: 0, + labels: [], }); }); @@ -277,6 +279,7 @@ describe('utils/subject.ts', () => { type: mockDiscussionAuthor.type, }, comments: 0, + labels: [], }); }); @@ -302,6 +305,7 @@ describe('utils/subject.ts', () => { type: mockDiscussionAuthor.type, }, comments: 0, + labels: [], }); }); @@ -327,6 +331,7 @@ describe('utils/subject.ts', () => { type: mockDiscussionAuthor.type, }, comments: 0, + labels: [], }); }); @@ -352,6 +357,41 @@ describe('utils/subject.ts', () => { type: mockDiscussionAuthor.type, }, comments: 0, + labels: [], + }); + }); + + it('discussion with labels', async () => { + const mockDiscussion = mockDiscussionNode(null, true); + mockDiscussion.labels = { + nodes: [ + { + name: 'enhancement', + }, + ], + }; + nock('https://api.github.com') + .post('/graphql') + .reply(200, { + data: { + search: { + nodes: [mockDiscussion], + }, + }, + }); + + const result = await getGitifySubjectDetails(mockNotification); + + expect(result).toEqual({ + state: 'ANSWERED', + user: { + login: mockDiscussionAuthor.login, + html_url: mockDiscussionAuthor.url, + avatar_url: mockDiscussionAuthor.avatar_url, + type: mockDiscussionAuthor.type, + }, + comments: 0, + labels: ['enhancement'], }); }); }); @@ -371,7 +411,7 @@ describe('utils/subject.ts', () => { it('open issue state', async () => { nock('https://api.github.com') .get('/repos/gitify-app/notifications-test/issues/1') - .reply(200, { state: 'open', user: mockAuthor }); + .reply(200, { state: 'open', user: mockAuthor, labels: [] }); nock('https://api.github.com') .get('/repos/gitify-app/notifications-test/issues/comments/302888448') @@ -387,13 +427,14 @@ describe('utils/subject.ts', () => { avatar_url: mockCommenter.avatar_url, type: mockCommenter.type, }, + labels: [], }); }); it('closed issue state', async () => { nock('https://api.github.com') .get('/repos/gitify-app/notifications-test/issues/1') - .reply(200, { state: 'closed', user: mockAuthor }); + .reply(200, { state: 'closed', user: mockAuthor, labels: [] }); nock('https://api.github.com') .get('/repos/gitify-app/notifications-test/issues/comments/302888448') @@ -409,6 +450,7 @@ describe('utils/subject.ts', () => { avatar_url: mockCommenter.avatar_url, type: mockCommenter.type, }, + labels: [], }); }); @@ -419,6 +461,7 @@ describe('utils/subject.ts', () => { state: 'closed', state_reason: 'completed', user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -435,6 +478,7 @@ describe('utils/subject.ts', () => { avatar_url: mockCommenter.avatar_url, type: mockCommenter.type, }, + labels: [], }); }); @@ -445,6 +489,7 @@ describe('utils/subject.ts', () => { state: 'open', state_reason: 'not_planned', user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -461,6 +506,7 @@ describe('utils/subject.ts', () => { avatar_url: mockCommenter.avatar_url, type: mockCommenter.type, }, + labels: [], }); }); @@ -471,6 +517,7 @@ describe('utils/subject.ts', () => { state: 'open', state_reason: 'reopened', user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -487,6 +534,7 @@ describe('utils/subject.ts', () => { avatar_url: mockCommenter.avatar_url, type: mockCommenter.type, }, + labels: [], }); }); @@ -500,6 +548,7 @@ describe('utils/subject.ts', () => { draft: false, merged: false, user: mockAuthor, + labels: [], }); const result = await getGitifySubjectDetails(mockNotification); @@ -512,6 +561,34 @@ describe('utils/subject.ts', () => { avatar_url: mockAuthor.avatar_url, type: mockAuthor.type, }, + labels: [], + }); + }); + + it('issue with labels', async () => { + nock('https://api.github.com') + .get('/repos/gitify-app/notifications-test/issues/1') + .reply(200, { + state: 'open', + user: mockAuthor, + labels: [{ name: 'enhancement' }], + }); + + nock('https://api.github.com') + .get('/repos/gitify-app/notifications-test/issues/comments/302888448') + .reply(200, { user: mockCommenter }); + + const result = await getGitifySubjectDetails(mockNotification); + + expect(result).toEqual({ + state: 'open', + user: { + login: mockCommenter.login, + html_url: mockCommenter.html_url, + avatar_url: mockCommenter.avatar_url, + type: mockCommenter.type, + }, + labels: ['enhancement'], }); }); }); @@ -537,6 +614,7 @@ describe('utils/subject.ts', () => { draft: false, merged: false, user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -558,6 +636,7 @@ describe('utils/subject.ts', () => { type: mockCommenter.type, }, reviews: null, + labels: [], linkedIssues: [], }); }); @@ -570,6 +649,7 @@ describe('utils/subject.ts', () => { draft: true, merged: false, user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -591,6 +671,7 @@ describe('utils/subject.ts', () => { type: mockCommenter.type, }, reviews: null, + labels: [], linkedIssues: [], }); }); @@ -603,6 +684,7 @@ describe('utils/subject.ts', () => { draft: false, merged: true, user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -624,6 +706,7 @@ describe('utils/subject.ts', () => { type: mockCommenter.type, }, reviews: null, + labels: [], linkedIssues: [], }); }); @@ -636,6 +719,7 @@ describe('utils/subject.ts', () => { draft: false, merged: false, user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -657,6 +741,7 @@ describe('utils/subject.ts', () => { type: mockCommenter.type, }, reviews: null, + labels: [], linkedIssues: [], }); }); @@ -672,6 +757,7 @@ describe('utils/subject.ts', () => { draft: false, merged: false, user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -689,6 +775,7 @@ describe('utils/subject.ts', () => { type: mockAuthor.type, }, reviews: null, + labels: [], linkedIssues: [], }); }); @@ -703,6 +790,7 @@ describe('utils/subject.ts', () => { draft: false, merged: false, user: mockAuthor, + labels: [], }); nock('https://api.github.com') @@ -720,6 +808,7 @@ describe('utils/subject.ts', () => { type: mockAuthor.type, }, reviews: null, + labels: [], linkedIssues: [], }); }); @@ -782,7 +871,42 @@ describe('utils/subject.ts', () => { }); }); - describe('Pull Request Reviews - Extract Linked Issues', () => { + it('Pull Requests With labels', async () => { + nock('https://api.github.com') + .get('/repos/gitify-app/notifications-test/pulls/1') + .reply(200, { + state: 'open', + draft: false, + merged: false, + user: mockAuthor, + labels: [{ name: 'enhancement' }], + }); + + nock('https://api.github.com') + .get('/repos/gitify-app/notifications-test/issues/comments/302888448') + .reply(200, { user: mockCommenter }); + + nock('https://api.github.com') + .get('/repos/gitify-app/notifications-test/pulls/1/reviews') + .reply(200, []); + + const result = await getGitifySubjectDetails(mockNotification); + + expect(result).toEqual({ + state: 'open', + user: { + login: mockCommenter.login, + html_url: mockCommenter.html_url, + avatar_url: mockCommenter.avatar_url, + type: mockCommenter.type, + }, + reviews: null, + labels: ['enhancement'], + linkedIssues: [], + }); + }); + + describe('Pull Request With Linked Issues', () => { it('returns empty if no pr body', () => { const result = parseLinkedIssuesFromPrBody(null); expect(result).toEqual([]); @@ -1078,5 +1202,6 @@ function mockDiscussionNode( nodes: [], totalCount: 0, }, + labels: null, }; } diff --git a/src/utils/subject.ts b/src/utils/subject.ts index 45085a41a..ffbc3736d 100644 --- a/src/utils/subject.ts +++ b/src/utils/subject.ts @@ -184,6 +184,7 @@ async function getGitifySubjectForDiscussion( state: discussionState, user: discussionUser, comments: discussion.comments.totalCount, + labels: discussion.labels?.nodes.map((label) => label.name) ?? [], }; } @@ -231,6 +232,7 @@ async function getGitifySubjectForIssue( type: issueCommentUser?.type ?? issue.user.type, }, comments: issue.comments, + labels: issue.labels?.map((label) => label.name) ?? [], }; } @@ -276,6 +278,7 @@ async function getGitifySubjectForPullRequest( }, reviews: reviews, comments: pr.comments, + labels: pr.labels?.map((label) => label.name) ?? [], linkedIssues: linkedIssues, }; }