Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-detail/dialogs/mr-related-issues/mr-related-issues.js b/static_src/elements/issue-detail/dialogs/mr-related-issues/mr-related-issues.js
new file mode 100644
index 0000000..e859bef
--- /dev/null
+++ b/static_src/elements/issue-detail/dialogs/mr-related-issues/mr-related-issues.js
@@ -0,0 +1,316 @@
+// 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 {LitElement, html, css} from 'lit-element';
+
+import 'elements/chops/chops-dialog/chops-dialog.js';
+import 'elements/framework/links/mr-issue-link/mr-issue-link.js';
+import {store, connectStore} from 'reducers/base.js';
+import * as issueV0 from 'reducers/issueV0.js';
+import {SHARED_STYLES} from 'shared/shared-styles.js';
+import {ISSUE_EDIT_PERMISSION} from 'shared/consts/permissions';
+import {prpcClient} from 'prpc-client-instance.js';
+
+/**
+ * `<mr-related-issues>`
+ *
+ * Component for showing a mini list view of blocking issues to users.
+ */
+export class MrRelatedIssues extends connectStore(LitElement) {
+ /** @override */
+ static get styles() {
+ return [
+ SHARED_STYLES,
+ css`
+ :host {
+ display: block;
+ font-size: var(--chops-main-font-size);
+ }
+ table {
+ word-wrap: break-word;
+ width: 100%;
+ }
+ tr {
+ font-weight: normal;
+ text-align: left;
+ margin: 0 auto;
+ padding: 2em 1em;
+ height: 20px;
+ }
+ td {
+ background: #f8f8f8;
+ padding: 4px;
+ padding-left: 8px;
+ text-overflow: ellipsis;
+ }
+ th {
+ text-decoration: none;
+ margin-right: 0;
+ padding-right: 0;
+ padding-left: 8px;
+ white-space: nowrap;
+ background: #e3e9ff;
+ text-align: left;
+ border-right: 1px solid #fff;
+ border-top: 1px solid #fff;
+ }
+ tr.dragged td {
+ background: #eee;
+ }
+ h3.medium-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ }
+ button {
+ background: none;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ margin: 0;
+ padding: 0;
+ }
+ i.material-icons {
+ font-size: var(--chops-icon-font-size);
+ color: var(--chops-primary-icon-color);
+ }
+ .draggable {
+ cursor: grab;
+ }
+ .error {
+ max-width: 100%;
+ color: red;
+ margin-bottom: 1em;
+ }
+ `,
+ ];
+ }
+
+ /** @override */
+ render() {
+ const rerankEnabled = (this.issuePermissions ||
+ []).includes(ISSUE_EDIT_PERMISSION);
+ return html`
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+ <chops-dialog closeOnOutsideClick>
+ <h3 class="medium-heading">
+ <span>Blocked on issues</span>
+ <button aria-label="close" @click=${this.close}>
+ <i class="material-icons">close</i>
+ </button>
+ </h3>
+ ${this.error ? html`
+ <div class="error">${this.error}</div>
+ ` : ''}
+ <table><tbody>
+ <tr>
+ ${rerankEnabled ? html`<th></th>` : ''}
+ ${this.columns.map((column) => html`
+ <th>${column}</th>
+ `)}
+ </tr>
+
+ ${this._renderedRows.map((row, index) => html`
+ <tr
+ class=${index === this.srcIndex ? 'dragged' : ''}
+ draggable=${rerankEnabled && row.draggable}
+ data-index=${index}
+ @dragstart=${this._dragstart}
+ @dragend=${this._dragend}
+ @dragover=${this._dragover}
+ @drop=${this._dragdrop}
+ >
+ ${rerankEnabled ? html`
+ <td>
+ ${rerankEnabled && row.draggable ? html`
+ <i class="material-icons draggable">drag_indicator</i>
+ ` : ''}
+ </td>
+ ` : ''}
+
+ ${row.cells.map((cell) => html`
+ <td>
+ ${cell.type === 'issue' ? html`
+ <mr-issue-link
+ .projectName=${this.issueRef.projectName}
+ .issue=${cell.issue}
+ ></mr-issue-link>
+ ` : ''}
+ ${cell.type === 'text' ? cell.content : ''}
+ </td>
+ `)}
+ </tr>
+ `)}
+ </tbody></table>
+ </chops-dialog>
+ `;
+ }
+
+ /** @override */
+ static get properties() {
+ return {
+ columns: {type: Array},
+ error: {type: String},
+ srcIndex: {type: Number},
+ issueRef: {type: Object},
+ issuePermissions: {type: Array},
+ sortedBlockedOn: {type: Array},
+ _renderedRows: {type: Array},
+ };
+ }
+
+ /** @override */
+ stateChanged(state) {
+ this.issueRef = issueV0.viewedIssueRef(state);
+ this.issuePermissions = issueV0.permissions(state);
+ this.sortedBlockedOn = issueV0.sortedBlockedOn(state);
+ }
+
+ /** @override */
+ constructor() {
+ super();
+ this.columns = ['Issue', 'Summary'];
+ }
+
+ /** @override */
+ update(changedProperties) {
+ if (changedProperties.has('sortedBlockedOn')) {
+ this.reset();
+ }
+ super.update(changedProperties);
+ }
+
+ /** @override */
+ updated(changedProperties) {
+ if (changedProperties.has('issueRef')) {
+ this.close();
+ }
+ }
+
+ get _rows() {
+ const blockedOn = this.sortedBlockedOn;
+ if (!blockedOn) return [];
+ return blockedOn.map((issue) => {
+ const isClosed = issue.statusRef ? !issue.statusRef.meansOpen : false;
+ let summary = issue.summary;
+ if (issue.extIdentifier) {
+ // Some federated references will have summaries.
+ summary = issue.summary || '(not available)';
+ }
+ const row = {
+ // Disallow reranking FedRefs/DanglingIssueRelations.
+ draggable: !isClosed && !issue.extIdentifier,
+ cells: [
+ {
+ type: 'issue',
+ issue: issue,
+ isClosed: Boolean(isClosed),
+ },
+ {
+ type: 'text',
+ content: summary,
+ },
+ ],
+ };
+ return row;
+ });
+ }
+
+ async open() {
+ await this.updateComplete;
+ this.reset();
+ this.shadowRoot.querySelector('chops-dialog').open();
+ }
+
+ close() {
+ this.shadowRoot.querySelector('chops-dialog').close();
+ }
+
+ reset() {
+ this.error = null;
+ this.srcIndex = null;
+ this._renderedRows = this._rows.slice();
+ }
+
+ _dragstart(e) {
+ if (e.currentTarget.draggable) {
+ this.srcIndex = Number(e.currentTarget.dataset.index);
+ e.dataTransfer.setDragImage(new Image(), 0, 0);
+ }
+ }
+
+ _dragover(e) {
+ if (e.currentTarget.draggable && this.srcIndex !== null) {
+ e.preventDefault();
+ const targetIndex = Number(e.currentTarget.dataset.index);
+ this._reorderRows(this.srcIndex, targetIndex);
+ this.srcIndex = targetIndex;
+ }
+ }
+
+ _dragend(e) {
+ if (this.srcIndex !== null) {
+ this.reset();
+ }
+ }
+
+ _dragdrop(e) {
+ if (e.currentTarget.draggable && this.srcIndex !== null) {
+ const src = this._renderedRows[this.srcIndex];
+ if (this.srcIndex > 0) {
+ const target = this._renderedRows[this.srcIndex - 1];
+ const above = false;
+ this._reorderBlockedOn(src, target, above);
+ } else if (this.srcIndex === 0 &&
+ this._renderedRows[1] && this._renderedRows[1].draggable) {
+ const target = this._renderedRows[1];
+ const above = true;
+ this._reorderBlockedOn(src, target, above);
+ }
+ this.srcIndex = null;
+ }
+ }
+
+ _reorderBlockedOn(srcArg, targetArg, above) {
+ const src = srcArg.cells[0].issue;
+ const target = targetArg.cells[0].issue;
+
+ const reorderRequest = prpcClient.call(
+ 'monorail.Issues', 'RerankBlockedOnIssues', {
+ issueRef: this.issueRef,
+ movedRef: {
+ projectName: src.projectName,
+ localId: src.localId,
+ },
+ targetRef: {
+ projectName: target.projectName,
+ localId: target.localId,
+ },
+ splitAbove: above,
+ });
+
+ reorderRequest.then((response) => {
+ store.dispatch(issueV0.fetch(this.issueRef));
+ }, (error) => {
+ this.reset();
+ this.error = error.description;
+ });
+ }
+
+ _reorderRows(srcIndex, toIndex) {
+ if (srcIndex <= toIndex) {
+ this._renderedRows = this._renderedRows.slice(0, srcIndex).concat(
+ this._renderedRows.slice(srcIndex + 1, toIndex + 1),
+ [this._renderedRows[srcIndex]],
+ this._renderedRows.slice(toIndex + 1));
+ } else {
+ this._renderedRows = this._renderedRows.slice(0, toIndex).concat(
+ [this._renderedRows[srcIndex]],
+ this._renderedRows.slice(toIndex, srcIndex),
+ this._renderedRows.slice(srcIndex + 1));
+ }
+ }
+}
+
+customElements.define('mr-related-issues', MrRelatedIssues);
diff --git a/static_src/elements/issue-detail/dialogs/mr-related-issues/mr-related-issues.test.js b/static_src/elements/issue-detail/dialogs/mr-related-issues/mr-related-issues.test.js
new file mode 100644
index 0000000..69ce7ee
--- /dev/null
+++ b/static_src/elements/issue-detail/dialogs/mr-related-issues/mr-related-issues.test.js
@@ -0,0 +1,191 @@
+// 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 {assert} from 'chai';
+import {MrRelatedIssues} from './mr-related-issues.js';
+
+
+let element;
+
+describe('mr-related-issues', () => {
+ beforeEach(() => {
+ element = document.createElement('mr-related-issues');
+ document.body.appendChild(element);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(element);
+ });
+
+ it('initializes', () => {
+ assert.instanceOf(element, MrRelatedIssues);
+ });
+
+ it('dialog closes when issueRef changes', async () => {
+ element.issueRef = {projectName: 'chromium', localId: 22};
+ await element.updateComplete;
+
+ const dialog = element.shadowRoot.querySelector('chops-dialog');
+
+ element.open();
+ await element.updateComplete;
+
+ assert.isTrue(dialog.opened);
+
+ element.issueRef = {projectName: 'chromium', localId: 23};
+ await element.updateComplete;
+
+ assert.isFalse(dialog.opened);
+ });
+
+ it('computes blocked on table rows', () => {
+ element.projectName = 'proj';
+ element.sortedBlockedOn = [
+ {projectName: 'proj', localId: 1, statusRef: {meansOpen: true},
+ summary: 'Issue 1'},
+ {projectName: 'proj', localId: 2, statusRef: {meansOpen: true},
+ summary: 'Issue 2'},
+ {projectName: 'proj', localId: 3,
+ summary: 'Issue 3'},
+ {projectName: 'proj2', localId: 4,
+ summary: 'Issue 4 on another project'},
+ {extIdentifier: 'b/123456', statusRef: {meansOpen: true}},
+ {extIdentifier: 'b/987654', statusRef: {meansOpen: false},
+ summary: 'FedRef with a summary'},
+ {projectName: 'proj', localId: 5, statusRef: {meansOpen: false},
+ summary: 'Issue 5'},
+ {projectName: 'proj2', localId: 6, statusRef: {meansOpen: false},
+ summary: 'Issue 6 on another project'},
+ ];
+ assert.deepEqual(element._rows, [
+ {
+ draggable: true,
+ cells: [
+ {
+ type: 'issue',
+ issue: {projectName: 'proj', localId: 1, statusRef: {meansOpen: true},
+ summary: 'Issue 1'},
+ isClosed: false,
+ },
+ {
+ type: 'text',
+ content: 'Issue 1',
+ },
+ ],
+ },
+ {
+ draggable: true,
+ cells: [
+ {
+ type: 'issue',
+ issue: {projectName: 'proj', localId: 2, statusRef: {meansOpen: true},
+ summary: 'Issue 2'},
+ isClosed: false,
+ },
+ {
+ type: 'text',
+ content: 'Issue 2',
+ },
+ ],
+ },
+ {
+ draggable: true,
+ cells: [
+ {
+ type: 'issue',
+ issue: {projectName: 'proj', localId: 3,
+ summary: 'Issue 3'},
+ isClosed: false,
+ },
+ {
+ type: 'text',
+ content: 'Issue 3',
+ },
+ ],
+ },
+ {
+ draggable: true,
+ cells: [
+ {
+ type: 'issue',
+ issue: {projectName: 'proj2', localId: 4,
+ summary: 'Issue 4 on another project'},
+ isClosed: false,
+ },
+ {
+ type: 'text',
+ content: 'Issue 4 on another project',
+ },
+ ],
+ },
+ {
+ draggable: false,
+ cells: [
+ {
+ type: 'issue',
+ issue: {
+ extIdentifier: 'b/123456',
+ statusRef: {meansOpen: true},
+ },
+ isClosed: false,
+ },
+ {
+ type: 'text',
+ content: '(not available)',
+ },
+ ],
+ },
+ {
+ draggable: false,
+ cells: [
+ {
+ type: 'issue',
+ issue: {
+ extIdentifier: 'b/987654',
+ statusRef: {meansOpen: false},
+ summary: 'FedRef with a summary',
+ },
+ isClosed: true,
+ },
+ {
+ type: 'text',
+ content: 'FedRef with a summary',
+ },
+ ],
+ },
+ {
+ draggable: false,
+ cells: [
+ {
+ type: 'issue',
+ issue: {projectName: 'proj', localId: 5,
+ statusRef: {meansOpen: false},
+ summary: 'Issue 5'},
+ isClosed: true,
+ },
+ {
+ type: 'text',
+ content: 'Issue 5',
+ },
+ ],
+ },
+ {
+ draggable: false,
+ cells: [
+ {
+ type: 'issue',
+ issue: {projectName: 'proj2', localId: 6,
+ statusRef: {meansOpen: false},
+ summary: 'Issue 6 on another project'},
+ isClosed: true,
+ },
+ {
+ type: 'text',
+ content: 'Issue 6 on another project',
+ },
+ ],
+ },
+ ]);
+ });
+});