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