Skip to content

Commit c9ecbbe

Browse files
mohamedamara1alexr00Copilot
authored
Display linked issue(s) from the PR Overview #5824 (#6835)
* 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> * Address review feedback for #6835 - Extract IssueReference type into views.ts and use it for closingIssues (alexr00 review feedback). - Sidebar: drop orphan tailwind-style class names (p-2, gap-2, p-4, text-sm, text-gray-500, text-center, h2) that are not defined in the project's CSS — replace with the project's section conventions. - Use the existing issueIcon / issueClosedIcon for the issue state icon instead of settingsIcon / closeIcon. - Switch on GithubItemStateEnum directly rather than lowercasing strings. - Move the React `key` to the iterating element and key by issue.number (titles are not guaranteed unique). - Render the issue number alongside the title. - Initialize PullRequestModel.closingIssues to [] so the field is never undefined when serialized to the webview. * Add issueIcon and issueClosedIcon exports The previous commit imported these from './icon' but they were removed from icon.tsx during the codicon refactor on main. Restore them as named exports — issueIcon uses the existing issue_webview.svg, issueClosedIcon uses the pass codicon (checkmark in circle). * Make linked issues clickable links to open in browser Adds the issue url to the GraphQL query and through the IssueReference type, then wraps the issue label in an <a href> in the sidebar. VS Code's webview intercepts the click and opens the URL via openExternal, matching the behavior of AuthorLink in the same view. * Color and align linked issue icons Wraps the issue state icon in a .section-icon span and adds two new state classes (.issue-open, .issue-closed) coloured with the existing --vscode-issues-open and --vscode-issues-closed tokens that this extension contributes. Matches the colouring convention already used for reviewer state icons (.section-icon.approved). The .section-icon container also provides flex centering and 3px padding, which fixes the icon-vs-link spacing in the row. * Address PR feedback Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Alex Ross <38270282+alexr00@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com>
1 parent 9c0c532 commit c9ecbbe

12 files changed

Lines changed: 142 additions & 7 deletions

File tree

src/github/graphql.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,15 @@ 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+
url: string,
778+
}[];
779+
};
771780
}
772781

