From ace4f07052b0f74b17d48fb34b7968fdce938f6c Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:24:51 +0200 Subject: [PATCH 1/7] Revive issue editor --- package.json | 5 + package.nls.json | 1 + src/commands.ts | 117 +++++++------- src/github/folderRepositoryManager.ts | 4 +- src/github/githubRepository.ts | 5 +- src/github/graphql.ts | 85 +++++++---- src/github/issueModel.ts | 58 +++++-- src/github/issueOverview.ts | 212 ++++++++++++++++++++++---- src/github/notifications.ts | 2 +- src/github/pullRequestModel.ts | 50 +++--- src/github/pullRequestOverview.ts | 185 +++------------------- src/github/queriesShared.gql | 17 ++- src/github/quickPicks.ts | 4 +- src/github/utils.ts | 5 +- src/github/views.ts | 38 ++--- src/issues/issuesView.ts | 7 + 16 files changed, 459 insertions(+), 336 deletions(-) diff --git a/package.json b/package.json index b75be349dc..e4b46f81a6 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%", 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/src/commands.ts b/src/commands.ts index bfb7381c26..17f127f84c 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,59 @@ 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); - }, - ), + await openDescription(telemetry, issueModel, descriptionNode, folderManager, !(argument instanceof DescriptionNode), !(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 +1474,7 @@ ${contents} vscode.env.openExternal(getPullsUrl(githubRepo)); } })); + context.subscriptions.push( vscode.commands.registerCommand('issues.openIssuesWebsite', async () => { const githubRepo = await chooseRepoToOpen(); 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..03bce406c7 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -461,6 +461,17 @@ export interface UpdatePullRequestResponse { }; } +export interface UpdateIssueResponse { + updateIssue: { + issue: { + body: string; + bodyHTML: string; + title: string; + titleHTML: string; + }; + }; +} + export interface AddPullRequestToProjectResponse { addProjectV2ItemById: { item: { @@ -522,12 +533,13 @@ 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 +548,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 +589,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 +642,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/issueModel.ts b/src/github/issueModel.ts index 0881fc5050..dee08220ff 100644 --- a/src/github/issueModel.ts +++ b/src/github/issueModel.ts @@ -15,6 +15,7 @@ import { AddPullRequestToProjectResponse, EditIssueCommentResponse, TimelineEventsResponse, + UpdateIssueResponse, UpdatePullRequestResponse, } from './graphql'; import { GithubItemStateEnum, IAccount, IMilestone, IProject, IProjectItem, IPullRequestEditData, Issue } from './interface'; @@ -157,24 +158,24 @@ export class IssueModel { try { const { mutate, schema } = await this.githubRepository.ensure(); - const { data } = await mutate({ - mutation: schema.UpdatePullRequest, + const { data } = await mutate({ + mutation: schema.UpdateIssue, variables: { input: { - pullRequestId: this.graphNodeId, + id: 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 +341,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: schema.UpdateIssue, + variables: { + input: { + id: this.item.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..eb316c3ece 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 { 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 => { @@ -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: @@ -309,6 +360,111 @@ 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) { + 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 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); } 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..9d4318d561 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -56,6 +56,7 @@ import { GithubItemStateEnum, IAccount, IGitTreeItem, + IPullRequestEditData, IRawFileChange, IRawFileContent, ISuggestedReviewer, @@ -306,6 +307,33 @@ export class PullRequestModel extends IssueModel implements IPullRe return false; } + override async edit(toEdit: IPullRequestEditData): Promise<{ body: string; bodyHTML: string; title: string; titleHTML: string }> { + try { + const { mutate, schema } = await this.githubRepository.ensure(); + + const { data } = await mutate({ + mutation: schema.UpdatePullRequest, + variables: { + input: { + id: 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; + this.invalidate(); + } + return data!.updatePullRequest.pullRequest; + } catch (e) { + throw new Error(formatError(e)); + } + } + /** * Approve the pull request. * @param message Optional approval comment text. @@ -443,7 +471,7 @@ export class PullRequestModel extends IssueModel implements IPullRe } } - async updateMilestone(id: string): Promise { + override async updateMilestone(id: string): Promise { const { mutate, schema } = await this.githubRepository.ensure(); const finalId = id === 'null' ? null : id; @@ -462,16 +490,6 @@ export class PullRequestModel extends IssueModel implements IPullRe } } - 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 +1029,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..5984d18770 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; @@ -834,14 +693,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/queriesShared.gql b/src/github/queriesShared.gql index 6850923267..ed9d7002db 100644 --- a/src/github/queriesShared.gql +++ b/src/github/queriesShared.gql @@ -538,13 +538,14 @@ 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) { + issue(number: $number) { number url state body bodyHTML title + titleHTML author { ...User ...Organization @@ -579,13 +580,14 @@ query Issue($owner: String!, $name: String!, $number: Int!) { query IssueWithComments($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { - pullRequest: issue(number: $number) { + issue(number: $number) { number url state body bodyHTML title + titleHTML author { ...User ...Organization @@ -801,6 +803,17 @@ mutation DeleteReaction($input: RemoveReactionInput!) { } } +mutation UpdateIssue($input: UpdateIssueInput!) { + updateIssue(input: $input) { + issue { + body + bodyHTML + title + titleHTML + } + } +} + mutation UpdatePullRequest($input: UpdatePullRequestInput!) { updatePullRequest(input: $input) { pullRequest { diff --git a/src/github/quickPicks.ts b/src/github/quickPicks.ts index 701f8fb82b..d08c7f192a 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([ diff --git a/src/github/utils.ts b/src/github/utils.ts index f902425fac..09004d41aa 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, @@ -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..abed39d848 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; hasReviewDraft: boolean; - lastReviewType?: ReviewType; revertable?: boolean; busy?: boolean; diff --git a/src/issues/issuesView.ts b/src/issues/issuesView.ts index 4d1a817058..984a605def 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: 'Open Issue Description', + arguments: [element] + }; + if (this.stateManager.currentIssue(element.uri)?.issue.number === element.number) { treeItem.label = `✓ ${treeItem.label as string}`; treeItem.contextValue = 'currentissue'; From 3a0f606e48b21a0b68b721fe1092cee88ab09d66 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:25:33 +0200 Subject: [PATCH 2/7] Consolidate UpdateIssue --- src/github/interface.ts | 2 +- src/github/issueModel.ts | 23 ++++++++++----- src/github/pullRequestModel.ts | 52 +++++----------------------------- src/github/queriesShared.gql | 4 +-- 4 files changed, 26 insertions(+), 55 deletions(-) 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 dee08220ff..81b6370902 100644 --- a/src/github/issueModel.ts +++ b/src/github/issueModel.ts @@ -16,9 +16,8 @@ import { EditIssueCommentResponse, TimelineEventsResponse, UpdateIssueResponse, - UpdatePullRequestResponse, } 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 { @@ -154,15 +153,25 @@ 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.UpdateIssue, + mutation: this.updateIssueSchema(schema), variables: { input: { - id: this.graphNodeId, + ...this.updateIssueInput(this.graphNodeId), body: toEdit.body, title: toEdit.title, }, @@ -348,10 +357,10 @@ export class IssueModel { try { await mutate({ - mutation: schema.UpdateIssue, + mutation: this.updateIssueSchema(schema), variables: { input: { - id: this.item.graphNodeId, + ...this.updateIssueInput(this.graphNodeId), milestoneId: finalId, }, }, diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index 9d4318d561..f3d22e5725 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -49,14 +49,12 @@ import { SubmitReviewResponse, TimelineEventsResponse, UnresolveReviewThreadResponse, - UpdatePullRequestResponse, } from './graphql'; import { AccountType, GithubItemStateEnum, IAccount, IGitTreeItem, - IPullRequestEditData, IRawFileChange, IRawFileContent, ISuggestedReviewer, @@ -307,31 +305,14 @@ export class PullRequestModel extends IssueModel implements IPullRe return false; } - override async edit(toEdit: IPullRequestEditData): Promise<{ body: string; bodyHTML: string; title: string; titleHTML: string }> { - try { - const { mutate, schema } = await this.githubRepository.ensure(); + protected override updateIssueInput(id: string): Object { + return { + pullRequestId: id, + }; + } - const { data } = await mutate({ - mutation: schema.UpdatePullRequest, - variables: { - input: { - id: 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; - this.invalidate(); - } - return data!.updatePullRequest.pullRequest; - } catch (e) { - throw new Error(formatError(e)); - } + protected override updateIssueSchema(schema: any): any { + return schema.UpdatePullRequest; } /** @@ -471,25 +452,6 @@ export class PullRequestModel extends IssueModel implements IPullRe } } - override 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); - } - } - /** * Query to see if there is an existing review. */ diff --git a/src/github/queriesShared.gql b/src/github/queriesShared.gql index ed9d7002db..d2e83c1532 100644 --- a/src/github/queriesShared.gql +++ b/src/github/queriesShared.gql @@ -815,8 +815,8 @@ mutation UpdateIssue($input: UpdateIssueInput!) { } mutation UpdatePullRequest($input: UpdatePullRequestInput!) { - updatePullRequest(input: $input) { - pullRequest { + updateIssue: updatePullRequest(input: $input) { + issue: pullRequest { body bodyHTML title From 5cfc371d375e51ae43f6d37b8a2f44052280482d Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:33:00 +0200 Subject: [PATCH 3/7] Fix milestone not showing on issue --- src/github/queriesShared.gql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/github/queriesShared.gql b/src/github/queriesShared.gql index d2e83c1532..01a966154e 100644 --- a/src/github/queriesShared.gql +++ b/src/github/queriesShared.gql @@ -552,6 +552,13 @@ query Issue($owner: String!, $name: String!, $number: Int!) { } createdAt updatedAt + milestone { + title + dueOn + createdAt + id + number + } assignees(first: 10) { nodes { ...User @@ -618,6 +625,13 @@ query IssueWithComments($owner: String!, $name: String!, $number: Int!) { } totalCount } + milestone { + title + dueOn + createdAt + id + number + } assignees(first: 10) { nodes { ...Node From 70a91adfae0444f07b96f3c0c9f31eb77f024e3e Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:29:58 +0200 Subject: [PATCH 4/7] Fix projects for issues and fix description revealin --- src/commands.ts | 4 +- src/github/issueOverview.ts | 11 +- src/github/queries.gql | 166 ++++++++++++++++++++++++----- src/github/queriesExtra.gql | 194 +++++++++++++++++++++++++++------- src/github/queriesLimited.gql | 163 +++++++++++++++++++++++----- src/github/queriesShared.gql | 184 -------------------------------- src/github/quickPicks.ts | 2 + 7 files changed, 443 insertions(+), 281 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 17f127f84c..3f9624daaf 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -848,7 +848,9 @@ export function registerCommands( descriptionNode = reviewManager.changesInPrDataProvider.getDescriptionNode(folderManager); } - await openDescription(telemetry, issueModel, 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( diff --git a/src/github/issueOverview.ts b/src/github/issueOverview.ts index eb316c3ece..2d0932a5e1 100644 --- a/src/github/issueOverview.ts +++ b/src/github/issueOverview.ts @@ -427,13 +427,14 @@ export class IssueOverviewPanel extends W } private async updateProjects(projects: IProject[] | undefined, message: IRequestMessage) { + let newProjects: IProjectItem[] = []; if (projects) { - const newProjects = await this._item.updateProjects(projects); - const projectItemsReply: ProjectItemsReply = { - projectItems: newProjects, - }; - return this._replyMessage(message, projectItemsReply); + newProjects = (await this._item.updateProjects(projects)) ?? []; } + const projectItemsReply: ProjectItemsReply = { + projectItems: newProjects, + }; + return this._replyMessage(message, projectItemsReply); } private async removeProject(message: IRequestMessage): Promise { 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 01a966154e..0f2dcd25fa 100644 --- a/src/github/queriesShared.gql +++ b/src/github/queriesShared.gql @@ -536,118 +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) { - issue(number: $number) { - 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 - } - } - } - rateLimit { - ...RateLimit - } -} - -query IssueWithComments($owner: String!, $name: String!, $number: Int!) { - repository(owner: $owner, name: $name) { - issue(number: $number) { - number - url - state - body - bodyHTML - title - titleHTML - 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 - } - milestone { - title - dueOn - createdAt - id - number - } - assignees(first: 10) { - nodes { - ...Node - ...User - } - } - reactions(first: 100) { - totalCount - } - } - } - rateLimit { - ...RateLimit - } -} - query GetUser($login: String!) { user(login: $login) { login @@ -955,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 d08c7f192a..1d8fa60338 100644 --- a/src/github/quickPicks.ts +++ b/src/github/quickPicks.ts @@ -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; } From 3be5653f90b6096ce905a98fd94d057146118d24 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:44:27 +0200 Subject: [PATCH 5/7] Fix subtitle --- resources/icons/issue.svg | 1 + resources/icons/issue_closed.svg | 1 + webviews/components/header.tsx | 27 +++++++++++++++------------ webviews/components/icon.tsx | 2 ++ 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 resources/icons/issue.svg create mode 100644 resources/icons/issue_closed.svg 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/webviews/components/header.tsx b/webviews/components/header.tsx index 7d11346c48..364881e702 100644 --- a/webviews/components/header.tsx +++ b/webviews/components/header.tsx @@ -8,7 +8,7 @@ import { GithubItemStateEnum } from '../../src/github/interface'; import { PullRequest } from '../../src/github/views'; import PullRequestContext from '../common/context'; import { useStateProp } from '../common/hooks'; -import { checkIcon, mergeIcon, prClosedIcon, prDraftIcon, prOpenIcon } from './icon'; +import { checkIcon, issueClosedIcon, issueIcon, mergeIcon, prClosedIcon, prDraftIcon, prOpenIcon } from './icon'; import { nbsp } from './space'; import { AuthorLink, Avatar } from './user'; @@ -123,22 +123,22 @@ function ButtonGroup({ isCurrentlyCheckedOut, canEdit, isIssue, repositoryDefaul } function Subtitle({ state, isDraft, isIssue, author, base, head }) { - const { text, color, icon } = getStatus(state, isDraft); + const { text, color, icon } = getStatus(state, isDraft, isIssue); return (
- {isIssue ? null : icon} + {icon} {text}
- {!isIssue ? : null} - {!isIssue ? ( -
- {getActionText(state)} into{' '} + {} +
+ {!isIssue ? (<> + {getActionText(state)} into{' '} {base} from {head} -
- ) : null} + ) : null} +
); @@ -201,13 +201,16 @@ const CheckoutButtons = ({ isCurrentlyCheckedOut, isIssue, repositoryDefaultBran } }; -export function getStatus(state: GithubItemStateEnum, isDraft: boolean) { +export function getStatus(state: GithubItemStateEnum, isDraft: boolean, isIssue: boolean) { + const closed = isIssue ? issueClosedIcon : prClosedIcon; + const open = isIssue ? issueIcon : prOpenIcon; + if (state === GithubItemStateEnum.Merged) { return { text: 'Merged', color: 'merged', icon: mergeIcon }; } else if (state === GithubItemStateEnum.Open) { - return isDraft ? { text: 'Draft', color: 'draft', icon: prDraftIcon } : { text: 'Open', color: 'open', icon: prOpenIcon }; + return isDraft ? { text: 'Draft', color: 'draft', icon: prDraftIcon } : { text: 'Open', color: 'open', icon: open }; } else { - return { text: 'Closed', color: 'closed', icon: prClosedIcon }; + return { text: 'Closed', color: 'closed', icon: closed }; } } diff --git a/webviews/components/icon.tsx b/webviews/components/icon.tsx index 20699ff318..b0cc36d770 100644 --- a/webviews/components/icon.tsx +++ b/webviews/components/icon.tsx @@ -43,3 +43,5 @@ export const milestoneIcon = ; export const sparkleIcon = ; export const stopCircleIcon = ; +export const issueIcon = ; +export const issueClosedIcon = ; From 133117ffd3dedeb84a6f5534aef5f14ae0cdc888 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:32:34 +0200 Subject: [PATCH 6/7] Fix comment button --- src/common/timelineEvent.ts | 2 +- src/github/issueOverview.ts | 20 +++++++++++++------ src/github/pullRequestOverview.ts | 4 +--- src/github/utils.ts | 2 +- src/github/views.ts | 2 +- webviews/common/context.tsx | 16 ++++++++++------ webviews/components/comment.tsx | 32 +++++++++++++++++++------------ 7 files changed, 48 insertions(+), 30 deletions(-) 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/issueOverview.ts b/src/github/issueOverview.ts index 2d0932a5e1..71073a96cb 100644 --- a/src/github/issueOverview.ts +++ b/src/github/issueOverview.ts @@ -9,7 +9,7 @@ import { openPullRequestOnGitHub } from '../commands'; import { IComment } from '../common/comment'; import Logger from '../common/logger'; import { ITelemetry } from '../common/telemetry'; -import { TimelineEvent } from '../common/timelineEvent'; +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'; @@ -235,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; @@ -280,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 { @@ -520,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/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 5984d18770..7c8bb40f9c 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -345,8 +345,6 @@ 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)); } diff --git a/src/github/utils.ts b/src/github/utils.ts index 09004d41aa..fb1c26dec7 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -1181,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, diff --git a/src/github/views.ts b/src/github/views.ts index abed39d848..a3f04f815c 100644 --- a/src/github/views.ts +++ b/src/github/views.ts @@ -90,7 +90,7 @@ export interface PullRequest extends Issue { squashCommitMeta?: { title: string, description: string }; reviewers: ReviewState[]; isDraft?: boolean; - currentUserReviewState: string; + currentUserReviewState?: string; hasReviewDraft: boolean; lastReviewType?: ReviewType; revertable?: boolean; 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 (
} className="comment-form main-comment-form" onSubmit={() => submit(textareaRef.current?.value ?? '')}> @@ -423,8 +423,16 @@ export function AddComment({ ); } -const COMMENT_METHODS = { +function commentMethods(isIssue: boolean) { + return isIssue ? ISSUE_COMMENT_METHODS : COMMENT_METHODS; +} + +const ISSUE_COMMENT_METHODS = { comment: 'Comment', +}; + +const COMMENT_METHODS = { + ...ISSUE_COMMENT_METHODS, approve: 'Approve', requestChanges: 'Request Changes', }; @@ -509,7 +517,7 @@ export const AddCommentSimple = (pr: PullRequest) => { approve: 'Approve on github.com', requestChanges: 'Request changes on github.com', } - : COMMENT_METHODS; + : commentMethods(pr.isIssue); return ( From de0498aeecb7fa7d8ed17b194706627d1bc60f53 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 8 Apr 2025 16:32:38 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 ++++ src/github/graphql.ts | 12 ------------ src/issues/issuesView.ts | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index e4b46f81a6..825fc2ff95 100644 --- a/package.json +++ b/package.json @@ -2075,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/src/github/graphql.ts b/src/github/graphql.ts index 03bce406c7..1cbf16630b 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -450,17 +450,6 @@ export interface DeleteReactionResponse { }; } -export interface UpdatePullRequestResponse { - updatePullRequest: { - pullRequest: { - body: string; - bodyHTML: string; - title: string; - titleHTML: string; - }; - }; -} - export interface UpdateIssueResponse { updateIssue: { issue: { @@ -533,7 +522,6 @@ export interface SuggestedReviewerResponse { export type MergeMethod = 'MERGE' | 'REBASE' | 'SQUASH'; export type MergeQueueState = 'AWAITING_CHECKS' | 'LOCKED' | 'MERGEABLE' | 'QUEUED' | 'UNMERGEABLE'; - export interface Issue { id: string; databaseId: number; diff --git a/src/issues/issuesView.ts b/src/issues/issuesView.ts index 984a605def..cc367e9442 100644 --- a/src/issues/issuesView.ts +++ b/src/issues/issuesView.ts @@ -82,7 +82,7 @@ export class IssuesTreeData treeItem.command = { command: 'issue.openDescription', - title: 'Open Issue Description', + title: vscode.l10n.t('View Issue Description'), arguments: [element] };