Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/framework/mr-issue-list/mr-show-columns-dropdown.js b/static_src/elements/framework/mr-issue-list/mr-show-columns-dropdown.js
new file mode 100644
index 0000000..5d6a97b
--- /dev/null
+++ b/static_src/elements/framework/mr-issue-list/mr-show-columns-dropdown.js
@@ -0,0 +1,310 @@
+// 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 {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);