Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/ezt/mr-bulk-approval-update/mr-bulk-approval-update.js b/static_src/elements/ezt/mr-bulk-approval-update/mr-bulk-approval-update.js
new file mode 100644
index 0000000..d9318fc
--- /dev/null
+++ b/static_src/elements/ezt/mr-bulk-approval-update/mr-bulk-approval-update.js
@@ -0,0 +1,283 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html} from 'lit-element';
+import 'elements/issue-detail/metadata/mr-edit-field/mr-edit-field.js';
+import 'elements/framework/mr-error/mr-error.js';
+import 'react/mr-react-autocomplete.tsx';
+import {prpcClient} from 'prpc-client-instance.js';
+import {EMPTY_FIELD_VALUE} from 'shared/issue-fields.js';
+import {TEXT_TO_STATUS_ENUM} from 'shared/consts/approval.js';
+
+
+export const NO_UPDATES_MESSAGE =
+ 'User lacks approver perms for approval in all issues.';
+export const NO_APPROVALS_MESSAGE = 'These issues don\'t have any approvals.';
+
+export class MrBulkApprovalUpdate extends LitElement {
+ /** @override */
+ render() {
+ return html`
+ <style>
+ mr-bulk-approval-update {
+ display: block;
+ margin-top: 30px;
+ position: relative;
+ }
+ button.clickable-text {
+ background: none;
+ border: 0;
+ color: hsl(0, 0%, 39%);
+ cursor: pointer;
+ text-decoration: underline;
+ }
+ .hidden {
+ display: none; !important;
+ }
+ .message {
+ background-color: beige;
+ width: 500px;
+ }
+ .note {
+ color: hsl(0, 0%, 25%);
+ font-size: 0.85em;
+ font-style: italic;
+ }
+ mr-bulk-approval-update table {
+ border: 1px dotted black;
+ cellspacing: 0;
+ cellpadding: 3;
+ }
+ #approversInput {
+ border-style: none;
+ }
+ </style>
+ <button
+ class="js-showApprovals clickable-text"
+ ?hidden=${this.approvalsFetched}
+ @click=${this.fetchApprovals}
+ >Show Approvals</button>
+ ${this.approvals.length ? html`
+ <form>
+ <table>
+ <tbody><tr>
+ <th><label for="approvalSelect">Approval:</label></th>
+ <td>
+ <select
+ id="approvalSelect"
+ @change=${this._changeHandlers.approval}
+ >
+ ${this.approvals.map(({fieldRef}) => html`
+ <option
+ value=${fieldRef.fieldName}
+ .selected=${fieldRef.fieldName === this._values.approval}
+ >
+ ${fieldRef.fieldName}
+ </option>
+ `)}
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <th><label for="approversInput">Approvers:</label></th>
+ <td>
+ <mr-react-autocomplete
+ label="approversInput"
+ vocabularyName="member"
+ .multiple=${true}
+ .value=${this._values.approvers}
+ .onChange=${this._changeHandlers.approvers}
+ ></mr-react-autocomplete>
+ </td>
+ </tr>
+ <tr><th><label for="statusInput">Status:</label></th>
+ <td>
+ <select
+ id="statusInput"
+ @change=${this._changeHandlers.status}
+ >
+ <option .selected=${!this._values.status}>
+ ${EMPTY_FIELD_VALUE}
+ </option>
+ ${this.statusOptions.map((status) => html`
+ <option
+ value=${status}
+ .selected=${status === this._values.status}
+ >${status}</option>
+ `)}
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <th><label for="commentText">Comment:</label></th>
+ <td colspan="4">
+ <textarea
+ cols="30"
+ rows="3"
+ id="commentText"
+ placeholder="Add an approval comment"
+ .value=${this._values.comment || ''}
+ @change=${this._changeHandlers.comment}
+ ></textarea>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <button
+ class="js-save"
+ @click=${this.save}
+ >Update Approvals only</button>
+ </td>
+ <td>
+ <span class="note">
+ Note: Some approvals may not be updated if you lack
+ approver perms.
+ </span>
+ </td>
+ </tr>
+ </tbody></table>
+ </form>
+ `: ''}
+ <div class="message">
+ ${this.responseMessage}
+ ${this.errorMessage ? html`
+ <mr-error>${this.errorMessage}</mr-error>
+ ` : ''}
+ </div>
+ `;
+ }
+
+ /** @override */
+ static get properties() {
+ return {
+ approvals: {type: Array},
+ approvalsFetched: {type: Boolean},
+ statusOptions: {type: Array},
+ localIdsStr: {type: String},
+ projectName: {type: String},
+ responseMessage: {type: String},
+ _values: {type: Object},
+ };
+ }
+
+ /** @override */
+ constructor() {
+ super();
+ this.approvals = [];
+ this.statusOptions = Object.keys(TEXT_TO_STATUS_ENUM);
+ this.responseMessage = '';
+
+ this._values = {};
+ this._changeHandlers = {
+ approval: this._onChange.bind(this, 'approval'),
+ approvers: this._onChange.bind(this, 'approvers'),
+ status: this._onChange.bind(this, 'status'),
+ comment: this._onChange.bind(this, 'comment'),
+ };
+ }
+
+ /** @override */
+ createRenderRoot() {
+ return this;
+ }
+
+ get issueRefs() {
+ const {projectName, localIdsStr} = this;
+ if (!projectName || !localIdsStr) return [];
+ const issueRefs = [];
+ const localIds = localIdsStr.split(',');
+ localIds.forEach((localId) => {
+ issueRefs.push({projectName: projectName, localId: localId});
+ });
+ return issueRefs;
+ }
+
+ fetchApprovals(evt) {
+ const message = {issueRefs: this.issueRefs};
+ prpcClient.call('monorail.Issues', 'ListApplicableFieldDefs', message).then(
+ (resp) => {
+ if (resp.fieldDefs) {
+ this.approvals = resp.fieldDefs.filter((fieldDef) => {
+ return fieldDef.fieldRef.type == 'APPROVAL_TYPE';
+ });
+ }
+ if (!this.approvals.length) {
+ this.errorMessage = NO_APPROVALS_MESSAGE;
+ }
+ this.approvalsFetched = true;
+ }, (error) => {
+ this.approvalsFetched = true;
+ this.errorMessage = error;
+ });
+ }
+
+ save(evt) {
+ this.responseMessage = '';
+ this.errorMessage = '';
+ this.toggleDisableForm();
+ const selectedFieldDef = this.approvals.find(
+ (approval) => approval.fieldRef.fieldName === this._values.approval
+ ) || this.approvals[0];
+ const message = {
+ issueRefs: this.issueRefs,
+ fieldRef: selectedFieldDef.fieldRef,
+ send_email: true,
+ };
+ message.commentContent = this._values.comment;
+ const delta = {};
+ if (this._values.status !== EMPTY_FIELD_VALUE) {
+ delta.status = TEXT_TO_STATUS_ENUM[this._values.status];
+ }
+ const approversAdded = this._values.approvers;
+ if (approversAdded) {
+ delta.approverRefsAdd = approversAdded.map(
+ (name) => ({'displayName': name}));
+ }
+ if (Object.keys(delta).length) {
+ message.approvalDelta = delta;
+ }
+ prpcClient.call('monorail.Issues', 'BulkUpdateApprovals', message).then(
+ (resp) => {
+ if (resp.issueRefs && resp.issueRefs.length) {
+ const idsStr = Array.from(resp.issueRefs,
+ (ref) => ref.localId).join(', ');
+ this.responseMessage = `${this.getTimeStamp()}: Updated ${
+ selectedFieldDef.fieldRef.fieldName} in issues: ${idsStr} (${
+ resp.issueRefs.length} of ${this.issueRefs.length}).`;
+ this._values = {};
+ } else {
+ this.errorMessage = NO_UPDATES_MESSAGE;
+ };
+ this.toggleDisableForm();
+ }, (error) => {
+ this.errorMessage = error;
+ this.toggleDisableForm();
+ });
+ }
+
+ getTimeStamp() {
+ const date = new Date();
+ return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
+ }
+
+ toggleDisableForm() {
+ this.querySelectorAll('input, textarea, select, button').forEach(
+ (input) => {
+ input.disabled = !input.disabled;
+ });
+ }
+
+ /**
+ * Generic onChange handler to be bound to each form field.
+ * @param {string} key Unique name for the form field we're binding this
+ * handler to. For example, 'owner', 'cc', or the name of a custom field.
+ * @param {Event | React.SyntheticEvent} event
+ * @param {string} value The new form value.
+ */
+ _onChange(key, event, value) {
+ this._values = {...this._values, [key]: value || event.target.value};
+ }
+}
+
+customElements.define('mr-bulk-approval-update', MrBulkApprovalUpdate);