blob: c888fcf1037cff656b8f401db5c988a8786dc36b [file] [log] [blame]
/**
* @license
* Copyright (C) 2020 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 '../../../scripts/bundled-polymer.js';
import '@polymer/paper-toggle-button/paper-toggle-button.js';
import '../../core/gr-reporting/gr-reporting.js';
import '../../shared/gr-button/gr-button.js';
import '../gr-message/gr-message.js';
import '../../../styles/shared-styles.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.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-messages-list-experimental_html.js';
import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
import {util} from '../../../scripts/util.js';
/**
* The content of the enum is also used in the UI for the button text.
*
* @enum {string}
*/
const ExpandAllState = {
EXPAND_ALL: 'Expand All',
COLLAPSE_ALL: 'Collapse All',
};
/**
* @extends Polymer.Element
*/
class GrMessagesListExperimental extends mixinBehaviors( [
KeyboardShortcutBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-messages-list-experimental'; }
static get properties() {
return {
changeNum: Number,
/**
* These are just the change messages. They are combined with reviewer
* updates below. So _combinedMessages is the more important property.
*/
messages: {
type: Array,
value() { return []; },
},
/**
* These are just the reviewer updates. They are combined with change
* messages above. So _combinedMessages is the more important property.
*/
reviewerUpdates: {
type: Array,
value() { return []; },
},
changeComments: Object,
projectName: String,
showReplyButtons: {
type: Boolean,
value: false,
},
labels: Object,
/**
* Keeps track of the state of the "Expand All" toggle button. Note that
* you can individually expand/collapse some messages without affecting
* the toggle button's state.
*
* @type {ExpandAllState}
*/
_expandAllState: {
type: String,
value: ExpandAllState.EXPAND_ALL,
},
_expandAllTitle: {
type: String,
computed: '_computeExpandAllTitle(_expandAllState)',
},
_hideAutomated: {
type: Boolean,
value: false,
observer: '_hideAutomatedChanged',
},
/**
* The merged array of change messages and reviewer updates.
*/
_combinedMessages: {
type: Array,
computed: '_computeCombinedMessages(messages, reviewerUpdates)',
observer: '_combinedMessagesChanged',
},
_labelExtremes: {
type: Object,
computed: '_computeLabelExtremes(labels.*)',
},
};
}
scrollToMessage(messageID) {
const selector = `[data-message-id="${messageID}"]`;
const el = this.shadowRoot.querySelector(selector);
if (!el && !this._hideAutomated) {
console.warn(`Failed to scroll to message: ${messageID}`);
return;
}
if (!el) {
this._hideAutomated = false;
setTimeout(() => this.scrollToMessage(messageID));
return;
}
el.set('message.expanded', true);
let top = el.offsetTop;
for (let offsetParent = el.offsetParent;
offsetParent;
offsetParent = offsetParent.offsetParent) {
top += offsetParent.offsetTop;
}
window.scrollTo(0, top);
this._highlightEl(el);
}
_isAutomated(message) {
const isReviewerUpdate =
!!(message.reviewer || message.type === 'REVIEWER_UPDATE');
const isAutoGenerated =
!!(message.tag && message.tag.startsWith('autogenerated'));
return isReviewerUpdate || isAutoGenerated;
}
_hideAutomatedChanged(hideAutomated) {
// We have to call render() such that the dom-repeat filter picks up the
// change.
this.$.messageRepeat.render();
}
/**
* Filter for the dom-repeat of combinedMessages.
*/
_isMessageVisible(message) {
return !(this._hideAutomated && this._isAutomated(message));
}
/**
* Merges change messages and reviewer updates into one array.
*/
_computeCombinedMessages(messages, reviewerUpdates) {
messages = messages || [];
reviewerUpdates = reviewerUpdates || [];
let mi = 0;
let ri = 0;
let combinedMessages = [];
let mDate;
let rDate;
for (let i = 0; i < messages.length; i++) {
messages[i]._index = i;
}
while (mi < messages.length || ri < reviewerUpdates.length) {
if (mi >= messages.length) {
combinedMessages = combinedMessages.concat(reviewerUpdates.slice(ri));
break;
}
if (ri >= reviewerUpdates.length) {
combinedMessages = combinedMessages.concat(messages.slice(mi));
break;
}
mDate = mDate || util.parseDate(messages[mi].date);
rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
if (rDate < mDate) {
combinedMessages.push(reviewerUpdates[ri++]);
rDate = null;
} else {
combinedMessages.push(messages[mi++]);
mDate = null;
}
}
combinedMessages.forEach(m => {
if (m.expanded === undefined) {
m.expanded = false;
}
});
return combinedMessages;
}
_updateExpandedStateOfAllMessages(exp) {
if (this._combinedMessages) {
for (let i = 0; i < this._combinedMessages.length; i++) {
this._combinedMessages[i].expanded = exp;
this.notifyPath(`_combinedMessages.${i}.expanded`);
}
}
}
_computeExpandAllTitle(_expandAllState) {
if (_expandAllState === ExpandAllState.COLLAPSED_ALL) {
return this.createTitle(
this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
}
if (_expandAllState === ExpandAllState.EXPAND_ALL) {
return this.createTitle(
this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
}
return '';
}
_highlightEl(el) {
const highlightedEls =
dom(this.root).querySelectorAll('.highlighted');
for (const highlighedEl of highlightedEls) {
highlighedEl.classList.remove('highlighted');
}
function handleAnimationEnd() {
el.removeEventListener('animationend', handleAnimationEnd);
el.classList.remove('highlighted');
}
el.addEventListener('animationend', handleAnimationEnd);
el.classList.add('highlighted');
}
/**
* @param {boolean} expand
*/
handleExpandCollapse(expand) {
this._expandAllState = expand ? ExpandAllState.COLLAPSE_ALL
: ExpandAllState.EXPAND_ALL;
this._updateExpandedStateOfAllMessages(expand);
}
_handleExpandCollapseTap(e) {
e.preventDefault();
this.handleExpandCollapse(
this._expandAllState === ExpandAllState.EXPAND_ALL);
}
_handleAnchorClick(e) {
this.scrollToMessage(e.detail.id);
}
_hasAutomatedMessages(messages) {
if (!messages) { return false; }
for (const message of messages) {
if (this._isAutomated(message)) {
return true;
}
}
return false;
}
/**
* Computes message author's file comments for change's message. The backend
* sets comment.change_message_id for matching, so this computation is fairly
* straightforward.
*
* @param {!Object} changeComments changeComment object, which includes
* a method to get all published comments (including robot comments),
* which returns a Hash of arrays of comments, filename as key.
* @param {!Object} message
* @return {!Object} Hash of arrays of comments, filename as key.
*/
_computeCommentsForMessage(changeComments, message) {
if ([changeComments, message].some(arg => arg === undefined)) {
return {};
}
const comments = changeComments.getAllPublishedComments();
if (message._index === undefined || !comments || !this.messages) {
return {};
}
const idFilter = comment => comment.change_message_id === message.id;
const msgComments = {};
for (const file in comments) {
if (!comments.hasOwnProperty(file)) { continue; }
const filtered = comments[file].filter(idFilter);
if (filtered.length) msgComments[file] = filtered;
}
return msgComments;
}
/**
* This method is for reporting stats only.
*/
_combinedMessagesChanged(combinedMessages) {
if (combinedMessages) {
if (combinedMessages.length === 0) return;
const tags = combinedMessages.map(
message => message.tag || message.type ||
(message.comments ? 'comments' : 'none'));
const tagsCounted = tags.reduce((acc, val) => {
acc[val] = (acc[val] || 0) + 1;
return acc;
}, {all: combinedMessages.length});
this.$.reporting.reportInteraction('messages-count', tagsCounted);
}
}
/**
* Compute a mapping from label name to objects representing the minimum and
* maximum possible values for that label.
*/
_computeLabelExtremes(labelRecord) {
const extremes = {};
const labels = labelRecord.base;
if (!labels) { return extremes; }
for (const key of Object.keys(labels)) {
if (!labels[key] || !labels[key].values) { continue; }
const values = Object.keys(labels[key].values)
.map(v => parseInt(v, 10));
values.sort((a, b) => a - b);
if (!values.length) { continue; }
extremes[key] = {min: values[0], max: values[values.length - 1]};
}
return extremes;
}
}
customElements.define(GrMessagesListExperimental.is,
GrMessagesListExperimental);