Merge changes from topics "gr-file-list-to-ts", "gr-smart-search-to-ts"
* changes: Convert files to typescript Rename files to preserve history Convert files to typescript Rename files to preserve history Convert gr-smart-search to typescript Rename files to preserve history
This commit is contained in:
@@ -277,6 +277,12 @@ module.exports = {
|
||||
"@typescript-eslint/restrict-plus-operands": "error",
|
||||
// https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/node-builtins.md
|
||||
"node/no-unsupported-features/node-builtins": "off",
|
||||
// Disable no-invalid-this for ts files, because it incorrectly reports
|
||||
// errors in some cases (see https://github.com/typescript-eslint/typescript-eslint/issues/491)
|
||||
// At the same time, we are using typescript in a strict mode and
|
||||
// it catches almost all errors related to invalid usage of this.
|
||||
"no-invalid-this": "off",
|
||||
|
||||
"jsdoc/no-types": 2,
|
||||
},
|
||||
"parserOptions": {
|
||||
|
||||
@@ -341,3 +341,20 @@ export enum NotifyType {
|
||||
OWNER_REVIEWERS = 'OWNER_REVIEWERS',
|
||||
ALL = 'ALL',
|
||||
}
|
||||
|
||||
/**
|
||||
* The authentication type that is configured on the server.
|
||||
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#auth-info
|
||||
*/
|
||||
export enum AuthType {
|
||||
OPENID = 'OPENID',
|
||||
OPENID_SSO = 'OPENID_SSO',
|
||||
OAUTH = 'OAUTH',
|
||||
HTTP = 'HTTP',
|
||||
HTTP_LDAP = 'HTTP_LDAP',
|
||||
CLIENT_SSL_CERT_LDAP = 'CLIENT_SSL_CERT_LDAP',
|
||||
LDAP = 'LDAP',
|
||||
LDAP_BIND = 'LDAP_BIND',
|
||||
CUSTOM_EXTENSION = 'CUSTOM_EXTENSION',
|
||||
DEVELOPMENT_BECOME_ANY_ACCOUNT = 'DEVELOPMENT_BECOME_ANY_ACCOUNT',
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import '../../shared/gr-icons/gr-icons.js';
|
||||
import '../gr-commit-info/gr-commit-info.js';
|
||||
import '../gr-download-dialog/gr-download-dialog.js';
|
||||
import '../gr-file-list-header/gr-file-list-header.js';
|
||||
import '../gr-file-list/gr-file-list.js';
|
||||
import '../gr-included-in-dialog/gr-included-in-dialog.js';
|
||||
import '../gr-messages-list/gr-messages-list.js';
|
||||
import '../gr-related-changes-list/gr-related-changes-list.js';
|
||||
@@ -73,6 +72,7 @@ import {
|
||||
} from '../../../utils/patch-set-util.js';
|
||||
import {changeStatuses, changeStatusString} from '../../../utils/change-util.js';
|
||||
import {EventType} from '../../plugins/gr-plugin-types.js';
|
||||
import {DEFAULT_NUM_FILES_SHOWN} from '../gr-file-list/gr-file-list.js';
|
||||
|
||||
const CHANGE_ID_ERROR = {
|
||||
MISMATCH: 'mismatch',
|
||||
@@ -82,7 +82,6 @@ const CHANGE_ID_REGEX_PATTERN =
|
||||
/^(Change-Id\:\s|Link:.*\/id\/)(I[0-9a-f]{8,40})/gm;
|
||||
|
||||
const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
|
||||
const DEFAULT_NUM_FILES_SHOWN = 200;
|
||||
|
||||
const REVIEWERS_REGEX = /^(R|CC)=/gm;
|
||||
const MIN_CHECK_INTERVAL_SECS = 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1901
polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
Normal file
1901
polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -375,7 +375,7 @@ export const htmlTemplate = html`
|
||||
<div class="stickyArea">
|
||||
<div
|
||||
class$="file-row row [[_computePathClass(file.__path, _expandedFiles.*)]]"
|
||||
data-file$="[[_computeFileRange(file)]]"
|
||||
data-file$="[[_computePatchSetFile(file)]]"
|
||||
tabindex="-1"
|
||||
role="row"
|
||||
>
|
||||
@@ -657,7 +657,7 @@ export const htmlTemplate = html`
|
||||
hidden="[[!_isFileExpanded(file.__path, _expandedFiles.*)]]"
|
||||
change-num="[[changeNum]]"
|
||||
patch-range="[[patchRange]]"
|
||||
file="[[_computeFileRange(file)]]"
|
||||
file="[[_computePatchSetFile(file)]]"
|
||||
path="[[file.__path]]"
|
||||
prefs="[[diffPrefs]]"
|
||||
project-name="[[change.project]]"
|
||||
|
||||
@@ -312,7 +312,7 @@ suite('gr-file-list tests', () => {
|
||||
|
||||
for (const bytes in table) {
|
||||
if (table.hasOwnProperty(bytes)) {
|
||||
assert.equal(element._formatBytes(bytes), table[bytes]);
|
||||
assert.equal(element._formatBytes(Number(bytes)), table[bytes]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1329,15 +1329,23 @@ suite('gr-file-list tests', () => {
|
||||
|
||||
suite('size bars', () => {
|
||||
test('_computeSizeBarLayout', () => {
|
||||
assert.isUndefined(element._computeSizeBarLayout(null));
|
||||
assert.isUndefined(element._computeSizeBarLayout({}));
|
||||
assert.deepEqual(element._computeSizeBarLayout({base: []}), {
|
||||
const defaultSizeBarLayout = {
|
||||
maxInserted: 0,
|
||||
maxDeleted: 0,
|
||||
maxAdditionWidth: 0,
|
||||
maxDeletionWidth: 0,
|
||||
deletionOffset: 0,
|
||||
});
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
element._computeSizeBarLayout(null),
|
||||
defaultSizeBarLayout);
|
||||
assert.deepEqual(
|
||||
element._computeSizeBarLayout({}),
|
||||
defaultSizeBarLayout);
|
||||
assert.deepEqual(
|
||||
element._computeSizeBarLayout({base: []}),
|
||||
defaultSizeBarLayout);
|
||||
|
||||
const files = [
|
||||
{__path: '/COMMIT_MSG', lines_inserted: 10000},
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2016 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 '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
|
||||
import '../../shared/gr-dropdown/gr-dropdown.js';
|
||||
import '../../shared/gr-icons/gr-icons.js';
|
||||
import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
|
||||
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
|
||||
import '../gr-account-dropdown/gr-account-dropdown.js';
|
||||
import '../gr-smart-search/gr-smart-search.js';
|
||||
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
|
||||
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
|
||||
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
|
||||
import {htmlTemplate} from './gr-main-header_html.js';
|
||||
import {getBaseUrl, getDocsBaseUrl} from '../../../utils/url-util.js';
|
||||
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
|
||||
import {getAdminLinks} from '../../../utils/admin-nav-util.js';
|
||||
|
||||
const DEFAULT_LINKS = [{
|
||||
title: 'Changes',
|
||||
links: [
|
||||
{
|
||||
url: '/q/status:open+-is:wip',
|
||||
name: 'Open',
|
||||
},
|
||||
{
|
||||
url: '/q/status:merged',
|
||||
name: 'Merged',
|
||||
},
|
||||
{
|
||||
url: '/q/status:abandoned',
|
||||
name: 'Abandoned',
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
const DOCUMENTATION_LINKS = [
|
||||
{
|
||||
url: '/index.html',
|
||||
name: 'Table of Contents',
|
||||
},
|
||||
{
|
||||
url: '/user-search.html',
|
||||
name: 'Searching',
|
||||
},
|
||||
{
|
||||
url: '/user-upload.html',
|
||||
name: 'Uploading',
|
||||
},
|
||||
{
|
||||
url: '/access-control.html',
|
||||
name: 'Access Control',
|
||||
},
|
||||
{
|
||||
url: '/rest-api.html',
|
||||
name: 'REST API',
|
||||
},
|
||||
{
|
||||
url: '/intro-project-owner.html',
|
||||
name: 'Project Owner Guide',
|
||||
},
|
||||
];
|
||||
|
||||
// Set of authentication methods that can provide custom registration page.
|
||||
const AUTH_TYPES_WITH_REGISTER_URL = new Set([
|
||||
'LDAP',
|
||||
'LDAP_BIND',
|
||||
'CUSTOM_EXTENSION',
|
||||
]);
|
||||
|
||||
/**
|
||||
* @extends PolymerElement
|
||||
*/
|
||||
class GrMainHeader extends GestureEventListeners(
|
||||
LegacyElementMixin(
|
||||
PolymerElement)) {
|
||||
static get template() { return htmlTemplate; }
|
||||
|
||||
static get is() { return 'gr-main-header'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
searchQuery: {
|
||||
type: String,
|
||||
notify: true,
|
||||
},
|
||||
loggedIn: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
|
||||
/** @type {?Object} */
|
||||
_account: Object,
|
||||
_adminLinks: {
|
||||
type: Array,
|
||||
value() { return []; },
|
||||
},
|
||||
_defaultLinks: {
|
||||
type: Array,
|
||||
value() {
|
||||
return DEFAULT_LINKS;
|
||||
},
|
||||
},
|
||||
_docBaseUrl: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
_links: {
|
||||
type: Array,
|
||||
computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' +
|
||||
'_topMenus, _docBaseUrl)',
|
||||
},
|
||||
loginUrl: {
|
||||
type: String,
|
||||
value: '/login',
|
||||
},
|
||||
_userLinks: {
|
||||
type: Array,
|
||||
value() { return []; },
|
||||
},
|
||||
_topMenus: {
|
||||
type: Array,
|
||||
value() { return []; },
|
||||
},
|
||||
_registerText: {
|
||||
type: String,
|
||||
value: 'Sign up',
|
||||
},
|
||||
_registerURL: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
mobileSearchHidden: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'_accountLoaded(_account)',
|
||||
];
|
||||
}
|
||||
|
||||
/** @override */
|
||||
ready() {
|
||||
super.ready();
|
||||
this._ensureAttribute('role', 'banner');
|
||||
}
|
||||
|
||||
/** @override */
|
||||
attached() {
|
||||
super.attached();
|
||||
this._loadAccount();
|
||||
this._loadConfig();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
detached() {
|
||||
super.detached();
|
||||
}
|
||||
|
||||
reload() {
|
||||
this._loadAccount();
|
||||
}
|
||||
|
||||
_computeRelativeURL(path) {
|
||||
return '//' + window.location.host + getBaseUrl() + path;
|
||||
}
|
||||
|
||||
_computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) {
|
||||
// Polymer 2: check for undefined
|
||||
if ([
|
||||
defaultLinks,
|
||||
userLinks,
|
||||
adminLinks,
|
||||
topMenus,
|
||||
docBaseUrl,
|
||||
].includes(undefined)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const links = defaultLinks.map(menu => {
|
||||
return {
|
||||
title: menu.title,
|
||||
links: menu.links.slice(),
|
||||
};
|
||||
});
|
||||
if (userLinks && userLinks.length > 0) {
|
||||
links.push({
|
||||
title: 'Your',
|
||||
links: userLinks.slice(),
|
||||
});
|
||||
}
|
||||
const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
|
||||
if (docLinks.length) {
|
||||
links.push({
|
||||
title: 'Documentation',
|
||||
links: docLinks,
|
||||
class: 'hideOnMobile',
|
||||
});
|
||||
}
|
||||
links.push({
|
||||
title: 'Browse',
|
||||
links: adminLinks.slice(),
|
||||
});
|
||||
const topMenuLinks = [];
|
||||
links.forEach(link => { topMenuLinks[link.title] = link.links; });
|
||||
for (const m of topMenus) {
|
||||
const items = m.items.map(this._fixCustomMenuItem).filter(link =>
|
||||
// Ignore GWT project links
|
||||
!link.url.includes('${projectName}')
|
||||
);
|
||||
if (m.name in topMenuLinks) {
|
||||
items.forEach(link => { topMenuLinks[m.name].push(link); });
|
||||
} else {
|
||||
links.push({
|
||||
title: m.name,
|
||||
links: topMenuLinks[m.name] = items,
|
||||
});
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
_getDocLinks(docBaseUrl, docLinks) {
|
||||
if (!docBaseUrl || !docLinks) {
|
||||
return [];
|
||||
}
|
||||
return docLinks.map(link => {
|
||||
let url = docBaseUrl;
|
||||
if (url && url[url.length - 1] === '/') {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
return {
|
||||
url: url + link.url,
|
||||
name: link.name,
|
||||
target: '_blank',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_loadAccount() {
|
||||
this.loading = true;
|
||||
const promises = [
|
||||
this.$.restAPI.getAccount(),
|
||||
this.$.restAPI.getTopMenus(),
|
||||
getPluginLoader().awaitPluginsLoaded(),
|
||||
];
|
||||
|
||||
return Promise.all(promises).then(result => {
|
||||
const account = result[0];
|
||||
this._account = account;
|
||||
this.loggedIn = !!account;
|
||||
this.loading = false;
|
||||
this._topMenus = result[1];
|
||||
|
||||
return getAdminLinks(account,
|
||||
params => this.$.restAPI.getAccountCapabilities(params),
|
||||
() => this.$.jsAPI.getAdminMenuLinks())
|
||||
.then(res => {
|
||||
this._adminLinks = res.links;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_loadConfig() {
|
||||
this.$.restAPI.getConfig()
|
||||
.then(config => {
|
||||
this._retrieveRegisterURL(config);
|
||||
return getDocsBaseUrl(config, this.$.restAPI);
|
||||
})
|
||||
.then(docBaseUrl => { this._docBaseUrl = docBaseUrl; });
|
||||
}
|
||||
|
||||
_accountLoaded(account) {
|
||||
if (!account) { return; }
|
||||
|
||||
this.$.restAPI.getPreferences().then(prefs => {
|
||||
this._userLinks = prefs && prefs.my ?
|
||||
prefs.my.map(this._fixCustomMenuItem) : [];
|
||||
});
|
||||
}
|
||||
|
||||
_retrieveRegisterURL(config) {
|
||||
if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) {
|
||||
this._registerURL = config.auth.register_url;
|
||||
if (config.auth.register_text) {
|
||||
this._registerText = config.auth.register_text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_computeIsInvisible(registerURL) {
|
||||
return registerURL ? '' : 'invisible';
|
||||
}
|
||||
|
||||
_fixCustomMenuItem(linkObj) {
|
||||
// Normalize all urls to PolyGerrit style.
|
||||
if (linkObj.url.startsWith('#')) {
|
||||
linkObj.url = linkObj.url.slice(1);
|
||||
}
|
||||
|
||||
// Delete target property due to complications of
|
||||
// https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
|
||||
//
|
||||
// The server tries to guess whether URL is a view within the UI.
|
||||
// If not, it sets target='_blank' on the menu item. The server
|
||||
// makes assumptions that work for the GWT UI, but not PolyGerrit,
|
||||
// so we'll just disable it altogether for now.
|
||||
delete linkObj.target;
|
||||
|
||||
return linkObj;
|
||||
}
|
||||
|
||||
_generateSettingsLink() {
|
||||
return getBaseUrl() + '/settings/';
|
||||
}
|
||||
|
||||
_onMobileSearchTap(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent('mobile-search', {
|
||||
composed: true, bubbles: false,
|
||||
}));
|
||||
}
|
||||
|
||||
_computeLinkGroupClass(linkGroup) {
|
||||
if (linkGroup && linkGroup.class) {
|
||||
return linkGroup.class;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
_computeShowHideAriaLabel(mobileSearchHidden) {
|
||||
if (mobileSearchHidden) {
|
||||
return 'Show Searchbar';
|
||||
} else {
|
||||
return 'Hide Searchbar';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(GrMainHeader.is, GrMainHeader);
|
||||
396
polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
Normal file
396
polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2016 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 '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
|
||||
import '../../shared/gr-dropdown/gr-dropdown';
|
||||
import '../../shared/gr-icons/gr-icons';
|
||||
import '../../shared/gr-js-api-interface/gr-js-api-interface';
|
||||
import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
|
||||
import '../gr-account-dropdown/gr-account-dropdown';
|
||||
import '../gr-smart-search/gr-smart-search';
|
||||
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-main-header_html';
|
||||
import {getBaseUrl, getDocsBaseUrl} from '../../../utils/url-util';
|
||||
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
|
||||
import {getAdminLinks, NavLink} from '../../../utils/admin-nav-util';
|
||||
import {customElement, property, observe} from '@polymer/decorators';
|
||||
import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
|
||||
import {
|
||||
AccountDetailInfo,
|
||||
ServerInfo,
|
||||
TopMenuEntryInfo,
|
||||
TopMenuItemInfo,
|
||||
} from '../../../types/common';
|
||||
import {JsApiService} from '../../shared/gr-js-api-interface/gr-js-api-types';
|
||||
import {AuthType} from '../../../constants/constants';
|
||||
|
||||
interface FixedTopMenuItemInfo extends Omit<TopMenuItemInfo, 'target'> {
|
||||
target?: never;
|
||||
}
|
||||
interface MainHeaderLink {
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
interface MainHeaderLinkGroup {
|
||||
title: string;
|
||||
links: MainHeaderLink[];
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_LINKS: MainHeaderLinkGroup[] = [
|
||||
{
|
||||
title: 'Changes',
|
||||
links: [
|
||||
{
|
||||
url: '/q/status:open+-is:wip',
|
||||
name: 'Open',
|
||||
},
|
||||
{
|
||||
url: '/q/status:merged',
|
||||
name: 'Merged',
|
||||
},
|
||||
{
|
||||
url: '/q/status:abandoned',
|
||||
name: 'Abandoned',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const DOCUMENTATION_LINKS: MainHeaderLink[] = [
|
||||
{
|
||||
url: '/index.html',
|
||||
name: 'Table of Contents',
|
||||
},
|
||||
{
|
||||
url: '/user-search.html',
|
||||
name: 'Searching',
|
||||
},
|
||||
{
|
||||
url: '/user-upload.html',
|
||||
name: 'Uploading',
|
||||
},
|
||||
{
|
||||
url: '/access-control.html',
|
||||
name: 'Access Control',
|
||||
},
|
||||
{
|
||||
url: '/rest-api.html',
|
||||
name: 'REST API',
|
||||
},
|
||||
{
|
||||
url: '/intro-project-owner.html',
|
||||
name: 'Project Owner Guide',
|
||||
},
|
||||
];
|
||||
|
||||
// Set of authentication methods that can provide custom registration page.
|
||||
const AUTH_TYPES_WITH_REGISTER_URL: Set<AuthType> = new Set([
|
||||
AuthType.LDAP,
|
||||
AuthType.LDAP_BIND,
|
||||
AuthType.CUSTOM_EXTENSION,
|
||||
]);
|
||||
|
||||
export interface GrMainHeader {
|
||||
$: {
|
||||
restAPI: RestApiService & Element;
|
||||
jsAPI: JsApiService & Element;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement('gr-main-header')
|
||||
export class GrMainHeader extends GestureEventListeners(
|
||||
LegacyElementMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return htmlTemplate;
|
||||
}
|
||||
|
||||
@property({type: String, notify: true})
|
||||
searchQuery?: string;
|
||||
|
||||
@property({type: Boolean, reflectToAttribute: true})
|
||||
loggedIn?: boolean;
|
||||
|
||||
@property({type: Boolean, reflectToAttribute: true})
|
||||
loading?: boolean;
|
||||
|
||||
@property({type: Object})
|
||||
_account?: AccountDetailInfo;
|
||||
|
||||
@property({type: Array})
|
||||
_adminLinks: NavLink[] = [];
|
||||
|
||||
@property({type: String})
|
||||
_docBaseUrl: string | null = null;
|
||||
|
||||
@property({
|
||||
type: Array,
|
||||
computed: '_computeLinks(_userLinks, _adminLinks, _topMenus, _docBaseUrl)',
|
||||
})
|
||||
_links?: MainHeaderLinkGroup[];
|
||||
|
||||
@property({type: String})
|
||||
loginUrl = '/login';
|
||||
|
||||
@property({type: Array})
|
||||
_userLinks: FixedTopMenuItemInfo[] = [];
|
||||
|
||||
@property({type: Array})
|
||||
_topMenus?: TopMenuEntryInfo[] = [];
|
||||
|
||||
@property({type: String})
|
||||
_registerText = 'Sign up';
|
||||
|
||||
@property({type: String})
|
||||
_registerURL?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
mobileSearchHidden = false;
|
||||
|
||||
/** @override */
|
||||
ready() {
|
||||
super.ready();
|
||||
this._ensureAttribute('role', 'banner');
|
||||
}
|
||||
|
||||
/** @override */
|
||||
attached() {
|
||||
super.attached();
|
||||
this._loadAccount();
|
||||
this._loadConfig();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
detached() {
|
||||
super.detached();
|
||||
}
|
||||
|
||||
reload() {
|
||||
this._loadAccount();
|
||||
}
|
||||
|
||||
_computeRelativeURL(path: string) {
|
||||
return '//' + window.location.host + getBaseUrl() + path;
|
||||
}
|
||||
|
||||
_computeLinks(
|
||||
userLinks?: FixedTopMenuItemInfo[],
|
||||
adminLinks?: NavLink[],
|
||||
topMenus?: TopMenuEntryInfo[],
|
||||
docBaseUrl?: string | null,
|
||||
// defaultLinks parameter is used in tests only
|
||||
defaultLinks = DEFAULT_LINKS
|
||||
) {
|
||||
// Polymer 2: check for undefined
|
||||
if (
|
||||
userLinks === undefined ||
|
||||
adminLinks === undefined ||
|
||||
topMenus === undefined ||
|
||||
docBaseUrl === undefined
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const links: MainHeaderLinkGroup[] = defaultLinks.map(menu => {
|
||||
return {
|
||||
title: menu.title,
|
||||
links: menu.links.slice(),
|
||||
};
|
||||
});
|
||||
if (userLinks && userLinks.length > 0) {
|
||||
links.push({
|
||||
title: 'Your',
|
||||
links: userLinks.slice(),
|
||||
});
|
||||
}
|
||||
const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
|
||||
if (docLinks.length) {
|
||||
links.push({
|
||||
title: 'Documentation',
|
||||
links: docLinks,
|
||||
class: 'hideOnMobile',
|
||||
});
|
||||
}
|
||||
links.push({
|
||||
title: 'Browse',
|
||||
links: adminLinks.slice(),
|
||||
});
|
||||
const topMenuLinks: {[name: string]: MainHeaderLink[]} = {};
|
||||
links.forEach(link => {
|
||||
topMenuLinks[link.title] = link.links;
|
||||
});
|
||||
for (const m of topMenus) {
|
||||
const items = m.items.map(this._fixCustomMenuItem).filter(
|
||||
link =>
|
||||
// Ignore GWT project links
|
||||
!link.url.includes('${projectName}')
|
||||
);
|
||||
if (m.name in topMenuLinks) {
|
||||
items.forEach(link => {
|
||||
topMenuLinks[m.name].push(link);
|
||||
});
|
||||
} else {
|
||||
links.push({
|
||||
title: m.name,
|
||||
links: topMenuLinks[m.name] = items,
|
||||
});
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
_getDocLinks(docBaseUrl: string | null, docLinks: MainHeaderLink[]) {
|
||||
if (!docBaseUrl) {
|
||||
return [];
|
||||
}
|
||||
return docLinks.map(link => {
|
||||
let url = docBaseUrl;
|
||||
if (url && url[url.length - 1] === '/') {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
return {
|
||||
url: url + link.url,
|
||||
name: link.name,
|
||||
target: '_blank',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_loadAccount() {
|
||||
this.loading = true;
|
||||
|
||||
return Promise.all([
|
||||
this.$.restAPI.getAccount(),
|
||||
this.$.restAPI.getTopMenus(),
|
||||
getPluginLoader().awaitPluginsLoaded(),
|
||||
]).then(result => {
|
||||
const account = result[0];
|
||||
this._account = account;
|
||||
this.loggedIn = !!account;
|
||||
this.loading = false;
|
||||
this._topMenus = result[1];
|
||||
|
||||
return getAdminLinks(
|
||||
account,
|
||||
() =>
|
||||
this.$.restAPI.getAccountCapabilities().then(capabilities => {
|
||||
if (!capabilities) {
|
||||
throw new Error('getAccountCapabilities returns undefined');
|
||||
}
|
||||
return capabilities;
|
||||
}),
|
||||
() => this.$.jsAPI.getAdminMenuLinks()
|
||||
).then(res => {
|
||||
this._adminLinks = res.links;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_loadConfig() {
|
||||
this.$.restAPI
|
||||
.getConfig()
|
||||
.then(config => {
|
||||
if (!config) {
|
||||
throw new Error('getConfig returned undefined');
|
||||
}
|
||||
this._retrieveRegisterURL(config);
|
||||
return getDocsBaseUrl(config, this.$.restAPI);
|
||||
})
|
||||
.then(docBaseUrl => {
|
||||
this._docBaseUrl = docBaseUrl;
|
||||
});
|
||||
}
|
||||
|
||||
@observe('_account')
|
||||
_accountLoaded(account?: AccountDetailInfo) {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.restAPI.getPreferences().then(prefs => {
|
||||
this._userLinks =
|
||||
prefs && prefs.my ? prefs.my.map(this._fixCustomMenuItem) : [];
|
||||
});
|
||||
}
|
||||
|
||||
_retrieveRegisterURL(config: ServerInfo) {
|
||||
if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) {
|
||||
this._registerURL = config.auth.register_url;
|
||||
if (config.auth.register_text) {
|
||||
this._registerText = config.auth.register_text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_computeIsInvisible(registerURL?: string) {
|
||||
return registerURL ? '' : 'invisible';
|
||||
}
|
||||
|
||||
_fixCustomMenuItem(linkObj: TopMenuItemInfo): FixedTopMenuItemInfo {
|
||||
// TODO(TS): make a copy of linkObj instead of modifying the existing one
|
||||
// Normalize all urls to PolyGerrit style.
|
||||
if (linkObj.url.startsWith('#')) {
|
||||
linkObj.url = linkObj.url.slice(1);
|
||||
}
|
||||
|
||||
// Delete target property due to complications of
|
||||
// https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
|
||||
//
|
||||
// The server tries to guess whether URL is a view within the UI.
|
||||
// If not, it sets target='_blank' on the menu item. The server
|
||||
// makes assumptions that work for the GWT UI, but not PolyGerrit,
|
||||
// so we'll just disable it altogether for now.
|
||||
delete linkObj.target;
|
||||
|
||||
return (linkObj as unknown) as FixedTopMenuItemInfo;
|
||||
}
|
||||
|
||||
_generateSettingsLink() {
|
||||
return getBaseUrl() + '/settings/';
|
||||
}
|
||||
|
||||
_onMobileSearchTap(e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('mobile-search', {
|
||||
composed: true,
|
||||
bubbles: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_computeLinkGroupClass(linkGroup: MainHeaderLinkGroup) {
|
||||
return linkGroup.class ?? '';
|
||||
}
|
||||
|
||||
_computeShowHideAriaLabel(mobileSearchHidden: boolean) {
|
||||
if (mobileSearchHidden) {
|
||||
return 'Show Searchbar';
|
||||
} else {
|
||||
return 'Hide Searchbar';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'gr-main-header': GrMainHeader;
|
||||
}
|
||||
}
|
||||
@@ -103,22 +103,22 @@ suite('gr-main-header tests', () => {
|
||||
|
||||
// When no admin links are passed, it should use the default.
|
||||
assert.deepEqual(element._computeLinks(
|
||||
defaultLinks,
|
||||
/* userLinks= */[],
|
||||
adminLinks,
|
||||
/* topMenus= */[],
|
||||
/* docBaseUrl= */ ''
|
||||
/* docBaseUrl= */ '',
|
||||
defaultLinks
|
||||
),
|
||||
defaultLinks.concat({
|
||||
title: 'Browse',
|
||||
links: adminLinks,
|
||||
}));
|
||||
assert.deepEqual(element._computeLinks(
|
||||
defaultLinks,
|
||||
userLinks,
|
||||
adminLinks,
|
||||
/* topMenus= */[],
|
||||
/* docBaseUrl= */ ''
|
||||
/* docBaseUrl= */ '',
|
||||
defaultLinks
|
||||
),
|
||||
defaultLinks.concat([
|
||||
{
|
||||
@@ -142,7 +142,6 @@ suite('gr-main-header tests', () => {
|
||||
|
||||
assert.deepEqual(element._getDocLinks(null, docLinks), []);
|
||||
assert.deepEqual(element._getDocLinks('', docLinks), []);
|
||||
assert.deepEqual(element._getDocLinks('base', null), []);
|
||||
assert.deepEqual(element._getDocLinks('base', []), []);
|
||||
|
||||
assert.deepEqual(element._getDocLinks('base', docLinks), [{
|
||||
@@ -172,11 +171,11 @@ suite('gr-main-header tests', () => {
|
||||
}],
|
||||
}];
|
||||
assert.deepEqual(element._computeLinks(
|
||||
/* defaultLinks= */ [],
|
||||
/* userLinks= */ [],
|
||||
adminLinks,
|
||||
topMenus,
|
||||
/* baseDocUrl= */ ''
|
||||
/* baseDocUrl= */ '',
|
||||
/* defaultLinks= */ []
|
||||
), [{
|
||||
title: 'Browse',
|
||||
links: adminLinks,
|
||||
@@ -208,11 +207,11 @@ suite('gr-main-header tests', () => {
|
||||
}],
|
||||
}];
|
||||
assert.deepEqual(element._computeLinks(
|
||||
/* defaultLinks= */ [],
|
||||
/* userLinks= */ [],
|
||||
adminLinks,
|
||||
topMenus,
|
||||
/* baseDocUrl= */ ''
|
||||
/* baseDocUrl= */ '',
|
||||
/* defaultLinks= */ []
|
||||
), [{
|
||||
title: 'Browse',
|
||||
links: adminLinks,
|
||||
@@ -247,11 +246,11 @@ suite('gr-main-header tests', () => {
|
||||
}],
|
||||
}];
|
||||
assert.deepEqual(element._computeLinks(
|
||||
/* defaultLinks= */ [],
|
||||
/* userLinks= */ [],
|
||||
adminLinks,
|
||||
topMenus,
|
||||
/* baseDocUrl= */ ''
|
||||
/* baseDocUrl= */ '',
|
||||
/* defaultLinks= */ []
|
||||
), [{
|
||||
title: 'Browse',
|
||||
links: adminLinks,
|
||||
@@ -284,11 +283,11 @@ suite('gr-main-header tests', () => {
|
||||
}],
|
||||
}];
|
||||
assert.deepEqual(element._computeLinks(
|
||||
defaultLinks,
|
||||
/* userLinks= */ [],
|
||||
/* adminLinks= */ [],
|
||||
topMenus,
|
||||
/* baseDocUrl= */ ''
|
||||
/* baseDocUrl= */ '',
|
||||
defaultLinks
|
||||
), [{
|
||||
title: 'Faves',
|
||||
links: defaultLinks[0].links.concat([{
|
||||
@@ -315,11 +314,11 @@ suite('gr-main-header tests', () => {
|
||||
}],
|
||||
}];
|
||||
assert.deepEqual(element._computeLinks(
|
||||
/* defaultLinks= */ [],
|
||||
userLinks,
|
||||
/* adminLinks= */ [],
|
||||
topMenus,
|
||||
/* baseDocUrl= */ ''
|
||||
/* baseDocUrl= */ '',
|
||||
/* defaultLinks= */ []
|
||||
), [{
|
||||
title: 'Your',
|
||||
links: userLinks.concat([{
|
||||
@@ -346,11 +345,11 @@ suite('gr-main-header tests', () => {
|
||||
}],
|
||||
}];
|
||||
assert.deepEqual(element._computeLinks(
|
||||
/* defaultLinks= */ [],
|
||||
/* userLinks= */ [],
|
||||
adminLinks,
|
||||
topMenus,
|
||||
/* baseDocUrl= */ ''
|
||||
/* baseDocUrl= */ '',
|
||||
/* defaultLinks= */ []
|
||||
), [{
|
||||
title: 'Browse',
|
||||
links: adminLinks.concat([{
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
ParentPatchSetNum,
|
||||
ServerInfo,
|
||||
} from '../../../types/common';
|
||||
import {ParsedChangeInfo} from '../../shared/gr-rest-api-interface/gr-reviewer-updates-parser';
|
||||
|
||||
// Navigation parameters object format:
|
||||
//
|
||||
@@ -664,7 +665,7 @@ export const GerritNav = {
|
||||
* @param basePatchNum The string 'PARENT' can be used for none.
|
||||
*/
|
||||
getUrlForDiff(
|
||||
change: ChangeInfo,
|
||||
change: ChangeInfo | ParsedChangeInfo,
|
||||
filePath: string,
|
||||
patchNum?: PatchSetNum,
|
||||
basePatchNum?: PatchSetNum,
|
||||
@@ -723,7 +724,7 @@ export const GerritNav = {
|
||||
},
|
||||
|
||||
getEditUrlForDiff(
|
||||
change: ChangeInfo,
|
||||
change: ChangeInfo | ParsedChangeInfo,
|
||||
filePath: string,
|
||||
patchNum?: PatchSetNum,
|
||||
lineNum?: number
|
||||
@@ -763,7 +764,7 @@ export const GerritNav = {
|
||||
* @param basePatchNum The string 'PARENT' can be used for none.
|
||||
*/
|
||||
navigateToDiff(
|
||||
change: ChangeInfo,
|
||||
change: ChangeInfo | ParsedChangeInfo,
|
||||
filePath: string,
|
||||
patchNum?: PatchSetNum,
|
||||
basePatchNum?: PatchSetNum,
|
||||
|
||||
@@ -124,11 +124,15 @@ const MAX_AUTOCOMPLETE_RESULTS = 10;
|
||||
|
||||
const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
|
||||
|
||||
type SuggestionProvider = (
|
||||
export type SuggestionProvider = (
|
||||
predicate: string,
|
||||
expression: string
|
||||
) => Promise<AutocompleteSuggestion[]>;
|
||||
|
||||
export interface SearchBarHandleSearchDetail {
|
||||
inputVal: string;
|
||||
}
|
||||
|
||||
export interface GrSearchBar {
|
||||
$: {
|
||||
restAPI: RestApiService & Element;
|
||||
@@ -254,7 +258,8 @@ export class GrSearchBar extends KeyboardShortcutMixin(
|
||||
} else {
|
||||
target.blur();
|
||||
}
|
||||
const trimmedInput = this._inputVal && this._inputVal.trim();
|
||||
if (!this._inputVal) return;
|
||||
const trimmedInput = this._inputVal.trim();
|
||||
if (trimmedInput) {
|
||||
const predefinedOpOnlyQuery = [
|
||||
...SEARCH_OPERATORS_WITH_NEGATIONS_SET,
|
||||
@@ -262,9 +267,12 @@ export class GrSearchBar extends KeyboardShortcutMixin(
|
||||
if (predefinedOpOnlyQuery) {
|
||||
return;
|
||||
}
|
||||
const detail: SearchBarHandleSearchDetail = {
|
||||
inputVal: this._inputVal,
|
||||
};
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('handle-search', {
|
||||
detail: {inputVal: this._inputVal},
|
||||
detail,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2016 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 '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
|
||||
import '../gr-search-bar/gr-search-bar.js';
|
||||
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
|
||||
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
|
||||
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
|
||||
import {htmlTemplate} from './gr-smart-search_html.js';
|
||||
import {GerritNav} from '../gr-navigation/gr-navigation.js';
|
||||
import {getUserName} from '../../../utils/display-name-util.js';
|
||||
|
||||
const MAX_AUTOCOMPLETE_RESULTS = 10;
|
||||
const SELF_EXPRESSION = 'self';
|
||||
const ME_EXPRESSION = 'me';
|
||||
|
||||
/**
|
||||
* @extends PolymerElement
|
||||
*/
|
||||
class GrSmartSearch extends GestureEventListeners(
|
||||
LegacyElementMixin(
|
||||
PolymerElement)) {
|
||||
static get template() { return htmlTemplate; }
|
||||
|
||||
static get is() { return 'gr-smart-search'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
searchQuery: String,
|
||||
_config: Object,
|
||||
_projectSuggestions: {
|
||||
type: Function,
|
||||
value() {
|
||||
return (predicate, expression) =>
|
||||
this._fetchProjects(predicate, expression);
|
||||
},
|
||||
},
|
||||
_groupSuggestions: {
|
||||
type: Function,
|
||||
value() {
|
||||
return (predicate, expression) =>
|
||||
this._fetchGroups(predicate, expression);
|
||||
},
|
||||
},
|
||||
_accountSuggestions: {
|
||||
type: Function,
|
||||
value() {
|
||||
return (predicate, expression) =>
|
||||
this._fetchAccounts(predicate, expression);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Invisible label for input element. This label is exposed to
|
||||
* screen readers by nested element
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @override */
|
||||
attached() {
|
||||
super.attached();
|
||||
this.$.restAPI.getConfig().then(cfg => {
|
||||
this._config = cfg;
|
||||
});
|
||||
}
|
||||
|
||||
_handleSearch(e) {
|
||||
const input = e.detail.inputVal;
|
||||
if (input) {
|
||||
GerritNav.navigateToSearchQuery(input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch from the API the predicted projects.
|
||||
*
|
||||
* @param {string} predicate - The first part of the search term, e.g.
|
||||
* 'project'
|
||||
* @param {string} expression - The second part of the search term, e.g.
|
||||
* 'gerr'
|
||||
* @return {!Promise} This returns a promise that resolves to an array of
|
||||
* strings.
|
||||
*/
|
||||
_fetchProjects(predicate, expression) {
|
||||
return this.$.restAPI.getSuggestedProjects(
|
||||
expression,
|
||||
MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(projects => {
|
||||
if (!projects) { return []; }
|
||||
const keys = Object.keys(projects);
|
||||
return keys.map(key => { return {text: predicate + ':' + key}; });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch from the API the predicted groups.
|
||||
*
|
||||
* @param {string} predicate - The first part of the search term, e.g.
|
||||
* 'ownerin'
|
||||
* @param {string} expression - The second part of the search term, e.g.
|
||||
* 'polyger'
|
||||
* @return {!Promise} This returns a promise that resolves to an array of
|
||||
* strings.
|
||||
*/
|
||||
_fetchGroups(predicate, expression) {
|
||||
if (expression.length === 0) { return Promise.resolve([]); }
|
||||
return this.$.restAPI.getSuggestedGroups(
|
||||
expression,
|
||||
MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(groups => {
|
||||
if (!groups) { return []; }
|
||||
const keys = Object.keys(groups);
|
||||
return keys.map(key => { return {text: predicate + ':' + key}; });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch from the API the predicted accounts.
|
||||
*
|
||||
* @param {string} predicate - The first part of the search term, e.g.
|
||||
* 'owner'
|
||||
* @param {string} expression - The second part of the search term, e.g.
|
||||
* 'kasp'
|
||||
* @return {!Promise} This returns a promise that resolves to an array of
|
||||
* strings.
|
||||
*/
|
||||
_fetchAccounts(predicate, expression) {
|
||||
if (expression.length === 0) { return Promise.resolve([]); }
|
||||
return this.$.restAPI.getSuggestedAccounts(
|
||||
expression,
|
||||
MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(accounts => {
|
||||
if (!accounts) { return []; }
|
||||
return this._mapAccountsHelper(accounts, predicate);
|
||||
})
|
||||
.then(accounts => {
|
||||
// When the expression supplied is a beginning substring of 'self',
|
||||
// add it as an autocomplete option.
|
||||
if (SELF_EXPRESSION.startsWith(expression)) {
|
||||
return accounts.concat(
|
||||
[{text: predicate + ':' + SELF_EXPRESSION}]);
|
||||
} else if (ME_EXPRESSION.startsWith(expression)) {
|
||||
return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
|
||||
} else {
|
||||
return accounts;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_mapAccountsHelper(accounts, predicate) {
|
||||
return accounts.map(account => {
|
||||
const userName = getUserName(this._serverConfig, account);
|
||||
return {
|
||||
label: account.name || '',
|
||||
text: account.email ?
|
||||
`${predicate}:${account.email}` :
|
||||
`${predicate}:"${userName}"`,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(GrSmartSearch.is, GrSmartSearch);
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2016 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 '../../shared/gr-rest-api-interface/gr-rest-api-interface';
|
||||
import '../gr-search-bar/gr-search-bar';
|
||||
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-smart-search_html';
|
||||
import {GerritNav} from '../gr-navigation/gr-navigation';
|
||||
import {getUserName} from '../../../utils/display-name-util';
|
||||
import {customElement, property} from '@polymer/decorators';
|
||||
import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
|
||||
import {AccountInfo, ServerInfo} from '../../../types/common';
|
||||
import {
|
||||
SearchBarHandleSearchDetail,
|
||||
SuggestionProvider,
|
||||
} from '../gr-search-bar/gr-search-bar';
|
||||
import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
|
||||
|
||||
const MAX_AUTOCOMPLETE_RESULTS = 10;
|
||||
const SELF_EXPRESSION = 'self';
|
||||
const ME_EXPRESSION = 'me';
|
||||
|
||||
export interface GrSmartSearch {
|
||||
$: {
|
||||
restAPI: RestApiService & Element;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement('gr-smart-search')
|
||||
export class GrSmartSearch extends GestureEventListeners(
|
||||
LegacyElementMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return htmlTemplate;
|
||||
}
|
||||
|
||||
@property({type: String})
|
||||
searchQuery?: string;
|
||||
|
||||
@property({type: Object})
|
||||
_config?: ServerInfo;
|
||||
|
||||
@property({type: Object})
|
||||
_projectSuggestions: SuggestionProvider = (predicate, expression) =>
|
||||
this._fetchProjects(predicate, expression);
|
||||
|
||||
@property({type: Object})
|
||||
_groupSuggestions: SuggestionProvider = (predicate, expression) =>
|
||||
this._fetchGroups(predicate, expression);
|
||||
|
||||
@property({type: Object})
|
||||
_accountSuggestions: SuggestionProvider = (predicate, expression) =>
|
||||
this._fetchAccounts(predicate, expression);
|
||||
|
||||
@property({type: String})
|
||||
label = '';
|
||||
|
||||
/** @override */
|
||||
attached() {
|
||||
super.attached();
|
||||
this.$.restAPI.getConfig().then(cfg => {
|
||||
this._config = cfg;
|
||||
});
|
||||
}
|
||||
|
||||
_handleSearch(e: CustomEvent<SearchBarHandleSearchDetail>) {
|
||||
const input = e.detail.inputVal;
|
||||
if (input) {
|
||||
GerritNav.navigateToSearchQuery(input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch from the API the predicted projects.
|
||||
*
|
||||
* @param predicate - The first part of the search term, e.g.
|
||||
* 'project'
|
||||
* @param expression - The second part of the search term, e.g.
|
||||
* 'gerr'
|
||||
*/
|
||||
_fetchProjects(
|
||||
predicate: string,
|
||||
expression: string
|
||||
): Promise<AutocompleteSuggestion[]> {
|
||||
return this.$.restAPI
|
||||
.getSuggestedProjects(expression, MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(projects => {
|
||||
if (!projects) {
|
||||
return [];
|
||||
}
|
||||
const keys = Object.keys(projects);
|
||||
return keys.map(key => {
|
||||
return {text: predicate + ':' + key};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch from the API the predicted groups.
|
||||
*
|
||||
* @param predicate - The first part of the search term, e.g.
|
||||
* 'ownerin'
|
||||
* @param expression - The second part of the search term, e.g.
|
||||
* 'polyger'
|
||||
*/
|
||||
_fetchGroups(
|
||||
predicate: string,
|
||||
expression: string
|
||||
): Promise<AutocompleteSuggestion[]> {
|
||||
if (expression.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.$.restAPI
|
||||
.getSuggestedGroups(expression, MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(groups => {
|
||||
if (!groups) {
|
||||
return [];
|
||||
}
|
||||
const keys = Object.keys(groups);
|
||||
return keys.map(key => {
|
||||
return {text: predicate + ':' + key};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch from the API the predicted accounts.
|
||||
*
|
||||
* @param predicate - The first part of the search term, e.g.
|
||||
* 'owner'
|
||||
* @param expression - The second part of the search term, e.g.
|
||||
* 'kasp'
|
||||
*/
|
||||
_fetchAccounts(
|
||||
predicate: string,
|
||||
expression: string
|
||||
): Promise<AutocompleteSuggestion[]> {
|
||||
if (expression.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.$.restAPI
|
||||
.getSuggestedAccounts(expression, MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(accounts => {
|
||||
if (!accounts) {
|
||||
return [];
|
||||
}
|
||||
return this._mapAccountsHelper(accounts, predicate);
|
||||
})
|
||||
.then(accounts => {
|
||||
// When the expression supplied is a beginning substring of 'self',
|
||||
// add it as an autocomplete option.
|
||||
if (SELF_EXPRESSION.startsWith(expression)) {
|
||||
return accounts.concat([{text: predicate + ':' + SELF_EXPRESSION}]);
|
||||
} else if (ME_EXPRESSION.startsWith(expression)) {
|
||||
return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
|
||||
} else {
|
||||
return accounts;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_mapAccountsHelper(
|
||||
accounts: AccountInfo[],
|
||||
predicate: string
|
||||
): AutocompleteSuggestion[] {
|
||||
return accounts.map(account => {
|
||||
const userName = getUserName(this._config, account);
|
||||
return {
|
||||
label: account.name || '',
|
||||
text: account.email
|
||||
? `${predicate}:${account.email}`
|
||||
: `${predicate}:"${userName}"`,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'gr-smart-search': GrSmartSearch;
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ export class GrCommentThread extends KeyboardShortcutMixin(
|
||||
notify: true,
|
||||
computed: '_computeRootId(comments.*)',
|
||||
})
|
||||
rootId?: string;
|
||||
rootId?: UrlEncodedCommentId;
|
||||
|
||||
@property({type: Boolean})
|
||||
showFilePath = false;
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
RevisionInfo,
|
||||
} from '../../../types/common';
|
||||
import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api';
|
||||
import {GrAdminApi} from '../../plugins/gr-admin-api/gr-admin-api';
|
||||
import {GrAdminApi, MenuLink} from '../../plugins/gr-admin-api/gr-admin-api';
|
||||
import {
|
||||
JsApiService,
|
||||
EventCallback,
|
||||
@@ -294,8 +294,8 @@ export class GrJsApiInterface
|
||||
);
|
||||
}
|
||||
|
||||
getAdminMenuLinks() {
|
||||
const links = [];
|
||||
getAdminMenuLinks(): MenuLink[] {
|
||||
const links: MenuLink[] = [];
|
||||
for (const cb of this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) {
|
||||
const adminApi = (cb as unknown) as GrAdminApi;
|
||||
links.push(...adminApi.getMenuLinks());
|
||||
|
||||
@@ -18,6 +18,7 @@ import {ActionInfo, ChangeInfo, PatchSetNum} from '../../../types/common';
|
||||
import {EventType, TargetElement} from '../../plugins/gr-plugin-types';
|
||||
import {DiffLayer} from '../../../types/types';
|
||||
import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api';
|
||||
import {MenuLink} from '../../plugins/gr-admin-api/gr-admin-api';
|
||||
|
||||
export interface ShowChangeDetail {
|
||||
change: ChangeInfo;
|
||||
@@ -50,5 +51,6 @@ export interface JsApiService {
|
||||
getDiffLayers(path: string, changeNum: number): DiffLayer[];
|
||||
disposeDiffLayers(path: string): void;
|
||||
getCoverageAnnotationApi(): Promise<GrAnnotationActionsInterface | undefined>;
|
||||
getAdminMenuLinks(): MenuLink[];
|
||||
// TODO(TS): Add more methods when needed for the TS conversion.
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ import {
|
||||
RevisionId,
|
||||
GroupName,
|
||||
Hashtag,
|
||||
TopMenuEntryInfo,
|
||||
} from '../../../types/common';
|
||||
import {
|
||||
CancelConditionCallback,
|
||||
@@ -1603,7 +1604,10 @@ export class GrRestApiInterface
|
||||
}) as Promise<string[] | undefined>;
|
||||
}
|
||||
|
||||
getChangeOrEditFiles(changeNum: NumericChangeId, patchRange: PatchRange) {
|
||||
getChangeOrEditFiles(
|
||||
changeNum: NumericChangeId,
|
||||
patchRange: PatchRange
|
||||
): Promise<FileNameToFileInfoMap | undefined> {
|
||||
if (patchNumEquals(patchRange.patchNum, EditPatchSetNum)) {
|
||||
return this.getChangeEditFiles(changeNum, patchRange).then(
|
||||
res => res && res.files
|
||||
@@ -2086,13 +2090,16 @@ export class GrRestApiInterface
|
||||
}) as Promise<ChangeInfo[] | undefined>;
|
||||
}
|
||||
|
||||
getReviewedFiles(changeNum: NumericChangeId, patchNum: PatchSetNum) {
|
||||
getReviewedFiles(
|
||||
changeNum: NumericChangeId,
|
||||
patchNum: PatchSetNum
|
||||
): Promise<string[] | undefined> {
|
||||
return this._getChangeURLAndFetch({
|
||||
changeNum,
|
||||
endpoint: '/files?reviewed',
|
||||
patchNum,
|
||||
reportEndpointAsIs: true,
|
||||
});
|
||||
}) as Promise<string[] | undefined>;
|
||||
}
|
||||
|
||||
saveFileReviewed(
|
||||
@@ -3215,12 +3222,12 @@ export class GrRestApiInterface
|
||||
}) as Promise<CapabilityInfoMap | undefined>;
|
||||
}
|
||||
|
||||
getTopMenus(errFn?: ErrorCallback) {
|
||||
getTopMenus(errFn?: ErrorCallback): Promise<TopMenuEntryInfo[] | undefined> {
|
||||
return this._fetchSharedCacheURL({
|
||||
url: '/config/server/top-menus',
|
||||
errFn,
|
||||
reportUrlAsIs: true,
|
||||
});
|
||||
}) as Promise<TopMenuEntryInfo[] | undefined>;
|
||||
}
|
||||
|
||||
setAssignee(
|
||||
|
||||
@@ -1082,7 +1082,7 @@ export interface KeyboardShortcutMixinInterface {
|
||||
_shortcut_v_key_last_pressed: number | null;
|
||||
_shortcut_go_table: Map<string, string>;
|
||||
_shortcut_v_table: Map<string, string>;
|
||||
keyboardShortcuts(): {[key: string]: string};
|
||||
keyboardShortcuts(): {[key: string]: string | null};
|
||||
createTitle(name: Shortcut, section: ShortcutSection): string;
|
||||
bindShortcut(shortcut: Shortcut, ...bindings: string[]): void;
|
||||
shouldSuppressKeyboardShortcut(event: CustomKeyboardEvent): boolean;
|
||||
|
||||
@@ -96,6 +96,8 @@ import {
|
||||
DashboardId,
|
||||
HashtagsInput,
|
||||
Hashtag,
|
||||
FileNameToFileInfoMap,
|
||||
TopMenuEntryInfo,
|
||||
} from '../../../types/common';
|
||||
import {ParsedChangeInfo} from '../../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
|
||||
import {HttpMethod, IgnoreWhitespaceType} from '../../../constants/constants';
|
||||
@@ -815,4 +817,31 @@ export interface RestApiService {
|
||||
changeNum: NumericChangeId,
|
||||
topic: string | null
|
||||
): Promise<string>;
|
||||
|
||||
getChangeOrEditFiles(
|
||||
changeNum: NumericChangeId,
|
||||
patchRange: PatchRange
|
||||
): Promise<FileNameToFileInfoMap | undefined>;
|
||||
|
||||
getReviewedFiles(
|
||||
changeNum: NumericChangeId,
|
||||
patchNum: PatchSetNum
|
||||
): Promise<string[] | undefined>;
|
||||
|
||||
saveFileReviewed(
|
||||
changeNum: NumericChangeId,
|
||||
patchNum: PatchSetNum,
|
||||
path: string,
|
||||
reviewed: boolean
|
||||
): Promise<Response>;
|
||||
|
||||
saveFileReviewed(
|
||||
changeNum: NumericChangeId,
|
||||
patchNum: PatchSetNum,
|
||||
path: string,
|
||||
reviewed: boolean,
|
||||
errFn: ErrorCallback
|
||||
): Promise<Response | undefined>;
|
||||
|
||||
getTopMenus(errFn?: ErrorCallback): Promise<TopMenuEntryInfo[] | undefined>;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
DraftsAction,
|
||||
NotifyType,
|
||||
EmailFormat,
|
||||
AuthType,
|
||||
} from '../constants/constants';
|
||||
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
|
||||
|
||||
@@ -743,10 +744,10 @@ export interface AccountsConfigInfo {
|
||||
/**
|
||||
* The AuthInfo entity contains information about the authentication
|
||||
* configuration of the Gerrit server.
|
||||
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html
|
||||
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#auth-info
|
||||
*/
|
||||
export interface AuthInfo {
|
||||
type: string;
|
||||
auth_type: AuthType; // docs incorrectly names it 'type'
|
||||
use_contributor_agreements: boolean;
|
||||
contributor_agreements?: ContributorAgreementInfo;
|
||||
editable_account_fields: string;
|
||||
@@ -1124,17 +1125,17 @@ export interface ThreadSummaryInfo {
|
||||
|
||||
/**
|
||||
* The TopMenuEntryInfo entity contains information about a top menu entry.
|
||||
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html
|
||||
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#top-menu-entry-info
|
||||
*/
|
||||
export interface TopMenuEntryInfo {
|
||||
name: string;
|
||||
items: string;
|
||||
items: TopMenuItemInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The TopMenuItemInfo entity contains information about a menu item ina top
|
||||
* menu entry.
|
||||
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html
|
||||
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#top-menu-item-info
|
||||
*/
|
||||
export interface TopMenuItemInfo {
|
||||
url: string;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
*/
|
||||
export function asyncForeach<T>(
|
||||
array: T[],
|
||||
fn: (item: T, stopCallback: () => void) => Promise<T>
|
||||
fn: (item: T, stopCallback: () => void) => Promise<unknown>
|
||||
): Promise<T | void> {
|
||||
if (!array.length) {
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -211,13 +211,13 @@ export function getEventPath<T extends PolymerEvent>(e?: T) {
|
||||
export function descendedFromClass(
|
||||
element: Element,
|
||||
className: string,
|
||||
opt_stopElement: Element
|
||||
stopElement?: Element
|
||||
) {
|
||||
let isDescendant = element.classList.contains(className);
|
||||
while (
|
||||
!isDescendant &&
|
||||
element.parentElement &&
|
||||
(!opt_stopElement || element.parentElement !== opt_stopElement)
|
||||
(!stopElement || element.parentElement !== stopElement)
|
||||
) {
|
||||
isDescendant = element.classList.contains(className);
|
||||
element = element.parentElement;
|
||||
|
||||
Reference in New Issue
Block a user