blob: 96b81bebafaaa767e5375e5fb491e9f9a8fbbfec [file] [log] [blame]
// 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 {css} from 'lit-element';
import {MrDropdown} from 'elements/framework/mr-dropdown/mr-dropdown.js';
import page from 'page';
import qs from 'qs';
import {connectStore} from 'reducers/base.js';
import * as projectV0 from 'reducers/projectV0.js';
import * as sitewide from 'reducers/sitewide.js';
import {fieldTypes, fieldsForIssue} from 'shared/issue-fields.js';
/**
* `<mr-show-columns-dropdown>`
*
* Issue list column options dropdown.
*
*/
export class MrShowColumnsDropdown extends connectStore(MrDropdown) {
/** @override */
static get styles() {
return [
...MrDropdown.styles,
css`
:host {
font-weight: normal;
color: var(--chops-link-color);
--mr-dropdown-icon-color: var(--chops-link-color);
--mr-dropdown-anchor-padding: 3px 8px;
--mr-dropdown-anchor-font-weight: bold;
--mr-dropdown-menu-min-width: 150px;
--mr-dropdown-menu-font-size: var(--chops-main-font-size);
--mr-dropdown-menu-icon-size: var(--chops-main-font-size);
/* Because we're using a sticky header, we need to make sure the
* dropdown cannot be taller than the screen. */
--mr-dropdown-menu-max-height: 80vh;
--mr-dropdown-menu-overflow: auto;
}
`,
];
}
/** @override */
static get properties() {
return {
...MrDropdown.properties,
/**
* Array of displayed columns.
*/
columns: {type: Array},
/**
* Array of displayed issues.
*/
issues: {type: Array},
/**
* Array of unique phase names to prepend to phase field columns.
*/
// TODO(dtu): Delete after removing EZT hotlist issue list.
phaseNames: {type: Array},
/**
* Array of built in fields that are available outside of project
* configuration.
*/
defaultFields: {type: Array},
_fieldDefs: {type: Array},
_labelPrefixFields: {type: Array},
// TODO(zhangtiff): Delete this legacy integration after removing
// the EZT issue list view.
onHideColumn: {type: Object},
onShowColumn: {type: Object},
};
}
/** @override */
constructor() {
super();
// Inherited from MrDropdown.
this.label = 'Show columns';
this.icon = 'more_horiz';
this.columns = [];
/** @type {Array<Issue>} */
this.issues = [];
this.phaseNames = [];
this.defaultFields = [];
// TODO(dtu): Delete after removing EZT hotlist issue list.
this._fieldDefs = [];
this._labelPrefixFields = [];
this._queryParams = {};
this._page = page;
// TODO(zhangtiff): Delete this legacy integration after removing
// the EZT issue list view.
this.onHideColumn = null;
this.onShowColumn = null;
}
/** @override */
stateChanged(state) {
this._fieldDefs = projectV0.fieldDefs(state) || [];
this._labelPrefixFields = projectV0.labelPrefixFields(state) || [];
this._queryParams = sitewide.queryParams(state);
}
/** @override */
update(changedProperties) {
if (this.issues.length) {
this.items = this.columnOptions();
} else {
// TODO(dtu): Delete after removing EZT hotlist issue list.
this.items = this.columnOptionsEzt(
this.defaultFields, this._fieldDefs, this._labelPrefixFields,
this.columns, this.phaseNames);
}
super.update(changedProperties);
}
/**
* Computes the column options available in the list view based on Issues.
* @return {Array<MenuItem>}
*/
columnOptions() {
const availableFields = new Set(this.defaultFields);
this.issues.forEach((issue) => {
fieldsForIssue(issue).forEach((field) => {
availableFields.add(field);
});
});
// Remove selected columns from available fields.
this.columns.forEach((field) => availableFields.delete(field));
const sortedFields = [...availableFields].sort();
return [
// Show selected options first.
...this.columns.map((field, i) => ({
icon: 'check',
text: field,
handler: () => this._removeColumn(i),
})),
// Unselected options come next.
...sortedFields.map((field) => ({
icon: '',
text: field,
handler: () => this._addColumn(field),
})),
];
}
// TODO(dtu): Delete after removing EZT hotlist issue list.
/**
* Computes the column options available in the list view based on project
* config data.
* @param {Array<string>} defaultFields List of built in columns.
* @param {Array<FieldDef>} fieldDefs List of custom fields configured in the
* viewed project.
* @param {Array<string>} labelPrefixes List of available label prefixes for
* the current project config..
* @param {Array<string>} selectedColumns List of columns the user is
* currently viewing.
* @param {Array<string>} phaseNames All phase namws present in the currently
* viewed issue list.
* @return {Array<MenuItem>}
*/
columnOptionsEzt(defaultFields, fieldDefs, labelPrefixes, selectedColumns,
phaseNames) {
const selectedOptions = new Set(
selectedColumns.map((col) => col.toLowerCase()));
const availableFields = new Set();
// Built-in, hard-coded fields like Owner, Status, and Labels.
defaultFields.forEach((field) => this._addUnselectedField(
availableFields, field, selectedOptions));
// Custom fields.
fieldDefs.forEach((fd) => {
const {fieldRef, isPhaseField} = fd;
const {fieldName, type} = fieldRef;
if (isPhaseField) {
// If the custom field belongs to phases, prefix the phase name for
// each phase.
phaseNames.forEach((phaseName) => {
this._addUnselectedField(
availableFields, `${phaseName}.${fieldName}`, selectedOptions);
});
return;
}
// TODO(zhangtiff): Prefix custom fields with "approvalName" defined by
// the approval name after deprecating the old issue list page.
// Most custom fields can be directly added to the list with no
// modifications.
this._addUnselectedField(
availableFields, fieldName, selectedOptions);
// If the custom field is type approval, then it also has a built in
// "Approver" field.
if (type === fieldTypes.APPROVAL_TYPE) {
this._addUnselectedField(
availableFields, `${fieldName}-Approver`, selectedOptions);
}
});
// Fields inferred from label prefixes.
labelPrefixes.forEach((field) => this._addUnselectedField(
availableFields, field, selectedOptions));
const sortedFields = [...availableFields];
sortedFields.sort();
return [
...selectedColumns.map((field, i) => ({
icon: 'check',
text: field,
handler: () => this._removeColumn(i),
})),
...sortedFields.map((field) => ({
icon: '',
text: field,
handler: () => this._addColumn(field),
})),
];
}
/**
* Helper that mutates a Set of column names in place, adding a given
* field only if it doesn't already show up in the list of selected
* fields.
* @param {Set<string>} availableFields Set of column names to mutate.
* @param {string} field Name of the field being added to the options.
* @param {Set<string>} selectedOptions Set of fieldNames that the user
* is viewing.
* @private
*/
_addUnselectedField(availableFields, field, selectedOptions) {
if (!selectedOptions.has(field.toLowerCase())) {
availableFields.add(field);
}
}
/**
* Removes the column at a particular index.
*
* @param {number} i the issue column to be removed.
*/
_removeColumn(i) {
if (this.onHideColumn) {
if (!this.onHideColumn(this.columns[i])) {
return;
}
}
const columns = [...this.columns];
columns.splice(i, 1);
this._reloadColspec(columns);
}
/**
* Adds a new column to a particular index.
*
* @param {string} name of the new column added.
*/
_addColumn(name) {
if (this.onShowColumn) {
if (!this.onShowColumn(name)) {
return;
}
}
this._reloadColspec([...this.columns, name]);
}
/**
* Reflects changes to the columns of an issue list to the URL, through
* frontend routing.
*
* @param {Array} newColumns the new colspec to set in the URL.
*/
_reloadColspec(newColumns) {
this._updateQueryParams({colspec: newColumns.join(' ')});
}
/**
* Navigates to the same URL as the current page, but with query
* params updated.
*
* @param {Object} newParams keys and values of the queryParams
* Object to be updated.
*/
_updateQueryParams(newParams) {
const params = {...this._queryParams, ...newParams};
this._page(`${this._baseUrl()}?${qs.stringify(params)}`);
}
/**
* Get the current URL of the page, without query params. Useful for
* test stubbing.
*
* @return {string} the URL of the list page, without params.
*/
_baseUrl() {
return window.location.pathname;
}
}
customElements.define('mr-show-columns-dropdown', MrShowColumnsDropdown);