Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-detail/metadata/mr-metadata/mr-metadata.js b/static_src/elements/issue-detail/metadata/mr-metadata/mr-metadata.js
new file mode 100644
index 0000000..0ce172d
--- /dev/null
+++ b/static_src/elements/issue-detail/metadata/mr-metadata/mr-metadata.js
@@ -0,0 +1,357 @@
+// 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, css} from 'lit-element';
+
+import {connectStore} from 'reducers/base.js';
+import 'elements/chops/chops-timestamp/chops-timestamp.js';
+import 'elements/framework/links/mr-issue-link/mr-issue-link.js';
+import 'elements/framework/links/mr-user-link/mr-user-link.js';
+import 'elements/framework/mr-issue-slo/mr-issue-slo.js';
+
+import * as issueV0 from 'reducers/issueV0.js';
+import * as sitewide from 'reducers/sitewide.js';
+import * as userV0 from 'reducers/userV0.js';
+import './mr-field-values.js';
+import {isExperimentEnabled, SLO_EXPERIMENT} from 'shared/experiments.js';
+import {EMPTY_FIELD_VALUE} from 'shared/issue-fields.js';
+import {HARDCODED_FIELD_GROUPS, valuesForField, fieldDefsWithGroup,
+ fieldDefsWithoutGroup} from 'shared/metadata-helpers.js';
+import 'shared/typedef.js';
+import {AVAILABLE_CUES, cueNames, specToCueName,
+ cueNameToSpec} from 'elements/help/mr-cue/cue-helpers.js';
+import {SHARED_STYLES} from 'shared/shared-styles.js';
+
+
+/**
+ * `<mr-metadata>`
+ *
+ * Generalized metadata components, used for either approvals or issues.
+ *
+ */
+export class MrMetadata extends connectStore(LitElement) {
+ /** @override */
+ static get styles() {
+ return [
+ SHARED_STYLES,
+ css`
+ :host {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+ }
+ td, th {
+ padding: 0.5em 4px;
+ vertical-align: top;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ td {
+ width: 60%;
+ }
+ td.allow-overflow {
+ overflow: visible;
+ }
+ th {
+ text-align: left;
+ width: 40%;
+ }
+ .group-separator {
+ border-top: var(--chops-normal-border);
+ }
+ .group-title {
+ font-weight: normal;
+ font-style: oblique;
+ border-bottom: var(--chops-normal-border);
+ text-align: center;
+ }
+ `,
+ ];
+ }
+
+ /** @override */
+ render() {
+ return html`
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
+ rel="stylesheet">
+ ${this._renderBuiltInFields()}
+ ${this._renderCustomFieldGroups()}
+ `;
+ }
+
+ /**
+ * Helper for handling the rendering of built in fields.
+ * @return {Array<TemplateResult>}
+ */
+ _renderBuiltInFields() {
+ return this.builtInFieldSpec.map((fieldName) => {
+ const fieldKey = fieldName.toLowerCase();
+
+ // Adding classes to table rows based on field names makes selecting
+ // rows with specific values easier, for example in tests.
+ let className = `row-${fieldKey}`;
+
+ const cueName = specToCueName(fieldKey);
+ if (cueName) {
+ className = `cue-${cueName}`;
+
+ if (!AVAILABLE_CUES.has(cueName)) return '';
+
+ return html`
+ <tr class=${className}>
+ <td colspan="2">
+ <mr-cue cuePrefName=${cueName}></mr-cue>
+ </td>
+ </tr>
+ `;
+ }
+
+ const isApprovalStatus = fieldKey === 'approvalstatus';
+ const isMergedInto = fieldKey === 'mergedinto';
+
+ const fieldValueTemplate = this._renderBuiltInFieldValue(fieldName);
+
+ if (!fieldValueTemplate) return '';
+
+ // Allow overflow to enable the FedRef popup to expand.
+ // TODO(jeffcarp): Look into a more elegant solution.
+ return html`
+ <tr class=${className}>
+ <th>${isApprovalStatus ? 'Status' : fieldName}:</th>
+ <td class=${isMergedInto ? 'allow-overflow' : ''}>
+ ${fieldValueTemplate}
+ </td>
+ </tr>
+ `;
+ });
+ }
+
+ /**
+ * A helper to display a single built-in field.
+ *
+ * @param {string} fieldName The name of the built in field to render.
+ * @return {TemplateResult|undefined} lit-html template for displaying the
+ * value of the built in field. If undefined, the rendering code assumes
+ * that the field should be hidden if empty.
+ */
+ _renderBuiltInFieldValue(fieldName) {
+ // TODO(zhangtiff): Merge with code in shared/issue-fields.js for further
+ // de-duplication.
+ switch (fieldName.toLowerCase()) {
+ case 'approvalstatus':
+ return this.approvalStatus || EMPTY_FIELD_VALUE;
+ case 'approvers':
+ return this.approvers && this.approvers.length ?
+ this.approvers.map((approver) => html`
+ <mr-user-link
+ .userRef=${approver}
+ showAvailabilityIcon
+ ></mr-user-link>
+ <br />
+ `) : EMPTY_FIELD_VALUE;
+ case 'setter':
+ return this.setter ? html`
+ <mr-user-link
+ .userRef=${this.setter}
+ showAvailabilityIcon
+ ></mr-user-link>
+ ` : undefined; // Hide the field when empty.
+ case 'owner':
+ return this.owner ? html`
+ <mr-user-link
+ .userRef=${this.owner}
+ showAvailabilityIcon
+ showAvailabilityText
+ ></mr-user-link>
+ ` : EMPTY_FIELD_VALUE;
+ case 'cc':
+ return this.cc && this.cc.length ?
+ this.cc.map((cc) => html`
+ <mr-user-link
+ .userRef=${cc}
+ showAvailabilityIcon
+ ></mr-user-link>
+ <br />
+ `) : EMPTY_FIELD_VALUE;
+ case 'status':
+ return this.issueStatus ? html`
+ ${this.issueStatus.status} <em>${
+ this.issueStatus.meansOpen ? '(Open)' : '(Closed)'}
+ </em>` : EMPTY_FIELD_VALUE;
+ case 'mergedinto':
+ // TODO(zhangtiff): This should use the project config to determine if a
+ // field allows merging rather than used a hard-coded value.
+ return this.issueStatus && this.issueStatus.status === 'Duplicate' ?
+ html`
+ <mr-issue-link
+ .projectName=${this.issueRef.projectName}
+ .issue=${this.mergedInto}
+ ></mr-issue-link>
+ `: undefined; // Hide the field when empty.
+ case 'components':
+ return (this.components && this.components.length) ?
+ this.components.map((comp) => html`
+ <a
+ href="/p/${this.issueRef.projectName
+ }/issues/list?q=component:${comp.path}"
+ title="${comp.path}${comp.docstring ?
+ ' = ' + comp.docstring : ''}"
+ >
+ ${comp.path}</a><br />
+ `) : EMPTY_FIELD_VALUE;
+ case 'modified':
+ return this.modifiedTimestamp ? html`
+ <chops-timestamp
+ .timestamp=${this.modifiedTimestamp}
+ short
+ ></chops-timestamp>
+ ` : EMPTY_FIELD_VALUE;
+ case 'slo':
+ if (isExperimentEnabled(
+ SLO_EXPERIMENT, this.currentUser, this.queryParams)) {
+ return html`<mr-issue-slo .issue=${this.issue}></mr-issue-slo>`;
+ } else {
+ return;
+ }
+ }
+
+ // Non-existent field.
+ return;
+ }
+
+ /**
+ * Helper for handling the rendering of custom fields defined in a project
+ * config.
+ * @return {TemplateResult} lit-html template.
+ */
+ _renderCustomFieldGroups() {
+ const grouped = fieldDefsWithGroup(this.fieldDefs,
+ this.fieldGroups, this.issueType);
+ const ungrouped = fieldDefsWithoutGroup(this.fieldDefs,
+ this.fieldGroups, this.issueType);
+ return html`
+ ${grouped.map((group) => html`
+ <tr>
+ <th class="group-title" colspan="2">
+ ${group.groupName}
+ </th>
+ </tr>
+ ${this._renderCustomFields(group.fieldDefs)}
+ <tr>
+ <th class="group-separator" colspan="2"></th>
+ </tr>
+ `)}
+
+ ${this._renderCustomFields(ungrouped)}
+ `;
+ }
+
+ /**
+ * Helper for handling the rendering of built in fields.
+ *
+ * @param {Array<FieldDef>} fieldDefs Arrays of configurations Objects
+ * for fields to render.
+ * @return {Array<TemplateResult>} Array of lit-html templates to render, each
+ * representing a single table row for a custom field.
+ */
+ _renderCustomFields(fieldDefs) {
+ if (!fieldDefs || !fieldDefs.length) return [];
+ return fieldDefs.map((field) => {
+ const fieldValues = valuesForField(
+ this.fieldValueMap, field.fieldRef.fieldName) || [];
+ return html`
+ <tr ?hidden=${field.isNiche && !fieldValues.length}>
+ <th title=${field.docstring}>${field.fieldRef.fieldName}:</th>
+ <td>
+ <mr-field-values
+ .name=${field.fieldRef.fieldName}
+ .type=${field.fieldRef.type}
+ .values=${fieldValues}
+ .projectName=${this.issueRef.projectName}
+ ></mr-field-values>
+ </td>
+ </tr>
+ `;
+ });
+ }
+
+ /** @override */
+ static get properties() {
+ return {
+ /**
+ * An Array of Strings to specify which built in fields to display.
+ */
+ builtInFieldSpec: {type: Array},
+ approvalStatus: {type: Array},
+ approvers: {type: Array},
+ setter: {type: Object},
+ cc: {type: Array},
+ components: {type: Array},
+ fieldDefs: {type: Array},
+ fieldGroups: {type: Array},
+ issue: {type: Object},
+ issueStatus: {type: String},
+ issueType: {type: String},
+ mergedInto: {type: Object},
+ modifiedTimestamp: {type: Number},
+ owner: {type: Object},
+ isApproval: {type: Boolean},
+ issueRef: {type: Object},
+ fieldValueMap: {type: Object},
+ currentUser: {type: Object},
+ queryParams: {type: Object},
+ };
+ }
+
+ /** @override */
+ constructor() {
+ super();
+ this.isApproval = false;
+ this.fieldGroups = HARDCODED_FIELD_GROUPS;
+ this.issueRef = {};
+
+ // Default built in fields used by issue metadata.
+ this.builtInFieldSpec = [
+ 'Owner', 'CC', cueNameToSpec(cueNames.AVAILABILITY_MSGS),
+ 'Status', 'MergedInto', 'Components', 'Modified', 'SLO',
+ ];
+ this.fieldValueMap = new Map();
+
+ this.approvalStatus = undefined;
+ this.approvers = undefined;
+ this.setter = undefined;
+ this.cc = undefined;
+ this.components = undefined;
+ this.fieldDefs = undefined;
+ this.issue = undefined;
+ this.issueStatus = undefined;
+ this.issueType = undefined;
+ this.mergedInto = undefined;
+ this.owner = undefined;
+ this.modifiedTimestamp = undefined;
+ this.currentUser = undefined;
+ this.queryParams = {};
+ }
+
+ /** @override */
+ connectedCallback() {
+ super.connectedCallback();
+
+ // This is set for accessibility. Do not override.
+ this.setAttribute('role', 'table');
+ }
+
+ /** @override */
+ stateChanged(state) {
+ this.fieldValueMap = issueV0.fieldValueMap(state);
+ this.issue = issueV0.viewedIssue(state);
+ this.issueType = issueV0.type(state);
+ this.issueRef = issueV0.viewedIssueRef(state);
+ this.relatedIssues = issueV0.relatedIssues(state);
+ this.currentUser = userV0.currentUser(state);
+ this.queryParams = sitewide.queryParams(state);
+ }
+}
+
+customElements.define('mr-metadata', MrMetadata);