Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | // 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 | |
| 5 | import {LitElement, html} from 'lit-element'; |
| 6 | import {repeat} from 'lit-html/directives/repeat'; |
| 7 | import page from 'page'; |
| 8 | import qs from 'qs'; |
| 9 | |
| 10 | import {getServerStatusCron} from 'shared/cron.js'; |
| 11 | import 'elements/framework/mr-site-banner/mr-site-banner.js'; |
Adrià Vilanova Martínez | 7cc6337 | 2022-05-15 23:16:25 +0200 | [diff] [blame] | 12 | import 'elements/framework/mr-vulnz-banner/mr-vulnz-banner.js'; |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 13 | import {store, connectStore} from 'reducers/base.js'; |
| 14 | import * as projectV0 from 'reducers/projectV0.js'; |
| 15 | import {hotlists} from 'reducers/hotlists.js'; |
| 16 | import * as issueV0 from 'reducers/issueV0.js'; |
| 17 | import * as permissions from 'reducers/permissions.js'; |
| 18 | import * as users from 'reducers/users.js'; |
| 19 | import * as userv0 from 'reducers/userV0.js'; |
| 20 | import * as ui from 'reducers/ui.js'; |
| 21 | import * as sitewide from 'reducers/sitewide.js'; |
| 22 | import {arrayToEnglish} from 'shared/helpers.js'; |
| 23 | import {trackPageChange} from 'shared/ga-helpers.js'; |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 24 | import 'elements/issue-list/mr-list-page/mr-list-page.js'; |
| 25 | import 'elements/issue-entry/mr-issue-entry-page.js'; |
| 26 | import 'elements/framework/mr-header/mr-header.js'; |
| 27 | import 'elements/help/mr-cue/mr-cue.js'; |
| 28 | import {cueNames} from 'elements/help/mr-cue/cue-helpers.js'; |
| 29 | import 'elements/chops/chops-snackbar/chops-snackbar.js'; |
| 30 | |
| 31 | import {SHARED_STYLES} from 'shared/shared-styles.js'; |
| 32 | |
| 33 | const QUERY_PARAMS_THAT_RESET_SCROLL = ['q', 'mode', 'id']; |
Adrià Vilanova Martínez | ac4a644 | 2022-05-15 19:05:13 +0200 | [diff] [blame] | 34 | const GOOGLE_EMAIL_SUFFIX = '@google.com'; |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 35 | |
| 36 | /** |
| 37 | * `<mr-app>` |
| 38 | * |
| 39 | * The container component for all pages under the Monorail SPA. |
| 40 | * |
| 41 | */ |
| 42 | export class MrApp extends connectStore(LitElement) { |
| 43 | /** @override */ |
| 44 | render() { |
| 45 | if (this.page === 'wizard') { |
| 46 | return html`<div id="reactMount"></div>`; |
| 47 | } |
| 48 | |
| 49 | return html` |
| 50 | <style> |
| 51 | ${SHARED_STYLES} |
| 52 | mr-app { |
| 53 | display: block; |
| 54 | padding-top: var(--monorail-header-height); |
| 55 | margin-top: -1px; /* Prevent a double border from showing up. */ |
| 56 | |
| 57 | /* From shared-styles.js. */ |
| 58 | --mr-edit-field-padding: 0.125em 4px; |
| 59 | --mr-edit-field-width: 90%; |
| 60 | --mr-input-grid-gap: 6px; |
| 61 | font-family: var(--chops-font-family); |
| 62 | color: var(--chops-primary-font-color); |
| 63 | font-size: var(--chops-main-font-size); |
| 64 | } |
| 65 | main { |
| 66 | border-top: var(--chops-normal-border); |
| 67 | } |
| 68 | .snackbar-container { |
| 69 | position: fixed; |
| 70 | bottom: 1em; |
| 71 | left: 1em; |
| 72 | display: flex; |
| 73 | flex-direction: column; |
| 74 | align-items: flex-start; |
| 75 | z-index: 1000; |
| 76 | } |
| 77 | /** Unfix <chops-snackbar> to allow stacking. */ |
| 78 | chops-snackbar { |
| 79 | position: static; |
| 80 | margin-top: 0.5em; |
| 81 | } |
| 82 | </style> |
| 83 | <mr-header |
| 84 | .userDisplayName=${this.userDisplayName} |
| 85 | .loginUrl=${this.loginUrl} |
| 86 | .logoutUrl=${this.logoutUrl} |
| 87 | ></mr-header> |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 88 | <mr-site-banner></mr-site-banner> |
Adrià Vilanova Martínez | 7cc6337 | 2022-05-15 23:16:25 +0200 | [diff] [blame] | 89 | <mr-vulnz-banner></mr-vulnz-banner> |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 90 | <mr-cue |
| 91 | cuePrefName=${cueNames.SWITCH_TO_PARENT_ACCOUNT} |
| 92 | .loginUrl=${this.loginUrl} |
| 93 | centered |
| 94 | nondismissible |
| 95 | ></mr-cue> |
| 96 | <mr-cue |
| 97 | cuePrefName=${cueNames.SEARCH_FOR_NUMBERS} |
| 98 | centered |
| 99 | ></mr-cue> |
| 100 | <main>${this._renderPage()}</main> |
| 101 | <div class="snackbar-container" aria-live="polite"> |
| 102 | ${repeat(this._snackbars, (snackbar) => html` |
| 103 | <chops-snackbar |
| 104 | @close=${this._closeSnackbar.bind(this, snackbar.id)} |
| 105 | >${snackbar.text}</chops-snackbar> |
| 106 | `)} |
| 107 | </div> |
| 108 | `; |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * @param {string} id The name of the snackbar to close. |
| 113 | */ |
| 114 | _closeSnackbar(id) { |
| 115 | store.dispatch(ui.hideSnackbar(id)); |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Helper for determiing which page component to render. |
| 120 | * @return {TemplateResult} |
| 121 | */ |
| 122 | _renderPage() { |
| 123 | switch (this.page) { |
| 124 | case 'detail': |
| 125 | return html` |
| 126 | <mr-issue-page |
| 127 | .userDisplayName=${this.userDisplayName} |
| 128 | .loginUrl=${this.loginUrl} |
| 129 | ></mr-issue-page> |
| 130 | `; |
| 131 | case 'entry': |
| 132 | return html` |
| 133 | <mr-issue-entry-page |
| 134 | .userDisplayName=${this.userDisplayName} |
| 135 | .loginUrl=${this.loginUrl} |
| 136 | ></mr-issue-entry-page> |
| 137 | `; |
| 138 | case 'grid': |
| 139 | return html` |
| 140 | <mr-grid-page |
| 141 | .userDisplayName=${this.userDisplayName} |
| 142 | ></mr-grid-page> |
| 143 | `; |
| 144 | case 'list': |
| 145 | return html` |
| 146 | <mr-list-page |
| 147 | .userDisplayName=${this.userDisplayName} |
| 148 | ></mr-list-page> |
| 149 | `; |
| 150 | case 'chart': |
| 151 | return html`<mr-chart-page></mr-chart-page>`; |
| 152 | case 'projects': |
| 153 | return html`<mr-projects-page></mr-projects-page>`; |
| 154 | case 'hotlist-issues': |
| 155 | return html`<mr-hotlist-issues-page></mr-hotlist-issues-page>`; |
| 156 | case 'hotlist-people': |
| 157 | return html`<mr-hotlist-people-page></mr-hotlist-people-page>`; |
| 158 | case 'hotlist-settings': |
| 159 | return html`<mr-hotlist-settings-page></mr-hotlist-settings-page>`; |
| 160 | default: |
| 161 | return; |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | /** @override */ |
| 166 | static get properties() { |
| 167 | return { |
| 168 | /** |
| 169 | * Backend-generated URL for the page the user is directed to for login. |
| 170 | */ |
| 171 | loginUrl: {type: String}, |
| 172 | /** |
| 173 | * Backend-generated URL for the page the user is directed to for logout. |
| 174 | */ |
| 175 | logoutUrl: {type: String}, |
| 176 | /** |
| 177 | * The display name of the currently logged in user. |
| 178 | */ |
| 179 | userDisplayName: {type: String}, |
| 180 | /** |
| 181 | * The search parameters in the user's current URL. |
| 182 | */ |
| 183 | queryParams: {type: Object}, |
| 184 | /** |
| 185 | * A list of forms to check for "dirty" values when the user navigates |
| 186 | * across pages. |
| 187 | */ |
| 188 | dirtyForms: {type: Array}, |
| 189 | /** |
| 190 | * App Engine ID for the current version being viewed. |
| 191 | */ |
| 192 | versionBase: {type: String}, |
| 193 | /** |
| 194 | * A String identifier for the page that the user is viewing. |
| 195 | */ |
| 196 | page: {type: String}, |
| 197 | /** |
| 198 | * A String for the title of the page that the user will see in their |
| 199 | * browser tab. ie: equivalent to the <title> tag. |
| 200 | */ |
| 201 | pageTitle: {type: String}, |
| 202 | /** |
| 203 | * Array of snackbar objects to render. |
| 204 | */ |
| 205 | _snackbars: {type: Array}, |
| 206 | }; |
| 207 | } |
| 208 | |
| 209 | /** @override */ |
| 210 | constructor() { |
| 211 | super(); |
| 212 | this.queryParams = {}; |
| 213 | this.dirtyForms = []; |
| 214 | this.userDisplayName = ''; |
| 215 | |
| 216 | /** |
| 217 | * @type {PageJS.Context} |
| 218 | * The context of the page. This should not be a LitElement property |
| 219 | * because we don't want to re-render when updating this. |
| 220 | */ |
| 221 | this._lastContext = undefined; |
| 222 | } |
| 223 | |
| 224 | /** @override */ |
| 225 | createRenderRoot() { |
| 226 | return this; |
| 227 | } |
| 228 | |
| 229 | /** @override */ |
| 230 | stateChanged(state) { |
| 231 | this.dirtyForms = ui.dirtyForms(state); |
| 232 | this.queryParams = sitewide.queryParams(state); |
| 233 | this.pageTitle = sitewide.pageTitle(state); |
| 234 | this._snackbars = ui.snackbars(state); |
| 235 | } |
| 236 | |
| 237 | /** @override */ |
| 238 | updated(changedProperties) { |
| 239 | if (changedProperties.has('userDisplayName') && this.userDisplayName) { |
| 240 | // TODO(https://crbug.com/monorail/7238): Migrate userv0 calls to v3 API. |
| 241 | store.dispatch(userv0.fetch(this.userDisplayName)); |
| 242 | |
| 243 | // Typically we would prefer 'users/<userId>' instead. |
| 244 | store.dispatch(users.fetch(`users/${this.userDisplayName}`)); |
| 245 | } |
| 246 | |
| 247 | if (changedProperties.has('pageTitle')) { |
| 248 | // To ensure that changes to the page title are easy to reason about, |
| 249 | // we want to sync the current pageTitle in the Redux state to |
| 250 | // document.title in only one place in the code. |
| 251 | document.title = this.pageTitle; |
| 252 | } |
| 253 | if (changedProperties.has('page')) { |
| 254 | trackPageChange(this.page, this.userDisplayName); |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | /** @override */ |
| 259 | connectedCallback() { |
| 260 | super.connectedCallback(); |
| 261 | |
Adrià Vilanova Martínez | ac4a644 | 2022-05-15 19:05:13 +0200 | [diff] [blame] | 262 | this._logGooglerUsage(); |
| 263 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 264 | // TODO(zhangtiff): Figure out some way to save Redux state between |
| 265 | // page loads. |
| 266 | |
| 267 | // page doesn't handle users reloading the page or closing a tab. |
| 268 | window.onbeforeunload = this._confirmDiscardMessage.bind(this); |
| 269 | |
| 270 | // Start a cron task to periodically request the status from the server. |
| 271 | getServerStatusCron.start(); |
| 272 | |
| 273 | const postRouteHandler = this._postRouteHandler.bind(this); |
| 274 | |
| 275 | // Populate the project route parameter before _preRouteHandler runs. |
| 276 | page('/p/:project/*', (_ctx, next) => next()); |
| 277 | page('*', this._preRouteHandler.bind(this)); |
| 278 | |
| 279 | page('/hotlists/:hotlist', (ctx) => { |
| 280 | page.redirect(`/hotlists/${ctx.params.hotlist}/issues`); |
| 281 | }); |
| 282 | page('/hotlists/:hotlist/*', this._selectHotlist); |
| 283 | page('/hotlists/:hotlist/issues', |
| 284 | this._loadHotlistIssuesPage.bind(this), postRouteHandler); |
| 285 | page('/hotlists/:hotlist/people', |
| 286 | this._loadHotlistPeoplePage.bind(this), postRouteHandler); |
| 287 | page('/hotlists/:hotlist/settings', |
| 288 | this._loadHotlistSettingsPage.bind(this), postRouteHandler); |
| 289 | |
| 290 | // Handle Monorail's landing page. |
| 291 | page('/p', '/'); |
| 292 | page('/projects', '/'); |
| 293 | page('/hosting', '/'); |
| 294 | page('/', this._loadProjectsPage.bind(this), postRouteHandler); |
| 295 | |
| 296 | page('/p/:project/issues/list', this._loadListPage.bind(this), |
| 297 | postRouteHandler); |
| 298 | page('/p/:project/issues/detail', this._loadIssuePage.bind(this), |
| 299 | postRouteHandler); |
| 300 | page('/p/:project/issues/entry_new', this._loadEntryPage.bind(this), |
| 301 | postRouteHandler); |
| 302 | page('/p/:project/issues/wizard', this._loadWizardPage.bind(this), |
| 303 | postRouteHandler); |
| 304 | |
| 305 | // Redirects from old hotlist pages to SPA hotlist pages. |
| 306 | const hotlistRedirect = (pageName) => async (ctx) => { |
| 307 | const name = |
| 308 | await hotlists.getHotlistName(ctx.params.user, ctx.params.hotlist); |
| 309 | page.redirect(`/${name}/${pageName}`); |
| 310 | }; |
| 311 | page('/users/:user/hotlists/:hotlist', hotlistRedirect('issues')); |
| 312 | page('/users/:user/hotlists/:hotlist/people', hotlistRedirect('people')); |
| 313 | page('/users/:user/hotlists/:hotlist/details', hotlistRedirect('settings')); |
| 314 | |
| 315 | page(); |
| 316 | } |
| 317 | |
| 318 | /** |
Adrià Vilanova Martínez | ac4a644 | 2022-05-15 19:05:13 +0200 | [diff] [blame] | 319 | * Helper to log how often Googlers access Monorail. |
| 320 | */ |
| 321 | _logGooglerUsage() { |
| 322 | const email = this.userDisplayName; |
| 323 | if (!email) return; |
| 324 | if (!email.endsWith(GOOGLE_EMAIL_SUFFIX)) return; |
| 325 | |
| 326 | const username = email.replace(GOOGLE_EMAIL_SUFFIX, ''); |
| 327 | |
| 328 | // Context: b/229758140 |
| 329 | window.fetch(`https://buganizer.corp.google.com/action/yes?monorail=yes&username=${username}`, |
| 330 | {mode: 'no-cors'}); |
| 331 | } |
| 332 | |
| 333 | /** |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 334 | * Handler that runs on every single route change, before the new page has |
| 335 | * loaded. This function should not use store.dispatch() or assign properties |
| 336 | * on this because running these actions causes extra re-renders to happen. |
| 337 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 338 | * @param {function} next Passes execution on to the next registered callback. |
| 339 | */ |
| 340 | _preRouteHandler(ctx, next) { |
| 341 | // We're not really navigating anywhere, so don't do anything. |
| 342 | if (this._lastContext && this._lastContext.path && |
| 343 | ctx.path === this._lastContext.path) { |
| 344 | Object.assign(ctx, this._lastContext); |
| 345 | // Set ctx.handled to false, so we don't push the state to browser's |
| 346 | // history. |
| 347 | ctx.handled = false; |
| 348 | return; |
| 349 | } |
| 350 | |
| 351 | // Check if there were forms with unsaved data before loading the next |
| 352 | // page. |
| 353 | const discardMessage = this._confirmDiscardMessage(); |
| 354 | if (discardMessage && !confirm(discardMessage)) { |
| 355 | Object.assign(ctx, this._lastContext); |
| 356 | // Set ctx.handled to false, so we don't push the state to browser's |
| 357 | // history. |
| 358 | ctx.handled = false; |
| 359 | // We don't call next to avoid loading whatever page was supposed to |
| 360 | // load next. |
| 361 | return; |
| 362 | } |
| 363 | |
| 364 | // Run query string parsing on all routes. Query params must be parsed |
| 365 | // before routes are loaded because some routes use them to conditionally |
| 366 | // load bundles. |
| 367 | // Based on: https://visionmedia.github.io/page.js/#plugins |
| 368 | const params = qs.parse(ctx.querystring); |
| 369 | |
| 370 | // Make sure queryParams are not case sensitive. |
| 371 | const lowerCaseParams = {}; |
| 372 | Object.keys(params).forEach((key) => { |
| 373 | lowerCaseParams[key.toLowerCase()] = params[key]; |
| 374 | }); |
| 375 | ctx.queryParams = lowerCaseParams; |
| 376 | |
| 377 | this._selectProject(ctx.params.project); |
| 378 | |
| 379 | next(); |
| 380 | } |
| 381 | |
| 382 | /** |
| 383 | * Handler that runs on every single route change, after the new page has |
| 384 | * loaded. |
| 385 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 386 | * @param {function} next Passes execution on to the next registered callback. |
| 387 | */ |
| 388 | _postRouteHandler(ctx, next) { |
| 389 | // Scroll to the requested element if a hash is present. |
| 390 | if (ctx.hash) { |
| 391 | store.dispatch(ui.setFocusId(ctx.hash)); |
| 392 | } |
| 393 | |
| 394 | // Sync queryParams to Redux after the route has loaded, rather than before, |
| 395 | // to avoid having extra queryParams update on the previously loaded |
| 396 | // component. |
| 397 | store.dispatch(sitewide.setQueryParams(ctx.queryParams)); |
| 398 | |
| 399 | // Increment the count of navigations in the Redux store. |
| 400 | store.dispatch(ui.incrementNavigationCount()); |
| 401 | |
| 402 | // Clear dirty forms when entering a new page. |
| 403 | store.dispatch(ui.clearDirtyForms()); |
| 404 | |
| 405 | |
| 406 | if (!this._lastContext || this._lastContext.pathname !== ctx.pathname || |
| 407 | this._hasReleventParamChanges(ctx.queryParams, |
| 408 | this._lastContext.queryParams)) { |
| 409 | // Reset the scroll position after a new page has rendered. |
| 410 | window.scrollTo(0, 0); |
| 411 | } |
| 412 | |
| 413 | // Save the context of this page to be compared to later. |
| 414 | this._lastContext = ctx; |
| 415 | } |
| 416 | |
| 417 | /** |
| 418 | * Finds if a route change changed query params in a way that should cause |
| 419 | * scrolling to reset. |
| 420 | * @param {Object} currentParams |
| 421 | * @param {Object} oldParams |
| 422 | * @param {Array<string>=} paramsToCompare Which params to check. |
| 423 | * @return {boolean} Whether any of the relevant query params changed. |
| 424 | */ |
| 425 | _hasReleventParamChanges(currentParams, oldParams, |
| 426 | paramsToCompare = QUERY_PARAMS_THAT_RESET_SCROLL) { |
| 427 | return paramsToCompare.some((paramName) => { |
| 428 | return currentParams[paramName] !== oldParams[paramName]; |
| 429 | }); |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * Helper to manage syncing project route state to Redux. |
| 434 | * @param {string=} project displayName for a referenced project. |
| 435 | * Defaults to null for consistency with Redux. |
| 436 | */ |
| 437 | _selectProject(project = null) { |
| 438 | if (projectV0.viewedProjectName(store.getState()) !== project) { |
| 439 | // Note: We want to update the project even if the new project |
| 440 | // is null. |
| 441 | store.dispatch(projectV0.select(project)); |
| 442 | if (project) { |
| 443 | store.dispatch(projectV0.fetch(project)); |
| 444 | } |
| 445 | } |
| 446 | } |
| 447 | |
| 448 | /** |
| 449 | * Loads and triggers rendering for the list of all projects. |
| 450 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 451 | * @param {function} next Passes execution on to the next registered callback. |
| 452 | */ |
| 453 | async _loadProjectsPage(ctx, next) { |
| 454 | await import(/* webpackChunkName: "mr-projects-page" */ |
| 455 | '../projects/mr-projects-page/mr-projects-page.js'); |
| 456 | this.page = 'projects'; |
| 457 | next(); |
| 458 | } |
| 459 | |
| 460 | /** |
| 461 | * Loads and triggers render for the issue detail page. |
| 462 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 463 | * @param {function} next Passes execution on to the next registered callback. |
| 464 | */ |
| 465 | async _loadIssuePage(ctx, next) { |
| 466 | performance.clearMarks('start load issue detail page'); |
| 467 | performance.mark('start load issue detail page'); |
| 468 | |
| 469 | await import(/* webpackChunkName: "mr-issue-page" */ |
| 470 | '../issue-detail/mr-issue-page/mr-issue-page.js'); |
| 471 | |
| 472 | const issueRef = { |
| 473 | localId: Number.parseInt(ctx.queryParams.id), |
| 474 | projectName: ctx.params.project, |
| 475 | }; |
| 476 | store.dispatch(issueV0.viewIssue(issueRef)); |
| 477 | store.dispatch(issueV0.fetchIssuePageData(issueRef)); |
| 478 | this.page = 'detail'; |
| 479 | next(); |
| 480 | } |
| 481 | |
| 482 | /** |
| 483 | * Loads and triggers render for the issue list page, including the list, |
| 484 | * grid, and chart modes. |
| 485 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 486 | * @param {function} next Passes execution on to the next registered callback. |
| 487 | */ |
| 488 | async _loadListPage(ctx, next) { |
| 489 | performance.clearMarks('start load issue list page'); |
| 490 | performance.mark('start load issue list page'); |
| 491 | switch (ctx.queryParams && ctx.queryParams.mode && |
| 492 | ctx.queryParams.mode.toLowerCase()) { |
| 493 | case 'grid': |
| 494 | await import(/* webpackChunkName: "mr-grid-page" */ |
| 495 | '../issue-list/mr-grid-page/mr-grid-page.js'); |
| 496 | this.page = 'grid'; |
| 497 | break; |
| 498 | case 'chart': |
| 499 | await import(/* webpackChunkName: "mr-chart-page" */ |
| 500 | '../issue-list/mr-chart-page/mr-chart-page.js'); |
| 501 | this.page = 'chart'; |
| 502 | break; |
| 503 | default: |
| 504 | this.page = 'list'; |
| 505 | break; |
| 506 | } |
| 507 | next(); |
| 508 | } |
| 509 | |
| 510 | /** |
| 511 | * Load the issue entry page |
| 512 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 513 | * @param {function} next Passes execution on to the next registered callback. |
| 514 | */ |
| 515 | _loadEntryPage(ctx, next) { |
| 516 | this.page = 'entry'; |
| 517 | next(); |
| 518 | } |
| 519 | |
| 520 | /** |
| 521 | * Load the issue wizard |
| 522 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 523 | * @param {function} next Passes execution on to the next registered callback. |
| 524 | */ |
| 525 | async _loadWizardPage(ctx, next) { |
| 526 | const {renderWizard} = await import( |
| 527 | /* webpackChunkName: "IssueWizard" */ '../../react/IssueWizard.tsx'); |
| 528 | |
| 529 | this.page = 'wizard'; |
| 530 | next(); |
| 531 | |
| 532 | await this.updateComplete; |
| 533 | |
| 534 | const mount = document.getElementById('reactMount'); |
| 535 | |
Adrià Vilanova Martínez | ac4a644 | 2022-05-15 19:05:13 +0200 | [diff] [blame] | 536 | renderWizard(mount, this.loginUrl, this.userDisplayName); |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 537 | } |
| 538 | |
| 539 | /** |
| 540 | * Gets the currently viewed HotlistRef from the URL, selects |
| 541 | * it in the Redux store, and fetches the Hotlist data. |
| 542 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 543 | * @param {function} next Passes execution on to the next registered callback. |
| 544 | */ |
| 545 | _selectHotlist(ctx, next) { |
| 546 | const name = 'hotlists/' + ctx.params.hotlist; |
| 547 | store.dispatch(hotlists.select(name)); |
| 548 | store.dispatch(hotlists.fetch(name)); |
| 549 | store.dispatch(hotlists.fetchItems(name)); |
| 550 | store.dispatch(permissions.batchGet([name])); |
| 551 | next(); |
| 552 | } |
| 553 | |
| 554 | /** |
| 555 | * Loads mr-hotlist-issues-page.js and makes it the currently viewed page. |
| 556 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 557 | * @param {function} next Passes execution on to the next registered callback. |
| 558 | */ |
| 559 | async _loadHotlistIssuesPage(ctx, next) { |
| 560 | await import(/* webpackChunkName: "mr-hotlist-issues-page" */ |
| 561 | `../hotlist/mr-hotlist-issues-page/mr-hotlist-issues-page.js`); |
| 562 | this.page = 'hotlist-issues'; |
| 563 | next(); |
| 564 | } |
| 565 | |
| 566 | /** |
| 567 | * Loads mr-hotlist-people-page.js and makes it the currently viewed page. |
| 568 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 569 | * @param {function} next Passes execution on to the next registered callback. |
| 570 | */ |
| 571 | async _loadHotlistPeoplePage(ctx, next) { |
| 572 | await import(/* webpackChunkName: "mr-hotlist-people-page" */ |
| 573 | `../hotlist/mr-hotlist-people-page/mr-hotlist-people-page.js`); |
| 574 | this.page = 'hotlist-people'; |
| 575 | next(); |
| 576 | } |
| 577 | |
| 578 | /** |
| 579 | * Loads mr-hotlist-settings-page.js and makes it the currently viewed page. |
| 580 | * @param {PageJS.Context} ctx A page.js Context containing routing state. |
| 581 | * @param {function} next Passes execution on to the next registered callback. |
| 582 | */ |
| 583 | async _loadHotlistSettingsPage(ctx, next) { |
| 584 | await import(/* webpackChunkName: "mr-hotlist-settings-page" */ |
| 585 | `../hotlist/mr-hotlist-settings-page/mr-hotlist-settings-page.js`); |
| 586 | this.page = 'hotlist-settings'; |
| 587 | next(); |
| 588 | } |
| 589 | |
| 590 | /** |
| 591 | * Constructs a message to warn users about dirty forms when they navigate |
| 592 | * away from a page, to prevent them from loasing data. |
| 593 | * @return {string} Message shown to users to warn about in flight form |
| 594 | * changes. |
| 595 | */ |
| 596 | _confirmDiscardMessage() { |
| 597 | if (!this.dirtyForms.length) return null; |
| 598 | const dirtyFormsMessage = |
| 599 | 'Discard your changes in the following forms?\n' + |
| 600 | arrayToEnglish(this.dirtyForms); |
| 601 | return dirtyFormsMessage; |
| 602 | } |
| 603 | } |
| 604 | |
| 605 | customElements.define('mr-app', MrApp); |