Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/framework/dialogs/mr-issue-hotlists-action/mr-update-issue-hotlists-dialog.js b/static_src/elements/framework/dialogs/mr-issue-hotlists-action/mr-update-issue-hotlists-dialog.js
new file mode 100644
index 0000000..08a8b25
--- /dev/null
+++ b/static_src/elements/framework/dialogs/mr-issue-hotlists-action/mr-update-issue-hotlists-dialog.js
@@ -0,0 +1,340 @@
+// 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 {html, css} from 'lit-element';
+import deepEqual from 'deep-equal';
+
+import 'elements/chops/chops-checkbox/chops-checkbox.js';
+import {store} from 'reducers/base.js';
+import * as issueV0 from 'reducers/issueV0.js';
+import * as userV0 from 'reducers/userV0.js';
+import {prpcClient} from 'prpc-client-instance.js';
+import {MrIssueHotlistsDialog} from './mr-issue-hotlists-dialog';
+
+/**
+ * `<mr-update-issue-hotlists-dialog>`
+ *
+ * Displays a dialog with the current hotlists's issues allowing the user to
+ * update which hotlists the issues are a member of.
+ */
+export class MrUpdateIssueDialog extends MrIssueHotlistsDialog {
+  /** @override */
+  static get styles() {
+    return [
+      ...super.styles,
+      css`
+        input[type="checkbox"] {
+          width: auto;
+          height: auto;
+        }
+        button.toggle {
+          background: none;
+          color: hsl(240, 100%, 40%);
+          border: 0;
+          width: 100%;
+          padding: 0.25em 0;
+          text-align: left;
+        }
+        button.toggle:hover {
+          cursor: pointer;
+          text-decoration: underline;
+        }
+        label, chops-checkbox {
+          display: flex;
+          line-height: 200%;
+          align-items: center;
+          width: 100%;
+          text-align: left;
+          font-weight: normal;
+          padding: 0.25em 8px;
+          box-sizing: border-box;
+        }
+        label input[type="checkbox"] {
+          margin-right: 8px;
+        }
+        .discard-button {
+          margin-right: 16px;
+        }
+        .edit-actions {
+          width: 100%;
+          margin: 0.5em 0;
+          text-align: right;
+        }
+        .input-grid {
+          align-items: center;
+        }
+        .input-grid > input {
+          width: 200px;
+          max-width: 100%;
+        }
+      `,
+    ];
+  }
+
+  /** @override */
+  renderHeader() {
+    return html`
+      <h3 class="medium-heading">Add issue to hotlists</h3>
+    `;
+  }
+
+  /** @override */
+  renderContent() {
+    return html`
+      ${this.renderFilter()}
+      <form id="issueHotlistsForm">
+        ${this.renderHotlists()}
+        <h3 class="medium-heading">Create new hotlist</h3>
+        <div class="input-grid">
+          <label for="newHotlistName">New hotlist name:</label>
+          <input type="text" name="newHotlistName">
+        </div>
+        ${this.renderError()}
+        <div class="edit-actions">
+          <chops-button
+            class="de-emphasized discard-button"
+            ?disabled=${this.disabled}
+            @click=${this.discard}
+          >
+            Discard
+          </chops-button>
+          <chops-button
+            class="emphasized"
+            ?disabled=${this.disabled}
+            @click=${this.save}
+          >
+            Save changes
+          </chops-button>
+        </div>
+      </form>
+    `;
+  }
+
+  /** @override */
+  renderFilteredHotlist(hotlist) {
+    return html`
+      <chops-checkbox
+        class="hotlist"
+        title=${this._checkboxTitle(hotlist, this.issueHotlists)}
+        data-hotlist-name="${hotlist.name}"
+        ?checked=${this.hotlistsToAdd.has(hotlist.name)}
+        @checked-change=${this._targetHotlistChecked}
+      >
+        ${hotlist.name}
+      </chops-checkbox>`;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      ...super.properties,
+      viewedIssueRef: {type: Object},
+      issueHotlists: {type: Array},
+      user: {type: Object},
+      hotlistsToAdd: {
+        type: Object,
+        hasChanged(newVal, oldVal) {
+          return !deepEqual(newVal, oldVal);
+        },
+      },
+    };
+  }
+
+  /** @override */
+  stateChanged(state) {
+    super.stateChanged(state);
+    this.viewedIssueRef = issueV0.viewedIssueRef(state);
+    this.user = userV0.currentUser(state);
+  }
+
+  /** @override */
+  constructor() {
+    super();
+
+    /** The list of Hotlists attached to the issueRefs. */
+    this.issueHotlists = [];
+
+    /** The Set of Hotlist names that the Issues will be added to. */
+    this.hotlistsToAdd = this._initializeHotlistsToAdd();
+  }
+
+  /** @override */
+  reset() {
+    const form = this.shadowRoot.querySelector('#issueHotlistsForm');
+    form.reset();
+    // LitElement's hasChanged needs an assignment to verify Set objects.
+    // https://lit-element.polymer-project.org/guide/properties#haschanged
+    this.hotlistsToAdd = this._initializeHotlistsToAdd();
+    super.reset();
+  }
+
+  /**
+   * An alias to the close method.
+   */
+  discard() {
+    this.close();
+  }
+
+  /**
+   * Saves all changes that were found in the dialog and issues async requests
+   * to update the issues.
+   * @fires Event#saveSuccess
+   */
+  async save() {
+    const changes = this.changes;
+    const issueRefs = this.issueRefs;
+    const viewedRef = this.viewedIssueRef;
+
+    if (!issueRefs || !changes) return;
+
+    // TODO(https://crbug.com/monorail/7778): Use action creators.
+    const promises = [];
+    if (changes.added && changes.added.length) {
+      promises.push(prpcClient.call(
+          'monorail.Features', 'AddIssuesToHotlists', {
+            hotlistRefs: changes.added,
+            issueRefs,
+          },
+      ));
+    }
+    if (changes.removed && changes.removed.length) {
+      promises.push(prpcClient.call(
+          'monorail.Features', 'RemoveIssuesFromHotlists', {
+            hotlistRefs: changes.removed,
+            issueRefs,
+          },
+      ));
+    }
+    if (changes.created) {
+      promises.push(prpcClient.call(
+          'monorail.Features', 'CreateHotlist', {
+            name: changes.created.name,
+            summary: changes.created.summary,
+            issueRefs,
+          },
+      ));
+    }
+
+    try {
+      await Promise.all(promises);
+
+      // Refresh the viewed issue's hotlists only if there is a viewed issue.
+      if (viewedRef) {
+        const viewedIssueWasUpdated = issueRefs.find((ref) =>
+          ref.projectName === viewedRef.projectName &&
+          ref.localId === viewedRef.localId);
+        if (viewedIssueWasUpdated) {
+          store.dispatch(issueV0.fetchHotlists(viewedRef));
+        }
+      }
+      store.dispatch(userV0.fetchHotlists({userId: this.user.userId}));
+      this.dispatchEvent(new Event('saveSuccess'));
+      this.close();
+    } catch (error) {
+      this.error = error.description;
+    }
+  }
+
+  /**
+   * Returns whether a given hotlist matches any of the given issue's hotlists.
+   * @param {Hotlist} hotlist Hotlist to look for.
+   * @param {Array<Hotlist>} issueHotlists Issue's hotlists to compare to.
+   * @return {boolean}
+   */
+  _issueInHotlist(hotlist, issueHotlists) {
+    return issueHotlists.some((issueHotlist) => {
+      // TODO(https://crbug.com/monorail/7451): use `===`.
+      return (hotlist.ownerRef.userId == issueHotlist.ownerRef.userId &&
+        hotlist.name === issueHotlist.name);
+    });
+  }
+
+  /**
+   * Get a Set of Hotlists to add the Issues to based on the
+   * Get the initial Set of Hotlists that Issues will be added to. Calculated
+   * using userHotlists and issueHotlists.
+   * @return {!Set<string>}
+   */
+  _initializeHotlistsToAdd() {
+    const userHotlistsInIssueHotlists = this.userHotlists.reduce(
+        (acc, hotlist) => {
+          if (this._issueInHotlist(hotlist, this.issueHotlists)) {
+            acc.push(hotlist.name);
+          }
+          return acc;
+        }, []);
+    return new Set(userHotlistsInIssueHotlists);
+  }
+
+  /**
+   * Gets the checkbox title, depending on the checked state.
+   * @param {boolean} isChecked Whether the input is checked.
+   * @return {string}
+   */
+  _getCheckboxTitle(isChecked) {
+    return (isChecked ? 'Remove issue from' : 'Add issue to') + ' this hotlist';
+  }
+
+  /**
+   * The checkbox title for the issue, shown on hover and for a11y.
+   * @param {Hotlist} hotlist Hotlist to look for.
+   * @param {Array<Hotlist>} issueHotlists Issue's hotlists to compare to.
+   * @return {string}
+   */
+  _checkboxTitle(hotlist, issueHotlists) {
+    return this._getCheckboxTitle(this._issueInHotlist(hotlist, issueHotlists));
+  }
+
+  /**
+   * Handles when the target Hotlist chops-checkbox has been checked.
+   * @param {Event} e
+   */
+  _targetHotlistChecked(e) {
+    const hotlistName = e.target.dataset.hotlistName;
+    const currentHotlistsToAdd = new Set(this.hotlistsToAdd);
+    if (hotlistName && e.detail.checked) {
+      currentHotlistsToAdd.add(hotlistName);
+    } else {
+      currentHotlistsToAdd.delete(hotlistName);
+    }
+    // LitElement's hasChanged needs an assignment to verify Set objects.
+    // https://lit-element.polymer-project.org/guide/properties#haschanged
+    this.hotlistsToAdd = currentHotlistsToAdd;
+    e.target.title = this._getCheckboxTitle(e.target.checked);
+  }
+
+  /**
+   * Gets the changes between the added, removed, and created hotlists .
+   */
+  get changes() {
+    const changes = {
+      added: [],
+      removed: [],
+    };
+    const form = this.shadowRoot.querySelector('#issueHotlistsForm');
+    this.userHotlists.forEach((hotlist) => {
+      const issueInHotlist = this._issueInHotlist(hotlist, this.issueHotlists);
+      if (issueInHotlist && !this.hotlistsToAdd.has(hotlist.name)) {
+        changes.removed.push({
+          name: hotlist.name,
+          owner: hotlist.ownerRef,
+        });
+      } else if (!issueInHotlist && this.hotlistsToAdd.has(hotlist.name)) {
+        changes.added.push({
+          name: hotlist.name,
+          owner: hotlist.ownerRef,
+        });
+      }
+    });
+    if (form.newHotlistName.value) {
+      changes.created = {
+        name: form.newHotlistName.value,
+        summary: 'Hotlist created from issue.',
+      };
+    }
+    return changes;
+  }
+}
+
+customElements.define('mr-update-issue-hotlists-dialog', MrUpdateIssueDialog);