diff --git a/src/components/NotificationRow.test.tsx b/src/components/NotificationRow.test.tsx index 7a2a05484..af7b1627d 100644 --- a/src/components/NotificationRow.test.tsx +++ b/src/components/NotificationRow.test.tsx @@ -117,6 +117,42 @@ describe('components/NotificationRow.tsx', () => { }); }); + describe('linked issues/prs', () => { + it('should render when linked to one issue/pr', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2024').valueOf()); + + const mockNotification = mockSingleNotification; + mockNotification.subject.linkedIssues = ['#1']; + + const props = { + notification: mockNotification, + hostname: 'github.com', + }; + + const tree = render(); + expect(tree).toMatchSnapshot(); + }); + + it('should render when linked to multiple issues/prs', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2024').valueOf()); + + const mockNotification = mockSingleNotification; + mockNotification.subject.linkedIssues = ['#1', '#2']; + + const props = { + notification: mockNotification, + hostname: 'github.com', + }; + + const tree = render(); + expect(tree).toMatchSnapshot(); + }); + }); + describe('notification interactions', () => { it('should open a notification in the browser - click', () => { const removeNotificationFromState = jest.fn(); diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx index a98eb5c80..d9cd9b9f1 100644 --- a/src/components/NotificationRow.tsx +++ b/src/components/NotificationRow.tsx @@ -3,6 +3,7 @@ import { CheckIcon, CommentIcon, FeedPersonIcon, + IssueClosedIcon, ReadIcon, } from '@primer/octicons-react'; import { @@ -91,6 +92,10 @@ export const NotificationRow: FC = ({ notification, hostname }) => { notification.subject.comments > 1 ? 'comments' : 'comment' }`; + const linkedIssuesPillDescription = `Linked to ${ + notification.subject.linkedIssues?.length > 1 ? 'issues' : 'issue' + } ${notification.subject?.linkedIssues?.join(', ')}`; + return (
= ({ notification, hostname }) => { {reason.title} {updatedAt} + {notification.subject?.linkedIssues?.length > 0 && ( + + + + )} {notification.subject.reviews ? notification.subject.reviews.map((review) => { const icon = getPullRequestReviewIcon(review); diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap index 58f5668d1..6479765ad 100644 --- a/src/components/__snapshots__/NotificationRow.test.tsx.snap +++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap @@ -1,5 +1,1143 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`components/NotificationRow.tsx linked issues/prs should render when linked to multiple issues/prs 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 + + + + + + + + + + + + + + + +
+
+
+ + + +
+
+
, + "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 linked issues/prs should render when linked to one issue/pr 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 + + + + + + + + + + + + + + + +
+
+
+ + + +
+
+
, + "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 rendering for notification comments count should render when 1 comment 1`] = ` { "asFragment": [Function], diff --git a/src/typesGitHub.ts b/src/typesGitHub.ts index 416b0164a..46f0ee943 100644 --- a/src/typesGitHub.ts +++ b/src/typesGitHub.ts @@ -264,6 +264,7 @@ export interface GitifySubject { state?: StateType; user?: SubjectUser; reviews?: GitifyPullRequestReview[]; + linkedIssues?: string[]; comments?: number; } diff --git a/src/utils/subject.test.ts b/src/utils/subject.test.ts index 43419582b..9ea16cad2 100644 --- a/src/utils/subject.test.ts +++ b/src/utils/subject.test.ts @@ -17,6 +17,7 @@ import { getGitifySubjectDetails, getLatestReviewForReviewers, getWorkflowRunAttributes, + parseLinkedIssuesFromPrBody, } from './subject'; const mockAuthor = partialMockUser('some-author'); @@ -557,6 +558,7 @@ describe('utils/subject.ts', () => { type: mockCommenter.type, }, reviews: null, + linkedIssues: [], }); }); @@ -589,6 +591,7 @@ describe('utils/subject.ts', () => { type: mockCommenter.type, }, reviews: null, + linkedIssues: [], }); }); @@ -621,6 +624,7 @@ describe('utils/subject.ts', () => { type: mockCommenter.type, }, reviews: null, + linkedIssues: [], }); }); @@ -653,6 +657,7 @@ describe('utils/subject.ts', () => { type: mockCommenter.type, }, reviews: null, + linkedIssues: [], }); }); @@ -684,6 +689,7 @@ describe('utils/subject.ts', () => { type: mockAuthor.type, }, reviews: null, + linkedIssues: [], }); }); @@ -714,6 +720,7 @@ describe('utils/subject.ts', () => { type: mockAuthor.type, }, reviews: null, + linkedIssues: [], }); }); @@ -774,6 +781,19 @@ describe('utils/subject.ts', () => { expect(result).toBeNull(); }); }); + + describe('Pull Request Reviews - Extract Linked Issues', () => { + it('returns empty if no pr body', () => { + const result = parseLinkedIssuesFromPrBody(null); + expect(result).toEqual([]); + }); + + it('returns linked issues', () => { + const mockPrBody = 'This PR is linked to #1, #2, and #3'; + const result = parseLinkedIssuesFromPrBody(mockPrBody); + expect(result).toEqual(['#1', '#2', '#3']); + }); + }); }); describe('Releases', () => { diff --git a/src/utils/subject.ts b/src/utils/subject.ts index ffcbe2c04..45085a41a 100644 --- a/src/utils/subject.ts +++ b/src/utils/subject.ts @@ -264,6 +264,7 @@ async function getGitifySubjectForPullRequest( } const reviews = await getLatestReviewForReviewers(notification); + const linkedIssues = parseLinkedIssuesFromPrBody(pr.body); return { state: prState, @@ -275,6 +276,7 @@ async function getGitifySubjectForPullRequest( }, reviews: reviews, comments: pr.comments, + linkedIssues: linkedIssues, }; } @@ -329,6 +331,26 @@ export async function getLatestReviewForReviewers( }); } +export function parseLinkedIssuesFromPrBody(body: string): string[] { + const linkedIssues: string[] = []; + + if (!body) { + return linkedIssues; + } + + const regexPattern = /\s*#(\d+)\s*/gi; + + const matches = body.matchAll(regexPattern); + + for (const match of matches) { + if (match[0]) { + linkedIssues.push(match[0].trim()); + } + } + + return linkedIssues; +} + async function getGitifySubjectForRelease( notification: Notification, ): Promise {