blob: 8dbe2df8833e32cd5af3f0cfa3bd218e56a53b3f [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001// Copyright 2019 The Chromium Authors
Copybara854996b2021-09-07 19:36:02 +00002// 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, css} from 'lit-element';
6
7import {connectStore} from 'reducers/base.js';
8import 'elements/chops/chops-timestamp/chops-timestamp.js';
9import 'elements/framework/links/mr-issue-link/mr-issue-link.js';
10import 'elements/framework/links/mr-user-link/mr-user-link.js';
11import 'elements/framework/mr-issue-slo/mr-issue-slo.js';
12
13import * as issueV0 from 'reducers/issueV0.js';
14import * as sitewide from 'reducers/sitewide.js';
15import * as userV0 from 'reducers/userV0.js';
16import './mr-field-values.js';
17import {isExperimentEnabled, SLO_EXPERIMENT} from 'shared/experiments.js';
18import {EMPTY_FIELD_VALUE} from 'shared/issue-fields.js';
19import {HARDCODED_FIELD_GROUPS, valuesForField, fieldDefsWithGroup,
20 fieldDefsWithoutGroup} from 'shared/metadata-helpers.js';
21import 'shared/typedef.js';
22import {AVAILABLE_CUES, cueNames, specToCueName,
23 cueNameToSpec} from 'elements/help/mr-cue/cue-helpers.js';
24import {SHARED_STYLES} from 'shared/shared-styles.js';
25
26
27/**
28 * `<mr-metadata>`
29 *
30 * Generalized metadata components, used for either approvals or issues.
31 *
32 */
33export class MrMetadata extends connectStore(LitElement) {
34 /** @override */
35 static get styles() {
36 return [
37 SHARED_STYLES,
38 css`
39 :host {
40 display: table;
41 table-layout: fixed;
42 width: 100%;
43 }
44 td, th {
45 padding: 0.5em 4px;
46 vertical-align: top;
47 text-overflow: ellipsis;
48 overflow: hidden;
49 }
50 td {
51 width: 60%;
52 }
53 td.allow-overflow {
54 overflow: visible;
55 }
56 th {
57 text-align: left;
58 width: 40%;
59 }
60 .group-separator {
61 border-top: var(--chops-normal-border);
62 }
63 .group-title {
64 font-weight: normal;
65 font-style: oblique;
66 border-bottom: var(--chops-normal-border);
67 text-align: center;
68 }
69 `,
70 ];
71 }
72
73 /** @override */
74 render() {
75 return html`
76 <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
77 rel="stylesheet">
78 ${this._renderBuiltInFields()}
79 ${this._renderCustomFieldGroups()}
80 `;
81 }
82
83 /**
84 * Helper for handling the rendering of built in fields.
85 * @return {Array<TemplateResult>}
86 */
87 _renderBuiltInFields() {
88 return this.builtInFieldSpec.map((fieldName) => {
89 const fieldKey = fieldName.toLowerCase();
90
91 // Adding classes to table rows based on field names makes selecting
92 // rows with specific values easier, for example in tests.
93 let className = `row-${fieldKey}`;
94
95 const cueName = specToCueName(fieldKey);
96 if (cueName) {
97 className = `cue-${cueName}`;
98
99 if (!AVAILABLE_CUES.has(cueName)) return '';
100
101 return html`
102 <tr class=${className}>
103 <td colspan="2">
104 <mr-cue cuePrefName=${cueName}></mr-cue>
105 </td>
106 </tr>
107 `;
108 }
109
110 const isApprovalStatus = fieldKey === 'approvalstatus';
111 const isMergedInto = fieldKey === 'mergedinto';
112
113 const fieldValueTemplate = this._renderBuiltInFieldValue(fieldName);
114
115 if (!fieldValueTemplate) return '';
116
117 // Allow overflow to enable the FedRef popup to expand.
118 // TODO(jeffcarp): Look into a more elegant solution.
119 return html`
120 <tr class=${className}>
121 <th>${isApprovalStatus ? 'Status' : fieldName}:</th>
122 <td class=${isMergedInto ? 'allow-overflow' : ''}>
123 ${fieldValueTemplate}
124 </td>
125 </tr>
126 `;
127 });
128 }
129
130 /**
131 * A helper to display a single built-in field.
132 *
133 * @param {string} fieldName The name of the built in field to render.
134 * @return {TemplateResult|undefined} lit-html template for displaying the
135 * value of the built in field. If undefined, the rendering code assumes
136 * that the field should be hidden if empty.
137 */
138 _renderBuiltInFieldValue(fieldName) {
139 // TODO(zhangtiff): Merge with code in shared/issue-fields.js for further
140 // de-duplication.
141 switch (fieldName.toLowerCase()) {
142 case 'approvalstatus':
143 return this.approvalStatus || EMPTY_FIELD_VALUE;
144 case 'approvers':
145 return this.approvers && this.approvers.length ?
146 this.approvers.map((approver) => html`
147 <mr-user-link
148 .userRef=${approver}
149 showAvailabilityIcon
150 ></mr-user-link>
151 <br />
152 `) : EMPTY_FIELD_VALUE;
153 case 'setter':
154 return this.setter ? html`
155 <mr-user-link
156 .userRef=${this.setter}
157 showAvailabilityIcon
158 ></mr-user-link>
159 ` : undefined; // Hide the field when empty.
160 case 'owner':
161 return this.owner ? html`
162 <mr-user-link
163 .userRef=${this.owner}
164 showAvailabilityIcon
165 showAvailabilityText
166 ></mr-user-link>
167 ` : EMPTY_FIELD_VALUE;
168 case 'cc':
169 return this.cc && this.cc.length ?
170 this.cc.map((cc) => html`
171 <mr-user-link
172 .userRef=${cc}
173 showAvailabilityIcon
174 ></mr-user-link>
175 <br />
176 `) : EMPTY_FIELD_VALUE;
177 case 'status':
178 return this.issueStatus ? html`
179 ${this.issueStatus.status} <em>${
180 this.issueStatus.meansOpen ? '(Open)' : '(Closed)'}
181 </em>` : EMPTY_FIELD_VALUE;
182 case 'mergedinto':
183 // TODO(zhangtiff): This should use the project config to determine if a
184 // field allows merging rather than used a hard-coded value.
185 return this.issueStatus && this.issueStatus.status === 'Duplicate' ?
186 html`
187 <mr-issue-link
188 .projectName=${this.issueRef.projectName}
189 .issue=${this.mergedInto}
190 ></mr-issue-link>
191 `: undefined; // Hide the field when empty.
192 case 'components':
193 return (this.components && this.components.length) ?
194 this.components.map((comp) => html`
195 <a
196 href="/p/${this.issueRef.projectName
197 }/issues/list?q=component:${comp.path}"
198 title="${comp.path}${comp.docstring ?
199 ' = ' + comp.docstring : ''}"
200 >
201 ${comp.path}</a><br />
202 `) : EMPTY_FIELD_VALUE;
203 case 'modified':
204 return this.modifiedTimestamp ? html`
205 <chops-timestamp
206 .timestamp=${this.modifiedTimestamp}
207 short
208 ></chops-timestamp>
209 ` : EMPTY_FIELD_VALUE;
210 case 'slo':
211 if (isExperimentEnabled(
212 SLO_EXPERIMENT, this.currentUser, this.queryParams)) {
213 return html`<mr-issue-slo .issue=${this.issue}></mr-issue-slo>`;
214 } else {
215 return;
216 }
217 }
218
219 // Non-existent field.
220 return;
221 }
222
223 /**
224 * Helper for handling the rendering of custom fields defined in a project
225 * config.
226 * @return {TemplateResult} lit-html template.
227 */
228 _renderCustomFieldGroups() {
229 const grouped = fieldDefsWithGroup(this.fieldDefs,
230 this.fieldGroups, this.issueType);
231 const ungrouped = fieldDefsWithoutGroup(this.fieldDefs,
232 this.fieldGroups, this.issueType);
233 return html`
234 ${grouped.map((group) => html`
235 <tr>
236 <th class="group-title" colspan="2">
237 ${group.groupName}
238 </th>
239 </tr>
240 ${this._renderCustomFields(group.fieldDefs)}
241 <tr>
242 <th class="group-separator" colspan="2"></th>
243 </tr>
244 `)}
245
246 ${this._renderCustomFields(ungrouped)}
247 `;
248 }
249
250 /**
251 * Helper for handling the rendering of built in fields.
252 *
253 * @param {Array<FieldDef>} fieldDefs Arrays of configurations Objects
254 * for fields to render.
255 * @return {Array<TemplateResult>} Array of lit-html templates to render, each
256 * representing a single table row for a custom field.
257 */
258 _renderCustomFields(fieldDefs) {
259 if (!fieldDefs || !fieldDefs.length) return [];
260 return fieldDefs.map((field) => {
261 const fieldValues = valuesForField(
262 this.fieldValueMap, field.fieldRef.fieldName) || [];
263 return html`
264 <tr ?hidden=${field.isNiche && !fieldValues.length}>
265 <th title=${field.docstring}>${field.fieldRef.fieldName}:</th>
266 <td>
267 <mr-field-values
268 .name=${field.fieldRef.fieldName}
269 .type=${field.fieldRef.type}
270 .values=${fieldValues}
271 .projectName=${this.issueRef.projectName}
272 ></mr-field-values>
273 </td>
274 </tr>
275 `;
276 });
277 }
278
279 /** @override */
280 static get properties() {
281 return {
282 /**
283 * An Array of Strings to specify which built in fields to display.
284 */
285 builtInFieldSpec: {type: Array},
286 approvalStatus: {type: Array},
287 approvers: {type: Array},
288 setter: {type: Object},
289 cc: {type: Array},
290 components: {type: Array},
291 fieldDefs: {type: Array},
292 fieldGroups: {type: Array},
293 issue: {type: Object},
294 issueStatus: {type: String},
295 issueType: {type: String},
296 mergedInto: {type: Object},
297 modifiedTimestamp: {type: Number},
298 owner: {type: Object},
299 isApproval: {type: Boolean},
300 issueRef: {type: Object},
301 fieldValueMap: {type: Object},
302 currentUser: {type: Object},
303 queryParams: {type: Object},
304 };
305 }
306
307 /** @override */
308 constructor() {
309 super();
310 this.isApproval = false;
311 this.fieldGroups = HARDCODED_FIELD_GROUPS;
312 this.issueRef = {};
313
314 // Default built in fields used by issue metadata.
315 this.builtInFieldSpec = [
316 'Owner', 'CC', cueNameToSpec(cueNames.AVAILABILITY_MSGS),
317 'Status', 'MergedInto', 'Components', 'Modified', 'SLO',
318 ];
319 this.fieldValueMap = new Map();
320
321 this.approvalStatus = undefined;
322 this.approvers = undefined;
323 this.setter = undefined;
324 this.cc = undefined;
325 this.components = undefined;
326 this.fieldDefs = undefined;
327 this.issue = undefined;
328 this.issueStatus = undefined;
329 this.issueType = undefined;
330 this.mergedInto = undefined;
331 this.owner = undefined;
332 this.modifiedTimestamp = undefined;
333 this.currentUser = undefined;
334 this.queryParams = {};
335 }
336
337 /** @override */
338 connectedCallback() {
339 super.connectedCallback();
340
341 // This is set for accessibility. Do not override.
342 this.setAttribute('role', 'table');
343 }
344
345 /** @override */
346 stateChanged(state) {
347 this.fieldValueMap = issueV0.fieldValueMap(state);
348 this.issue = issueV0.viewedIssue(state);
349 this.issueType = issueV0.type(state);
350 this.issueRef = issueV0.viewedIssueRef(state);
351 this.relatedIssues = issueV0.relatedIssues(state);
352 this.currentUser = userV0.currentUser(state);
353 this.queryParams = sitewide.queryParams(state);
354 }
355}
356
357customElements.define('mr-metadata', MrMetadata);