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