blob: 174bbbbeee161aadff9da41ac40803af1dc33a81 [file] [log] [blame]
/**
* @license
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../../shared/gr-account-chip/gr-account-chip.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../../../styles/shared-styles.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-reviewer-list_html.js';
import {
hasAttention,
isServiceUser,
} from '../../../utils/account-util.js';
/**
* @extends PolymerElement
*/
class GrReviewerList extends GestureEventListeners(
LegacyElementMixin(PolymerElement)) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-reviewer-list'; }
/**
* Fired when the "Add reviewer..." button is tapped.
*
* @event show-reply-dialog
*/
static get properties() {
return {
change: Object,
serverConfig: Object,
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
mutable: {
type: Boolean,
value: false,
},
reviewersOnly: {
type: Boolean,
value: false,
},
ccsOnly: {
type: Boolean,
value: false,
},
_displayedReviewers: {
type: Array,
value() { return []; },
},
_reviewers: {
type: Array,
value() { return []; },
},
_showInput: {
type: Boolean,
value: false,
},
_addLabel: {
type: String,
computed: '_computeAddLabel(ccsOnly)',
},
_hiddenReviewerCount: {
type: Number,
computed: '_computeHiddenCount(_reviewers, _displayedReviewers)',
},
// Used for testing.
_lastAutocompleteRequest: Object,
_xhrPromise: Object,
};
}
static get observers() {
return [
'_reviewersChanged(change.reviewers.*, change.owner, serverConfig)',
];
}
/**
* Converts change.permitted_labels to an array of hashes of label keys to
* numeric scores.
* Example:
* [{
* 'Code-Review': ['-1', ' 0', '+1']
* }]
* will be converted to
* [{
* label: 'Code-Review',
* scores: [-1, 0, 1]
* }]
*/
_permittedLabelsToNumericScores(labels) {
if (!labels) return [];
return Object.keys(labels).map(label => {
return {
label,
scores: labels[label].map(v => parseInt(v, 10)),
};
});
}
/**
* Returns hash of labels to max permitted score.
*
* @param {!Object} change
* @returns {!Object} labels to max permitted scores hash
*/
_getMaxPermittedScores(change) {
return this._permittedLabelsToNumericScores(change.permitted_labels)
.map(({label, scores}) => {
return {
[label]: scores
.map(v => parseInt(v, 10))
.reduce((a, b) => Math.max(a, b))};
})
.reduce((acc, i) => Object.assign(acc, i), {});
}
/**
* Returns max permitted score for reviewer.
*
* @param {!Object} reviewer
* @param {!Object} change
* @param {string} label
* @return {number}
*/
_getReviewerPermittedScore(reviewer, change, label) {
// Note (issue 7874): sometimes the "all" list is not included in change
// detail responses, even when DETAILED_LABELS is included in options.
if (!change.labels[label].all) { return NaN; }
const detailed = change.labels[label].all.filter(
({_account_id}) => reviewer._account_id === _account_id).pop();
if (!detailed) {
return NaN;
}
if (detailed.hasOwnProperty('permitted_voting_range')) {
return detailed.permitted_voting_range.max;
} else if (detailed.hasOwnProperty('value')) {
// If preset, user can vote on the label.
return 0;
}
return NaN;
}
_computeVoteableText(reviewer, change) {
if (!change || !change.labels) { return ''; }
const maxScores = [];
const maxPermitted = this._getMaxPermittedScores(change);
for (const label of Object.keys(change.labels)) {
const maxScore =
this._getReviewerPermittedScore(reviewer, change, label);
if (isNaN(maxScore) || maxScore < 0) { continue; }
if (maxScore > 0 && maxScore === maxPermitted[label]) {
maxScores.push(`${label}: +${maxScore}`);
} else {
maxScores.push(`${label}`);
}
}
return maxScores.join(', ');
}
_reviewersChanged(changeRecord, owner, serverConfig) {
// Polymer 2: check for undefined
if ([changeRecord, owner, serverConfig].includes(undefined)) {
return;
}
let result = [];
const reviewers = changeRecord.base;
for (const key in reviewers) {
if (this.reviewersOnly && key !== 'REVIEWER') {
continue;
}
if (this.ccsOnly && key !== 'CC') {
continue;
}
if (key === 'REVIEWER' || key === 'CC') {
result = result.concat(reviewers[key]);
}
}
this._reviewers = result
.filter(reviewer => reviewer._account_id != owner._account_id)
// Sort order:
// 1. Human users in the attention set.
// 2. Other human users.
// 3. Service users.
.sort((r1, r2) => {
const a1 = hasAttention(serverConfig, r1, this.change) ? 1 : 0;
const a2 = hasAttention(serverConfig, r2, this.change) ? 1 : 0;
const s1 = isServiceUser(r1) ? -2 : 0;
const s2 = isServiceUser(r2) ? -2 : 0;
return a2 - a1 + s2 - s1;
});
if (this._reviewers.length > 8) {
this._displayedReviewers = this._reviewers.slice(0, 6);
} else {
this._displayedReviewers = this._reviewers;
}
}
_computeHiddenCount(reviewers, displayedReviewers) {
// Polymer 2: check for undefined
if ([reviewers, displayedReviewers].includes(undefined)) {
return undefined;
}
return reviewers.length - displayedReviewers.length;
}
_computeCanRemoveReviewer(reviewer, mutable) {
if (!mutable) { return false; }
let current;
for (let i = 0; i < this.change.removable_reviewers.length; i++) {
current = this.change.removable_reviewers[i];
if (current._account_id === reviewer._account_id ||
(!reviewer._account_id && current.email === reviewer.email)) {
return true;
}
}
return false;
}
_handleRemove(e) {
e.preventDefault();
const target = dom(e).rootTarget;
if (!target.account) { return; }
const accountID = target.account._account_id || target.account.email;
this.disabled = true;
this._xhrPromise = this._removeReviewer(accountID).then(response => {
this.disabled = false;
if (!response.ok) { return response; }
const reviewers = this.change.reviewers;
for (const type of ['REVIEWER', 'CC']) {
reviewers[type] = reviewers[type] || [];
for (let i = 0; i < reviewers[type].length; i++) {
if (reviewers[type][i]._account_id == accountID ||
reviewers[type][i].email == accountID) {
this.splice('change.reviewers.' + type, i, 1);
break;
}
}
}
})
.catch(err => {
this.disabled = false;
throw err;
});
}
_handleAddTap(e) {
e.preventDefault();
const value = {};
if (this.reviewersOnly) {
value.reviewersOnly = true;
}
if (this.ccsOnly) {
value.ccsOnly = true;
}
this.dispatchEvent(new CustomEvent('show-reply-dialog', {
detail: {value},
composed: true, bubbles: true,
}));
}
_handleViewAll(e) {
this._displayedReviewers = this._reviewers;
}
_removeReviewer(id) {
return this.$.restAPI.removeChangeReviewer(this.change._number, id);
}
_computeAddLabel(ccsOnly) {
return ccsOnly ? 'Add CC' : 'Add reviewer';
}
}
customElements.define(GrReviewerList.is, GrReviewerList);