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