diff --git a/src/github/graphql.ts b/src/github/graphql.ts index 8959383941..632083453b 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -632,6 +632,14 @@ export interface PullRequest extends Issue { viewerCanDisableAutoMerge: boolean; isDraft?: boolean; suggestedReviewers: SuggestedReviewerResponse[]; + closingIssuesReferences?: { + nodes: { + id: number, + title: string, + number: number, + state: 'CLOSED' | 'OPEN' + }[]; + }; } export enum DefaultCommitTitle { diff --git a/src/github/interface.ts b/src/github/interface.ts index 71e4f156be..d388af9be7 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -204,6 +204,13 @@ export interface Issue { reactionCount: number; } +export interface IssueReference { + id: number; + number: number; + title: string; + state: GithubItemStateEnum; +} + export interface PullRequest extends Issue { isDraft?: boolean; isRemoteHeadDeleted?: boolean; @@ -223,6 +230,7 @@ export interface PullRequest extends Issue { mergeCommitMeta?: { title: string, description: string }; squashCommitMeta?: { title: string, description: string }; suggestedReviewers?: ISuggestedReviewer[]; + closingIssues?: IssueReference[] hasComments?: boolean; } diff --git a/src/github/issueModel.ts b/src/github/issueModel.ts index 39e903e374..8a6a58d172 100644 --- a/src/github/issueModel.ts +++ b/src/github/issueModel.ts @@ -18,7 +18,7 @@ import { UpdateIssueResponse, } from './graphql'; import { GithubItemStateEnum, IAccount, IIssueEditData, IMilestone, IProject, IProjectItem, Issue } from './interface'; -import { parseGraphQlIssueComment, parseGraphQLTimelineEvents } from './utils'; +import { parseGraphQlIssueComment, parseGraphQLTimelineEvents, parsePullRequestState } from './utils'; export class IssueModel { static ID = 'IssueModel'; @@ -104,11 +104,7 @@ export class IssueModel { } protected updateState(state: string) { - if (state.toLowerCase() === 'open') { - this.state = GithubItemStateEnum.Open; - } else { - this.state = GithubItemStateEnum.Closed; - } + this.state = parsePullRequestState(state); } update(issue: TItem): void { diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index a11d43e7af..19cb2ae19b 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -57,6 +57,7 @@ import { IGitTreeItem, IRawFileChange, IRawFileContent, + IssueReference, ISuggestedReviewer, ITeam, MergeMethod, @@ -83,6 +84,7 @@ import { parseGraphQLTimelineEvents, parseMergeability, parseMergeQueueEntry, + parsePullRequestState, restPaginate, } from './utils'; @@ -121,6 +123,7 @@ export class PullRequestModel extends IssueModel implements IPullRe public conflicts?: string[]; public suggestedReviewers?: ISuggestedReviewer[]; public hasChangesSinceLastReview?: boolean; + public closingIssues: IssueReference[]; private _showChangesSinceReview: boolean; private _hasPendingReview: boolean = false; private _onDidChangePendingReviewState: vscode.EventEmitter = new vscode.EventEmitter(); @@ -235,20 +238,15 @@ export class PullRequestModel extends IssueModel implements IPullRe public base: GitHubRef; protected override updateState(state: string) { - if (state.toLowerCase() === 'open') { - this.state = GithubItemStateEnum.Open; - } else if (state.toLowerCase() === 'merged' || this.item.merged) { - this.state = GithubItemStateEnum.Merged; - } else { - this.state = GithubItemStateEnum.Closed; - } + const newState = parsePullRequestState(state); + this.state = this.item.merged ? GithubItemStateEnum.Merged : newState; } override update(item: PullRequest): void { super.update(item); this.isDraft = item.isDraft; this.suggestedReviewers = item.suggestedReviewers; - + this.closingIssues = item.closingIssues ?? []; if (item.isRemoteHeadDeleted != null) { this.isRemoteHeadDeleted = item.isRemoteHeadDeleted; } diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 62657bd527..961fbb6f75 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -283,7 +283,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel | undefined +): Array<{ id: number, number: number, title: string, state: GithubItemStateEnum }> { + if (!closingIssuesReferences) { + return []; + } + + return closingIssuesReferences.map(issue => ({ + id: issue.id, + number: issue.number, + title: issue.title, + state: parsePullRequestState(issue.state) + })); +} + /** * Used for case insensitive sort by login */ diff --git a/src/github/views.ts b/src/github/views.ts index 0f261208f2..70ebc0fab4 100644 --- a/src/github/views.ts +++ b/src/github/views.ts @@ -96,6 +96,7 @@ export interface PullRequest extends Issue { lastReviewType?: ReviewType; revertable?: boolean; busy?: boolean; + closingIssues: Pick[]; } export interface ProjectItemsReply { diff --git a/webviews/components/sidebar.tsx b/webviews/components/sidebar.tsx index eeb5d6cf24..fd0aa8420e 100644 --- a/webviews/components/sidebar.tsx +++ b/webviews/components/sidebar.tsx @@ -6,7 +6,7 @@ import React, { useContext } from 'react'; import { COPILOT_LOGINS } from '../../src/common/copilot'; import { gitHubLabelColor } from '../../src/common/utils'; -import { IMilestone, IProjectItem, reviewerId } from '../../src/github/interface'; +import { IMilestone, IProjectItem, Issue, reviewerId, } from '../../src/github/interface'; import { PullRequest } from '../../src/github/views'; import PullRequestContext from '../common/context'; import { Label } from '../common/label'; @@ -14,7 +14,7 @@ import { AuthorLink, Avatar } from '../components/user'; import { closeIcon, copilotIcon, settingsIcon } from './icon'; import { Reviewer } from './reviewer'; -export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot }: PullRequest) { +export default function Sidebar({ reviewers, labels, closingIssues, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot }: PullRequest) { const { addReviewers, addAssignees, @@ -54,7 +54,7 @@ export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue {reviewers && reviewers.length ? ( reviewers.map(state => ( - + )) ) : (
None yet
@@ -199,7 +199,27 @@ export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue {milestone ? ( ) : ( -
No milestone
+ <> +
No milestone
+ + )} + +
+
+
Linked Issues
+
+ {closingIssues.length > 0 ? ( +
+ {closingIssues.map(issue => ( +
+
+ +
+
+ ))} +
+ ) : ( +
None yet
)}
@@ -273,3 +293,27 @@ function Project(project: IProjectItem & { canDelete: boolean }) { ); } + +function IssueItem({ issue }: { issue: Pick }) { + return ( + <> + + {issue.title} + + + ); +} + +function IssueStateIcon({ state }: { state: string }) { + const normalizedState = state.toLowerCase().trim(); + + switch (normalizedState) { + case 'open': + return settingsIcon; + case 'closed': + return closeIcon; + default: + return closeIcon; + } +} + diff --git a/webviews/editorWebview/test/builder/pullRequest.ts b/webviews/editorWebview/test/builder/pullRequest.ts index af776ab9a2..9ba3703cf0 100644 --- a/webviews/editorWebview/test/builder/pullRequest.ts +++ b/webviews/editorWebview/test/builder/pullRequest.ts @@ -57,5 +57,6 @@ export const PullRequestBuilder = createBuilderClass()({ hasReviewDraft: { default: false }, busy: { default: undefined }, lastReviewType: { default: undefined }, + closingIssues: { default: [] }, canAssignCopilot: { default: false }, });