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