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:
Dmitrii Filippov
2020-10-14 17:09:16 +00:00
committed by Gerrit Code Review
23 changed files with 2621 additions and 2207 deletions

View File

@@ -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": {

View File

@@ -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',
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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]]"

View File

@@ -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},

View File

@@ -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);

View 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;
}
}

View File

@@ -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([{

View File

@@ -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,

View File

@@ -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,
})
);
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -143,7 +143,7 @@ export class GrCommentThread extends KeyboardShortcutMixin(
notify: true,
computed: '_computeRootId(comments.*)',
})
rootId?: string;
rootId?: UrlEncodedCommentId;
@property({type: Boolean})
showFilePath = false;

View File

@@ -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());

View File

@@ -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.
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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>;
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;