diff --git a/package.json b/package.json
index b75be349dc..825fc2ff95 100644
--- a/package.json
+++ b/package.json
@@ -1316,6 +1316,11 @@
"title": "%command.pr.createPrMenuRebase.title%",
"category": "%command.pull.request.category%"
},
+ {
+ "command": "issue.openDescription",
+ "title": "%command.issue.openDescription.title%",
+ "category": "%command.issues.category%"
+ },
{
"command": "issue.createIssueFromSelection",
"title": "%command.issue.createIssueFromSelection.title%",
@@ -2070,6 +2075,10 @@
"command": "pr.acceptMerge",
"when": "isMergeResultEditor && mergeEditorBaseUri =~ /^(githubpr|gitpr):/"
},
+ {
+ "command": "issue.openDescription",
+ "when": "false"
+ },
{
"command": "issue.copyGithubPermalink",
"when": "github:hasGitHubRemotes"
diff --git a/package.nls.json b/package.nls.json
index 7ba11bc38a..8474b1e27d 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -260,6 +260,7 @@
"command.pr.closeRelatedEditors.title": "Close All Pull Request Editors",
"command.pr.toggleEditorCommentingOn.title": "Toggle Editor Commenting On",
"command.pr.toggleEditorCommentingOff.title": "Toggle Editor Commenting Off",
+ "command.issue.openDescription.title": "View Issue Description",
"command.issue.copyGithubDevLink.title": "Copy github.dev Link",
"command.issue.copyGithubPermalink.title": "Copy GitHub Permalink",
"command.issue.copyGithubHeadLink.title": "Copy GitHub Head Link",
diff --git a/resources/icons/issue.svg b/resources/icons/issue.svg
new file mode 100644
index 0000000000..666a3baac7
--- /dev/null
+++ b/resources/icons/issue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/icons/issue_closed.svg b/resources/icons/issue_closed.svg
new file mode 100644
index 0000000000..6a6c314255
--- /dev/null
+++ b/resources/icons/issue_closed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/commands.ts b/src/commands.ts
index bfb7381c26..3f9624daaf 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -67,32 +67,37 @@ function ensurePR>(
export async function openDescription(
telemetry: ITelemetry,
- pullRequestModel: IssueModel,
+ issueModel: IssueModel,
descriptionNode: DescriptionNode | undefined,
folderManager: FolderRepositoryManager,
revealNode: boolean,
preserveFocus: boolean = true,
notificationProvider?: NotificationProvider
) {
- const pullRequest = ensurePR(folderManager, pullRequestModel);
+ const issue = ensurePR(folderManager, issueModel);
if (revealNode) {
descriptionNode?.reveal(descriptionNode, { select: true, focus: true });
}
// Create and show a new webview
- if (pullRequest instanceof PullRequestModel) {
- await PullRequestOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, pullRequest, undefined, preserveFocus);
+ if (issue instanceof PullRequestModel) {
+ await PullRequestOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, issue, undefined, preserveFocus);
+ /* __GDPR__
+ "pr.openDescription" : {}
+ */
+ telemetry.sendTelemetryEvent('pr.openDescription');
} else {
- await IssueOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, pullRequest);
+ await IssueOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, issue);
+ /* __GDPR__
+ "issue.openDescription" : {}
+ */
+ telemetry.sendTelemetryEvent('issue.openDescription');
}
- if (notificationProvider?.hasNotification(pullRequest)) {
- notificationProvider.markPrNotificationsAsRead(pullRequest);
+ if (notificationProvider?.hasNotification(issue)) {
+ notificationProvider.markPrNotificationsAsRead(issue);
}
- /* __GDPR__
- "pr.openDescription" : {}
- */
- telemetry.sendTelemetryEvent('pr.openDescription');
+
}
async function chooseItem(
@@ -115,7 +120,7 @@ async function chooseItem(
return (await vscode.window.showQuickPick(items, options))?.itemValue;
}
-export async function openPullRequestOnGitHub(e: PRNode | DescriptionNode | PullRequestModel | NotificationTreeItem, telemetry: ITelemetry) {
+export async function openPullRequestOnGitHub(e: PRNode | DescriptionNode | IssueModel | NotificationTreeItem, telemetry: ITelemetry) {
if (e instanceof PRNode || e instanceof DescriptionNode) {
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.pullRequestModel.html_url));
} else if (isNotificationTreeItem(e)) {
@@ -805,50 +810,61 @@ export function registerCommands(
}),
);
- context.subscriptions.push(
- vscode.commands.registerCommand(
- 'pr.openDescription',
- async (argument: DescriptionNode | IssueModel | undefined) => {
- let pullRequestModel: IssueModel | undefined;
- if (!argument) {
- const activePullRequests: PullRequestModel[] = reposManager.folderManagers
- .map(manager => manager.activePullRequest!)
- .filter(activePR => !!activePR);
- if (activePullRequests.length >= 1) {
- pullRequestModel = await chooseItem(
- activePullRequests,
- itemValue => itemValue.title,
- );
- }
- } else {
- pullRequestModel = argument instanceof DescriptionNode ? argument.pullRequestModel : argument;
- }
+ async function openDescriptionCommand(argument: DescriptionNode | IssueModel | undefined) {
+ let issueModel: IssueModel | undefined;
+ if (!argument) {
+ const activePullRequests: PullRequestModel[] = reposManager.folderManagers
+ .map(manager => manager.activePullRequest!)
+ .filter(activePR => !!activePR);
+ if (activePullRequests.length >= 1) {
+ issueModel = await chooseItem(
+ activePullRequests,
+ itemValue => itemValue.title,
+ );
+ }
+ } else {
+ issueModel = argument instanceof DescriptionNode ? argument.pullRequestModel : argument;
+ }
- if (!pullRequestModel) {
- Logger.appendLine('No pull request found.', logId);
- return;
- }
+ if (!issueModel) {
+ Logger.appendLine('No pull request found.', logId);
+ return;
+ }
- const folderManager = reposManager.getManagerForIssueModel(pullRequestModel);
- if (!folderManager) {
- return;
- }
+ const folderManager = reposManager.getManagerForIssueModel(issueModel);
+ if (!folderManager) {
+ return;
+ }
- let descriptionNode: DescriptionNode | undefined;
- if (argument instanceof DescriptionNode) {
- descriptionNode = argument;
- } else {
- const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewsManager.reviewManagers, folderManager);
- if (!reviewManager) {
- return;
- }
+ let descriptionNode: DescriptionNode | undefined;
+ if (argument instanceof DescriptionNode) {
+ descriptionNode = argument;
+ } else {
+ const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewsManager.reviewManagers, folderManager);
+ if (!reviewManager) {
+ return;
+ }
- descriptionNode = reviewManager.changesInPrDataProvider.getDescriptionNode(folderManager);
- }
+ descriptionNode = reviewManager.changesInPrDataProvider.getDescriptionNode(folderManager);
+ }
- await openDescription(telemetry, pullRequestModel, descriptionNode, folderManager, !(argument instanceof DescriptionNode), !(argument instanceof RepositoryChangesNode), tree.notificationProvider);
- },
- ),
+ const revealDescription = !(argument instanceof DescriptionNode) && (!(argument instanceof IssueModel) || (argument instanceof PullRequestModel));
+
+ await openDescription(telemetry, issueModel, descriptionNode, folderManager, revealDescription, !(argument instanceof RepositoryChangesNode), tree.notificationProvider);
+ }
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ 'pr.openDescription',
+ openDescriptionCommand
+ )
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ 'issue.openDescription',
+ openDescriptionCommand
+ )
);
context.subscriptions.push(
@@ -1460,6 +1476,7 @@ ${contents}
vscode.env.openExternal(getPullsUrl(githubRepo));
}
}));
+
context.subscriptions.push(
vscode.commands.registerCommand('issues.openIssuesWebsite', async () => {
const githubRepo = await chooseRepoToOpen();
diff --git a/src/common/timelineEvent.ts b/src/common/timelineEvent.ts
index 0bbacc1158..99023688b3 100644
--- a/src/common/timelineEvent.ts
+++ b/src/common/timelineEvent.ts
@@ -33,7 +33,7 @@ export interface CommentEvent {
htmlUrl: string;
body: string;
bodyHTML?: string;
- user: IAccount;
+ user?: IAccount;
event: EventType.Commented;
canEdit?: boolean;
canDelete?: boolean;
diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts
index 40b35d42ef..cb4831c7d7 100644
--- a/src/github/folderRepositoryManager.ts
+++ b/src/github/folderRepositoryManager.ts
@@ -2087,9 +2087,9 @@ export class FolderRepositoryManager extends Disposable {
}
async getPullRequestRepositoryAccessAndMergeMethods(
- pullRequest: PullRequestModel,
+ issue: IssueModel,
): Promise {
- const mergeOptions = await pullRequest.githubRepository.getRepoAccessAndMergeMethods();
+ const mergeOptions = await issue.githubRepository.getRepoAccessAndMergeMethods();
return mergeOptions;
}
diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts
index 4564d4e548..e58537717c 100644
--- a/src/github/githubRepository.ts
+++ b/src/github/githubRepository.ts
@@ -22,6 +22,7 @@ import {
GetBranchResponse,
GetChecksResponse,
isCheckRun,
+ IssueResponse,
IssuesSearchResponse,
ListBranchesResponse,
MaxIssueResponse,
@@ -1003,7 +1004,7 @@ export class GitHubRepository extends Disposable {
Logger.debug(`Fetch issue ${id} - enter`, this.id);
const { query, remote, schema } = await this.ensure();
- const { data } = await query({
+ const { data } = await query({
query: withComments ? schema.IssueWithComments : schema.Issue,
variables: {
owner: remote.owner,
@@ -1018,7 +1019,7 @@ export class GitHubRepository extends Disposable {
}
Logger.debug(`Fetch issue ${id} - done`, this.id);
- return new IssueModel(this, remote, parseGraphQLIssue(data.repository.pullRequest, this));
+ return new IssueModel(this, remote, parseGraphQLIssue(data.repository.issue, this));
} catch (e) {
Logger.error(`Unable to fetch issue: ${e}`, this.id);
return;
diff --git a/src/github/graphql.ts b/src/github/graphql.ts
index 7fb811ca4a..1cbf16630b 100644
--- a/src/github/graphql.ts
+++ b/src/github/graphql.ts
@@ -450,9 +450,9 @@ export interface DeleteReactionResponse {
};
}
-export interface UpdatePullRequestResponse {
- updatePullRequest: {
- pullRequest: {
+export interface UpdateIssueResponse {
+ updateIssue: {
+ issue: {
body: string;
bodyHTML: string;
title: string;
@@ -522,12 +522,12 @@ export interface SuggestedReviewerResponse {
export type MergeMethod = 'MERGE' | 'REBASE' | 'SQUASH';
export type MergeQueueState = 'AWAITING_CHECKS' | 'LOCKED' | 'MERGEABLE' | 'QUEUED' | 'UNMERGEABLE';
-export interface PullRequest {
+export interface Issue {
id: string;
databaseId: number;
number: number;
url: string;
- state: 'OPEN' | 'CLOSED' | 'MERGED';
+ state: 'OPEN' | 'CLOSED' | 'MERGED'; // TODO: don't allow merged in an issue
body: string;
bodyHTML: string;
title: string;
@@ -536,48 +536,19 @@ export interface PullRequest {
nodes: Account[];
};
author: Account;
- commits: {
- nodes: {
- commit: {
- message: string;
- };
- }[];
- };
comments: {
nodes?: AbbreviatedIssueComment[];
totalCount: number;
};
createdAt: string;
updatedAt: string;
- headRef?: Ref;
- headRefName: string;
- headRefOid: string;
- headRepository?: RefRepository;
- baseRef?: Ref;
- baseRefName: string;
- baseRefOid: string;
- baseRepository: BaseRefRepository;
labels: {
nodes: {
name: string;
color: string;
}[];
};
- merged: boolean;
- mergeable: 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN';
- mergeQueueEntry?: MergeQueueEntry | null;
- mergeStateStatus: 'BEHIND' | 'BLOCKED' | 'CLEAN' | 'DIRTY' | 'HAS_HOOKS' | 'UNKNOWN' | 'UNSTABLE';
- reviewThreads: {
- totalCount: number;
- }
- autoMergeRequest?: {
- mergeMethod: MergeMethod;
- };
- viewerCanEnableAutoMerge: boolean;
- viewerCanDisableAutoMerge: boolean;
viewerCanUpdate: boolean;
- isDraft?: boolean;
- suggestedReviewers: SuggestedReviewerResponse[];
projectItems?: {
nodes: {
project: {
@@ -606,6 +577,39 @@ export interface PullRequest {
}
}
+
+export interface PullRequest extends Issue {
+ commits: {
+ nodes: {
+ commit: {
+ message: string;
+ };
+ }[];
+ };
+ headRef?: Ref;
+ headRefName: string;
+ headRefOid: string;
+ headRepository?: RefRepository;
+ baseRef?: Ref;
+ baseRefName: string;
+ baseRefOid: string;
+ baseRepository: BaseRefRepository;
+ merged: boolean;
+ mergeable: 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN';
+ mergeQueueEntry?: MergeQueueEntry | null;
+ mergeStateStatus: 'BEHIND' | 'BLOCKED' | 'CLEAN' | 'DIRTY' | 'HAS_HOOKS' | 'UNKNOWN' | 'UNSTABLE';
+ reviewThreads: {
+ totalCount: number;
+ }
+ autoMergeRequest?: {
+ mergeMethod: MergeMethod;
+ };
+ viewerCanEnableAutoMerge: boolean;
+ viewerCanDisableAutoMerge: boolean;
+ isDraft?: boolean;
+ suggestedReviewers: SuggestedReviewerResponse[];
+}
+
export enum DefaultCommitTitle {
prTitle = 'PR_TITLE',
commitOrPrTitle = 'COMMIT_OR_PR_TITLE',
@@ -626,6 +630,13 @@ export interface PullRequestResponse {
rateLimit: RateLimit;
}
+export interface IssueResponse {
+ repository: {
+ issue: PullRequest;
+ } | null;
+ rateLimit: RateLimit;
+}
+
export interface PullRequestMergabilityResponse {
repository: {
pullRequest: {
diff --git a/src/github/interface.ts b/src/github/interface.ts
index 67bebfc98b..6a518ef950 100644
--- a/src/github/interface.ts
+++ b/src/github/interface.ts
@@ -287,7 +287,7 @@ export interface IPullRequestsPagingOptions {
fetchOnePagePerRepo?: boolean;
}
-export interface IPullRequestEditData {
+export interface IIssueEditData {
body?: string;
title?: string;
}
diff --git a/src/github/issueModel.ts b/src/github/issueModel.ts
index 0881fc5050..81b6370902 100644
--- a/src/github/issueModel.ts
+++ b/src/github/issueModel.ts
@@ -15,9 +15,9 @@ import {
AddPullRequestToProjectResponse,
EditIssueCommentResponse,
TimelineEventsResponse,
- UpdatePullRequestResponse,
+ UpdateIssueResponse,
} from './graphql';
-import { GithubItemStateEnum, IAccount, IMilestone, IProject, IProjectItem, IPullRequestEditData, Issue } from './interface';
+import { GithubItemStateEnum, IAccount, IIssueEditData, IMilestone, IProject, IProjectItem, Issue } from './interface';
import { parseGraphQlIssueComment, parseGraphQLTimelineEvents } from './utils';
export class IssueModel {
@@ -153,28 +153,38 @@ export class IssueModel {
return true;
}
- async edit(toEdit: IPullRequestEditData): Promise<{ body: string; bodyHTML: string; title: string; titleHTML: string }> {
+ protected updateIssueInput(id: string): Object {
+ return {
+ id
+ };
+ }
+
+ protected updateIssueSchema(schema: any): any {
+ return schema.UpdateIssue;
+ }
+
+ async edit(toEdit: IIssueEditData): Promise<{ body: string; bodyHTML: string; title: string; titleHTML: string }> {
try {
const { mutate, schema } = await this.githubRepository.ensure();
- const { data } = await mutate({
- mutation: schema.UpdatePullRequest,
+ const { data } = await mutate({
+ mutation: this.updateIssueSchema(schema),
variables: {
input: {
- pullRequestId: this.graphNodeId,
+ ...this.updateIssueInput(this.graphNodeId),
body: toEdit.body,
title: toEdit.title,
},
},
});
- if (data?.updatePullRequest.pullRequest) {
- this.item.body = data.updatePullRequest.pullRequest.body;
- this.bodyHTML = data.updatePullRequest.pullRequest.bodyHTML;
- this.title = data.updatePullRequest.pullRequest.title;
- this.titleHTML = data.updatePullRequest.pullRequest.titleHTML;
+ if (data?.updateIssue.issue) {
+ this.item.body = data.updateIssue.issue.body;
+ this.bodyHTML = data.updateIssue.issue.bodyHTML;
+ this.title = data.updateIssue.issue.title;
+ this.titleHTML = data.updateIssue.issue.titleHTML;
this.invalidate();
}
- return data!.updatePullRequest.pullRequest;
+ return data!.updateIssue.issue;
} catch (e) {
throw new Error(formatError(e));
}
@@ -340,4 +350,43 @@ export class IssueModel {
return [];
}
}
+
+ async updateMilestone(id: string): Promise {
+ const { mutate, schema } = await this.githubRepository.ensure();
+ const finalId = id === 'null' ? null : id;
+
+ try {
+ await mutate({
+ mutation: this.updateIssueSchema(schema),
+ variables: {
+ input: {
+ ...this.updateIssueInput(this.graphNodeId),
+ milestoneId: finalId,
+ },
+ },
+ });
+ } catch (err) {
+ Logger.error(err, IssueModel.ID);
+ }
+ }
+
+ async addAssignees(assignees: string[]): Promise {
+ const { octokit, remote } = await this.githubRepository.ensure();
+ await octokit.call(octokit.api.issues.addAssignees, {
+ owner: remote.owner,
+ repo: remote.repositoryName,
+ issue_number: this.number,
+ assignees,
+ });
+ }
+
+ async deleteAssignees(assignees: string[]): Promise {
+ const { octokit, remote } = await this.githubRepository.ensure();
+ await octokit.call(octokit.api.issues.removeAssignees, {
+ owner: remote.owner,
+ repo: remote.repositoryName,
+ issue_number: this.number,
+ assignees,
+ });
+ }
}
diff --git a/src/github/issueOverview.ts b/src/github/issueOverview.ts
index 461bfb627c..71073a96cb 100644
--- a/src/github/issueOverview.ts
+++ b/src/github/issueOverview.ts
@@ -5,16 +5,20 @@
'use strict';
import * as vscode from 'vscode';
+import { openPullRequestOnGitHub } from '../commands';
import { IComment } from '../common/comment';
import Logger from '../common/logger';
import { ITelemetry } from '../common/telemetry';
+import { CommentEvent, EventType, TimelineEvent } from '../common/timelineEvent';
import { asPromise, formatError } from '../common/utils';
import { getNonce, IRequestMessage, WebviewBase } from '../common/webview';
import { DescriptionNode } from '../view/treeNodes/descriptionNode';
import { FolderRepositoryManager } from './folderRepositoryManager';
-import { ILabel } from './interface';
+import { IAccount, ILabel, IMilestone, IProject, IProjectItem, RepoAccessAndMergeMethods } from './interface';
import { IssueModel } from './issueModel';
-import { getLabelOptions } from './quickPicks';
+import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick } from './quickPicks';
+import { isInCodespaces, vscodeDevPrLink } from './utils';
+import { Issue, ProjectItemsReply } from './views';
export class IssueOverviewPanel extends WebviewBase {
public static ID: string = 'IssueOverviewPanel';
@@ -84,6 +88,10 @@ export class IssueOverviewPanel extends W
title: string,
folderRepositoryManager: FolderRepositoryManager,
type: string = IssueOverviewPanel._viewType,
+ iconSubpath?: {
+ light: string,
+ dark: string,
+ }
) {
super();
this._folderRepositoryManager = folderRepositoryManager;
@@ -99,6 +107,13 @@ export class IssueOverviewPanel extends W
enableFindWidget: true
}));
+ if (iconSubpath) {
+ this._panel.iconPath = {
+ dark: vscode.Uri.joinPath(_extensionUri, iconSubpath.dark),
+ light: vscode.Uri.joinPath(_extensionUri, iconSubpath.light)
+ };
+ }
+
this._webview = this._panel.webview;
super.initialize();
@@ -124,6 +139,46 @@ export class IssueOverviewPanel extends W
}
}
+ protected continueOnGitHub() {
+ return isInCodespaces();
+ }
+
+ protected getInitializeContext(issue: IssueModel, timelineEvents: TimelineEvent[], repositoryAccess: RepoAccessAndMergeMethods, viewerCanEdit: boolean): Issue {
+ const hasWritePermission = repositoryAccess!.hasWritePermission;
+ const canEdit = hasWritePermission || viewerCanEdit;
+ const context: Issue = {
+ number: issue.number,
+ title: issue.title,
+ titleHTML: issue.titleHTML,
+ url: issue.html_url,
+ createdAt: issue.createdAt,
+ body: issue.body,
+ bodyHTML: issue.bodyHTML,
+ labels: issue.item.labels,
+ author: {
+ login: issue.author.login,
+ name: issue.author.name,
+ avatarUrl: issue.userAvatar,
+ url: issue.author.url,
+ id: issue.author.id,
+ accountType: issue.author.accountType,
+ },
+ state: issue.state,
+ events: timelineEvents,
+ continueOnGitHub: this.continueOnGitHub(),
+ canEdit,
+ hasWritePermission,
+ isIssue: true,
+ projectItems: issue.item.projectItems,
+ milestone: issue.milestone,
+ assignees: issue.assignees ?? [],
+ isEnterprise: issue.githubRepository.remote.isEnterprise,
+ isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark
+ };
+
+ return context;
+ }
+
public async updateIssue(issueModel: IssueModel): Promise {
return Promise.all([
this._folderRepositoryManager.resolveIssue(
@@ -132,10 +187,11 @@ export class IssueOverviewPanel extends W
issueModel.number,
),
issueModel.getIssueTimelineEvents(),
- this._folderRepositoryManager.getPullRequestRepositoryDefaultBranch(issueModel),
+ this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(issueModel),
+ issueModel.canEdit()
])
.then(result => {
- const [issue, timelineEvents, defaultBranch] = result;
+ const [issue, timelineEvents, repositoryAccess, viewerCanEdit] = result;
if (!issue) {
throw new Error(
`Fail to resolve issue #${issueModel.number} in ${issueModel.remote.owner}/${issueModel.remote.repositoryName}`,
@@ -148,30 +204,7 @@ export class IssueOverviewPanel extends W
Logger.debug('pr.initialize', IssueOverviewPanel.ID);
this._postMessage({
command: 'pr.initialize',
- pullrequest: {
- number: this._item.number,
- title: this._item.title,
- titleHTML: this._item.titleHTML,
- url: this._item.html_url,
- createdAt: this._item.createdAt,
- body: this._item.body,
- bodyHTML: this._item.bodyHTML,
- labels: this._item.item.labels,
- author: {
- login: this._item.author.login,
- name: this._item.author.name,
- avatarUrl: this._item.userAvatar,
- url: this._item.author.url,
- },
- state: this._item.state,
- events: timelineEvents,
- repositoryDefaultBranch: defaultBranch,
- canEdit: true,
- // TODO@eamodio What is status?
- status: /*status ? status :*/ { statuses: [] },
- isIssue: true,
- isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark
- },
+ pullrequest: this.getInitializeContext(issue, timelineEvents, repositoryAccess, viewerCanEdit)
});
})
.catch(e => {
@@ -202,8 +235,8 @@ export class IssueOverviewPanel extends W
return;
case 'pr.close':
return this.close(message);
- case 'pr.comment':
- return this.createComment(message);
+ case 'pr.submit':
+ return this.submitReviewMessage(message);
case 'scroll':
this._scrollPosition = message.args.scrollPosition;
return;
@@ -222,6 +255,24 @@ export class IssueOverviewPanel extends W
return this.addLabels(message);
case 'pr.remove-label':
return this.removeLabel(message);
+ case 'pr.change-assignees':
+ return this.changeAssignees(message);
+ case 'pr.remove-milestone':
+ return this.removeMilestone(message);
+ case 'pr.add-milestone':
+ return this.addMilestone(message);
+ case 'pr.change-projects':
+ return this.changeProjects(message);
+ case 'pr.remove-project':
+ return this.removeProject(message);
+ case 'pr.add-assignee-yourself':
+ return this.addAssigneeYourself(message);
+ case 'pr.copy-prlink':
+ return this.copyItemLink();
+ case 'pr.copy-vscodedevlink':
+ return this.copyVscodeDevLink();
+ case 'pr.openOnGitHub':
+ return openPullRequestOnGitHub(this._item, (this._item as any)._telemetry);
case 'pr.debug':
return this.webviewDebug(message);
default:
@@ -229,6 +280,10 @@ export class IssueOverviewPanel extends W
}
}
+ protected submitReviewMessage(message: IRequestMessage) {
+ return this.createComment(message);
+ }
+
private async addLabels(message: IRequestMessage): Promise {
const quickPick = vscode.window.createQuickPick();
try {
@@ -309,6 +364,112 @@ export class IssueOverviewPanel extends W
});
}
+ private async changeAssignees(message: IRequestMessage): Promise {
+ const quickPick = vscode.window.createQuickPick();
+
+ try {
+ quickPick.busy = true;
+ quickPick.canSelectMany = true;
+ quickPick.matchOnDescription = true;
+ quickPick.show();
+ quickPick.items = await getAssigneesQuickPickItems(this._folderRepositoryManager, undefined, this._item.remote.remoteName, this._item.assignees ?? [], this._item);
+ quickPick.selectedItems = quickPick.items.filter(item => item.picked);
+
+ quickPick.busy = false;
+ const acceptPromise = asPromise(quickPick.onDidAccept).then(() => {
+ return quickPick.selectedItems.filter(item => item.user) as (vscode.QuickPickItem & { user: IAccount })[] | undefined;
+ });
+ const hidePromise = asPromise(quickPick.onDidHide);
+ const allAssignees = await Promise.race<(vscode.QuickPickItem & { user: IAccount })[] | void>([acceptPromise, hidePromise]);
+ quickPick.busy = true;
+
+ if (allAssignees) {
+ const newAssignees: IAccount[] = allAssignees.map(item => item.user);
+ const removeAssignees: IAccount[] = this._item.assignees?.filter(currentAssignee => !newAssignees.find(newAssignee => newAssignee.login === currentAssignee.login)) ?? [];
+ this._item.assignees = newAssignees;
+
+ await this._item.addAssignees(newAssignees.map(assignee => assignee.login));
+ await this._item.deleteAssignees(removeAssignees.map(assignee => assignee.login));
+ await this._replyMessage(message, {
+ assignees: newAssignees,
+ });
+ }
+ } catch (e) {
+ vscode.window.showErrorMessage(formatError(e));
+ } finally {
+ quickPick.hide();
+ quickPick.dispose();
+ }
+ }
+
+
+ private async addMilestone(message: IRequestMessage): Promise {
+ return getMilestoneFromQuickPick(this._folderRepositoryManager, this._item.githubRepository, this._item.milestone, (milestone) => this.updateMilestone(milestone, message));
+ }
+
+ private async updateMilestone(milestone: IMilestone | undefined, message: IRequestMessage) {
+ if (!milestone) {
+ return this.removeMilestone(message);
+ }
+ await this._item.updateMilestone(milestone.id);
+ this._replyMessage(message, {
+ added: milestone,
+ });
+ }
+
+ private async removeMilestone(message: IRequestMessage): Promise {
+ try {
+ await this._item.updateMilestone('null');
+ this._replyMessage(message, {});
+ } catch (e) {
+ vscode.window.showErrorMessage(formatError(e));
+ }
+ }
+
+ private async changeProjects(message: IRequestMessage): Promise {
+ return getProjectFromQuickPick(this._folderRepositoryManager, this._item.githubRepository, this._item.item.projectItems?.map(item => item.project), (project) => this.updateProjects(project, message));
+ }
+
+ private async updateProjects(projects: IProject[] | undefined, message: IRequestMessage) {
+ let newProjects: IProjectItem[] = [];
+ if (projects) {
+ newProjects = (await this._item.updateProjects(projects)) ?? [];
+ }
+ const projectItemsReply: ProjectItemsReply = {
+ projectItems: newProjects,
+ };
+ return this._replyMessage(message, projectItemsReply);
+ }
+
+ private async removeProject(message: IRequestMessage): Promise {
+ await this._item.removeProjects([message.args]);
+ return this._replyMessage(message, {});
+ }
+
+ private async addAssigneeYourself(message: IRequestMessage): Promise {
+ try {
+ const currentUser = await this._folderRepositoryManager.getCurrentUser();
+ const alreadyAssigned = this._item.assignees?.find(user => user.login === currentUser.login);
+ if (!alreadyAssigned) {
+ this._item.assignees = this._item.assignees?.concat(currentUser);
+ await this._item.addAssignees([currentUser.login]);
+ }
+ this._replyMessage(message, {
+ assignees: this._item.assignees,
+ });
+ } catch (e) {
+ vscode.window.showErrorMessage(formatError(e));
+ }
+ }
+
+ private async copyItemLink(): Promise {
+ return vscode.env.clipboard.writeText(this._item.html_url);
+ }
+
+ private async copyVscodeDevLink(): Promise {
+ return vscode.env.clipboard.writeText(vscodeDevPrLink(this._item));
+ }
+
protected editCommentPromise(comment: IComment, text: string): Promise {
return this._item.editIssueComment(comment, text);
}
@@ -363,9 +524,13 @@ export class IssueOverviewPanel extends W
}
private createComment(message: IRequestMessage) {
- this._item.createIssueComment(message.args).then(comment => {
- this._replyMessage(message, {
- value: comment,
+ return this._item.createIssueComment(message.args).then(comment => {
+ const commentedEvent: CommentEvent = {
+ ...comment,
+ event: EventType.Commented
+ };
+ return this._replyMessage(message, {
+ event: commentedEvent,
});
});
}
diff --git a/src/github/notifications.ts b/src/github/notifications.ts
index b736ffac69..b28a0d2cb1 100644
--- a/src/github/notifications.ts
+++ b/src/github/notifications.ts
@@ -125,7 +125,7 @@ export class NotificationProvider extends Disposable {
}
private getPrIdentifier(pullRequest: IssueModel | OctokitResponse['data']): string {
- if (pullRequest instanceof PullRequestModel) {
+ if (pullRequest instanceof IssueModel) {
return `${pullRequest.remote.url}:${pullRequest.number}`;
}
const splitPrUrl = pullRequest.subject.url.split('/');
diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts
index 6c570a1c37..f3d22e5725 100644
--- a/src/github/pullRequestModel.ts
+++ b/src/github/pullRequestModel.ts
@@ -49,7 +49,6 @@ import {
SubmitReviewResponse,
TimelineEventsResponse,
UnresolveReviewThreadResponse,
- UpdatePullRequestResponse,
} from './graphql';
import {
AccountType,
@@ -306,6 +305,16 @@ export class PullRequestModel extends IssueModel implements IPullRe
return false;
}
+ protected override updateIssueInput(id: string): Object {
+ return {
+ pullRequestId: id,
+ };
+ }
+
+ protected override updateIssueSchema(schema: any): any {
+ return schema.UpdatePullRequest;
+ }
+
/**
* Approve the pull request.
* @param message Optional approval comment text.
@@ -443,35 +452,6 @@ export class PullRequestModel extends IssueModel implements IPullRe
}
}
- async updateMilestone(id: string): Promise {
- const { mutate, schema } = await this.githubRepository.ensure();
- const finalId = id === 'null' ? null : id;
-
- try {
- await mutate({
- mutation: schema.UpdatePullRequest,
- variables: {
- input: {
- pullRequestId: this.item.graphNodeId,
- milestoneId: finalId,
- },
- },
- });
- } catch (err) {
- Logger.error(err, PullRequestModel.ID);
- }
- }
-
- async addAssignees(assignees: string[]): Promise {
- const { octokit, remote } = await this.githubRepository.ensure();
- await octokit.call(octokit.api.issues.addAssignees, {
- owner: remote.owner,
- repo: remote.repositoryName,
- issue_number: this.number,
- assignees,
- });
- }
-
/**
* Query to see if there is an existing review.
*/
@@ -1011,16 +991,6 @@ export class PullRequestModel extends IssueModel implements IPullRe
});
}
- async deleteAssignees(assignees: string[]): Promise {
- const { octokit, remote } = await this.githubRepository.ensure();
- await octokit.call(octokit.api.issues.removeAssignees, {
- owner: remote.owner,
- repo: remote.repositoryName,
- issue_number: this.number,
- assignees,
- });
- }
-
private diffThreads(oldReviewThreads: IReviewThread[], newReviewThreads: IReviewThread[]): void {
const added: IReviewThread[] = [];
const changed: IReviewThread[] = [];
diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts
index 93afe8bc6c..7c8bb40f9c 100644
--- a/src/github/pullRequestOverview.ts
+++ b/src/github/pullRequestOverview.ts
@@ -19,9 +19,6 @@ import { FolderRepositoryManager } from './folderRepositoryManager';
import {
GithubItemStateEnum,
IAccount,
- IMilestone,
- IProject,
- IProjectItem,
isTeam,
ITeam,
MergeMethod,
@@ -35,9 +32,9 @@ import { IssueOverviewPanel } from './issueOverview';
import { PullRequestGitHelper } from './pullRequestGitHelper';
import { PullRequestModel } from './pullRequestModel';
import { PullRequestView } from './pullRequestOverviewCommon';
-import { getAssigneesQuickPickItems, getMilestoneFromQuickPick, getProjectFromQuickPick, pickEmail, reviewersQuickPick } from './quickPicks';
-import { isInCodespaces, parseReviewers, vscodeDevPrLink } from './utils';
-import { MergeArguments, MergeResult, ProjectItemsReply, PullRequest, ReviewType } from './views';
+import { pickEmail, reviewersQuickPick } from './quickPicks';
+import { parseReviewers } from './utils';
+import { MergeArguments, MergeResult, PullRequest, ReviewType } from './views';
export class PullRequestOverviewPanel extends IssueOverviewPanel {
public static override ID: string = 'PullRequestOverviewPanel';
@@ -108,11 +105,10 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel {
return Promise.all([
this._folderRepositoryManager.resolvePullRequest(
@@ -245,43 +249,20 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel email.toLowerCase() === gitEmail.toLowerCase())) ?? currentUser.email);
Logger.debug('pr.initialize', PullRequestOverviewPanel.ID);
+ const baseContext = this.getInitializeContext(pullRequest, timelineEvents, repositoryAccess, viewerCanEdit);
+
const context: Partial = {
- number: pullRequest.number,
- title: pullRequest.title,
- titleHTML: pullRequest.titleHTML,
- url: pullRequest.html_url,
- createdAt: pullRequest.createdAt,
- body: pullRequest.body,
- bodyHTML: pullRequest.bodyHTML,
- labels: pullRequest.item.labels,
- author: {
- id: pullRequest.author.id,
- login: pullRequest.author.login,
- name: pullRequest.author.name,
- avatarUrl: pullRequest.userAvatar,
- url: pullRequest.author.url,
- accountType: pullRequest.author.accountType
- },
- state: pullRequest.state,
- events: timelineEvents,
+ ...baseContext,
isCurrentlyCheckedOut: isCurrentlyCheckedOut,
isRemoteBaseDeleted: pullRequest.isRemoteBaseDeleted,
base: pullRequest.base.label,
@@ -289,8 +270,6 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): Promise {
- return getMilestoneFromQuickPick(this._folderRepositoryManager, this._item.githubRepository, this._item.milestone, (milestone) => this.updateMilestone(milestone, message));
- }
-
- private async updateMilestone(milestone: IMilestone | undefined, message: IRequestMessage) {
- if (!milestone) {
- return this.removeMilestone(message);
- }
- await this._item.updateMilestone(milestone.id);
- this._replyMessage(message, {
- added: milestone,
- });
- }
-
- private async removeMilestone(message: IRequestMessage): Promise {
- try {
- await this._item.updateMilestone('null');
- this._replyMessage(message, {});
- } catch (e) {
- vscode.window.showErrorMessage(formatError(e));
- }
- }
-
- private async changeProjects(message: IRequestMessage): Promise {
- return getProjectFromQuickPick(this._folderRepositoryManager, this._item.githubRepository, this._item.item.projectItems?.map(item => item.project), (project) => this.updateProjects(project, message));
- }
-
- private async updateProjects(projects: IProject[] | undefined, message: IRequestMessage) {
- if (projects) {
- const newProjects = await this._item.updateProjects(projects);
- const projectItemsReply: ProjectItemsReply = {
- projectItems: newProjects,
- };
- return this._replyMessage(message, projectItemsReply);
- }
- }
-
- private async removeProject(message: IRequestMessage): Promise {
- await this._item.removeProjects([message.args]);
- return this._replyMessage(message, {});
- }
-
- private async changeAssignees(message: IRequestMessage): Promise {
- const quickPick = vscode.window.createQuickPick();
-
- try {
- quickPick.busy = true;
- quickPick.canSelectMany = true;
- quickPick.matchOnDescription = true;
- quickPick.show();
- quickPick.items = await getAssigneesQuickPickItems(this._folderRepositoryManager, undefined, this._item.remote.remoteName, this._item.assignees ?? [], this._item);
- quickPick.selectedItems = quickPick.items.filter(item => item.picked);
-
- quickPick.busy = false;
- const acceptPromise = asPromise(quickPick.onDidAccept).then(() => {
- return quickPick.selectedItems.filter(item => item.user) as (vscode.QuickPickItem & { user: IAccount })[] | undefined;
- });
- const hidePromise = asPromise(quickPick.onDidHide);
- const allAssignees = await Promise.race<(vscode.QuickPickItem & { user: IAccount })[] | void>([acceptPromise, hidePromise]);
- quickPick.busy = true;
-
- if (allAssignees) {
- const newAssignees: IAccount[] = allAssignees.map(item => item.user);
- const removeAssignees: IAccount[] = this._item.assignees?.filter(currentAssignee => !newAssignees.find(newAssignee => newAssignee.login === currentAssignee.login)) ?? [];
- this._item.assignees = newAssignees;
-
- await this._item.addAssignees(newAssignees.map(assignee => assignee.login));
- await this._item.deleteAssignees(removeAssignees.map(assignee => assignee.login));
- await this._replyMessage(message, {
- assignees: newAssignees,
- });
- }
- } catch (e) {
- vscode.window.showErrorMessage(formatError(e));
- } finally {
- quickPick.hide();
- quickPick.dispose();
- }
- }
-
- private async addAssigneeYourself(message: IRequestMessage): Promise {
- try {
- const currentUser = await this._folderRepositoryManager.getCurrentUser();
- const alreadyAssigned = this._item.assignees?.find(user => user.login === currentUser.login);
- if (!alreadyAssigned) {
- this._item.assignees = this._item.assignees?.concat(currentUser);
- await this._item.addAssignees([currentUser.login]);
- }
- this._replyMessage(message, {
- assignees: this._item.assignees,
- });
- } catch (e) {
- vscode.window.showErrorMessage(formatError(e));
- }
- }
-
private async applyPatch(message: IRequestMessage<{ comment: IComment }>): Promise {
try {
const comment = message.args.comment;
@@ -790,7 +647,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel this.submitReview(body));
}
- private submitReviewMessage(message: IRequestMessage) {
+ protected override submitReviewMessage(message: IRequestMessage) {
return this.doReviewMessage(message, (body) => this.submitReview(body));
}
@@ -834,14 +691,6 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel {
- return vscode.env.clipboard.writeText(this._item.html_url);
- }
-
- private async copyVscodeDevLink(): Promise {
- return vscode.env.clipboard.writeText(vscodeDevPrLink(this._item));
- }
-
private async updateAutoMerge(message: IRequestMessage<{ autoMerge?: boolean, autoMergeMethod: MergeMethod }>): Promise {
let replyMessage: { autoMerge: boolean, autoMergeMethod?: MergeMethod };
if (!message.args.autoMerge && !this._item.autoMerge) {
diff --git a/src/github/queries.gql b/src/github/queries.gql
index 1b2db38e8e..44beff4815 100644
--- a/src/github/queries.gql
+++ b/src/github/queries.gql
@@ -43,18 +43,118 @@ fragment Team on Team { # Team is not an Actor
...Node
}
-fragment PullRequestFragment on PullRequest {
+fragment IssueBase on Issue {
number
url
state
body
bodyHTML
+ title
titleHTML
+ author {
+ ...User
+ ...Organization
+ }
+ createdAt
+ updatedAt
+ milestone {
+ title
+ dueOn
+ createdAt
+ id
+ number
+ }
+ assignees(first: 10) {
+ nodes {
+ ...User
+ }
+ }
+ labels(first: 50) {
+ nodes {
+ name
+ color
+ }
+ }
+ id
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+}
+
+fragment IssueFragment on Issue {
+ ...IssueBase
+ comments(first: 1) {
+ totalCount
+ }
+}
+
+fragment IssueWithComments on Issue {
+ ...IssueBase
+ comments(first: 50) {
+ nodes {
+ author {
+ ...Node
+ ...Actor
+ ...User
+ ...Organization
+ }
+ body
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+ }
+ totalCount
+ }
+}
+
+fragment PullRequestFragment on PullRequest {
+ number
+ url
+ state
+ body
+ bodyHTML
title
+ titleHTML
author {
...User
...Organization
}
+ createdAt
+ updatedAt
+ milestone {
+ title
+ dueOn
+ createdAt
+ id
+ number
+ }
+ assignees(first: 10) {
+ nodes {
+ ...User
+ }
+ }
+ labels(first: 50) {
+ nodes {
+ name
+ color
+ }
+ }
+ id
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+
+ comments(first: 1) {
+ totalCount
+ }
+
+ comments(first: 1) {
+ totalCount
+ }
+
commits(first: 50) {
nodes {
commit {
@@ -62,8 +162,6 @@ fragment PullRequestFragment on PullRequest {
}
}
}
- createdAt
- updatedAt
headRef {
...Ref
}
@@ -92,12 +190,6 @@ fragment PullRequestFragment on PullRequest {
mergeCommitMessage
mergeCommitTitle
}
- labels(first: 50) {
- nodes {
- name
- color
- }
- }
merged
mergeable
mergeQueueEntry {
@@ -113,21 +205,7 @@ fragment PullRequestFragment on PullRequest {
viewerCanEnableAutoMerge
viewerCanDisableAutoMerge
viewerCanUpdate
- id
- databaseId
isDraft
- milestone {
- title
- dueOn
- createdAt
- id
- number
- }
- assignees(first: 10) {
- nodes {
- ...User
- }
- }
suggestedReviewers {
isAuthor
isCommenter
@@ -137,11 +215,45 @@ fragment PullRequestFragment on PullRequest {
...Node
}
}
- reactions(first: 1) {
- totalCount
+}
+
+query Issue($owner: String!, $name: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ issue(number: $number) {
+ ...IssueFragment
+ }
}
- comments(first: 1) {
- totalCount
+ rateLimit {
+ ...RateLimit
+ }
+}
+
+query IssueWithComments($owner: String!, $name: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ issue(number: $number) {
+ ...IssueWithComments
+ }
+ }
+ rateLimit {
+ ...RateLimit
+ }
+}
+
+query Issues($query: String!) {
+ search(first: 100, type: ISSUE, query: $query) {
+ issueCount
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ edges {
+ node {
+ ...IssueFragment
+ }
+ }
+ }
+ rateLimit {
+ ...RateLimit
}
}
diff --git a/src/github/queriesExtra.gql b/src/github/queriesExtra.gql
index e9de5c780b..72b55c9d8c 100644
--- a/src/github/queriesExtra.gql
+++ b/src/github/queriesExtra.gql
@@ -42,18 +42,137 @@ fragment Team on Team { # Team is not an Actor
...Node
}
-fragment PullRequestFragment on PullRequest {
+
+fragment IssueBase on Issue {
number
url
state
body
bodyHTML
+ title
titleHTML
+ author {
+ ...User
+ ...Organization
+ }
+ createdAt
+ updatedAt
+ milestone {
+ title
+ dueOn
+ createdAt
+ id
+ number
+ }
+ assignees(first: 10) {
+ nodes {
+ ...User
+ }
+ }
+ labels(first: 50) {
+ nodes {
+ name
+ color
+ }
+ }
+ id
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+ projectItems(first: 100) {
+ nodes {
+ id
+ project {
+ title
+ id
+ }
+ }
+ }
+}
+
+fragment IssueFragment on Issue {
+ ...IssueBase
+ comments(first: 1) {
+ totalCount
+ }
+}
+
+fragment IssueWithComments on Issue {
+ ...IssueBase
+ comments(first: 50) {
+ nodes {
+ author {
+ ...Node
+ ...Actor
+ ...User
+ ...Organization
+ }
+ body
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+ }
+ totalCount
+ }
+}
+
+fragment PullRequestFragment on PullRequest {
+ number
+ url
+ state
+ body
+ bodyHTML
title
+ titleHTML
author {
...User
...Organization
}
+ createdAt
+ updatedAt
+ milestone {
+ title
+ dueOn
+ createdAt
+ id
+ number
+ }
+ assignees(first: 10) {
+ nodes {
+ ...User
+ }
+ }
+ labels(first: 50) {
+ nodes {
+ name
+ color
+ }
+ }
+ id
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+ projectItems(first: 100) {
+ nodes {
+ id
+ project {
+ title
+ id
+ }
+ }
+ }
+
+ comments(first: 1) {
+ totalCount
+ }
+
+ comments(first: 1) {
+ totalCount
+ }
+
commits(first: 50) {
nodes {
commit {
@@ -61,8 +180,6 @@ fragment PullRequestFragment on PullRequest {
}
}
}
- createdAt
- updatedAt
headRef {
...Ref
}
@@ -91,12 +208,6 @@ fragment PullRequestFragment on PullRequest {
mergeCommitMessage
mergeCommitTitle
}
- labels(first: 50) {
- nodes {
- name
- color
- }
- }
merged
mergeable
mergeQueueEntry {
@@ -112,30 +223,7 @@ fragment PullRequestFragment on PullRequest {
viewerCanEnableAutoMerge
viewerCanDisableAutoMerge
viewerCanUpdate
- id
- databaseId
isDraft
- projectItems(first: 100) {
- nodes {
- id
- project {
- title
- id
- }
- }
- }
- milestone {
- title
- dueOn
- createdAt
- id
- number
- }
- assignees(first: 10) {
- nodes {
- ...User
- }
- }
suggestedReviewers {
isAuthor
isCommenter
@@ -145,11 +233,45 @@ fragment PullRequestFragment on PullRequest {
...Node
}
}
- reactions(first: 1) {
- totalCount
+}
+
+query Issue($owner: String!, $name: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ issue(number: $number) {
+ ...IssueFragment
+ }
}
- comments(first: 1) {
- totalCount
+ rateLimit {
+ ...RateLimit
+ }
+}
+
+query IssueWithComments($owner: String!, $name: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ issue(number: $number) {
+ ...IssueWithComments
+ }
+ }
+ rateLimit {
+ ...RateLimit
+ }
+}
+
+query Issues($query: String!) {
+ search(first: 100, type: ISSUE, query: $query) {
+ issueCount
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ edges {
+ node {
+ ...IssueFragment
+ }
+ }
+ }
+ rateLimit {
+ ...RateLimit
}
}
diff --git a/src/github/queriesLimited.gql b/src/github/queriesLimited.gql
index 276a432f61..47427e7e90 100644
--- a/src/github/queriesLimited.gql
+++ b/src/github/queriesLimited.gql
@@ -32,18 +32,114 @@ fragment Organization on Organization {
...Node
}
-fragment PullRequestFragment on PullRequest {
+fragment IssueBase on Issue {
number
url
state
body
bodyHTML
+ title
titleHTML
+ author {
+ ...User
+ ...Organization
+ }
+ createdAt
+ updatedAt
+ milestone {
+ title
+ dueOn
+ createdAt
+ id
+ number
+ }
+ assignees(first: 10) {
+ nodes {
+ ...User
+ }
+ }
+ labels(first: 50) {
+ nodes {
+ name
+ color
+ }
+ }
+ id
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+}
+
+fragment IssueFragment on Issue {
+ ...IssueBase
+ comments(first: 1) {
+ totalCount
+ }
+}
+
+fragment IssueWithComments on Issue {
+ ...IssueBase
+ comments(first: 50) {
+ nodes {
+ author {
+ ...Node
+ ...Actor
+ ...User
+ ...Organization
+ }
+ body
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+ }
+ totalCount
+ }
+}
+
+fragment PullRequestFragment on PullRequest {
+ number
+ url
+ state
+ body
+ bodyHTML
title
+ titleHTML
author {
...User
...Organization
}
+ createdAt
+ updatedAt
+ milestone {
+ title
+ dueOn
+ createdAt
+ id
+ number
+ }
+ assignees(first: 10) {
+ nodes {
+ ...User
+ }
+ }
+ labels(first: 50) {
+ nodes {
+ name
+ color
+ }
+ }
+ id
+ databaseId
+ reactions(first: 100) {
+ totalCount
+ }
+
+ comments(first: 1) {
+ totalCount
+ }
+
commits(first: 50) {
nodes {
commit {
@@ -51,8 +147,6 @@ fragment PullRequestFragment on PullRequest {
}
}
}
- createdAt
- updatedAt
headRef {
...Ref
}
@@ -77,12 +171,6 @@ fragment PullRequestFragment on PullRequest {
}
url
}
- labels(first: 50) {
- nodes {
- name
- color
- }
- }
merged
mergeable
mergeStateStatus
@@ -95,21 +183,7 @@ fragment PullRequestFragment on PullRequest {
viewerCanEnableAutoMerge
viewerCanDisableAutoMerge
viewerCanUpdate
- id
- databaseId
isDraft
- milestone {
- title
- dueOn
- createdAt
- id
- number
- }
- assignees(first: 10) {
- nodes {
- ...User
- }
- }
suggestedReviewers {
isAuthor
isCommenter
@@ -119,11 +193,45 @@ fragment PullRequestFragment on PullRequest {
...Node
}
}
- reactions(first: 1) {
- totalCount
+}
+
+query Issue($owner: String!, $name: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ issue(number: $number) {
+ ...IssueFragment
+ }
}
- comments(first: 1) {
- totalCount
+ rateLimit {
+ ...RateLimit
+ }
+}
+
+query IssueWithComments($owner: String!, $name: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ issue(number: $number) {
+ ...IssueWithComments
+ }
+ }
+ rateLimit {
+ ...RateLimit
+ }
+}
+
+query Issues($query: String!) {
+ search(first: 100, type: ISSUE, query: $query) {
+ issueCount
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ edges {
+ node {
+ ...IssueFragment
+ }
+ }
+ }
+ rateLimit {
+ ...RateLimit
}
}
@@ -138,7 +246,6 @@ query PullRequest($owner: String!, $name: String!, $number: Int!) {
}
}
-
query PullRequestForHead($owner: String!, $name: String!, $headRefName: String!) {
repository(owner: $owner, name: $name) {
pullRequests(first: 3, headRefName: $headRefName, orderBy: { field: CREATED_AT, direction: DESC }) {
diff --git a/src/github/queriesShared.gql b/src/github/queriesShared.gql
index 6850923267..0f2dcd25fa 100644
--- a/src/github/queriesShared.gql
+++ b/src/github/queriesShared.gql
@@ -536,102 +536,6 @@ query PullRequestFiles($owner: String!, $name: String!, $number: Int!, $after: S
}
}
-query Issue($owner: String!, $name: String!, $number: Int!) {
- repository(owner: $owner, name: $name) {
- pullRequest: issue(number: $number) {
- number
- url
- state
- body
- bodyHTML
- title
- author {
- ...User
- ...Organization
- }
- createdAt
- updatedAt
- assignees(first: 10) {
- nodes {
- ...User
- }
- }
- labels(first: 50) {
- nodes {
- name
- color
- }
- }
- id
- databaseId
- reactions(first: 100) {
- totalCount
- }
- comments(first: 1) {
- totalCount
- }
- }
- }
- rateLimit {
- ...RateLimit
- }
-}
-
-query IssueWithComments($owner: String!, $name: String!, $number: Int!) {
- repository(owner: $owner, name: $name) {
- pullRequest: issue(number: $number) {
- number
- url
- state
- body
- bodyHTML
- title
- author {
- ...User
- ...Organization
- }
- createdAt
- updatedAt
- labels(first: 50) {
- nodes {
- name
- color
- }
- }
- id
- databaseId
- comments(first: 50) {
- nodes {
- author {
- ...Node
- ...Actor
- ...User
- ...Organization
- }
- body
- databaseId
- reactions(first: 100) {
- totalCount
- }
- }
- totalCount
- }
- assignees(first: 10) {
- nodes {
- ...Node
- ...User
- }
- }
- reactions(first: 100) {
- totalCount
- }
- }
- }
- rateLimit {
- ...RateLimit
- }
-}
-
query GetUser($login: String!) {
user(login: $login) {
login
@@ -801,9 +705,20 @@ mutation DeleteReaction($input: RemoveReactionInput!) {
}
}
+mutation UpdateIssue($input: UpdateIssueInput!) {
+ updateIssue(input: $input) {
+ issue {
+ body
+ bodyHTML
+ title
+ titleHTML
+ }
+ }
+}
+
mutation UpdatePullRequest($input: UpdatePullRequestInput!) {
- updatePullRequest(input: $input) {
- pullRequest {
+ updateIssue: updatePullRequest(input: $input) {
+ issue: pullRequest {
body
bodyHTML
title
@@ -928,78 +843,6 @@ query GetMilestones($owner: String!, $name: String!, $states: [MilestoneState!]!
}
}
-query Issues($query: String!) {
- search(first: 100, type: ISSUE, query: $query) {
- issueCount
- pageInfo {
- hasNextPage
- endCursor
- }
- edges {
- node {
- ... on Issue {
- number
- url
- state
- body
- bodyHTML
- title
- assignees(first: 10) {
- nodes {
- ...User
- }
- }
- author {
- login
- url
- avatarUrl(size: 50)
- ... on User {
- email
- id
- }
- ... on Organization {
- email
- id
- }
- }
- createdAt
- updatedAt
- labels(first: 50) {
- nodes {
- name
- color
- }
- }
- id
- databaseId
- milestone {
- title
- dueOn
- id
- createdAt
- }
- repository {
- name
- owner {
- login
- }
- url
- }
- reactions(first: 1) {
- totalCount
- }
- comments(first: 1) {
- totalCount
- }
- }
- }
- }
- }
- rateLimit {
- ...RateLimit
- }
-}
-
query GetViewerPermission($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
viewerPermission
diff --git a/src/github/quickPicks.ts b/src/github/quickPicks.ts
index 701f8fb82b..1d8fa60338 100644
--- a/src/github/quickPicks.ts
+++ b/src/github/quickPicks.ts
@@ -12,7 +12,7 @@ import { formatError } from '../common/utils';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { GitHubRepository, TeamReviewerRefreshKind } from './githubRepository';
import { AccountType, IAccount, ILabel, IMilestone, IProject, isSuggestedReviewer, isTeam, ISuggestedReviewer, ITeam, reviewerId, ReviewState } from './interface';
-import { PullRequestModel } from './pullRequestModel';
+import { IssueModel } from './issueModel';
async function getItems(context: vscode.ExtensionContext, skipList: Set, users: T[], picked: boolean, tooManyAssignable: boolean = false): Promise<(vscode.QuickPickItem & { user?: T })[]> {
const alreadyAssignedItems: (vscode.QuickPickItem & { user?: T })[] = [];
@@ -53,7 +53,7 @@ async function getItems(context
return alreadyAssignedItems;
}
-export async function getAssigneesQuickPickItems(folderRepositoryManager: FolderRepositoryManager, gitHubRepository: GitHubRepository | undefined, remoteName: string, alreadyAssigned: IAccount[], item?: PullRequestModel, assignYourself?: boolean):
+export async function getAssigneesQuickPickItems(folderRepositoryManager: FolderRepositoryManager, gitHubRepository: GitHubRepository | undefined, remoteName: string, alreadyAssigned: IAccount[], item?: IssueModel, assignYourself?: boolean):
Promise<(vscode.QuickPickItem & { user?: IAccount })[]> {
const [allAssignableUsers, participantsAndViewer] = await Promise.all([
@@ -246,8 +246,10 @@ export async function getProjectFromQuickPick(folderRepoManager: FolderRepositor
quickPick.busy = true;
quickPick.canSelectMany = true;
quickPick.title = vscode.l10n.t('Set projects');
+ quickPick.ignoreFocusOut = true;
quickPick.show();
quickPick.items = await getProjectOptions();
+ quickPick.ignoreFocusOut = false;
if (quickPick.items.length === 1) {
quickPick.canSelectMany = false;
}
diff --git a/src/github/utils.ts b/src/github/utils.ts
index f902425fac..fb1c26dec7 100644
--- a/src/github/utils.ts
+++ b/src/github/utils.ts
@@ -50,7 +50,6 @@ import {
} from './interface';
import { IssueModel } from './issueModel';
import { GHPRComment, GHPRCommentThread } from './prComment';
-import { PullRequestModel } from './pullRequestModel';
export const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/;
export const ISSUE_OR_URL_EXPRESSION = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+)(#issuecomment\-([0-9]+))?)|(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/;
@@ -858,7 +857,7 @@ function parseComments(comments: GraphQL.AbbreviatedIssueComment[] | undefined,
return parsedComments;
}
-export function parseGraphQLIssue(issue: GraphQL.PullRequest, githubRepository: GitHubRepository): Issue {
+export function parseGraphQLIssue(issue: GraphQL.Issue, githubRepository: GitHubRepository): Issue {
return {
id: issue.databaseId,
graphNodeId: issue.id,
@@ -1182,7 +1181,7 @@ export function getRelatedUsersFromTimelineEvents(
});
}
- if (event.event === Common.EventType.Commented) {
+ if ((event.event === Common.EventType.Commented) && event.user) {
ret.push({
login: event.user.login,
name: event.user.name ?? event.user.login,
@@ -1513,7 +1512,7 @@ export async function findDotComAndEnterpriseRemotes(folderManagers: FolderRepos
return { dotComRemotes, enterpriseRemotes, unknownRemotes };
}
-export function vscodeDevPrLink(pullRequest: PullRequestModel) {
+export function vscodeDevPrLink(pullRequest: IssueModel) {
const itemUri = vscode.Uri.parse(pullRequest.html_url);
return `https://${vscode.env.appName.toLowerCase().includes('insider') ? 'insiders.' : ''}vscode.dev/github${itemUri.path}`;
}
diff --git a/src/github/views.ts b/src/github/views.ts
index 4bc82039f0..a3f04f815c 100644
--- a/src/github/views.ts
+++ b/src/github/views.ts
@@ -25,7 +25,7 @@ export enum ReviewType {
RequestChanges = 'requestChanges',
}
-export interface PullRequest {
+export interface Issue {
number: number;
title: string;
titleHTML: string;
@@ -34,20 +34,12 @@ export interface PullRequest {
body: string;
bodyHTML?: string;
author: IAccount;
- state: GithubItemStateEnum;
+ state: GithubItemStateEnum; // TODO: don't allow merged
events: TimelineEvent[];
- isCurrentlyCheckedOut: boolean;
- isRemoteBaseDeleted?: boolean;
- base: string;
- isRemoteHeadDeleted?: boolean;
- isLocalHeadDeleted?: boolean;
- head: string;
labels: ILabel[];
assignees: IAccount[];
- commitsCount: number;
projectItems: IProjectItem[] | undefined;
milestone: IMilestone | undefined;
- repositoryDefaultBranch: string;
/**
* User can edit PR title and description (author or user with push access)
*/
@@ -57,9 +49,27 @@ export interface PullRequest {
* edit title/description, assign reviewers/labels etc.
*/
hasWritePermission: boolean;
- emailForCommit?: string;
pendingCommentText?: string;
pendingCommentDrafts?: { [key: string]: string };
+ isIssue: boolean;
+ isAuthor?: boolean;
+ continueOnGitHub: boolean;
+ isDarkTheme: boolean;
+ isEnterprise: boolean;
+ busy?: boolean;
+}
+
+export interface PullRequest extends Issue {
+ isCurrentlyCheckedOut: boolean;
+ isRemoteBaseDeleted?: boolean;
+ base: string;
+ isRemoteHeadDeleted?: boolean;
+ isLocalHeadDeleted?: boolean;
+ head: string;
+ commitsCount: number;
+ projectItems: IProjectItem[] | undefined;
+ repositoryDefaultBranch: string;
+ emailForCommit?: string;
pendingReviewType?: ReviewType;
status: PullRequestChecks | null;
reviewRequirement: PullRequestReviewRequirement | null;
@@ -80,14 +90,8 @@ export interface PullRequest {
squashCommitMeta?: { title: string, description: string };
reviewers: ReviewState[];
isDraft?: boolean;
- isIssue: boolean;
- isAuthor?: boolean;
- continueOnGitHub: boolean;
- currentUserReviewState: string;
- isDarkTheme: boolean;
- isEnterprise: boolean;
+ currentUserReviewState?: string;
hasReviewDraft: boolean;
-
lastReviewType?: ReviewType;
revertable?: boolean;
busy?: boolean;
diff --git a/src/issues/issuesView.ts b/src/issues/issuesView.ts
index 4d1a817058..cc367e9442 100644
--- a/src/issues/issuesView.ts
+++ b/src/issues/issuesView.ts
@@ -79,6 +79,13 @@ export class IssuesTreeData
treeItem.iconPath = element.isOpen
? new vscode.ThemeIcon('issues', new vscode.ThemeColor('issues.open'))
: new vscode.ThemeIcon('issue-closed', new vscode.ThemeColor('issues.closed'));
+
+ treeItem.command = {
+ command: 'issue.openDescription',
+ title: vscode.l10n.t('View Issue Description'),
+ arguments: [element]
+ };
+
if (this.stateManager.currentIssue(element.uri)?.issue.number === element.number) {
treeItem.label = `✓ ${treeItem.label as string}`;
treeItem.contextValue = 'currentissue';
diff --git a/webviews/common/context.tsx b/webviews/common/context.tsx
index 3f436f5934..b710f68aa3 100644
--- a/webviews/common/context.tsx
+++ b/webviews/common/context.tsx
@@ -157,22 +157,26 @@ export class PRContext {
this.postMessage({ command: 'pr.apply-patch', args: { comment } });
};
- private appendReview({ review, reviewers }: { review?: ReviewEvent, reviewers?: ReviewState[] }) {
+ private appendReview({ event, reviewers }: { event?: ReviewEvent | TimelineEvent, reviewers?: ReviewState[] }) {
const state = this.pr;
state.busy = false;
- if (!review || !reviewers) {
+ if (!event) {
this.updatePR(state);
return;
}
- const events = state.events.filter(e => e.event !== EventType.Reviewed || e.state.toLowerCase() !== 'pending');
+ const events = state.events.filter(e => e.event !== EventType.Reviewed || e.state?.toLowerCase() !== 'pending');
events.forEach(event => {
if (event.event === EventType.Reviewed) {
event.comments.forEach(c => (c.isDraft = false));
}
});
- state.reviewers = reviewers;
- state.events = [...state.events.filter(e => (e.event === EventType.Reviewed ? e.state !== 'PENDING' : e)), review];
- state.currentUserReviewState = review.state;
+ if (reviewers) {
+ state.reviewers = reviewers;
+ }
+ state.events = [...state.events.filter(e => (e.event === EventType.Reviewed ? e.state !== 'PENDING' : e)), event];
+ if (event.event === EventType.Reviewed) {
+ state.currentUserReviewState = event.state;
+ }
state.pendingCommentText = '';
state.pendingReviewType = undefined;
this.updatePR(state);
diff --git a/webviews/components/comment.tsx b/webviews/components/comment.tsx
index cb908e760d..13cdee7977 100644
--- a/webviews/components/comment.tsx
+++ b/webviews/components/comment.tsx
@@ -320,15 +320,6 @@ export function AddComment({
textareaRef.current?.focus();
});
- const onKeyDown = useCallback(
- e => {
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
- submit(textareaRef.current?.value ?? '');
- }
- },
- [submit],
- );
-
const closeButton = e => {
e.preventDefault();
const { value } = textareaRef.current!;
@@ -357,6 +348,15 @@ export function AddComment({
setBusy(false);
}
+ const onKeyDown = useCallback(
+ e => {
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
+ submitAction(currentSelection);
+ }
+ },
+ [submit],
+ );
+
async function defaultSubmitAction(): Promise {
await submitAction(currentSelection);
}
@@ -369,7 +369,7 @@ export function AddComment({
[ReviewType.Approve]: 'Approve on github.com',
[ReviewType.RequestChanges]: 'Request changes on github.com',
}
- : COMMENT_METHODS;
+ : commentMethods(isIssue);
return (