| // 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', |
| }; |
| |
| var DiffSides = { |
| LEFT: 'left', |
| RIGHT: 'right', |
| }; |
| |
| var HASH_PATTERN = /^b?\d+$/; |
| |
| Polymer({ |
| is: 'gr-diff-view', |
| |
| /** |
| * Fired when the title of the page should change. |
| * |
| * @event title-change |
| */ |
| |
| properties: { |
| /** |
| * URL params passed from the router. |
| */ |
| params: { |
| type: Object, |
| observer: '_paramsChanged', |
| }, |
| keyEventTarget: { |
| type: Object, |
| value: function() { return document.body; }, |
| }, |
| changeViewState: { |
| type: Object, |
| notify: true, |
| value: function() { return {}; }, |
| }, |
| |
| _patchRange: Object, |
| _change: Object, |
| _changeNum: String, |
| _diff: Object, |
| _fileList: { |
| type: Array, |
| value: function() { return []; }, |
| }, |
| _path: { |
| type: String, |
| observer: '_pathChanged', |
| }, |
| _loggedIn: { |
| type: Boolean, |
| value: false, |
| }, |
| _loading: { |
| type: Boolean, |
| value: true, |
| }, |
| _prefs: Object, |
| _localPrefs: Object, |
| _projectConfig: Object, |
| _userPrefs: Object, |
| _diffMode: { |
| type: String, |
| computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)', |
| }, |
| _isImageDiff: Boolean, |
| _filesWeblinks: Object, |
| }, |
| |
| behaviors: [ |
| Gerrit.KeyboardShortcutBehavior, |
| ], |
| |
| observers: [ |
| '_getChangeDetail(_changeNum)', |
| '_getProjectConfig(_change.project)', |
| '_getFiles(_changeNum, _patchRange.*)', |
| ], |
| |
| attached: function() { |
| this._getLoggedIn().then(function(loggedIn) { |
| this._loggedIn = loggedIn; |
| if (loggedIn) { |
| this._setReviewed(true); |
| } |
| }.bind(this)); |
| |
| if (this._path) { |
| this.fire('title-change', |
| {title: this._computeFileDisplayName(this._path)}); |
| } |
| |
| this.$.cursor.push('diffs', this.$.diff); |
| }, |
| |
| detached: function() { |
| // Reset the diff mode to null so that it reverts to the user preference. |
| this.changeViewState.diffMode = null; |
| }, |
| |
| _getLoggedIn: function() { |
| return this.$.restAPI.getLoggedIn(); |
| }, |
| |
| _getProjectConfig: function(project) { |
| return this.$.restAPI.getProjectConfig(project).then( |
| function(config) { |
| this._projectConfig = config; |
| }.bind(this)); |
| }, |
| |
| _getChangeDetail: function(changeNum) { |
| return this.$.restAPI.getDiffChangeDetail(changeNum).then( |
| function(change) { |
| this._change = change; |
| }.bind(this)); |
| }, |
| |
| _getFiles: function(changeNum, patchRangeRecord) { |
| var patchRange = patchRangeRecord.base; |
| return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray( |
| changeNum, patchRange).then(function(files) { |
| this._fileList = files; |
| }.bind(this)); |
| }, |
| |
| _getDiffPreferences: function() { |
| return this.$.restAPI.getDiffPreferences(); |
| }, |
| |
| _getPreferences: function() { |
| return this.$.restAPI.getPreferences(); |
| }, |
| |
| _handleReviewedChange: function(e) { |
| this._setReviewed(Polymer.dom(e).rootTarget.checked); |
| }, |
| |
| _setReviewed: function(reviewed) { |
| this.$.reviewed.checked = reviewed; |
| this._saveReviewedState(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(reviewed) { |
| return this.$.restAPI.saveFileReviewed(this._changeNum, |
| this._patchRange.patchNum, this._path, reviewed); |
| }, |
| |
| _handleKey: function(e) { |
| if (this.shouldSupressKeyboardShortcut(e)) { return; } |
| |
| switch (e.keyCode) { |
| case 37: // left |
| if (e.shiftKey) { |
| e.preventDefault(); |
| this.$.cursor.moveLeft(); |
| } |
| break; |
| case 39: // right |
| if (e.shiftKey) { |
| e.preventDefault(); |
| this.$.cursor.moveRight(); |
| } |
| break; |
| case 40: // down |
| case 74: // 'j' |
| e.preventDefault(); |
| this.$.cursor.moveDown(); |
| break; |
| case 38: // up |
| case 75: // 'k' |
| e.preventDefault(); |
| this.$.cursor.moveUp(); |
| break; |
| case 67: // 'c' |
| if (!this.$.diff.isRangeSelected()) { |
| e.preventDefault(); |
| var line = this.$.cursor.getTargetLineElement(); |
| if (line) { |
| this.$.diff.addDraftAtLine(line); |
| } |
| } |
| break; |
| case 219: // '[' |
| e.preventDefault(); |
| this._navToFile(this._fileList, -1); |
| break; |
| case 221: // ']' |
| e.preventDefault(); |
| this._navToFile(this._fileList, 1); |
| break; |
| case 78: // 'n' |
| e.preventDefault(); |
| if (e.shiftKey) { |
| this.$.cursor.moveToNextCommentThread(); |
| } else { |
| this.$.cursor.moveToNextChunk(); |
| } |
| break; |
| case 80: // 'p' |
| 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.$.diff.toggleLeftDiff(); |
| break; |
| } |
| |
| if (!this._loggedIn) { break; } |
| |
| this.set('changeViewState.showReplyDialog', true); |
| /* falls through */ // required by JSHint |
| case 85: // 'u' |
| if (this._changeNum && this._patchRange.patchNum) { |
| e.preventDefault(); |
| page.show(this._getChangePath( |
| this._changeNum, |
| this._patchRange, |
| this._change && this._change.revisions)); |
| } |
| break; |
| case 188: // ',' |
| e.preventDefault(); |
| this.$.prefsOverlay.open(); |
| break; |
| } |
| }, |
| |
| _navToFile: function(fileList, direction) { |
| if (fileList.length == 0) { return; } |
| |
| var idx = fileList.indexOf(this._path) + direction; |
| if (idx < 0 || idx > fileList.length - 1) { |
| page.show(this._getChangePath( |
| this._changeNum, |
| this._patchRange, |
| this._change && this._change.revisions)); |
| return; |
| } |
| page.show(this._getDiffURL(this._changeNum, |
| this._patchRange, |
| fileList[idx])); |
| }, |
| |
| _paramsChanged: function(value) { |
| if (value.view != this.tagName.toLowerCase()) { return; } |
| |
| this._loadHash(location.hash); |
| |
| this._changeNum = value.changeNum; |
| this._patchRange = { |
| patchNum: value.patchNum, |
| basePatchNum: value.basePatchNum || 'PARENT', |
| }; |
| this._path = value.path; |
| |
| this.fire('title-change', |
| {title: this._computeFileDisplayName(this._path)}); |
| |
| // When navigating away from the page, there is a possibility that the |
| // patch number is no longer a part of the URL (say when navigating to |
| // the top-level change info view) and therefore undefined in `params`. |
| if (!this._patchRange.patchNum) { |
| return; |
| } |
| |
| var promises = []; |
| |
| this._localPrefs = this.$.storage.getPreferences(); |
| promises.push(this._getDiffPreferences().then(function(prefs) { |
| this._prefs = prefs; |
| }.bind(this))); |
| |
| promises.push(this._getPreferences().then(function(prefs) { |
| this._userPrefs = prefs; |
| }.bind(this))); |
| |
| promises.push(this._getChangeDetail(this._changeNum)); |
| |
| Promise.all(promises) |
| .then(function() { return this.$.diff.reload(); }.bind(this)) |
| .then(function() { this._loading = false; }.bind(this)); |
| }, |
| |
| /** |
| * If the URL hash is a diff address then configure the diff cursor. |
| */ |
| _loadHash: function(hash) { |
| var hash = hash.replace(/^#/, ''); |
| if (!HASH_PATTERN.test(hash)) { return; } |
| if (hash[0] === 'b') { |
| this.$.cursor.side = DiffSides.LEFT; |
| hash = hash.substring(1); |
| } else { |
| this.$.cursor.side = DiffSides.RIGHT; |
| } |
| this.$.cursor.initialLineNumber = parseInt(hash, 10); |
| }, |
| |
| _pathChanged: function(path) { |
| if (this._fileList.length == 0) { return; } |
| |
| this.set('changeViewState.selectedFileIndex', |
| this._fileList.indexOf(path)); |
| |
| if (this._loggedIn) { |
| this._setReviewed(true); |
| } |
| }, |
| |
| _getDiffURL: function(changeNum, patchRange, path) { |
| return '/c/' + changeNum + '/' + this._patchRangeStr(patchRange) + '/' + |
| path; |
| }, |
| |
| _computeDiffURL: function(changeNum, patchRangeRecord, path) { |
| return this._getDiffURL(changeNum, patchRangeRecord.base, path); |
| }, |
| |
| _patchRangeStr: function(patchRange) { |
| var patchStr = patchRange.patchNum; |
| if (patchRange.basePatchNum != null && |
| patchRange.basePatchNum != 'PARENT') { |
| patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum; |
| } |
| return patchStr; |
| }, |
| |
| _computeAvailablePatches: function(revisions) { |
| var patchNums = []; |
| for (var rev in revisions) { |
| patchNums.push(revisions[rev]._number); |
| } |
| return patchNums.sort(function(a, b) { return a - b; }); |
| }, |
| |
| _getChangePath: function(changeNum, patchRange, revisions) { |
| var base = '/c/' + changeNum + '/'; |
| |
| // The change may not have loaded yet, making revisions unavailable. |
| if (!revisions) { |
| return base + this._patchRangeStr(patchRange); |
| } |
| |
| var latestPatchNum = -1; |
| for (var rev in revisions) { |
| latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number); |
| } |
| if (patchRange.basePatchNum !== 'PARENT' || |
| parseInt(patchRange.patchNum, 10) !== latestPatchNum) { |
| return base + this._patchRangeStr(patchRange); |
| } |
| |
| return base; |
| }, |
| |
| _computeChangePath: function(changeNum, patchRangeRecord, revisions) { |
| return this._getChangePath(changeNum, patchRangeRecord.base, revisions); |
| }, |
| |
| _computeFileDisplayName: function(path) { |
| return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path; |
| }, |
| |
| _computeFileSelected: function(path, currentPath) { |
| return path == currentPath; |
| }, |
| |
| _computePrefsButtonHidden: function(prefs, loggedIn) { |
| return !loggedIn || !prefs; |
| }, |
| |
| _computeKeyNav: function(path, selectedPath, fileList) { |
| var selectedIndex = fileList.indexOf(selectedPath); |
| if (fileList.indexOf(path) == selectedIndex - 1) { |
| return '['; |
| } |
| if (fileList.indexOf(path) == selectedIndex + 1) { |
| return ']'; |
| } |
| return ''; |
| }, |
| |
| _handleFileTap: function(e) { |
| this.$.dropdown.close(); |
| }, |
| |
| _handleMobileSelectChange: function(e) { |
| var path = Polymer.dom(e).rootTarget.value; |
| page.show(this._getDiffURL(this._changeNum, this._patchRange, path)); |
| }, |
| |
| _showDropdownTapHandler: function(e) { |
| this.$.dropdown.open(); |
| }, |
| |
| _handlePrefsTap: function(e) { |
| e.preventDefault(); |
| this.$.prefsOverlay.open(); |
| }, |
| |
| _handlePrefsSave: function(e) { |
| e.stopPropagation(); |
| var el = Polymer.dom(e).rootTarget; |
| el.disabled = true; |
| this.$.storage.savePreferences(this._localPrefs); |
| this._saveDiffPreferences().then(function(response) { |
| el.disabled = false; |
| if (!response.ok) { return response; } |
| |
| this.$.prefsOverlay.close(); |
| }.bind(this)).catch(function(err) { |
| el.disabled = false; |
| }.bind(this)); |
| }, |
| |
| _saveDiffPreferences: function() { |
| return this.$.restAPI.saveDiffPreferences(this._prefs); |
| }, |
| |
| _handlePrefsCancel: function(e) { |
| e.stopPropagation(); |
| this.$.prefsOverlay.close(); |
| }, |
| |
| /** |
| * _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.changeViewState.diffMode) { |
| return this.changeViewState.diffMode; |
| } else if (this._userPrefs && this._userPrefs.diff_view) { |
| return this.changeViewState.diffMode = this._userPrefs.diff_view; |
| } |
| |
| return DiffViewMode.SIDE_BY_SIDE; |
| }, |
| |
| _computeModeSelectHidden: function() { |
| return this._isImageDiff; |
| }, |
| |
| _onLineSelected: function(e, detail) { |
| this.$.cursor.moveToLineNumber(detail.number, detail.side); |
| history.pushState(null, null, '#' + this.$.cursor.getAddress()); |
| }, |
| }); |
| })(); |