Confirm dialog for large groups in reviewer flow
This dialog matches the same confirm flow in the reply-dialog.
https://imgur.com/a/al7n41m
Release-Notes: skip
Change-Id: I11273b3872dada2fade2d7be822627c1c01c40f9
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index 34080a1..80606b1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -14,6 +14,7 @@
ChangeInfo,
NumericChangeId,
ServerInfo,
+ SuggestedReviewerGroupInfo,
} from '../../../types/common';
import {subscribe} from '../../lit/subscription-controller';
import '../../shared/gr-overlay/gr-overlay';
@@ -32,12 +33,13 @@
import {getDisplayName} from '../../../utils/display-name-util';
import {
AccountInput,
- AccountInputDetail,
+ GrAccountList,
} from '../../shared/gr-account-list/gr-account-list';
import '@polymer/iron-icon/iron-icon';
import {getReplyByReason} from '../../../utils/attention-set-util';
import {intersection} from '../../../utils/common-util';
import {accountOrGroupKey} from '../../../utils/account-util';
+import {ValueChangedEvent} from '../../../types/events';
@customElement('gr-change-list-reviewer-flow')
export class GrChangeListReviewerFlow extends LitElement {
@@ -66,7 +68,25 @@
@state() private serverConfig?: ServerInfo;
- @query('gr-overlay') private overlay!: GrOverlay;
+ @state()
+ private groupPendingConfirmationByReviewerState: Map<
+ ReviewerState,
+ SuggestedReviewerGroupInfo | null
+ > = new Map([
+ [ReviewerState.REVIEWER, null],
+ [ReviewerState.CC, null],
+ ]);
+
+ @query('gr-overlay#flow') private overlay?: GrOverlay;
+
+ @query('gr-account-list#reviewer-list') private reviewerList?: GrAccountList;
+
+ @query('gr-account-list#cc-list') private ccList?: GrAccountList;
+
+ @query('gr-overlay#confirm-reviewer')
+ private reviewerConfirmOverlay?: GrOverlay;
+
+ @query('gr-overlay#confirm-cc') private ccConfirmOverlay?: GrOverlay;
private readonly reportingService = getAppContext().reportingService;
@@ -113,6 +133,14 @@
--iron-icon-height: 18px;
--iron-icon-width: 18px;
}
+ gr-overlay#confirm-cc,
+ gr-overlay#confirm-reviewer {
+ padding: var(--spacing-l);
+ text-align: center;
+ }
+ .confirmation-buttons {
+ margin-top: var(--spacing-l);
+ }
`;
}
@@ -141,7 +169,6 @@
}
override render() {
- // TODO: factor out button+dialog component with promise-progress tracking
return html`
<gr-button
id="start-flow"
@@ -150,7 +177,7 @@
@click=${() => this.openOverlay()}
>add reviewer/cc</gr-button
>
- <gr-overlay with-backdrop>
+ <gr-overlay id="flow" with-backdrop>
${this.isOverlayOpen ? this.renderDialog() : nothing}
</gr-overlay>
`;
@@ -195,8 +222,6 @@
if (!updatedAccounts || !suggestionsProvider) {
return;
}
- // @accounts-changed will notify us when an account is added or removed, so
- // we need to re-render to update warning messages.
return html`
<gr-account-list
id=${id}
@@ -204,11 +229,48 @@
.removableValues=${[]}
.suggestionsProvider=${suggestionsProvider}
.placeholder=${placeholder}
- @accounts-changed=${() => this.requestUpdate()}
- @account-added=${(e: CustomEvent<AccountInputDetail>) =>
- this.onAccountAdded(reviewerState, e)}
+ .pendingConfirmation=${this.groupPendingConfirmationByReviewerState.get(
+ reviewerState
+ )}
+ @accounts-changed=${() => this.onAccountsChanged(reviewerState)}
+ @pending-confirmation-changed=${(
+ ev: ValueChangedEvent<SuggestedReviewerGroupInfo | null>
+ ) => this.onPendingConfirmationChanged(reviewerState, ev)}
>
</gr-account-list>
+ ${this.renderConfirmationDialog(reviewerState)}
+ `;
+ }
+
+ private renderConfirmationDialog(reviewerState: ReviewerState) {
+ const id =
+ reviewerState === ReviewerState.CC ? 'confirm-cc' : 'confirm-reviewer';
+ const suggestion =
+ this.groupPendingConfirmationByReviewerState.get(reviewerState);
+ return html`
+ <gr-overlay
+ id=${id}
+ @iron-overlay-canceled=${() => this.cancelPendingGroup(reviewerState)}
+ >
+ <div class="confirmation-text">
+ Group
+ <span class="groupName"> ${suggestion?.group.name} </span>
+ has
+ <span class="groupSize"> ${suggestion?.count} </span>
+ members.
+ <br />
+ Are you sure you want to add them all?
+ </div>
+ <div class="confirmation-buttons">
+ <gr-button
+ @click=${() => this.confirmPendingGroup(reviewerState, suggestion)}
+ >Yes</gr-button
+ >
+ <gr-button @click=${() => this.cancelPendingGroup(reviewerState)}
+ >No</gr-button
+ >
+ </div>
+ </gr-overlay>
`;
}
@@ -269,12 +331,12 @@
private openOverlay() {
this.resetFlow();
this.isOverlayOpen = true;
- this.overlay.open();
+ this.overlay?.open();
}
private closeOverlay() {
this.isOverlayOpen = false;
- this.overlay.close();
+ this.overlay?.close();
}
private resetFlow() {
@@ -299,11 +361,15 @@
this.requestUpdate();
}
- /* Removes accounts from one list when they are added to the other */
- private onAccountAdded(
- reviewerState: ReviewerState,
- event: CustomEvent<AccountInputDetail>
- ) {
+ /*
+ * Removes accounts from one list when they are added to the other. Also
+ * trigger re-render so warnings will update as accounts are added, removed,
+ * and confirmed.
+ */
+ private onAccountsChanged(reviewerState: ReviewerState) {
+ const reviewerStateKeys = this.updatedAccountsByReviewerState
+ .get(reviewerState)!
+ .map(accountOrGroupKey);
const oppositeReviewerState =
reviewerState === ReviewerState.CC
? ReviewerState.REVIEWER
@@ -311,13 +377,61 @@
const oppositeUpdatedAccounts = this.updatedAccountsByReviewerState.get(
oppositeReviewerState
)!;
- const oppositeUpdatedAccountIndex = oppositeUpdatedAccounts.findIndex(
- acc => accountOrGroupKey(acc) === accountOrGroupKey(event.detail.account)
+
+ const notOverwrittenOppositeAccounts = oppositeUpdatedAccounts.filter(
+ acc => !reviewerStateKeys.includes(accountOrGroupKey(acc))
);
- if (oppositeUpdatedAccountIndex >= 0) {
- oppositeUpdatedAccounts.splice(oppositeUpdatedAccountIndex, 1);
- this.requestUpdate();
+ if (
+ notOverwrittenOppositeAccounts.length !== oppositeUpdatedAccounts.length
+ ) {
+ this.updatedAccountsByReviewerState.set(
+ oppositeReviewerState,
+ notOverwrittenOppositeAccounts
+ );
}
+ this.requestUpdate();
+ }
+
+ private async onPendingConfirmationChanged(
+ reviewerState: ReviewerState,
+ ev: ValueChangedEvent<SuggestedReviewerGroupInfo | null>
+ ) {
+ this.groupPendingConfirmationByReviewerState.set(
+ reviewerState,
+ ev.detail.value
+ );
+ this.requestUpdate();
+ await this.updateComplete;
+
+ const overlay =
+ reviewerState === ReviewerState.CC
+ ? this.ccConfirmOverlay
+ : this.reviewerConfirmOverlay;
+ if (ev.detail.value === null) {
+ overlay?.close();
+ } else {
+ overlay?.open();
+ }
+ }
+
+ private cancelPendingGroup(reviewerState: ReviewerState) {
+ const overlay =
+ reviewerState === ReviewerState.CC
+ ? this.ccConfirmOverlay
+ : this.reviewerConfirmOverlay;
+ overlay?.close();
+ this.groupPendingConfirmationByReviewerState.set(reviewerState, null);
+ this.requestUpdate();
+ }
+
+ private confirmPendingGroup(
+ reviewerState: ReviewerState,
+ suggestion: SuggestedReviewerGroupInfo | null | undefined
+ ) {
+ if (!suggestion) return;
+ const accountList =
+ reviewerState === ReviewerState.CC ? this.ccList : this.reviewerList;
+ accountList?.confirmGroup(suggestion.group);
}
private onConfirm(overallStatus: ProgressStatus) {
@@ -326,10 +440,10 @@
this.saveReviewers();
break;
case ProgressStatus.SUCCESSFUL:
- this.overlay.close();
+ this.overlay?.close();
break;
case ProgressStatus.FAILED:
- this.overlay.close();
+ this.overlay?.close();
break;
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
index 4a142e4..9139c38 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
@@ -5,7 +5,12 @@
*/
import {fixture, html} from '@open-wc/testing-helpers';
import {SinonStubbedMember} from 'sinon';
-import {AccountInfo, GroupInfo, ReviewerState} from '../../../api/rest-api';
+import {
+ AccountInfo,
+ GroupId,
+ GroupInfo,
+ ReviewerState,
+} from '../../../api/rest-api';
import {
BulkActionsModel,
bulkActionsModelToken,
@@ -108,6 +113,7 @@
>add reviewer/cc</gr-button
>
<gr-overlay
+ id="flow"
aria-hidden="true"
with-backdrop=""
tabindex="-1"
@@ -184,6 +190,7 @@
>add reviewer/cc</gr-button
>
<gr-overlay
+ id="flow"
with-backdrop=""
tabindex="-1"
style="outline: none; display: none;"
@@ -194,8 +201,54 @@
<div class="grid">
<span>Reviewers</span>
<gr-account-list id="reviewer-list"></gr-account-list>
+ <gr-overlay
+ aria-hidden="true"
+ id="confirm-reviewer"
+ style="outline: none; display: none;"
+ >
+ <div class="confirmation-text">
+ Group
+ <span class="groupName"></span>
+ has
+ <span class="groupSize"></span>
+ members.
+ <br />
+ Are you sure you want to add them all?
+ </div>
+ <div class="confirmation-buttons">
+ <gr-button aria-disabled="false" role="button" tabindex="0">
+ Yes
+ </gr-button>
+ <gr-button aria-disabled="false" role="button" tabindex="0">
+ No
+ </gr-button>
+ </div>
+ </gr-overlay>
<span>CC</span>
<gr-account-list id="cc-list"></gr-account-list>
+ <gr-overlay
+ aria-hidden="true"
+ id="confirm-cc"
+ style="outline: none; display: none;"
+ >
+ <div class="confirmation-text">
+ Group
+ <span class="groupName"></span>
+ has
+ <span class="groupSize"></span>
+ members.
+ <br />
+ Are you sure you want to add them all?
+ </div>
+ <div class="confirmation-buttons">
+ <gr-button aria-disabled="false" role="button" tabindex="0">
+ Yes
+ </gr-button>
+ <gr-button aria-disabled="false" role="button" tabindex="0">
+ No
+ </gr-button>
+ </div>
+ </gr-overlay>
</div>
</div>
</gr-dialog>
@@ -385,7 +438,7 @@
);
await flush();
- // prettier and shadowDom string don't agree on long text in divs
+ // prettier and shadowDom string don't agree on the long text in divs
expect(element).shadowDom.to.equal(
/* prettier-ignore */
/* HTML */ `
@@ -397,15 +450,65 @@
tabindex="0"
>add reviewer/cc</gr-button
>
- <gr-overlay with-backdrop="" tabindex="-1">
+ <gr-overlay id="flow" with-backdrop="" tabindex="-1">
<gr-dialog role="dialog">
<div slot="header">Add reviewer / CC</div>
<div slot="main">
<div class="grid">
<span>Reviewers</span>
<gr-account-list id="reviewer-list"></gr-account-list>
+ <gr-overlay aria-hidden="true" id="confirm-reviewer">
+ <div class="confirmation-text">
+ Group
+ <span class="groupName"></span>
+ has
+ <span class="groupSize"></span>
+ members.
+ <br>
+ Are you sure you want to add them all?
+ </div>
+ <div class="confirmation-buttons">
+ <gr-button
+ aria-disabled="false"
+ role="button"
+ tabindex="0">
+ Yes
+ </gr-button>
+ <gr-button
+ aria-disabled="false"
+ role="button"
+ tabindex="0">
+ No
+ </gr-button>
+ </div>
+ </gr-overlay>
<span>CC</span>
<gr-account-list id="cc-list"></gr-account-list>
+ <gr-overlay aria-hidden="true" id="confirm-cc">
+ <div class="confirmation-text">
+ Group
+ <span class="groupName"></span>
+ has
+ <span class="groupSize"></span>
+ members.
+ <br>
+ Are you sure you want to add them all?
+ </div>
+ <div class="confirmation-buttons">
+ <gr-button
+ aria-disabled="false"
+ role="button"
+ tabindex="0">
+ Yes
+ </gr-button>
+ <gr-button
+ aria-disabled="false"
+ role="button"
+ tabindex="0">
+ No
+ </gr-button>
+ </div>
+ </gr-overlay>
</div>
<div class="warning">
<iron-icon icon="gr-icons:warning"></iron-icon>
@@ -429,5 +532,154 @@
}
);
});
+
+ test('shows confirmation dialog when large group is added', async () => {
+ const reviewerList = queryAndAssert<GrAccountList>(
+ dialog,
+ 'gr-account-list#reviewer-list'
+ );
+ reviewerList.handleAdd(
+ new CustomEvent('add', {
+ detail: {
+ value: {
+ group: {
+ id: '5',
+ name: 'large-group',
+ },
+ count: 12,
+ confirm: true,
+ },
+ },
+ }) as unknown as ValueChangedEvent<string>
+ );
+ await flush();
+
+ const confirmDialog = queryAndAssert(
+ element,
+ 'gr-overlay#confirm-reviewer'
+ );
+ assert.isTrue(
+ getComputedStyle(confirmDialog).getPropertyValue('display') !== 'none'
+ );
+ });
+
+ test('confirms large group', async () => {
+ const reviewerList = queryAndAssert<GrAccountList>(
+ dialog,
+ 'gr-account-list#reviewer-list'
+ );
+ reviewerList.handleAdd(
+ new CustomEvent('add', {
+ detail: {
+ value: {
+ group: {
+ id: '5',
+ name: 'large-group',
+ },
+ count: 12,
+ confirm: true,
+ },
+ },
+ }) as unknown as ValueChangedEvent<string>
+ );
+ await flush();
+ // "Yes" button is first
+ queryAndAssert<GrButton>(
+ element,
+ '.confirmation-buttons > gr-button:first-of-type'
+ ).click();
+ await flush();
+
+ const confirmDialog = queryAndAssert(
+ element,
+ 'gr-overlay#confirm-reviewer'
+ );
+ assert.isTrue(
+ getComputedStyle(confirmDialog).getPropertyValue('display') === 'none'
+ );
+ assert.deepEqual(reviewerList.accounts[1], {
+ _group: true,
+ _pendingAdd: true,
+ confirmed: true,
+ id: '5' as GroupId,
+ name: 'large-group',
+ });
+ });
+
+ test('no confirmation dialog for small group', async () => {
+ const reviewerList = queryAndAssert<GrAccountList>(
+ dialog,
+ 'gr-account-list#reviewer-list'
+ );
+ // "confirm" field is used to decide whether to use the confirmation flow,
+ // not the count. "confirm" value comes from server based on count
+ // threshold
+ reviewerList.handleAdd(
+ new CustomEvent('add', {
+ detail: {
+ value: {
+ group: {
+ id: '5',
+ name: 'small-group',
+ },
+ count: 2,
+ confirm: false,
+ },
+ },
+ }) as unknown as ValueChangedEvent<string>
+ );
+ await flush();
+ const confirmDialog = queryAndAssert(
+ element,
+ 'gr-overlay#confirm-reviewer'
+ );
+ assert.isTrue(
+ getComputedStyle(confirmDialog).getPropertyValue('display') === 'none'
+ );
+ assert.deepEqual(reviewerList.accounts[1], {
+ _group: true,
+ _pendingAdd: true,
+ id: '5' as GroupId,
+ name: 'small-group',
+ });
+ });
+
+ test('no button cancels large group', async () => {
+ const reviewerList = queryAndAssert<GrAccountList>(
+ dialog,
+ 'gr-account-list#reviewer-list'
+ );
+ reviewerList.handleAdd(
+ new CustomEvent('add', {
+ detail: {
+ value: {
+ group: {
+ id: '5',
+ name: 'large-group',
+ },
+ count: 12,
+ confirm: true,
+ },
+ },
+ }) as unknown as ValueChangedEvent<string>
+ );
+ await flush();
+ // "No" button is last
+ queryAndAssert<GrButton>(
+ element,
+ '.confirmation-buttons > gr-button:last-of-type'
+ ).click();
+ await flush();
+
+ const confirmDialog = queryAndAssert(
+ element,
+ 'gr-overlay#confirm-reviewer'
+ );
+ assert.isTrue(
+ getComputedStyle(confirmDialog).getPropertyValue('display') === 'none'
+ );
+ // Group not present
+ assert.sameDeepMembers(reviewerList.accounts, [accounts[0]]);
+ });
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 6a9bd92..3fd7c89 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -1276,6 +1276,8 @@
return selectorEl?.selectedValue;
}
+ // TODO: Combine logic into handleReviewersChanged & handleCCsChanged and
+ // remove account-added event from GrAccountList.
accountAdded(e: CustomEvent<AccountInputDetail>) {
const account = e.detail.account;
const key = accountOrGroupKey(account);