| // Copyright 2019 The Chromium Authors |
| // 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); |