blob: 08a8b2588927ad0a9b47d1c81feddd51ec69891f [file] [log] [blame]
// 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);