blob: c933c7c55ea6cad518fa02f84b2069bc3610fa08 [file] [log] [blame]
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04001/**
2 * @license
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01003 * Copyright (C) 2015 The Android Open Source Project
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04004 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010017import '../../../scripts/bundled-polymer.js';
18
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010019import '../../shared/gr-account-chip/gr-account-chip.js';
20import '../../shared/gr-button/gr-button.js';
21import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
22import '../../../styles/shared-styles.js';
23import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010024import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
25import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
26import {PolymerElement} from '@polymer/polymer/polymer-element.js';
27import {htmlTemplate} from './gr-reviewer-list_html.js';
28
29/**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010030 * @extends Polymer.Element
31 */
Tao Zhoue25546a2020-04-14 12:25:25 +020032class GrReviewerList extends GestureEventListeners(
33 LegacyElementMixin(PolymerElement)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010034 static get template() { return htmlTemplate; }
35
36 static get is() { return 'gr-reviewer-list'; }
37 /**
38 * Fired when the "Add reviewer..." button is tapped.
39 *
40 * @event show-reply-dialog
41 */
42
43 static get properties() {
44 return {
45 change: Object,
Dmitrii Filippovbcf84d22020-03-18 15:31:59 +010046 serverConfig: Object,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010047 disabled: {
48 type: Boolean,
49 value: false,
50 reflectToAttribute: true,
51 },
52 mutable: {
53 type: Boolean,
54 value: false,
55 },
56 reviewersOnly: {
57 type: Boolean,
58 value: false,
59 },
60 ccsOnly: {
61 type: Boolean,
62 value: false,
63 },
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010064
65 _displayedReviewers: {
66 type: Array,
67 value() { return []; },
68 },
69 _reviewers: {
70 type: Array,
71 value() { return []; },
72 },
73 _showInput: {
74 type: Boolean,
75 value: false,
76 },
77 _addLabel: {
78 type: String,
79 computed: '_computeAddLabel(ccsOnly)',
80 },
81 _hiddenReviewerCount: {
82 type: Number,
83 computed: '_computeHiddenCount(_reviewers, _displayedReviewers)',
84 },
85
86 // Used for testing.
87 _lastAutocompleteRequest: Object,
88 _xhrPromise: Object,
89 };
90 }
91
92 static get observers() {
93 return [
Dmitrii Filippovbcf84d22020-03-18 15:31:59 +010094 '_reviewersChanged(change.reviewers.*, change.owner, serverConfig)',
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010095 ];
96 }
Andrew Bonventre78792e82016-03-04 17:48:22 -050097
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +010098 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010099 * Converts change.permitted_labels to an array of hashes of label keys to
100 * numeric scores.
101 * Example:
102 * [{
103 * 'Code-Review': ['-1', ' 0', '+1']
104 * }]
105 * will be converted to
106 * [{
107 * label: 'Code-Review',
108 * scores: [-1, 0, 1]
109 * }]
Tao Zhou9a076812019-12-17 09:59:28 +0100110 */
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100111 _permittedLabelsToNumericScores(labels) {
112 if (!labels) return [];
113 return Object.keys(labels).map(label => {
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100114 return {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100115 label,
116 scores: labels[label].map(v => parseInt(v, 10)),
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100117 };
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100118 });
119 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500120
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100121 /**
122 * Returns hash of labels to max permitted score.
123 *
124 * @param {!Object} change
125 * @returns {!Object} labels to max permitted scores hash
126 */
127 _getMaxPermittedScores(change) {
128 return this._permittedLabelsToNumericScores(change.permitted_labels)
129 .map(({label, scores}) => {
130 return {
131 [label]: scores
132 .map(v => parseInt(v, 10))
133 .reduce((a, b) => Math.max(a, b))};
134 })
135 .reduce((acc, i) => Object.assign(acc, i), {});
136 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500137
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100138 /**
139 * Returns max permitted score for reviewer.
140 *
141 * @param {!Object} reviewer
142 * @param {!Object} change
143 * @param {string} label
144 * @return {number}
145 */
146 _getReviewerPermittedScore(reviewer, change, label) {
147 // Note (issue 7874): sometimes the "all" list is not included in change
148 // detail responses, even when DETAILED_LABELS is included in options.
149 if (!change.labels[label].all) { return NaN; }
150 const detailed = change.labels[label].all.filter(
151 ({_account_id}) => reviewer._account_id === _account_id).pop();
152 if (!detailed) {
Viktar Donich14130f12018-04-18 14:44:25 -0700153 return NaN;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100154 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100155 if (detailed.hasOwnProperty('permitted_voting_range')) {
156 return detailed.permitted_voting_range.max;
157 } else if (detailed.hasOwnProperty('value')) {
158 // If preset, user can vote on the label.
159 return 0;
160 }
161 return NaN;
162 }
Viktar Donich8de21eb2017-11-21 16:17:48 -0800163
Dmitrii Filippov6c0b1b12020-03-18 14:17:56 +0100164 _computeVoteableText(reviewer, change) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100165 if (!change || !change.labels) { return ''; }
166 const maxScores = [];
167 const maxPermitted = this._getMaxPermittedScores(change);
168 for (const label of Object.keys(change.labels)) {
169 const maxScore =
170 this._getReviewerPermittedScore(reviewer, change, label);
171 if (isNaN(maxScore) || maxScore < 0) { continue; }
172 if (maxScore > 0 && maxScore === maxPermitted[label]) {
173 maxScores.push(`${label}: +${maxScore}`);
Viktar Donich8de21eb2017-11-21 16:17:48 -0800174 } else {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100175 maxScores.push(`${label}`);
Viktar Donich8de21eb2017-11-21 16:17:48 -0800176 }
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100177 }
Dmitrii Filippov6c0b1b12020-03-18 14:17:56 +0100178 return maxScores.join(', ');
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100179 }
180
Dmitrii Filippovbcf84d22020-03-18 15:31:59 +0100181 _reviewersChanged(changeRecord, owner, serverConfig) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100182 // Polymer 2: check for undefined
Dmitrii Filippovbcf84d22020-03-18 15:31:59 +0100183 if ([changeRecord, owner, serverConfig].some(arg => arg === undefined)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100184 return;
185 }
186
187 let result = [];
188 const reviewers = changeRecord.base;
189 for (const key in reviewers) {
190 if (this.reviewersOnly && key !== 'REVIEWER') {
191 continue;
192 }
193 if (this.ccsOnly && key !== 'CC') {
194 continue;
195 }
196 if (key === 'REVIEWER' || key === 'CC') {
197 result = result.concat(reviewers[key]);
198 }
199 }
200 this._reviewers = result
201 .filter(reviewer => reviewer._account_id != owner._account_id);
202
Dmitrii Filippovbcf84d22020-03-18 15:31:59 +0100203 const isFirstNameConfigured = serverConfig.accounts
204 && serverConfig.accounts.default_display_name === 'FIRST_NAME';
205 const maxReviewers = isFirstNameConfigured ? 6 : 3;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100206 // If there is one or two more than the max reviewers, don't show the
207 // 'show more' button, because it takes up just as much space.
Dmitrii Filippovbcf84d22020-03-18 15:31:59 +0100208 if (this._reviewers.length > maxReviewers + 2) {
209 this._displayedReviewers = this._reviewers.slice(0, maxReviewers);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100210 } else {
211 this._displayedReviewers = this._reviewers;
212 }
213 }
214
215 _computeHiddenCount(reviewers, displayedReviewers) {
216 // Polymer 2: check for undefined
217 if ([reviewers, displayedReviewers].some(arg => arg === undefined)) {
218 return undefined;
219 }
220
221 return reviewers.length - displayedReviewers.length;
222 }
223
224 _computeCanRemoveReviewer(reviewer, mutable) {
225 if (!mutable) { return false; }
226
227 let current;
228 for (let i = 0; i < this.change.removable_reviewers.length; i++) {
229 current = this.change.removable_reviewers[i];
230 if (current._account_id === reviewer._account_id ||
231 (!reviewer._account_id && current.email === reviewer.email)) {
232 return true;
233 }
234 }
235 return false;
236 }
237
238 _handleRemove(e) {
239 e.preventDefault();
240 const target = dom(e).rootTarget;
241 if (!target.account) { return; }
242 const accountID = target.account._account_id || target.account.email;
243 this.disabled = true;
244 this._xhrPromise = this._removeReviewer(accountID).then(response => {
245 this.disabled = false;
246 if (!response.ok) { return response; }
247
248 const reviewers = this.change.reviewers;
249
250 for (const type of ['REVIEWER', 'CC']) {
251 reviewers[type] = reviewers[type] || [];
252 for (let i = 0; i < reviewers[type].length; i++) {
253 if (reviewers[type][i]._account_id == accountID ||
254 reviewers[type][i].email == accountID) {
255 this.splice('change.reviewers.' + type, i, 1);
256 break;
257 }
258 }
259 }
260 })
261 .catch(err => {
262 this.disabled = false;
263 throw err;
264 });
265 }
266
267 _handleAddTap(e) {
268 e.preventDefault();
269 const value = {};
270 if (this.reviewersOnly) {
271 value.reviewersOnly = true;
272 }
273 if (this.ccsOnly) {
274 value.ccsOnly = true;
275 }
Tao Zhou0a3c9862020-04-08 14:45:37 +0200276 this.dispatchEvent(new CustomEvent('show-reply-dialog', {
277 detail: {value},
278 composed: true, bubbles: true,
279 }));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100280 }
281
282 _handleViewAll(e) {
283 this._displayedReviewers = this._reviewers;
284 }
285
286 _removeReviewer(id) {
287 return this.$.restAPI.removeChangeReviewer(this.change._number, id);
288 }
289
290 _computeAddLabel(ccsOnly) {
291 return ccsOnly ? 'Add CC' : 'Add reviewer';
292 }
293}
294
295customElements.define(GrReviewerList.is, GrReviewerList);