Skip to content
15 changes: 14 additions & 1 deletion src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel';
import { PullRequestView } from './pullRequestOverviewCommon';
import { pickEmail, reviewersQuickPick } from './quickPicks';
import { parseReviewers } from './utils';
import { CancelCodingAgentReply, MergeArguments, MergeResult, PullRequest, ReviewType, SubmitReviewReply } from './views';
import { CancelCodingAgentReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType, SubmitReviewReply } from './views';

export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestModel> {
public static override ID: string = 'PullRequestOverviewPanel';
Expand Down Expand Up @@ -402,6 +402,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
return this.cancelCodingAgent(message);
case 'pr.openCommitChanges':
return this.openCommitChanges(message);
case 'pr.delete-review':
return this.deleteReview(message);
}
}

Expand Down Expand Up @@ -862,6 +864,17 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
return this._item.deleteReviewComment(comment.id.toString());
}

private async deleteReview(message: IRequestMessage<void>) {
try {
const result: DeleteReviewResult = await this._item.deleteReview();
await this._replyMessage(message, result);
} catch (e) {
Logger.error(formatError(e), PullRequestOverviewPanel.ID);
vscode.window.showErrorMessage(vscode.l10n.t('Deleting review failed. {0}', formatError(e)));
this._throwError(message, `${formatError(e)}`);
}
}

override dispose() {
super.dispose();
disposeAll(this._prListeners);
Expand Down
6 changes: 6 additions & 0 deletions src/github/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IComment } from '../common/comment';
import { CommentEvent, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../common/timelineEvent';
import {
GithubItemStateEnum,
Expand Down Expand Up @@ -138,6 +139,11 @@ export interface MergeResult {
events?: TimelineEvent[];
}

export interface DeleteReviewResult {
deletedReviewId: number;
deletedReviewComments: IComment[];
}

export enum PreReviewState {
None = 0,
Available,
Expand Down
2 changes: 1 addition & 1 deletion src/test/github/pullRequestGitHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe('PullRequestGitHelper', function () {
// Verify that the original local branch is preserved with its commit
const originalBranch = await repository.getBranch('my-branch');
assert.strictEqual(originalBranch.commit, 'local-commit-hash', 'Original branch should be preserved');

// Verify that a unique branch was created and checked out
const uniqueBranch = await repository.getBranch('pr/me/100');
assert.strictEqual(uniqueBranch.commit, 'remote-commit-hash', 'Unique branch should have remote commit');
Expand Down
4 changes: 2 additions & 2 deletions src/test/issues/stateManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { USE_BRANCH_FOR_ISSUES, ISSUES_SETTINGS_NAMESPACE } from '../../common/s

// Mock classes for testing
class MockFolderRepositoryManager {
constructor(public repository: { rootUri: vscode.Uri }) {}
constructor(public repository: { rootUri: vscode.Uri }) { }
}

class MockSingleRepoState {
currentIssue?: MockCurrentIssue;
constructor(public folderManager: MockFolderRepositoryManager) {}
constructor(public folderManager: MockFolderRepositoryManager) { }
}

class MockCurrentIssue {
Expand Down
26 changes: 25 additions & 1 deletion webviews/common/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CloseResult, OpenCommitChangesArgs } from '../../common/views';
import { IComment } from '../../src/common/comment';
import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../../src/common/timelineEvent';
import { IProjectItem, MergeMethod, ReadyForReview } from '../../src/github/interface';
import { CancelCodingAgentReply, ChangeAssigneesReply, MergeArguments, MergeResult, ProjectItemsReply, PullRequest, SubmitReviewReply } from '../../src/github/views';
import { CancelCodingAgentReply, ChangeAssigneesReply, DeleteReviewResult, MergeArguments, MergeResult, ProjectItemsReply, PullRequest, SubmitReviewReply } from '../../src/github/views';
import { getState, setState, updateState } from './cache';
import { getMessageHandler, MessageHandler } from './message';

Expand Down Expand Up @@ -158,6 +158,30 @@ export class PRContext {

public submit = (body: string) => this.submitReviewCommand('pr.submit', body);

public deleteReview = async () => {
try {
const result: DeleteReviewResult = await this.postMessage({ command: 'pr.delete-review' });

const state = this.pr;
const eventsWithoutPendingReview = state?.events.filter(event =>
!(event.event === EventType.Reviewed && event.id === result.deletedReviewId)
) ?? [];

if (state && (eventsWithoutPendingReview.length < state.events.length)) {
// Update the PR state to reflect the deleted review
state.busy = false;
state.pendingCommentText = '';
state.pendingCommentDrafts = {};
// Remove the deleted review from events
state.events = eventsWithoutPendingReview;
this.updatePR(state);
}
return result;
} catch (error) {
return this.updatePR({ busy: false });
}
};

public close = async (body?: string) => {
const { pr } = this;
if (!pr) {
Expand Down
17 changes: 16 additions & 1 deletion webviews/components/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ function CommentThread({ thread, event }: { thread: IComment[]; event: ReviewEve
}

function AddReviewSummaryComment() {
const { requestChanges, approve, submit, pr } = useContext(PullRequestContext);
const { requestChanges, approve, submit, deleteReview, pr } = useContext(PullRequestContext);
const isAuthor = pr?.isAuthor;
const comment = useRef<HTMLTextAreaElement>();
const [isBusy, setBusy] = useState(false);
Expand All @@ -292,6 +292,13 @@ function AddReviewSummaryComment() {
setBusy(false);
}

async function cancelReview(event: React.MouseEvent): Promise<void> {
event.preventDefault();
setBusy(true);
await deleteReview();
setBusy(false);
}

const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
submitAction(event, ReviewType.Comment);
Expand All @@ -317,6 +324,14 @@ function AddReviewSummaryComment() {
value={commentText}
></textarea>
<div className="form-actions">
<button
id="cancel-review"
className='secondary'
disabled={isBusy || pr?.busy}
onClick={cancelReview}
>
Cancel Review
</button>
{isAuthor ? null : (
<button
id="request-changes"
Expand Down