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