Skip to content

Commit dfed4c4

Browse files
Copilotalexr00
andauthored
Add "Convert to draft" action in PR reviewers section (#8258)
* Initial plan * Initial plan for adding "Convert to draft" feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add convert to draft functionality - fix function replacement error Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Remove unrelated change from chatParticipantAdditions.d.ts Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Clean up * Change Convert to draft from button to link style Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Clean up --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 7886871 commit dfed4c4

File tree

9 files changed

+123
-2
lines changed

9 files changed

+123
-2
lines changed

src/github/graphql.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,16 @@ export interface MarkPullRequestReadyForReviewResponse {
506506
};
507507
}
508508

509+
export interface ConvertPullRequestToDraftResponse {
510+
convertPullRequestToDraft: {
511+
pullRequest: {
512+
isDraft: boolean;
513+
mergeable: 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN';
514+
mergeStateStatus: 'BEHIND' | 'BLOCKED' | 'CLEAN' | 'DIRTY' | 'HAS_HOOKS' | 'UNKNOWN' | 'UNSTABLE';
515+
};
516+
};
517+
}
518+
509519
export interface MergeQueueForBranchResponse {
510520
repository: {
511521
mergeQueue?: {

src/github/interface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export interface ReadyForReview {
5050
allowAutoMerge: boolean;
5151
}
5252

53+
export interface ConvertToDraft {
54+
isDraft: boolean;
55+
mergeable: PullRequestMergeability;
56+
}
57+
5358
export interface IActor {
5459
login: string;
5560
avatarUrl?: string;

src/github/pullRequestModel.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
AddReactionResponse,
2020
AddReviewRequestResponse as AddReviewsResponse,
2121
AddReviewThreadResponse,
22+
ConvertPullRequestToDraftResponse,
2223
DeleteReactionResponse,
2324
DeleteReviewResponse,
2425
DequeuePullRequestResponse,
@@ -45,6 +46,7 @@ import {
4546
} from './graphql';
4647
import {
4748
AccountType,
49+
ConvertToDraft,
4850
GithubItemStateEnum,
4951
IAccount,
5052
IGitTreeItem,
@@ -1846,6 +1848,44 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
18461848
}
18471849
}
18481850

1851+
/**
1852+
* Convert a pull request to draft.
1853+
*/
1854+
async convertToDraft(): Promise<ConvertToDraft> {
1855+
try {
1856+
const { mutate, schema } = await this.githubRepository.ensure();
1857+
1858+
const { data } = await mutate<ConvertPullRequestToDraftResponse>({
1859+
mutation: schema.ConvertToDraft,
1860+
variables: {
1861+
input: {
1862+
pullRequestId: this.graphNodeId,
1863+
},
1864+
},
1865+
});
1866+
1867+
/* __GDPR__
1868+
"pr.convertToDraft.success" : {}
1869+
*/
1870+
this._telemetry.sendTelemetryEvent('pr.convertToDraft.success');
1871+
1872+
const result: ConvertToDraft = {
1873+
isDraft: data!.convertPullRequestToDraft.pullRequest.isDraft,
1874+
mergeable: parseMergeability(data!.convertPullRequestToDraft.pullRequest.mergeable, data!.convertPullRequestToDraft.pullRequest.mergeStateStatus),
1875+
};
1876+
this.item.isDraft = result.isDraft;
1877+
this.item.mergeable = result.mergeable;
1878+
this._onDidChange.fire({ draft: true });
1879+
return result;
1880+
} catch (e) {
1881+
/* __GDPR__
1882+
"pr.convertToDraft.failure" : {}
1883+
*/
1884+
this._telemetry.sendTelemetryErrorEvent('pr.convertToDraft.failure');
1885+
throw e;
1886+
}
1887+
}
1888+
18491889
private updateCommentReactions(graphNodeId: string, reactionGroups: ReactionGroup[]) {
18501890
const reviewThread = this._reviewThreadsCache?.find(thread =>
18511891
thread.comments.some(c => c.graphNodeId === graphNodeId),

src/github/pullRequestOverview.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
393393
return this.setReadyForReview(message);
394394
case 'pr.readyForReviewAndMerge':
395395
return this.setReadyForReviewAndMerge(message);
396+
case 'pr.convertToDraft':
397+
return this.setConvertToDraft(message);
396398
case 'pr.approve':
397399
return this.approvePullRequestMessage(message);
398400
case 'pr.request-changes':
@@ -683,6 +685,10 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
683685
return PullRequestReviewCommon.setReadyForReviewAndMerge(this.getReviewContext(), message);
684686
}
685687

688+
private async setConvertToDraft(message: IRequestMessage<{}>): Promise<void> {
689+
return PullRequestReviewCommon.setConvertToDraft(this.getReviewContext(), message);
690+
}
691+
686692
private async readyForReviewCommand(): Promise<void> {
687693
return PullRequestReviewCommon.readyForReviewCommand(this.getReviewContext());
688694
}

src/github/pullRequestReviewCommon.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { FolderRepositoryManager } from './folderRepositoryManager';
99
import { IAccount, isITeam, ITeam, MergeMethod, PullRequestMergeability, reviewerId, ReviewState } from './interface';
1010
import { BranchInfo } from './pullRequestGitHelper';
1111
import { PullRequestModel } from './pullRequestModel';
12-
import { PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views';
12+
import { ConvertToDraftReply, PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views';
1313
import { DEFAULT_DELETION_METHOD, PR_SETTINGS_NAMESPACE, SELECT_LOCAL_BRANCH, SELECT_REMOTE } from '../common/settingKeys';
1414
import { ReviewEvent, TimelineEvent } from '../common/timelineEvent';
1515
import { Schemes } from '../common/uri';
@@ -230,6 +230,16 @@ export namespace PullRequestReviewCommon {
230230
}
231231
}
232232

233+
export async function setConvertToDraft(ctx: ReviewContext, _message: IRequestMessage<{}>): Promise<void> {
234+
try {
235+
const result: ConvertToDraftReply = await ctx.item.convertToDraft();
236+
ctx.replyMessage(_message, result);
237+
} catch (e) {
238+
vscode.window.showErrorMessage(vscode.l10n.t('Unable to convert pull request to draft. {0}', formatError(e)));
239+
ctx.throwError(_message, '');
240+
}
241+
}
242+
233243
export async function readyForReviewCommand(ctx: ReviewContext): Promise<void> {
234244
ctx.postMessage({
235245
command: 'pr.readying-for-review'

src/github/queriesShared.gql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,16 @@ mutation ReadyForReview($input: MarkPullRequestReadyForReviewInput!) {
740740
}
741741
}
742742

743+
mutation ConvertToDraft($input: ConvertPullRequestToDraftInput!) {
744+
convertPullRequestToDraft(input: $input) {
745+
pullRequest {
746+
isDraft
747+
mergeable
748+
mergeStateStatus
749+
}
750+
}
751+
}
752+
743753
mutation StartReview($input: AddPullRequestReviewInput!) {
744754
addPullRequestReview(input: $input) {
745755
pullRequestReview {

src/github/views.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ export interface ReadyForReviewReply {
138138
autoMerge?: boolean;
139139
}
140140

141+
export interface ConvertToDraftReply {
142+
isDraft: boolean;
143+
}
144+
141145
export interface MergeArguments {
142146
title: string | undefined;
143147
description: string | undefined;

webviews/common/context.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { CloseResult, OpenCommitChangesArgs } from '../../common/views';
1111
import { IComment } from '../../src/common/comment';
1212
import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../../src/common/timelineEvent';
1313
import { IProjectItem, MergeMethod, ReadyForReview } from '../../src/github/interface';
14-
import { CancelCodingAgentReply, ChangeAssigneesReply, DeleteReviewResult, MergeArguments, MergeResult, ProjectItemsReply, PullRequest, ReadyForReviewReply, SubmitReviewReply } from '../../src/github/views';
14+
import { CancelCodingAgentReply, ChangeAssigneesReply, ConvertToDraftReply, DeleteReviewResult, MergeArguments, MergeResult, ProjectItemsReply, PullRequest, ReadyForReviewReply, SubmitReviewReply } from '../../src/github/views';
1515

1616
export class PRContext {
1717
constructor(
@@ -92,6 +92,8 @@ export class PRContext {
9292

9393
public readyForReviewAndMerge = (args: { mergeMethod: MergeMethod }): Promise<ReadyForReview> => this.postMessage({ command: 'pr.readyForReviewAndMerge', args });
9494

95+
public convertToDraft = (): Promise<ConvertToDraftReply> => this.postMessage({ command: 'pr.convertToDraft' });
96+
9597
public addReviewers = () => this.postMessage({ command: 'pr.change-reviewers' });
9698
public addReviewerCopilot = () => this.postMessage({ command: 'pr.add-reviewer-copilot' });
9799
public changeProjects = (): Promise<ProjectItemsReply> => this.postMessage({ command: 'pr.change-projects' });

webviews/components/sidebar.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue
131131
) : (
132132
<div className="section-placeholder">None yet</div>
133133
)}
134+
{!pr!.isDraft && (hasWritePermission || pr!.isAuthor) && (
135+
<ConvertToDraft />
136+
)}
134137
</Section>
135138
)}
136139

@@ -543,3 +546,34 @@ function Project(project: IProjectItem & { canDelete: boolean }) {
543546
</div>
544547
);
545548
}
549+
550+
function ConvertToDraft() {
551+
const { convertToDraft, updatePR, pr } = useContext(PullRequestContext);
552+
const [isBusy, setBusy] = useState(false);
553+
554+
const handleConvertToDraft = async () => {
555+
try {
556+
setBusy(true);
557+
const result = await convertToDraft();
558+
updatePR({ isDraft: result.isDraft });
559+
} finally {
560+
setBusy(false);
561+
}
562+
};
563+
564+
return (
565+
<div className="section-placeholder" style={{ marginTop: '8px' }}>
566+
Still in progress?{' '}
567+
<a
568+
onClick={handleConvertToDraft}
569+
style={{
570+
pointerEvents: (isBusy || pr?.busy) ? 'none' : 'auto',
571+
opacity: (isBusy || pr?.busy) ? 0.5 : 1,
572+
cursor: 'pointer'
573+
}}
574+
>
575+
Convert to draft
576+
</a>
577+
</div>
578+
);
579+
}

0 commit comments

Comments
 (0)