blob: 1cd80417424c8ace3cd297e39df3421f59573036 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import page from 'page';
import {LitElement, html} from 'lit-element';
import 'elements/chops/chops-button/chops-button.js';
import './mr-issue-header.js';
import './mr-restriction-indicator';
import './mr-migrated-banner';
import '../mr-issue-details/mr-issue-details.js';
import '../metadata/mr-metadata/mr-issue-metadata.js';
import '../mr-launch-overview/mr-launch-overview.js';
import {store, connectStore} from 'reducers/base.js';
import * as issueV0 from 'reducers/issueV0.js';
import * as projectV0 from 'reducers/projectV0.js';
import * as userV0 from 'reducers/userV0.js';
import * as sitewide from 'reducers/sitewide.js';
import {ISSUE_DELETE_PERMISSION} from 'shared/consts/permissions.js';
import {generateProjectIssueURL} from 'shared/helpers.js';
// eslint-disable-next-line max-len
import 'elements/framework/dialogs/mr-issue-hotlists-action/mr-update-issue-hotlists-dialog.js';
import '../dialogs/mr-edit-description/mr-edit-description.js';
import '../dialogs/mr-move-copy-issue/mr-move-copy-issue.js';
import '../dialogs/mr-convert-issue/mr-convert-issue.js';
import '../dialogs/mr-related-issues/mr-related-issues.js';
import '../../help/mr-click-throughs/mr-click-throughs.js';
import {prpcClient} from 'prpc-client-instance.js';
const APPROVAL_COMMENT_COUNT = 5;
const DETAIL_COMMENT_COUNT = 100;
/**
* `<mr-issue-page>`
*
* The main entry point for a Monorail issue detail page.
*
*/
export class MrIssuePage extends connectStore(LitElement) {
/** @override */
render() {
return html`
<style>
mr-issue-page {
--mr-issue-page-horizontal-padding: 12px;
--mr-toggled-font-family: inherit;
--monorail-metadata-toggled-bg: var(--monorail-metadata-open-bg);
}
mr-issue-page[issueClosed] {
--monorail-metadata-toggled-bg: var(--monorail-metadata-closed-bg);
}
mr-issue-page[codeFont] {
--mr-toggled-font-family: Monospace;
}
.container-issue {
width: 100%;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
z-index: 200;
}
.container-issue-content {
padding: 0;
flex-grow: 1;
display: flex;
align-items: stretch;
justify-content: space-between;
flex-direction: row;
flex-wrap: nowrap;
box-sizing: border-box;
padding-top: 0.5em;
}
.container-outside {
box-sizing: border-box;
width: 100%;
max-width: 100%;
margin: auto;
padding: 0;
display: flex;
align-items: stretch;
justify-content: space-between;
flex-direction: row;
flex-wrap: no-wrap;
}
.container-no-issue {
padding: 0.5em 16px;
font-size: var(--chops-large-font-size);
}
.metadata-container {
font-size: var(--chops-main-font-size);
background: var(--monorail-metadata-toggled-bg);
border-right: var(--chops-normal-border);
border-bottom: var(--chops-normal-border);
width: 24em;
min-width: 256px;
flex-grow: 0;
flex-shrink: 0;
box-sizing: border-box;
z-index: 100;
}
.issue-header-container {
z-index: 10;
position: sticky;
top: var(--monorail-header-height);
margin-bottom: 0.25em;
width: 100%;
}
mr-issue-details {
min-width: 50%;
max-width: 1000px;
flex-grow: 1;
box-sizing: border-box;
min-height: 100%;
padding-left: var(--mr-issue-page-horizontal-padding);
padding-right: var(--mr-issue-page-horizontal-padding);
}
mr-issue-metadata {
position: sticky;
overflow-y: auto;
top: var(--monorail-header-height);
height: calc(100vh - var(--monorail-header-height));
}
mr-launch-overview {
border-left: var(--chops-normal-border);
padding-left: var(--mr-issue-page-horizontal-padding);
padding-right: var(--mr-issue-page-horizontal-padding);
flex-grow: 0;
flex-shrink: 0;
width: 50%;
box-sizing: border-box;
min-height: 100%;
}
@media (max-width: 1126px) {
.container-issue-content {
flex-direction: column;
padding: 0 var(--mr-issue-page-horizontal-padding);
}
mr-issue-details, mr-launch-overview {
width: 100%;
padding: 0;
border: 0;
}
}
@media (max-width: 840px) {
.container-outside {
flex-direction: column;
}
.metadata-container {
width: 100%;
height: auto;
border: 0;
border-bottom: var(--chops-normal-border);
}
mr-issue-metadata {
min-width: auto;
max-width: auto;
width: 100%;
padding: 0;
min-height: 0;
border: 0;
}
mr-issue-metadata, .issue-header-container {
position: static;
}
}
</style>
<mr-click-throughs
.userDisplayName=${this.userDisplayName}></mr-click-throughs>
${this._renderIssue()}
`;
}
/**
* Render the issue.
* @return {TemplateResult}
*/
_renderIssue() {
const issueIsEmpty = !this.issue || !this.issue.localId;
const movedToRef = this.issue.movedToRef;
const commentShown = this.issue.approvalValues ? APPROVAL_COMMENT_COUNT :
DETAIL_COMMENT_COUNT;
if (this.fetchIssueError) {
return html`
<div class="container-no-issue" id="fetch-error">
${this.fetchIssueError.description}
</div>
`;
}
if (this.fetchingIssue && issueIsEmpty) {
return html`
<div class="container-no-issue" id="loading">
Loading...
</div>
`;
}
if (this.issue.isDeleted) {
return html`
<div class="container-no-issue" id="deleted">
<p>Issue ${this.issueRef.localId} has been deleted.</p>
${this.issuePermissions.includes(ISSUE_DELETE_PERMISSION) ? html`
<chops-button
@click=${this._undeleteIssue}
class="undelete emphasized"
>
Undelete Issue
</chops-button>
`: ''}
</div>
`;
}
if (movedToRef && movedToRef.localId) {
const params = {'id': movedToRef.localId};
return html`
<div class="container-no-issue" id="moved">
<h2>Issue has moved.</h2>
<p>
This issue was moved to ${movedToRef.projectName}.
<a
class="new-location"
href="${generateProjectIssueURL(movedToRef.projectName, '/detail', params)}"
>
Go to issue</a>.
</p>
</div>
`;
}
if (!issueIsEmpty) {
return html`
<div
class="container-outside"
@open-dialog=${this._openDialog}
id="issue"
>
<aside class="metadata-container">
<mr-issue-metadata></mr-issue-metadata>
</aside>
<div class="container-issue">
<div class="issue-header-container">
<mr-issue-header
.userDisplayName=${this.userDisplayName}
></mr-issue-header>
<mr-restriction-indicator></mr-restriction-indicator>
<mr-migrated-banner></mr-migrated-banner>
</div>
<div class="container-issue-content">
<mr-issue-details
class="main-item"
.commentsShownCount=${commentShown}
></mr-issue-details>
<mr-launch-overview class="main-item"></mr-launch-overview>
</div>
</div>
</div>
<mr-edit-description id="edit-description"></mr-edit-description>
<mr-move-copy-issue id="move-copy-issue"></mr-move-copy-issue>
<mr-convert-issue id="convert-issue"></mr-convert-issue>
<mr-related-issues id="reorder-related-issues"></mr-related-issues>
<mr-update-issue-hotlists-dialog
id="update-issue-hotlists"
.issueRefs=${[this.issueRef]}
.issueHotlists=${this.issueHotlists}
></mr-update-issue-hotlists-dialog>
`;
}
return '';
}
/** @override */
static get properties() {
return {
userDisplayName: {type: String},
// Redux state.
fetchIssueError: {type: String},
fetchingIssue: {type: Boolean},
fetchingProjectConfig: {type: Boolean},
issue: {type: Object},
issueHotlists: {type: Array},
issueClosed: {
type: Boolean,
reflect: true,
},
codeFont: {
type: Boolean,
reflect: true,
},
issuePermissions: {type: Object},
issueRef: {type: Object},
prefs: {type: Object},
loginUrl: {type: String},
};
}
/** @override */
constructor() {
super();
this.issue = {};
this.issueRef = {};
this.issuePermissions = [];
this.prefs = {};
this.codeFont = false;
}
/** @override */
createRenderRoot() {
return this;
}
/** @override */
stateChanged(state) {
this.projectName = projectV0.viewedProjectName(state);
this.issue = issueV0.viewedIssue(state);
this.issueHotlists = issueV0.hotlists(state);
this.issueRef = issueV0.viewedIssueRef(state);
this.fetchIssueError = issueV0.requests(state).fetch.error;
this.fetchingIssue = issueV0.requests(state).fetch.requesting;
this.fetchingProjectConfig = projectV0.fetchingConfig(state);
this.issueClosed = !issueV0.isOpen(state);
this.issuePermissions = issueV0.permissions(state);
this.prefs = userV0.prefs(state);
}
/** @override */
update(changedProperties) {
if (changedProperties.has('prefs')) {
this.codeFont = !!this.prefs.get('code_font');
}
if (changedProperties.has('fetchIssueError') &&
!this.userDisplayName && this.fetchIssueError &&
this.fetchIssueError.codeName === 'PERMISSION_DENIED') {
page(this.loginUrl);
}
super.update(changedProperties);
}
/** @override */
updated(changedProperties) {
if (changedProperties.has('issueRef') || changedProperties.has('issue')) {
const title = this._pageTitle(this.issueRef, this.issue);
store.dispatch(sitewide.setPageTitle(title));
}
}
/**
* Generates a title for the currently viewed page based on issue data.
* @param {IssueRef} issueRef
* @param {Issue} issue
* @return {string}
*/
_pageTitle(issueRef, issue) {
const titlePieces = [];
if (issueRef.localId) {
titlePieces.push(issueRef.localId);
}
if (!issue || !issue.localId) {
// Issue is not loaded.
titlePieces.push('Loading issue...');
} else {
if (issue.isDeleted) {
titlePieces.push('Deleted issue');
} else if (issue.summary) {
titlePieces.push(issue.summary);
}
}
return titlePieces.join(' - ');
}
/**
* Opens a dialog with a specific ID based on an Event.
* @param {CustomEvent} e
*/
_openDialog(e) {
this.querySelector('#' + e.detail.dialogId).open(e);
}
/**
* Undeletes the current issue.
*/
_undeleteIssue() {
prpcClient.call('monorail.Issues', 'DeleteIssue', {
issueRef: this.issueRef,
delete: false,
}).then(() => {
store.dispatch(issueV0.fetchIssuePageData(this.issueRef));
});
}
}
customElements.define('mr-issue-page', MrIssuePage);