| // 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 CHANGE_ID_ERROR = { |
| MISMATCH: 'mismatch', |
| MISSING: 'missing', |
| }; |
| var CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm; |
| var COMMENT_SAVE = 'Saving... Try again after all comments are saved.'; |
| |
| var MIN_LINES_FOR_COMMIT_COLLAPSE = 30; |
| |
| // Maximum length for patch set descriptions. |
| var PATCH_DESC_MAX_LENGTH = 500; |
| var REVIEWERS_REGEX = /^R=/gm; |
| |
| Polymer({ |
| is: 'gr-change-view', |
| |
| /** |
| * Fired when the title of the page should change. |
| * |
| * @event title-change |
| */ |
| |
| /** |
| * Fired if an error occurs when fetching the change data. |
| * |
| * @event page-error |
| */ |
| |
| properties: { |
| /** |
| * URL params passed from the router. |
| */ |
| params: { |
| type: Object, |
| observer: '_paramsChanged', |
| }, |
| viewState: { |
| type: Object, |
| notify: true, |
| value: function() { return {}; }, |
| }, |
| backPage: String, |
| hasParent: Boolean, |
| serverConfig: Object, |
| keyEventTarget: { |
| type: Object, |
| value: function() { return document.body; }, |
| }, |
| |
| _account: { |
| type: Object, |
| value: {}, |
| }, |
| _comments: Object, |
| _change: { |
| type: Object, |
| observer: '_changeChanged', |
| }, |
| _commitInfo: Object, |
| _files: Object, |
| _changeNum: String, |
| _diffDrafts: { |
| type: Object, |
| value: function() { return {}; }, |
| }, |
| _editingCommitMessage: { |
| type: Boolean, |
| value: false, |
| }, |
| _hideEditCommitMessage: { |
| type: Boolean, |
| computed: '_computeHideEditCommitMessage(_loggedIn, ' + |
| '_editingCommitMessage, _change)', |
| }, |
| _latestCommitMessage: { |
| type: String, |
| value: '', |
| }, |
| _lineHeight: Number, |
| _changeIdCommitMessageError: { |
| type: String, |
| computed: |
| '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)', |
| }, |
| _patchRange: { |
| type: Object, |
| observer: '_updateSelected', |
| }, |
| _relatedChangesLoading: { |
| type: Boolean, |
| value: true, |
| }, |
| _currentRevisionActions: Object, |
| _allPatchSets: { |
| type: Array, |
| computed: '_computeAllPatchSets(_change, _change.revisions.*)', |
| }, |
| _loggedIn: { |
| type: Boolean, |
| value: false, |
| }, |
| _loading: Boolean, |
| _projectConfig: Object, |
| _rebaseOnCurrent: Boolean, |
| _replyButtonLabel: { |
| type: String, |
| value: 'Reply', |
| computed: '_computeReplyButtonLabel(_diffDrafts.*)', |
| }, |
| _selectedPatchSet: String, |
| _initialLoadComplete: { |
| type: Boolean, |
| value: false, |
| }, |
| _descriptionReadOnly: { |
| type: Boolean, |
| computed: '_computeDescriptionReadOnly(_loggedIn, _change, _account)', |
| }, |
| _replyDisabled: { |
| type: Boolean, |
| value: true, |
| computed: '_computeReplyDisabled(serverConfig)', |
| }, |
| _changeStatus: { |
| type: String, |
| computed: '_computeChangeStatus(_change, _patchRange.patchNum)', |
| }, |
| _commitCollapsed: { |
| type: Boolean, |
| value: true, |
| }, |
| _relatedChangesCollapsed: { |
| type: Boolean, |
| value: true, |
| }, |
| }, |
| |
| behaviors: [ |
| Gerrit.BaseUrlBehavior, |
| Gerrit.KeyboardShortcutBehavior, |
| Gerrit.PatchSetBehavior, |
| Gerrit.RESTClientBehavior, |
| ], |
| |
| observers: [ |
| '_labelsChanged(_change.labels.*)', |
| '_paramsAndChangeChanged(params, _change)', |
| ], |
| |
| keyBindings: { |
| 'shift+r': '_handleCapitalRKey', |
| 'a': '_handleAKey', |
| 'd': '_handleDKey', |
| 's': '_handleSKey', |
| 'u': '_handleUKey', |
| 'x': '_handleXKey', |
| 'z': '_handleZKey', |
| }, |
| |
| attached: function() { |
| this._getLoggedIn().then(function(loggedIn) { |
| this._loggedIn = loggedIn; |
| if (loggedIn) { |
| this.$.restAPI.getAccount().then(function(acct) { |
| this._account = acct; |
| }.bind(this)); |
| } |
| }.bind(this)); |
| |
| this.addEventListener('comment-save', this._handleCommentSave.bind(this)); |
| this.addEventListener('comment-discard', |
| this._handleCommentDiscard.bind(this)); |
| this.addEventListener('editable-content-save', |
| this._handleCommitMessageSave.bind(this)); |
| this.addEventListener('editable-content-cancel', |
| this._handleCommitMessageCancel.bind(this)); |
| this.listen(window, 'scroll', '_handleScroll'); |
| }, |
| |
| detached: function() { |
| this.unlisten(window, 'scroll', '_handleScroll'); |
| }, |
| |
| _handleEditCommitMessage: function(e) { |
| this._editingCommitMessage = true; |
| this.$.commitMessageEditor.focusTextarea(); |
| }, |
| |
| _handleCommitMessageSave: function(e) { |
| var message = e.detail.content; |
| |
| this.$.jsAPI.handleCommitMessage(this._change, message); |
| |
| this.$.commitMessageEditor.disabled = true; |
| this._saveCommitMessage(message).then(function(resp) { |
| this.$.commitMessageEditor.disabled = false; |
| if (!resp.ok) { return; } |
| |
| this._latestCommitMessage = this._prepareCommitMsgForLinkify(message); |
| this._editingCommitMessage = false; |
| this._reloadWindow(); |
| }.bind(this)).catch(function(err) { |
| this.$.commitMessageEditor.disabled = false; |
| }.bind(this)); |
| }, |
| |
| _reloadWindow: function() { |
| window.location.reload(); |
| }, |
| |
| _handleCommitMessageCancel: function(e) { |
| this._editingCommitMessage = false; |
| }, |
| |
| _saveCommitMessage: function(message) { |
| return this.$.restAPI.saveChangeCommitMessageEdit( |
| this._changeNum, message).then(function(resp) { |
| if (!resp.ok) { return resp; } |
| |
| return this.$.restAPI.publishChangeEdit(this._changeNum); |
| }.bind(this)); |
| }, |
| |
| _computeHideEditCommitMessage: function(loggedIn, editing, change) { |
| if (!loggedIn || editing || change.status === this.ChangeStatus.MERGED) { |
| return true; |
| } |
| |
| return false; |
| }, |
| |
| _handleCommentSave: function(e) { |
| if (!e.target.comment.__draft) { return; } |
| |
| var draft = e.target.comment; |
| draft.patch_set = draft.patch_set || this._patchRange.patchNum; |
| |
| // The use of path-based notification helpers (set, push) can’t be used |
| // because the paths could contain dots in them. A new object must be |
| // created to satisfy Polymer’s dirty checking. |
| // https://github.com/Polymer/polymer/issues/3127 |
| // TODO(andybons): Polyfill for Object.assign in IE. |
| var diffDrafts = Object.assign({}, this._diffDrafts); |
| if (!diffDrafts[draft.path]) { |
| diffDrafts[draft.path] = [draft]; |
| this._diffDrafts = diffDrafts; |
| return; |
| } |
| for (var i = 0; i < this._diffDrafts[draft.path].length; i++) { |
| if (this._diffDrafts[draft.path][i].id === draft.id) { |
| diffDrafts[draft.path][i] = draft; |
| this._diffDrafts = diffDrafts; |
| return; |
| } |
| } |
| diffDrafts[draft.path].push(draft); |
| diffDrafts[draft.path].sort(function(c1, c2) { |
| // No line number means that it’s a file comment. Sort it above the |
| // others. |
| return (c1.line || -1) - (c2.line || -1); |
| }); |
| this._diffDrafts = diffDrafts; |
| }, |
| |
| _handleCommentDiscard: function(e) { |
| if (!e.target.comment.__draft) { return; } |
| |
| var draft = e.target.comment; |
| if (!this._diffDrafts[draft.path]) { |
| return; |
| } |
| var index = -1; |
| for (var i = 0; i < this._diffDrafts[draft.path].length; i++) { |
| if (this._diffDrafts[draft.path][i].id === draft.id) { |
| index = i; |
| break; |
| } |
| } |
| if (index === -1) { |
| // It may be a draft that hasn’t been added to _diffDrafts since it was |
| // never saved. |
| return; |
| } |
| |
| draft.patch_set = draft.patch_set || this._patchRange.patchNum; |
| |
| // The use of path-based notification helpers (set, push) can’t be used |
| // because the paths could contain dots in them. A new object must be |
| // created to satisfy Polymer’s dirty checking. |
| // https://github.com/Polymer/polymer/issues/3127 |
| // TODO(andybons): Polyfill for Object.assign in IE. |
| var diffDrafts = Object.assign({}, this._diffDrafts); |
| diffDrafts[draft.path].splice(index, 1); |
| if (diffDrafts[draft.path].length === 0) { |
| delete diffDrafts[draft.path]; |
| } |
| this._diffDrafts = diffDrafts; |
| }, |
| |
| _handlePatchChange: function(e) { |
| this._changePatchNum(parseInt(e.target.value, 10), true); |
| }, |
| |
| _handleReplyTap: function(e) { |
| e.preventDefault(); |
| this._openReplyDialog(); |
| }, |
| |
| _handleDownloadTap: function(e) { |
| e.preventDefault(); |
| this.$.downloadOverlay.open().then(function() { |
| this.$.downloadOverlay |
| .setFocusStops(this.$.downloadDialog.getFocusStops()); |
| this.$.downloadDialog.focus(); |
| }.bind(this)); |
| }, |
| |
| _handleDownloadDialogClose: function(e) { |
| this.$.downloadOverlay.close(); |
| }, |
| |
| _handleMessageReply: function(e) { |
| var msg = e.detail.message.message; |
| var quoteStr = msg.split('\n').map( |
| function(line) { return '> ' + line; }).join('\n') + '\n\n'; |
| |
| if (quoteStr !== this.$.replyDialog.quote) { |
| this.$.replyDialog.draft = quoteStr; |
| } |
| this.$.replyDialog.quote = quoteStr; |
| this._openReplyDialog(); |
| }, |
| |
| _handleReplyOverlayOpen: function(e) { |
| this.$.replyDialog.focus(); |
| }, |
| |
| _handleReplySent: function(e) { |
| this.$.replyOverlay.close(); |
| this._reload(); |
| }, |
| |
| _handleReplyCancel: function(e) { |
| this.$.replyOverlay.close(); |
| }, |
| |
| _handleReplyAutogrow: function(e) { |
| this.$.replyOverlay.refit(); |
| }, |
| |
| _handleShowReplyDialog: function(e) { |
| var target = this.$.replyDialog.FocusTarget.REVIEWERS; |
| if (e.detail.value && e.detail.value.ccsOnly) { |
| target = this.$.replyDialog.FocusTarget.CCS; |
| } |
| this._openReplyDialog(target); |
| }, |
| |
| _handleScroll: function() { |
| this.debounce('scroll', function() { |
| history.replaceState( |
| { |
| scrollTop: document.body.scrollTop, |
| path: location.pathname, |
| }, |
| location.pathname); |
| }, 150); |
| }, |
| |
| _paramsChanged: function(value) { |
| if (value.view !== this.tagName.toLowerCase()) { |
| this._initialLoadComplete = false; |
| return; |
| } |
| |
| var patchChanged = this._patchRange && |
| (value.patchNum !== undefined && value.basePatchNum !== undefined) && |
| (this._patchRange.patchNum !== value.patchNum || |
| this._patchRange.basePatchNum !== value.basePatchNum); |
| |
| if (this._changeNum !== value.changeNum) { |
| this._initialLoadComplete = false; |
| } |
| |
| var patchRange = { |
| patchNum: value.patchNum, |
| basePatchNum: value.basePatchNum || 'PARENT', |
| }; |
| |
| if (this._initialLoadComplete && patchChanged) { |
| if (patchRange.patchNum == null) { |
| patchRange.patchNum = this._computeLatestPatchNum(this._allPatchSets); |
| } |
| this._patchRange = patchRange; |
| this._reloadPatchNumDependentResources().then(function() { |
| this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, { |
| change: this._change, |
| patchNum: patchRange.patchNum, |
| }); |
| }.bind(this)); |
| return; |
| } |
| |
| this._changeNum = value.changeNum; |
| this._patchRange = patchRange; |
| this.$.relatedChanges.clear(); |
| |
| this._reload().then(function() { |
| this._performPostLoadTasks(); |
| }.bind(this)); |
| }, |
| |
| _performPostLoadTasks: function() { |
| // Allow the message list and related changes to render before scrolling. |
| // Related changes are loaded here (after everything else) because they |
| // take the longest and are secondary information. Because the element may |
| // alter the total height of the page, the call to potentially scroll to |
| // a linked message is performed after related changes is fully loaded. |
| this.$.relatedChanges.reload().then(function() { |
| this.async(function() { |
| if (history.state && history.state.scrollTop) { |
| document.documentElement.scrollTop = |
| document.body.scrollTop = history.state.scrollTop; |
| } else { |
| this._maybeScrollToMessage(); |
| } |
| }, 1); |
| }.bind(this)); |
| |
| this._maybeShowReplyDialog(); |
| |
| this._maybeShowRevertDialog(); |
| |
| this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, { |
| change: this._change, |
| patchNum: this._patchRange.patchNum, |
| }); |
| |
| this._initialLoadComplete = true; |
| }, |
| |
| _paramsAndChangeChanged: function(value) { |
| // If the change number or patch range is different, then reset the |
| // selected file index. |
| var patchRangeState = this.viewState.patchRange; |
| if (this.viewState.changeNum !== this._changeNum || |
| patchRangeState.basePatchNum !== this._patchRange.basePatchNum || |
| patchRangeState.patchNum !== this._patchRange.patchNum) { |
| this._resetFileListViewState(); |
| } |
| }, |
| |
| _maybeScrollToMessage: function() { |
| var msgPrefix = '#message-'; |
| var hash = window.location.hash; |
| if (hash.indexOf(msgPrefix) === 0) { |
| this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length)); |
| } |
| }, |
| |
| _getLocationSearch: function() { |
| // Not inlining to make it easier to test. |
| return window.location.search; |
| }, |
| |
| _getUrlParameter: function(param) { |
| var pageURL = this._getLocationSearch().substring(1); |
| var vars = pageURL.split('&'); |
| for (var i = 0; i < vars.length; i++) { |
| var name = vars[i].split('='); |
| if (name[0] == param) { |
| return name[0]; |
| } |
| } |
| return null; |
| }, |
| |
| _maybeShowRevertDialog: function() { |
| Gerrit.awaitPluginsLoaded() |
| .then(this._getLoggedIn.bind(this)) |
| .then(function(loggedIn) { |
| if (!loggedIn || this._change.status !== this.ChangeStatus.MERGED) { |
| // Do not display dialog if not logged-in or the change is not |
| // merged. |
| return; |
| } |
| if (!!this._getUrlParameter('revert')) { |
| this.$.actions.showRevertDialog(); |
| } |
| }.bind(this)); |
| }, |
| |
| _maybeShowReplyDialog: function() { |
| this._getLoggedIn().then(function(loggedIn) { |
| if (!loggedIn) { return; } |
| |
| if (this.viewState.showReplyDialog) { |
| this._openReplyDialog(); |
| this.async(function() { this.$.replyOverlay.center(); }, 1); |
| this.set('viewState.showReplyDialog', false); |
| } |
| }.bind(this)); |
| }, |
| |
| _resetFileListViewState: function() { |
| this.set('viewState.selectedFileIndex', 0); |
| if (!!this.viewState.changeNum && |
| this.viewState.changeNum !== this._changeNum) { |
| // Reset the diff mode to null when navigating from one change to |
| // another, so that the user's preference is restored. |
| this.set('viewState.diffMode', null); |
| } |
| this.set('viewState.changeNum', this._changeNum); |
| this.set('viewState.patchRange', this._patchRange); |
| }, |
| |
| _changeChanged: function(change) { |
| if (!change) { return; } |
| this.set('_patchRange.basePatchNum', |
| this._patchRange.basePatchNum || 'PARENT'); |
| this.set('_patchRange.patchNum', |
| this._patchRange.patchNum || |
| this._computeLatestPatchNum(this._allPatchSets)); |
| |
| this._updateSelected(); |
| |
| var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; |
| this.fire('title-change', {title: title}); |
| }, |
| |
| /** |
| * Change active patch to the provided patch num. |
| * @param {number} patchNum the patchn number to be viewed. |
| * @param {boolean} opt_forceParams When set to true, the resulting URL will |
| * always include the patch range, even if the requested patchNum is |
| * known to be the latest. |
| */ |
| _changePatchNum: function(patchNum, opt_forceParams) { |
| if (!opt_forceParams) { |
| var currentPatchNum; |
| if (this._change.current_revision) { |
| currentPatchNum = |
| this._change.revisions[this._change.current_revision]._number; |
| } else { |
| currentPatchNum = this._computeLatestPatchNum(this._allPatchSets); |
| } |
| if (patchNum === currentPatchNum && |
| this._patchRange.basePatchNum === 'PARENT') { |
| page.show(this.changePath(this._changeNum)); |
| return; |
| } |
| } |
| var patchExpr = this._patchRange.basePatchNum === 'PARENT' ? patchNum : |
| this._patchRange.basePatchNum + '..' + patchNum; |
| page.show(this.changePath(this._changeNum) + '/' + patchExpr); |
| }, |
| |
| _computeChangePermalink: function(changeNum) { |
| return this.getBaseUrl() + '/' + changeNum; |
| }, |
| |
| _computeChangeStatus: function(change, patchNum) { |
| var statusString = this.changeStatusString(change); |
| if (change.status === this.ChangeStatus.NEW) { |
| var rev = this.getRevisionByPatchNum(change.revisions, patchNum); |
| if (rev && rev.draft === true) { |
| statusString = 'Draft'; |
| } |
| } |
| return statusString; |
| }, |
| |
| _computeShowCommitInfo: function(changeStatus, current_revision) { |
| return changeStatus === 'Merged' && current_revision; |
| }, |
| |
| _computeMergedCommitInfo: function(current_revision, revisions) { |
| var rev = revisions[current_revision]; |
| if (!rev || !rev.commit) { return {}; } |
| // CommitInfo.commit is optional. Set commit in all cases to avoid error |
| // in <gr-commit-info>. @see Issue 5337 |
| if (!rev.commit.commit) { rev.commit.commit = current_revision; } |
| return rev.commit; |
| }, |
| |
| _computeChangeIdClass: function(displayChangeId) { |
| return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : ''; |
| }, |
| |
| _computeTitleAttributeWarning: function(displayChangeId) { |
| if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) { |
| return 'Change-Id mismatch'; |
| } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) { |
| return 'No Change-Id in commit message'; |
| } |
| }, |
| |
| _computeChangeIdCommitMessageError: function(commitMessage, change) { |
| if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; } |
| |
| // Find the last match in the commit message: |
| var changeId; |
| var changeIdArr; |
| |
| while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) { |
| changeId = changeIdArr[1]; |
| } |
| |
| if (changeId) { |
| // A change-id is detected in the commit message. |
| |
| if (changeId === change.change_id) { |
| // The change-id found matches the real change-id. |
| return null; |
| } |
| // The change-id found does not match the change-id. |
| return CHANGE_ID_ERROR.MISMATCH; |
| } |
| // There is no change-id in the commit message. |
| return CHANGE_ID_ERROR.MISSING; |
| }, |
| |
| _computeLatestPatchNum: function(allPatchSets) { |
| return allPatchSets[allPatchSets.length - 1].num; |
| }, |
| |
| _computePatchInfoClass: function(patchNum, allPatchSets) { |
| if (parseInt(patchNum, 10) === |
| this._computeLatestPatchNum(allPatchSets)) { |
| return ''; |
| } |
| return 'patchInfo--oldPatchSet'; |
| }, |
| |
| /** |
| * Determines if a patch number should be disabled based on value of the |
| * basePatchNum from gr-file-list. |
| * @param {Number} patchNum Patch number available in dropdown |
| * @param {Number|String} basePatchNum Base patch number from file list |
| * @return {Boolean} |
| */ |
| _computePatchSetDisabled: function(patchNum, basePatchNum) { |
| basePatchNum = basePatchNum === 'PARENT' ? 0 : basePatchNum; |
| return parseInt(patchNum, 10) <= parseInt(basePatchNum, 10); |
| }, |
| |
| _computeAllPatchSets: function(change) { |
| var patchNums = []; |
| for (var commit in change.revisions) { |
| if (change.revisions.hasOwnProperty(commit)) { |
| patchNums.push({ |
| num: change.revisions[commit]._number, |
| desc: change.revisions[commit].description, |
| }); |
| } |
| } |
| return patchNums.sort(function(a, b) { return a.num - b.num; }); |
| }, |
| |
| _computeLabelNames: function(labels) { |
| return Object.keys(labels).sort(); |
| }, |
| |
| _computeLabelValues: function(labelName, labels) { |
| var result = []; |
| var t = labels[labelName]; |
| if (!t) { return result; } |
| var approvals = t.all || []; |
| approvals.forEach(function(label) { |
| if (label.value && label.value != labels[labelName].default_value) { |
| var labelClassName; |
| var labelValPrefix = ''; |
| if (label.value > 0) { |
| labelValPrefix = '+'; |
| labelClassName = 'approved'; |
| } else if (label.value < 0) { |
| labelClassName = 'notApproved'; |
| } |
| result.push({ |
| value: labelValPrefix + label.value, |
| className: labelClassName, |
| account: label, |
| }); |
| } |
| }); |
| return result; |
| }, |
| |
| _computeReplyButtonLabel: function(changeRecord) { |
| var drafts = (changeRecord && changeRecord.base) || {}; |
| var draftCount = Object.keys(drafts).reduce(function(count, file) { |
| return count + drafts[file].length; |
| }, 0); |
| |
| var label = 'Reply'; |
| if (draftCount > 0) { |
| label += ' (' + draftCount + ')'; |
| } |
| return label; |
| }, |
| |
| _handleAKey: function(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e) || |
| !this._loggedIn) { return; } |
| |
| e.preventDefault(); |
| this._openReplyDialog(); |
| }, |
| |
| _handleDKey: function(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.downloadOverlay.open(); |
| }, |
| |
| _handleCapitalRKey: function(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| e.preventDefault(); |
| page.show('/c/' + this._change._number); |
| }, |
| |
| _handleSKey: function(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.changeStar.toggleStar(); |
| }, |
| |
| _handleUKey: function(e) { |
| if (this.shouldSuppressKeyboardShortcut(e)) { return; } |
| |
| e.preventDefault(); |
| this._determinePageBack(); |
| }, |
| |
| _handleXKey: function(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.messageList.handleExpandCollapse(true); |
| }, |
| |
| _handleZKey: function(e) { |
| if (this.shouldSuppressKeyboardShortcut(e) || |
| this.modifierPressed(e)) { return; } |
| |
| e.preventDefault(); |
| this.$.messageList.handleExpandCollapse(false); |
| }, |
| |
| _determinePageBack: function() { |
| // Default backPage to '/' if user came to change view page |
| // via an email link, etc. |
| page.show(this.backPage || '/'); |
| }, |
| |
| _handleLabelRemoved: function(splices, path) { |
| for (var i = 0; i < splices.length; i++) { |
| var splice = splices[i]; |
| for (var j = 0; j < splice.removed.length; j++) { |
| var removed = splice.removed[j]; |
| var changePath = path.split('.'); |
| var labelPath = changePath.splice(0, changePath.length - 2); |
| var labelDict = this.get(labelPath); |
| if (labelDict.approved && |
| labelDict.approved._account_id === removed._account_id) { |
| this._reload(); |
| return; |
| } |
| } |
| } |
| }, |
| |
| _labelsChanged: function(changeRecord) { |
| if (!changeRecord) { return; } |
| if (changeRecord.value.indexSplices) { |
| this._handleLabelRemoved(changeRecord.value.indexSplices, |
| changeRecord.path); |
| } |
| this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, { |
| change: this._change, |
| }); |
| }, |
| |
| _openReplyDialog: function(opt_section) { |
| if (this.$.restAPI.hasPendingDiffDrafts()) { |
| this.dispatchEvent(new CustomEvent('show-alert', |
| {detail: {message: COMMENT_SAVE}, bubbles: true})); |
| return; |
| } |
| this.$.replyOverlay.open().then(function() { |
| this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops()); |
| this.$.replyDialog.open(opt_section); |
| }.bind(this)); |
| }, |
| |
| _handleReloadChange: function(e) { |
| return this._reload().then(function() { |
| // If the change was rebased, we need to reload the page with the |
| // latest patch. |
| if (e.detail.action === 'rebase') { |
| page.show(this.changePath(this._changeNum)); |
| } |
| }.bind(this)); |
| }, |
| |
| _handleGetChangeDetailError: function(response) { |
| this.fire('page-error', {response: response}); |
| }, |
| |
| _getDiffDrafts: function() { |
| return this.$.restAPI.getDiffDrafts(this._changeNum).then( |
| function(drafts) { |
| return this._diffDrafts = drafts; |
| }.bind(this)); |
| }, |
| |
| _getLoggedIn: function() { |
| return this.$.restAPI.getLoggedIn(); |
| }, |
| |
| _getProjectConfig: function() { |
| return this.$.restAPI.getProjectConfig(this._change.project).then( |
| function(config) { |
| this._projectConfig = config; |
| }.bind(this)); |
| }, |
| |
| _updateRebaseAction: function(revisionActions) { |
| if (revisionActions && revisionActions.rebase) { |
| revisionActions.rebase.rebaseOnCurrent = |
| !!revisionActions.rebase.enabled; |
| revisionActions.rebase.enabled = true; |
| } |
| return revisionActions; |
| }, |
| |
| _prepareCommitMsgForLinkify: function(msg) { |
| // TODO(wyatta) switch linkify sequence, see issue 5526. |
| // This is a zero-with space. It is added to prevent the linkify library |
| // from including R= as part of the email address. |
| return msg.replace(REVIEWERS_REGEX, 'R=\u200B'); |
| }, |
| |
| _getChangeDetail: function() { |
| return this.$.restAPI.getChangeDetail(this._changeNum, |
| this._handleGetChangeDetailError.bind(this)).then( |
| function(change) { |
| // Issue 4190: Coalesce missing topics to null. |
| if (!change.topic) { change.topic = null; } |
| if (!change.reviewer_updates) { |
| change.reviewer_updates = null; |
| } |
| var latestRevisionSha = this._getLatestRevisionSHA(change); |
| var currentRevision = change.revisions[latestRevisionSha]; |
| if (currentRevision.commit && currentRevision.commit.message) { |
| this._latestCommitMessage = this._prepareCommitMsgForLinkify( |
| currentRevision.commit.message); |
| } else { |
| this._latestCommitMessage = null; |
| } |
| var lineHeight = getComputedStyle(this).lineHeight; |
| this._lineHeight = lineHeight.slice(0, lineHeight.length - 2); |
| |
| this._change = change; |
| if (!this._patchRange || !this._patchRange.patchNum || |
| this._patchRange.patchNum === currentRevision._number) { |
| // CommitInfo.commit is optional, and may need patching. |
| if (!currentRevision.commit.commit) { |
| currentRevision.commit.commit = latestRevisionSha; |
| } |
| this._commitInfo = currentRevision.commit; |
| this._currentRevisionActions = |
| this._updateRebaseAction(currentRevision.actions); |
| // TODO: Fetch and process files. |
| } |
| }.bind(this)); |
| }, |
| |
| _getComments: function() { |
| return this.$.restAPI.getDiffComments(this._changeNum).then( |
| function(comments) { |
| this._comments = comments; |
| }.bind(this)); |
| }, |
| |
| _getLatestCommitMessage: function() { |
| return this.$.restAPI.getChangeCommitInfo(this._changeNum, |
| this._computeLatestPatchNum(this._allPatchSets)).then( |
| function(commitInfo) { |
| this._latestCommitMessage = |
| this._prepareCommitMsgForLinkify(commitInfo.message); |
| }.bind(this)); |
| }, |
| |
| _getLatestRevisionSHA: function(change) { |
| if (change.current_revision) { |
| return change.current_revision; |
| } |
| // current_revision may not be present in the case where the latest rev is |
| // a draft and the user doesn’t have permission to view that rev. |
| var latestRev = null; |
| var latestPatchNum = -1; |
| for (var rev in change.revisions) { |
| if (!change.revisions.hasOwnProperty(rev)) { continue; } |
| |
| if (change.revisions[rev]._number > latestPatchNum) { |
| latestRev = rev; |
| latestPatchNum = change.revisions[rev]._number; |
| } |
| } |
| return latestRev; |
| }, |
| |
| _getCommitInfo: function() { |
| return this.$.restAPI.getChangeCommitInfo( |
| this._changeNum, this._patchRange.patchNum).then( |
| function(commitInfo) { |
| this._commitInfo = commitInfo; |
| }.bind(this)); |
| }, |
| |
| _reloadDiffDrafts: function() { |
| this._diffDrafts = {}; |
| this._getDiffDrafts().then(function() { |
| if (this.$.replyOverlay.opened) { |
| this.async(function() { this.$.replyOverlay.center(); }, 1); |
| } |
| }.bind(this)); |
| }, |
| |
| _reload: function() { |
| this._loading = true; |
| this._relatedChangesCollapsed = true; |
| |
| this._getLoggedIn().then(function(loggedIn) { |
| if (!loggedIn) { return; } |
| |
| this._reloadDiffDrafts(); |
| }.bind(this)); |
| |
| var detailCompletes = this._getChangeDetail().then(function() { |
| this._loading = false; |
| this._getProjectConfig(); |
| }.bind(this)); |
| this._getComments(); |
| |
| if (this._patchRange.patchNum) { |
| return Promise.all([ |
| this._reloadPatchNumDependentResources(), |
| detailCompletes, |
| ]).then(function() { |
| return this.$.actions.reload(); |
| }.bind(this)); |
| } else { |
| // The patch number is reliant on the change detail request. |
| return detailCompletes.then(function() { |
| this.$.fileList.reload(); |
| if (!this._latestCommitMessage) { |
| this._getLatestCommitMessage(); |
| } |
| }.bind(this)); |
| } |
| }, |
| |
| /** |
| * Kicks off requests for resources that rely on the patch range |
| * (`this._patchRange`) being defined. |
| */ |
| _reloadPatchNumDependentResources: function() { |
| return Promise.all([ |
| this._getCommitInfo(), |
| this.$.fileList.reload(), |
| ]); |
| }, |
| |
| _updateSelected: function() { |
| this._selectedPatchSet = this._patchRange.patchNum; |
| }, |
| |
| _computePatchSetDescription: function(change, patchNum) { |
| var rev = this.getRevisionByPatchNum(change.revisions, patchNum); |
| return (rev && rev.description) ? |
| rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; |
| }, |
| |
| _computeDescriptionPlaceholder: function(readOnly) { |
| return (readOnly ? 'No' : 'Add a') + ' patch set description'; |
| }, |
| |
| _handleDescriptionChanged: function(e) { |
| var desc = e.detail.trim(); |
| var rev = this.getRevisionByPatchNum(this._change.revisions, |
| this._selectedPatchSet); |
| var sha = this._getPatchsetHash(this._change.revisions, rev); |
| this.$.restAPI.setDescription(this._changeNum, |
| this._selectedPatchSet, desc) |
| .then(function(res) { |
| if (res.ok) { |
| this.set(['_change', 'revisions', sha, 'description'], desc); |
| } |
| }.bind(this)); |
| }, |
| |
| |
| /** |
| * @param {Object} revisions The revisions object keyed by revision hashes |
| * @param {Object} patchSet A revision already fetched from {revisions} |
| * @return {string} the SHA hash corresponding to the revision. |
| */ |
| _getPatchsetHash: function(revisions, patchSet) { |
| for (var rev in revisions) { |
| if (revisions.hasOwnProperty(rev) && |
| revisions[rev] === patchSet) { |
| return rev; |
| } |
| } |
| }, |
| |
| _computeDescriptionReadOnly: function(loggedIn, change, account) { |
| return !(loggedIn && (account._account_id === change.owner._account_id)); |
| }, |
| |
| _computeReplyDisabled: function() { return false; }, |
| |
| _computeChangePermalinkAriaLabel: function(changeNum) { |
| return 'Change ' + changeNum; |
| }, |
| |
| _computeCommitClass: function(collapsed, commitMessage) { |
| if (this._computeCommitToggleHidden(commitMessage)) { return ''; } |
| return collapsed ? 'collapsed' : ''; |
| }, |
| |
| _computeRelatedChangesClass: function(collapsed, loading) { |
| if (!loading && !this.customStyle['--relation-chain-max-height']) { |
| this._updateRelatedChangeMaxHeight(); |
| } |
| return collapsed ? 'collapsed' : ''; |
| }, |
| |
| _computeCollapseText: function(collapsed) { |
| // Symbols are up and down triangles. |
| return collapsed ? '\u25bc Show more' : '\u25b2 Show less'; |
| }, |
| |
| _toggleCommitCollapsed: function() { |
| this._commitCollapsed = !this._commitCollapsed; |
| if (this._commitCollapsed) { |
| window.scrollTo(0, 0); |
| } |
| }, |
| |
| _toggleRelatedChangesCollapsed: function() { |
| this._relatedChangesCollapsed = !this._relatedChangesCollapsed; |
| if (this._relatedChangesCollapsed) { |
| window.scrollTo(0, 0); |
| } |
| }, |
| |
| _computeCommitToggleHidden: function(commitMessage) { |
| if (!commitMessage) { return true; } |
| return commitMessage.split('\n').length < MIN_LINES_FOR_COMMIT_COLLAPSE; |
| }, |
| |
| _getOffsetHeight: function(element) { |
| return element.offsetHeight; |
| }, |
| |
| _getScrollHeight: function(element) { |
| return element.scrollHeight; |
| }, |
| |
| /** |
| * Get the line height of an element to the nearest integer. |
| */ |
| _getLineHeight: function(element) { |
| var lineHeightStr = getComputedStyle(element).lineHeight; |
| return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2)); |
| }, |
| |
| /** |
| * New max height for the related changes section, shorter than the existing |
| * change info height. |
| */ |
| _updateRelatedChangeMaxHeight: function() { |
| // Takes into account approximate height for the expand button and |
| // bottom margin |
| var extraHeight = 24; |
| var maxExistingHeight; |
| var hasCommitToggle = |
| !this._computeCommitToggleHidden(this._latestCommitMessage); |
| if (hasCommitToggle) { |
| // Make sure the content is lined up if both areas have buttons. If the |
| // commit message is not collapsed, instead use the change info hight. |
| maxExistingHeight = this._getOffsetHeight(this.$.commitMessage); |
| } else { |
| maxExistingHeight = this._getOffsetHeight(this.$.mainChangeInfo) - |
| extraHeight; |
| } |
| |
| // Get the line height of related changes, and convert it to the nearest |
| // integer. |
| var lineHeight = this._getLineHeight(this.$.relatedChanges); |
| |
| // Figure out a new height that is divisible by the rounded line height. |
| var remainder = maxExistingHeight % lineHeight; |
| var newHeight = maxExistingHeight - remainder; |
| |
| // Update the max-height of the relation chain to this new height; |
| this.customStyle['--relation-chain-max-height'] = newHeight + 'px'; |
| if (hasCommitToggle) { |
| this.customStyle['--related-change-btn-top-padding'] = remainder + 'px'; |
| } |
| this.updateStyles(); |
| }, |
| |
| _computeRelatedChangesToggleHidden: function() { |
| return this._getScrollHeight(this.$.relatedChanges) <= |
| this._getOffsetHeight(this.$.relatedChanges); |
| }, |
| }); |
| })(); |