blob: 88cb92c11588cab3957a52aeb8de8b71337cec54 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import page from 'page';
6import {LitElement, html} from 'lit-element';
7
8import 'elements/chops/chops-button/chops-button.js';
9import './mr-issue-header.js';
10import './mr-restriction-indicator';
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020011import './mr-migrated-banner';
Copybara854996b2021-09-07 19:36:02 +000012import '../mr-issue-details/mr-issue-details.js';
13import '../metadata/mr-metadata/mr-issue-metadata.js';
14import '../mr-launch-overview/mr-launch-overview.js';
15import {store, connectStore} from 'reducers/base.js';
16import * as issueV0 from 'reducers/issueV0.js';
17import * as projectV0 from 'reducers/projectV0.js';
18import * as userV0 from 'reducers/userV0.js';
19import * as sitewide from 'reducers/sitewide.js';
20
21import {ISSUE_DELETE_PERMISSION} from 'shared/consts/permissions.js';
22
23// eslint-disable-next-line max-len
24import 'elements/framework/dialogs/mr-issue-hotlists-action/mr-update-issue-hotlists-dialog.js';
25import '../dialogs/mr-edit-description/mr-edit-description.js';
26import '../dialogs/mr-move-copy-issue/mr-move-copy-issue.js';
27import '../dialogs/mr-convert-issue/mr-convert-issue.js';
28import '../dialogs/mr-related-issues/mr-related-issues.js';
29import '../../help/mr-click-throughs/mr-click-throughs.js';
30import {prpcClient} from 'prpc-client-instance.js';
31
32const APPROVAL_COMMENT_COUNT = 5;
33const DETAIL_COMMENT_COUNT = 100;
34
35/**
36 * `<mr-issue-page>`
37 *
38 * The main entry point for a Monorail issue detail page.
39 *
40 */
41export class MrIssuePage extends connectStore(LitElement) {
42 /** @override */
43 render() {
44 return html`
45 <style>
46 mr-issue-page {
47 --mr-issue-page-horizontal-padding: 12px;
48 --mr-toggled-font-family: inherit;
49 --monorail-metadata-toggled-bg: var(--monorail-metadata-open-bg);
50 }
51 mr-issue-page[issueClosed] {
52 --monorail-metadata-toggled-bg: var(--monorail-metadata-closed-bg);
53 }
54 mr-issue-page[codeFont] {
55 --mr-toggled-font-family: Monospace;
56 }
57 .container-issue {
58 width: 100%;
59 flex-direction: column;
60 align-items: stretch;
61 justify-content: flex-start;
62 z-index: 200;
63 }
64 .container-issue-content {
65 padding: 0;
66 flex-grow: 1;
67 display: flex;
68 align-items: stretch;
69 justify-content: space-between;
70 flex-direction: row;
71 flex-wrap: nowrap;
72 box-sizing: border-box;
73 padding-top: 0.5em;
74 }
75 .container-outside {
76 box-sizing: border-box;
77 width: 100%;
78 max-width: 100%;
79 margin: auto;
80 padding: 0;
81 display: flex;
82 align-items: stretch;
83 justify-content: space-between;
84 flex-direction: row;
85 flex-wrap: no-wrap;
86 }
87 .container-no-issue {
88 padding: 0.5em 16px;
89 font-size: var(--chops-large-font-size);
90 }
91 .metadata-container {
92 font-size: var(--chops-main-font-size);
93 background: var(--monorail-metadata-toggled-bg);
94 border-right: var(--chops-normal-border);
95 border-bottom: var(--chops-normal-border);
96 width: 24em;
97 min-width: 256px;
98 flex-grow: 0;
99 flex-shrink: 0;
100 box-sizing: border-box;
101 z-index: 100;
102 }
103 .issue-header-container {
104 z-index: 10;
105 position: sticky;
106 top: var(--monorail-header-height);
107 margin-bottom: 0.25em;
108 width: 100%;
109 }
110 mr-issue-details {
111 min-width: 50%;
112 max-width: 1000px;
113 flex-grow: 1;
114 box-sizing: border-box;
115 min-height: 100%;
116 padding-left: var(--mr-issue-page-horizontal-padding);
117 padding-right: var(--mr-issue-page-horizontal-padding);
118 }
119 mr-issue-metadata {
120 position: sticky;
121 overflow-y: auto;
122 top: var(--monorail-header-height);
123 height: calc(100vh - var(--monorail-header-height));
124 }
125 mr-launch-overview {
126 border-left: var(--chops-normal-border);
127 padding-left: var(--mr-issue-page-horizontal-padding);
128 padding-right: var(--mr-issue-page-horizontal-padding);
129 flex-grow: 0;
130 flex-shrink: 0;
131 width: 50%;
132 box-sizing: border-box;
133 min-height: 100%;
134 }
135 @media (max-width: 1126px) {
136 .container-issue-content {
137 flex-direction: column;
138 padding: 0 var(--mr-issue-page-horizontal-padding);
139 }
140 mr-issue-details, mr-launch-overview {
141 width: 100%;
142 padding: 0;
143 border: 0;
144 }
145 }
146 @media (max-width: 840px) {
147 .container-outside {
148 flex-direction: column;
149 }
150 .metadata-container {
151 width: 100%;
152 height: auto;
153 border: 0;
154 border-bottom: var(--chops-normal-border);
155 }
156 mr-issue-metadata {
157 min-width: auto;
158 max-width: auto;
159 width: 100%;
160 padding: 0;
161 min-height: 0;
162 border: 0;
163 }
164 mr-issue-metadata, .issue-header-container {
165 position: static;
166 }
167 }
168 </style>
169 <mr-click-throughs
170 .userDisplayName=${this.userDisplayName}></mr-click-throughs>
171 ${this._renderIssue()}
172 `;
173 }
174
175 /**
176 * Render the issue.
177 * @return {TemplateResult}
178 */
179 _renderIssue() {
180 const issueIsEmpty = !this.issue || !this.issue.localId;
181 const movedToRef = this.issue.movedToRef;
182 const commentShown = this.issue.approvalValues ? APPROVAL_COMMENT_COUNT :
183 DETAIL_COMMENT_COUNT;
184
185 if (this.fetchIssueError) {
186 return html`
187 <div class="container-no-issue" id="fetch-error">
188 ${this.fetchIssueError.description}
189 </div>
190 `;
191 }
192
193 if (this.fetchingIssue && issueIsEmpty) {
194 return html`
195 <div class="container-no-issue" id="loading">
196 Loading...
197 </div>
198 `;
199 }
200
201 if (this.issue.isDeleted) {
202 return html`
203 <div class="container-no-issue" id="deleted">
204 <p>Issue ${this.issueRef.localId} has been deleted.</p>
205 ${this.issuePermissions.includes(ISSUE_DELETE_PERMISSION) ? html`
206 <chops-button
207 @click=${this._undeleteIssue}
208 class="undelete emphasized"
209 >
210 Undelete Issue
211 </chops-button>
212 `: ''}
213 </div>
214 `;
215 }
216
217 if (movedToRef && movedToRef.localId) {
218 return html`
219 <div class="container-no-issue" id="moved">
220 <h2>Issue has moved.</h2>
221 <p>
222 This issue was moved to ${movedToRef.projectName}.
223 <a
224 class="new-location"
225 href="/p/${movedToRef.projectName}/issues/detail?id=${movedToRef.localId}"
226 >
227 Go to issue</a>.
228 </p>
229 </div>
230 `;
231 }
232
233 if (!issueIsEmpty) {
234 return html`
235 <div
236 class="container-outside"
237 @open-dialog=${this._openDialog}
238 id="issue"
239 >
240 <aside class="metadata-container">
241 <mr-issue-metadata></mr-issue-metadata>
242 </aside>
243 <div class="container-issue">
244 <div class="issue-header-container">
245 <mr-issue-header
246 .userDisplayName=${this.userDisplayName}
247 ></mr-issue-header>
248 <mr-restriction-indicator></mr-restriction-indicator>
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200249 <mr-migrated-banner></mr-migrated-banner>
Copybara854996b2021-09-07 19:36:02 +0000250 </div>
251 <div class="container-issue-content">
252 <mr-issue-details
253 class="main-item"
254 .commentsShownCount=${commentShown}
255 ></mr-issue-details>
256 <mr-launch-overview class="main-item"></mr-launch-overview>
257 </div>
258 </div>
259 </div>
260 <mr-edit-description id="edit-description"></mr-edit-description>
261 <mr-move-copy-issue id="move-copy-issue"></mr-move-copy-issue>
262 <mr-convert-issue id="convert-issue"></mr-convert-issue>
263 <mr-related-issues id="reorder-related-issues"></mr-related-issues>
264 <mr-update-issue-hotlists-dialog
265 id="update-issue-hotlists"
266 .issueRefs=${[this.issueRef]}
267 .issueHotlists=${this.issueHotlists}
268 ></mr-update-issue-hotlists-dialog>
269 `;
270 }
271
272 return '';
273 }
274
275 /** @override */
276 static get properties() {
277 return {
278 userDisplayName: {type: String},
279 // Redux state.
280 fetchIssueError: {type: String},
281 fetchingIssue: {type: Boolean},
282 fetchingProjectConfig: {type: Boolean},
283 issue: {type: Object},
284 issueHotlists: {type: Array},
285 issueClosed: {
286 type: Boolean,
287 reflect: true,
288 },
289 codeFont: {
290 type: Boolean,
291 reflect: true,
292 },
293 issuePermissions: {type: Object},
294 issueRef: {type: Object},
295 prefs: {type: Object},
296 loginUrl: {type: String},
297 };
298 }
299
300 /** @override */
301 constructor() {
302 super();
303 this.issue = {};
304 this.issueRef = {};
305 this.issuePermissions = [];
306 this.prefs = {};
307 this.codeFont = false;
308 }
309
310 /** @override */
311 createRenderRoot() {
312 return this;
313 }
314
315 /** @override */
316 stateChanged(state) {
317 this.projectName = projectV0.viewedProjectName(state);
318 this.issue = issueV0.viewedIssue(state);
319 this.issueHotlists = issueV0.hotlists(state);
320 this.issueRef = issueV0.viewedIssueRef(state);
321 this.fetchIssueError = issueV0.requests(state).fetch.error;
322 this.fetchingIssue = issueV0.requests(state).fetch.requesting;
323 this.fetchingProjectConfig = projectV0.fetchingConfig(state);
324 this.issueClosed = !issueV0.isOpen(state);
325 this.issuePermissions = issueV0.permissions(state);
326 this.prefs = userV0.prefs(state);
327 }
328
329 /** @override */
330 update(changedProperties) {
331 if (changedProperties.has('prefs')) {
332 this.codeFont = !!this.prefs.get('code_font');
333 }
334 if (changedProperties.has('fetchIssueError') &&
335 !this.userDisplayName && this.fetchIssueError &&
336 this.fetchIssueError.codeName === 'PERMISSION_DENIED') {
337 page(this.loginUrl);
338 }
339 super.update(changedProperties);
340 }
341
342 /** @override */
343 updated(changedProperties) {
344 if (changedProperties.has('issueRef') || changedProperties.has('issue')) {
345 const title = this._pageTitle(this.issueRef, this.issue);
346 store.dispatch(sitewide.setPageTitle(title));
347 }
348 }
349
350 /**
351 * Generates a title for the currently viewed page based on issue data.
352 * @param {IssueRef} issueRef
353 * @param {Issue} issue
354 * @return {string}
355 */
356 _pageTitle(issueRef, issue) {
357 const titlePieces = [];
358 if (issueRef.localId) {
359 titlePieces.push(issueRef.localId);
360 }
361 if (!issue || !issue.localId) {
362 // Issue is not loaded.
363 titlePieces.push('Loading issue...');
364 } else {
365 if (issue.isDeleted) {
366 titlePieces.push('Deleted issue');
367 } else if (issue.summary) {
368 titlePieces.push(issue.summary);
369 }
370 }
371 return titlePieces.join(' - ');
372 }
373
374 /**
375 * Opens a dialog with a specific ID based on an Event.
376 * @param {CustomEvent} e
377 */
378 _openDialog(e) {
379 this.querySelector('#' + e.detail.dialogId).open(e);
380 }
381
382 /**
383 * Undeletes the current issue.
384 */
385 _undeleteIssue() {
386 prpcClient.call('monorail.Issues', 'DeleteIssue', {
387 issueRef: this.issueRef,
388 delete: false,
389 }).then(() => {
390 store.dispatch(issueV0.fetchIssuePageData(this.issueRef));
391 });
392 }
393}
394
395customElements.define('mr-issue-page', MrIssuePage);