diff --git a/src/renderer/typesGitHub.ts b/src/renderer/typesGitHub.ts index a3c7f8e35..c78308096 100644 --- a/src/renderer/typesGitHub.ts +++ b/src/renderer/typesGitHub.ts @@ -50,10 +50,16 @@ export type UserType = | 'User'; /** - * Note: draft and merged are not official states in the GitHub API. - * These are derived from the pull request's `merged` and `draft` properties. + * Note: draft, merged and queued are not official states in the GitHub API. + * These are derived from the pull request's `merged` and `draft` properties, or + * by checking the PR merge queue state. */ -export type PullRequestStateType = 'closed' | 'draft' | 'merged' | 'open'; +export type PullRequestStateType = + | 'closed' + | 'draft' + | 'merged' + | 'open' + | 'queued'; export type StateType = | CheckSuiteStatus @@ -497,6 +503,19 @@ export interface GraphQLSearch { }; } +export interface GraphQLMergeQueue { + data: { + repository: { + pullRequest: { + mergeQueueEntry: { + state: string; + position: number; + } | null; + }; + }; + }; +} + export interface Discussion { number: number; title: string; diff --git a/src/renderer/utils/api/client.ts b/src/renderer/utils/api/client.ts index 0fdfe4920..6c104bfc8 100644 --- a/src/renderer/utils/api/client.ts +++ b/src/renderer/utils/api/client.ts @@ -12,6 +12,7 @@ import type { Commit, CommitComment, Discussion, + GraphQLMergeQueue, GraphQLSearch, Issue, IssueOrPullRequestComment, @@ -25,6 +26,7 @@ import type { import { isAnsweredDiscussionFeatureSupported } from '../features'; import { rendererLogError } from '../logger'; import { QUERY_SEARCH_DISCUSSIONS } from './graphql/discussions'; +import { QUERY_CHECK_MERGE_QUEUE_FOR_PR } from './graphql/pullRequests'; import { formatAsGitHubSearchSyntax } from './graphql/utils'; import { apiRequestAuth } from './request'; import { getGitHubAPIBaseUrl, getGitHubGraphQLUrl } from './utils'; @@ -285,3 +287,27 @@ export async function getLatestDiscussion( ); } } + +/** + * Check if PR is in merge queue. + */ +export async function queryMergeQueueForPr( + notification: Notification, + prNumber: number, +): AxiosPromise { + const url = getGitHubGraphQLUrl(notification.account.hostname); + + return apiRequestAuth( + url.toString() as Link, + 'POST', + notification.account.token, + { + query: print(QUERY_CHECK_MERGE_QUEUE_FOR_PR), + variables: { + owner: notification.repository.owner.login, + repository: notification.repository.name, + prNumber: prNumber, + }, + }, + ); +} diff --git a/src/renderer/utils/api/graphql/pullRequests.ts b/src/renderer/utils/api/graphql/pullRequests.ts new file mode 100644 index 000000000..d370ce911 --- /dev/null +++ b/src/renderer/utils/api/graphql/pullRequests.ts @@ -0,0 +1,18 @@ +import gql from 'graphql-tag'; + +export const QUERY_CHECK_MERGE_QUEUE_FOR_PR = gql` + query checkMergeQueueForPR( + $owner: String! + $repository: String! + $prNumber: Int! + ) { + repository(owner: $owner, name: $repository) { + pullRequest(number: $prNumber) { + mergeQueueEntry { + state + position + } + } + } + } +`; diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index 99f66b474..1c0054f03 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -42,6 +42,8 @@ export class DefaultHandler implements NotificationTypeHandler { case 'RESOLVED': case 'merged': return IconColor.PURPLE; + case 'queued': + return IconColor.YELLOW; default: return IconColor.GRAY; } diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index f58eccd98..0e89cae23 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -3,6 +3,7 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { GitMergeIcon, + GitMergeQueueIcon, GitPullRequestClosedIcon, GitPullRequestDraftIcon, GitPullRequestIcon, @@ -23,6 +24,7 @@ import { getIssueOrPullRequestComment, getPullRequest, getPullRequestReviews, + queryMergeQueueForPr, } from '../../api/client'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; @@ -44,6 +46,12 @@ class PullRequestHandler extends DefaultHandler { prState = 'merged'; } else if (pr.draft) { prState = 'draft'; + } else if (prState === 'open') { + const mergeQueue = await isPRInMergeQueue(notification, pr.number); + + if (mergeQueue) { + prState = 'queued'; + } } // Return early if this notification would be hidden by state filters @@ -95,6 +103,8 @@ class PullRequestHandler extends DefaultHandler { return GitPullRequestClosedIcon; case 'merged': return GitMergeIcon; + case 'queued': + return GitMergeQueueIcon; default: return GitPullRequestIcon; } @@ -173,3 +183,15 @@ export function parseLinkedIssuesFromPr(pr: PullRequest): string[] { return linkedIssues; } + +export async function isPRInMergeQueue( + notification: Notification, + prNumber: number, +): Promise { + const mergeQueueResponse = await queryMergeQueueForPr(notification, prNumber); + + const mergeQueueEntry = + mergeQueueResponse.data.data.repository.pullRequest.mergeQueueEntry; + + return mergeQueueEntry !== null; +}