Merge "Replace "Since" by "Waiting""
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index c7c6c63..21bcc77 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -625,7 +625,7 @@
private void processCommandsUnsafe(
Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
logger.atFine().log("Calling user: %s", user.getLoggableName());
- logger.atFine().log("Groups: %s", user.getEffectiveGroups().getKnownGroups());
+ logger.atFine().log("Groups: %s", lazy(() -> user.getEffectiveGroups().getKnownGroups()));
if (!projectState.getProject().getState().permitsWrite()) {
for (ReceiveCommand cmd : commands) {
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 37de0d1..defec4b 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.flogger.LazyArgs.lazy;
import static com.google.gerrit.entities.RefNames.REFS_CACHE_AUTOMERGE;
import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
import static com.google.gerrit.entities.RefNames.REFS_USERS_SELF;
@@ -134,7 +135,7 @@
"Filter refs for repository %s by visibility (options = %s, refs = %s)",
projectState.getNameKey(), opts, refs);
logger.atFinest().log("Calling user: %s", user.getLoggableName());
- logger.atFinest().log("Groups: %s", user.getEffectiveGroups().getKnownGroups());
+ logger.atFinest().log("Groups: %s", lazy(() -> user.getEffectiveGroups().getKnownGroups()));
logger.atFinest().log(
"auth.skipFullRefEvaluationIfAllRefsAreVisible = %s",
skipFullRefEvaluationIfAllRefsAreVisible);
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 226cc4c..399ae6e 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -277,6 +277,12 @@
"@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": {
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 5a925ff..8e8eaf3 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -341,3 +341,20 @@
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',
+}
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index e5b3daf..20fe1a6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -37,7 +37,6 @@
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 @@
} 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 @@
/^(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;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
deleted file mode 100644
index bf45eb6..0000000
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ /dev/null
@@ -1,1614 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../../styles/shared-styles.js';
-import '../../diff/gr-diff-cursor/gr-diff-cursor.js';
-import '../../diff/gr-diff-host/gr-diff-host.js';
-import '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js';
-import '../../edit/gr-edit-file-controls/gr-edit-file-controls.js';
-import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
-import '../../shared/gr-icons/gr-icons.js';
-import '../../shared/gr-linked-text/gr-linked-text.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../shared/gr-select/gr-select.js';
-import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
-import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.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-file-list_html.js';
-import {asyncForeach} from '../../../utils/async-util.js';
-import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
-import {FilesExpandedState} from '../gr-file-list-constants.js';
-import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {appContext} from '../../../services/app-context.js';
-import {SpecialFilePath} from '../../../constants/constants.js';
-import {descendedFromClass} from '../../../utils/dom-util.js';
-import {
- addUnmodifiedFiles,
- computeDisplayPath,
- computeTruncatedPath,
- isMagicPath,
- specialFilePathCompare,
-} from '../../../utils/path-list-util.js';
-
-const WARN_SHOW_ALL_THRESHOLD = 1000;
-const LOADING_DEBOUNCE_INTERVAL = 100;
-
-const SIZE_BAR_MAX_WIDTH = 61;
-const SIZE_BAR_GAP_WIDTH = 1;
-const SIZE_BAR_MIN_WIDTH = 1.5;
-
-const RENDER_TIMING_LABEL = 'FileListRenderTime';
-const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile';
-const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs';
-const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff';
-
-const FileStatus = {
- A: 'Added',
- C: 'Copied',
- D: 'Deleted',
- M: 'Modified',
- R: 'Renamed',
- W: 'Rewritten',
- U: 'Unchanged',
-};
-
-const FILE_ROW_CLASS = 'file-row';
-
-/**
- * Type for FileInfo
- *
- * This should match with the type returned from `files` API plus
- * additional info like `__path`.
- *
- * @typedef {Object} FileInfo
- * @property {string} __path
- * @property {?string} old_path
- * @property {number} size
- * @property {number} size_delta - fallback to 0 if not present in api
- * @property {number} lines_deleted - fallback to 0 if not present in api
- * @property {number} lines_inserted - fallback to 0 if not present in api
- */
-
-/**
- * @extends PolymerElement
- */
-class GrFileList extends KeyboardShortcutMixin(
- GestureEventListeners(
- LegacyElementMixin(PolymerElement))) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-file-list'; }
- /**
- * Fired when a draft refresh should get triggered
- *
- * @event reload-drafts
- */
-
- static get properties() {
- return {
- /** @type {?} */
- patchRange: Object,
- patchNum: String,
- changeNum: String,
- /** @type {?} */
- changeComments: Object,
- drafts: Object,
- revisions: Array,
- projectConfig: Object,
- selectedIndex: {
- type: Number,
- notify: true,
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- /** @type {?} */
- change: Object,
- diffViewMode: {
- type: String,
- notify: true,
- observer: '_updateDiffPreferences',
- },
- editMode: {
- type: Boolean,
- observer: '_editModeChanged',
- },
- filesExpanded: {
- type: String,
- value: FilesExpandedState.NONE,
- notify: true,
- },
- _filesByPath: Object,
-
- /** @type {!Array<FileInfo>} */
- _files: {
- type: Array,
- observer: '_filesChanged',
- value() { return []; },
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _reviewed: {
- type: Array,
- value() { return []; },
- },
- diffPrefs: {
- type: Object,
- notify: true,
- observer: '_updateDiffPreferences',
- },
- /** @type {?} */
- _userPrefs: Object,
- _showInlineDiffs: Boolean,
- numFilesShown: {
- type: Number,
- notify: true,
- },
- /** @type {?} */
- _patchChange: {
- type: Object,
- computed: '_calculatePatchChange(_files)',
- },
- fileListIncrement: Number,
- _hideChangeTotals: {
- type: Boolean,
- computed: '_shouldHideChangeTotals(_patchChange)',
- },
- _hideBinaryChangeTotals: {
- type: Boolean,
- computed: '_shouldHideBinaryChangeTotals(_patchChange)',
- },
-
- _shownFiles: {
- type: Array,
- computed: '_computeFilesShown(numFilesShown, _files)',
- },
-
- /**
- * The amount of files added to the shown files list the last time it was
- * updated. This is used for reporting the average render time.
- */
- _reportinShownFilesIncrement: Number,
-
- /** @type {!Array<Gerrit.FileRange>} */
- _expandedFiles: {
- type: Array,
- value() { return []; },
- },
- _displayLine: Boolean,
- _loading: {
- type: Boolean,
- observer: '_loadingChanged',
- },
- /** @type {Gerrit.LayoutStats|undefined} */
- _sizeBarLayout: {
- type: Object,
- computed: '_computeSizeBarLayout(_shownFiles.*)',
- },
-
- _showSizeBars: {
- type: Boolean,
- value: true,
- computed: '_computeShowSizeBars(_userPrefs)',
- },
-
- /** @type {Function} */
- _cancelForEachDiff: Function,
-
- _showDynamicColumns: {
- type: Boolean,
- computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
- '_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
- },
- _showPrependedDynamicColumns: {
- type: Boolean,
- computed: '_computeShowPrependedDynamicColumns(' +
- '_dynamicPrependedHeaderEndpoints, _dynamicPrependedContentEndpoints)',
- },
- /** @type {Array<string>} */
- _dynamicHeaderEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicContentEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicSummaryEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicPrependedHeaderEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicPrependedContentEndpoints: {
- type: Array,
- },
- };
- }
-
- static get observers() {
- return [
- '_expandedFilesChanged(_expandedFiles.splices)',
- '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
- '_loading)',
- ];
- }
-
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- };
- }
-
- keyboardShortcuts() {
- return {
- [Shortcut.LEFT_PANE]: '_handleLeftPane',
- [Shortcut.RIGHT_PANE]: '_handleRightPane',
- [Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
- [Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
- [Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
- '_handleToggleHideAllCommentThreads',
- [Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
- [Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
- [Shortcut.NEXT_LINE]: '_handleCursorNext',
- [Shortcut.PREV_LINE]: '_handleCursorPrev',
- [Shortcut.NEW_COMMENT]: '_handleNewComment',
- [Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
- [Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
- [Shortcut.OPEN_FILE]: '_handleOpenFile',
- [Shortcut.NEXT_CHUNK]: '_handleNextChunk',
- [Shortcut.PREV_CHUNK]: '_handlePrevChunk',
- [Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
- [Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
-
- // Final two are actually handled by gr-comment-thread.
- [Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
- [Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
- };
- }
-
- constructor() {
- super();
- this.reporting = appContext.reportingService;
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('keydown',
- e => this._scopedKeydownHandler(e));
- }
-
- /** @override */
- attached() {
- super.attached();
- getPluginLoader().awaitPluginsLoaded()
- .then(() => {
- this._dynamicHeaderEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-header');
- this._dynamicContentEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-content');
- this._dynamicPrependedHeaderEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-header-prepend');
- this._dynamicPrependedContentEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-content-prepend');
- this._dynamicSummaryEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-summary');
-
- if (this._dynamicHeaderEndpoints.length !==
- this._dynamicContentEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list header and content.');
- }
- if (this._dynamicPrependedHeaderEndpoints.length !==
- this._dynamicPrependedContentEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list header and content.');
- }
- if (this._dynamicHeaderEndpoints.length !==
- this._dynamicSummaryEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list headers and summary.');
- }
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this._cancelDiffs();
- }
-
- /**
- * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
- * events must be scoped to a component level (e.g. `enter`) in order to not
- * override native browser functionality.
- *
- * Context: Issue 7277
- */
- _scopedKeydownHandler(e) {
- if (e.keyCode === 13) {
- // Enter.
- this._handleOpenFile(e);
- }
- }
-
- reload() {
- if (!this.changeNum || !this.patchRange.patchNum) {
- return Promise.resolve();
- }
-
- this._loading = true;
-
- this.collapseAllDiffs();
- const promises = [];
-
- promises.push(this._getFiles().then(filesByPath => {
- this._filesByPath = filesByPath;
- }));
- promises.push(this._getLoggedIn()
- .then(loggedIn => this._loggedIn = loggedIn)
- .then(loggedIn => {
- if (!loggedIn) { return; }
-
- return this._getReviewedFiles().then(reviewed => {
- this._reviewed = reviewed;
- });
- }));
-
- promises.push(this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- }));
-
- promises.push(this._getPreferences().then(prefs => {
- this._userPrefs = prefs;
- }));
-
- return Promise.all(promises).then(() => {
- this._loading = false;
- this._detectChromiteButler();
- this.reporting.fileListDisplayed();
- });
- }
-
- _detectChromiteButler() {
- const hasButler = !!document.getElementById('butler-suggested-owners');
- if (hasButler) {
- this.reporting.reportExtension('butler');
- }
- }
-
- get diffs() {
- const diffs = this.root.querySelectorAll('gr-diff-host');
- // It is possible that a bogus diff element is hanging around invisibly
- // from earlier with a different patch set choice and associated with a
- // different entry in the files array. So filter on visible items only.
- return Array.from(diffs).filter(
- el => !!el && !!el.style && el.style.display !== 'none');
- }
-
- openDiffPrefs() {
- this.$.diffPreferencesDialog.open();
- }
-
- _calculatePatchChange(files) {
- const magicFilesExcluded = files.filter(files =>
- !isMagicPath(files.__path)
- );
-
- return magicFilesExcluded.reduce((acc, obj) => {
- const inserted = obj.lines_inserted ? obj.lines_inserted : 0;
- const deleted = obj.lines_deleted ? obj.lines_deleted : 0;
- const total_size = (obj.size && obj.binary) ? obj.size : 0;
- const size_delta_inserted =
- obj.binary && obj.size_delta > 0 ? obj.size_delta : 0;
- const size_delta_deleted =
- obj.binary && obj.size_delta < 0 ? obj.size_delta : 0;
-
- return {
- inserted: acc.inserted + inserted,
- deleted: acc.deleted + deleted,
- size_delta_inserted: acc.size_delta_inserted + size_delta_inserted,
- size_delta_deleted: acc.size_delta_deleted + size_delta_deleted,
- total_size: acc.total_size + total_size,
- };
- }, {inserted: 0, deleted: 0, size_delta_inserted: 0,
- size_delta_deleted: 0, total_size: 0});
- }
-
- _getDiffPreferences() {
- return this.$.restAPI.getDiffPreferences();
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- _toggleFileExpanded(file) {
- // Is the path in the list of expanded diffs? IF so remove it, otherwise
- // add it to the list.
- const pathIndex = this._expandedFiles.findIndex(f => f.path === file.path);
- if (pathIndex === -1) {
- this.push('_expandedFiles', file);
- } else {
- this.splice('_expandedFiles', pathIndex, 1);
- }
- }
-
- _toggleFileExpandedByIndex(index) {
- this._toggleFileExpanded(this._computeFileRange(this._files[index]));
- }
-
- _updateDiffPreferences() {
- if (!this.diffs.length) { return; }
- // Re-render all expanded diffs sequentially.
- this.reporting.time(EXPAND_ALL_TIMING_LABEL);
- this._renderInOrder(this._expandedFiles, this.diffs,
- this._expandedFiles.length);
- }
-
- _forEachDiff(fn) {
- const diffs = this.diffs;
- for (let i = 0; i < diffs.length; i++) {
- fn(diffs[i]);
- }
- }
-
- expandAllDiffs() {
- this._showInlineDiffs = true;
-
- // Find the list of paths that are in the file list, but not in the
- // expanded list.
- const newFiles = [];
- let path;
- for (let i = 0; i < this._shownFiles.length; i++) {
- path = this._shownFiles[i].__path;
- if (!this._expandedFiles.some(f => f.path === path)) {
- newFiles.push(this._computeFileRange(this._shownFiles[i]));
- }
- }
-
- this.splice(...['_expandedFiles', 0, 0].concat(newFiles));
- }
-
- collapseAllDiffs() {
- this._showInlineDiffs = false;
- this._expandedFiles = [];
- this.filesExpanded = this._computeExpandedFiles(
- this._expandedFiles.length, this._files.length);
- this.$.diffCursor.handleDiffUpdate();
- }
-
- /**
- * Computes a string with the number of comments and unresolved comments.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeCommentsString(changeComments, patchRange, path) {
- if ([changeComments, patchRange, path].includes(undefined)) {
- return '';
- }
- const unresolvedCount =
- changeComments.computeUnresolvedNum({
- patchNum: patchRange.basePatchNum,
- path,
- }) +
- changeComments.computeUnresolvedNum({
- patchNum: patchRange.patchNum,
- path,
- });
- const commentCount =
- changeComments.computeCommentCount({
- patchNum: patchRange.basePatchNum,
- path,
- }) +
- changeComments.computeCommentCount({
- patchNum: patchRange.patchNum,
- path,
- });
- const commentString = GrCountStringFormatter.computePluralString(
- commentCount, 'comment');
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
-
- return commentString +
- // Add a space if both comments and unresolved
- (commentString && unresolvedString ? ' ' : '') +
- // Add parentheses around unresolved if it exists.
- (unresolvedString ? `(${unresolvedString})` : '');
- }
-
- /**
- * Computes a string with the number of drafts.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeDraftsString(changeComments, patchRange, path) {
- if ([changeComments, patchRange, path].includes(undefined)) {
- return '';
- }
- const draftCount =
- changeComments.computeDraftCount({
- patchNum: patchRange.basePatchNum,
- path,
- }) +
- changeComments.computeDraftCount({
- patchNum: patchRange.patchNum,
- path,
- });
- return GrCountStringFormatter.computePluralString(draftCount, 'draft');
- }
-
- /**
- * Computes a shortened string with the number of drafts.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeDraftsStringMobile(changeComments, patchRange, path) {
- if ([changeComments, patchRange, path].includes(undefined)) {
- return '';
- }
- const draftCount =
- changeComments.computeDraftCount({
- patchNum: patchRange.basePatchNum,
- path,
- }) +
- changeComments.computeDraftCount({
- patchNum: patchRange.patchNum,
- path,
- });
- return GrCountStringFormatter.computeShortString(draftCount, 'd');
- }
-
- /**
- * Computes a shortened string with the number of comments.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeCommentsStringMobile(changeComments, patchRange, path) {
- if ([changeComments, patchRange, path].includes(undefined)) {
- return '';
- }
- const commentCount =
- changeComments.computeCommentCount({
- patchNum: patchRange.basePatchNum,
- path,
- }) +
- changeComments.computeCommentCount({
- patchNum: patchRange.patchNum,
- path,
- });
- return GrCountStringFormatter.computeShortString(commentCount, 'c');
- }
-
- /**
- * @param {string} path
- * @param {boolean=} opt_reviewed
- */
- _reviewFile(path, opt_reviewed) {
- if (this.editMode) { return; }
- const index = this._files.findIndex(file => file.__path === path);
- const reviewed = opt_reviewed || !this._files[index].isReviewed;
-
- this.set(['_files', index, 'isReviewed'], reviewed);
- if (index < this._shownFiles.length) {
- this.notifyPath(`_shownFiles.${index}.isReviewed`);
- }
-
- this._saveReviewedState(path, reviewed);
- }
-
- _saveReviewedState(path, reviewed) {
- return this.$.restAPI.saveFileReviewed(this.changeNum,
- this.patchRange.patchNum, path, reviewed);
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getReviewedFiles() {
- if (this.editMode) { return Promise.resolve([]); }
- return this.$.restAPI.getReviewedFiles(this.changeNum,
- this.patchRange.patchNum);
- }
-
- _getFiles() {
- return this.$.restAPI.getChangeOrEditFiles(
- this.changeNum, this.patchRange);
- }
-
- /**
- *
- * @returns {!Array<FileInfo>}
- */
- _normalizeChangeFilesResponse(response) {
- if (!response) { return []; }
- const paths = Object.keys(response).sort(specialFilePathCompare);
- const files = [];
- for (let i = 0; i < paths.length; i++) {
- const info = response[paths[i]];
- info.__path = paths[i];
- info.lines_inserted = info.lines_inserted || 0;
- info.lines_deleted = info.lines_deleted || 0;
- info.size_delta = info.size_delta || 0;
- files.push(info);
- }
- return files;
- }
-
- /**
- * Returns true if the event e is a click on an element.
- *
- * The click is: mouse click or pressing Enter or Space key
- * P.S> Screen readers sends click event as well
- */
- _isClickEvent(e) {
- if (e.type === 'click') {
- return true;
- }
- const isSpaceOrEnter = (e.key === 'Enter' || e.key === ' ');
- return e.type === 'keydown' && isSpaceOrEnter;
- }
-
- _fileActionClick(e, fileAction) {
- if (this._isClickEvent(e)) {
- const fileRow = this._getFileRowFromEvent(e);
- if (!fileRow) {
- return;
- }
- // Prevent default actions (e.g. scrolling for space key)
- e.preventDefault();
- // Prevent _handleFileListClick handler call
- e.stopPropagation();
- this.$.fileCursor.setCursor(fileRow.element);
- fileAction(fileRow.file);
- }
- }
-
- _reviewedClick(e) {
- this._fileActionClick(e,
- file => this._reviewFile(file.path));
- }
-
- _expandedClick(e) {
- this._fileActionClick(e,
- file => this._toggleFileExpanded(file));
- }
-
- /**
- * Handle all events from the file list dom-repeat so event handleers don't
- * have to get registered for potentially very long lists.
- */
- _handleFileListClick(e) {
- const fileRow = this._getFileRowFromEvent(e);
- if (!fileRow) {
- return;
- }
- const file = fileRow.file;
- const path = file.path;
- // If a path cannot be interpreted from the click target (meaning it's not
- // somewhere in the row, e.g. diff content) or if the user clicked the
- // link, defer to the native behavior.
- if (!path || descendedFromClass(e.target, 'pathLink')) { return; }
-
- // Disregard the event if the click target is in the edit controls.
- if (descendedFromClass(e.target, 'editFileControls')) { return; }
-
- e.preventDefault();
- this.$.fileCursor.setCursor(fileRow.element);
- this._toggleFileExpanded(file);
- }
-
- _getFileRowFromEvent(e) {
- // Traverse upwards to find the row element if the target is not the row.
- let row = e.target;
- while (!row.classList.contains(FILE_ROW_CLASS) && row.parentElement) {
- row = row.parentElement;
- }
-
- // No action needed for item without a valid file
- if (!row.dataset['file']) {
- return null;
- }
-
- return {
- file: JSON.parse(row.dataset['file']),
- element: row,
- };
- }
-
- /**
- * Generates file range from file info object.
- *
- * @param {FileInfo} file
- * @returns {Gerrit.FileRange}
- */
- _computeFileRange(file) {
- const fileData = {
- path: file.__path,
- };
- if (file.old_path) {
- fileData.basePath = file.old_path;
- }
- return fileData;
- }
-
- _handleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- this.$.diffCursor.moveLeft();
- }
-
- _handleRightPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- this.$.diffCursor.moveRight();
- }
-
- _handleToggleInlineDiff(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) ||
- this.$.fileCursor.index === -1) { return; }
-
- e.preventDefault();
- this._toggleFileExpandedByIndex(this.$.fileCursor.index);
- }
-
- _handleToggleAllInlineDiffs(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._toggleInlineDiffs();
- }
-
- _handleToggleHideAllCommentThreads(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- e.preventDefault();
- this.toggleClass('hideComments');
- }
-
- _handleCursorNext(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- if (this._showInlineDiffs) {
- e.preventDefault();
- this.$.diffCursor.moveDown();
- this._displayLine = true;
- } else {
- // Down key
- if (this.getKeyboardEvent(e).keyCode === 40) { return; }
- e.preventDefault();
- this.$.fileCursor.next();
- this.selectedIndex = this.$.fileCursor.index;
- }
- }
-
- _handleCursorPrev(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- if (this._showInlineDiffs) {
- e.preventDefault();
- this.$.diffCursor.moveUp();
- this._displayLine = true;
- } else {
- // Up key
- if (this.getKeyboardEvent(e).keyCode === 38) { return; }
- e.preventDefault();
- this.$.fileCursor.previous();
- this.selectedIndex = this.$.fileCursor.index;
- }
- }
-
- _handleNewComment(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
- this.$.diffCursor.createCommentInPlace();
- }
-
- _handleOpenLastFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._openSelectedFile(this._files.length - 1);
- }
-
- _handleOpenFirstFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._openSelectedFile(0);
- }
-
- _handleOpenFile(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
-
- if (this._showInlineDiffs) {
- this._openCursorFile();
- return;
- }
-
- this._openSelectedFile();
- }
-
- _handleNextChunk(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
- this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- if (this.isModifierPressed(e, 'shiftKey')) {
- this.$.diffCursor.moveToNextCommentThread();
- } else {
- this.$.diffCursor.moveToNextChunk();
- }
- }
-
- _handlePrevChunk(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
- this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- if (this.isModifierPressed(e, 'shiftKey')) {
- this.$.diffCursor.moveToPreviousCommentThread();
- } else {
- this.$.diffCursor.moveToPreviousChunk();
- }
- }
-
- _handleToggleFileReviewed(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- e.preventDefault();
- if (!this._files[this.$.fileCursor.index]) { return; }
- this._reviewFile(this._files[this.$.fileCursor.index].__path);
- }
-
- _handleToggleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._forEachDiff(diff => {
- diff.toggleLeftDiff();
- });
- }
-
- _toggleInlineDiffs() {
- if (this._showInlineDiffs) {
- this.collapseAllDiffs();
- } else {
- this.expandAllDiffs();
- }
- }
-
- _openCursorFile() {
- const diff = this.$.diffCursor.getTargetDiffElement();
- GerritNav.navigateToDiff(this.change, diff.path,
- diff.patchRange.patchNum, this.patchRange.basePatchNum);
- }
-
- /**
- * @param {number=} opt_index
- */
- _openSelectedFile(opt_index) {
- if (opt_index != null) {
- this.$.fileCursor.setCursorAtIndex(opt_index);
- }
- if (!this._files[this.$.fileCursor.index]) { return; }
- GerritNav.navigateToDiff(this.change,
- this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum,
- this.patchRange.basePatchNum);
- }
-
- _addDraftAtTarget() {
- const diff = this.$.diffCursor.getTargetDiffElement();
- const target = this.$.diffCursor.getTargetLineElement();
- if (diff && target) {
- diff.addDraftAtLine(target);
- }
- }
-
- _shouldHideChangeTotals(_patchChange) {
- return _patchChange.inserted === 0 && _patchChange.deleted === 0;
- }
-
- _shouldHideBinaryChangeTotals(_patchChange) {
- return _patchChange.size_delta_inserted === 0 &&
- _patchChange.size_delta_deleted === 0;
- }
-
- _computeFileStatus(status) {
- return status || 'M';
- }
-
- _computeDiffURL(change, patchRange, path, editMode) {
- // Polymer 2: check for undefined
- if ([change, patchRange, path, editMode]
- .some(arg => arg === undefined)) {
- return;
- }
- if (editMode && path !== SpecialFilePath.MERGE_LIST) {
- return GerritNav.getEditUrlForDiff(change, path, patchRange.patchNum,
- patchRange.basePatchNum);
- }
- return GerritNav.getUrlForDiff(change, path, patchRange.patchNum,
- patchRange.basePatchNum);
- }
-
- _formatBytes(bytes) {
- if (bytes == 0) return '+/-0 B';
- const bits = 1024;
- const decimals = 1;
- const sizes =
- ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
- const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits));
- const prepend = bytes > 0 ? '+' : '';
- return prepend + parseFloat((bytes / Math.pow(bits, exponent))
- .toFixed(decimals)) + ' ' + sizes[exponent];
- }
-
- _formatPercentage(size, delta) {
- const oldSize = size - delta;
-
- if (oldSize === 0) { return ''; }
-
- const percentage = Math.round(Math.abs(delta * 100 / oldSize));
- return '(' + (delta > 0 ? '+' : '-') + percentage + '%)';
- }
-
- _computeBinaryClass(delta) {
- if (delta === 0) { return; }
- return delta >= 0 ? 'added' : 'removed';
- }
-
- /**
- * @param {string} baseClass
- * @param {string} path
- */
- _computeClass(baseClass, path) {
- const classes = [];
- if (baseClass) {
- classes.push(baseClass);
- }
- if (path === SpecialFilePath.COMMIT_MESSAGE ||
- path === SpecialFilePath.MERGE_LIST) {
- classes.push('invisible');
- }
- return classes.join(' ');
- }
-
- _computeStatusClass(file) {
- const classStr = this._computeClass('status', file.__path);
- return `${classStr} ${this._computeFileStatus(file.status)}`;
- }
-
- _computePathClass(path, expandedFilesRecord) {
- return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
- }
-
- _computeShowHideIcon(path, expandedFilesRecord) {
- return this._isFileExpanded(path, expandedFilesRecord) ?
- 'gr-icons:expand-less' : 'gr-icons:expand-more';
- }
-
- _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
- // Polymer 2: check for undefined
- if ([
- filesByPath,
- changeComments,
- patchRange,
- reviewed,
- loading,
- ].includes(undefined)) {
- return;
- }
-
- // Await all promises resolving from reload. @See Issue 9057
- if (loading || !changeComments) { return; }
-
- const commentedPaths = changeComments.getPaths(patchRange);
- const files = {...filesByPath};
- addUnmodifiedFiles(files, commentedPaths);
- const reviewedSet = new Set(reviewed || []);
- for (const filePath in files) {
- if (!files.hasOwnProperty(filePath)) { continue; }
- files[filePath].isReviewed = reviewedSet.has(filePath);
- }
-
- this._files = this._normalizeChangeFilesResponse(files);
- }
-
- _computeFilesShown(numFilesShown, files) {
- // Polymer 2: check for undefined
- if ([numFilesShown, files].includes(undefined)) {
- return undefined;
- }
-
- const previousNumFilesShown = this._shownFiles ?
- this._shownFiles.length : 0;
-
- const filesShown = files.slice(0, numFilesShown);
- this.dispatchEvent(new CustomEvent('files-shown-changed', {
- detail: {length: filesShown.length},
- composed: true, bubbles: true,
- }));
-
- // Start the timer for the rendering work hwere because this is where the
- // _shownFiles property is being set, and _shownFiles is used in the
- // dom-repeat binding.
- this.reporting.time(RENDER_TIMING_LABEL);
-
- // How many more files are being shown (if it's an increase).
- this._reportinShownFilesIncrement =
- Math.max(0, filesShown.length - previousNumFilesShown);
-
- return filesShown;
- }
-
- _updateDiffCursor() {
- // Overwrite the cursor's list of diffs:
- this.$.diffCursor.splice(
- ...['diffs', 0, this.$.diffCursor.diffs.length].concat(this.diffs));
- }
-
- _filesChanged() {
- if (this._files && this._files.length > 0) {
- flush();
- this.$.fileCursor.stops = Array.from(
- this.root.querySelectorAll(`.${FILE_ROW_CLASS}`));
- this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
- }
- }
-
- _incrementNumFilesShown() {
- this.numFilesShown += this.fileListIncrement;
- }
-
- _computeFileListControlClass(numFilesShown, files) {
- return numFilesShown >= files.length ? 'invisible' : '';
- }
-
- _computeIncrementText(numFilesShown, files) {
- if (!files) { return ''; }
- const text =
- Math.min(this.fileListIncrement, files.length - numFilesShown);
- return 'Show ' + text + ' more';
- }
-
- _computeShowAllText(files) {
- if (!files) { return ''; }
- return 'Show all ' + files.length + ' files';
- }
-
- _computeWarnShowAll(files) {
- return files.length > WARN_SHOW_ALL_THRESHOLD;
- }
-
- _computeShowAllWarning(files) {
- if (!this._computeWarnShowAll(files)) { return ''; }
- return 'Warning: showing all ' + files.length +
- ' files may take several seconds.';
- }
-
- _showAllFiles() {
- this.numFilesShown = this._files.length;
- }
-
- /**
- * Get a descriptive label for use in the status indicator's tooltip and
- * ARIA label.
- *
- * @param {string} status
- * @return {string}
- */
- _computeFileStatusLabel(status) {
- const statusCode = this._computeFileStatus(status);
- return FileStatus.hasOwnProperty(statusCode) ?
- FileStatus[statusCode] : 'Status Unknown';
- }
-
- /**
- * Converts any boolean-like variable to the string 'true' or 'false'
- *
- * This method is useful when you bind aria-checked attribute to a boolean
- * value. The aria-checked attribute is string attribute. Binding directly
- * to boolean variable causes problem on gerrit-CI.
- *
- * @param {object} val
- * @return {string} 'true' if val is true-like, otherwise false
- */
- _booleanToString(val) {
- return val ? 'true' : 'false';
- }
-
- _isFileExpanded(path, expandedFilesRecord) {
- return expandedFilesRecord.base.some(f => f.path === path);
- }
-
- _isFileExpandedStr(path, expandedFilesRecord) {
- return this._booleanToString(
- this._isFileExpanded(path, expandedFilesRecord));
- }
-
- _computeExpandedFiles(expandedCount, totalCount) {
- if (expandedCount === 0) {
- return FilesExpandedState.NONE;
- } else if (expandedCount === totalCount) {
- return FilesExpandedState.ALL;
- }
- return FilesExpandedState.SOME;
- }
-
- /**
- * Handle splices to the list of expanded file paths. If there are any new
- * entries in the expanded list, then render each diff corresponding in
- * order by waiting for the previous diff to finish before starting the next
- * one.
- *
- * @param {!Array} record The splice record in the expanded paths list.
- */
- _expandedFilesChanged(record) {
- // Clear content for any diffs that are not open so if they get re-opened
- // the stale content does not flash before it is cleared and reloaded.
- const collapsedDiffs = this.diffs.filter(diff =>
- this._expandedFiles.findIndex(f => f.path === diff.path) === -1);
- this._clearCollapsedDiffs(collapsedDiffs);
-
- if (!record) { return; } // Happens after "Collapse all" clicked.
-
- this.filesExpanded = this._computeExpandedFiles(
- this._expandedFiles.length, this._files.length);
-
- // Find the paths introduced by the new index splices:
- const newFiles = record.indexSplices
- .map(splice => splice.object.slice(
- splice.index, splice.index + splice.addedCount))
- .reduce((acc, paths) => acc.concat(paths), []);
-
- // Required so that the newly created diff view is included in this.diffs.
- flush();
-
- this.reporting.time(EXPAND_ALL_TIMING_LABEL);
-
- if (newFiles.length) {
- this._renderInOrder(newFiles, this.diffs, newFiles.length);
- }
-
- this._updateDiffCursor();
- this.$.diffCursor.reInitAndUpdateStops();
- }
-
- _clearCollapsedDiffs(collapsedDiffs) {
- for (const diff of collapsedDiffs) {
- diff.cancel();
- diff.clearDiffContent();
- }
- }
-
- /**
- * Given an array of paths and a NodeList of diff elements, render the diff
- * for each path in order, awaiting the previous render to complete before
- * continuing.
- *
- * @param {!Array<Gerrit.FileRange>} files
- * @param {!NodeList<!Object>} diffElements (GrDiffHostElement)
- * @param {number} initialCount The total number of paths in the pass. This
- * is used to generate log messages.
- * @return {!Promise}
- */
- _renderInOrder(files, diffElements, initialCount) {
- let iter = 0;
-
- for (const file of files) {
- const path = file.path;
- const diffElem = this._findDiffByPath(path, diffElements);
- if (diffElem) {
- diffElem.prefetchDiff();
- }
- }
-
- return (new Promise(resolve => {
- this.dispatchEvent(new CustomEvent('reload-drafts', {
- detail: {resolve},
- composed: true, bubbles: true,
- }));
- })).then(() => asyncForeach(files, (file, cancel) => {
- const path = file.path;
- this._cancelForEachDiff = cancel;
-
- iter++;
- console.info('Expanding diff', iter, 'of', initialCount, ':',
- path);
- const diffElem = this._findDiffByPath(path, diffElements);
- if (!diffElem) {
- console.warn(`Did not find <gr-diff-host> element for ${path}`);
- return Promise.resolve();
- }
- diffElem.comments = this.changeComments.getCommentsBySideForFile(
- file, this.patchRange, this.projectConfig);
- const promises = [diffElem.reload()];
- if (this._loggedIn && !this.diffPrefs.manual_review) {
- promises.push(this._reviewFile(path, true));
- }
- return Promise.all(promises);
- }).then(() => {
- this._cancelForEachDiff = null;
- this._nextRenderParams = null;
- console.info('Finished expanding', initialCount, 'diff(s)');
- this.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
- EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
- /* Block diff cursor from auto scrolling after files are done rendering.
- * This prevents the bug where the screen jumps to the first diff chunk
- * after files are done being rendered after the user has already begun
- * scrolling.
- * This also however results in the fact that the cursor does not auto
- * focus on the first diff chunk on a small screen. This is however, a use
- * case we are willing to not support for now.
-
- * Using handleDiffUpdate resulted in diffCursor.row being set which
- * prevented the issue of scrolling to top when we expand the second
- * file individually.
- */
- this.$.diffCursor.reInitAndUpdateStops();
- }));
- }
-
- /** Cancel the rendering work of every diff in the list */
- _cancelDiffs() {
- if (this._cancelForEachDiff) { this._cancelForEachDiff(); }
- this._forEachDiff(d => d.cancel());
- }
-
- /**
- * In the given NodeList of diff elements, find the diff for the given path.
- *
- * @param {string} path
- * @param {!NodeList<!Object>} diffElements (GrDiffElement)
- * @return {!Object|undefined} (GrDiffElement)
- */
- _findDiffByPath(path, diffElements) {
- for (let i = 0; i < diffElements.length; i++) {
- if (diffElements[i].path === path) {
- return diffElements[i];
- }
- }
- }
-
- /**
- * Reset the comments of a modified thread
- *
- * @param {string} rootId
- * @param {string} path
- */
- reloadCommentsForThreadWithRootId(rootId, path) {
- // Don't bother continuing if we already know that the path that contains
- // the updated comment thread is not expanded.
- if (!this._expandedFiles.some(f => f.path === path)) { return; }
- const diff = this.diffs.find(d => d.path === path);
-
- const threadEl = diff.getThreadEls().find(t => t.rootId === rootId);
- if (!threadEl) { return; }
-
- const newComments = this.changeComments.getCommentsForThread(rootId);
-
- // If newComments is null, it means that a single draft was
- // removed from a thread in the thread view, and the thread should
- // no longer exist. Remove the existing thread element in the diff
- // view.
- if (!newComments) {
- threadEl.fireRemoveSelf();
- return;
- }
-
- // Comments are not returned with the commentSide attribute from
- // the api, but it's necessary to be stored on the diff's
- // comments due to use in the _handleCommentUpdate function.
- // The comment thread already has a side associated with it, so
- // set the comment's side to match.
- threadEl.comments = newComments.map(c => Object.assign(
- c, {__commentSide: threadEl.commentSide}
- ));
- flush();
- }
-
- _handleEscKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
- this._displayLine = false;
- }
-
- /**
- * Update the loading class for the file list rows. The update is inside a
- * debouncer so that the file list doesn't flash gray when the API requests
- * are reasonably fast.
- *
- * @param {boolean} loading
- */
- _loadingChanged(loading) {
- this.debounce('loading-change', () => {
- // Only show set the loading if there have been files loaded to show. In
- // this way, the gray loading style is not shown on initial loads.
- this.classList.toggle('loading', loading && !!this._files.length);
- }, LOADING_DEBOUNCE_INTERVAL);
- }
-
- _editModeChanged(editMode) {
- this.classList.toggle('editMode', editMode);
- }
-
- _computeReviewedClass(isReviewed) {
- return isReviewed ? 'isReviewed' : '';
- }
-
- _computeReviewedText(isReviewed) {
- return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED';
- }
-
- /**
- * Given a file path, return whether that path should have visible size bars
- * and be included in the size bars calculation.
- *
- * @param {string} path
- * @return {boolean}
- */
- _showBarsForPath(path) {
- return path !== SpecialFilePath.COMMIT_MESSAGE &&
- path !== SpecialFilePath.MERGE_LIST;
- }
-
- /**
- * Compute size bar layout values from the file list.
- *
- * @return {Gerrit.LayoutStats|undefined}
- *
- */
- _computeSizeBarLayout(shownFilesRecord) {
- if (!shownFilesRecord || !shownFilesRecord.base) { return undefined; }
- const stats = {
- maxInserted: 0,
- maxDeleted: 0,
- maxAdditionWidth: 0,
- maxDeletionWidth: 0,
- deletionOffset: 0,
- };
- shownFilesRecord.base
- .filter(f => this._showBarsForPath(f.__path))
- .forEach(f => {
- if (f.lines_inserted) {
- stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted);
- }
- if (f.lines_deleted) {
- stats.maxDeleted = Math.max(stats.maxDeleted, f.lines_deleted);
- }
- });
- const ratio = stats.maxInserted / (stats.maxInserted + stats.maxDeleted);
- if (!isNaN(ratio)) {
- stats.maxAdditionWidth =
- (SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio;
- stats.maxDeletionWidth =
- SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth;
- stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH;
- }
- return stats;
- }
-
- /**
- * Get the width of the addition bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarAdditionWidth(file, stats) {
- if (stats.maxInserted === 0 ||
- !file.lines_inserted ||
- !this._showBarsForPath(file.__path)) {
- return 0;
- }
- const width =
- stats.maxAdditionWidth * file.lines_inserted / stats.maxInserted;
- return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
- }
-
- /**
- * Get the x-offset of the addition bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarAdditionX(file, stats) {
- return stats.maxAdditionWidth -
- this._computeBarAdditionWidth(file, stats);
- }
-
- /**
- * Get the width of the deletion bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarDeletionWidth(file, stats) {
- if (stats.maxDeleted === 0 ||
- !file.lines_deleted ||
- !this._showBarsForPath(file.__path)) {
- return 0;
- }
- const width =
- stats.maxDeletionWidth * file.lines_deleted / stats.maxDeleted;
- return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
- }
-
- /**
- * Get the x-offset of the deletion bar for a file.
- *
- * @param {Gerrit.LayoutStats} stats
- *
- * @return {number}
- */
- _computeBarDeletionX(stats) {
- return stats.deletionOffset;
- }
-
- _computeShowSizeBars(userPrefs) {
- return !!userPrefs.size_bar_in_change_table;
- }
-
- _computeSizeBarsClass(showSizeBars, path) {
- let hideClass = '';
- if (!showSizeBars) {
- hideClass = 'hide';
- } else if (!this._showBarsForPath(path)) {
- hideClass = 'invisible';
- }
- return `sizeBars desktop ${hideClass}`;
- }
-
- /**
- * Shows registered dynamic columns iff the 'header', 'content' and
- * 'summary' endpoints are registered the exact same number of times.
- * Ideally, there should be a better way to enforce the expectation of the
- * dependencies between dynamic endpoints.
- */
- _computeShowDynamicColumns(
- headerEndpoints, contentEndpoints, summaryEndpoints) {
- return headerEndpoints && contentEndpoints && summaryEndpoints &&
- headerEndpoints.length &&
- headerEndpoints.length === contentEndpoints.length &&
- headerEndpoints.length === summaryEndpoints.length;
- }
-
- /**
- * Shows registered dynamic prepended columns iff the 'header', 'content'
- * endpoints are registered the exact same number of times.
- */
- _computeShowPrependedDynamicColumns(
- headerEndpoints, contentEndpoints) {
- return headerEndpoints && contentEndpoints &&
- headerEndpoints.length &&
- headerEndpoints.length === contentEndpoints.length;
- }
-
- /**
- * Returns true if none of the inline diffs have been expanded.
- *
- * @return {boolean}
- */
- _noDiffsExpanded() {
- return this.filesExpanded === FilesExpandedState.NONE;
- }
-
- /**
- * Method to call via binding when each file list row is rendered. This
- * allows approximate detection of when the dom-repeat has completed
- * rendering.
- *
- * @param {number} index The index of the row being rendered.
- * @return {string} an empty string.
- */
- _reportRenderedRow(index) {
- if (index === this._shownFiles.length - 1) {
- this.async(() => {
- this.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
- RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
- }, 1);
- }
- return '';
- }
-
- _reviewedTitle(reviewed) {
- if (reviewed) {
- return 'Mark as not reviewed (shortcut: r)';
- }
-
- return 'Mark as reviewed (shortcut: r)';
- }
-
- _handleReloadingDiffPreference() {
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
- }
-
- /**
- * Wrapper for using in the element template and computed properties
- */
- _computeDisplayPath(path) {
- return computeDisplayPath(path);
- }
-
- /**
- * Wrapper for using in the element template and computed properties
- */
- _computeTruncatedPath(path) {
- return computeTruncatedPath(path);
- }
-}
-
-customElements.define(GrFileList.is, GrFileList);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
new file mode 100644
index 0000000..3a2c858
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -0,0 +1,1901 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../styles/shared-styles';
+import '../../diff/gr-diff-cursor/gr-diff-cursor';
+import '../../diff/gr-diff-host/gr-diff-host';
+import '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog';
+import '../../edit/gr-edit-file-controls/gr-edit-file-controls';
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-cursor-manager/gr-cursor-manager';
+import '../../shared/gr-icons/gr-icons';
+import '../../shared/gr-linked-text/gr-linked-text';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../../shared/gr-select/gr-select';
+import '../../shared/gr-tooltip-content/gr-tooltip-content';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
+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-file-list_html';
+import {asyncForeach} from '../../../utils/async-util';
+import {
+ CustomKeyboardEvent,
+ KeyboardShortcutMixin,
+ Modifier,
+ Shortcut,
+} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {FilesExpandedState} from '../gr-file-list-constants';
+import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {appContext} from '../../../services/app-context';
+import {DiffViewMode, SpecialFilePath} from '../../../constants/constants';
+import {descendedFromClass} from '../../../utils/dom-util';
+import {
+ addUnmodifiedFiles,
+ computeDisplayPath,
+ computeTruncatedPath,
+ isMagicPath,
+ specialFilePathCompare,
+} from '../../../utils/path-list-util';
+import {customElement, observe, property} from '@polymer/decorators';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {
+ ConfigInfo,
+ DiffPreferencesInfo,
+ ElementPropertyDeepChange,
+ FileInfo,
+ FileNameToFileInfoMap,
+ NumericChangeId,
+ PatchRange,
+ PreferencesInfo,
+ RevisionInfo,
+ UrlEncodedCommentId,
+} from '../../../types/common';
+import {GrDiffHost} from '../../diff/gr-diff-host/gr-diff-host';
+import {GrDiffPreferencesDialog} from '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog';
+import {hasOwnProperty} from '../../../utils/common-util';
+import {GrDiffCursor} from '../../diff/gr-diff-cursor/gr-diff-cursor';
+import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
+import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
+import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
+import {PatchSetFile, UIDraft} from '../../../utils/comment-util';
+import {ParsedChangeInfo} from '../../shared/gr-rest-api-interface/gr-reviewer-updates-parser';
+
+export const DEFAULT_NUM_FILES_SHOWN = 200;
+
+const WARN_SHOW_ALL_THRESHOLD = 1000;
+const LOADING_DEBOUNCE_INTERVAL = 100;
+
+const SIZE_BAR_MAX_WIDTH = 61;
+const SIZE_BAR_GAP_WIDTH = 1;
+const SIZE_BAR_MIN_WIDTH = 1.5;
+
+const RENDER_TIMING_LABEL = 'FileListRenderTime';
+const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile';
+const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs';
+const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff';
+
+const FileStatus = {
+ A: 'Added',
+ C: 'Copied',
+ D: 'Deleted',
+ M: 'Modified',
+ R: 'Renamed',
+ W: 'Rewritten',
+ U: 'Unchanged',
+};
+
+const FILE_ROW_CLASS = 'file-row';
+
+export interface GrFileList {
+ $: {
+ restAPI: RestApiService & Element;
+ diffPreferencesDialog: GrDiffPreferencesDialog;
+ diffCursor: GrDiffCursor;
+ fileCursor: GrCursorManager;
+ };
+}
+
+interface ReviewedFileInfo extends FileInfo {
+ isReviewed?: boolean;
+}
+interface NormalizedFileInfo extends ReviewedFileInfo {
+ __path: string;
+}
+
+interface PatchChange {
+ inserted: number;
+ deleted: number;
+ size_delta_inserted: number;
+ size_delta_deleted: number;
+ total_size: number;
+}
+
+function createDefaultPatchChange(): PatchChange {
+ // Use function instead of const to prevent unexpected changes in the default
+ // values.
+ return {
+ inserted: 0,
+ deleted: 0,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
+ };
+}
+
+interface SizeBarLayout {
+ maxInserted: number;
+ maxDeleted: number;
+ maxAdditionWidth: number;
+ maxDeletionWidth: number;
+ deletionOffset: number;
+}
+
+function createDefaultSizeBarLayout(): SizeBarLayout {
+ // Use function instead of const to prevent unexpected changes in the default
+ // values.
+ return {
+ maxInserted: 0,
+ maxDeleted: 0,
+ maxAdditionWidth: 0,
+ maxDeletionWidth: 0,
+ deletionOffset: 0,
+ };
+}
+
+interface FileRow {
+ file: PatchSetFile;
+ element: HTMLElement;
+}
+
+export type FileNameToReviewedFileInfoMap = {[name: string]: ReviewedFileInfo};
+
+/**
+ * Type for FileInfo
+ *
+ * This should match with the type returned from `files` API plus
+ * additional info like `__path`.
+ *
+ * @typedef {Object} FileInfo
+ * @property {string} __path
+ * @property {?string} old_path
+ * @property {number} size
+ * @property {number} size_delta - fallback to 0 if not present in api
+ * @property {number} lines_deleted - fallback to 0 if not present in api
+ * @property {number} lines_inserted - fallback to 0 if not present in api
+ */
+
+@customElement('gr-file-list')
+export class GrFileList extends KeyboardShortcutMixin(
+ GestureEventListeners(LegacyElementMixin(PolymerElement))
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when a draft refresh should get triggered
+ *
+ * @event reload-drafts
+ */
+
+ @property({type: Object})
+ patchRange?: PatchRange;
+
+ @property({type: String})
+ patchNum?: string;
+
+ @property({type: Number})
+ changeNum?: NumericChangeId;
+
+ @property({type: Object})
+ changeComments?: ChangeComments;
+
+ @property({type: Object})
+ drafts?: {[path: string]: UIDraft[]};
+
+ @property({type: Array})
+ revisions?: {[revisionId: string]: RevisionInfo};
+
+ @property({type: Object})
+ projectConfig?: ConfigInfo;
+
+ @property({type: Number, notify: true})
+ selectedIndex = -1;
+
+ @property({type: Object})
+ keyEventTarget = document.body;
+
+ @property({type: Object})
+ change?: ParsedChangeInfo;
+
+ @property({type: String, notify: true, observer: '_updateDiffPreferences'})
+ diffViewMode?: DiffViewMode;
+
+ @property({type: Boolean, observer: '_editModeChanged'})
+ editMode?: boolean;
+
+ @property({type: String, notify: true})
+ filesExpanded = FilesExpandedState.NONE;
+
+ @property({type: Object})
+ _filesByPath?: FileNameToFileInfoMap;
+
+ @property({type: Array, observer: '_filesChanged'})
+ _files: NormalizedFileInfo[] = [];
+
+ @property({type: Boolean})
+ _loggedIn = false;
+
+ @property({type: Array})
+ _reviewed?: string[] = [];
+
+ @property({type: Object, notify: true, observer: '_updateDiffPreferences'})
+ diffPrefs?: DiffPreferencesInfo;
+
+ @property({type: Object})
+ _userPrefs?: PreferencesInfo;
+
+ @property({type: Boolean})
+ _showInlineDiffs?: boolean;
+
+ @property({type: Number, notify: true})
+ numFilesShown: number = DEFAULT_NUM_FILES_SHOWN;
+
+ @property({type: Object, computed: '_calculatePatchChange(_files)'})
+ _patchChange: PatchChange = createDefaultPatchChange();
+
+ @property({type: Number})
+ fileListIncrement: number = DEFAULT_NUM_FILES_SHOWN;
+
+ @property({type: Boolean, computed: '_shouldHideChangeTotals(_patchChange)'})
+ _hideChangeTotals = true;
+
+ @property({
+ type: Boolean,
+ computed: '_shouldHideBinaryChangeTotals(_patchChange)',
+ })
+ _hideBinaryChangeTotals = true;
+
+ @property({
+ type: Array,
+ computed: '_computeFilesShown(numFilesShown, _files)',
+ })
+ _shownFiles: NormalizedFileInfo[] = [];
+
+ @property({type: Number})
+ _reportinShownFilesIncrement = 0;
+
+ @property({type: Array})
+ _expandedFiles: PatchSetFile[] = [];
+
+ @property({type: Boolean})
+ _displayLine?: boolean;
+
+ @property({type: Boolean, observer: '_loadingChanged'})
+ _loading?: boolean;
+
+ @property({type: Object, computed: '_computeSizeBarLayout(_shownFiles.*)'})
+ _sizeBarLayout: SizeBarLayout = createDefaultSizeBarLayout();
+
+ @property({type: Boolean, computed: '_computeShowSizeBars(_userPrefs)'})
+ _showSizeBars = true;
+
+ private _cancelForEachDiff?: () => void;
+
+ @property({
+ type: Boolean,
+ computed:
+ '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
+ '_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
+ })
+ _showDynamicColumns = false;
+
+ @property({
+ type: Boolean,
+ computed:
+ '_computeShowPrependedDynamicColumns(' +
+ '_dynamicPrependedHeaderEndpoints, _dynamicPrependedContentEndpoints)',
+ })
+ _showPrependedDynamicColumns = false;
+
+ @property({type: Array})
+ _dynamicHeaderEndpoints?: string[];
+
+ @property({type: Array})
+ _dynamicContentEndpoints?: string[];
+
+ @property({type: Array})
+ _dynamicSummaryEndpoints?: string[];
+
+ @property({type: Array})
+ _dynamicPrependedHeaderEndpoints?: string[];
+
+ @property({type: Array})
+ _dynamicPrependedContentEndpoints?: string[];
+
+ private readonly reporting = appContext.reportingService;
+
+ get keyBindings() {
+ return {
+ esc: '_handleEscKey',
+ };
+ }
+
+ keyboardShortcuts() {
+ return {
+ [Shortcut.LEFT_PANE]: '_handleLeftPane',
+ [Shortcut.RIGHT_PANE]: '_handleRightPane',
+ [Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
+ [Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
+ [Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
+ '_handleToggleHideAllCommentThreads',
+ [Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
+ [Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
+ [Shortcut.NEXT_LINE]: '_handleCursorNext',
+ [Shortcut.PREV_LINE]: '_handleCursorPrev',
+ [Shortcut.NEW_COMMENT]: '_handleNewComment',
+ [Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
+ [Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
+ [Shortcut.OPEN_FILE]: '_handleOpenFile',
+ [Shortcut.NEXT_CHUNK]: '_handleNextChunk',
+ [Shortcut.PREV_CHUNK]: '_handlePrevChunk',
+ [Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+ [Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
+
+ // Final two are actually handled by gr-comment-thread.
+ [Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+ [Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('keydown', e => this._scopedKeydownHandler(e));
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ getPluginLoader()
+ .awaitPluginsLoaded()
+ .then(() => {
+ this._dynamicHeaderEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-view-file-list-header'
+ );
+ this._dynamicContentEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-view-file-list-content'
+ );
+ this._dynamicPrependedHeaderEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-view-file-list-header-prepend'
+ );
+ this._dynamicPrependedContentEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-view-file-list-content-prepend'
+ );
+ this._dynamicSummaryEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-view-file-list-summary'
+ );
+
+ if (
+ this._dynamicHeaderEndpoints.length !==
+ this._dynamicContentEndpoints.length
+ ) {
+ console.warn(
+ 'Different number of dynamic file-list header and content.'
+ );
+ }
+ if (
+ this._dynamicPrependedHeaderEndpoints.length !==
+ this._dynamicPrependedContentEndpoints.length
+ ) {
+ console.warn(
+ 'Different number of dynamic file-list header and content.'
+ );
+ }
+ if (
+ this._dynamicHeaderEndpoints.length !==
+ this._dynamicSummaryEndpoints.length
+ ) {
+ console.warn(
+ 'Different number of dynamic file-list headers and summary.'
+ );
+ }
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this._cancelDiffs();
+ }
+
+ /**
+ * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
+ * events must be scoped to a component level (e.g. `enter`) in order to not
+ * override native browser functionality.
+ *
+ * Context: Issue 7277
+ */
+ _scopedKeydownHandler(e: KeyboardEvent) {
+ if (e.keyCode === 13) {
+ // TODO(TS): e is not an instance of CustomKeyboardEvent.
+ // However, to fix it we should fix keyboard-shortcut-mixin first
+ // The keyboard-shortcut-mixin will be updated in a separate change
+ this._handleOpenFile((e as unknown) as CustomKeyboardEvent);
+ }
+ }
+
+ reload() {
+ if (!this.changeNum || !this.patchRange?.patchNum) {
+ return Promise.resolve();
+ }
+ const changeNum = this.changeNum;
+ const patchRange = this.patchRange;
+
+ this._loading = true;
+
+ this.collapseAllDiffs();
+ const promises = [];
+
+ promises.push(
+ this.$.restAPI
+ .getChangeOrEditFiles(changeNum, patchRange)
+ .then(filesByPath => {
+ this._filesByPath = filesByPath;
+ })
+ );
+ promises.push(
+ this._getLoggedIn()
+ .then(loggedIn => (this._loggedIn = loggedIn))
+ .then(loggedIn => {
+ if (!loggedIn) {
+ return;
+ }
+
+ return this._getReviewedFiles(changeNum, patchRange).then(
+ reviewed => {
+ this._reviewed = reviewed;
+ }
+ );
+ })
+ );
+
+ promises.push(
+ this._getDiffPreferences().then(prefs => {
+ this.diffPrefs = prefs;
+ })
+ );
+
+ promises.push(
+ this._getPreferences().then(prefs => {
+ this._userPrefs = prefs;
+ })
+ );
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
+ this._detectChromiteButler();
+ this.reporting.fileListDisplayed();
+ });
+ }
+
+ _detectChromiteButler() {
+ const hasButler = !!document.getElementById('butler-suggested-owners');
+ if (hasButler) {
+ this.reporting.reportExtension('butler');
+ }
+ }
+
+ get diffs(): GrDiffHost[] {
+ const diffs = this.root!.querySelectorAll('gr-diff-host');
+ // It is possible that a bogus diff element is hanging around invisibly
+ // from earlier with a different patch set choice and associated with a
+ // different entry in the files array. So filter on visible items only.
+ return Array.from(diffs).filter(
+ el => !!el && !!el.style && el.style.display !== 'none'
+ );
+ }
+
+ openDiffPrefs() {
+ this.$.diffPreferencesDialog.open();
+ }
+
+ _calculatePatchChange(files: NormalizedFileInfo[]): PatchChange {
+ const magicFilesExcluded = files.filter(
+ files => !isMagicPath(files.__path)
+ );
+
+ return magicFilesExcluded.reduce((acc, obj) => {
+ const inserted = obj.lines_inserted ? obj.lines_inserted : 0;
+ const deleted = obj.lines_deleted ? obj.lines_deleted : 0;
+ const total_size = obj.size && obj.binary ? obj.size : 0;
+ const size_delta_inserted =
+ obj.binary && obj.size_delta > 0 ? obj.size_delta : 0;
+ const size_delta_deleted =
+ obj.binary && obj.size_delta < 0 ? obj.size_delta : 0;
+
+ return {
+ inserted: acc.inserted + inserted,
+ deleted: acc.deleted + deleted,
+ size_delta_inserted: acc.size_delta_inserted + size_delta_inserted,
+ size_delta_deleted: acc.size_delta_deleted + size_delta_deleted,
+ total_size: acc.total_size + total_size,
+ };
+ }, createDefaultPatchChange());
+ }
+
+ _getDiffPreferences() {
+ return this.$.restAPI.getDiffPreferences();
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ private _toggleFileExpanded(file: PatchSetFile) {
+ // Is the path in the list of expanded diffs? IF so remove it, otherwise
+ // add it to the list.
+ const pathIndex = this._expandedFiles.findIndex(f => f.path === file.path);
+ if (pathIndex === -1) {
+ this.push('_expandedFiles', file);
+ } else {
+ this.splice('_expandedFiles', pathIndex, 1);
+ }
+ }
+
+ _toggleFileExpandedByIndex(index: number) {
+ this._toggleFileExpanded(this._computePatchSetFile(this._files[index]));
+ }
+
+ _updateDiffPreferences() {
+ if (!this.diffs.length) {
+ return;
+ }
+ // Re-render all expanded diffs sequentially.
+ this.reporting.time(EXPAND_ALL_TIMING_LABEL);
+ this._renderInOrder(
+ this._expandedFiles,
+ this.diffs,
+ this._expandedFiles.length
+ );
+ }
+
+ _forEachDiff(fn: (host: GrDiffHost) => void) {
+ const diffs = this.diffs;
+ for (let i = 0; i < diffs.length; i++) {
+ fn(diffs[i]);
+ }
+ }
+
+ expandAllDiffs() {
+ this._showInlineDiffs = true;
+
+ // Find the list of paths that are in the file list, but not in the
+ // expanded list.
+ const newFiles: PatchSetFile[] = [];
+ let path: string;
+ for (let i = 0; i < this._shownFiles.length; i++) {
+ path = this._shownFiles[i].__path;
+ if (!this._expandedFiles.some(f => f.path === path)) {
+ newFiles.push(this._computePatchSetFile(this._shownFiles[i]));
+ }
+ }
+
+ this.splice('_expandedFiles', 0, 0, ...newFiles);
+ }
+
+ collapseAllDiffs() {
+ this._showInlineDiffs = false;
+ this._expandedFiles = [];
+ this.filesExpanded = this._computeExpandedFiles(
+ this._expandedFiles.length,
+ this._files.length
+ );
+ this.$.diffCursor.handleDiffUpdate();
+ }
+
+ /**
+ * Computes a string with the number of comments and unresolved comments.
+ */
+ _computeCommentsString(
+ changeComments?: ChangeComments,
+ patchRange?: PatchRange,
+ path?: string
+ ) {
+ if (
+ changeComments === undefined ||
+ patchRange === undefined ||
+ path === undefined
+ ) {
+ return '';
+ }
+ const unresolvedCount =
+ changeComments.computeUnresolvedNum({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeUnresolvedNum({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ const commentCount =
+ changeComments.computeCommentCount({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeCommentCount({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount,
+ 'comment'
+ );
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount,
+ 'unresolved'
+ );
+
+ return (
+ commentString +
+ // Add a space if both comments and unresolved
+ (commentString && unresolvedString ? ' ' : '') +
+ // Add parentheses around unresolved if it exists.
+ (unresolvedString ? `(${unresolvedString})` : '')
+ );
+ }
+
+ /**
+ * Computes a string with the number of drafts.
+ */
+ _computeDraftsString(
+ changeComments?: ChangeComments,
+ patchRange?: PatchRange,
+ path?: string
+ ) {
+ if (
+ changeComments === undefined ||
+ patchRange === undefined ||
+ path === undefined
+ ) {
+ return '';
+ }
+ const draftCount =
+ changeComments.computeDraftCount({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeDraftCount({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ return GrCountStringFormatter.computePluralString(draftCount, 'draft');
+ }
+
+ /**
+ * Computes a shortened string with the number of drafts.
+ */
+ _computeDraftsStringMobile(
+ changeComments?: ChangeComments,
+ patchRange?: PatchRange,
+ path?: string
+ ) {
+ if (
+ changeComments === undefined ||
+ patchRange === undefined ||
+ path === undefined
+ ) {
+ return '';
+ }
+ const draftCount =
+ changeComments.computeDraftCount({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeDraftCount({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ return GrCountStringFormatter.computeShortString(draftCount, 'd');
+ }
+
+ /**
+ * Computes a shortened string with the number of comments.
+ */
+ _computeCommentsStringMobile(
+ changeComments?: ChangeComments,
+ patchRange?: PatchRange,
+ path?: string
+ ) {
+ if (
+ changeComments === undefined ||
+ patchRange === undefined ||
+ path === undefined
+ ) {
+ return '';
+ }
+ const commentCount =
+ changeComments.computeCommentCount({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeCommentCount({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ return GrCountStringFormatter.computeShortString(commentCount, 'c');
+ }
+
+ private _reviewFile(path: string, reviewed?: boolean) {
+ if (this.editMode) {
+ return Promise.resolve();
+ }
+ const index = this._files.findIndex(file => file.__path === path);
+ reviewed = reviewed || !this._files[index].isReviewed;
+
+ this.set(['_files', index, 'isReviewed'], reviewed);
+ if (index < this._shownFiles.length) {
+ this.notifyPath(`_shownFiles.${index}.isReviewed`);
+ }
+
+ return this._saveReviewedState(path, reviewed);
+ }
+
+ _saveReviewedState(path: string, reviewed: boolean) {
+ if (!this.changeNum || !this.patchRange) {
+ throw new Error('changeNum and patchRange must be set');
+ }
+
+ return this.$.restAPI.saveFileReviewed(
+ this.changeNum,
+ this.patchRange.patchNum,
+ path,
+ reviewed
+ );
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getReviewedFiles(changeNum: NumericChangeId, patchRange: PatchRange) {
+ if (this.editMode) {
+ return Promise.resolve([]);
+ }
+ return this.$.restAPI.getReviewedFiles(changeNum, patchRange.patchNum);
+ }
+
+ _normalizeChangeFilesResponse(
+ response: FileNameToReviewedFileInfoMap
+ ): NormalizedFileInfo[] {
+ const paths = Object.keys(response).sort(specialFilePathCompare);
+ const files: NormalizedFileInfo[] = [];
+ for (let i = 0; i < paths.length; i++) {
+ // TODO(TS): make copy instead of as NormalizedFileInfo
+ const info = response[paths[i]] as NormalizedFileInfo;
+ info.__path = paths[i];
+ info.lines_inserted = info.lines_inserted || 0;
+ info.lines_deleted = info.lines_deleted || 0;
+ info.size_delta = info.size_delta || 0;
+ files.push(info);
+ }
+ return files;
+ }
+
+ /**
+ * Returns true if the event e is a click on an element.
+ *
+ * The click is: mouse click or pressing Enter or Space key
+ * P.S> Screen readers sends click event as well
+ */
+ _isClickEvent(e: MouseEvent | KeyboardEvent) {
+ if (e.type === 'click') {
+ return true;
+ }
+ const ke = e as KeyboardEvent;
+ const isSpaceOrEnter = ke.key === 'Enter' || ke.key === ' ';
+ return ke.type === 'keydown' && isSpaceOrEnter;
+ }
+
+ _fileActionClick(
+ e: MouseEvent | KeyboardEvent,
+ fileAction: (file: PatchSetFile) => void
+ ) {
+ if (this._isClickEvent(e)) {
+ const fileRow = this._getFileRowFromEvent(e);
+ if (!fileRow) {
+ return;
+ }
+ // Prevent default actions (e.g. scrolling for space key)
+ e.preventDefault();
+ // Prevent _handleFileListClick handler call
+ e.stopPropagation();
+ this.$.fileCursor.setCursor(fileRow.element);
+ fileAction(fileRow.file);
+ }
+ }
+
+ _reviewedClick(e: MouseEvent | KeyboardEvent) {
+ this._fileActionClick(e, file => this._reviewFile(file.path));
+ }
+
+ _expandedClick(e: MouseEvent | KeyboardEvent) {
+ this._fileActionClick(e, file => this._toggleFileExpanded(file));
+ }
+
+ /**
+ * Handle all events from the file list dom-repeat so event handleers don't
+ * have to get registered for potentially very long lists.
+ */
+ _handleFileListClick(e: MouseEvent) {
+ if (!e.target) {
+ return;
+ }
+ const fileRow = this._getFileRowFromEvent(e);
+ if (!fileRow) {
+ return;
+ }
+ const file = fileRow.file;
+ const path = file.path;
+ // If a path cannot be interpreted from the click target (meaning it's not
+ // somewhere in the row, e.g. diff content) or if the user clicked the
+ // link, defer to the native behavior.
+ if (!path || descendedFromClass(e.target as Element, 'pathLink')) {
+ return;
+ }
+
+ // Disregard the event if the click target is in the edit controls.
+ if (descendedFromClass(e.target as Element, 'editFileControls')) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.fileCursor.setCursor(fileRow.element);
+ this._toggleFileExpanded(file);
+ }
+
+ _getFileRowFromEvent(e: Event): FileRow | null {
+ // Traverse upwards to find the row element if the target is not the row.
+ let row = e.target as HTMLElement;
+ while (!row.classList.contains(FILE_ROW_CLASS) && row.parentElement) {
+ row = row.parentElement;
+ }
+
+ // No action needed for item without a valid file
+ if (!row.dataset['file']) {
+ return null;
+ }
+
+ return {
+ file: JSON.parse(row.dataset['file']) as PatchSetFile,
+ element: row,
+ };
+ }
+
+ /**
+ * Generates file range from file info object.
+ */
+ _computePatchSetFile(file: NormalizedFileInfo): PatchSetFile {
+ const fileData: PatchSetFile = {
+ path: file.__path,
+ };
+ if (file.old_path) {
+ fileData.basePath = file.old_path;
+ }
+ return fileData;
+ }
+
+ _handleLeftPane(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.diffCursor.moveLeft();
+ }
+
+ _handleRightPane(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.diffCursor.moveRight();
+ }
+
+ _handleToggleInlineDiff(e: CustomKeyboardEvent) {
+ if (
+ this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e) ||
+ this.$.fileCursor.index === -1
+ ) {
+ return;
+ }
+
+ e.preventDefault();
+ this._toggleFileExpandedByIndex(this.$.fileCursor.index);
+ }
+
+ _handleToggleAllInlineDiffs(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this._toggleInlineDiffs();
+ }
+
+ _handleToggleHideAllCommentThreads(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this.toggleClass('hideComments');
+ }
+
+ _handleCursorNext(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ if (this._showInlineDiffs) {
+ e.preventDefault();
+ this.$.diffCursor.moveDown();
+ this._displayLine = true;
+ } else {
+ // Down key
+ if (this.getKeyboardEvent(e).keyCode === 40) {
+ return;
+ }
+ e.preventDefault();
+ this.$.fileCursor.next();
+ this.selectedIndex = this.$.fileCursor.index;
+ }
+ }
+
+ _handleCursorPrev(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ if (this._showInlineDiffs) {
+ e.preventDefault();
+ this.$.diffCursor.moveUp();
+ this._displayLine = true;
+ } else {
+ // Up key
+ if (this.getKeyboardEvent(e).keyCode === 38) {
+ return;
+ }
+ e.preventDefault();
+ this.$.fileCursor.previous();
+ this.selectedIndex = this.$.fileCursor.index;
+ }
+ }
+
+ _handleNewComment(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+ e.preventDefault();
+ this.$.diffCursor.createCommentInPlace();
+ }
+
+ _handleOpenLastFile(e: CustomKeyboardEvent) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (
+ this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey
+ ) {
+ return;
+ }
+
+ e.preventDefault();
+ this._openSelectedFile(this._files.length - 1);
+ }
+
+ _handleOpenFirstFile(e: CustomKeyboardEvent) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (
+ this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey
+ ) {
+ return;
+ }
+
+ e.preventDefault();
+ this._openSelectedFile(0);
+ }
+
+ _handleOpenFile(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+ e.preventDefault();
+
+ if (this._showInlineDiffs) {
+ this._openCursorFile();
+ return;
+ }
+
+ this._openSelectedFile();
+ }
+
+ _handleNextChunk(e: CustomKeyboardEvent) {
+ if (
+ this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) &&
+ !this.isModifierPressed(e, Modifier.SHIFT_KEY)) ||
+ this._noDiffsExpanded()
+ ) {
+ return;
+ }
+
+ e.preventDefault();
+ if (this.isModifierPressed(e, Modifier.SHIFT_KEY)) {
+ this.$.diffCursor.moveToNextCommentThread();
+ } else {
+ this.$.diffCursor.moveToNextChunk();
+ }
+ }
+
+ _handlePrevChunk(e: CustomKeyboardEvent) {
+ if (
+ this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) &&
+ !this.isModifierPressed(e, Modifier.SHIFT_KEY)) ||
+ this._noDiffsExpanded()
+ ) {
+ return;
+ }
+
+ e.preventDefault();
+ if (this.isModifierPressed(e, Modifier.SHIFT_KEY)) {
+ this.$.diffCursor.moveToPreviousCommentThread();
+ } else {
+ this.$.diffCursor.moveToPreviousChunk();
+ }
+ }
+
+ _handleToggleFileReviewed(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ if (!this._files[this.$.fileCursor.index]) {
+ return;
+ }
+ this._reviewFile(this._files[this.$.fileCursor.index].__path);
+ }
+
+ _handleToggleLeftPane(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this._forEachDiff(diff => {
+ diff.toggleLeftDiff();
+ });
+ }
+
+ _toggleInlineDiffs() {
+ if (this._showInlineDiffs) {
+ this.collapseAllDiffs();
+ } else {
+ this.expandAllDiffs();
+ }
+ }
+
+ _openCursorFile() {
+ const diff = this.$.diffCursor.getTargetDiffElement();
+ if (
+ !this.change ||
+ !diff ||
+ !this.patchRange ||
+ !diff.path ||
+ !diff.patchRange
+ ) {
+ throw new Error('change, diff and patchRange must be all set and valid');
+ }
+ GerritNav.navigateToDiff(
+ this.change,
+ diff.path,
+ diff.patchRange.patchNum,
+ this.patchRange.basePatchNum
+ );
+ }
+
+ _openSelectedFile(index?: number) {
+ if (index !== undefined) {
+ this.$.fileCursor.setCursorAtIndex(index);
+ }
+ if (!this._files[this.$.fileCursor.index]) {
+ return;
+ }
+ if (!this.change || !this.patchRange) {
+ throw new Error('change and patchRange must be set');
+ }
+ GerritNav.navigateToDiff(
+ this.change,
+ this._files[this.$.fileCursor.index].__path,
+ this.patchRange.patchNum,
+ this.patchRange.basePatchNum
+ );
+ }
+
+ _addDraftAtTarget() {
+ const diff = this.$.diffCursor.getTargetDiffElement();
+ const target = this.$.diffCursor.getTargetLineElement();
+ if (diff && target) {
+ diff.addDraftAtLine(target);
+ }
+ }
+
+ _shouldHideChangeTotals(_patchChange: PatchChange): boolean {
+ return _patchChange.inserted === 0 && _patchChange.deleted === 0;
+ }
+
+ _shouldHideBinaryChangeTotals(_patchChange: PatchChange) {
+ return (
+ _patchChange.size_delta_inserted === 0 &&
+ _patchChange.size_delta_deleted === 0
+ );
+ }
+
+ _computeFileStatus(
+ status?: keyof typeof FileStatus
+ ): keyof typeof FileStatus {
+ return status || 'M';
+ }
+
+ _computeDiffURL(
+ change?: ParsedChangeInfo,
+ patchRange?: PatchRange,
+ path?: string,
+ editMode?: boolean
+ ) {
+ // Polymer 2: check for undefined
+ if (
+ change === undefined ||
+ patchRange === undefined ||
+ path === undefined ||
+ editMode === undefined
+ ) {
+ return;
+ }
+ if (editMode && path !== SpecialFilePath.MERGE_LIST) {
+ return GerritNav.getEditUrlForDiff(change, path, patchRange.patchNum);
+ }
+ return GerritNav.getUrlForDiff(
+ change,
+ path,
+ patchRange.patchNum,
+ patchRange.basePatchNum
+ );
+ }
+
+ _formatBytes(bytes?: number) {
+ if (!bytes) return '+/-0 B';
+ const bits = 1024;
+ const decimals = 1;
+ const sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+ const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits));
+ const prepend = bytes > 0 ? '+' : '';
+ const value = parseFloat(
+ (bytes / Math.pow(bits, exponent)).toFixed(decimals)
+ );
+ return `${prepend}${value} ${sizes[exponent]}`;
+ }
+
+ _formatPercentage(size?: number, delta?: number) {
+ if (size === undefined || delta === undefined) {
+ return '';
+ }
+ const oldSize = size - delta;
+
+ if (oldSize === 0) {
+ return '';
+ }
+
+ const percentage = Math.round(Math.abs((delta * 100) / oldSize));
+ return `(${delta > 0 ? '+' : '-'}${percentage}%)`;
+ }
+
+ _computeBinaryClass(delta?: number) {
+ if (!delta) {
+ return;
+ }
+ return delta > 0 ? 'added' : 'removed';
+ }
+
+ _computeClass(baseClass?: string, path?: string) {
+ const classes = [];
+ if (baseClass) {
+ classes.push(baseClass);
+ }
+ if (
+ path === SpecialFilePath.COMMIT_MESSAGE ||
+ path === SpecialFilePath.MERGE_LIST
+ ) {
+ classes.push('invisible');
+ }
+ return classes.join(' ');
+ }
+
+ _computeStatusClass(file?: NormalizedFileInfo) {
+ if (!file) return '';
+ const classStr = this._computeClass('status', file.__path);
+ return `${classStr} ${this._computeFileStatus(file.status)}`;
+ }
+
+ _computePathClass(
+ path: string | undefined,
+ expandedFilesRecord: ElementPropertyDeepChange<GrFileList, '_expandedFiles'>
+ ) {
+ return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
+ }
+
+ _computeShowHideIcon(
+ path: string | undefined,
+ expandedFilesRecord: ElementPropertyDeepChange<GrFileList, '_expandedFiles'>
+ ) {
+ return this._isFileExpanded(path, expandedFilesRecord)
+ ? 'gr-icons:expand-less'
+ : 'gr-icons:expand-more';
+ }
+
+ @observe(
+ '_filesByPath',
+ 'changeComments',
+ 'patchRange',
+ '_reviewed',
+ '_loading'
+ )
+ _computeFiles(
+ filesByPath?: FileNameToFileInfoMap,
+ changeComments?: ChangeComments,
+ patchRange?: PatchRange,
+ reviewed?: string[],
+ loading?: boolean
+ ) {
+ // Polymer 2: check for undefined
+ if (
+ filesByPath === undefined ||
+ changeComments === undefined ||
+ patchRange === undefined ||
+ reviewed === undefined ||
+ loading === undefined
+ ) {
+ return;
+ }
+
+ // Await all promises resolving from reload. @See Issue 9057
+ if (loading || !changeComments) {
+ return;
+ }
+
+ const commentedPaths = changeComments.getPaths(patchRange);
+ const files: FileNameToReviewedFileInfoMap = {...filesByPath};
+ addUnmodifiedFiles(files, commentedPaths);
+ const reviewedSet = new Set(reviewed || []);
+ for (const filePath in files) {
+ if (!hasOwnProperty(files, filePath)) {
+ continue;
+ }
+ files[filePath].isReviewed = reviewedSet.has(filePath);
+ }
+
+ this._files = this._normalizeChangeFilesResponse(files);
+ }
+
+ _computeFilesShown(
+ numFilesShown: number,
+ files: NormalizedFileInfo[]
+ ): NormalizedFileInfo[] | undefined {
+ // Polymer 2: check for undefined
+ if (numFilesShown === undefined || files === undefined) return undefined;
+
+ const previousNumFilesShown = this._shownFiles
+ ? this._shownFiles.length
+ : 0;
+
+ const filesShown = files.slice(0, numFilesShown);
+ this.dispatchEvent(
+ new CustomEvent('files-shown-changed', {
+ detail: {length: filesShown.length},
+ composed: true,
+ bubbles: true,
+ })
+ );
+
+ // Start the timer for the rendering work hwere because this is where the
+ // _shownFiles property is being set, and _shownFiles is used in the
+ // dom-repeat binding.
+ this.reporting.time(RENDER_TIMING_LABEL);
+
+ // How many more files are being shown (if it's an increase).
+ this._reportinShownFilesIncrement = Math.max(
+ 0,
+ filesShown.length - previousNumFilesShown
+ );
+
+ return filesShown;
+ }
+
+ _updateDiffCursor() {
+ // Overwrite the cursor's list of diffs:
+ this.$.diffCursor.splice(
+ 'diffs',
+ 0,
+ this.$.diffCursor.diffs.length,
+ ...this.diffs
+ );
+ }
+
+ _filesChanged() {
+ if (this._files && this._files.length > 0) {
+ flush();
+ this.$.fileCursor.stops = Array.from(
+ this.root!.querySelectorAll(`.${FILE_ROW_CLASS}`)
+ );
+ this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
+ }
+ }
+
+ _incrementNumFilesShown() {
+ this.numFilesShown += this.fileListIncrement;
+ }
+
+ _computeFileListControlClass(
+ numFilesShown?: number,
+ files?: NormalizedFileInfo[]
+ ) {
+ if (numFilesShown === undefined || files === undefined) return 'invisible';
+ return numFilesShown >= files.length ? 'invisible' : '';
+ }
+
+ _computeIncrementText(numFilesShown?: number, files?: NormalizedFileInfo[]) {
+ if (numFilesShown === undefined || files === undefined) return '';
+ const text = Math.min(this.fileListIncrement, files.length - numFilesShown);
+ return `Show ${text} more`;
+ }
+
+ _computeShowAllText(files: NormalizedFileInfo[]) {
+ if (!files) {
+ return '';
+ }
+ return `Show all ${files.length} files`;
+ }
+
+ _computeWarnShowAll(files: NormalizedFileInfo[]) {
+ return files.length > WARN_SHOW_ALL_THRESHOLD;
+ }
+
+ _computeShowAllWarning(files: NormalizedFileInfo[]) {
+ if (!this._computeWarnShowAll(files)) {
+ return '';
+ }
+ return `Warning: showing all ${files.length} files may take several seconds.`;
+ }
+
+ _showAllFiles() {
+ this.numFilesShown = this._files.length;
+ }
+
+ /**
+ * Get a descriptive label for use in the status indicator's tooltip and
+ * ARIA label.
+ */
+ _computeFileStatusLabel(status?: keyof typeof FileStatus) {
+ const statusCode = this._computeFileStatus(status);
+ return hasOwnProperty(FileStatus, statusCode)
+ ? FileStatus[statusCode]
+ : 'Status Unknown';
+ }
+
+ /**
+ * Converts any boolean-like variable to the string 'true' or 'false'
+ *
+ * This method is useful when you bind aria-checked attribute to a boolean
+ * value. The aria-checked attribute is string attribute. Binding directly
+ * to boolean variable causes problem on gerrit-CI.
+ *
+ * @return 'true' if val is true-like, otherwise false
+ */
+ _booleanToString(val?: unknown) {
+ return val ? 'true' : 'false';
+ }
+
+ _isFileExpanded(
+ path: string | undefined,
+ expandedFilesRecord: ElementPropertyDeepChange<GrFileList, '_expandedFiles'>
+ ) {
+ return expandedFilesRecord.base.some(f => f.path === path);
+ }
+
+ _isFileExpandedStr(
+ path: string | undefined,
+ expandedFilesRecord: ElementPropertyDeepChange<GrFileList, '_expandedFiles'>
+ ) {
+ return this._booleanToString(
+ this._isFileExpanded(path, expandedFilesRecord)
+ );
+ }
+
+ private _computeExpandedFiles(
+ expandedCount: number,
+ totalCount: number
+ ): FilesExpandedState {
+ if (expandedCount === 0) {
+ return FilesExpandedState.NONE;
+ } else if (expandedCount === totalCount) {
+ return FilesExpandedState.ALL;
+ }
+ return FilesExpandedState.SOME;
+ }
+
+ /**
+ * Handle splices to the list of expanded file paths. If there are any new
+ * entries in the expanded list, then render each diff corresponding in
+ * order by waiting for the previous diff to finish before starting the next
+ * one.
+ *
+ * @param record The splice record in the expanded paths list.
+ */
+ @observe('_expandedFiles.splices')
+ _expandedFilesChanged(record?: PolymerSpliceChange<PatchSetFile[]>) {
+ // Clear content for any diffs that are not open so if they get re-opened
+ // the stale content does not flash before it is cleared and reloaded.
+ const collapsedDiffs = this.diffs.filter(
+ diff => this._expandedFiles.findIndex(f => f.path === diff.path) === -1
+ );
+ this._clearCollapsedDiffs(collapsedDiffs);
+
+ if (!record) {
+ return;
+ } // Happens after "Collapse all" clicked.
+
+ this.filesExpanded = this._computeExpandedFiles(
+ this._expandedFiles.length,
+ this._files.length
+ );
+
+ // Find the paths introduced by the new index splices:
+ const newFiles = record.indexSplices
+ .map(splice =>
+ splice.object.slice(splice.index, splice.index + splice.addedCount)
+ )
+ .reduce((acc, paths) => acc.concat(paths), []);
+
+ // Required so that the newly created diff view is included in this.diffs.
+ flush();
+
+ this.reporting.time(EXPAND_ALL_TIMING_LABEL);
+
+ if (newFiles.length) {
+ this._renderInOrder(newFiles, this.diffs, newFiles.length);
+ }
+
+ this._updateDiffCursor();
+ this.$.diffCursor.reInitAndUpdateStops();
+ }
+
+ private _clearCollapsedDiffs(collapsedDiffs: GrDiffHost[]) {
+ for (const diff of collapsedDiffs) {
+ diff.cancel();
+ diff.clearDiffContent();
+ }
+ }
+
+ /**
+ * Given an array of paths and a NodeList of diff elements, render the diff
+ * for each path in order, awaiting the previous render to complete before
+ * continuing.
+ *
+ * @param initialCount The total number of paths in the pass. This
+ * is used to generate log messages.
+ */
+ private _renderInOrder(
+ files: PatchSetFile[],
+ diffElements: GrDiffHost[],
+ initialCount: number
+ ) {
+ let iter = 0;
+
+ for (const file of files) {
+ const path = file.path;
+ const diffElem = this._findDiffByPath(path, diffElements);
+ if (diffElem) {
+ diffElem.prefetchDiff();
+ }
+ }
+
+ return new Promise(resolve => {
+ this.dispatchEvent(
+ new CustomEvent('reload-drafts', {
+ detail: {resolve},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }).then(() =>
+ asyncForeach(files, (file, cancel) => {
+ const path = file.path;
+ this._cancelForEachDiff = cancel;
+
+ iter++;
+ console.info('Expanding diff', iter, 'of', initialCount, ':', path);
+ const diffElem = this._findDiffByPath(path, diffElements);
+ if (!diffElem) {
+ console.warn(`Did not find <gr-diff-host> element for ${path}`);
+ return Promise.resolve();
+ }
+ if (!this.changeComments || !this.patchRange || !this.diffPrefs) {
+ throw new Error(
+ 'changeComments, patchRange and diffPrefs must be set'
+ );
+ }
+ diffElem.comments = this.changeComments.getCommentsBySideForFile(
+ file,
+ this.patchRange,
+ this.projectConfig
+ );
+ const promises: Array<Promise<unknown>> = [diffElem.reload()];
+ if (this._loggedIn && !this.diffPrefs.manual_review) {
+ promises.push(this._reviewFile(path, true));
+ }
+ return Promise.all(promises);
+ }).then(() => {
+ this._cancelForEachDiff = undefined;
+ console.info('Finished expanding', initialCount, 'diff(s)');
+ this.reporting.timeEndWithAverage(
+ EXPAND_ALL_TIMING_LABEL,
+ EXPAND_ALL_AVG_TIMING_LABEL,
+ initialCount
+ );
+ /* Block diff cursor from auto scrolling after files are done rendering.
+ * This prevents the bug where the screen jumps to the first diff chunk
+ * after files are done being rendered after the user has already begun
+ * scrolling.
+ * This also however results in the fact that the cursor does not auto
+ * focus on the first diff chunk on a small screen. This is however, a use
+ * case we are willing to not support for now.
+
+ * Using handleDiffUpdate resulted in diffCursor.row being set which
+ * prevented the issue of scrolling to top when we expand the second
+ * file individually.
+ */
+ this.$.diffCursor.reInitAndUpdateStops();
+ })
+ );
+ }
+
+ /** Cancel the rendering work of every diff in the list */
+ _cancelDiffs() {
+ if (this._cancelForEachDiff) {
+ this._cancelForEachDiff();
+ }
+ this._forEachDiff(d => d.cancel());
+ }
+
+ /**
+ * In the given NodeList of diff elements, find the diff for the given path.
+ */
+ private _findDiffByPath(path: string, diffElements: GrDiffHost[]) {
+ for (let i = 0; i < diffElements.length; i++) {
+ if (diffElements[i].path === path) {
+ return diffElements[i];
+ }
+ }
+ return undefined;
+ }
+
+ /**
+ * Reset the comments of a modified thread
+ */
+ reloadCommentsForThreadWithRootId(rootId: UrlEncodedCommentId, path: string) {
+ // Don't bother continuing if we already know that the path that contains
+ // the updated comment thread is not expanded.
+ if (!this._expandedFiles.some(f => f.path === path)) {
+ return;
+ }
+ const diff = this.diffs.find(d => d.path === path);
+
+ if (!diff) {
+ throw new Error("Can't find diff by path");
+ }
+
+ const threadEl = diff.getThreadEls().find(t => t.rootId === rootId);
+ if (!threadEl) {
+ return;
+ }
+
+ if (!this.changeComments) {
+ throw new Error('changeComments must be set');
+ }
+
+ const newComments = this.changeComments.getCommentsForThread(rootId);
+
+ // If newComments is null, it means that a single draft was
+ // removed from a thread in the thread view, and the thread should
+ // no longer exist. Remove the existing thread element in the diff
+ // view.
+ if (!newComments) {
+ threadEl.fireRemoveSelf();
+ return;
+ }
+
+ // Comments are not returned with the commentSide attribute from
+ // the api, but it's necessary to be stored on the diff's
+ // comments due to use in the _handleCommentUpdate function.
+ // The comment thread already has a side associated with it, so
+ // set the comment's side to match.
+ threadEl.comments = newComments.map(c =>
+ Object.assign(c, {__commentSide: threadEl.commentSide})
+ );
+ flush();
+ }
+
+ _handleEscKey(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+ e.preventDefault();
+ this._displayLine = false;
+ }
+
+ /**
+ * Update the loading class for the file list rows. The update is inside a
+ * debouncer so that the file list doesn't flash gray when the API requests
+ * are reasonably fast.
+ */
+ _loadingChanged(loading?: boolean) {
+ this.debounce(
+ 'loading-change',
+ () => {
+ // Only show set the loading if there have been files loaded to show. In
+ // this way, the gray loading style is not shown on initial loads.
+ this.classList.toggle('loading', loading && !!this._files.length);
+ },
+ LOADING_DEBOUNCE_INTERVAL
+ );
+ }
+
+ _editModeChanged(editMode?: boolean) {
+ this.classList.toggle('editMode', editMode);
+ }
+
+ _computeReviewedClass(isReviewed?: boolean) {
+ return isReviewed ? 'isReviewed' : '';
+ }
+
+ _computeReviewedText(isReviewed?: boolean) {
+ return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED';
+ }
+
+ /**
+ * Given a file path, return whether that path should have visible size bars
+ * and be included in the size bars calculation.
+ */
+ _showBarsForPath(path?: string) {
+ return (
+ path !== SpecialFilePath.COMMIT_MESSAGE &&
+ path !== SpecialFilePath.MERGE_LIST
+ );
+ }
+
+ /**
+ * Compute size bar layout values from the file list.
+ */
+ _computeSizeBarLayout(
+ shownFilesRecord?: ElementPropertyDeepChange<GrFileList, '_shownFiles'>
+ ) {
+ const stats: SizeBarLayout = createDefaultSizeBarLayout();
+ if (!shownFilesRecord || !shownFilesRecord.base) {
+ return stats;
+ }
+ shownFilesRecord.base
+ .filter(f => this._showBarsForPath(f.__path))
+ .forEach(f => {
+ if (f.lines_inserted) {
+ stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted);
+ }
+ if (f.lines_deleted) {
+ stats.maxDeleted = Math.max(stats.maxDeleted, f.lines_deleted);
+ }
+ });
+ const ratio = stats.maxInserted / (stats.maxInserted + stats.maxDeleted);
+ if (!isNaN(ratio)) {
+ stats.maxAdditionWidth =
+ (SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio;
+ stats.maxDeletionWidth =
+ SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth;
+ stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH;
+ }
+ return stats;
+ }
+
+ /**
+ * Get the width of the addition bar for a file.
+ */
+ _computeBarAdditionWidth(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
+ if (
+ !file ||
+ !stats ||
+ stats.maxInserted === 0 ||
+ !file.lines_inserted ||
+ !this._showBarsForPath(file.__path)
+ ) {
+ return 0;
+ }
+ const width =
+ (stats.maxAdditionWidth * file.lines_inserted) / stats.maxInserted;
+ return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
+ }
+
+ /**
+ * Get the x-offset of the addition bar for a file.
+ */
+ _computeBarAdditionX(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
+ if (!file || !stats) return;
+ return stats.maxAdditionWidth - this._computeBarAdditionWidth(file, stats);
+ }
+
+ /**
+ * Get the width of the deletion bar for a file.
+ */
+ _computeBarDeletionWidth(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
+ if (
+ !file ||
+ !stats ||
+ stats.maxDeleted === 0 ||
+ !file.lines_deleted ||
+ !this._showBarsForPath(file.__path)
+ ) {
+ return 0;
+ }
+ const width =
+ (stats.maxDeletionWidth * file.lines_deleted) / stats.maxDeleted;
+ return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
+ }
+
+ /**
+ * Get the x-offset of the deletion bar for a file.
+ */
+ _computeBarDeletionX(stats: SizeBarLayout) {
+ return stats.deletionOffset;
+ }
+
+ _computeShowSizeBars(userPrefs?: PreferencesInfo) {
+ return !!userPrefs?.size_bar_in_change_table;
+ }
+
+ _computeSizeBarsClass(showSizeBars?: boolean, path?: string) {
+ let hideClass = '';
+ if (!showSizeBars) {
+ hideClass = 'hide';
+ } else if (!this._showBarsForPath(path)) {
+ hideClass = 'invisible';
+ }
+ return `sizeBars desktop ${hideClass}`;
+ }
+
+ /**
+ * Shows registered dynamic columns iff the 'header', 'content' and
+ * 'summary' endpoints are registered the exact same number of times.
+ * Ideally, there should be a better way to enforce the expectation of the
+ * dependencies between dynamic endpoints.
+ */
+ _computeShowDynamicColumns(
+ headerEndpoints?: string,
+ contentEndpoints?: string,
+ summaryEndpoints?: string
+ ) {
+ return (
+ headerEndpoints &&
+ contentEndpoints &&
+ summaryEndpoints &&
+ headerEndpoints.length &&
+ headerEndpoints.length === contentEndpoints.length &&
+ headerEndpoints.length === summaryEndpoints.length
+ );
+ }
+
+ /**
+ * Shows registered dynamic prepended columns iff the 'header', 'content'
+ * endpoints are registered the exact same number of times.
+ */
+ _computeShowPrependedDynamicColumns(
+ headerEndpoints?: string,
+ contentEndpoints?: string
+ ) {
+ return (
+ headerEndpoints &&
+ contentEndpoints &&
+ headerEndpoints.length &&
+ headerEndpoints.length === contentEndpoints.length
+ );
+ }
+
+ /**
+ * Returns true if none of the inline diffs have been expanded.
+ */
+ _noDiffsExpanded() {
+ return this.filesExpanded === FilesExpandedState.NONE;
+ }
+
+ /**
+ * Method to call via binding when each file list row is rendered. This
+ * allows approximate detection of when the dom-repeat has completed
+ * rendering.
+ *
+ * @param index The index of the row being rendered.
+ */
+ _reportRenderedRow(index: number) {
+ if (index === this._shownFiles.length - 1) {
+ this.async(() => {
+ this.reporting.timeEndWithAverage(
+ RENDER_TIMING_LABEL,
+ RENDER_AVG_TIMING_LABEL,
+ this._reportinShownFilesIncrement
+ );
+ }, 1);
+ }
+ return '';
+ }
+
+ _reviewedTitle(reviewed?: boolean) {
+ if (reviewed) {
+ return 'Mark as not reviewed (shortcut: r)';
+ }
+
+ return 'Mark as reviewed (shortcut: r)';
+ }
+
+ _handleReloadingDiffPreference() {
+ this._getDiffPreferences().then(prefs => {
+ this.diffPrefs = prefs;
+ });
+ }
+
+ /**
+ * Wrapper for using in the element template and computed properties
+ */
+ _computeDisplayPath(path: string) {
+ return computeDisplayPath(path);
+ }
+
+ /**
+ * Wrapper for using in the element template and computed properties
+ */
+ _computeTruncatedPath(path: string) {
+ return computeTruncatedPath(path);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-file-list': GrFileList;
+ }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
index a577eb6..1c30a65 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
@@ -375,7 +375,7 @@
<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 @@
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]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 8aebf9b..129b65e 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -312,7 +312,7 @@
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('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},
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
deleted file mode 100644
index 88c4cef..0000000
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ /dev/null
@@ -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);
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
new file mode 100644
index 0000000..5ad0d92
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -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;
+ }
+}
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.js
index 48194a6..e4db65f 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.js
@@ -103,22 +103,22 @@
// 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 @@
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 @@
}],
}];
assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
/* userLinks= */ [],
adminLinks,
topMenus,
- /* baseDocUrl= */ ''
+ /* baseDocUrl= */ '',
+ /* defaultLinks= */ []
), [{
title: 'Browse',
links: adminLinks,
@@ -208,11 +207,11 @@
}],
}];
assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
/* userLinks= */ [],
adminLinks,
topMenus,
- /* baseDocUrl= */ ''
+ /* baseDocUrl= */ '',
+ /* defaultLinks= */ []
), [{
title: 'Browse',
links: adminLinks,
@@ -247,11 +246,11 @@
}],
}];
assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
/* userLinks= */ [],
adminLinks,
topMenus,
- /* baseDocUrl= */ ''
+ /* baseDocUrl= */ '',
+ /* defaultLinks= */ []
), [{
title: 'Browse',
links: adminLinks,
@@ -284,11 +283,11 @@
}],
}];
assert.deepEqual(element._computeLinks(
- defaultLinks,
/* userLinks= */ [],
/* adminLinks= */ [],
topMenus,
- /* baseDocUrl= */ ''
+ /* baseDocUrl= */ '',
+ defaultLinks
), [{
title: 'Faves',
links: defaultLinks[0].links.concat([{
@@ -315,11 +314,11 @@
}],
}];
assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
userLinks,
/* adminLinks= */ [],
topMenus,
- /* baseDocUrl= */ ''
+ /* baseDocUrl= */ '',
+ /* defaultLinks= */ []
), [{
title: 'Your',
links: userLinks.concat([{
@@ -346,11 +345,11 @@
}],
}];
assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
/* userLinks= */ [],
adminLinks,
topMenus,
- /* baseDocUrl= */ ''
+ /* baseDocUrl= */ '',
+ /* defaultLinks= */ []
), [{
title: 'Browse',
links: adminLinks.concat([{
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 0bb334a..8470611 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -32,6 +32,7 @@
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 @@
* @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 @@
},
getEditUrlForDiff(
- change: ChangeInfo,
+ change: ChangeInfo | ParsedChangeInfo,
filePath: string,
patchNum?: PatchSetNum,
lineNum?: number
@@ -763,7 +764,7 @@
* @param basePatchNum The string 'PARENT' can be used for none.
*/
navigateToDiff(
- change: ChangeInfo,
+ change: ChangeInfo | ParsedChangeInfo,
filePath: string,
patchNum?: PatchSetNum,
basePatchNum?: PatchSetNum,
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index acc2cbd..180e4a7 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -124,11 +124,15 @@
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 @@
} 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 @@
if (predefinedOpOnlyQuery) {
return;
}
+ const detail: SearchBarHandleSearchDetail = {
+ inputVal: this._inputVal,
+ };
this.dispatchEvent(
new CustomEvent('handle-search', {
- detail: {inputVal: this._inputVal},
+ detail,
})
);
}
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
deleted file mode 100644
index 813298c..0000000
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ /dev/null
@@ -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);
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
new file mode 100644
index 0000000..a818c59
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
@@ -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;
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 4bfa6a9..fa31d09 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -1212,7 +1212,7 @@
}
_computeModeSelectHideClass(_diff) {
- return _diff.binary ? 'hide' : '';
+ return (!_diff || _diff.binary) ? 'hide' : '';
}
_onLineSelected(e, detail) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index de283d8..a9df3f9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -143,7 +143,7 @@
notify: true,
computed: '_computeRootId(comments.*)',
})
- rootId?: string;
+ rootId?: UrlEncodedCommentId;
@property({type: Boolean})
showFilePath = false;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 26bf50a..8c26d4a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -26,7 +26,7 @@
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 @@
);
}
- 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());
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index 505e62e..261298b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -18,6 +18,7 @@
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 @@
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.
}
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index d1459a3..15cdac4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -138,6 +138,7 @@
RevisionId,
GroupName,
Hashtag,
+ TopMenuEntryInfo,
} from '../../../types/common';
import {
CancelConditionCallback,
@@ -1603,7 +1604,10 @@
}) 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 @@
}) 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 @@
}) 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(
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index 80df630..b255ea5 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -1082,7 +1082,7 @@
_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;
diff --git a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
index 3082aab..6b93082 100644
--- a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
@@ -96,6 +96,8 @@
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 @@
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>;
}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index ed36412..5bcf0b8 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -43,6 +43,7 @@
DraftsAction,
NotifyType,
EmailFormat,
+ AuthType,
} from '../constants/constants';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
@@ -743,10 +744,10 @@
/**
* 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 @@
/**
* 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;
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index 7e63f70..119b09b 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -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();
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index ae6d616..76db40b 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -211,13 +211,13 @@
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;