| /** |
| * @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 '@polymer/iron-dropdown/iron-dropdown.js'; |
| import '@polymer/iron-input/iron-input.js'; |
| import '../../../styles/shared-styles.js'; |
| import '../../shared/gr-button/gr-button.js'; |
| import '../../shared/gr-dropdown/gr-dropdown.js'; |
| import '../../shared/gr-dropdown-list/gr-dropdown-list.js'; |
| import '../../shared/gr-icons/gr-icons.js'; |
| import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; |
| import '../../shared/gr-select/gr-select.js'; |
| import '../../shared/revision-info/revision-info.js'; |
| import '../gr-comment-api/gr-comment-api.js'; |
| import '../gr-diff-cursor/gr-diff-cursor.js'; |
| import '../gr-apply-fix-dialog/gr-apply-fix-dialog.js'; |
| import '../gr-diff-host/gr-diff-host.js'; |
| import '../gr-diff-mode-selector/gr-diff-mode-selector.js'; |
| import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog.js'; |
| import '../gr-patch-range-select/gr-patch-range-select.js'; |
| import {dom} 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-diff-view_html.js'; |
| import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js'; |
| import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js'; |
| import {GerritNav} from '../../core/gr-navigation/gr-navigation.js'; |
| import {RevisionInfo} from '../../shared/revision-info/revision-info.js'; |
| import {appContext} from '../../../services/app-context.js'; |
| import { |
| computeAllPatchSets, |
| computeLatestPatchNum, |
| patchNumEquals, |
| SPECIAL_PATCH_SET_NUM, |
| } from '../../../utils/patch-set-util.js'; |
| import { |
| addUnmodifiedFiles, computeDisplayPath, computeTruncatedPath, |
| isMagicPath, specialFilePathCompare, |
| } from '../../../utils/path-list-util.js'; |
| import {changeBaseURL, changeIsOpen} from '../../../utils/change-util.js'; |
| |
| const ERR_REVIEW_STATUS = 'Couldn’t change file review status.'; |
| const MSG_LOADING_BLAME = 'Loading blame...'; |
| const MSG_LOADED_BLAME = 'Blame loaded'; |
| |
| const PARENT = 'PARENT'; |
| |
| const DiffSides = { |
| LEFT: 'left', |
| RIGHT: 'right', |
| }; |
| |
| const DiffViewMode = { |
| SIDE_BY_SIDE: 'SIDE_BY_SIDE', |
| UNIFIED: 'UNIFIED_DIFF', |
| }; |
| |
| /** |
| * @extends PolymerElement |
| */ |
| class GrDiffView extends KeyboardShortcutMixin( |
| GestureEventListeners(LegacyElementMixin(PolymerElement))) { |
| static get template() { return htmlTemplate; } |
| |
| static get is() { return 'gr-diff-view'; } |
| /** |
| * Fired when the title of the page should change. |
| * |
| * @event title-change |
| */ |
| |
| /** |
| * Fired when user tries to navigate away while comments are pending save. |
| * |
| * @event show-alert |
| */ |
| |
| static get properties() { |
| return { |
| /** |
| * URL params passed from the router. |
| */ |
| params: { |
| type: Object, |
| observer: '_paramsChanged', |
| }, |
| keyEventTarget: { |
| type: Object, |
| value() { return document.body; }, |
| }, |
| /** |
| * @type {{ diffMode: (string|undefined) }} |
| */ |
| changeViewState: { |
| type: Object, |
| notify: true, |
| value() { return {}; }, |
| observer: '_changeViewStateChanged', |
| }, |
| disableDiffPrefs: { |
| type: Boolean, |
| value: false, |
| }, |
| _diffPrefsDisabled: { |
| type: Boolean, |
| computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', |
| }, |
| /** @type {?} */ |
| _patchRange: Object, |
| /** @type {?} */ |
| _commitRange: Object, |
| /** |
| * @type {{ |
| * subject: string, |
| * project: string, |
| * revisions: string, |
| * }} |
| */ |
| _change: Object, |
| /** @type {?} */ |
| _changeComments: Object, |
| _changeNum: String, |
| /** |
| * This is a DiffInfo object. |
| * This is retrieved and owned by a child component. |
| */ |
| _diff: Object, |
| // An array specifically formatted to be used in a gr-dropdown-list |
| // element for selected a file to view. |
| _formattedFiles: { |
| type: Array, |
| computed: '_formatFilesForDropdown(_files, ' + |
| '_patchRange.patchNum, _changeComments)', |
| }, |
| // An sorted array of files, as returned by the rest API. |
| _fileList: { |
| type: Array, |
| computed: '_getSortedFileList(_files)', |
| }, |
| /** |
| * Contains information about files as returned by the rest API. |
| * |
| * @type {{ sortedFileList: Array<string>, changeFilesByPath: Object }} |
| */ |
| _files: { |
| type: Object, |
| value() { return {sortedFileList: [], changeFilesByPath: {}}; }, |
| }, |
| |
| /** @type {Gerrit.FileRange} */ |
| _file: { |
| type: Object, |
| computed: '_getCurrentFile(_files, _path)', |
| }, |
| |
| _path: { |
| type: String, |
| observer: '_pathChanged', |
| }, |
| _fileNum: { |
| type: Number, |
| computed: '_computeFileNum(_path, _formattedFiles)', |
| }, |
| _loggedIn: { |
| type: Boolean, |
| value: false, |
| }, |
| _loading: { |
| type: Boolean, |
| value: true, |
| }, |
| _prefs: Object, |
| _projectConfig: Object, |
| _userPrefs: Object, |
| _diffMode: { |
| type: String, |
| computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)', |
| }, |
| _isImageDiff: Boolean, |
| // The return type is FilesWebLinks from gr-patch-range-select. |
| _filesWeblinks: Object, |
| |
| /** |
| * Map of paths in the current change and patch range that have comments |
| * or drafts or robot comments. |
| */ |
| _commentMap: Object, |
| |
| _commentsForDiff: Object, |
| |
| /** |
| * Object to contain the path of the next and previous file in the current |
| * change and patch range that has comments. |
| */ |
| _commentSkips: { |
| type: Object, |
| computed: '_computeCommentSkips(_commentMap, _fileList, _path)', |
| }, |
| _editMode: { |
| type: Boolean, |
| computed: '_computeEditMode(_patchRange.*)', |
| }, |
| _isBlameLoaded: Boolean, |
| _isBlameLoading: { |
| type: Boolean, |
| value: false, |
| }, |
| _allPatchSets: { |
| type: Array, |
| computed: '_computeAllPatchSets(_change, _change.revisions.*)', |
| }, |
| _revisionInfo: { |
| type: Object, |
| computed: '_getRevisionInfo(_change)', |
| }, |
| _reviewedFiles: { |
| type: Object, |
| value: () => new Set(), |
| }, |
| // line number on the diff which should be scrolled to upon loading |
| _focusLineNum: Number, |
| }; |
| } |
| |
| static get observers() { |
| return [ |
| '_getProjectConfig(_change.project)', |
| '_getFiles(_changeNum, _patchRange.*, _changeComments)', |
| '_setReviewedObserver(_loggedIn, params.*, _prefs, _patchRange.*)', |
| '_recomputeComments(_files.changeFilesByPath,' + |
| '_path, _patchRange, _projectConfig)', |
| ]; |
| } |
| |
| get keyBindings() { |
| return { |
| esc: '_handleEscKey', |
| }; |
| } |
| |
| keyboardShortcuts() { |
| return { |
| [Shortcut.LEFT_PANE]: '_handleLeftPane', |
| [Shortcut.RIGHT_PANE]: '_handleRightPane', |
| [Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments', |
| [Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments', |
| [Shortcut.VISIBLE_LINE]: '_handleVisibleLine', |
| [Shortcut.NEXT_FILE_WITH_COMMENTS]: |
| '_handleNextLineOrFileWithComments', |
| [Shortcut.PREV_FILE_WITH_COMMENTS]: |
| '_handlePrevLineOrFileWithComments', |
| [Shortcut.NEW_COMMENT]: '_handleNewComment', |
| [Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding |
| [Shortcut.NEXT_FILE]: '_handleNextFile', |
| [Shortcut.PREV_FILE]: '_handlePrevFile', |
| [Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread', |
| [Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread', |
| [Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread', |
| [Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread', |
| [Shortcut.OPEN_REPLY_DIALOG]: |
| '_handleOpenReplyDialogOrToggleLeftPane', |
| [Shortcut.TOGGLE_LEFT_PANE]: |
| '_handleOpenReplyDialogOrToggleLeftPane', |
| [Shortcut.OPEN_DOWNLOAD_DIALOG]: |
| '_handleOpenDownloadDialog', |
| [Shortcut.UP_TO_CHANGE]: '_handleUpToChange', |
| [Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey', |
| [Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode', |
| [Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed', |
| [Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext', |
| [Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile', |
| [Shortcut.TOGGLE_BLAME]: '_handleToggleBlame', |
| [Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]: |
| '_handleToggleHideAllCommentThreads', |
| [Shortcut.DIFF_AGAINST_BASE]: '_handleDiffAgainstBase', |
| [Shortcut.DIFF_AGAINST_LATEST]: '_handleDiffAgainstLatest', |
| [Shortcut.DIFF_BASE_AGAINST_LEFT]: '_handleDiffBaseAgainstLeft', |
| [Shortcut.DIFF_RIGHT_AGAINST_LATEST]: |
| '_handleDiffRightAgainstLatest', |
| [Shortcut.DIFF_BASE_AGAINST_LATEST]: |
| '_handleDiffBaseAgainstLatest', |
| |
| // 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; |
| this.flagsService = appContext.flagsService; |
| } |
| |
| /** @override */ |
| attached() { |
| super.attached(); |
| this._getLoggedIn().then(loggedIn => { |
| this._loggedIn = loggedIn; |
| }); |
| |
| this.addEventListener('open-fix-preview', |
| e => this._onOpenFixPreview(e)); |
| this.$.cursor.push('diffs', this.$.diffHost); |
| this._onRenderHandler = () => { |
| this.$.cursor.reInitCursor(); |
| }; |
| this.$.diffHost.addEventListener('render', this._onRenderHandler); |
| } |
| |
| detached() { |
| this.$.diffHost.removeEventListener('render', this._onRenderHandler); |
| } |
| |
| _getLoggedIn() { |
| return this.$.restAPI.getLoggedIn(); |
| } |
| |
| _getProjectConfig(project) { |
| if (!project) return; |
| return this.$.restAPI.getProjectConfig(project).then( |
| config => { |
| this._projectConfig = config; |
| }); |
| } |
| |
| _getChangeDetail(changeNum) { |
| return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => { |
| this._change = change; |
| return change; |
| }); |
| } |
| |
| _getChangeEdit(changeNum) { |
| return this.$.restAPI.getChangeEdit(this._changeNum); |
| } |
| |
| _getSortedFileList(files) { |
| if (!files) return []; |
| return files.sortedFileList; |
| } |
| |
| /** |
| * @param {!Object} files |
| * @param {string} path |
| * @returns {!Gerrit.FileRange} |
| */ |
| _getCurrentFile(files, path) { |
| if ([files, path].includes(undefined)) return; |
| const fileInfo = files.changeFilesByPath[path]; |
| const fileRange = {path}; |
| if (fileInfo && fileInfo.old_path) { |
| fileRange.basePath = fileInfo.old_path; |
| } |
| return fileRange; |
| } |
| |
| _getFiles(changeNum, patchRangeRecord, changeComments) { |
| // Polymer 2: check for undefined |
| if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments] |
| .some(arg => arg === undefined)) { |
| return Promise.resolve(); |
| } |
| |
| if (!patchRangeRecord.base.patchNum) { |
| return Promise.resolve(); |
| } |
| |
| const patchRange = patchRangeRecord.base; |
| return this.$.restAPI.getChangeFiles( |
| changeNum, patchRange).then(changeFiles => { |
| if (!changeFiles) return; |
| const commentedPaths = changeComments.getPaths(patchRange); |
| const files = {...changeFiles}; |
| addUnmodifiedFiles(files, commentedPaths); |
| this._files = { |
| sortedFileList: Object.keys(files).sort(specialFilePathCompare), |
| changeFilesByPath: files, |
| }; |
| }); |
| } |
| |
| _getDiffPreferences() { |
| return this.$.restAPI.getDiffPreferences().then(prefs => { |
| this._prefs = prefs; |
| }); |
| } |
| |
| _getPreferences() { |
| return this.$.restAPI.getPreferences(); |
| } |
| |
| _getWindowWidth() { |
| return window.innerWidth; |
| } |
| |
| _handleReviewedChange(e) { |
| this._setReviewed(dom(e).rootTarget.checked); |
| } |
| |
| _setReviewed(reviewed) { |
| if (this._editMode) { return; } |
| this.$.reviewed.checked = reviewed; |
| if (!this._patchRange.patchNum) return; |
| this._saveReviewedState(reviewed).catch(err => { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: {message: ERR_REVIEW_STATUS}, |
| composed: true, bubbles: true, |
| })); |
| throw err; |
| }); |
| } |
| |
| _saveReviewedState(reviewed) { |
| return this.$.restAPI.saveFileReviewed(this._changeNum, |
| this._patchRange.patchNum, this._path, reviewed); |
| } |
| |
| _handleToggleFileReviewed(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this._setReviewed(!this.$.reviewed.checked); |
| } |
| |
| _handleEscKey(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.diffHost.displayLine = false; |
| } |
| |
| _handleLeftPane(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.cursor.moveLeft(); |
| } |
| |
| _handleRightPane(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.cursor.moveRight(); |
| } |
| |
| _handlePrevLineOrFileWithComments(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| if (e.detail.keyboardEvent.shiftKey && |
| e.detail.keyboardEvent.keyCode === 75) { // 'K' |
| this._moveToPreviousFileWithComment(); |
| return; |
| } |
| if (this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.diffHost.displayLine = true; |
| this.$.cursor.moveUp(); |
| } |
| |
| _handleVisibleLine(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.cursor.moveToVisibleArea(); |
| } |
| |
| _onOpenFixPreview(e) { |
| this.$.applyFixDialog.open(e); |
| } |
| |
| _handleNextLineOrFileWithComments(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| if (e.detail.keyboardEvent.shiftKey && |
| e.detail.keyboardEvent.keyCode === 74) { // 'J' |
| this._moveToNextFileWithComment(); |
| return; |
| } |
| if (this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.diffHost.displayLine = true; |
| this.$.cursor.moveDown(); |
| } |
| |
| _moveToPreviousFileWithComment() { |
| if (!this._commentSkips) { return; } |
| |
| // If there is no previous diff with comments, then return to the change |
| // view. |
| if (!this._commentSkips.previous) { |
| this._navToChangeView(); |
| return; |
| } |
| |
| GerritNav.navigateToDiff(this._change, this._commentSkips.previous, |
| this._patchRange.patchNum, this._patchRange.basePatchNum); |
| } |
| |
| _moveToNextFileWithComment() { |
| if (!this._commentSkips) { return; } |
| |
| // If there is no next diff with comments, then return to the change view. |
| if (!this._commentSkips.next) { |
| this._navToChangeView(); |
| return; |
| } |
| |
| GerritNav.navigateToDiff(this._change, this._commentSkips.next, |
| this._patchRange.patchNum, this._patchRange.basePatchNum); |
| } |
| |
| _handleNewComment(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| e.preventDefault(); |
| this.$.cursor.createCommentInPlace(); |
| } |
| |
| _handlePrevFile(e) { |
| // Check for meta key to avoid overriding native chrome shortcut. |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.getKeyboardEvent(e).metaKey) { return; } |
| |
| e.preventDefault(); |
| this._navToFile(this._path, this._fileList, -1); |
| } |
| |
| _handleNextFile(e) { |
| // Check for meta key to avoid overriding native chrome shortcut. |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.getKeyboardEvent(e).metaKey) { return; } |
| |
| e.preventDefault(); |
| this._navToFile(this._path, this._fileList, 1); |
| } |
| |
| _handleNextChunkOrCommentThread(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| |
| e.preventDefault(); |
| if (e.detail.keyboardEvent.shiftKey) { |
| this.$.cursor.moveToNextCommentThread(); |
| } else { |
| if (this.modifierPressed(e)) { return; } |
| // navigate to next file if key is not being held down |
| this.$.cursor.moveToNextChunk(/* opt_clipToTop = */false, |
| /* opt_navigateToNextFile = */!e.detail.keyboardEvent.repeat); |
| } |
| } |
| |
| _handlePrevChunkOrCommentThread(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| |
| e.preventDefault(); |
| if (e.detail.keyboardEvent.shiftKey) { |
| this.$.cursor.moveToPreviousCommentThread(); |
| } else { |
| if (this.modifierPressed(e)) { return; } |
| this.$.cursor.moveToPreviousChunk(); |
| } |
| } |
| |
| _handleOpenReplyDialogOrToggleLeftPane(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| |
| if (e.detail.keyboardEvent.shiftKey) { // Hide left diff. |
| e.preventDefault(); |
| this.$.diffHost.toggleLeftDiff(); |
| return; |
| } |
| |
| if (this.modifierPressed(e)) { return; } |
| |
| if (!this._loggedIn) { return; } |
| |
| this.set('changeViewState.showReplyDialog', true); |
| e.preventDefault(); |
| this._navToChangeView(); |
| } |
| |
| _handleOpenDownloadDialog(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| if (this.modifierPressed(e)) { return; } |
| this.set('changeViewState.showDownloadDialog', true); |
| e.preventDefault(); |
| this._navToChangeView(); |
| } |
| |
| _handleUpToChange(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this._navToChangeView(); |
| } |
| |
| _handleCommaKey(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| if (this._diffPrefsDisabled) { return; } |
| |
| e.preventDefault(); |
| this.$.diffPreferencesDialog.open(); |
| } |
| |
| _handleToggleDiffMode(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) { |
| this.$.modeSelect.setMode(DiffViewMode.UNIFIED); |
| } else { |
| this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE); |
| } |
| } |
| |
| _navToChangeView() { |
| if (!this._changeNum || !this._patchRange.patchNum) { return; } |
| this._navigateToChange( |
| this._change, |
| this._patchRange, |
| this._change && this._change.revisions); |
| } |
| |
| _navToFile(path, fileList, direction) { |
| const newPath = this._getNavLinkPath(path, fileList, direction); |
| if (!newPath) { return; } |
| |
| if (newPath.up) { |
| this._navigateToChange( |
| this._change, |
| this._patchRange, |
| this._change && this._change.revisions); |
| return; |
| } |
| |
| GerritNav.navigateToDiff(this._change, newPath.path, |
| this._patchRange.patchNum, this._patchRange.basePatchNum); |
| } |
| |
| /** |
| * @param {?string} path The path of the current file being shown. |
| * @param {!Array<string>} fileList The list of files in this change and |
| * patch range. |
| * @param {number} direction Either 1 (next file) or -1 (prev file). |
| * @param {(number|boolean)} opt_noUp Whether to return to the change view |
| * when advancing the file goes outside the bounds of fileList. |
| * |
| * @return {?string} The next URL when proceeding in the specified |
| * direction. |
| */ |
| _computeNavLinkURL(change, path, fileList, direction, opt_noUp) { |
| const newPath = this._getNavLinkPath(path, fileList, direction, opt_noUp); |
| if (!newPath) { return null; } |
| |
| if (newPath.up) { |
| return this._getChangePath( |
| this._change, |
| this._patchRange, |
| this._change && this._change.revisions); |
| } |
| return this._getDiffUrl(this._change, this._patchRange, newPath.path); |
| } |
| |
| _goToEditFile() { |
| // TODO(taoalpha): add a shortcut for editing |
| const cursorAddress = this.$.cursor.getAddress(); |
| const editUrl = GerritNav.getEditUrlForDiff( |
| this._change, |
| this._path, |
| this._patchRange.patchNum, |
| cursorAddress && cursorAddress.number |
| ); |
| return GerritNav.navigateToRelativeUrl(editUrl); |
| } |
| |
| /** |
| * Gives an object representing the target of navigating either left or |
| * right through the change. The resulting object will have one of the |
| * following forms: |
| * * {path: "<target file path>"} - When another file path should be the |
| * result of the navigation. |
| * * {up: true} - When the result of navigating should go back to the |
| * change view. |
| * * null - When no navigation is possible for the given direction. |
| * |
| * @param {?string} path The path of the current file being shown. |
| * @param {!Array<string>} fileList The list of files in this change and |
| * patch range. |
| * @param {number} direction Either 1 (next file) or -1 (prev file). |
| * @param {?number|boolean=} opt_noUp Whether to return to the change view |
| * when advancing the file goes outside the bounds of fileList. |
| * @return {?Object} |
| */ |
| _getNavLinkPath(path, fileList, direction, opt_noUp) { |
| if (!path || !fileList || fileList.length === 0) { return null; } |
| |
| let idx = fileList.indexOf(path); |
| if (idx === -1) { |
| const file = direction > 0 ? |
| fileList[0] : |
| fileList[fileList.length - 1]; |
| return {path: file}; |
| } |
| |
| idx += direction; |
| // Redirect to the change view if opt_noUp isn’t truthy and idx falls |
| // outside the bounds of [0, fileList.length). |
| if (idx < 0 || idx > fileList.length - 1) { |
| if (opt_noUp) { return null; } |
| return {up: true}; |
| } |
| |
| return {path: fileList[idx]}; |
| } |
| |
| _getReviewedFiles(changeNum, patchNum) { |
| return this.$.restAPI.getReviewedFiles(changeNum, patchNum) |
| .then(files => { |
| this._reviewedFiles = new Set(files); |
| return this._reviewedFiles; |
| }); |
| } |
| |
| _getReviewedStatus(editMode, changeNum, patchNum, path) { |
| if (editMode) { return Promise.resolve(false); } |
| return this._getReviewedFiles(changeNum, patchNum) |
| .then(files => files.has(path)); |
| } |
| |
| _initLineOfInterestAndCursor(leftSide) { |
| this.$.diffHost.lineOfInterest = |
| this._getLineOfInterest({ |
| leftSide, |
| }); |
| this._initCursor({ |
| leftSide, |
| }); |
| } |
| |
| _displayDiffBaseAgainstLeftToast() { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| // \u2190 = ← |
| message: `Patchset ${this._patchRange.basePatchNum} vs ` + |
| `${this._patchRange.patchNum} selected. Press v + \u2190 to view ` |
| + `Base vs ${this._patchRange.basePatchNum}`, |
| }, |
| composed: true, bubbles: true, |
| })); |
| } |
| |
| _displayDiffAgainstLatestToast(latestPatchNum) { |
| const leftPatchset = patchNumEquals( |
| this._patchRange.basePatchNum, 'PARENT') |
| ? 'Base' : `Patchset ${this._patchRange.basePatchNum}`; |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| // \u2191 = ↑ |
| message: `${leftPatchset} vs |
| ${this._patchRange.patchNum} selected\n. Press v + \u2191 to view |
| ${leftPatchset} vs Patchset ${latestPatchNum}`, |
| }, |
| composed: true, bubbles: true, |
| })); |
| } |
| |
| _displayToasts() { |
| if (!patchNumEquals(this._patchRange.basePatchNum, 'PARENT')) { |
| this._displayDiffBaseAgainstLeftToast(); |
| return; |
| } |
| const latestPatchNum = computeLatestPatchNum(this._allPatchSets); |
| if (!patchNumEquals(this._patchRange.patchNum, latestPatchNum)) { |
| this._displayDiffAgainstLatestToast(latestPatchNum); |
| return; |
| } |
| } |
| |
| _initCommitRange() { |
| let commit; |
| let baseCommit; |
| if (!this._patchRange || !this._patchRange.patchNum) return; |
| for (const commitSha in this._change.revisions) { |
| if (!this._change.revisions.hasOwnProperty(commitSha)) continue; |
| const revision = this._change.revisions[commitSha]; |
| const patchNum = revision._number.toString(); |
| if (patchNum === this._patchRange.patchNum) { |
| commit = commitSha; |
| const commitObj = revision.commit || {}; |
| const parents = commitObj.parents || []; |
| if (this._patchRange.basePatchNum === PARENT && parents.length) { |
| baseCommit = parents[parents.length - 1].commit; |
| } |
| } else if (patchNum === this._patchRange.basePatchNum) { |
| baseCommit = commitSha; |
| } |
| } |
| this._commitRange = {commit, baseCommit}; |
| } |
| |
| _initPatchRange() { |
| let leftSide; |
| if (this.params.commentId) { |
| const comment = this._changeComments.findCommentById( |
| this.params.commentId); |
| if (!comment) { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| message: 'comment not found', |
| }, |
| composed: true, bubbles: true, |
| })); |
| GerritNav.navigateToChange(this._change); |
| return; |
| } |
| this._path = comment.path; |
| const latestPatchNum = computeLatestPatchNum(this._allPatchSets); |
| if (patchNumEquals(latestPatchNum, comment.patch_set)) { |
| this._patchRange = { |
| patchNum: latestPatchNum, |
| basePatchNum: PARENT, |
| }; |
| leftSide = comment.__commentSide === 'left'; |
| } else { |
| this._patchRange = { |
| patchNum: latestPatchNum, |
| basePatchNum: comment.patch_set, |
| }; |
| // comment is now on the left side since we are showing |
| // comment.patch_set vs latest |
| leftSide = true; |
| } |
| this._focusLineNum = comment.line; |
| } else { |
| if (this.params.path) { |
| this._path = this.params.path; |
| } |
| if (this.params.patchNum) { |
| this._patchRange = { |
| patchNum: this.params.patchNum, |
| basePatchNum: this.params.basePatchNum || PARENT, |
| }; |
| } |
| if (this.params.lineNum) { |
| this._focusLineNum = this.params.lineNum; |
| leftSide = this.params.leftSide; |
| } |
| } |
| this._initLineOfInterestAndCursor(leftSide); |
| this._commentMap = this._getPaths(this._patchRange); |
| |
| this._commentsForDiff = this._getCommentsForPath(this._path, |
| this._patchRange, this._projectConfig); |
| } |
| |
| _isFileUnchanged(diff) { |
| if (!diff || !diff.content) return false; |
| return !diff.content.some(content => |
| (content.a && !content.common) || |
| (content.b && !content.common) |
| ); |
| } |
| |
| _paramsChanged(value) { |
| if (value.view !== GerritNav.View.DIFF) { return; } |
| |
| this._change = undefined; |
| this._files = undefined; |
| this._path = undefined; |
| this._patchRange = undefined; |
| this._commitRange = undefined; |
| this._changeComments = undefined; |
| this._focusLineNum = undefined; |
| |
| if (value.changeNum && value.project) { |
| this.$.restAPI.setInProjectLookup(value.changeNum, value.project); |
| } |
| |
| this._changeNum = value.changeNum; |
| this.classList.remove('hideComments'); |
| |
| // 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 route is of type /comment/<commentId>/ then no patchNum is present |
| if (!value.patchNum && !value.commentLink) { |
| console.warn('invalid url, no patchNum found'); |
| return; |
| } |
| |
| const promises = []; |
| |
| promises.push(this._getDiffPreferences()); |
| |
| promises.push(this._getPreferences().then(prefs => { |
| this._userPrefs = prefs; |
| })); |
| |
| promises.push(this._getChangeDetail(this._changeNum)); |
| promises.push(this._loadComments()); |
| |
| promises.push(this._getChangeEdit(this._changeNum)); |
| |
| this.$.diffHost.cancel(); |
| this.$.diffHost.clearDiffContent(); |
| this._loading = true; |
| return Promise.all(promises) |
| .then(r => { |
| this._loading = false; |
| this._initPatchRange(); |
| this._initCommitRange(); |
| this.$.diffHost.comments = this._commentsForDiff; |
| const edit = r[4]; |
| if (edit) { |
| this.set('_change.revisions.' + edit.commit.commit, { |
| _number: SPECIAL_PATCH_SET_NUM.EDIT, |
| basePatchNum: edit.base_patch_set_number, |
| commit: edit.commit, |
| }); |
| } |
| return this.$.diffHost.reload(true); |
| }) |
| .then(() => { |
| this.reporting.diffViewFullyLoaded(); |
| // If diff view displayed has not ended yet, it ends here. |
| this.reporting.diffViewDisplayed(); |
| }) |
| .then(() => { |
| const fileUnchanged = this._isFileUnchanged(this._diff); |
| if (fileUnchanged && value.commentLink) { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| message: `File is unchanged between Patchset |
| ${this._patchRange.basePatchNum} and |
| ${this._patchRange.patchNum}. Showing diff of Base vs |
| ${this._patchRange.basePatchNum}`, |
| }, |
| composed: true, bubbles: true, |
| })); |
| GerritNav.navigateToDiff( |
| this._change, this._path, this._patchRange.basePatchNum, |
| 'PARENT', this._focusLineNum); |
| return; |
| } |
| if (value.commentLink) { |
| this._displayToasts(); |
| } |
| // If the blame was loaded for a previous file and user navigates to |
| // another file, then we load the blame for this file too |
| if (this._isBlameLoaded) this._loadBlame(); |
| }); |
| } |
| |
| _changeViewStateChanged(changeViewState) { |
| if (changeViewState.diffMode === null) { |
| // If screen size is small, always default to unified view. |
| this.$.restAPI.getPreferences().then(prefs => { |
| this.set('changeViewState.diffMode', prefs.default_diff_view); |
| }); |
| } |
| } |
| |
| _setReviewedObserver(_loggedIn, paramsRecord, _prefs, patchRangeRecord) { |
| // Polymer 2: check for undefined |
| if ([_loggedIn, paramsRecord, _prefs, patchRangeRecord, |
| patchRangeRecord.base].includes( |
| undefined)) { |
| return; |
| } |
| const patchRange = patchRangeRecord.base; |
| const params = paramsRecord.base || {}; |
| if (!_loggedIn) { return; } |
| |
| if (_prefs.manual_review) { |
| // Checkbox state needs to be set explicitly only when manual_review |
| // is specified. |
| |
| if (patchRange.patchNum) { |
| this._getReviewedStatus(this.editMode, this._changeNum, |
| patchRange.patchNum, this._path).then(status => { |
| this.$.reviewed.checked = status; |
| }); |
| } |
| return; |
| } |
| |
| if (params.view === GerritNav.View.DIFF) { |
| this._setReviewed(true); |
| } |
| } |
| |
| /** |
| * If the params specify a diff address then configure the diff cursor. |
| */ |
| _initCursor(params) { |
| if (this._focusLineNum === undefined) { return; } |
| if (params.leftSide) { |
| this.$.cursor.side = DiffSides.LEFT; |
| } else { |
| this.$.cursor.side = DiffSides.RIGHT; |
| } |
| this.$.cursor.initialLineNumber = this._focusLineNum; |
| } |
| |
| _getLineOfInterest(params) { |
| // If there is a line number specified, pass it along to the diff so that |
| // it will not get collapsed. |
| if (!this._focusLineNum) { return null; } |
| return {number: this._focusLineNum, leftSide: params.leftSide}; |
| } |
| |
| _pathChanged(path) { |
| if (path) { |
| this.dispatchEvent(new CustomEvent('title-change', { |
| detail: {title: computeTruncatedPath(path)}, |
| composed: true, bubbles: true, |
| })); |
| } |
| |
| if (!this._fileList || this._fileList.length == 0) { return; } |
| |
| this.set('changeViewState.selectedFileIndex', |
| this._fileList.indexOf(path)); |
| } |
| |
| _getDiffUrl(change, patchRange, path) { |
| if ([change, patchRange, path].includes(undefined)) { |
| return ''; |
| } |
| return GerritNav.getUrlForDiff(change, path, patchRange.patchNum, |
| patchRange.basePatchNum); |
| } |
| |
| _patchRangeStr(patchRange) { |
| let patchStr = patchRange.patchNum; |
| if (patchRange.basePatchNum != null && |
| patchRange.basePatchNum != PARENT) { |
| patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum; |
| } |
| return patchStr; |
| } |
| |
| /** |
| * When the latest patch of the change is selected (and there is no base |
| * patch) then the patch range need not appear in the URL. Return a patch |
| * range object with undefined values when a range is not needed. |
| * |
| * @param {!Object} patchRange |
| * @param {!Object} revisions |
| * @return {!Object} |
| */ |
| _getChangeUrlRange(patchRange, revisions) { |
| let patchNum = undefined; |
| let basePatchNum = undefined; |
| let latestPatchNum = -1; |
| for (const rev of Object.values(revisions || {})) { |
| latestPatchNum = Math.max(latestPatchNum, rev._number); |
| } |
| if (patchRange.basePatchNum !== PARENT || |
| parseInt(patchRange.patchNum, 10) !== latestPatchNum) { |
| patchNum = patchRange.patchNum; |
| basePatchNum = patchRange.basePatchNum; |
| } |
| return {patchNum, basePatchNum}; |
| } |
| |
| _getChangePath(change, patchRange, revisions) { |
| if ([change, patchRange].includes(undefined)) { |
| return ''; |
| } |
| const range = this._getChangeUrlRange(patchRange, revisions); |
| return GerritNav.getUrlForChange(change, range.patchNum, |
| range.basePatchNum); |
| } |
| |
| _navigateToChange(change, patchRange, revisions) { |
| const range = this._getChangeUrlRange(patchRange, revisions); |
| GerritNav.navigateToChange(change, range.patchNum, range.basePatchNum); |
| } |
| |
| _computeChangePath(change, patchRangeRecord, revisions) { |
| return this._getChangePath(change, patchRangeRecord.base, revisions); |
| } |
| |
| _formatFilesForDropdown(files, patchNum, changeComments) { |
| // Polymer 2: check for undefined |
| if ([ |
| files, |
| patchNum, |
| changeComments, |
| ].includes(undefined)) { |
| return; |
| } |
| |
| if (!files) { return; } |
| const dropdownContent = []; |
| for (const path of files.sortedFileList) { |
| dropdownContent.push({ |
| text: computeDisplayPath(path), |
| mobileText: computeTruncatedPath(path), |
| value: path, |
| bottomText: this._computeCommentString(changeComments, patchNum, |
| path, files.changeFilesByPath[path]), |
| }); |
| } |
| return dropdownContent; |
| } |
| |
| _computeCommentString(changeComments, patchNum, path, changeFileInfo) { |
| const unresolvedCount = changeComments.computeUnresolvedNum({patchNum, |
| path}); |
| const commentCount = changeComments.computeCommentCount({patchNum, path}); |
| const commentString = GrCountStringFormatter.computePluralString( |
| commentCount, 'comment'); |
| const unresolvedString = GrCountStringFormatter.computeString( |
| unresolvedCount, 'unresolved'); |
| |
| const unmodifiedString = changeFileInfo.status === 'U' ? 'no changes': ''; |
| |
| return [ |
| unmodifiedString, |
| commentString, |
| unresolvedString] |
| .filter(v => v && v.length > 0).join(', '); |
| } |
| |
| _computePrefsButtonHidden(prefs, prefsDisabled) { |
| return prefsDisabled || !prefs; |
| } |
| |
| _handleFileChange(e) { |
| // This is when it gets set initially. |
| const path = e.detail.value; |
| if (path === this._path) { |
| return; |
| } |
| |
| GerritNav.navigateToDiff(this._change, path, this._patchRange.patchNum, |
| this._patchRange.basePatchNum); |
| } |
| |
| _handleFileTap(e) { |
| // async is needed so that that the click event is fired before the |
| // dropdown closes (This was a bug for touch devices). |
| this.async(() => { |
| this.$.dropdown.close(); |
| }, 1); |
| } |
| |
| _handlePatchChange(e) { |
| const {basePatchNum, patchNum} = e.detail; |
| if (patchNumEquals(basePatchNum, this._patchRange.basePatchNum) && |
| patchNumEquals(patchNum, this._patchRange.patchNum)) { return; } |
| GerritNav.navigateToDiff( |
| this._change, this._path, patchNum, basePatchNum); |
| } |
| |
| _handlePrefsTap(e) { |
| e.preventDefault(); |
| this.$.diffPreferencesDialog.open(); |
| } |
| |
| /** |
| * _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 or they |
| * are on a mobile device. 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() { |
| if (this.changeViewState.diffMode) { |
| return this.changeViewState.diffMode; |
| } else if (this._userPrefs) { |
| this.set('changeViewState.diffMode', this._userPrefs.default_diff_view); |
| return this._userPrefs.default_diff_view; |
| } else { |
| return 'SIDE_BY_SIDE'; |
| } |
| } |
| |
| _computeModeSelectHideClass(_diff) { |
| return _diff.binary ? 'hide' : ''; |
| } |
| |
| _onLineSelected(e, detail) { |
| if (!this._change) { return; } |
| const number = detail.number; |
| // for on-comment-anchor-tap side can be PARENT/REVISIONS |
| // for on-line-selected side can be LEFT/RIGHT |
| const leftSide = detail.side === 'LEFT' || detail.side === 'PARENT'; |
| const url = GerritNav.getUrlForDiffById(this._changeNum, |
| this._change.project, this._path, this._patchRange.patchNum, |
| this._patchRange.basePatchNum, number, leftSide); |
| history.replaceState(null, '', url); |
| } |
| |
| _computeDownloadDropdownLinks( |
| project, changeNum, patchRange, path, diff) { |
| if (!patchRange || !patchRange.patchNum) { return []; } |
| |
| const links = [ |
| { |
| url: this._computeDownloadPatchLink( |
| project, changeNum, patchRange, path), |
| name: 'Patch', |
| }, |
| ]; |
| |
| if (diff && diff.meta_a) { |
| let leftPath = path; |
| if (diff.change_type === 'RENAMED') { |
| leftPath = diff.meta_a.name; |
| } |
| links.push( |
| { |
| url: this._computeDownloadFileLink( |
| project, changeNum, patchRange, leftPath, true), |
| name: 'Left Content', |
| } |
| ); |
| } |
| |
| if (diff && diff.meta_b) { |
| links.push( |
| { |
| url: this._computeDownloadFileLink( |
| project, changeNum, patchRange, path, false), |
| name: 'Right Content', |
| } |
| ); |
| } |
| |
| return links; |
| } |
| |
| _computeDownloadFileLink( |
| project, changeNum, patchRange, path, isBase) { |
| let patchNum = patchRange.patchNum; |
| |
| const comparedAgainsParent = patchRange.basePatchNum === 'PARENT'; |
| |
| if (isBase && !comparedAgainsParent) { |
| patchNum = patchRange.basePatchNum; |
| } |
| |
| let url = changeBaseURL(project, changeNum, patchNum) + |
| `/files/${encodeURIComponent(path)}/download`; |
| |
| if (isBase && comparedAgainsParent) { |
| url += '?parent=1'; |
| } |
| |
| return url; |
| } |
| |
| _computeDownloadPatchLink(project, changeNum, patchRange, path) { |
| let url = changeBaseURL(project, changeNum, patchRange.patchNum); |
| url += '/patch?zip&path=' + encodeURIComponent(path); |
| return url; |
| } |
| |
| _loadComments() { |
| return this.$.commentAPI.loadAll(this._changeNum).then(comments => { |
| this._changeComments = comments; |
| }); |
| } |
| |
| _recomputeComments(files, path, patchRange, projectConfig) { |
| // Polymer 2: check for undefined |
| if ([ |
| files, |
| path, |
| patchRange, |
| projectConfig, |
| ].includes(undefined)) { |
| return undefined; |
| } |
| |
| const file = files[path]; |
| if (file && file.old_path) { |
| this._commentsForDiff = this._changeComments.getCommentsBySideForFile( |
| {path, basePath: file.old_path}, |
| patchRange, |
| projectConfig); |
| |
| this.$.diffHost.comments = this._commentsForDiff; |
| } |
| } |
| |
| _getPaths(patchRange) { |
| return this._changeComments.getPaths(patchRange); |
| } |
| |
| _getCommentsForPath(path, patchRange, projectConfig) { |
| return this._changeComments.getCommentsBySideForPath(path, patchRange, |
| projectConfig); |
| } |
| |
| _getDiffDrafts() { |
| return this.$.restAPI.getDiffDrafts(this._changeNum); |
| } |
| |
| _computeCommentSkips(commentMap, fileList, path) { |
| // Polymer 2: check for undefined |
| if ([ |
| commentMap, |
| fileList, |
| path, |
| ].includes(undefined)) { |
| return undefined; |
| } |
| |
| const skips = {previous: null, next: null}; |
| if (!fileList.length) { return skips; } |
| const pathIndex = fileList.indexOf(path); |
| |
| // Scan backward for the previous file. |
| for (let i = pathIndex - 1; i >= 0; i--) { |
| if (commentMap[fileList[i]]) { |
| skips.previous = fileList[i]; |
| break; |
| } |
| } |
| |
| // Scan forward for the next file. |
| for (let i = pathIndex + 1; i < fileList.length; i++) { |
| if (commentMap[fileList[i]]) { |
| skips.next = fileList[i]; |
| break; |
| } |
| } |
| |
| return skips; |
| } |
| |
| _computeContainerClass(editMode) { |
| return editMode ? 'editMode' : ''; |
| } |
| |
| /** |
| * @param {!Object} patchRangeRecord |
| */ |
| _computeEditMode(patchRangeRecord) { |
| const patchRange = patchRangeRecord.base || {}; |
| return patchNumEquals(patchRange.patchNum, SPECIAL_PATCH_SET_NUM.EDIT); |
| } |
| |
| _computeBlameToggleLabel(loaded, loading) { |
| if (loaded) { return 'Hide blame'; } |
| return 'Show blame'; |
| } |
| |
| _loadBlame() { |
| this._isBlameLoading = true; |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: {message: MSG_LOADING_BLAME}, |
| composed: true, bubbles: true, |
| })); |
| this.$.diffHost.loadBlame() |
| .then(() => { |
| this._isBlameLoading = false; |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: {message: MSG_LOADED_BLAME}, |
| composed: true, bubbles: true, |
| })); |
| }) |
| .catch(() => { |
| this._isBlameLoading = false; |
| }); |
| } |
| |
| /** |
| * Load and display blame information if it has not already been loaded. |
| * Otherwise hide it. |
| */ |
| _toggleBlame() { |
| if (this._isBlameLoaded) { |
| this.$.diffHost.clearBlame(); |
| return; |
| } |
| this._loadBlame(); |
| } |
| |
| _handleToggleBlame(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| this._toggleBlame(); |
| } |
| |
| _handleToggleHideAllCommentThreads(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| this.toggleClass('hideComments'); |
| } |
| |
| _handleDiffAgainstBase(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| if (patchNumEquals(this._patchRange.basePatchNum, |
| SPECIAL_PATCH_SET_NUM.PARENT)) { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| message: 'Base is already selected.', |
| }, |
| composed: true, bubbles: true, |
| })); |
| return; |
| } |
| GerritNav.navigateToDiff( |
| this._change, this._path, this._patchRange.patchNum); |
| } |
| |
| _handleDiffBaseAgainstLeft(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| if (patchNumEquals(this._patchRange.basePatchNum, |
| SPECIAL_PATCH_SET_NUM.PARENT)) { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| message: 'Left is already base.', |
| }, |
| composed: true, bubbles: true, |
| })); |
| return; |
| } |
| GerritNav.navigateToDiff(this._change, this._path, |
| this._patchRange.basePatchNum); |
| } |
| |
| _handleDiffAgainstLatest(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| |
| const latestPatchNum = computeLatestPatchNum(this._allPatchSets); |
| if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| message: 'Latest is already selected.', |
| }, |
| composed: true, bubbles: true, |
| })); |
| return; |
| } |
| |
| GerritNav.navigateToDiff( |
| this._change, this._path, latestPatchNum, |
| this._patchRange.basePatchNum); |
| } |
| |
| _handleDiffRightAgainstLatest(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| const latestPatchNum = computeLatestPatchNum(this._allPatchSets); |
| if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| message: 'Right is already latest.', |
| }, |
| composed: true, bubbles: true, |
| })); |
| return; |
| } |
| GerritNav.navigateToDiff(this._change, this._path, latestPatchNum, |
| this._patchRange.patchNum); |
| } |
| |
| _handleDiffBaseAgainstLatest(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| const latestPatchNum = computeLatestPatchNum(this._allPatchSets); |
| if (patchNumEquals(this._patchRange.patchNum, latestPatchNum) && |
| patchNumEquals(this._patchRange.basePatchNum, |
| SPECIAL_PATCH_SET_NUM.PARENT)) { |
| this.dispatchEvent(new CustomEvent('show-alert', { |
| detail: { |
| message: 'Already diffing base against latest.', |
| }, |
| composed: true, bubbles: true, |
| })); |
| return; |
| } |
| GerritNav.navigateToDiff(this._change, this._path, latestPatchNum); |
| } |
| |
| _computeBlameLoaderClass(isImageDiff, path) { |
| return !isMagicPath(path) && !isImageDiff ? 'show' : ''; |
| } |
| |
| _getRevisionInfo(change) { |
| return new RevisionInfo(change); |
| } |
| |
| _computeFileNum(file, files) { |
| // Polymer 2: check for undefined |
| if ([file, files].includes(undefined)) { |
| return undefined; |
| } |
| |
| return files.findIndex(({value}) => value === file) + 1; |
| } |
| |
| /** |
| * @param {number} fileNum |
| * @param {!Array<string>} files |
| * @return {string} |
| */ |
| _computeFileNumClass(fileNum, files) { |
| if (files && fileNum > 0) { |
| return 'show'; |
| } |
| return ''; |
| } |
| |
| _handleExpandAllDiffContext(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| this.$.diffHost.expandAllContext(); |
| } |
| |
| _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) { |
| return disableDiffPrefs || !loggedIn; |
| } |
| |
| _handleNextUnreviewedFile(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| this._setReviewed(true); |
| // Ensure that the currently viewed file always appears in unreviewedFiles |
| // so we resolve the right "next" file. |
| const unreviewedFiles = this._fileList |
| .filter(file => |
| (file === this._path || !this._reviewedFiles.has(file))); |
| this._navToFile(this._path, unreviewedFiles, 1); |
| } |
| |
| _handleReloadingDiffPreference() { |
| this._getDiffPreferences(); |
| } |
| |
| _computeCanEdit(loggedIn, changeChangeRecord) { |
| if ([changeChangeRecord, changeChangeRecord.base] |
| .some(arg => arg === undefined)) { |
| return false; |
| } |
| return loggedIn && changeIsOpen(changeChangeRecord.base); |
| } |
| |
| _computeIsLoggedIn(loggedIn) { |
| return loggedIn ? true : false; |
| } |
| |
| /** |
| * Wrapper for using in the element template and computed properties |
| */ |
| _computeAllPatchSets(change) { |
| return computeAllPatchSets(change); |
| } |
| |
| /** |
| * Wrapper for using in the element template and computed properties |
| */ |
| _computeDisplayPath(path) { |
| return computeDisplayPath(path); |
| } |
| } |
| |
| customElements.define(GrDiffView.is, GrDiffView); |