Skip to content

Commit df87f4d

Browse files
Display linked issues in PR overview sidebar
Adds a "Linked Issues" section to the PR Overview sidebar that lists the issues a PR closes, using the GraphQL `closingIssuesReferences` field. The section is read-only since GitHub's API does not expose a mutation for linking/unlinking issues. Squashed and rebased onto current main; drops the now-redundant `parsePullRequestState` helper since main has independently introduced `stateToStateEnum` for the same dedup, and replaces it with an inline OPEN/CLOSED mapping in `parseClosingIssuesReferences`. Co-Authored-By: Alex Ross <38270282+alexr00@users.noreply.github.com>
1 parent c972550 commit df87f4d

9 files changed

Lines changed: 92 additions & 4 deletions

File tree

src/github/graphql.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,14 @@ export interface PullRequest extends Issue {
768768
suggestedReviewers: SuggestedReviewerResponse[];
769769
additions?: number;
770770
deletions?: number;
771+
closingIssuesReferences?: {
772+
nodes: {
773+
id: number,
774+
title: string,
775+
number: number,
776+
state: 'CLOSED' | 'OPEN'
777+
}[];
778+
};
771779
}
772780

773781
export enum DefaultCommitTitle {

src/github/interface.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,13 @@ export interface Issue {
223223
reactions: Reaction[];
224224
}
225225

226+
export interface IssueReference {
227+
id: number;
228+
number: number;
229+
title: string;
230+
state: GithubItemStateEnum;
231+
}
232+
226233
export interface PullRequest extends Issue {
227234
isDraft?: boolean;
228235
isRemoteHeadDeleted?: boolean;
@@ -242,6 +249,7 @@ export interface PullRequest extends Issue {
242249
mergeCommitMeta?: { title: string, description: string };
243250
squashCommitMeta?: { title: string, description: string };
244251
suggestedReviewers?: ISuggestedReviewer[];
252+
closingIssues?: IssueReference[]
245253
hasComments?: boolean;
246254
additions?: number;
247255
deletions?: number;

src/github/pullRequestModel.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
IGitTreeItem,
5555
IRawFileChange,
5656
IRawFileContent,
57+
IssueReference,
5758
ISuggestedReviewer,
5859
ITeam,
5960
MergeMethod,
@@ -137,6 +138,7 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
137138
public conflicts?: string[];
138139
public suggestedReviewers?: ISuggestedReviewer[];
139140
public hasChangesSinceLastReview?: boolean;
141+
public closingIssues: IssueReference[];
140142
private _showChangesSinceReview: boolean;
141143
private _hasPendingReview: boolean = false;
142144
private _onDidChangePendingReviewState: vscode.EventEmitter<boolean> = this._register(new vscode.EventEmitter<boolean>());
@@ -265,7 +267,7 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
265267
}
266268

267269
this.suggestedReviewers = item.suggestedReviewers;
268-
270+
this.closingIssues = item.closingIssues ?? [];
269271
if (item.isRemoteHeadDeleted != null) {
270272
this.isRemoteHeadDeleted = item.isRemoteHeadDeleted;
271273
}

src/github/pullRequestOverview.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
458458
currentUserReviewState: reviewState,
459459
revertable: pullRequest.state === GithubItemStateEnum.Merged,
460460
isCopilotOnMyBehalf: await isCopilotOnMyBehalf(pullRequest, currentUser, coAuthors),
461-
generateDescriptionTitle: this.getGenerateDescriptionTitle()
461+
generateDescriptionTitle: this.getGenerateDescriptionTitle(),
462+
closingIssues: pullRequest.closingIssues,
462463
};
463464
this._postMessage({
464465
command: 'pr.initialize',

src/github/queries.gql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ fragment PullRequestFragment on PullRequest {
224224
mergeCommitMessage
225225
mergeCommitTitle
226226
}
227+
closingIssuesReferences(first: 50) {
228+
nodes {
229+
id
230+
number
231+
title
232+
state
233+
}
234+
}
227235
merged
228236
mergeable
229237
mergeQueueEntry {

src/github/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,7 @@ export async function parseGraphQLPullRequest(
924924
commentCount: graphQLPullRequest.comments.totalCount,
925925
additions: graphQLPullRequest.additions,
926926
deletions: graphQLPullRequest.deletions,
927+
closingIssues: parseClosingIssuesReferences(graphQLPullRequest.closingIssuesReferences?.nodes),
927928
};
928929
pr.mergeCommitMeta = parseCommitMeta(graphQLPullRequest.baseRepository.mergeCommitTitle, graphQLPullRequest.baseRepository.mergeCommitMessage, pr);
929930
pr.squashCommitMeta = parseCommitMeta(graphQLPullRequest.baseRepository.squashMergeCommitTitle, graphQLPullRequest.baseRepository.squashMergeCommitMessage, pr);
@@ -1068,6 +1069,21 @@ function parseSuggestedReviewers(
10681069
return ret.sort(loginComparator);
10691070
}
10701071

1072+
function parseClosingIssuesReferences(
1073+
closingIssuesReferences: Array<{ id: number, number: number, title: string, state: string }> | undefined
1074+
): Array<{ id: number, number: number, title: string, state: GithubItemStateEnum }> {
1075+
if (!closingIssuesReferences) {
1076+
return [];
1077+
}
1078+
1079+
return closingIssuesReferences.map(issue => ({
1080+
id: issue.id,
1081+
number: issue.number,
1082+
title: issue.title,
1083+
state: issue.state === 'OPEN' ? GithubItemStateEnum.Open : GithubItemStateEnum.Closed,
1084+
}));
1085+
}
1086+
10711087
/**
10721088
* Used for case insensitive sort by login
10731089
*/

src/github/views.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export interface PullRequest extends Issue {
112112
busy?: boolean;
113113
loadingCommit?: string;
114114
generateDescriptionTitle?: string;
115+
closingIssues: Pick<Issue, 'title' | 'number' | 'state'>[];
115116
}
116117

117118
export interface ProjectItemsReply {

webviews/components/sidebar.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { closeIcon, copilotIcon, settingsIcon } from './icon';
88
import { Reviewer } from './reviewer';
99
import { COPILOT_LOGINS } from '../../src/common/copilot';
1010
import { gitHubLabelColor } from '../../src/common/utils';
11-
import { IAccount, IMilestone, IProjectItem, isITeam, reviewerId, reviewerLabel, ReviewState } from '../../src/github/interface';
11+
import { IAccount, IMilestone, IProjectItem, Issue, isITeam, reviewerId, reviewerLabel, ReviewState } from '../../src/github/interface';
1212
import { ChangeReviewersReply, PullRequest } from '../../src/github/views';
1313
import PullRequestContext from '../common/context';
1414
import { Label } from '../common/label';
@@ -53,7 +53,7 @@ function Section({
5353
);
5454
}
5555

56-
export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot, canRequestCopilotReview }: PullRequest) {
56+
export default function Sidebar({ reviewers, labels, closingIssues, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot, canRequestCopilotReview }: PullRequest) {
5757
const {
5858
addReviewers,
5959
addReviewerCopilot,
@@ -268,6 +268,26 @@ export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue
268268
<div className="section-placeholder">No milestone</div>
269269
)}
270270
</Section>
271+
272+
<Section
273+
id="closingIssues"
274+
title="Linked Issues"
275+
hasWritePermission={false}
276+
>
277+
{closingIssues.length > 0 ? (
278+
<div className="p-2">
279+
{closingIssues.map(issue => (
280+
<div className="section-item reviewer">
281+
<div className="avatar-with-author gap-2">
282+
<IssueItem key={issue.title} issue={issue} />
283+
</div>
284+
</div>
285+
))}
286+
</div>
287+
) : (
288+
<div className="p-4 text-sm text-gray-500 text-center">None yet</div>
289+
)}
290+
</Section>
271291
</div>
272292
);
273293
}
@@ -577,3 +597,26 @@ function ConvertToDraft() {
577597
</div>
578598
);
579599
}
600+
601+
function IssueItem({ issue }: { issue: Pick<Issue, 'title' | 'number' | 'state'> }) {
602+
return (
603+
<>
604+
<IssueStateIcon state={issue.state} />
605+
<span className="h2">{issue.title}</span>
606+
</>
607+
);
608+
}
609+
610+
function IssueStateIcon({ state }: { state: string }) {
611+
const normalizedState = state.toLowerCase().trim();
612+
613+
switch (normalizedState) {
614+
case 'open':
615+
return settingsIcon;
616+
case 'closed':
617+
return closeIcon;
618+
default:
619+
return closeIcon;
620+
}
621+
}
622+

webviews/editorWebview/test/builder/pullRequest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const PullRequestBuilder = createBuilderClass<PullRequest>()({
6161
hasReviewDraft: { default: false },
6262
busy: { default: undefined },
6363
lastReviewType: { default: undefined },
64+
closingIssues: { default: [] },
6465
canAssignCopilot: { default: false },
6566
canRequestCopilotReview: { default: false },
6667
isCopilotOnMyBehalf: { default: false },

0 commit comments

Comments
 (0)