blob: d153ba6b28c0841c6fe441640925b5e3340df719 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {LitElement, html, css} from 'lit-element';
import page from 'page';
import qs from 'qs';
import {store, connectStore} from 'elements/reducers/base.js';
import * as issue from 'elements/reducers/issue.js';
import * as ui from 'elements/reducers/ui.js';
import {arrayToEnglish} from 'elements/shared/helpers.js';
import 'elements/framework/mr-header/mr-header.js';
import 'elements/framework/mr-keystrokes/mr-keystrokes.js';
import {SHARED_STYLES} from 'elements/shared/shared-styles.js';
/**
* `<mr-app>`
*
* The container component for all pages under the Monorail SPA.
*
*/
export class MrApp extends connectStore(LitElement) {
static get styles() {
return [SHARED_STYLES, css`
main {
border-top: var(--chops-normal-border);
}
`];
}
render() {
return html`
<mr-keystrokes
.projectName=${this.projectName}
.issueId=${this.queryParams.id}
.queryParams=${this.queryParams}
.issueEntryUrl=${this.issueEntryUrl}
></mr-keystrokes>
<mr-header
.projectName=${this.projectName}
.userDisplayName=${this.userDisplayName}
.issueEntryUrl=${this.issueEntryUrl}
.loginUrl=${this.loginUrl}
.logoutUrl=${this.logoutUrl}
></mr-header>
<mr-cue cuePrefName="switch_to_parent_account"
.loginUrl=${this.loginUrl}
centered nondismissible></mr-cue>
<mr-cue cuePrefName="search_for_numbers" centered></mr-cue>
<main></main>
`;
}
static get properties() {
return {
issueEntryUrl: {type: String},
loginUrl: {type: String},
logoutUrl: {type: String},
projectName: {type: String},
userDisplayName: {type: String},
queryParams: {type: Object},
dirtyForms: {type: Array},
versionBase: {type: String},
_currentContext: {type: Object},
};
}
constructor() {
super();
this.queryParams = {};
this.dirtyForms = [];
this._currentContext = [];
}
stateChanged(state) {
this.dirtyForms = ui.dirtyForms(state);
}
connectedCallback() {
super.connectedCallback();
// TODO(zhangtiff): Figure out some way to save Redux state between
// page loads.
// page doesn't handle users reloading the page or closing a tab.
window.onbeforeunload = this._confirmDiscardMessage.bind(this);
page('*', (ctx, next) => {
// Navigate to the requested element if a hash is present.
if (ctx.hash) {
store.dispatch(ui.setFocusId(ctx.hash));
}
// We're not really navigating anywhere, so don't do anything.
if (ctx.path === this._currentContext.path) {
Object.assign(ctx, this._currentContext);
// Set ctx.handled to false, so we don't push the state to browser's
// history.
ctx.handled = false;
return;
}
// Check if there were forms with unsaved data before loading the next
// page.
const discardMessage = this._confirmDiscardMessage();
if (!discardMessage || confirm(discardMessage)) {
// Clear the forms to be checked, since we're navigating away.
store.dispatch(ui.clearDirtyForms());
} else {
Object.assign(ctx, this._currentContext);
// Set ctx.handled to false, so we don't push the state to browser's
// history.
ctx.handled = false;
// We don't call next to avoid loading whatever page was supposed to
// load next.
return;
}
// Run query string parsing on all routes.
// Based on: https://visionmedia.github.io/page.js/#plugins
ctx.query = qs.parse(ctx.querystring);
this.queryParams = ctx.query;
this._currentContext = ctx;
// Increment the count of navigations in the Redux store.
store.dispatch(ui.incrementNavigationCount());
next();
});
page('/p/:project/issues/detail', this._loadIssuePage.bind(this));
page();
}
loadWebComponent(name, props) {
const component = document.createElement(name, {is: name});
for (const key in props) {
if (props.hasOwnProperty(key)) {
component[key] = props[key];
}
}
const main = this.shadowRoot.querySelector('main');
if (main) {
// Clone the main tag without copying its children.
const mainClone = main.cloneNode(false);
mainClone.appendChild(component);
main.parentNode.replaceChild(mainClone, main);
}
}
async _loadIssuePage(ctx, next) {
performance.clearMarks('start load issue detail page');
performance.mark('start load issue detail page');
store.dispatch(issue.setIssueRef(
Number.parseInt(ctx.query.id), ctx.params.project));
this.projectName = ctx.params.project;
await import(/* webpackChunkName: "mr-issue-page" */ '../issue-detail/mr-issue-page/mr-issue-page.js');
// TODO(zhangtiff): Make sure the properties passed in to the loaded
// component can still dynamically change.
this.loadWebComponent('mr-issue-page', {
'projectName': ctx.params.project,
'userDisplayName': this.userDisplayName,
'issueEntryUrl': this.issueEntryUrl,
'queryParams': ctx.params,
'loginUrl': this.loginUrl,
});
}
_confirmDiscardMessage() {
if (!this.dirtyForms.length) return null;
const dirtyFormsMessage =
'Discard your changes in the following forms?\n' +
arrayToEnglish(this.dirtyForms);
return dirtyFormsMessage;
}
}
customElements.define('mr-app', MrApp);