// 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 */ /** * Fired if being logged in is required. * * @event show-auth-required */ 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.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 '/' + changeNum; }, _computeChangeStatus: function(change, patchNum) { var statusString; if (change.status === this.ChangeStatus.NEW) { var rev = this.getRevisionByPatchNum(change.revisions, patchNum); if (rev && rev.draft === true) { statusString = 'Draft'; } } else { statusString = this.changeStatusString(change); } 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 . @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)) { return; } this._getLoggedIn().then(function(isLoggedIn) { if (!isLoggedIn) { this.fire('show-auth-required'); return; } e.preventDefault(); this._openReplyDialog(); }.bind(this)); }, _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; }, /** * @desc 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)); }, /** * @desc 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); }, }); })();