
Also adds a change-service, which maintains the change model. The new router model is maintained by gr-router, which will eventually be moved and renamed to router-service. Change-Id: I15fcf39d1de21dea975a687fb6efb6823d658c22
715 lines
22 KiB
TypeScript
715 lines
22 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright (C) 2019 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
import '../styles/shared-styles';
|
|
import '../styles/themes/app-theme';
|
|
import {applyTheme as applyDarkTheme} from '../styles/themes/dark-theme';
|
|
import './admin/gr-admin-view/gr-admin-view';
|
|
import './documentation/gr-documentation-search/gr-documentation-search';
|
|
import './change-list/gr-change-list-view/gr-change-list-view';
|
|
import './change-list/gr-dashboard-view/gr-dashboard-view';
|
|
import './change/gr-change-view/gr-change-view';
|
|
import './core/gr-error-manager/gr-error-manager';
|
|
import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog';
|
|
import './core/gr-main-header/gr-main-header';
|
|
import './core/gr-router/gr-router';
|
|
import './core/gr-smart-search/gr-smart-search';
|
|
import './diff/gr-diff-view/gr-diff-view';
|
|
import './edit/gr-editor-view/gr-editor-view';
|
|
import './plugins/gr-endpoint-decorator/gr-endpoint-decorator';
|
|
import './plugins/gr-endpoint-param/gr-endpoint-param';
|
|
import './plugins/gr-endpoint-slot/gr-endpoint-slot';
|
|
import './plugins/gr-external-style/gr-external-style';
|
|
import './plugins/gr-plugin-host/gr-plugin-host';
|
|
import './settings/gr-cla-view/gr-cla-view';
|
|
import './settings/gr-registration-dialog/gr-registration-dialog';
|
|
import './settings/gr-settings-view/gr-settings-view';
|
|
import './shared/gr-lib-loader/gr-lib-loader';
|
|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
|
|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
|
|
import {PolymerElement} from '@polymer/polymer/polymer-element';
|
|
import {htmlTemplate} from './gr-app-element_html';
|
|
import {getBaseUrl} from '../utils/url-util';
|
|
import {
|
|
KeyboardShortcutMixin,
|
|
Shortcut,
|
|
SPECIAL_SHORTCUT,
|
|
} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
|
|
import {GerritNav} from './core/gr-navigation/gr-navigation';
|
|
import {appContext} from '../services/app-context';
|
|
import {flush} from '@polymer/polymer/lib/utils/flush';
|
|
import {customElement, observe, property} from '@polymer/decorators';
|
|
import {GrRouter} from './core/gr-router/gr-router';
|
|
import {
|
|
AccountDetailInfo,
|
|
ElementPropertyDeepChange,
|
|
ServerInfo,
|
|
} from '../types/common';
|
|
import {GrErrorManager} from './core/gr-error-manager/gr-error-manager';
|
|
import {GrOverlay} from './shared/gr-overlay/gr-overlay';
|
|
import {GrRegistrationDialog} from './settings/gr-registration-dialog/gr-registration-dialog';
|
|
import {
|
|
AppElementJustRegisteredParams,
|
|
AppElementParams,
|
|
isAppElementJustRegisteredParams,
|
|
} from './gr-app-types';
|
|
import {GrMainHeader} from './core/gr-main-header/gr-main-header';
|
|
import {GrSettingsView} from './settings/gr-settings-view/gr-settings-view';
|
|
import {
|
|
CustomKeyboardEvent,
|
|
LocationChangeEvent,
|
|
PageErrorEventDetail,
|
|
RpcLogEvent,
|
|
ShortcutTriggeredEvent,
|
|
TitleChangeEventDetail,
|
|
} from '../types/events';
|
|
import {ViewState} from '../types/types';
|
|
import {EventType} from '../utils/event-util';
|
|
import {GerritView} from '../services/router/router-model';
|
|
|
|
interface ErrorInfo {
|
|
text: string;
|
|
emoji?: string;
|
|
moreInfo?: string;
|
|
}
|
|
|
|
export interface GrAppElement {
|
|
$: {
|
|
router: GrRouter;
|
|
errorManager: GrErrorManager;
|
|
errorView: HTMLDivElement;
|
|
mainHeader: GrMainHeader;
|
|
};
|
|
}
|
|
|
|
// TODO(TS): implement AppElement interface from gr-app-types.ts
|
|
@customElement('gr-app-element')
|
|
export class GrAppElement extends KeyboardShortcutMixin(
|
|
GestureEventListeners(LegacyElementMixin(PolymerElement))
|
|
) {
|
|
static get template() {
|
|
return htmlTemplate;
|
|
}
|
|
|
|
/**
|
|
* Fired when the URL location changes.
|
|
*
|
|
* @event location-change
|
|
*/
|
|
|
|
@property({type: Object})
|
|
params?: AppElementParams;
|
|
|
|
@property({type: Object})
|
|
keyEventTarget = document.body;
|
|
|
|
@property({type: Object, observer: '_accountChanged'})
|
|
_account?: AccountDetailInfo;
|
|
|
|
@property({type: Number})
|
|
_lastGKeyPressTimestamp: number | null = null;
|
|
|
|
@property({type: Object})
|
|
_serverConfig?: ServerInfo;
|
|
|
|
@property({type: String})
|
|
_version?: string;
|
|
|
|
@property({type: Boolean})
|
|
_showChangeListView?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showDashboardView?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showChangeView?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showDiffView?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showSettingsView?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showAdminView?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showCLAView?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showEditorView?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showPluginScreen?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showDocumentationSearch?: boolean;
|
|
|
|
@property({type: Object})
|
|
_viewState?: ViewState;
|
|
|
|
@property({type: Object})
|
|
_lastError?: ErrorInfo;
|
|
|
|
@property({type: String})
|
|
_lastSearchPage?: string;
|
|
|
|
@property({type: String})
|
|
_path?: string;
|
|
|
|
@property({type: String, computed: '_computePluginScreenName(params)'})
|
|
_pluginScreenName?: string;
|
|
|
|
@property({type: String})
|
|
_settingsUrl?: string;
|
|
|
|
@property({type: String})
|
|
_feedbackUrl?: string;
|
|
|
|
@property({type: Boolean})
|
|
mobileSearch = false;
|
|
|
|
@property({type: String})
|
|
_loginUrl = '/login';
|
|
|
|
@property({type: Boolean})
|
|
loadRegistrationDialog = false;
|
|
|
|
@property({type: Boolean})
|
|
loadKeyboardShortcutsDialog = false;
|
|
|
|
private reporting = appContext.reportingService;
|
|
|
|
private restApiService = appContext.restApiService;
|
|
|
|
keyboardShortcuts() {
|
|
return {
|
|
[Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
|
|
[Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard',
|
|
[Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
|
|
[Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
|
|
[Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
|
|
[Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges',
|
|
};
|
|
}
|
|
|
|
/** @override */
|
|
created() {
|
|
super.created();
|
|
this._bindKeyboardShortcuts();
|
|
this.addEventListener(EventType.PAGE_ERROR, e => {
|
|
this._handlePageError(e);
|
|
});
|
|
this.addEventListener(EventType.TITLE_CHANGE, e => {
|
|
this._handleTitleChange(e);
|
|
});
|
|
this.addEventListener('location-change', e =>
|
|
this._handleLocationChange(e)
|
|
);
|
|
document.addEventListener('gr-rpc-log', e => this._handleRpcLog(e));
|
|
this.addEventListener('shortcut-triggered', e =>
|
|
this._handleShortcutTriggered(e)
|
|
);
|
|
// Ideally individual views should handle this event and respond with a soft
|
|
// reload. This is a catch-all for all views that cannot or have not
|
|
// implemented that.
|
|
this.addEventListener('reload', () => window.location.reload());
|
|
}
|
|
|
|
/** @override */
|
|
ready() {
|
|
super.ready();
|
|
this._updateLoginUrl();
|
|
this.reporting.appStarted();
|
|
this.$.router.start();
|
|
|
|
this.restApiService.getAccount().then(account => {
|
|
this._account = account;
|
|
const role = account ? 'user' : 'guest';
|
|
this.reporting.reportLifeCycle(`Started as ${role}`);
|
|
});
|
|
this.restApiService.getConfig().then(config => {
|
|
this._serverConfig = config;
|
|
|
|
if (config && config.gerrit && config.gerrit.report_bug_url) {
|
|
this._feedbackUrl = config.gerrit.report_bug_url;
|
|
}
|
|
});
|
|
this.restApiService.getVersion().then(version => {
|
|
this._version = version;
|
|
this._logWelcome();
|
|
});
|
|
|
|
if (window.localStorage.getItem('dark-theme')) {
|
|
applyDarkTheme();
|
|
}
|
|
|
|
// Note: this is evaluated here to ensure that it only happens after the
|
|
// router has been initialized. @see Issue 7837
|
|
this._settingsUrl = GerritNav.getUrlForSettings();
|
|
|
|
this._viewState = {
|
|
changeView: {
|
|
changeNum: null,
|
|
patchRange: null,
|
|
selectedFileIndex: 0,
|
|
showReplyDialog: false,
|
|
showDownloadDialog: false,
|
|
diffMode: null,
|
|
numFilesShown: null,
|
|
scrollTop: 0,
|
|
},
|
|
changeListView: {
|
|
query: null,
|
|
offset: 0,
|
|
selectedChangeIndex: 0,
|
|
},
|
|
dashboardView: {
|
|
selectedChangeIndex: 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
_bindKeyboardShortcuts() {
|
|
this.bindShortcut(
|
|
Shortcut.SEND_REPLY,
|
|
SPECIAL_SHORTCUT.DOC_ONLY,
|
|
'ctrl+enter',
|
|
'meta+enter'
|
|
);
|
|
this.bindShortcut(Shortcut.EMOJI_DROPDOWN, SPECIAL_SHORTCUT.DOC_ONLY, ':');
|
|
|
|
this.bindShortcut(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
|
|
this.bindShortcut(
|
|
Shortcut.GO_TO_USER_DASHBOARD,
|
|
SPECIAL_SHORTCUT.GO_KEY,
|
|
'i'
|
|
);
|
|
this.bindShortcut(
|
|
Shortcut.GO_TO_OPENED_CHANGES,
|
|
SPECIAL_SHORTCUT.GO_KEY,
|
|
'o'
|
|
);
|
|
this.bindShortcut(
|
|
Shortcut.GO_TO_MERGED_CHANGES,
|
|
SPECIAL_SHORTCUT.GO_KEY,
|
|
'm'
|
|
);
|
|
this.bindShortcut(
|
|
Shortcut.GO_TO_ABANDONED_CHANGES,
|
|
SPECIAL_SHORTCUT.GO_KEY,
|
|
'a'
|
|
);
|
|
this.bindShortcut(
|
|
Shortcut.GO_TO_WATCHED_CHANGES,
|
|
SPECIAL_SHORTCUT.GO_KEY,
|
|
'w'
|
|
);
|
|
|
|
this.bindShortcut(Shortcut.CURSOR_NEXT_CHANGE, 'j');
|
|
this.bindShortcut(Shortcut.CURSOR_PREV_CHANGE, 'k');
|
|
this.bindShortcut(Shortcut.OPEN_CHANGE, 'o');
|
|
this.bindShortcut(Shortcut.NEXT_PAGE, 'n', ']');
|
|
this.bindShortcut(Shortcut.PREV_PAGE, 'p', '[');
|
|
this.bindShortcut(Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
|
|
this.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's:keydown');
|
|
this.bindShortcut(Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
|
|
this.bindShortcut(Shortcut.EDIT_TOPIC, 't');
|
|
|
|
this.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a:keyup');
|
|
this.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd:keyup');
|
|
this.bindShortcut(Shortcut.EXPAND_ALL_MESSAGES, 'x');
|
|
this.bindShortcut(Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
|
|
this.bindShortcut(Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
|
|
this.bindShortcut(Shortcut.UP_TO_DASHBOARD, 'u');
|
|
this.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
|
|
this.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
|
|
this.bindShortcut(
|
|
Shortcut.DIFF_AGAINST_BASE,
|
|
SPECIAL_SHORTCUT.V_KEY,
|
|
'down',
|
|
's'
|
|
);
|
|
// this keyboard shortcut is used in toast _displayDiffAgainstLatestToast
|
|
// in gr-diff-view. Any updates here should be reflected there
|
|
this.bindShortcut(
|
|
Shortcut.DIFF_AGAINST_LATEST,
|
|
SPECIAL_SHORTCUT.V_KEY,
|
|
'up',
|
|
'w'
|
|
);
|
|
// this keyboard shortcut is used in toast _displayDiffBaseAgainstLeftToast
|
|
// in gr-diff-view. Any updates here should be reflected there
|
|
this.bindShortcut(
|
|
Shortcut.DIFF_BASE_AGAINST_LEFT,
|
|
SPECIAL_SHORTCUT.V_KEY,
|
|
'left',
|
|
'a'
|
|
);
|
|
this.bindShortcut(
|
|
Shortcut.DIFF_RIGHT_AGAINST_LATEST,
|
|
SPECIAL_SHORTCUT.V_KEY,
|
|
'right',
|
|
'd'
|
|
);
|
|
this.bindShortcut(
|
|
Shortcut.DIFF_BASE_AGAINST_LATEST,
|
|
SPECIAL_SHORTCUT.V_KEY,
|
|
'b'
|
|
);
|
|
|
|
this.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
|
|
this.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
|
|
if (this._isCursorManagerSupportMoveToVisibleLine()) {
|
|
this.bindShortcut(Shortcut.VISIBLE_LINE, '.');
|
|
}
|
|
this.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
|
|
this.bindShortcut(Shortcut.PREV_CHUNK, 'p');
|
|
this.bindShortcut(Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
|
|
this.bindShortcut(Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
|
|
this.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
|
|
this.bindShortcut(
|
|
Shortcut.EXPAND_ALL_COMMENT_THREADS,
|
|
SPECIAL_SHORTCUT.DOC_ONLY,
|
|
'e'
|
|
);
|
|
this.bindShortcut(
|
|
Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
|
|
SPECIAL_SHORTCUT.DOC_ONLY,
|
|
'shift+e'
|
|
);
|
|
this.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
|
|
this.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
|
|
this.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
|
|
this.bindShortcut(Shortcut.NEW_COMMENT, 'c');
|
|
this.bindShortcut(
|
|
Shortcut.SAVE_COMMENT,
|
|
'ctrl+enter',
|
|
'meta+enter',
|
|
'ctrl+s',
|
|
'meta+s'
|
|
);
|
|
this.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
|
|
this.bindShortcut(Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
|
|
|
|
this.bindShortcut(Shortcut.NEXT_FILE, ']');
|
|
this.bindShortcut(Shortcut.PREV_FILE, '[');
|
|
this.bindShortcut(Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
|
|
this.bindShortcut(Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
|
|
this.bindShortcut(Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
|
|
this.bindShortcut(Shortcut.CURSOR_PREV_FILE, 'k', 'up');
|
|
this.bindShortcut(Shortcut.OPEN_FILE, 'o', 'enter');
|
|
this.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
|
|
this.bindShortcut(Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
|
|
this.bindShortcut(Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
|
|
this.bindShortcut(Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
|
|
this.bindShortcut(Shortcut.TOGGLE_BLAME, 'b:keyup');
|
|
this.bindShortcut(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
|
|
this.bindShortcut(Shortcut.OPEN_FILE_LIST, 'f');
|
|
|
|
this.bindShortcut(Shortcut.OPEN_FIRST_FILE, ']');
|
|
this.bindShortcut(Shortcut.OPEN_LAST_FILE, '[');
|
|
|
|
this.bindShortcut(Shortcut.SEARCH, '/');
|
|
}
|
|
|
|
_isCursorManagerSupportMoveToVisibleLine() {
|
|
// This method is a copy-paste from the
|
|
// method _isIntersectionObserverSupported of gr-cursor-manager.js
|
|
// It is better share this method with gr-cursor-manager,
|
|
// but doing it require a lot if changes instead of 1-line copied code
|
|
return 'IntersectionObserver' in window;
|
|
}
|
|
|
|
_accountChanged(account?: AccountDetailInfo) {
|
|
if (!account) return;
|
|
|
|
// Preferences are cached when a user is logged in; warm them.
|
|
this.restApiService.getPreferences();
|
|
this.restApiService.getDiffPreferences();
|
|
this.restApiService.getEditPreferences();
|
|
this.$.errorManager.knownAccountId =
|
|
(this._account && this._account._account_id) || null;
|
|
}
|
|
|
|
@observe('params.view')
|
|
_viewChanged(view?: GerritView) {
|
|
this.$.errorView.classList.remove('show');
|
|
this.set('_showChangeListView', view === GerritView.SEARCH);
|
|
this.set('_showDashboardView', view === GerritView.DASHBOARD);
|
|
this.set('_showChangeView', view === GerritView.CHANGE);
|
|
this.set('_showDiffView', view === GerritView.DIFF);
|
|
this.set('_showSettingsView', view === GerritView.SETTINGS);
|
|
// _showAdminView must be in sync with the gr-admin-view AdminViewParams type
|
|
this.set(
|
|
'_showAdminView',
|
|
view === GerritView.ADMIN ||
|
|
view === GerritView.GROUP ||
|
|
view === GerritView.REPO
|
|
);
|
|
this.set('_showCLAView', view === GerritView.AGREEMENTS);
|
|
this.set('_showEditorView', view === GerritView.EDIT);
|
|
const isPluginScreen = view === GerritView.PLUGIN_SCREEN;
|
|
this.set('_showPluginScreen', false);
|
|
// Navigation within plugin screens does not restamp gr-endpoint-decorator
|
|
// because _showPluginScreen value does not change. To force restamp,
|
|
// change _showPluginScreen value between true and false.
|
|
if (isPluginScreen) {
|
|
this.async(() => this.set('_showPluginScreen', true), 1);
|
|
}
|
|
this.set(
|
|
'_showDocumentationSearch',
|
|
view === GerritView.DOCUMENTATION_SEARCH
|
|
);
|
|
if (
|
|
this.params &&
|
|
isAppElementJustRegisteredParams(this.params) &&
|
|
this.params.justRegistered
|
|
) {
|
|
this.loadRegistrationDialog = true;
|
|
flush();
|
|
const registrationOverlay = this.shadowRoot!.querySelector(
|
|
'#registrationOverlay'
|
|
) as GrOverlay;
|
|
const registrationDialog = this.shadowRoot!.querySelector(
|
|
'#registrationDialog'
|
|
) as GrRegistrationDialog;
|
|
registrationOverlay.open();
|
|
registrationDialog.loadData().then(() => {
|
|
registrationOverlay.refit();
|
|
});
|
|
}
|
|
// To fix bug announce read after each new view, we reset announce with
|
|
// empty space
|
|
this.fire('iron-announce', {text: ' '}, {bubbles: true});
|
|
}
|
|
|
|
_handleShortcutTriggered(event: ShortcutTriggeredEvent) {
|
|
const {event: e, goKey, vKey} = event.detail;
|
|
// eg: {key: "k:keydown", ..., from: "gr-diff-view"}
|
|
let key = `${((e as unknown) as KeyboardEvent).key}:${e.type}`;
|
|
if (goKey) key = 'g+' + key;
|
|
if (vKey) key = 'v+' + key;
|
|
if (e.shiftKey) key = 'shift+' + key;
|
|
if (e.ctrlKey) key = 'ctrl+' + key;
|
|
if (e.metaKey) key = 'meta+' + key;
|
|
if (e.altKey) key = 'alt+' + key;
|
|
const path = event.composedPath();
|
|
this.reporting.reportInteraction('shortcut-triggered', {
|
|
key,
|
|
from: (path && path[0] && (path[0] as Element).nodeName) ?? 'unknown',
|
|
});
|
|
}
|
|
|
|
_handlePageError(e: CustomEvent<PageErrorEventDetail>) {
|
|
const props = [
|
|
'_showChangeListView',
|
|
'_showDashboardView',
|
|
'_showChangeView',
|
|
'_showDiffView',
|
|
'_showSettingsView',
|
|
'_showAdminView',
|
|
];
|
|
for (const showProp of props) {
|
|
this.set(showProp, false);
|
|
}
|
|
|
|
this.$.errorView.classList.add('show');
|
|
const response = e.detail.response;
|
|
const err: ErrorInfo = {
|
|
text: [response.status, response.statusText].join(' '),
|
|
};
|
|
if (response.status === 404) {
|
|
err.emoji = '¯\\_(ツ)_/¯';
|
|
this._lastError = err;
|
|
} else {
|
|
err.emoji = 'o_O';
|
|
response.text().then(text => {
|
|
err.moreInfo = text;
|
|
this._lastError = err;
|
|
});
|
|
}
|
|
}
|
|
|
|
_handleLocationChange(e: LocationChangeEvent) {
|
|
this._updateLoginUrl();
|
|
|
|
const hash = e.detail.hash.substring(1);
|
|
let pathname = e.detail.pathname;
|
|
if (pathname.startsWith('/c/') && Number(hash) > 0) {
|
|
pathname += '@' + hash;
|
|
}
|
|
this.set('_path', pathname);
|
|
}
|
|
|
|
_updateLoginUrl() {
|
|
const baseUrl = getBaseUrl();
|
|
if (baseUrl) {
|
|
// Strip the canonical path from the path since needing canonical in
|
|
// the path is unneeded and breaks the url.
|
|
this._loginUrl =
|
|
baseUrl +
|
|
'/login/' +
|
|
encodeURIComponent(
|
|
'/' +
|
|
window.location.pathname.substring(baseUrl.length) +
|
|
window.location.search +
|
|
window.location.hash
|
|
);
|
|
} else {
|
|
this._loginUrl =
|
|
'/login/' +
|
|
encodeURIComponent(
|
|
window.location.pathname +
|
|
window.location.search +
|
|
window.location.hash
|
|
);
|
|
}
|
|
}
|
|
|
|
@observe('params.*')
|
|
_paramsChanged(
|
|
paramsRecord: ElementPropertyDeepChange<GrAppElement, 'params'>
|
|
) {
|
|
const params = paramsRecord.base;
|
|
const viewsToCheck = [GerritView.SEARCH, GerritView.DASHBOARD];
|
|
if (params?.view && viewsToCheck.includes(params.view)) {
|
|
this.set('_lastSearchPage', location.pathname);
|
|
}
|
|
}
|
|
|
|
_handleTitleChange(e: CustomEvent<TitleChangeEventDetail>) {
|
|
if (e.detail.title) {
|
|
document.title = e.detail.title + ' · Gerrit Code Review';
|
|
} else {
|
|
document.title = '';
|
|
}
|
|
}
|
|
|
|
handleShowKeyboardShortcuts() {
|
|
this.loadKeyboardShortcutsDialog = true;
|
|
flush();
|
|
(this.shadowRoot!.querySelector('#keyboardShortcuts') as GrOverlay).open();
|
|
}
|
|
|
|
_showKeyboardShortcuts(e: CustomKeyboardEvent) {
|
|
// same shortcut should close the dialog if pressed again
|
|
// when dialog is open
|
|
this.loadKeyboardShortcutsDialog = true;
|
|
flush();
|
|
const keyboardShortcuts = this.shadowRoot!.querySelector(
|
|
'#keyboardShortcuts'
|
|
) as GrOverlay;
|
|
if (!keyboardShortcuts) return;
|
|
if (keyboardShortcuts.opened) {
|
|
keyboardShortcuts.close();
|
|
return;
|
|
}
|
|
if (this.shouldSuppressKeyboardShortcut(e)) {
|
|
return;
|
|
}
|
|
keyboardShortcuts.open();
|
|
}
|
|
|
|
_handleKeyboardShortcutDialogClose() {
|
|
(this.shadowRoot!.querySelector('#keyboardShortcuts') as GrOverlay).close();
|
|
}
|
|
|
|
_handleAccountDetailUpdate() {
|
|
this.$.mainHeader.reload();
|
|
if (this.params?.view === GerritView.SETTINGS) {
|
|
(this.shadowRoot!.querySelector(
|
|
'gr-settings-view'
|
|
) as GrSettingsView).reloadAccountDetail();
|
|
}
|
|
}
|
|
|
|
_handleRegistrationDialogClose() {
|
|
// The registration dialog is visible only if this.params is
|
|
// instanceof AppElementJustRegisteredParams
|
|
(this.params as AppElementJustRegisteredParams).justRegistered = false;
|
|
(this.shadowRoot!.querySelector(
|
|
'#registrationOverlay'
|
|
) as GrOverlay).close();
|
|
}
|
|
|
|
_goToOpenedChanges() {
|
|
GerritNav.navigateToStatusSearch('open');
|
|
}
|
|
|
|
_goToUserDashboard() {
|
|
GerritNav.navigateToUserDashboard();
|
|
}
|
|
|
|
_goToMergedChanges() {
|
|
GerritNav.navigateToStatusSearch('merged');
|
|
}
|
|
|
|
_goToAbandonedChanges() {
|
|
GerritNav.navigateToStatusSearch('abandoned');
|
|
}
|
|
|
|
_goToWatchedChanges() {
|
|
// The query is hardcoded, and doesn't respect custom menu entries
|
|
GerritNav.navigateToSearchQuery('is:watched is:open');
|
|
}
|
|
|
|
_computePluginScreenName(params: AppElementParams) {
|
|
if (params.view !== GerritView.PLUGIN_SCREEN) return '';
|
|
if (!params.plugin || !params.screen) return '';
|
|
return `${params.plugin}-screen-${params.screen}`;
|
|
}
|
|
|
|
_logWelcome() {
|
|
console.group('Runtime Info');
|
|
console.info('Gerrit UI (PolyGerrit)');
|
|
console.info(`Gerrit Server Version: ${this._version}`);
|
|
if (window.VERSION_INFO) {
|
|
console.info(`UI Version Info: ${window.VERSION_INFO}`);
|
|
}
|
|
if (this._feedbackUrl) {
|
|
console.info(`Please file bugs and feedback at: ${this._feedbackUrl}`);
|
|
}
|
|
console.groupEnd();
|
|
}
|
|
|
|
/**
|
|
* Intercept RPC log events emitted by REST API interfaces.
|
|
* Note: the REST API interface cannot use gr-reporting directly because
|
|
* that would create a cyclic dependency.
|
|
*/
|
|
_handleRpcLog(e: RpcLogEvent) {
|
|
this.reporting.reportRpcTiming(e.detail.anonymizedUrl, e.detail.elapsed);
|
|
}
|
|
|
|
_mobileSearchToggle() {
|
|
this.mobileSearch = !this.mobileSearch;
|
|
}
|
|
|
|
getThemeEndpoint() {
|
|
// For now, we only have dark mode and light mode
|
|
return window.localStorage.getItem('dark-theme')
|
|
? 'app-theme-dark'
|
|
: 'app-theme-light';
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'gr-app-element': GrAppElement;
|
|
}
|
|
}
|