blob: bd88b3fbfc6c35db6a24c551c2e60ff7e7e164da [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {LitElement, html} from 'lit-element';
6
7import {store, connectStore} from 'reducers/base.js';
8import * as issueV0 from 'reducers/issueV0.js';
9import * as ui from 'reducers/ui.js';
10import 'elements/framework/mr-comment-content/mr-description.js';
11import '../mr-comment-list/mr-comment-list.js';
12import '../metadata/mr-edit-metadata/mr-edit-issue.js';
13import {commentListToDescriptionList} from 'shared/convertersV0.js';
14
15/**
16 * `<mr-issue-details>`
17 *
18 * This is the main details section for a given issue.
19 *
20 */
21export class MrIssueDetails extends connectStore(LitElement) {
22 /** @override */
23 render() {
24 let comments = [];
25 let descriptions = [];
26
27 if (this.commentsByApproval && this.commentsByApproval.has('')) {
28 // Comments without an approval go into the main view.
29 const mainComments = this.commentsByApproval.get('');
30 comments = mainComments.slice(1);
31 descriptions = commentListToDescriptionList(mainComments);
32 }
33
34 return html`
35 <style>
36 mr-issue-details {
37 font-size: var(--chops-main-font-size);
38 background-color: var(--chops-white);
39 padding-bottom: 1em;
40 display: flex;
41 align-items: stretch;
42 justify-content: flex-start;
43 flex-direction: column;
44 margin: 0;
45 box-sizing: border-box;
46 }
47 h3 {
48 margin-top: 1em;
49 }
50 mr-description {
51 margin-bottom: 1em;
52 }
53 mr-edit-issue {
54 margin-top: 40px;
55 }
56 </style>
57 <mr-description .descriptionList=${descriptions}></mr-description>
58 <mr-comment-list
59 headingLevel="2"
60 .comments=${comments}
61 .commentsShownCount=${this.commentsShownCount}
62 ></mr-comment-list>
63 ${this.issuePermissions.includes('addissuecomment') ?
64 html`<mr-edit-issue></mr-edit-issue>` : ''}
65 `;
66 }
67
68 /** @override */
69 static get properties() {
70 return {
71 commentsByApproval: {type: Object},
72 commentsShownCount: {type: Number},
73 issuePermissions: {type: Array},
74 };
75 }
76
77 /** @override */
78 constructor() {
79 super();
80 this.commentsByApproval = new Map();
81 this.issuePermissions = [];
82 }
83
84 /** @override */
85 createRenderRoot() {
86 return this;
87 }
88
89 /** @override */
90 stateChanged(state) {
91 this.commentsByApproval = issueV0.commentsByApprovalName(state);
92 this.issuePermissions = issueV0.permissions(state);
93 }
94
95 /** @override */
96 updated(changedProperties) {
97 super.updated(changedProperties);
98 this._measureCommentLoadTime(changedProperties);
99 }
100
101 async _measureCommentLoadTime(changedProperties) {
102 if (!changedProperties.has('commentsByApproval')) {
103 return;
104 }
105 if (!this.commentsByApproval || this.commentsByApproval.size === 0) {
106 // For cold loads, if the GetIssue call returns before ListComments,
107 // commentsByApproval is initially set to an empty Map. Filter that out.
108 return;
109 }
110 const fullAppLoad = ui.navigationCount(store.getState()) === 1;
111 if (!(fullAppLoad || changedProperties.get('commentsByApproval'))) {
112 // For hot loads, the previous issue data is still in the Redux store, so
113 // the first update sets the comments to the previous issue's comments.
114 // We need to wait for the following update.
115 return;
116 }
117 const startMark = fullAppLoad ? undefined : 'start load issue detail page';
118 if (startMark && !performance.getEntriesByName(startMark).length) {
119 // Modifying the issue template, description, comments, or attachments
120 // triggers a comment update. We only want to include full issue loads.
121 return;
122 }
123
124 await Promise.all(_subtreeUpdateComplete(this));
125
126 const endMark = 'finish load issue detail comments';
127 performance.mark(endMark);
128
129 const measurementType = fullAppLoad ? 'from outside app' : 'within app';
130 const measurementName = `load issue detail page (${measurementType})`;
131 performance.measure(measurementName, startMark, endMark);
132
133 const measurement =
134 performance.getEntriesByName(measurementName)[0].duration;
135 window.getTSMonClient().recordIssueCommentsLoadTiming(
136 measurement, fullAppLoad);
137
138 // Be sure to clear this mark even on full page navigations.
139 performance.clearMarks('start load issue detail page');
140 performance.clearMarks(endMark);
141 performance.clearMeasures(measurementName);
142 }
143}
144
145/**
146 * Recursively traverses all shadow DOMs in an element subtree and returns an
147 * Array containing the updateComplete Promises for all lit-element nodes.
148 * @param {!LitElement} element
149 * @return {!Array<Promise<Boolean>>}
150 */
151function _subtreeUpdateComplete(element) {
152 if (!element.updateComplete) {
153 return [];
154 }
155
156 const context = element.shadowRoot ? element.shadowRoot : element;
157 const children = context.querySelectorAll('*');
158 const childPromises = Array.from(children, (e) => _subtreeUpdateComplete(e));
159 return [element.updateComplete].concat(...childPromises);
160}
161
162customElements.define('mr-issue-details', MrIssueDetails);