blob: 458fe9a96526b49c577efa6b4cb65e6915e6710c [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {css, html, LitElement, nothing} from 'lit';
import {customElement, query, state} from 'lit/decorators';
import {ProgressStatus, ReviewerState} from '../../../constants/constants';
import {bulkActionsModelToken} from '../../../models/bulk-actions/bulk-actions-model';
import {resolve} from '../../../models/dependency';
import {AccountInfo, ChangeInfo, NumericChangeId} from '../../../types/common';
import {subscribe} from '../../lit/subscription-controller';
import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-dialog/gr-dialog';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {getAppContext} from '../../../services/app-context';
import {
GrReviewerSuggestionsProvider,
ReviewerSuggestionsProvider,
SUGGESTIONS_PROVIDERS_USERS_TYPES,
} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import '../../shared/gr-account-list/gr-account-list';
import {getOverallStatus} from '../../../utils/bulk-flow-util';
const SUGGESTIONS_PROVIDERS_USERS_TYPES_BY_REVIEWER_STATE: Record<
ReviewerState,
SUGGESTIONS_PROVIDERS_USERS_TYPES
> = {
REVIEWER: SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER,
CC: SUGGESTIONS_PROVIDERS_USERS_TYPES.CC,
REMOVED: SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY,
};
@customElement('gr-change-list-reviewer-flow')
export class GrChangeListReviewerFlow extends LitElement {
@state() private selectedChanges: ChangeInfo[] = [];
// contents are given to gr-account-lists to mutate
@state() private updatedAccountsByReviewerState: Map<
ReviewerState,
AccountInfo[]
> = new Map();
@state() private suggestionsProviderByReviewerState: Map<
ReviewerState,
ReviewerSuggestionsProvider
> = new Map();
@state() private progressByChangeNum = new Map<
NumericChangeId,
ProgressStatus
>();
@state() private isOverlayOpen = false;
@query('gr-overlay') private overlay!: GrOverlay;
private getBulkActionsModel = resolve(this, bulkActionsModelToken);
private restApiService = getAppContext().restApiService;
static override get styles() {
return css`
gr-dialog {
width: 60em;
}
.grid {
display: grid;
grid-template-columns: min-content 1fr;
column-gap: var(--spacing-l);
}
gr-account-list {
display: flex;
flex-wrap: wrap;
}
`;
}
override connectedCallback(): void {
super.connectedCallback();
subscribe(
this,
this.getBulkActionsModel().selectedChanges$,
selectedChanges => {
this.selectedChanges = selectedChanges;
}
);
}
override render() {
// TODO: factor out button+dialog component with promise-progress tracking
return html`
<gr-button
id="start-flow"
.disabled=${this.isFlowDisabled()}
flatten
@click=${() => this.openOverlay()}
>add reviewer/cc</gr-button
>
<gr-overlay with-backdrop>
${this.isOverlayOpen ? this.renderDialog() : nothing}
</gr-overlay>
`;
}
private renderDialog() {
const overallStatus = getOverallStatus(this.progressByChangeNum);
return html`
<gr-dialog
@cancel=${() => this.closeOverlay()}
@confirm=${() => this.onConfirm(overallStatus)}
.confirmLabel=${this.getConfirmLabel(overallStatus)}
.disabled=${overallStatus === ProgressStatus.RUNNING}
>
<div slot="header">Add Reviewer / CC</div>
<div slot="main" class="grid">
<span>Reviewers</span>
${this.renderAccountList(
ReviewerState.REVIEWER,
'reviewer-list',
'Add reviewer'
)}
<span>CC</span>
${this.renderAccountList(ReviewerState.CC, 'cc-list', 'Add CC')}
</div>
</gr-dialog>
`;
}
private renderAccountList(
reviewerState: ReviewerState,
id: string,
placeholder: string
) {
const updatedAccounts =
this.updatedAccountsByReviewerState.get(reviewerState);
const suggestionsProvider =
this.suggestionsProviderByReviewerState.get(reviewerState);
if (!updatedAccounts || !suggestionsProvider) {
return;
}
return html`
<gr-account-list
id=${id}
.accounts=${updatedAccounts}
.removableValues=${[]}
.suggestionsProvider=${suggestionsProvider}
.placeholder=${placeholder}
>
</gr-account-list>
`;
}
private openOverlay() {
this.resetFlow();
this.isOverlayOpen = true;
this.overlay.open();
}
private closeOverlay() {
this.isOverlayOpen = false;
this.overlay.close();
}
private resetFlow() {
this.progressByChangeNum = new Map(
this.selectedChanges.map(change => [
change._number,
ProgressStatus.NOT_STARTED,
])
);
for (const state of [ReviewerState.REVIEWER, ReviewerState.CC]) {
this.updatedAccountsByReviewerState.set(
state,
this.getCurrentAccounts(state)
);
if (this.selectedChanges.length > 0) {
this.suggestionsProviderByReviewerState.set(
state,
this.createSuggestionsProvider(state)
);
}
}
this.requestUpdate();
}
private onConfirm(overallStatus: ProgressStatus) {
switch (overallStatus) {
case ProgressStatus.NOT_STARTED:
this.saveReviewers();
break;
case ProgressStatus.SUCCESSFUL:
this.overlay.close();
break;
case ProgressStatus.FAILED:
this.overlay.close();
break;
}
}
private saveReviewers() {
this.progressByChangeNum = new Map(
this.selectedChanges.map(change => [
change._number,
ProgressStatus.RUNNING,
])
);
const inFlightActions = this.getBulkActionsModel().addReviewers(
this.updatedAccountsByReviewerState
);
for (let index = 0; index < this.selectedChanges.length; index++) {
const change = this.selectedChanges[index];
inFlightActions[index]
.then(() => {
this.progressByChangeNum.set(
change._number,
ProgressStatus.SUCCESSFUL
);
this.requestUpdate();
})
.catch(() => {
this.progressByChangeNum.set(change._number, ProgressStatus.FAILED);
this.requestUpdate();
});
}
}
private isFlowDisabled() {
// No additional checks are necessary. If the user has visibility enough to
// see the change, they have permission enough to add reviewers/cc.
return this.selectedChanges.length === 0;
}
private getConfirmLabel(overallStatus: ProgressStatus) {
return overallStatus === ProgressStatus.NOT_STARTED
? 'Add'
: overallStatus === ProgressStatus.RUNNING
? 'Running'
: 'Close';
}
private getCurrentAccounts(reviewerState: ReviewerState) {
const reviewersPerChange = this.selectedChanges.map(
change => change.reviewers[reviewerState] ?? []
);
if (reviewersPerChange.length === 0) {
return [];
}
// Gets reviewers present in all changes
return reviewersPerChange.reduce((a, b) =>
a.filter(reviewer => b.includes(reviewer))
);
}
private createSuggestionsProvider(
state: ReviewerState
): ReviewerSuggestionsProvider {
const suggestionsProvider = GrReviewerSuggestionsProvider.create(
this.restApiService,
// TODO: fan out and get suggestions allowed by all changes
this.selectedChanges[0]._number,
SUGGESTIONS_PROVIDERS_USERS_TYPES_BY_REVIEWER_STATE[state]
);
suggestionsProvider.init();
return suggestionsProvider;
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-change-list-reviewer-flow': GrChangeListReviewerFlow;
}
}