Files
gerrit/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
Dmitrii Filippov b82003c49a Update eslint version and eslint rules
Legacy indent rules doesn't handle all cases. As a result there are
different indents in .js files. This commit update eslint rules and add
autofix for incorrect indents. It is expected that fix should be run
after converting to class-based elements.

Change-Id: I9d37a3d4319e2af71ddb93100a6791b8ddb7de79
2019-11-06 13:00:04 +01:00

1331 lines
41 KiB
JavaScript

/**
* @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.
*/
(function() {
'use strict';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
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',
};
Polymer({
is: 'gr-file-list',
/**
* Fired when a draft refresh should get triggered
*
* @event reload-drafts
*/
properties: {
/** @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: GrFileListConstants.FilesExpandedState.NONE,
notify: true,
},
_filesByPath: Object,
_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,
_expandedFilePaths: {
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)',
},
/** @type {Array<string>} */
_dynamicHeaderEndpoints: {
type: Array,
},
/** @type {Array<string>} */
_dynamicContentEndpoints: {
type: Array,
},
/** @type {Array<string>} */
_dynamicSummaryEndpoints: {
type: Array,
},
},
behaviors: [
Gerrit.AsyncForeachBehavior,
Gerrit.DomUtilBehavior,
Gerrit.FireBehavior,
Gerrit.KeyboardShortcutBehavior,
Gerrit.PatchSetBehavior,
Gerrit.PathListBehavior,
],
observers: [
'_expandedPathsChanged(_expandedFilePaths.splices)',
'_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
'_loading)',
],
keyBindings: {
esc: '_handleEscKey',
},
keyboardShortcuts() {
return {
[this.Shortcut.LEFT_PANE]: '_handleLeftPane',
[this.Shortcut.RIGHT_PANE]: '_handleRightPane',
[this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
[this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
[this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
[this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
[this.Shortcut.NEXT_LINE]: '_handleCursorNext',
[this.Shortcut.PREV_LINE]: '_handleCursorPrev',
[this.Shortcut.NEW_COMMENT]: '_handleNewComment',
[this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
[this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
[this.Shortcut.OPEN_FILE]: '_handleOpenFile',
[this.Shortcut.NEXT_CHUNK]: '_handleNextChunk',
[this.Shortcut.PREV_CHUNK]: '_handlePrevChunk',
[this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
[this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
// Final two are actually handled by gr-comment-thread.
[this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
[this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
};
},
listeners: {
keydown: '_scopedKeydownHandler',
},
attached() {
Gerrit.awaitPluginsLoaded().then(() => {
this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
'change-view-file-list-header');
this._dynamicContentEndpoints = Gerrit._endpoints.getDynamicEndpoints(
'change-view-file-list-content');
this._dynamicSummaryEndpoints = Gerrit._endpoints.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._dynamicHeaderEndpoints.length !==
this._dynamicSummaryEndpoints.length) {
console.warn(
'Different number of dynamic file-list headers and summary.');
}
});
},
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 => {
return 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() {
return Array.from(
Polymer.dom(this.root).querySelectorAll('gr-diff-host'));
},
openDiffPrefs() {
this.$.diffPreferencesDialog.open();
},
_calculatePatchChange(files) {
const magicFilesExcluded = files.filter(files => {
return files.__path !== '/COMMIT_MSG' && files.__path !== '/MERGE_LIST';
});
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();
},
_togglePathExpanded(path) {
// Is the path in the list of expanded diffs? IF so remove it, otherwise
// add it to the list.
const pathIndex = this._expandedFilePaths.indexOf(path);
if (pathIndex === -1) {
this.push('_expandedFilePaths', path);
} else {
this.splice('_expandedFilePaths', pathIndex, 1);
}
},
_togglePathExpandedByIndex(index) {
this._togglePathExpanded(this._files[index].__path);
},
_updateDiffPreferences() {
if (!this.diffs.length) { return; }
// Re-render all expanded diffs sequentially.
this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
this._renderInOrder(this._expandedFilePaths, this.diffs,
this._expandedFilePaths.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 newPaths = [];
let path;
for (let i = 0; i < this._shownFiles.length; i++) {
path = this._shownFiles[i].__path;
if (!this._expandedFilePaths.includes(path)) {
newPaths.push(path);
}
}
this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
},
collapseAllDiffs() {
this._showInlineDiffs = false;
this._expandedFilePaths = [];
this.filesExpanded = this._computeExpandedFiles(
this._expandedFilePaths.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) {
const unresolvedCount =
changeComments.computeUnresolvedNum(patchRange.basePatchNum, path) +
changeComments.computeUnresolvedNum(patchRange.patchNum, path);
const commentCount =
changeComments.computeCommentCount(patchRange.basePatchNum, path) +
changeComments.computeCommentCount(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) {
const draftCount =
changeComments.computeDraftCount(patchRange.basePatchNum, path) +
changeComments.computeDraftCount(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) {
const draftCount =
changeComments.computeDraftCount(patchRange.basePatchNum, path) +
changeComments.computeDraftCount(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) {
const commentCount =
changeComments.computeCommentCount(patchRange.basePatchNum, path) +
changeComments.computeCommentCount(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);
},
/**
* The closure compiler doesn't realize this.specialFilePathCompare is
* valid.
* @suppress {checkTypes}
*/
_normalizeChangeFilesResponse(response) {
if (!response) { return []; }
const paths = Object.keys(response).sort(this.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;
files.push(info);
}
return files;
},
/**
* 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) {
// Traverse upwards to find the row element if the target is not the row.
let row = e.target;
while (!row.classList.contains('row') && row.parentElement) {
row = row.parentElement;
}
const path = row.dataset.path;
// Handle checkbox mark as reviewed.
if (e.target.classList.contains('markReviewed')) {
e.preventDefault();
return this._reviewFile(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 || this.descendedFromClass(e.target, 'pathLink')) { return; }
// Disregard the event if the click target is in the edit controls.
if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
e.preventDefault();
this._togglePathExpanded(path);
},
_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._togglePathExpandedByIndex(this.$.fileCursor.index);
},
_handleToggleAllInlineDiffs(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
e.preventDefault();
this._toggleInlineDiffs();
},
_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; }
const isRangeSelected = this.diffs.some(diff => {
return diff.isRangeSelected();
}, this);
if (!isRangeSelected) {
e.preventDefault();
this._addDraftAtTarget();
}
},
_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();
Gerrit.Nav.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; }
Gerrit.Nav.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, patchNum, basePatchNum, path, editMode) {
// Polymer 2: check for undefined
if ([change, patchNum, basePatchNum, path, editMode]
.some(arg => arg === undefined)) {
return;
}
// TODO(kaspern): Fix editing for commit messages and merge lists.
if (editMode && path !== this.COMMIT_MESSAGE_PATH &&
path !== this.MERGE_LIST_PATH) {
return Gerrit.Nav.getEditUrlForDiff(change, path, patchNum,
basePatchNum);
}
return Gerrit.Nav.getUrlForDiff(change, path, patchNum, 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 === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
classes.push('invisible');
}
return classes.join(' ');
},
_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,
].some(arg => arg === undefined)) {
return;
}
// Await all promises resolving from reload. @See Issue 9057
if (loading || !changeComments) { return; }
const commentedPaths = changeComments.getPaths(patchRange);
const files = Object.assign({}, filesByPath);
Object.keys(commentedPaths).forEach(commentedPath => {
if (files.hasOwnProperty(commentedPath)) { return; }
files[commentedPath] = {status: 'U'};
});
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].some(arg => arg === undefined)) {
return undefined;
}
const previousNumFilesShown = this._shownFiles ?
this._shownFiles.length : 0;
const filesShown = files.slice(0, numFilesShown);
this.fire('files-shown-changed', {length: filesShown.length});
// 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) {
Polymer.dom.flush();
const files = Array.from(
Polymer.dom(this.root).querySelectorAll('.file-row'));
this.$.fileCursor.stops = files;
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;
},
_computePatchSetDescription(revisions, patchNum) {
// Polymer 2: check for undefined
if ([revisions, patchNum].some(arg => arg === undefined)) {
return '';
}
const rev = this.getRevisionByPatchNum(revisions, patchNum);
return (rev && rev.description) ?
rev.description.substring(0, PATCH_DESC_MAX_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';
},
_isFileExpanded(path, expandedFilesRecord) {
return expandedFilesRecord.base.includes(path);
},
_onLineSelected(e, detail) {
this.$.diffCursor.moveToLineNumber(detail.number, detail.side,
detail.path);
},
_computeExpandedFiles(expandedCount, totalCount) {
if (expandedCount === 0) {
return GrFileListConstants.FilesExpandedState.NONE;
} else if (expandedCount === totalCount) {
return GrFileListConstants.FilesExpandedState.ALL;
}
return GrFileListConstants.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.
*/
_expandedPathsChanged(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._expandedFilePaths.indexOf(diff.path) === -1);
this._clearCollapsedDiffs(collapsedDiffs);
if (!record) { return; } // Happens after "Collapse all" clicked.
this.filesExpanded = this._computeExpandedFiles(
this._expandedFilePaths.length, this._files.length);
// Find the paths introduced by the new index splices:
const newPaths = 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.
Polymer.dom.flush();
this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
if (newPaths.length) {
this._renderInOrder(newPaths, this.diffs, newPaths.length);
}
this._updateDiffCursor();
this.$.diffCursor.handleDiffUpdate();
},
_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
* continung.
* @param {!Array<string>} paths
* @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(paths, diffElements, initialCount) {
let iter = 0;
return (new Promise(resolve => {
this.fire('reload-drafts', {resolve});
})).then(() => {
return this.asyncForeach(paths, (path, cancel) => {
this._cancelForEachDiff = cancel;
iter++;
console.log('Expanding diff', iter, 'of', initialCount, ':',
path);
const diffElem = this._findDiffByPath(path, diffElements);
diffElem.comments = this.changeComments.getCommentsBySideForPath(
path, 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.log('Finished expanding', initialCount, 'diff(s)');
this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
this.$.diffCursor.handleDiffUpdate();
});
});
},
/** 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._expandedFilePaths.includes(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 => {
return Object.assign(c, {__commentSide: threadEl.commentSide});
});
Polymer.dom.flush();
return;
},
_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 !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH;
},
/**
* 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 regiestered 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 === contentEndpoints.length &&
headerEndpoints.length === summaryEndpoints.length;
},
/**
* Returns true if none of the inline diffs have been expanded.
* @return {boolean}
*/
_noDiffsExpanded() {
return this.filesExpanded === GrFileListConstants.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;
});
},
});
})();