blob: d2b279de2302b0ebfb3e309a5b953bf154cffd78 [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';
var STORAGE_DEBOUNCE_INTERVAL_MS = 400;
var FocusTarget = {
ANY: 'any',
BODY: 'body',
CCS: 'cc',
REVIEWERS: 'reviewers',
};
Polymer({
is: 'gr-reply-dialog',
/**
* Fired when a reply is successfully sent.
*
* @event send
*/
/**
* Fired when the user presses the cancel button.
*
* @event cancel
*/
/**
* Fired when the main textarea's value changes, which may have triggered
* a change in size for the dialog.
*
* @event autogrow
*/
properties: {
change: Object,
patchNum: String,
revisions: Object,
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
draft: {
type: String,
value: '',
observer: '_draftChanged',
},
diffDrafts: Object,
filterReviewerSuggestion: {
type: Function,
value: function() {
return this._filterReviewerSuggestion.bind(this);
},
},
labels: Object,
permittedLabels: Object,
serverConfig: Object,
_account: Object,
_ccs: Array,
_ccPendingConfirmation: {
type: Object,
observer: '_reviewerPendingConfirmationUpdated',
},
_owner: Object,
_reviewers: Array,
_reviewerPendingConfirmation: {
type: Object,
observer: '_reviewerPendingConfirmationUpdated',
},
},
FocusTarget: FocusTarget,
behaviors: [
Gerrit.RESTClientBehavior,
],
observers: [
'_changeUpdated(change.reviewers.*, change.owner, serverConfig)',
],
attached: function() {
this._getAccount().then(function(account) {
this._account = account;
}.bind(this));
},
ready: function() {
this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
},
open: function(opt_focusTarget) {
this._focusOn(opt_focusTarget);
if (!this.draft || !this.draft.length) {
this.draft = this._loadStoredDraft();
}
},
focus: function() {
this._focusOn(FocusTarget.ANY);
},
getFocusStops: function() {
return {
start: this.$.reviewers.focusStart,
end: this.$.cancelButton,
};
},
setLabelValue: function(label, value) {
var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
// The selector may not be present if it’s not at the latest patch set.
if (!selectorEl) { return; }
var item = selectorEl.$$('gr-button[data-value="' + value + '"]');
if (!item) { return; }
selectorEl.selectIndex(selectorEl.indexOf(item));
},
_mapReviewer: function(reviewer) {
var reviewerId;
var confirmed;
if (reviewer.account) {
reviewerId = reviewer.account._account_id;
} else if (reviewer.group) {
reviewerId = reviewer.group.id;
confirmed = reviewer.group.confirmed;
}
return {reviewer: reviewerId, confirmed: confirmed};
},
send: function() {
var obj = {
drafts: 'PUBLISH_ALL_REVISIONS',
labels: {},
};
for (var label in this.permittedLabels) {
if (!this.permittedLabels.hasOwnProperty(label)) { continue; }
var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
// The selector may not be present if it’s not at the latest patch set.
if (!selectorEl) { continue; }
var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
selectedVal = parseInt(selectedVal, 10);
obj.labels[label] = selectedVal;
}
if (this.draft != null) {
obj.message = this.draft;
}
obj.reviewers = this.$.reviewers.additions().map(this._mapReviewer);
if (this.serverConfig.note_db_enabled) {
this.$$('#ccs').additions().forEach(function(reviewer) {
reviewer = this._mapReviewer(reviewer);
reviewer.state = 'CC';
obj.reviewers.push(reviewer);
}.bind(this));
}
this.disabled = true;
var errFn = this._handle400Error.bind(this);
return this._saveReview(obj, errFn).then(function(response) {
if (!response || !response.ok) {
return response;
}
this.disabled = false;
this.draft = '';
this.fire('send', null, {bubbles: false});
}.bind(this)).catch(function(err) {
this.disabled = false;
throw err;
}.bind(this));
},
_focusOn: function(section) {
if (section === FocusTarget.ANY) {
section = this._chooseFocusTarget();
}
if (section === FocusTarget.BODY) {
var textarea = this.$.textarea;
textarea.async(textarea.textarea.focus.bind(textarea.textarea));
} else if (section === FocusTarget.REVIEWERS) {
var reviewerEntry = this.$.reviewers.focusStart;
reviewerEntry.async(reviewerEntry.focus);
} else if (section === FocusTarget.CCS) {
var ccEntry = this.$$('#ccs').focusStart;
ccEntry.async(ccEntry.focus);
}
},
_chooseFocusTarget: function() {
// If we are the owner and the reviewers field is empty, focus on that.
if (this._account && this.change.owner &&
this._account._account_id === this.change.owner._account_id &&
(!this._reviewers || this._reviewers.length === 0)) {
return FocusTarget.REVIEWERS;
}
// Default to BODY.
return FocusTarget.BODY;
},
_handle400Error: function(response) {
// A call to _saveReview could fail with a server error if erroneous
// reviewers were requested. This is signalled with a 400 Bad Request
// status. The default gr-rest-api-interface error handling would
// result in a large JSON response body being displayed to the user in
// the gr-error-manager toast.
//
// We can modify the error handling behavior by passing this function
// through to restAPI as a custom error handling function. Since we're
// short-circuiting restAPI we can do our own response parsing and fire
// the server-error ourselves.
//
this.disabled = false;
if (response.status !== 400) {
// This is all restAPI does when there is no custom error handling.
this.fire('server-error', {response: response});
return response;
}
// Process the response body, format a better error message, and fire
// an event for gr-event-manager to display.
var jsonPromise = this.$.restAPI.getResponseObject(response);
return jsonPromise.then(function(result) {
var errors = [];
['reviewers', 'ccs'].forEach(function(state) {
for (var input in result[state]) {
var reviewer = result[state][input];
if (!!reviewer.error) {
errors.push(reviewer.error);
}
}
});
response = {
ok: false,
status: response.status,
text: function() { return Promise.resolve(errors.join(', ')); },
};
this.fire('server-error', {response: response});
}.bind(this));
},
_computeShowLabels: function(patchNum, revisions) {
var num = parseInt(patchNum, 10);
for (var rev in revisions) {
if (revisions[rev]._number > num) {
return false;
}
}
return true;
},
_computeHideDraftList: function(drafts) {
return Object.keys(drafts || {}).length == 0;
},
_computeDraftsTitle: function(drafts) {
var total = 0;
for (var file in drafts) {
total += drafts[file].length;
}
if (total == 0) { return ''; }
if (total == 1) { return '1 Draft'; }
if (total > 1) { return total + ' Drafts'; }
},
_computeLabelValueTitle: function(labels, label, value) {
return labels[label] && labels[label].values[value];
},
_computeLabelArray: function(labelsObj) {
return Object.keys(labelsObj).sort();
},
_computeIndexOfLabelValue: function(
labels, permittedLabels, labelName, account) {
var t = labels[labelName];
if (!t) { return null; }
var labelValue = t.default_value;
// Is there an existing vote for the current user? If so, use that.
var votes = labels[labelName];
if (votes.all && votes.all.length > 0) {
for (var i = 0; i < votes.all.length; i++) {
if (votes.all[i]._account_id == account._account_id) {
labelValue = votes.all[i].value;
break;
}
}
}
var len = permittedLabels[labelName] != null ?
permittedLabels[labelName].length : 0;
for (var i = 0; i < len; i++) {
var val = parseInt(permittedLabels[labelName][i], 10);
if (val == labelValue) {
return i;
}
}
return null;
},
_computePermittedLabelValues: function(permittedLabels, label) {
return permittedLabels[label];
},
_changeUpdated: function(changeRecord, owner, serverConfig) {
this._owner = owner;
var reviewers = [];
var ccs = [];
for (var key in changeRecord.base) {
if (key !== 'REVIEWER' && key !== 'CC') {
console.warn('unexpected reviewer state:', key);
continue;
}
changeRecord.base[key].forEach(function(entry) {
if (entry._account_id === owner._account_id) {
return;
}
switch (key) {
case 'REVIEWER':
reviewers.push(entry);
break;
case 'CC':
ccs.push(entry);
break;
}
});
}
if (serverConfig.note_db_enabled) {
this._ccs = ccs;
} else {
this._ccs = [];
reviewers = reviewers.concat(ccs);
}
this._reviewers = reviewers;
},
_accountOrGroupKey: function(entry) {
return entry.id || entry._account_id;
},
_filterReviewerSuggestion: function(suggestion) {
var entry;
if (suggestion.account) {
entry = suggestion.account;
} else if (suggestion.group) {
entry = suggestion.group;
} else {
console.warn('received suggestion that was neither account nor group:',
suggestion);
}
if (entry._account_id === this._owner._account_id) {
return false;
}
var key = this._accountOrGroupKey(entry);
var finder = function(entry) {
return this._accountOrGroupKey(entry) === key;
}.bind(this);
return this._reviewers.find(finder) === undefined &&
this._ccs.find(finder) === undefined;
},
_getAccount: function() {
return this.$.restAPI.getAccount();
},
_cancelTapHandler: function(e) {
e.preventDefault();
this.fire('cancel', null, {bubbles: false});
},
_sendTapHandler: function(e) {
e.preventDefault();
this.send();
},
_saveReview: function(review, opt_errFn) {
return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
review, opt_errFn);
},
_reviewerPendingConfirmationUpdated: function(reviewer) {
if (reviewer === null) {
this.$.reviewerConfirmationOverlay.close();
} else {
this.$.reviewerConfirmationOverlay.open();
}
},
_confirmPendingReviewer: function() {
if (this._ccPendingConfirmation) {
this.$$('#ccs').confirmGroup(this._ccPendingConfirmation.group);
this._focusOn(FocusTarget.CCS);
} else {
this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
this._focusOn(FocusTarget.REVIEWERS);
}
},
_cancelPendingReviewer: function() {
this._ccPendingConfirmation = null;
this._reviewerPendingConfirmation = null;
var target =
this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
this._focusOn(target);
},
_getStorageLocation: function() {
return {
changeNum: this.change._number,
patchNum: this.patchNum,
path: '@change',
};
},
_loadStoredDraft: function() {
var draft = this.$.storage.getDraftComment(this._getStorageLocation());
return draft ? draft.message : '';
},
_draftChanged: function(newDraft, oldDraft) {
this.debounce('store', function() {
if (!newDraft.length && oldDraft) {
// If the draft has been modified to be empty, then erase the storage
// entry.
this.$.storage.eraseDraftComment(this._getStorageLocation());
} else if (newDraft.length) {
this.$.storage.setDraftComment(this._getStorageLocation(),
this.draft);
}
}, STORAGE_DEBOUNCE_INTERVAL_MS);
},
_handleTextareaChanged: function(e) {
// If the textarea resizes, we need to re-fit the overlay.
this.debounce('autogrow', function() {
this.fire('autogrow');
});
},
});
})();