blob: d9318fc823065d87c5f49fa715275f17ba874883 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {LitElement, html} from 'lit-element';
6import 'elements/issue-detail/metadata/mr-edit-field/mr-edit-field.js';
7import 'elements/framework/mr-error/mr-error.js';
8import 'react/mr-react-autocomplete.tsx';
9import {prpcClient} from 'prpc-client-instance.js';
10import {EMPTY_FIELD_VALUE} from 'shared/issue-fields.js';
11import {TEXT_TO_STATUS_ENUM} from 'shared/consts/approval.js';
12
13
14export const NO_UPDATES_MESSAGE =
15 'User lacks approver perms for approval in all issues.';
16export const NO_APPROVALS_MESSAGE = 'These issues don\'t have any approvals.';
17
18export class MrBulkApprovalUpdate extends LitElement {
19 /** @override */
20 render() {
21 return html`
22 <style>
23 mr-bulk-approval-update {
24 display: block;
25 margin-top: 30px;
26 position: relative;
27 }
28 button.clickable-text {
29 background: none;
30 border: 0;
31 color: hsl(0, 0%, 39%);
32 cursor: pointer;
33 text-decoration: underline;
34 }
35 .hidden {
36 display: none; !important;
37 }
38 .message {
39 background-color: beige;
40 width: 500px;
41 }
42 .note {
43 color: hsl(0, 0%, 25%);
44 font-size: 0.85em;
45 font-style: italic;
46 }
47 mr-bulk-approval-update table {
48 border: 1px dotted black;
49 cellspacing: 0;
50 cellpadding: 3;
51 }
52 #approversInput {
53 border-style: none;
54 }
55 </style>
56 <button
57 class="js-showApprovals clickable-text"
58 ?hidden=${this.approvalsFetched}
59 @click=${this.fetchApprovals}
60 >Show Approvals</button>
61 ${this.approvals.length ? html`
62 <form>
63 <table>
64 <tbody><tr>
65 <th><label for="approvalSelect">Approval:</label></th>
66 <td>
67 <select
68 id="approvalSelect"
69 @change=${this._changeHandlers.approval}
70 >
71 ${this.approvals.map(({fieldRef}) => html`
72 <option
73 value=${fieldRef.fieldName}
74 .selected=${fieldRef.fieldName === this._values.approval}
75 >
76 ${fieldRef.fieldName}
77 </option>
78 `)}
79 </select>
80 </td>
81 </tr>
82 <tr>
83 <th><label for="approversInput">Approvers:</label></th>
84 <td>
85 <mr-react-autocomplete
86 label="approversInput"
87 vocabularyName="member"
88 .multiple=${true}
89 .value=${this._values.approvers}
90 .onChange=${this._changeHandlers.approvers}
91 ></mr-react-autocomplete>
92 </td>
93 </tr>
94 <tr><th><label for="statusInput">Status:</label></th>
95 <td>
96 <select
97 id="statusInput"
98 @change=${this._changeHandlers.status}
99 >
100 <option .selected=${!this._values.status}>
101 ${EMPTY_FIELD_VALUE}
102 </option>
103 ${this.statusOptions.map((status) => html`
104 <option
105 value=${status}
106 .selected=${status === this._values.status}
107 >${status}</option>
108 `)}
109 </select>
110 </td>
111 </tr>
112 <tr>
113 <th><label for="commentText">Comment:</label></th>
114 <td colspan="4">
115 <textarea
116 cols="30"
117 rows="3"
118 id="commentText"
119 placeholder="Add an approval comment"
120 .value=${this._values.comment || ''}
121 @change=${this._changeHandlers.comment}
122 ></textarea>
123 </td>
124 </tr>
125 <tr>
126 <td>
127 <button
128 class="js-save"
129 @click=${this.save}
130 >Update Approvals only</button>
131 </td>
132 <td>
133 <span class="note">
134 Note: Some approvals may not be updated if you lack
135 approver perms.
136 </span>
137 </td>
138 </tr>
139 </tbody></table>
140 </form>
141 `: ''}
142 <div class="message">
143 ${this.responseMessage}
144 ${this.errorMessage ? html`
145 <mr-error>${this.errorMessage}</mr-error>
146 ` : ''}
147 </div>
148 `;
149 }
150
151 /** @override */
152 static get properties() {
153 return {
154 approvals: {type: Array},
155 approvalsFetched: {type: Boolean},
156 statusOptions: {type: Array},
157 localIdsStr: {type: String},
158 projectName: {type: String},
159 responseMessage: {type: String},
160 _values: {type: Object},
161 };
162 }
163
164 /** @override */
165 constructor() {
166 super();
167 this.approvals = [];
168 this.statusOptions = Object.keys(TEXT_TO_STATUS_ENUM);
169 this.responseMessage = '';
170
171 this._values = {};
172 this._changeHandlers = {
173 approval: this._onChange.bind(this, 'approval'),
174 approvers: this._onChange.bind(this, 'approvers'),
175 status: this._onChange.bind(this, 'status'),
176 comment: this._onChange.bind(this, 'comment'),
177 };
178 }
179
180 /** @override */
181 createRenderRoot() {
182 return this;
183 }
184
185 get issueRefs() {
186 const {projectName, localIdsStr} = this;
187 if (!projectName || !localIdsStr) return [];
188 const issueRefs = [];
189 const localIds = localIdsStr.split(',');
190 localIds.forEach((localId) => {
191 issueRefs.push({projectName: projectName, localId: localId});
192 });
193 return issueRefs;
194 }
195
196 fetchApprovals(evt) {
197 const message = {issueRefs: this.issueRefs};
198 prpcClient.call('monorail.Issues', 'ListApplicableFieldDefs', message).then(
199 (resp) => {
200 if (resp.fieldDefs) {
201 this.approvals = resp.fieldDefs.filter((fieldDef) => {
202 return fieldDef.fieldRef.type == 'APPROVAL_TYPE';
203 });
204 }
205 if (!this.approvals.length) {
206 this.errorMessage = NO_APPROVALS_MESSAGE;
207 }
208 this.approvalsFetched = true;
209 }, (error) => {
210 this.approvalsFetched = true;
211 this.errorMessage = error;
212 });
213 }
214
215 save(evt) {
216 this.responseMessage = '';
217 this.errorMessage = '';
218 this.toggleDisableForm();
219 const selectedFieldDef = this.approvals.find(
220 (approval) => approval.fieldRef.fieldName === this._values.approval
221 ) || this.approvals[0];
222 const message = {
223 issueRefs: this.issueRefs,
224 fieldRef: selectedFieldDef.fieldRef,
225 send_email: true,
226 };
227 message.commentContent = this._values.comment;
228 const delta = {};
229 if (this._values.status !== EMPTY_FIELD_VALUE) {
230 delta.status = TEXT_TO_STATUS_ENUM[this._values.status];
231 }
232 const approversAdded = this._values.approvers;
233 if (approversAdded) {
234 delta.approverRefsAdd = approversAdded.map(
235 (name) => ({'displayName': name}));
236 }
237 if (Object.keys(delta).length) {
238 message.approvalDelta = delta;
239 }
240 prpcClient.call('monorail.Issues', 'BulkUpdateApprovals', message).then(
241 (resp) => {
242 if (resp.issueRefs && resp.issueRefs.length) {
243 const idsStr = Array.from(resp.issueRefs,
244 (ref) => ref.localId).join(', ');
245 this.responseMessage = `${this.getTimeStamp()}: Updated ${
246 selectedFieldDef.fieldRef.fieldName} in issues: ${idsStr} (${
247 resp.issueRefs.length} of ${this.issueRefs.length}).`;
248 this._values = {};
249 } else {
250 this.errorMessage = NO_UPDATES_MESSAGE;
251 };
252 this.toggleDisableForm();
253 }, (error) => {
254 this.errorMessage = error;
255 this.toggleDisableForm();
256 });
257 }
258
259 getTimeStamp() {
260 const date = new Date();
261 return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
262 }
263
264 toggleDisableForm() {
265 this.querySelectorAll('input, textarea, select, button').forEach(
266 (input) => {
267 input.disabled = !input.disabled;
268 });
269 }
270
271 /**
272 * Generic onChange handler to be bound to each form field.
273 * @param {string} key Unique name for the form field we're binding this
274 * handler to. For example, 'owner', 'cc', or the name of a custom field.
275 * @param {Event | React.SyntheticEvent} event
276 * @param {string} value The new form value.
277 */
278 _onChange(key, event, value) {
279 this._values = {...this._values, [key]: value || event.target.value};
280 }
281}
282
283customElements.define('mr-bulk-approval-update', MrBulkApprovalUpdate);