| // 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'; |
| |
| var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; |
| |
| var DiffViewMode = { |
| SIDE_BY_SIDE: 'SIDE_BY_SIDE', |
| UNIFIED: 'UNIFIED_DIFF', |
| }; |
| |
| Polymer({ |
| is: 'gr-file-list', |
| |
| properties: { |
| patchRange: Object, |
| patchNum: String, |
| changeNum: String, |
| comments: Object, |
| drafts: Object, |
| revisions: Object, |
| projectConfig: Object, |
| selectedIndex: { |
| type: Number, |
| notify: true, |
| }, |
| keyEventTarget: { |
| type: Object, |
| value: function() { return document.body; }, |
| }, |
| change: Object, |
| diffViewMode: { |
| type: String, |
| notify: true, |
| }, |
| _files: { |
| type: Array, |
| observer: '_filesChanged', |
| value: function() { return []; }, |
| }, |
| _loggedIn: { |
| type: Boolean, |
| value: false, |
| }, |
| _reviewed: { |
| type: Array, |
| value: function() { return []; }, |
| }, |
| _diffPrefs: Object, |
| _userPrefs: Object, |
| _localPrefs: Object, |
| _showInlineDiffs: Boolean, |
| _numFilesShown: { |
| type: Number, |
| value: 75, |
| }, |
| _patchChange: { |
| type: Object, |
| computed: '_calculatePatchChange(_files)', |
| }, |
| _fileListIncrement: { |
| type: Number, |
| readOnly: true, |
| value: 75, |
| }, |
| _hideChangeTotals: { |
| type: Boolean, |
| computed: '_shouldHideChangeTotals(_patchChange)', |
| }, |
| _shownFiles: { |
| type: Array, |
| computed: '_computeFilesShown(_numFilesShown, _files.*)', |
| }, |
| _diffMode: { |
| type: String, |
| computed: '_getDiffViewMode(diffViewMode, _userPrefs)', |
| }, |
| // Caps the number of files that can be shown and have the 'show diffs' / |
| // 'hide diffs' buttons still be functional. |
| _maxFilesForBulkActions: { |
| type: Number, |
| readOnly: true, |
| value: 225, |
| }, |
| }, |
| |
| behaviors: [ |
| Gerrit.KeyboardShortcutBehavior, |
| Gerrit.URLEncodingBehavior, |
| ], |
| |
| reload: function() { |
| if (!this.changeNum || !this.patchRange.patchNum) { |
| return Promise.resolve(); |
| } |
| this._collapseAllDiffs(); |
| var promises = []; |
| var _this = this; |
| |
| promises.push(this._getFiles().then(function(files) { |
| _this._files = files; |
| })); |
| promises.push(this._getLoggedIn().then(function(loggedIn) { |
| return _this._loggedIn = loggedIn; |
| }).then(function(loggedIn) { |
| if (!loggedIn) { return; } |
| |
| return _this._getReviewedFiles().then(function(reviewed) { |
| _this._reviewed = reviewed; |
| }); |
| })); |
| |
| this._localPrefs = this.$.storage.getPreferences(); |
| promises.push(this._getDiffPreferences().then(function(prefs) { |
| this._diffPrefs = prefs; |
| }.bind(this))); |
| |
| // Initialize with user's diff mode preference. Default to |
| // SIDE_BY_SIDE in the meantime. |
| var setDiffViewMode = this.diffViewMode === null; |
| if (setDiffViewMode) { |
| this.set('diffViewMode', DiffViewMode.SIDE_BY_SIDE); |
| } |
| promises.push(this._getPreferences().then(function(prefs) { |
| this._userPrefs = prefs; |
| if (setDiffViewMode) { |
| this.set('diffViewMode', prefs.diff_view); |
| } |
| }.bind(this))); |
| }, |
| |
| get diffs() { |
| return Polymer.dom(this.root).querySelectorAll('gr-diff'); |
| }, |
| |
| _calculatePatchChange: function(files) { |
| var filesNoCommitMsg = files.filter(function(files) { |
| return files.__path !== '/COMMIT_MSG'; |
| }); |
| |
| return filesNoCommitMsg.reduce(function(acc, obj) { |
| var inserted = obj.lines_inserted ? obj.lines_inserted : 0; |
| var deleted = obj.lines_deleted ? obj.lines_deleted : 0; |
| |
| return { |
| inserted: acc.inserted + inserted, |
| deleted: acc.deleted + deleted, |
| }; |
| }, {inserted: 0, deleted: 0}); |
| }, |
| |
| _getDiffPreferences: function() { |
| return this.$.restAPI.getDiffPreferences(); |
| }, |
| |
| _getPreferences: function() { |
| return this.$.restAPI.getPreferences(); |
| }, |
| |
| _computePatchSets: function(revisions) { |
| var patchNums = []; |
| for (var commit in revisions) { |
| patchNums.push(revisions[commit]._number); |
| } |
| return patchNums.sort(function(a, b) { return a - b; }); |
| }, |
| |
| _computePatchSetDisabled: function(patchNum, currentPatchNum) { |
| return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10); |
| }, |
| |
| _computePatchSetSelected: function(patchNum, basePatchNum) { |
| return parseInt(patchNum, 10) === parseInt(basePatchNum, 10); |
| }, |
| |
| _handleHiddenChange: function(e) { |
| var model = e.model; |
| model.set('file.__expanded', !model.file.__expanded); |
| }, |
| |
| _handlePatchChange: function(e) { |
| var patchRange = Object.assign({}, this.patchRange); |
| patchRange.basePatchNum = Polymer.dom(e).rootTarget.value; |
| page.show(this.encodeURL('/c/' + this.changeNum + '/' + |
| this._patchRangeStr(patchRange), true)); |
| }, |
| |
| _forEachDiff: function(fn) { |
| var diffs = this.diffs; |
| for (var i = 0; i < diffs.length; i++) { |
| fn(diffs[i]); |
| } |
| }, |
| |
| /** |
| * Until upgrading to Polymer 2.0, manual management of reflection between |
| * _shownFiles and _files is necessary. Performance of linkPaths is very |
| * poor. |
| */ |
| _expandAllDiffs: function(e) { |
| this._showInlineDiffs = true; |
| for (var i = 0; i < this._shownFiles.length; i++) { |
| this.set(['_shownFiles', i, '__expanded'], true); |
| this.set(['_files', i, '__expanded'], true); |
| } |
| if (e && e.target) { |
| e.target.blur(); |
| } |
| }, |
| |
| _collapseAllDiffs: function(e) { |
| this._showInlineDiffs = false; |
| for (var i = 0; i < this._shownFiles.length; i++) { |
| this.set(['_shownFiles', i, '__expanded'], false); |
| this.set(['_files', i, '__expanded'], false); |
| } |
| this.$.cursor.handleDiffUpdate(); |
| if (e && e.target) { |
| e.target.blur(); |
| } |
| }, |
| |
| _computeCommentsString: function(comments, patchNum, path) { |
| return this._computeCountString(comments, patchNum, path, 'comment'); |
| }, |
| |
| _computeDraftsString: function(drafts, patchNum, path) { |
| return this._computeCountString(drafts, patchNum, path, 'draft'); |
| }, |
| |
| _computeCountString: function(comments, patchNum, path, noun) { |
| if (!comments) { return ''; } |
| |
| var patchComments = (comments[path] || []).filter(function(c) { |
| return parseInt(c.patch_set, 10) === parseInt(patchNum, 10); |
| }); |
| var num = patchComments.length; |
| if (num === 0) { return ''; } |
| return num + ' ' + noun + (num > 1 ? 's' : ''); |
| }, |
| |
| _computeReviewed: function(file, _reviewed) { |
| return _reviewed.indexOf(file.__path) !== -1; |
| }, |
| |
| _handleReviewedChange: function(e) { |
| var path = Polymer.dom(e).rootTarget.getAttribute('data-path'); |
| var index = this._reviewed.indexOf(path); |
| var reviewed = index !== -1; |
| if (reviewed) { |
| this.splice('_reviewed', index, 1); |
| } else { |
| this.push('_reviewed', path); |
| } |
| |
| this._saveReviewedState(path, !reviewed).catch(function(err) { |
| alert('Couldn’t change file review status. Check the console ' + |
| 'and contact the PolyGerrit team for assistance.'); |
| throw err; |
| }.bind(this)); |
| }, |
| |
| _saveReviewedState: function(path, reviewed) { |
| return this.$.restAPI.saveFileReviewed(this.changeNum, |
| this.patchRange.patchNum, path, reviewed); |
| }, |
| |
| _getLoggedIn: function() { |
| return this.$.restAPI.getLoggedIn(); |
| }, |
| |
| _getReviewedFiles: function() { |
| return this.$.restAPI.getReviewedFiles(this.changeNum, |
| this.patchRange.patchNum); |
| }, |
| |
| _getFiles: function() { |
| return this.$.restAPI.getChangeFilesAsSpeciallySortedArray( |
| this.changeNum, this.patchRange).then(function(files) { |
| // Append UI-specific properties. |
| return files.map(function(file) { |
| file.__expanded = false; |
| return file; |
| }); |
| }); |
| }, |
| |
| _handleKey: function(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| switch (e.keyCode) { |
| case 37: // left |
| if (e.shiftKey && this._showInlineDiffs) { |
| e.preventDefault(); |
| this.$.cursor.moveLeft(); |
| } |
| break; |
| case 39: // right |
| if (e.shiftKey && this._showInlineDiffs) { |
| e.preventDefault(); |
| this.$.cursor.moveRight(); |
| } |
| break; |
| case 73: // 'i' |
| if (e.shiftKey) { |
| e.preventDefault(); |
| this._toggleInlineDiffs(); |
| } else if (this.selectedIndex !== undefined) { |
| e.preventDefault(); |
| var expanded = this._files[this.selectedIndex].__expanded; |
| // Until Polymer 2.0, manual management of reflection between _files |
| // and _shownFiles is necessary. |
| this.set(['_shownFiles', this.selectedIndex, '__expanded'], |
| !expanded); |
| this.set(['_files', this.selectedIndex, '__expanded'], !expanded); |
| } |
| break; |
| case 40: // down |
| case 74: // 'j' |
| e.preventDefault(); |
| if (this._showInlineDiffs) { |
| this.$.cursor.moveDown(); |
| } else { |
| this.selectedIndex = |
| Math.min(this._numFilesShown, this.selectedIndex + 1); |
| this._scrollToSelectedFile(); |
| } |
| break; |
| case 38: // up |
| case 75: // 'k' |
| e.preventDefault(); |
| if (this._showInlineDiffs) { |
| this.$.cursor.moveUp(); |
| } else { |
| this.selectedIndex = Math.max(0, this.selectedIndex - 1); |
| this._scrollToSelectedFile(); |
| } |
| break; |
| case 67: // 'c' |
| var isRangeSelected = this.diffs.some(function(diff) { |
| return diff.isRangeSelected(); |
| }, this); |
| if (this._showInlineDiffs && !isRangeSelected) { |
| e.preventDefault(); |
| this._addDraftAtTarget(); |
| } |
| break; |
| case 219: // '[' |
| e.preventDefault(); |
| this._openSelectedFile(this._files.length - 1); |
| break; |
| case 221: // ']' |
| e.preventDefault(); |
| this._openSelectedFile(0); |
| break; |
| case 13: // <enter> |
| case 79: // 'o' |
| e.preventDefault(); |
| if (this._showInlineDiffs) { |
| this._openCursorFile(); |
| } else { |
| this._openSelectedFile(); |
| } |
| break; |
| case 78: // 'n' |
| if (this._showInlineDiffs) { |
| e.preventDefault(); |
| if (e.shiftKey) { |
| this.$.cursor.moveToNextCommentThread(); |
| } else { |
| this.$.cursor.moveToNextChunk(); |
| } |
| } |
| break; |
| case 80: // 'p' |
| if (this._showInlineDiffs) { |
| e.preventDefault(); |
| if (e.shiftKey) { |
| this.$.cursor.moveToPreviousCommentThread(); |
| } else { |
| this.$.cursor.moveToPreviousChunk(); |
| } |
| } |
| break; |
| case 65: // 'a' |
| if (e.shiftKey) { // Hide left diff. |
| e.preventDefault(); |
| this._forEachDiff(function(diff) { |
| diff.toggleLeftDiff(); |
| }); |
| } |
| break; |
| } |
| }, |
| |
| _toggleInlineDiffs: function() { |
| if (this._showInlineDiffs) { |
| this._collapseAllDiffs(); |
| } else { |
| this._expandAllDiffs(); |
| } |
| }, |
| |
| _openCursorFile: function() { |
| var diff = this.$.cursor.getTargetDiffElement(); |
| page.show(this._computeDiffURL(diff.changeNum, diff.patchRange, |
| diff.path)); |
| }, |
| |
| _openSelectedFile: function(opt_index) { |
| if (opt_index != null) { |
| this.selectedIndex = opt_index; |
| } |
| page.show(this._computeDiffURL(this.changeNum, this.patchRange, |
| this._files[this.selectedIndex].__path)); |
| }, |
| |
| _addDraftAtTarget: function() { |
| var diff = this.$.cursor.getTargetDiffElement(); |
| var target = this.$.cursor.getTargetLineElement(); |
| if (diff && target) { |
| diff.addDraftAtLine(target); |
| } |
| }, |
| |
| _scrollToSelectedFile: function() { |
| var el = this.$$('.row[selected]'); |
| var top = 0; |
| for (var node = el; node; node = node.offsetParent) { |
| top += node.offsetTop; |
| } |
| |
| // Don't scroll if it's already in view. |
| if (top > window.pageYOffset && |
| top < window.pageYOffset + window.innerHeight - el.clientHeight) { |
| return; |
| } |
| |
| window.scrollTo(0, top - document.body.clientHeight / 2); |
| }, |
| |
| _shouldHideChangeTotals: function(_patchChange) { |
| return (_patchChange.inserted === 0 && _patchChange.deleted === 0); |
| }, |
| |
| _computeFileSelected: function(index, selectedIndex) { |
| return index === selectedIndex; |
| }, |
| |
| _computeFileStatus: function(status) { |
| return status || 'M'; |
| }, |
| |
| _computeDiffURL: function(changeNum, patchRange, path) { |
| return this.encodeURL('/c/' + changeNum + '/' + |
| this._patchRangeStr(patchRange) + '/' + path, true); |
| }, |
| |
| _patchRangeStr: function(patchRange) { |
| return patchRange.basePatchNum !== 'PARENT' ? |
| patchRange.basePatchNum + '..' + patchRange.patchNum : |
| patchRange.patchNum + ''; |
| }, |
| |
| _computeFileDisplayName: function(path) { |
| return path === COMMIT_MESSAGE_PATH ? 'Commit message' : path; |
| }, |
| |
| _computeClass: function(baseClass, path) { |
| var classes = [baseClass]; |
| if (path === COMMIT_MESSAGE_PATH) { |
| classes.push('invisible'); |
| } |
| return classes.join(' '); |
| }, |
| |
| _computeShowHideText: function(expanded) { |
| return expanded ? '▼' : '◀'; |
| }, |
| |
| _computeFilesShown: function(numFilesShown, files) { |
| return files.base.slice(0, numFilesShown); |
| }, |
| |
| _filesChanged: function() { |
| this.async(function() { |
| var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff'); |
| |
| // Overwrite the cursor's list of diffs: |
| this.$.cursor.splice.apply(this.$.cursor, |
| ['diffs', 0, this.$.cursor.diffs.length].concat(diffElements)); |
| }.bind(this), 1); |
| }, |
| |
| _incrementNumFilesShown: function() { |
| this._numFilesShown += this._fileListIncrement; |
| }, |
| |
| _computeFileListButtonHidden: function(numFilesShown, files) { |
| return numFilesShown >= files.length; |
| }, |
| |
| _computeIncrementText: function(numFilesShown, files) { |
| if (!files) { return ''; } |
| var text = |
| Math.min(this._fileListIncrement, files.length - numFilesShown); |
| return 'Show ' + text + ' more'; |
| }, |
| |
| _computeShowAllText: function(files) { |
| if (!files) { return ''; } |
| return 'Show all ' + files.length + ' files'; |
| }, |
| |
| _showAllFiles: function() { |
| this._numFilesShown = this._files.length; |
| }, |
| |
| /** |
| * _getDiffViewMode: Get the diff view (side-by-side or unified) based on |
| * the current state. |
| * |
| * The expected behavior is to use the mode specified in the user's |
| * preferences unless they have manually chosen the alternative view. If the |
| * user navigates up to the change view, it should clear this choice and |
| * revert to the preference the next time a diff is viewed. |
| * |
| * Use side-by-side if the user is not logged in. |
| * |
| * @return {String} |
| */ |
| _getDiffViewMode: function() { |
| if (this.diffViewMode) { |
| return this.diffViewMode; |
| } else if (this._userPrefs && this._userPrefs.diff_view) { |
| return this.diffViewMode = this._userPrefs.diff_view; |
| } |
| |
| return DiffViewMode.SIDE_BY_SIDE; |
| }, |
| |
| _handleDropdownChange: function(e) { |
| e.target.blur(); |
| }, |
| |
| _fileListActionsVisible: function(numFilesShown, maxFilesForBulkActions) { |
| return numFilesShown <= maxFilesForBulkActions; |
| }, |
| }); |
| })(); |