| // 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'; |
| |
| 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 {}; }, |
| }, |
| serverConfig: Object, |
| keyEventTarget: { |
| type: Object, |
| value: function() { return document.body; }, |
| }, |
| |
| _comments: Object, |
| _change: { |
| type: Object, |
| observer: '_changeChanged', |
| }, |
| _commitInfo: Object, |
| _changeNum: String, |
| _diffDrafts: { |
| type: Object, |
| value: function() { return {}; }, |
| }, |
| _editingCommitMessage: { |
| type: Boolean, |
| value: false, |
| }, |
| _hideEditCommitMessage: { |
| type: Boolean, |
| computed: '_computeHideEditCommitMessage(_loggedIn, ' + |
| '_editingCommitMessage, _change.*, _patchRange.patchNum)', |
| }, |
| _patchRange: Object, |
| _allPatchSets: { |
| type: Array, |
| computed: '_computeAllPatchSets(_change)', |
| }, |
| _loggedIn: { |
| type: Boolean, |
| value: false, |
| }, |
| _loading: Boolean, |
| _headerContainerEl: Object, |
| _headerEl: Object, |
| _projectConfig: Object, |
| _replyButtonLabel: { |
| type: String, |
| value: 'Reply', |
| computed: '_computeReplyButtonLabel(_diffDrafts.*)', |
| }, |
| }, |
| |
| behaviors: [ |
| Gerrit.KeyboardShortcutBehavior, |
| Gerrit.RESTClientBehavior, |
| ], |
| |
| observers: [ |
| '_labelsChanged(_change.labels.*)', |
| '_paramsAndChangeChanged(params, _change)', |
| ], |
| |
| ready: function() { |
| this._headerEl = this.$$('.header'); |
| }, |
| |
| attached: function() { |
| this._getLoggedIn().then(function(loggedIn) { |
| this._loggedIn = loggedIn; |
| }.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', '_handleBodyScroll'); |
| }, |
| |
| detached: function() { |
| this.unlisten(window, 'scroll', '_handleBodyScroll'); |
| }, |
| |
| _handleBodyScroll: function(e) { |
| var containerEl = this._headerContainerEl || |
| this.$$('.headerContainer'); |
| |
| // Calculate where the header is relative to the window. |
| var top = containerEl.offsetTop; |
| for (var offsetParent = containerEl.offsetParent; |
| offsetParent; |
| offsetParent = offsetParent.offsetParent) { |
| top += offsetParent.offsetTop; |
| } |
| // The element may not be displayed yet, in which case do nothing. |
| if (top == 0) { return; } |
| |
| this._headerEl.classList.toggle('pinned', window.scrollY >= top); |
| }, |
| |
| _resetHeaderEl: function() { |
| var el = this._headerEl || this.$$('.header'); |
| this._headerEl = el; |
| el.classList.remove('pinned'); |
| }, |
| |
| _handleEditCommitMessage: function(e) { |
| this._editingCommitMessage = true; |
| this.$.commitMessageEditor.focusTextarea(); |
| }, |
| |
| _handleCommitMessageSave: function(e) { |
| var message = e.detail.content; |
| |
| this.$.commitMessageEditor.disabled = true; |
| this._saveCommitMessage(message).then(function(resp) { |
| this.$.commitMessageEditor.disabled = false; |
| if (!resp.ok) { return; } |
| |
| this.set('_commitInfo.message', 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, changeRecord, |
| patchNum) { |
| if (!changeRecord || !loggedIn || editing) { return true; } |
| |
| patchNum = parseInt(patchNum, 10); |
| if (isNaN(patchNum)) { return true; } |
| |
| var change = changeRecord.base; |
| if (!change.current_revision) { return true; } |
| if (change.revisions[change.current_revision]._number !== patchNum) { |
| 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) { |
| var patchNum = e.target.value; |
| 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) { |
| page.show(this.changePath(this._changeNum)); |
| return; |
| } |
| page.show(this.changePath(this._changeNum) + '/' + patchNum); |
| }, |
| |
| _handleReplyTap: function(e) { |
| e.preventDefault(); |
| this._openReplyDialog(); |
| }, |
| |
| _handleDownloadTap: function(e) { |
| e.preventDefault(); |
| this.$.downloadOverlay.open(); |
| }, |
| |
| _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'; |
| this.$.replyDialog.draft += 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); |
| }, |
| |
| _paramsChanged: function(value) { |
| if (value.view !== this.tagName.toLowerCase()) { return; } |
| |
| this._changeNum = value.changeNum; |
| this._patchRange = { |
| patchNum: value.patchNum, |
| basePatchNum: value.basePatchNum || 'PARENT', |
| }; |
| |
| this._reload().then(function() { |
| this.$.messageList.topMargin = this._headerEl.offsetHeight; |
| this.$.fileList.topMargin = this._headerEl.offsetHeight; |
| |
| // Allow the message list to render before scrolling. |
| this.async(function() { |
| this._maybeScrollToMessage(); |
| }.bind(this), 1); |
| |
| this._maybeShowReplyDialog(); |
| |
| this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, { |
| change: this._change, |
| patchNum: this._patchRange.patchNum, |
| }); |
| }.bind(this)); |
| }, |
| |
| _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)); |
| } |
| }, |
| |
| _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); |
| 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)); |
| |
| var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; |
| this.fire('title-change', {title: title}); |
| }, |
| |
| _computeChangePermalink: function(changeNum) { |
| return '/' + changeNum; |
| }, |
| |
| _computeChangeStatus: function(change, patchNum) { |
| var statusString; |
| if (change.status === this.ChangeStatus.NEW) { |
| var rev = this._getRevisionNumber(change, patchNum); |
| if (rev && rev.draft === true) { |
| statusString = 'Draft'; |
| } |
| } else { |
| statusString = this.changeStatusString(change); |
| } |
| return statusString ? '(' + statusString + ')' : ''; |
| }, |
| |
| _computeLatestPatchNum: function(allPatchSets) { |
| return allPatchSets[allPatchSets.length - 1]; |
| }, |
| |
| _computeAllPatchSets: function(change) { |
| var patchNums = []; |
| for (var rev in change.revisions) { |
| patchNums.push(change.revisions[rev]._number); |
| } |
| return patchNums.sort(function(a, b) { |
| return a - b; |
| }); |
| }, |
| |
| _getRevisionNumber: function(change, patchNum) { |
| for (var rev in change.revisions) { |
| if (change.revisions[rev]._number == patchNum) { |
| return change.revisions[rev]; |
| } |
| } |
| }, |
| |
| _computePatchIndexIsSelected: function(index, patchNum) { |
| return this._allPatchSets[index] == patchNum; |
| }, |
| |
| _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; |
| }, |
| |
| _computeReplyButtonHighlighted: function(changeRecord) { |
| var drafts = (changeRecord && changeRecord.base) || {}; |
| return Object.keys(drafts).length > 0; |
| }, |
| |
| _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; |
| }, |
| |
| _handleKey: function(e) { |
| if (this.shouldSupressKeyboardShortcut(e)) { return; } |
| |
| switch (e.keyCode) { |
| case 65: // 'a' |
| if (this._loggedIn && !e.shiftKey) { |
| e.preventDefault(); |
| this._openReplyDialog(); |
| } |
| break; |
| case 85: // 'u' |
| e.preventDefault(); |
| page.show('/'); |
| break; |
| } |
| }, |
| |
| _labelsChanged: function(changeRecord) { |
| if (!changeRecord) { return; } |
| this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, { |
| change: this._change, |
| }); |
| }, |
| |
| _openReplyDialog: function(opt_section) { |
| this.$.replyOverlay.open().then(function() { |
| this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops()); |
| this.$.replyDialog.open(opt_section); |
| }.bind(this)); |
| }, |
| |
| _handleReloadChange: function() { |
| page.show(this.changePath(this._changeNum)); |
| }, |
| |
| _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)); |
| }, |
| |
| _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; |
| } |
| this._change = change; |
| }.bind(this)); |
| }, |
| |
| _getComments: function() { |
| return this.$.restAPI.getDiffComments(this._changeNum).then( |
| function(comments) { |
| this._comments = comments; |
| }.bind(this)); |
| }, |
| |
| _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._getLoggedIn().then(function(loggedIn) { |
| if (!loggedIn) { return; } |
| |
| this._reloadDiffDrafts(); |
| }.bind(this)); |
| |
| var detailCompletes = this._getChangeDetail().then(function() { |
| this._loading = false; |
| }.bind(this)); |
| this._getComments(); |
| |
| var reloadPatchNumDependentResources = function() { |
| return Promise.all([ |
| this._getCommitInfo(), |
| this.$.actions.reload(), |
| this.$.fileList.reload(), |
| ]); |
| }.bind(this); |
| var reloadDetailDependentResources = function() { |
| if (!this._change) { return Promise.resolve(); } |
| |
| return Promise.all([ |
| this.$.relatedChanges.reload(), |
| this._getProjectConfig(), |
| ]); |
| }.bind(this); |
| |
| this._resetHeaderEl(); |
| |
| if (this._patchRange.patchNum) { |
| return reloadPatchNumDependentResources().then(function() { |
| return detailCompletes; |
| }).then(reloadDetailDependentResources); |
| } else { |
| // The patch number is reliant on the change detail request. |
| return detailCompletes.then(reloadPatchNumDependentResources).then( |
| reloadDetailDependentResources); |
| } |
| }, |
| }); |
| })(); |