Skip to content

Commit 2174ebd

Browse files
committed
feat(api): codegen for discussions
Signed-off-by: Adam Setch <[email protected]>
1 parent af2e099 commit 2174ebd

File tree

5 files changed

+110
-106
lines changed

5 files changed

+110
-106
lines changed

src/renderer/typesGitHub.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,6 @@ export interface SubjectUser {
176176
type: UserType;
177177
}
178178

179-
export interface DiscussionAuthor {
180-
login: string;
181-
url: Link;
182-
avatar_url: Link;
183-
type: UserType;
184-
}
185-
186179
export interface Repository {
187180
id: number;
188181
node_id: string;
@@ -489,47 +482,6 @@ export interface Release {
489482
published_at: string | null;
490483
}
491484

492-
export interface GraphQLSearch<T> {
493-
data: {
494-
search: {
495-
nodes: T[];
496-
};
497-
};
498-
}
499-
500-
export interface Discussion {
501-
number: number;
502-
title: string;
503-
stateReason: DiscussionStateType;
504-
isAnswered: boolean | null;
505-
url: Link;
506-
author: DiscussionAuthor;
507-
comments: DiscussionComments;
508-
labels: DiscussionLabels | null;
509-
}
510-
511-
export interface DiscussionLabels {
512-
nodes: DiscussionLabel[];
513-
}
514-
515-
export interface DiscussionLabel {
516-
name: string;
517-
}
518-
519-
export interface DiscussionComments {
520-
nodes: DiscussionComment[];
521-
totalCount: number;
522-
}
523-
524-
export interface DiscussionComment {
525-
databaseId: string | number;
526-
createdAt: string;
527-
author: DiscussionAuthor;
528-
replies?: {
529-
nodes: DiscussionComment[];
530-
};
531-
}
532-
533485
export interface CheckSuiteAttributes {
534486
workflowName: string;
535487
attemptNumber?: number;

src/renderer/utils/api/__mocks__/response-mocks.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ import {
44
} from '../../../__mocks__/account-mocks';
55
import type { Link } from '../../../types';
66
import type {
7-
Discussion,
87
DiscussionAuthor,
9-
DiscussionComments,
10-
DiscussionLabels,
11-
GraphQLSearch,
128
Notification,
139
Repository,
1410
User,
1511
} from '../../../typesGitHub';
12+
import type { FetchDiscussionsQuery } from '../graphql/generated/graphql';
13+
import type { GitHubGraphQLResponse } from '../types';
1614

1715
export const mockNotificationUser: User = {
1816
login: 'octocat',
@@ -418,29 +416,30 @@ export const mockDiscussionLabels: DiscussionLabels = {
418416
],
419417
};
420418

421-
export const mockGraphQLResponse: GraphQLSearch<Discussion> = {
422-
data: {
423-
search: {
424-
nodes: [
425-
{
426-
number: 123,
427-
title: '1.16.0',
428-
isAnswered: false,
429-
stateReason: 'OPEN',
430-
url: 'https://github.com/gitify-app/notifications-test/discussions/612' as Link,
431-
author: {
432-
login: 'discussion-creator',
433-
url: 'https://github.com/discussion-creator' as Link,
434-
avatar_url:
435-
'https://avatars.githubusercontent.com/u/123456789?v=4' as Link,
436-
type: 'User',
419+
export const mockGraphQLResponse: GitHubGraphQLResponse<FetchDiscussionsQuery> =
420+
{
421+
data: {
422+
search: {
423+
nodes: [
424+
{
425+
number: 123,
426+
title: '1.16.0',
427+
isAnswered: false,
428+
stateReason: 'OPEN',
429+
url: 'https://github.com/gitify-app/notifications-test/discussions/612' as Link,
430+
author: {
431+
login: 'discussion-creator',
432+
url: 'https://github.com/discussion-creator' as Link,
433+
avatar_url:
434+
'https://avatars.githubusercontent.com/u/123456789?v=4' as Link,
435+
type: 'User',
436+
},
437+
comments: mockDiscussionComments,
438+
labels: mockDiscussionLabels,
437439
},
438-
comments: mockDiscussionComments,
439-
labels: mockDiscussionLabels,
440-
},
441-
],
440+
],
441+
},
442442
},
443-
},
444-
};
443+
};
445444

446445
export const mockSingleNotification: Notification = mockGitHubNotifications[0];

src/renderer/utils/helpers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { Constants } from '../constants';
88
import type { Chevron, Hostname, Link } from '../types';
99
import type { Notification } from '../typesGitHub';
1010
import { getHtmlUrl, getLatestDiscussion } from './api/client';
11+
import { useFragment as getFragmentData } from './api/graphql/generated/fragment-masking';
12+
import { DiscussionCommentFieldsFragmentDoc } from './api/graphql/generated/graphql';
1113
import type { PlatformType } from './auth/types';
1214
import { rendererLogError } from './logger';
1315
import { getCheckSuiteAttributes } from './notifications/handlers/checkSuite';
@@ -89,9 +91,16 @@ async function getDiscussionUrl(notification: Notification): Promise<Link> {
8991
if (discussion) {
9092
url.href = discussion.url;
9193

94+
// Unwrap discussion comments from fragment-masked types
95+
const discussionComments =
96+
getFragmentData(
97+
DiscussionCommentFieldsFragmentDoc,
98+
discussion.comments.nodes,
99+
) || [];
100+
92101
const closestComment = getClosestDiscussionCommentOrReply(
93102
notification,
94-
discussion.comments.nodes,
103+
discussionComments,
95104
);
96105
if (closestComment) {
97106
url.hash = `#discussioncomment-${closestComment.databaseId}`;

src/renderer/utils/notifications/handlers/discussion.test.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import {
77
} from '../../../__mocks__/notifications-mocks';
88
import { mockSettings } from '../../../__mocks__/state-mocks';
99
import type { Link } from '../../../types';
10-
import type {
11-
Discussion,
12-
DiscussionAuthor,
13-
DiscussionStateType,
14-
Repository,
15-
} from '../../../typesGitHub';
10+
import type { Repository } from '../../../typesGitHub';
11+
import {
12+
type AuthorFieldsFragment,
13+
type DiscussionFieldsFragment,
14+
DiscussionStateReason,
15+
} from '../../api/graphql/generated/graphql';
1616
import { discussionHandler } from './discussion';
1717

18-
const mockDiscussionAuthor: DiscussionAuthor = {
18+
const mockDiscussionAuthor: AuthorFieldsFragment = {
1919
login: 'discussion-author',
2020
url: 'https://github.com/discussion-author' as Link,
2121
avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link,
@@ -79,7 +79,9 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
7979
.reply(200, {
8080
data: {
8181
search: {
82-
nodes: [mockDiscussionNode('DUPLICATE', false)],
82+
nodes: [
83+
mockDiscussionNode(DiscussionStateReason.Duplicate, false),
84+
],
8385
},
8486
},
8587
});
@@ -139,7 +141,9 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
139141
.reply(200, {
140142
data: {
141143
search: {
142-
nodes: [mockDiscussionNode('OUTDATED', false)],
144+
nodes: [
145+
mockDiscussionNode(DiscussionStateReason.Outdated, false),
146+
],
143147
},
144148
},
145149
});
@@ -169,7 +173,9 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
169173
.reply(200, {
170174
data: {
171175
search: {
172-
nodes: [mockDiscussionNode('REOPENED', false)],
176+
nodes: [
177+
mockDiscussionNode(DiscussionStateReason.Reopened, false),
178+
],
173179
},
174180
},
175181
});
@@ -199,7 +205,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
199205
.reply(200, {
200206
data: {
201207
search: {
202-
nodes: [mockDiscussionNode('RESOLVED', true)],
208+
nodes: [mockDiscussionNode(DiscussionStateReason.Resolved, true)],
203209
},
204210
},
205211
});
@@ -308,9 +314,9 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
308314
});
309315

310316
function mockDiscussionNode(
311-
state: DiscussionStateType,
317+
state: DiscussionStateReason,
312318
isAnswered: boolean,
313-
): Discussion {
319+
): DiscussionFieldsFragment {
314320
return {
315321
number: 123,
316322
title: 'This is a mock discussion',

src/renderer/utils/notifications/handlers/discussion.ts

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ import type {
1919
SubjectUser,
2020
} from '../../../typesGitHub';
2121
import { getLatestDiscussion } from '../../api/client';
22+
import { useFragment as getFragmentData } from '../../api/graphql/generated/fragment-masking';
2223
import type {
23-
AuthorFieldsFragment,
2424
CommentFieldsFragment,
2525
DiscussionCommentFieldsFragment,
2626
} from '../../api/graphql/generated/graphql';
27+
import {
28+
AuthorFieldsFragmentDoc,
29+
CommentFieldsFragmentDoc,
30+
DiscussionCommentFieldsFragmentDoc,
31+
} from '../../api/graphql/generated/graphql';
2732
import { isStateFilteredOut } from '../filters/filter';
2833
import { DefaultHandler } from './default';
2934

@@ -52,25 +57,50 @@ class DiscussionHandler extends DefaultHandler {
5257
return null;
5358
}
5459

60+
// Unwrap discussion comments from fragment-masked types
61+
const discussionComments =
62+
getFragmentData(
63+
DiscussionCommentFieldsFragmentDoc,
64+
discussion.comments.nodes,
65+
) || [];
66+
5567
const latestDiscussionComment = getClosestDiscussionCommentOrReply(
5668
notification,
57-
discussion.comments.nodes as DiscussionCommentFieldsFragment[],
69+
discussionComments,
5870
);
5971

60-
const discussionAuthor = discussion.author as AuthorFieldsFragment;
72+
// Unwrap author from fragment-masked type
73+
const discussionAuthor = getFragmentData(
74+
AuthorFieldsFragmentDoc,
75+
discussion.author,
76+
);
77+
78+
let discussionUser: SubjectUser;
6179

62-
let discussionUser: SubjectUser = {
63-
login: discussionAuthor.login,
64-
html_url: discussionAuthor.url,
65-
avatar_url: discussionAuthor.avatar_url,
66-
type: discussionAuthor.type,
67-
};
6880
if (latestDiscussionComment) {
81+
// Unwrap author from the latest comment
82+
const commentAuthor = getFragmentData(
83+
AuthorFieldsFragmentDoc,
84+
latestDiscussionComment.author,
85+
);
86+
87+
if (commentAuthor) {
88+
discussionUser = {
89+
login: commentAuthor.login,
90+
html_url: commentAuthor.url,
91+
avatar_url: commentAuthor.avatar_url,
92+
type: commentAuthor.type,
93+
};
94+
}
95+
}
96+
97+
// Fall back to discussion author if no comment author found
98+
if (!discussionUser && discussionAuthor) {
6999
discussionUser = {
70-
login: latestDiscussionComment.author.login,
71-
html_url: latestDiscussionComment.author.url,
72-
avatar_url: latestDiscussionComment.author.avatar_url,
73-
type: latestDiscussionComment.author.type,
100+
login: discussionAuthor.login,
101+
html_url: discussionAuthor.url,
102+
avatar_url: discussionAuthor.avatar_url,
103+
type: discussionAuthor.type,
74104
};
75105
}
76106

@@ -102,17 +132,25 @@ export const discussionHandler = new DiscussionHandler();
102132
export function getClosestDiscussionCommentOrReply(
103133
notification: Notification,
104134
comments: DiscussionCommentFieldsFragment[],
105-
): DiscussionCommentFieldsFragment | null {
135+
): CommentFieldsFragment | null {
106136
if (!comments || comments.length === 0) {
107137
return null;
108138
}
109139

110140
const targetTimestamp = notification.updated_at;
111141

112-
const allCommentsAndReplies = comments.flatMap((comment) => [
113-
comment,
114-
...comment.replies.nodes,
115-
]);
142+
// Unwrap all comments and their replies from fragment-masked types
143+
const allCommentsAndReplies: CommentFieldsFragment[] = comments.flatMap(
144+
(comment) => {
145+
const unwrappedComment = getFragmentData(
146+
CommentFieldsFragmentDoc,
147+
comment,
148+
);
149+
const unwrappedReplies =
150+
getFragmentData(CommentFieldsFragmentDoc, comment.replies.nodes) || [];
151+
return [unwrappedComment, ...unwrappedReplies];
152+
},
153+
);
116154

117155
// Find the closest match using the target timestamp
118156
const closestComment = allCommentsAndReplies.reduce(

0 commit comments

Comments
 (0)