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