Skip to content

Commit d2e04d2

Browse files
Copilotalexr00
andauthored
Fix review button disable logic with special handling for approve button (#7507)
* Initial plan * Fix review button disable logic - prevent empty review submission Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use hasReviewDraft in PR overview * Apply same disable logic to AddComment components in comment.tsx Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Allow approve button even with empty content and no pending review Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Handle disablement in dropdown button * nit * Fix test --------- 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 3872678 commit d2e04d2

File tree

6 files changed

+78
-41
lines changed

6 files changed

+78
-41
lines changed

package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,12 +1343,14 @@
13431343
{
13441344
"command": "review.comment",
13451345
"title": "%command.review.comment.title%",
1346-
"category": "%command.pull.request.category%"
1346+
"category": "%command.pull.request.category%",
1347+
"enablement": "github:reviewCommentCommentEnabled"
13471348
},
13481349
{
13491350
"command": "review.requestChanges",
13501351
"title": "%command.review.requestChanges.title%",
1351-
"category": "%command.pull.request.category%"
1352+
"category": "%command.pull.request.category%",
1353+
"enablement": "github:reviewRequestChangesEnabled"
13521354
},
13531355
{
13541356
"command": "review.approveOnDotCom",
@@ -1368,12 +1370,14 @@
13681370
{
13691371
"command": "review.commentDescription",
13701372
"title": "%command.review.comment.title%",
1371-
"category": "%command.pull.request.category%"
1373+
"category": "%command.pull.request.category%",
1374+
"enablement": "github:reviewCommentCommentEnabled"
13721375
},
13731376
{
13741377
"command": "review.requestChangesDescription",
13751378
"title": "%command.review.requestChanges.title%",
1376-
"category": "%command.pull.request.category%"
1379+
"category": "%command.pull.request.category%",
1380+
"enablement": "github:reviewRequestChangesEnabled"
13771381
},
13781382
{
13791383
"command": "review.approveOnDotComDescription",

src/github/pullRequestOverview.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
236236
mergeability,
237237
emailForCommit,
238238
coAuthors,
239+
hasReviewDraft
239240
] = await Promise.all([
240241
this._folderRepositoryManager.resolvePullRequest(
241242
pullRequestModel.remote.owner,
@@ -255,7 +256,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
255256
this._folderRepositoryManager.isHeadUpToDateWithBase(pullRequestModel),
256257
pullRequestModel.getMergeability(),
257258
this._folderRepositoryManager.getPreferredEmail(pullRequestModel),
258-
pullRequestModel.getCoAuthors()
259+
pullRequestModel.getCoAuthors(),
260+
pullRequestModel.validateDraftMode(),
259261
]);
260262
if (!pullRequest) {
261263
throw new Error(
@@ -300,6 +302,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
300302
isDraft: pullRequest.isDraft,
301303
mergeMethodsAvailability,
302304
defaultMergeMethod,
305+
hasReviewDraft,
303306
autoMerge: pullRequest.autoMerge,
304307
allowAutoMerge: pullRequest.allowAutoMerge,
305308
autoMergeMethod: pullRequest.autoMergeMethod,

src/test/github/pullRequestOverview.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ describe('PullRequestOverview', function () {
119119
const resolveStub = sinon.stub(pullRequestManager, 'resolvePullRequest').resolves(prModel0);
120120
sinon.stub(prModel0, 'getReviewRequests').resolves([]);
121121
sinon.stub(prModel0, 'getTimelineEvents').resolves([]);
122+
sinon.stub(prModel0, 'validateDraftMode').resolves(true);
122123
sinon.stub(prModel0, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]);
123124
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel0);
124125

@@ -132,6 +133,7 @@ describe('PullRequestOverview', function () {
132133
resolveStub.resolves(prModel1);
133134
sinon.stub(prModel1, 'getReviewRequests').resolves([]);
134135
sinon.stub(prModel1, 'getTimelineEvents').resolves([]);
136+
sinon.stub(prModel1, 'validateDraftMode').resolves(true);
135137
sinon.stub(prModel1, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]);
136138
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel1);
137139

webviews/components/comment.tsx

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function CommentView(commentProps: Props) {
4444
const [bodyMd, setBodyMd] = useStateProp(body);
4545
const [bodyHTMLState, setBodyHtml] = useStateProp(bodyHTML);
4646
const { deleteComment, editComment, setDescription, pr } = useContext(PullRequestContext);
47-
const currentDraft = pr.pendingCommentDrafts && pr.pendingCommentDrafts[id];
47+
const currentDraft = pr?.pendingCommentDrafts && pr.pendingCommentDrafts[id];
4848
const [inEditMode, setEditMode] = useState(!!currentDraft);
4949
const [showActionBar, setShowActionBar] = useState(false);
5050

@@ -55,7 +55,7 @@ export function CommentView(commentProps: Props) {
5555
key={`editComment${id}`}
5656
body={currentDraft || bodyMd}
5757
onCancel={() => {
58-
if (pr.pendingCommentDrafts) {
58+
if (pr?.pendingCommentDrafts) {
5959
delete pr.pendingCommentDrafts[id];
6060
}
6161
setEditMode(false);
@@ -114,7 +114,7 @@ export function CommentView(commentProps: Props) {
114114
comment={comment as IComment}
115115
bodyHTML={bodyHTMLState}
116116
body={bodyMd}
117-
canApplyPatch={pr.isCurrentlyCheckedOut}
117+
canApplyPatch={!!pr?.isCurrentlyCheckedOut}
118118
allowEmpty={!!commentProps.allowEmpty}
119119
specialDisplayBodyPostfix={(comment as IComment).specialDisplayBodyPostfix}
120120
/>
@@ -349,6 +349,7 @@ export function AddComment({
349349
currentUserReviewState,
350350
lastReviewType,
351351
busy,
352+
hasReviewDraft,
352353
}: PullRequest) {
353354
const { updatePR, requestChanges, approve, close, openOnGitHub, submit } = useContext(PullRequestContext);
354355
const [isBusy, setBusy] = useState(false);
@@ -413,8 +414,13 @@ export function AddComment({
413414
}
414415
: commentMethods(isIssue);
415416

417+
// Disable buttons when summary comment is empty AND there are no review comments
418+
// Note: Approve button is allowed even with empty content and no pending review
419+
const shouldDisableNonApproveButtons = !pendingCommentText?.trim() && !hasReviewDraft;
420+
const shouldDisableApproveButton = false; // Approve is always allowed (when not busy)
421+
416422
return (
417-
<form id="comment-form" ref={form as React.MutableRefObject<HTMLFormElement>} className="comment-form main-comment-form" onSubmit={() => submit(textareaRef.current?.value ?? '')}>
423+
<form id="comment-form" ref={form as React.MutableRefObject<HTMLFormElement>} className="comment-form main-comment-form" >
418424
<textarea
419425
id="comment-textarea"
420426
name="body"
@@ -445,20 +451,20 @@ export function AddComment({
445451

446452

447453
<ContextDropdown
448-
optionsContext={() => makeCommentMenuContext(availableActions, pendingCommentText)}
454+
optionsContext={() => makeCommentMenuContext(availableActions, pendingCommentText, shouldDisableNonApproveButtons)}
449455
defaultAction={defaultSubmitAction}
450456
defaultOptionLabel={() => availableActions[currentSelection]!}
451457
defaultOptionValue={() => currentSelection}
452458
allOptions={() => {
453-
const actions: { label: string; value: string; action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void }[] = [];
459+
const actions: { label: string; value: string; optionDisabled: boolean; action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void }[] = [];
454460
if (availableActions.approve) {
455-
actions.push({ label: availableActions[ReviewType.Approve]!, value: ReviewType.Approve, action: () => submitAction(ReviewType.Approve) });
461+
actions.push({ label: availableActions[ReviewType.Approve]!, value: ReviewType.Approve, action: () => submitAction(ReviewType.Approve), optionDisabled: shouldDisableApproveButton });
456462
}
457463
if (availableActions.comment) {
458-
actions.push({ label: availableActions[ReviewType.Comment]!, value: ReviewType.Comment, action: () => submitAction(ReviewType.Comment) });
464+
actions.push({ label: availableActions[ReviewType.Comment]!, value: ReviewType.Comment, action: () => submitAction(ReviewType.Comment), optionDisabled: shouldDisableNonApproveButtons });
459465
}
460466
if (availableActions.requestChanges) {
461-
actions.push({ label: availableActions[ReviewType.RequestChanges]!, value: ReviewType.RequestChanges, action: () => submitAction(ReviewType.RequestChanges) });
467+
actions.push({ label: availableActions[ReviewType.RequestChanges]!, value: ReviewType.RequestChanges, action: () => submitAction(ReviewType.RequestChanges), optionDisabled: shouldDisableNonApproveButtons });
462468
}
463469
return actions;
464470
}}
@@ -486,7 +492,7 @@ const COMMENT_METHODS = {
486492
requestChanges: 'Request Changes',
487493
};
488494

489-
const makeCommentMenuContext = (availableActions: { comment?: string, approve?: string, requestChanges?: string }, pendingCommentText: string | undefined) => {
495+
const makeCommentMenuContext = (availableActions: { comment?: string, approve?: string, requestChanges?: string }, pendingCommentText: string | undefined, shouldDisableNonApproveButtons: boolean) => {
490496
const createMenuContexts = {
491497
'preventDefaultContextMenuItems': true,
492498
'github:reviewCommentMenu': true,
@@ -500,10 +506,16 @@ const makeCommentMenuContext = (availableActions: { comment?: string, approve?:
500506
}
501507
if (availableActions.comment) {
502508
createMenuContexts['github:reviewCommentComment'] = true;
509+
if (!shouldDisableNonApproveButtons) {
510+
createMenuContexts['github:reviewCommentCommentEnabled'] = true;
511+
}
503512
}
504513
if (availableActions.requestChanges) {
505514
if (availableActions.requestChanges === COMMENT_METHODS.requestChanges) {
506515
createMenuContexts['github:reviewCommentRequestChanges'] = true;
516+
if (!shouldDisableNonApproveButtons) {
517+
createMenuContexts['github:reviewRequestChangesEnabled'] = true;
518+
}
507519
} else {
508520
createMenuContexts['github:reviewCommentRequestChangesOnDotCom'] = true;
509521
}
@@ -568,6 +580,11 @@ export const AddCommentSimple = (pr: PullRequest) => {
568580
}
569581
: commentMethods(pr.isIssue);
570582

583+
// Disable buttons when summary comment is empty AND there are no review comments
584+
// Note: Approve button is allowed even with empty content and no pending review
585+
const shouldDisableNonApproveButtons = !pr.pendingCommentText?.trim() && !pr.hasReviewDraft;
586+
const shouldDisableApproveButton = false; // Approve is always allowed (when not busy)
587+
571588
return (
572589
<span className="comment-form">
573590
<textarea
@@ -582,20 +599,20 @@ export const AddCommentSimple = (pr: PullRequest) => {
582599
/>
583600
<div className='comment-button'>
584601
<ContextDropdown
585-
optionsContext={() => makeCommentMenuContext(availableActions, pr.pendingCommentText)}
602+
optionsContext={() => makeCommentMenuContext(availableActions, pr.pendingCommentText, shouldDisableNonApproveButtons)}
586603
defaultAction={defaultSubmitAction}
587604
defaultOptionLabel={() => availableActions[currentSelection]!}
588605
defaultOptionValue={() => currentSelection}
589606
allOptions={() => {
590-
const actions: { label: string; value: string; action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void }[] = [];
607+
const actions: { label: string; value: string; optionDisabled: boolean; action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void }[] = [];
591608
if (availableActions.approve) {
592-
actions.push({ label: availableActions[ReviewType.Approve]!, value: ReviewType.Approve, action: () => submitAction(ReviewType.Approve) });
609+
actions.push({ label: availableActions[ReviewType.Approve]!, value: ReviewType.Approve, action: () => submitAction(ReviewType.Approve), optionDisabled: shouldDisableApproveButton });
593610
}
594611
if (availableActions.comment) {
595-
actions.push({ label: availableActions[ReviewType.Comment]!, value: ReviewType.Comment, action: () => submitAction(ReviewType.Comment) });
612+
actions.push({ label: availableActions[ReviewType.Comment]!, value: ReviewType.Comment, action: () => submitAction(ReviewType.Comment), optionDisabled: shouldDisableNonApproveButtons });
596613
}
597614
if (availableActions.requestChanges) {
598-
actions.push({ label: availableActions[ReviewType.RequestChanges]!, value: ReviewType.RequestChanges, action: () => submitAction(ReviewType.RequestChanges) });
615+
actions.push({ label: availableActions[ReviewType.RequestChanges]!, value: ReviewType.RequestChanges, action: () => submitAction(ReviewType.RequestChanges), optionDisabled: shouldDisableNonApproveButtons });
599616
}
600617
return actions;
601618
}}

webviews/components/contextDropdown.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
77
import { chevronDownIcon } from './icon';
88

9-
interface ContextDropdownProps {
10-
optionsContext: () => string;
11-
defaultOptionLabel: () => string | React.ReactNode;
12-
defaultOptionValue: () => string;
13-
defaultAction: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
14-
allOptions?: () => { label: string; value: string; action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void }[];
15-
optionsTitle: string;
16-
disabled?: boolean;
17-
hasSingleAction?: boolean;
18-
spreadable: boolean;
19-
isSecondary?: boolean;
9+
interface ContextDropdownProps {
10+
optionsContext: () => string;
11+
defaultOptionLabel: () => string | React.ReactNode;
12+
defaultOptionValue: () => string;
13+
defaultAction: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
14+
allOptions?: () => { label: string; value: string; action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void; optionDisabled?: boolean }[];
15+
optionsTitle: string;
16+
disabled?: boolean;
17+
hasSingleAction?: boolean;
18+
spreadable: boolean;
19+
isSecondary?: boolean;
2020
}
2121

2222
function useWindowSize() {
@@ -55,14 +55,14 @@ export const ContextDropdown = ({ optionsContext, defaultOptionLabel, defaultOpt
5555
useWindowSize();
5656

5757
return <div className={`dropdown-container${spreadable ? ' spreadable' : ''}`} ref={divRef}>
58-
{divRef.current && spreadable && (divRef.current.clientWidth > 375) && options && !hasSingleAction ? options().map(({ label, value, action }) => {
59-
return <button className='inlined-dropdown' key={value} title={label} disabled={disabled} onClick={action} value={value}>{label}</button>;
58+
{divRef.current && spreadable && (divRef.current.clientWidth > 375) && options && !hasSingleAction ? options().map(({ label, value, action, optionDisabled }) => {
59+
return <button className='inlined-dropdown' key={value} title={label} disabled={optionDisabled || disabled} onClick={action} value={value}>{label}</button>;
6060
})
6161
:
62-
<div className='primary-split-button'>
63-
<button className={`split-left${isSecondary ? ' secondary' : ''}`} disabled={disabled} onClick={defaultAction} value={defaultOptionValue()}
64-
title={typeof defaultOptionLabel() === 'string' ? defaultOptionLabel() as string : optionsTitle}>
65-
{defaultOptionLabel()}
62+
<div className='primary-split-button'>
63+
<button className={`split-left${isSecondary ? ' secondary' : ''}`} disabled={disabled} onClick={defaultAction} value={defaultOptionValue()}
64+
title={typeof defaultOptionLabel() === 'string' ? defaultOptionLabel() as string : optionsTitle}>
65+
{defaultOptionLabel()}
6666
</button>
6767
{hasSingleAction ? null :
6868
<div className={`split${isSecondary ? ' secondary' : ''}${disabled ? ' disabled' : ''}`}><div className={`separator${disabled ? ' disabled' : ''}`}></div></div>

webviews/components/timeline.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,14 @@ function CommentThread({ thread, event }: { thread: IComment[]; event: ReviewEve
270270

271271
function AddReviewSummaryComment() {
272272
const { requestChanges, approve, submit, pr } = useContext(PullRequestContext);
273-
const { isAuthor } = pr;
273+
const isAuthor = pr?.isAuthor;
274274
const comment = useRef<HTMLTextAreaElement>();
275275
const [isBusy, setBusy] = useState(false);
276+
const [commentText, setCommentText] = useState('');
276277

277278
async function submitAction(event: React.MouseEvent | React.KeyboardEvent, action: ReviewType): Promise<void> {
278279
event.preventDefault();
279-
const { value } = comment.current!;
280+
const value = commentText;
280281
setBusy(true);
281282
switch (action) {
282283
case ReviewType.RequestChanges:
@@ -297,20 +298,30 @@ function AddReviewSummaryComment() {
297298
}
298299
};
299300

301+
const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
302+
setCommentText(event.target.value);
303+
};
304+
305+
// Disable buttons when summary comment is empty AND there are no review comments
306+
// Note: Approve button is allowed even with empty content and no pending review
307+
const shouldDisableButtons = !commentText.trim() && !pr.hasReviewDraft;
308+
300309
return (
301310
<form>
302311
<textarea
303312
id='pending-review'
304313
ref={comment}
305314
placeholder="Leave a review summary comment"
306315
onKeyDown={onKeyDown}
316+
onChange={onTextareaChange}
317+
value={commentText}
307318
></textarea>
308319
<div className="form-actions">
309320
{isAuthor ? null : (
310321
<button
311322
id="request-changes"
312323
className='secondary'
313-
disabled={isBusy || pr.busy}
324+
disabled={isBusy || pr.busy || shouldDisableButtons}
314325
onClick={(event) => submitAction(event, ReviewType.RequestChanges)}
315326
>
316327
Request Changes
@@ -326,7 +337,7 @@ function AddReviewSummaryComment() {
326337
</button>
327338
)}
328339
<button
329-
disabled={isBusy || pr.busy}
340+
disabled={isBusy || pr.busy || shouldDisableButtons}
330341
onClick={(event) => submitAction(event, ReviewType.Comment)}
331342
>Submit Review</button>
332343
</div>

0 commit comments

Comments
 (0)