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);