diff --git a/src/renderer/components/metrics/MetricGroup.test.tsx b/src/renderer/components/metrics/MetricGroup.test.tsx index 030c171c4..7c738570e 100644 --- a/src/renderer/components/metrics/MetricGroup.test.tsx +++ b/src/renderer/components/metrics/MetricGroup.test.tsx @@ -1,7 +1,10 @@ import { renderWithAppContext } from '../../__helpers__/test-utils'; import { mockSettings } from '../../__mocks__/state-mocks'; -import type { Milestone } from '../../typesGitHub'; import { mockSingleNotification } from '../../utils/api/__mocks__/response-mocks'; +import { + type MilestoneFieldsFragment, + MilestoneState, +} from '../../utils/api/graphql/generated/graphql'; import { MetricGroup } from './MetricGroup'; describe('renderer/components/metrics/MetricGroup.tsx', () => { @@ -103,8 +106,8 @@ describe('renderer/components/metrics/MetricGroup.tsx', () => { const mockNotification = mockSingleNotification; mockNotification.subject.milestone = { title: 'Milestone 1', - state: 'open', - } as Milestone; + state: MilestoneState.Open, + } as MilestoneFieldsFragment; const props = { notification: mockNotification, @@ -118,8 +121,8 @@ describe('renderer/components/metrics/MetricGroup.tsx', () => { const mockNotification = mockSingleNotification; mockNotification.subject.milestone = { title: 'Milestone 1', - state: 'closed', - } as Milestone; + state: MilestoneState.Closed, + } as MilestoneFieldsFragment; const props = { notification: mockNotification, diff --git a/src/renderer/components/metrics/MetricGroup.tsx b/src/renderer/components/metrics/MetricGroup.tsx index cdb1bd3d4..6afc50d7a 100644 --- a/src/renderer/components/metrics/MetricGroup.tsx +++ b/src/renderer/components/metrics/MetricGroup.tsx @@ -10,6 +10,7 @@ import { import { AppContext } from '../../context/App'; import { IconColor } from '../../types'; import type { Notification } from '../../typesGitHub'; +import { MilestoneState } from '../../utils/api/graphql/generated/graphql'; import { getPullRequestReviewIcon } from '../../utils/icons'; import { MetricPill } from './MetricPill'; @@ -84,7 +85,7 @@ export const MetricGroup: FC = ({ {notification.subject.milestone && ( { - return apiRequestAuth(url, 'GET', token); -} - -/** - * Get comments on issues and pull requests. - * Every pull request is an issue, but not every issue is a pull request. - * - * Endpoint documentation: https://docs.github.com/en/rest/issues/comments#get-an-issue-comment - */ -export function getIssueOrPullRequestComment( - url: Link, - token: Token, -): AxiosPromise { - return apiRequestAuth(url, 'GET', token); -} - -/** - * Get details of a pull request. - * - * Endpoint documentation: https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request - */ -export function getPullRequest( - url: Link, - token: Token, -): AxiosPromise { - return apiRequestAuth(url, 'GET', token); -} - -/** - * Lists all reviews for a specified pull request. The list of reviews returns in chronological order. - * - * Endpoint documentation: https://docs.github.com/en/rest/pulls/reviews#list-reviews-for-a-pull-request - */ -export function getPullRequestReviews( - url: Link, - token: Token, -): AxiosPromise { - return apiRequestAuth(url, 'GET', token); -} - /** * Gets a public release with the specified release ID. * @@ -234,10 +188,51 @@ export async function getHtmlUrl(url: Link, token: Token): Promise { } /** - * Search for Discussions that match notification title and repository. - * - * Returns the latest discussion and their latest comments / replies - * + * Fetch GitHub Issue by Issue Number. + */ +export async function fetchIssueByNumber( + notification: Notification, +): Promise> { + const url = getGitHubGraphQLUrl(notification.account.hostname); + const number = getNumberFromUrl(notification.subject.url); + + return performGraphQLRequest( + url.toString() as Link, + notification.account.token, + FetchIssueByNumberDocument, + { + owner: notification.repository.owner.login, + name: notification.repository.name, + number: number, + firstLabels: 100, + }, + ); +} + +/** + * Fetch GitHub Pull Request by PR Number. + */ +export async function fetchPullByNumber( + notification: Notification, +): Promise> { + const url = getGitHubGraphQLUrl(notification.account.hostname); + const number = getNumberFromUrl(notification.subject.url); + + return performGraphQLRequest( + url.toString() as Link, + notification.account.token, + FetchPullByNumberDocument, + { + owner: notification.repository.owner.login, + name: notification.repository.name, + number: number, + firstLabels: 100, + }, + ); +} + +/** + * Fetch GitHub Discussion by Discussion Number. */ export async function fetchDiscussionByNumber( notification: Notification, diff --git a/src/renderer/utils/api/graphql/common.graphql b/src/renderer/utils/api/graphql/common.graphql new file mode 100644 index 000000000..afb3c4151 --- /dev/null +++ b/src/renderer/utils/api/graphql/common.graphql @@ -0,0 +1,6 @@ +fragment AuthorFields on Actor { + login + html_url: url + avatar_url: avatarUrl + type: __typename +} diff --git a/src/renderer/utils/api/graphql/discussions.graphql b/src/renderer/utils/api/graphql/discussion.graphql similarity index 90% rename from src/renderer/utils/api/graphql/discussions.graphql rename to src/renderer/utils/api/graphql/discussion.graphql index 92c3c7f06..1095fc98a 100644 --- a/src/renderer/utils/api/graphql/discussions.graphql +++ b/src/renderer/utils/api/graphql/discussion.graphql @@ -38,17 +38,11 @@ query FetchDiscussionByNumber( } } -fragment AuthorFields on Actor { - login - url - avatar_url: avatarUrl - type: __typename -} - fragment CommentFields on DiscussionComment { databaseId createdAt author { ...AuthorFields } + url } diff --git a/src/renderer/utils/api/graphql/generated/gql.ts b/src/renderer/utils/api/graphql/generated/gql.ts index 1a24e20d9..42467e918 100644 --- a/src/renderer/utils/api/graphql/generated/gql.ts +++ b/src/renderer/utils/api/graphql/generated/gql.ts @@ -15,16 +15,34 @@ import * as types from './graphql'; * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment AuthorFields on Actor {\n login\n url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}": typeof types.FetchDiscussionByNumberDocument, + "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": typeof types.AuthorFieldsFragmentDoc, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": typeof types.FetchDiscussionByNumberDocument, + "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchIssueByNumberDocument, + "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullByNumberDocument, }; const documents: Documents = { - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment AuthorFields on Actor {\n login\n url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}": types.FetchDiscussionByNumberDocument, + "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": types.AuthorFieldsFragmentDoc, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": types.FetchDiscussionByNumberDocument, + "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchIssueByNumberDocument, + "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullByNumberDocument, }; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment AuthorFields on Actor {\n login\n url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; +export function graphql(source: "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}"): typeof import('./graphql').AuthorFieldsFragmentDoc; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchIssueByNumberDocument; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchPullByNumberDocument; export function graphql(source: string) { diff --git a/src/renderer/utils/api/graphql/generated/graphql.ts b/src/renderer/utils/api/graphql/generated/graphql.ts index 4ac16af25..e65b3a522 100644 --- a/src/renderer/utils/api/graphql/generated/graphql.ts +++ b/src/renderer/utils/api/graphql/generated/graphql.ts @@ -36168,6 +36168,24 @@ export type WorkflowsParametersInput = { export type _Entity = Issue; +type AuthorFields_Bot_Fragment = { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' }; + +type AuthorFields_EnterpriseUserAccount_Fragment = { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' }; + +type AuthorFields_Mannequin_Fragment = { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' }; + +type AuthorFields_Organization_Fragment = { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' }; + +type AuthorFields_User_Fragment = { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' }; + +export type AuthorFieldsFragment = + | AuthorFields_Bot_Fragment + | AuthorFields_EnterpriseUserAccount_Fragment + | AuthorFields_Mannequin_Fragment + | AuthorFields_Organization_Fragment + | AuthorFields_User_Fragment +; + export type FetchDiscussionByNumberQueryVariables = Exact<{ owner: Scalars['String']['input']; name: Scalars['String']['input']; @@ -36180,50 +36198,84 @@ export type FetchDiscussionByNumberQueryVariables = Exact<{ export type FetchDiscussionByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', discussion?: { __typename: 'Discussion', number: number, title: string, stateReason?: DiscussionStateReason | null, isAnswered?: boolean | null, url: any, author?: - | { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' } - | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, replies: { __typename?: 'DiscussionCommentConnection', nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, author?: - | { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } | null } | null> | null }, author?: - | { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; -type AuthorFields_Bot_Fragment = { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' }; +export type CommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null }; -type AuthorFields_EnterpriseUserAccount_Fragment = { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' }; +export type FetchIssueByNumberQueryVariables = Exact<{ + owner: Scalars['String']['input']; + name: Scalars['String']['input']; + number: Scalars['Int']['input']; + firstLabels?: InputMaybe; +}>; -type AuthorFields_Mannequin_Fragment = { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' }; -type AuthorFields_Organization_Fragment = { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' }; +export type FetchIssueByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', issue?: { __typename: 'Issue', number: number, title: string, url: any, state: IssueState, stateReason?: IssueStateReason | null, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; -type AuthorFields_User_Fragment = { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' }; +export type MilestoneFieldsFragment = { __typename?: 'Milestone', state: MilestoneState, title: string }; -export type AuthorFieldsFragment = - | AuthorFields_Bot_Fragment - | AuthorFields_EnterpriseUserAccount_Fragment - | AuthorFields_Mannequin_Fragment - | AuthorFields_Organization_Fragment - | AuthorFields_User_Fragment -; +export type FetchPullByNumberQueryVariables = Exact<{ + owner: Scalars['String']['input']; + name: Scalars['String']['input']; + number: Scalars['Int']['input']; + firstLabels?: InputMaybe; +}>; -export type CommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, author?: - | { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' } - | null }; + +export type FetchPullByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', pullRequest?: { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', url: any, createdAt: any, author?: + | { __typename?: 'Bot', login: string } + | { __typename?: 'EnterpriseUserAccount', login: string } + | { __typename?: 'Mannequin', login: string } + | { __typename?: 'Organization', login: string } + | { __typename?: 'User', login: string } + | null } | null> | null } | null, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null, closingIssuesReferences?: { __typename?: 'IssueConnection', nodes?: Array<{ __typename?: 'Issue', number: number } | null> | null } | null } | null } | null }; export class TypedDocumentString extends String @@ -36246,7 +36298,7 @@ export class TypedDocumentString export const AuthorFieldsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login - url + html_url: url avatar_url: avatarUrl type: __typename } @@ -36258,13 +36310,20 @@ export const CommentFieldsFragmentDoc = new TypedDocumentString(` author { ...AuthorFields } + url } fragment AuthorFields on Actor { login - url + html_url: url avatar_url: avatarUrl type: __typename }`, {"fragmentName":"CommentFields"}) as unknown as TypedDocumentString; +export const MilestoneFieldsFragmentDoc = new TypedDocumentString(` + fragment MilestoneFields on Milestone { + state + title +} + `, {"fragmentName":"MilestoneFields"}) as unknown as TypedDocumentString; export const FetchDiscussionByNumberDocument = new TypedDocumentString(` query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) { repository(owner: $owner, name: $name) { @@ -36299,7 +36358,7 @@ export const FetchDiscussionByNumberDocument = new TypedDocumentString(` } fragment AuthorFields on Actor { login - url + html_url: url avatar_url: avatarUrl type: __typename } @@ -36309,4 +36368,108 @@ fragment CommentFields on DiscussionComment { author { ...AuthorFields } -}`) as unknown as TypedDocumentString; \ No newline at end of file + url +}`) as unknown as TypedDocumentString; +export const FetchIssueByNumberDocument = new TypedDocumentString(` + query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) { + repository(owner: $owner, name: $name) { + issue(number: $number) { + __typename + number + title + url + state + stateReason + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: 1) { + totalCount + nodes { + url + author { + ...AuthorFields + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + } + } +} + fragment AuthorFields on Actor { + login + html_url: url + avatar_url: avatarUrl + type: __typename +} +fragment MilestoneFields on Milestone { + state + title +}`) as unknown as TypedDocumentString; +export const FetchPullByNumberDocument = new TypedDocumentString(` + query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + __typename + number + title + url + state + merged + isDraft + isInMergeQueue + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: 1) { + totalCount + nodes { + url + author { + ...AuthorFields + } + } + } + reviews(last: 1) { + totalCount + nodes { + url + createdAt + author { + login + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + closingIssuesReferences(first: 50) { + nodes { + number + } + } + } + } +} + fragment AuthorFields on Actor { + login + html_url: url + avatar_url: avatarUrl + type: __typename +} +fragment MilestoneFields on Milestone { + state + title +}`) as unknown as TypedDocumentString; \ No newline at end of file diff --git a/src/renderer/utils/api/graphql/issue.graphql b/src/renderer/utils/api/graphql/issue.graphql new file mode 100644 index 000000000..f042b0c4e --- /dev/null +++ b/src/renderer/utils/api/graphql/issue.graphql @@ -0,0 +1,42 @@ +query FetchIssueByNumber( + $owner: String! + $name: String! + $number: Int! + $firstLabels: Int +) { + repository(owner: $owner, name: $name) { + issue(number: $number) { + __typename + number + title + url + state + stateReason + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: 1) { + totalCount + nodes { + url + author { + ...AuthorFields + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + } + } +} + +fragment MilestoneFields on Milestone { + state + title +} diff --git a/src/renderer/utils/api/graphql/pull.graphql b/src/renderer/utils/api/graphql/pull.graphql new file mode 100644 index 000000000..e42c67980 --- /dev/null +++ b/src/renderer/utils/api/graphql/pull.graphql @@ -0,0 +1,59 @@ +query FetchPullByNumber( + $owner: String! + $name: String! + $number: Int! + $firstLabels: Int +) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + __typename + number + title + url + state + merged + isDraft + isInMergeQueue + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: 1) { + totalCount + nodes { + url + author { + ...AuthorFields + } + } + } + reviews(last: 1) { + totalCount + nodes { + url + createdAt + author { + login + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + closingIssuesReferences(first: 50) { + nodes { + number + } + } + } + } +} + +fragment MilestoneFields on Milestone { + state + title +} diff --git a/src/renderer/utils/helpers.ts b/src/renderer/utils/helpers.ts index 5a8bf5803..be990d817 100644 --- a/src/renderer/utils/helpers.ts +++ b/src/renderer/utils/helpers.ts @@ -7,12 +7,9 @@ import { import { Constants } from '../constants'; import type { Chevron, Hostname, Link } from '../types'; import type { Notification } from '../typesGitHub'; -import { fetchDiscussionByNumber, getHtmlUrl } from './api/client'; +import { getHtmlUrl } from './api/client'; import type { PlatformType } from './auth/types'; import { rendererLogError } from './logger'; -import { getCheckSuiteAttributes } from './notifications/handlers/checkSuite'; -import { getClosestDiscussionCommentOrReply } from './notifications/handlers/discussion'; -import { getWorkflowRunAttributes } from './notifications/handlers/workflowRun'; export function getPlatformFromHostname(hostname: string): PlatformType { return hostname.endsWith(Constants.DEFAULT_AUTH_OPTIONS.hostname) @@ -31,40 +28,6 @@ export function generateNotificationReferrerId( return btoa(raw); } -export function getCheckSuiteUrl(notification: Notification): Link { - const filters = []; - - const checkSuiteAttributes = getCheckSuiteAttributes(notification); - - if (checkSuiteAttributes?.workflowName) { - filters.push( - `workflow:"${checkSuiteAttributes.workflowName.replaceAll(' ', '+')}"`, - ); - } - - if (checkSuiteAttributes?.status) { - filters.push(`is:${checkSuiteAttributes.status}`); - } - - if (checkSuiteAttributes?.branchName) { - filters.push(`branch:${checkSuiteAttributes.branchName}`); - } - - return actionsURL(notification.repository.html_url, filters); -} - -export function getWorkflowRunUrl(notification: Notification): Link { - const filters = []; - - const workflowRunAttributes = getWorkflowRunAttributes(notification); - - if (workflowRunAttributes?.status) { - filters.push(`is:${workflowRunAttributes.status}`); - } - - return actionsURL(notification.repository.html_url, filters); -} - /** * Construct a GitHub Actions URL for a repository with optional filters. */ @@ -80,56 +43,22 @@ export function actionsURL(repositoryURL: string, filters: string[]): Link { return url.toString().replaceAll('%2B', '+') as Link; } -async function getDiscussionUrl(notification: Notification): Promise { - const url = new URL(notification.repository.html_url); - url.pathname += '/discussions'; - - const response = await fetchDiscussionByNumber(notification); - const discussion = response.data.repository.discussion; - - if (discussion) { - url.href = discussion.url; - - const closestComment = getClosestDiscussionCommentOrReply( - notification, - discussion.comments.nodes, - ); - if (closestComment) { - url.hash = `#discussioncomment-${closestComment.databaseId}`; - } - } - - return url.toString() as Link; -} - export async function generateGitHubWebUrl( notification: Notification, ): Promise { const url = new URL(getDefaultURLForType(notification)); try { - switch (notification.subject.type) { - case 'CheckSuite': - url.href = getCheckSuiteUrl(notification); - break; - case 'Discussion': - url.href = await getDiscussionUrl(notification); - break; - case 'WorkflowRun': - url.href = getWorkflowRunUrl(notification); - break; - default: - if (notification.subject.latest_comment_url) { - url.href = await getHtmlUrl( - notification.subject.latest_comment_url, - notification.account.token, - ); - } else if (notification.subject.url) { - url.href = await getHtmlUrl( - notification.subject.url, - notification.account.token, - ); - } + if (notification.subject.latest_comment_url) { + url.href = await getHtmlUrl( + notification.subject.latest_comment_url, + notification.account.token, + ); + } else if (notification.subject.url) { + url.href = await getHtmlUrl( + notification.subject.url, + notification.account.token, + ); } } catch (err) { rendererLogError( @@ -152,6 +81,9 @@ export function getDefaultURLForType(notification: Notification) { const url = new URL(notification.repository.html_url); switch (notification.subject.type) { + case 'CheckSuite': + url.pathname += '/actions'; + break; case 'Discussion': url.pathname += '/discussions'; break; @@ -161,12 +93,18 @@ export function getDefaultURLForType(notification: Notification) { case 'PullRequest': url.pathname += '/pulls'; break; + case 'Release': + url.pathname += '/releases'; + break; case 'RepositoryInvitation': url.pathname += '/invitations'; break; case 'RepositoryDependabotAlertsThread': url.pathname += '/security/dependabot'; break; + case 'WorkflowRun': + url.pathname += '/actions'; + break; default: break; } diff --git a/src/renderer/utils/links.ts b/src/renderer/utils/links.ts index 88ce44109..d5d0a43fc 100644 --- a/src/renderer/utils/links.ts +++ b/src/renderer/utils/links.ts @@ -5,7 +5,7 @@ import type { Account, Hostname, Link } from '../types'; import type { Notification, Repository, SubjectUser } from '../typesGitHub'; import { getDeveloperSettingsURL } from './auth/utils'; import { openExternalLink } from './comms'; -import { generateGitHubWebUrl } from './helpers'; +import { generateNotificationReferrerId } from './helpers'; export function openGitifyReleaseNotes(version: string) { openExternalLink( @@ -55,8 +55,14 @@ export function openRepository(repository: Repository) { } export async function openNotification(notification: Notification) { - const url = await generateGitHubWebUrl(notification); - openExternalLink(url); + const url = new URL(notification.subject.htmlUrl); + + url.searchParams.set( + 'notification_referrer_id', + generateNotificationReferrerId(notification), + ); + + openExternalLink(url.toString() as Link); } export function openGitHubParticipatingDocs() { diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index e4a6fcf36..0c285dbd9 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -9,7 +9,7 @@ import { XIcon, } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { CheckSuiteAttributes, CheckSuiteStatus, @@ -17,6 +17,7 @@ import type { Notification, Subject, } from '../../../typesGitHub'; +import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; class CheckSuiteHandler extends DefaultHandler { @@ -32,6 +33,7 @@ class CheckSuiteHandler extends DefaultHandler { return { state: state, user: null, + htmlUrl: getCheckSuiteUrl(notification), }; } @@ -99,3 +101,25 @@ function getCheckSuiteStatus(statusDisplayName: string): CheckSuiteStatus { return null; } } + +export function getCheckSuiteUrl(notification: Notification): Link { + const filters = []; + + const checkSuiteAttributes = getCheckSuiteAttributes(notification); + + if (checkSuiteAttributes?.workflowName) { + filters.push( + `workflow:"${checkSuiteAttributes.workflowName.replaceAll(' ', '+')}"`, + ); + } + + if (checkSuiteAttributes?.status) { + filters.push(`is:${checkSuiteAttributes.status}`); + } + + if (checkSuiteAttributes?.branchName) { + filters.push(`branch:${checkSuiteAttributes.branchName}`); + } + + return actionsURL(notification.repository.html_url, filters); +} diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index 99f66b474..6624f7543 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -11,6 +11,11 @@ import type { Subject, SubjectType, } from '../../../typesGitHub'; +import { + IssueState, + IssueStateReason, + PullRequestState, +} from '../../api/graphql/generated/graphql'; import type { NotificationTypeHandler } from './types'; import { formatForDisplay } from './utils'; @@ -34,13 +39,20 @@ export class DefaultHandler implements NotificationTypeHandler { case 'reopened': case 'ANSWERED': case 'success': + case IssueState.Open: + case IssueStateReason.Reopened: + case PullRequestState.Open: return IconColor.GREEN; case 'closed': case 'failure': + case IssueState.Closed: + case PullRequestState.Closed: return IconColor.RED; case 'completed': case 'RESOLVED': case 'merged': + case IssueStateReason.Completed: + case PullRequestState.Merged: return IconColor.PURPLE; default: return IconColor.GRAY; @@ -53,11 +65,13 @@ export class DefaultHandler implements NotificationTypeHandler { notification.subject.type, ]); } + formattedNotificationNumber(notification: Notification): string { return notification.subject?.number ? `#${notification.subject.number}` : ''; } + formattedNotificationTitle(notification: Notification): string { let title = notification.subject.title; diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 5f28fb321..1c6230f26 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -17,7 +17,7 @@ import { discussionHandler } from './discussion'; const mockDiscussionAuthor: AuthorFieldsFragment = { login: 'discussion-author', - url: 'https://github.com/discussion-author' as Link, + html_url: 'https://github.com/discussion-author' as Link, avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, type: 'User', }; @@ -70,7 +70,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -103,7 +103,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'DUPLICATE', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -133,7 +133,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'OPEN', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -166,7 +166,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'OUTDATED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -199,7 +199,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'REOPENED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -232,7 +232,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'RESOLVED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -272,7 +272,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index cf66a8de7..aa269876f 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -75,7 +75,7 @@ class DiscussionHandler extends DefaultHandler { let discussionUser: SubjectUser = { login: discussion.author.login, - html_url: discussion.author.url, + html_url: discussion.author.html_url, avatar_url: discussion.author.avatar_url, type: discussion.author.type, }; @@ -83,7 +83,7 @@ class DiscussionHandler extends DefaultHandler { if (latestDiscussionComment) { discussionUser = { login: latestDiscussionComment.author.login, - html_url: latestDiscussionComment.author.url, + html_url: latestDiscussionComment.author.html_url, avatar_url: latestDiscussionComment.author.avatar_url, type: latestDiscussionComment.author.type, }; @@ -95,6 +95,7 @@ class DiscussionHandler extends DefaultHandler { user: discussionUser, comments: discussion.comments.totalCount, labels: discussion.labels?.nodes.map((label) => label.name) ?? [], + htmlUrl: latestDiscussionComment.url ?? discussion.url, }; } diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index c351b7a5c..ce2eda76f 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -14,50 +14,38 @@ import type { GitifySubject, Notification, Subject, - User, } from '../../../typesGitHub'; -import { getIssue, getIssueOrPullRequestComment } from '../../api/client'; -import { isStateFilteredOut } from '../filters/filter'; +import { fetchIssueByNumber } from '../../api/client'; import { DefaultHandler } from './default'; -import { getSubjectUser } from './utils'; class IssueHandler extends DefaultHandler { readonly type = 'Issue'; async enrich( notification: Notification, - settings: SettingsState, + _settings: SettingsState, ): Promise { - const issue = ( - await getIssue(notification.subject.url, notification.account.token) - ).data; + const response = await fetchIssueByNumber(notification); + const issue = response.data.repository?.issue; - const issueState = issue.state_reason ?? issue.state; + // const issueState = issue.stateReason ?? issue.state; // Return early if this notification would be hidden by filters - if (isStateFilteredOut(issueState, settings)) { - return null; - } - - let issueCommentUser: User; + // if (isStateFilteredOut(issueState, settings)) { + // return null; + // } - if (notification.subject.latest_comment_url) { - const issueComment = ( - await getIssueOrPullRequestComment( - notification.subject.latest_comment_url, - notification.account.token, - ) - ).data; - issueCommentUser = issueComment.user; - } + // const issueCommentUser = issue.comments.nodes[0]?.author; return { number: issue.number, - state: issueState, - user: getSubjectUser([issueCommentUser, issue.user]), - comments: issue.comments, - labels: issue.labels?.map((label) => label.name) ?? [], + // state: issueState + state: null, + user: null, //getSubjectUser([issueCommentUser, issue.author]), + comments: issue.comments.totalCount, + labels: issue.labels.nodes?.map((label) => label.name) ?? [], milestone: issue.milestone, + htmlUrl: issue.comments.nodes[0]?.url ?? issue.url, }; } diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index c251d6fe2..9f5d8ce91 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -8,12 +8,8 @@ import { import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; import type { Link } from '../../../types'; -import type { Notification, PullRequest } from '../../../typesGitHub'; -import { - getLatestReviewForReviewers, - parseLinkedIssuesFromPr, - pullRequestHandler, -} from './pullRequest'; +import type { Notification } from '../../../typesGitHub'; +import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { let mockNotification: Notification; @@ -357,42 +353,6 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); }); - describe('Pull Request With Linked Issues', () => { - it('returns empty if no pr body', () => { - const mockPr = { - user: { - type: 'User', - }, - body: null, - } as PullRequest; - - const result = parseLinkedIssuesFromPr(mockPr); - expect(result).toEqual([]); - }); - - it('returns empty if pr from non-user', () => { - const mockPr = { - user: { - type: 'Bot', - }, - body: 'This PR is linked to #1, #2, and #3', - } as PullRequest; - const result = parseLinkedIssuesFromPr(mockPr); - expect(result).toEqual([]); - }); - - it('returns linked issues', () => { - const mockPr = { - user: { - type: 'User', - }, - body: 'This PR is linked to #1, #2, and #3', - } as PullRequest; - const result = parseLinkedIssuesFromPr(mockPr); - expect(result).toEqual(['#1', '#2', '#3']); - }); - }); - it('early return if pull request state filtered', async () => { nock('https://api.github.com') .get('/repos/gitify-app/notifications-test/pulls/1') diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index f58eccd98..b98b4699d 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -8,25 +8,20 @@ import { GitPullRequestIcon, } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; +import type { SettingsState } from '../../../types'; import type { GitifyPullRequestReview, GitifySubject, Notification, - PullRequest, PullRequestReview, PullRequestStateType, Subject, - User, } from '../../../typesGitHub'; -import { - getIssueOrPullRequestComment, - getPullRequest, - getPullRequestReviews, -} from '../../api/client'; +import { fetchPullByNumber } from '../../api/client'; +import type { PullRequestState } from '../../api/graphql/generated/graphql'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; -import { getSubjectUser } from './utils'; +import { getSubjectAuthor } from './utils'; class PullRequestHandler extends DefaultHandler { readonly type = 'PullRequest' as const; @@ -35,14 +30,11 @@ class PullRequestHandler extends DefaultHandler { notification: Notification, settings: SettingsState, ): Promise { - const pr = ( - await getPullRequest(notification.subject.url, notification.account.token) - ).data; - - let prState: PullRequestStateType = pr.state; - if (pr.merged) { - prState = 'merged'; - } else if (pr.draft) { + const response = await fetchPullByNumber(notification); + const pr = response.data.repository.pullRequest; + + let prState: PullRequestStateType | PullRequestState = pr.state; + if (pr.isDraft) { prState = 'draft'; } @@ -51,39 +43,29 @@ class PullRequestHandler extends DefaultHandler { return null; } - let prCommentUser: User; - if ( - notification.subject.latest_comment_url && - notification.subject.latest_comment_url !== notification.subject.url - ) { - const prComment = ( - await getIssueOrPullRequestComment( - notification.subject.latest_comment_url, - notification.account.token, - ) - ).data; - prCommentUser = prComment.user; - } + const prCommentUser = pr.comments.nodes[0]?.author; - const prUser = getSubjectUser([prCommentUser, pr.user]); + const prUser = getSubjectAuthor([prCommentUser, pr.author]); // Return early if this notification would be hidden by user filters if (isUserFilteredOut(prUser, settings)) { return null; } - const reviews = await getLatestReviewForReviewers(notification); - const linkedIssues = parseLinkedIssuesFromPr(pr); + const reviews = null; // await getLatestReviewForReviewers(notification); return { number: pr.number, state: prState, user: prUser, reviews: reviews, - comments: pr.comments, - labels: pr.labels?.map((label) => label.name) ?? [], - linkedIssues: linkedIssues, - milestone: pr.milestone, + comments: pr.comments.totalCount, + labels: pr.labels.nodes?.map((label) => label.name) ?? [], + linkedIssues: pr.closingIssuesReferences.nodes.map( + (issue) => `#${issue.number}`, + ), + milestone: null, //pr.milestone, + htmlUrl: pr.comments.nodes[0]?.url ?? pr.url, }; } @@ -110,10 +92,7 @@ export async function getLatestReviewForReviewers( return null; } - const prReviews = await getPullRequestReviews( - `${notification.subject.url}/reviews` as Link, - notification.account.token, - ); + const prReviews = null; if (!prReviews.data.length) { return null; @@ -154,22 +133,3 @@ export async function getLatestReviewForReviewers( return a.state.localeCompare(b.state); }); } - -export function parseLinkedIssuesFromPr(pr: PullRequest): string[] { - const linkedIssues: string[] = []; - - if (!pr.body || pr.user.type !== 'User') { - return linkedIssues; - } - - const regexPattern = /\s?#(\d+)\s?/gi; - const matches = pr.body.matchAll(regexPattern); - - for (const match of matches) { - if (match[0]) { - linkedIssues.push(match[0].trim()); - } - } - - return linkedIssues; -} diff --git a/src/renderer/utils/notifications/handlers/utils.ts b/src/renderer/utils/notifications/handlers/utils.ts index e8dc1b13b..e46b70b90 100644 --- a/src/renderer/utils/notifications/handlers/utils.ts +++ b/src/renderer/utils/notifications/handlers/utils.ts @@ -1,4 +1,5 @@ import type { SubjectUser, User } from '../../../typesGitHub'; +import type { AuthorFieldsFragment } from '../../api/graphql/generated/graphql'; /** * Construct the notification subject user based on an order prioritized list of users @@ -24,6 +25,30 @@ export function getSubjectUser(users: User[]): SubjectUser { return subjectUser; } +/** + * Construct the notification subject user based on an order prioritized list of users + * @param users array of users in order or priority + * @returns the subject user + */ +export function getSubjectAuthor(users: AuthorFieldsFragment[]): SubjectUser { + let subjectUser: SubjectUser = null; + + for (const user of users) { + if (user) { + subjectUser = { + login: user.login, + html_url: user.html_url, + avatar_url: user.avatar_url, + type: user.type, + }; + + return subjectUser; + } + } + + return subjectUser; +} + export function formatForDisplay(text: string[]): string { if (!text) { return ''; diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index 9dfd7df2e..3bfe40742 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -3,7 +3,7 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { RocketIcon } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { CheckSuiteStatus, GitifySubject, @@ -11,6 +11,7 @@ import type { Subject, WorkflowRunAttributes, } from '../../../typesGitHub'; +import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; class WorkflowRunHandler extends DefaultHandler { @@ -26,6 +27,7 @@ class WorkflowRunHandler extends DefaultHandler { return { state: state, user: null, + htmlUrl: getWorkflowRunUrl(notification), }; } @@ -72,3 +74,15 @@ function getWorkflowRunStatus(statusDisplayName: string): CheckSuiteStatus { return null; } } + +export function getWorkflowRunUrl(notification: Notification): Link { + const filters = []; + + const workflowRunAttributes = getWorkflowRunAttributes(notification); + + if (workflowRunAttributes?.status) { + filters.push(`is:${workflowRunAttributes.status}`); + } + + return actionsURL(notification.repository.html_url, filters); +}