773782
export enum DefaultCommitTitle {

src/github/interface.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ 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+
url: string;
232+
}
233+
226234
export interface PullRequest extends Issue {
227235
isDraft?: boolean;
228236
isRemoteHeadDeleted?: boolean;
@@ -242,6 +250,7 @@ export interface PullRequest extends Issue {
242250
mergeCommitMeta?: { title: string, description: string };
243251
squashCommitMeta?: { title: string, description: string };
244252
suggestedReviewers?: ISuggestedReviewer[];
253+
closingIssues?: IssueReference[]
245254
hasComments?: boolean;
246255
additions?: number;
247256
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: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { IssueOverviewPanel, panelKey } from './issueOverview';
2727
import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel';
2828
import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon';
2929
import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
30-
import { parseReviewers, processDiffLinks, processPermalinks } from './utils';
30+
import { ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, parseReviewers, processDiffLinks, processPermalinks } from './utils';
3131
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewAndMergeContext, ReadyForReviewContext, ReviewCommentContext, ReviewType, UnresolvedIdentity } from './views';
3232
import { debounce } from '../common/async';
3333
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
@@ -38,6 +38,7 @@ import Logger from '../common/logger';
3838
import { CHECKOUT_DEFAULT_BRANCH, CHECKOUT_PULL_REQUEST_BASE_BRANCH, DEFAULT_MERGE_METHOD, DELETE_BRANCH_AFTER_MERGE, POST_DONE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
3939
import { ITelemetry } from '../common/telemetry';
4040
import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../common/timelineEvent';
41+
import { toOpenIssueWebviewUri } from '../common/uri';
4142
import { asPromise, formatError } from '../common/utils';
4243
import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview';
4344
import { toCheckRunLogUri } from '../view/checkRunLogContentProvider';
@@ -458,7 +459,14 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
458459
currentUserReviewState: reviewState,
459460
revertable: pullRequest.state === GithubItemStateEnum.Merged,
460461
isCopilotOnMyBehalf: await isCopilotOnMyBehalf(pullRequest, currentUser, coAuthors),
461-
generateDescriptionTitle: this.getGenerateDescriptionTitle()
462+
generateDescriptionTitle: this.getGenerateDescriptionTitle(),
463+
closingIssues: await Promise.all((pullRequest.closingIssues ?? []).map(async issue => {
464+
const parsed = parseIssueExpressionOutput(issue.url.match(ISSUE_OR_URL_EXPRESSION));
465+
const owner = parsed?.owner ?? pullRequest.remote.owner;
466+
const repo = parsed?.name ?? pullRequest.remote.repositoryName;
467+
const webviewUri = await toOpenIssueWebviewUri({ owner, repo, issueNumber: issue.number });
468+
return { ...issue, url: webviewUri.toString() };
469+
})),
462470
};
463471
this._postMessage({
464472
command: 'pr.initialize',

src/github/queries.gql

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

src/github/queriesExtra.gql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,15 @@ fragment PullRequestFragment on PullRequest {
237237
mergeCommitMessage
238238
mergeCommitTitle
239239
}
240+
closingIssuesReferences(first: 50) {
241+
nodes {
242+
id
243+
number
244+
title
245+
state
246+
url
247+
}
248+
}
240249
merged
241250
mergeable
242251
mergeQueueEntry {

src/github/queriesLimited.gql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,15 @@ fragment PullRequestFragment on PullRequest {
206206
}
207207
url
208208
}
209+
closingIssuesReferences(first: 50) {
210+
nodes {
211+
id
212+
number
213+
title
214+
state
215+
url
216+
}
217+
}
209218
merged
210219
mergeable
211220
mergeStateStatus

src/github/utils.ts

Lines changed: 17 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,22 @@ function parseSuggestedReviewers(
10681069
return ret.sort(loginComparator);
10691070
}
10701071

1072+
function parseClosingIssuesReferences(
1073+
closingIssuesReferences: Array<{ id: number, number: number, title: string, state: string, url: string }> | undefined
1074+
): Array<{ id: number, number: number, title: string, state: GithubItemStateEnum, url: string }> {
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+
url: issue.url,
1085+
}));
1086+
}
1087+
10711088
/**
10721089
* Used for case insensitive sort by login
10731090
*/

src/github/views.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ export enum ReviewType {
2828
RequestChanges = 'requestChanges',
2929
}
3030

31+
export interface IssueReference {
32+
number: number;
33+
title: string;
34+
state: GithubItemStateEnum;
35+
url: string;
36+
}
37+
3138
export interface DisplayLabel extends ILabel {
3239
displayName: string;
3340
}
@@ -112,6 +119,7 @@ export interface PullRequest extends Issue {
112119
busy?: boolean;
113120
loadingCommit?: string;
114121
generateDescriptionTitle?: string;
122+
closingIssues: IssueReference[];
115123
}
116124

117125
export interface ProjectItemsReply {

webviews/common/common.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,35 @@ body img.avatar {
312312
fill: var(--vscode-issues-open);
313313
}
314314

315+
.section-icon.issue-open svg path {
316+
fill: var(--vscode-issues-open);
317+
}
318+
319+
.section-icon.issue-closed svg path {
320+
fill: var(--vscode-issues-closed);
321+
}
322+
323+
.issue-item {
324+
display: flex;
325+
align-items: center;
326+
gap: 6px;
327+
color: var(--vscode-foreground);
328+
text-decoration: none;
329+
overflow: hidden;
330+
}
331+
332+
.issue-item:hover {
333+
text-decoration: underline;
334+
color: var(--vscode-foreground);
335+
}
336+
337+
.issue-item-text {
338+
white-space: nowrap;
339+
overflow: hidden;
340+
text-overflow: ellipsis;
341+
flex: 1;
342+
min-width: 0;
343+
}
315344
.reviewer-icons {
316345
display: flex;
317346
gap: 4px;

0 commit comments

Comments
 (0)