/** * @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 '../../../styles/gr-change-list-styles.js'; import '../../shared/gr-account-link/gr-account-link.js'; import '../../shared/gr-change-star/gr-change-star.js'; import '../../shared/gr-change-status/gr-change-status.js'; import '../../shared/gr-date-formatter/gr-date-formatter.js'; import '../../shared/gr-icons/gr-icons.js'; import '../../shared/gr-limited-text/gr-limited-text.js'; import '../../shared/gr-tooltip-content/gr-tooltip-content.js'; import '../../../styles/shared-styles.js'; import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; import '../../plugins/gr-endpoint-param/gr-endpoint-param.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-change-list-item_html.js'; import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin.js'; import {GerritNav} from '../../core/gr-navigation/gr-navigation.js'; import {getDisplayName} from '../../../utils/display-name-util.js'; import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js'; import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js'; import {appContext} from '../../../services/app-context.js'; import {truncatePath} from '../../../utils/path-list-util.js'; import {changeStatuses} from '../../../utils/change-util.js'; import {isServiceUser} from '../../../utils/account-util.js'; const CHANGE_SIZE = { XS: 10, SMALL: 50, MEDIUM: 250, LARGE: 1000, }; // How many reviewers should be shown with an account-label? const PRIMARY_REVIEWERS_COUNT = 2; /** * @extends PolymerElement */ class GrChangeListItem extends ChangeTableMixin(GestureEventListeners( LegacyElementMixin(PolymerElement))) { static get template() { return htmlTemplate; } static get is() { return 'gr-change-list-item'; } static get properties() { return { /** The logged-in user's account, or null if no user is logged in. */ account: { type: Object, value: null, }, visibleChangeTableColumns: Array, labelNames: { type: Array, }, /** @type {?} */ change: Object, config: Object, /** Name of the section in the change-list. Used for reporting. */ sectionName: String, changeURL: { type: String, computed: '_computeChangeURL(change)', }, statuses: { type: Array, computed: '_changeStatuses(change)', }, showStar: { type: Boolean, value: false, }, showNumber: Boolean, _changeSize: { type: String, computed: '_computeChangeSize(change)', }, _dynamicCellEndpoints: { type: Array, }, }; } constructor() { super(); this.reporting = appContext.reportingService; } /** @override */ attached() { super.attached(); getPluginLoader().awaitPluginsLoaded() .then(() => { this._dynamicCellEndpoints = getPluginEndpoints().getDynamicEndpoints( 'change-list-item-cell'); }); } _changeStatuses(change) { return changeStatuses(change); } _computeChangeURL(change) { return GerritNav.getUrlForChange(change); } _computeLabelTitle(change, labelName) { const label = change.labels[labelName]; if (!label) { return 'Label not applicable'; } const significantLabel = label.rejected || label.approved || label.disliked || label.recommended; if (significantLabel && significantLabel.name) { return labelName + '\nby ' + significantLabel.name; } return labelName; } _computeLabelClass(change, labelName) { const label = change.labels[labelName]; // Mimic a Set. const classes = { cell: true, label: true, }; if (label) { if (label.approved) { classes['u-green'] = true; } if (label.value == 1) { classes['u-monospace'] = true; classes['u-green'] = true; } else if (label.value == -1) { classes['u-monospace'] = true; classes['u-red'] = true; } if (label.rejected) { classes['u-red'] = true; } } else { classes['u-gray-background'] = true; } return Object.keys(classes).sort() .join(' '); } _computeLabelValue(change, labelName) { const label = change.labels[labelName]; if (!label) { return ''; } if (label.approved) { return '✓'; } if (label.rejected) { return '✕'; } if (label.value > 0) { return '+' + label.value; } if (label.value < 0) { return label.value; } return ''; } _computeRepoUrl(change) { return GerritNav.getUrlForProjectChanges(change.project, true, change.internalHost); } _computeRepoBranchURL(change) { return GerritNav.getUrlForBranch(change.branch, change.project, null, change.internalHost); } _computeTopicURL(change) { if (!change.topic) { return ''; } return GerritNav.getUrlForTopic(change.topic, change.internalHost); } /** * Computes the display string for the project column. If there is a host * specified in the change detail, the string will be prefixed with it. * * @param {!Object} change * @param {string=} truncate whether or not the project name should be * truncated. If this value is truthy, the name will be truncated. * @return {string} */ _computeRepoDisplay(change, truncate) { if (!change || !change.project) { return ''; } let str = ''; if (change.internalHost) { str += change.internalHost + '/'; } str += truncate ? truncatePath(change.project, 2) : change.project; return str; } _computeSizeTooltip(change) { if (change.insertions + change.deletions === 0 || isNaN(change.insertions + change.deletions)) { return 'Size unknown'; } else { return `added ${change.insertions}, removed ${change.deletions} lines`; } } _hasAttention(account) { if (!this.change || !this.change.attention_set) return false; return this.change.attention_set.hasOwnProperty(account._account_id); } /** * Computes the array of all reviewers with sorting the reviewers in the * attention set before others, and the current user first. */ _computeReviewers(change) { if (!change || !change.reviewers || !change.reviewers.REVIEWER) return []; const reviewers = [...change.reviewers.REVIEWER].filter(r => (!change.owner || change.owner._account_id !== r._account_id) && !isServiceUser(r) ); reviewers.sort((r1, r2) => { if (this.account) { if (r1._account_id === this.account._account_id) return -1; if (r2._account_id === this.account._account_id) return 1; } if (this._hasAttention(r1) && !this._hasAttention(r2)) return -1; if (this._hasAttention(r2) && !this._hasAttention(r1)) return 1; return (r1.name || '').localeCompare(r2.name || ''); }); return reviewers; } _computePrimaryReviewers(change) { return this._computeReviewers(change).slice(0, PRIMARY_REVIEWERS_COUNT); } _computeAdditionalReviewers(change) { return this._computeReviewers(change).slice(PRIMARY_REVIEWERS_COUNT); } _computeAdditionalReviewersCount(change) { return this._computeAdditionalReviewers(change).length; } _computeAdditionalReviewersTitle(change, config) { if (!change || !config) return ''; return this._computeAdditionalReviewers(change) .map(user => getDisplayName(config, user, true)) .join(', '); } _computeComments(unresolved_comment_count) { if (!unresolved_comment_count || unresolved_comment_count < 1) return ''; return `${unresolved_comment_count} unresolved`; } /** * TShirt sizing is based on the following paper: * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf */ _computeChangeSize(change) { const delta = change.insertions + change.deletions; if (isNaN(delta) || delta === 0) { return null; // Unknown } if (delta < CHANGE_SIZE.XS) { return 'XS'; } else if (delta < CHANGE_SIZE.SMALL) { return 'S'; } else if (delta < CHANGE_SIZE.MEDIUM) { return 'M'; } else if (delta < CHANGE_SIZE.LARGE) { return 'L'; } else { return 'XL'; } } toggleReviewed() { const newVal = !this.change.reviewed; this.set('change.reviewed', newVal); this.dispatchEvent(new CustomEvent('toggle-reviewed', { bubbles: true, composed: true, detail: {change: this.change, reviewed: newVal}, })); } _handleChangeClick(e) { // Don't prevent the default and neither stop bubbling. We just want to // report the click, but then let the browser handle the click on the link. const selfId = (this.account && this.account._account_id) || -1; const ownerId = (this.change && this.change.owner && this.change.owner._account_id) || -1; this.reporting.reportInteraction('change-row-clicked', { section: this.sectionName, isOwner: selfId === ownerId, }); } } customElements.define(GrChangeListItem.is, GrChangeListItem);