blob: bd88b3fbfc6c35db6a24c551c2e60ff7e7e164da [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {LitElement, html} from 'lit-element';
import {store, connectStore} from 'reducers/base.js';
import * as issueV0 from 'reducers/issueV0.js';
import * as ui from 'reducers/ui.js';
import 'elements/framework/mr-comment-content/mr-description.js';
import '../mr-comment-list/mr-comment-list.js';
import '../metadata/mr-edit-metadata/mr-edit-issue.js';
import {commentListToDescriptionList} from 'shared/convertersV0.js';
/**
* `<mr-issue-details>`
*
* This is the main details section for a given issue.
*
*/
export class MrIssueDetails extends connectStore(LitElement) {
/** @override */
render() {
let comments = [];
let descriptions = [];
if (this.commentsByApproval && this.commentsByApproval.has('')) {
// Comments without an approval go into the main view.
const mainComments = this.commentsByApproval.get('');
comments = mainComments.slice(1);
descriptions = commentListToDescriptionList(mainComments);
}
return html`
<style>
mr-issue-details {
font-size: var(--chops-main-font-size);
background-color: var(--chops-white);
padding-bottom: 1em;
display: flex;
align-items: stretch;
justify-content: flex-start;
flex-direction: column;
margin: 0;
box-sizing: border-box;
}
h3 {
margin-top: 1em;
}
mr-description {
margin-bottom: 1em;
}
mr-edit-issue {
margin-top: 40px;
}
</style>
<mr-description .descriptionList=${descriptions}></mr-description>
<mr-comment-list
headingLevel="2"
.comments=${comments}
.commentsShownCount=${this.commentsShownCount}
></mr-comment-list>
${this.issuePermissions.includes('addissuecomment') ?
html`<mr-edit-issue></mr-edit-issue>` : ''}
`;
}
/** @override */
static get properties() {
return {
commentsByApproval: {type: Object},
commentsShownCount: {type: Number},
issuePermissions: {type: Array},
};
}
/** @override */
constructor() {
super();
this.commentsByApproval = new Map();
this.issuePermissions = [];
}
/** @override */
createRenderRoot() {
return this;
}
/** @override */
stateChanged(state) {
this.commentsByApproval = issueV0.commentsByApprovalName(state);
this.issuePermissions = issueV0.permissions(state);
}
/** @override */
updated(changedProperties) {
super.updated(changedProperties);
this._measureCommentLoadTime(changedProperties);
}
async _measureCommentLoadTime(changedProperties) {
if (!changedProperties.has('commentsByApproval')) {
return;
}
if (!this.commentsByApproval || this.commentsByApproval.size === 0) {
// For cold loads, if the GetIssue call returns before ListComments,
// commentsByApproval is initially set to an empty Map. Filter that out.
return;
}
const fullAppLoad = ui.navigationCount(store.getState()) === 1;
if (!(fullAppLoad || changedProperties.get('commentsByApproval'))) {
// For hot loads, the previous issue data is still in the Redux store, so
// the first update sets the comments to the previous issue's comments.
// We need to wait for the following update.
return;
}
const startMark = fullAppLoad ? undefined : 'start load issue detail page';
if (startMark && !performance.getEntriesByName(startMark).length) {
// Modifying the issue template, description, comments, or attachments
// triggers a comment update. We only want to include full issue loads.
return;
}
await Promise.all(_subtreeUpdateComplete(this));
const endMark = 'finish load issue detail comments';
performance.mark(endMark);
const measurementType = fullAppLoad ? 'from outside app' : 'within app';
const measurementName = `load issue detail page (${measurementType})`;
performance.measure(measurementName, startMark, endMark);
const measurement =
performance.getEntriesByName(measurementName)[0].duration;
window.getTSMonClient().recordIssueCommentsLoadTiming(
measurement, fullAppLoad);
// Be sure to clear this mark even on full page navigations.
performance.clearMarks('start load issue detail page');
performance.clearMarks(endMark);
performance.clearMeasures(measurementName);
}
}
/**
* Recursively traverses all shadow DOMs in an element subtree and returns an
* Array containing the updateComplete Promises for all lit-element nodes.
* @param {!LitElement} element
* @return {!Array<Promise<Boolean>>}
*/
function _subtreeUpdateComplete(element) {
if (!element.updateComplete) {
return [];
}
const context = element.shadowRoot ? element.shadowRoot : element;
const children = context.querySelectorAll('*');
const childPromises = Array.from(children, (e) => _subtreeUpdateComplete(e));
return [element.updateComplete].concat(...childPromises);
}
customElements.define('mr-issue-details', MrIssueDetails);