blob: 14ac4d1acb861a67fef8bbf6757584cc10f65aaa [file] [log] [blame]
// 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);
}
},
});
})();