890 lines
26 KiB
JavaScript
890 lines
26 KiB
JavaScript
/**
|
|
* @license
|
|
* 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';
|
|
|
|
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
|
|
|
|
const FocusTarget = {
|
|
ANY: 'any',
|
|
BODY: 'body',
|
|
CCS: 'cc',
|
|
REVIEWERS: 'reviewers',
|
|
};
|
|
|
|
const ReviewerTypes = {
|
|
REVIEWER: 'REVIEWER',
|
|
CC: 'CC',
|
|
};
|
|
|
|
const LatestPatchState = {
|
|
LATEST: 'latest',
|
|
CHECKING: 'checking',
|
|
NOT_LATEST: 'not-latest',
|
|
};
|
|
|
|
const ButtonLabels = {
|
|
START_REVIEW: 'Start review',
|
|
SEND: 'Send',
|
|
};
|
|
|
|
const ButtonTooltips = {
|
|
SAVE: 'Save reply but do not send notification',
|
|
START_REVIEW: 'Mark as ready for review and send reply',
|
|
SEND: 'Send reply',
|
|
};
|
|
|
|
// TODO(logan): Remove once the fix for issue 6841 is stable on
|
|
// googlesource.com.
|
|
const START_REVIEW_MESSAGE = 'This change is ready for review.';
|
|
|
|
const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
|
|
|
|
const SEND_REPLY_TIMING_LABEL = 'SendReply';
|
|
|
|
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
|
|
*/
|
|
|
|
/**
|
|
* Fires to show an alert when a send is attempted on the non-latest patch.
|
|
*
|
|
* @event show-alert
|
|
*/
|
|
|
|
/**
|
|
* Fires when the reply dialog believes that the server side diff drafts
|
|
* have been updated and need to be refreshed.
|
|
*
|
|
* @event comment-refresh
|
|
*/
|
|
|
|
/**
|
|
* Fires when the state of the send button (enabled/disabled) changes.
|
|
*
|
|
* @event send-disabled-changed
|
|
*/
|
|
|
|
properties: {
|
|
/**
|
|
* @type {{ _number: number, removable_reviewers: Array }}
|
|
*/
|
|
change: Object,
|
|
patchNum: String,
|
|
canBeStarted: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
value: false,
|
|
reflectToAttribute: true,
|
|
},
|
|
draft: {
|
|
type: String,
|
|
value: '',
|
|
observer: '_draftChanged',
|
|
},
|
|
quote: {
|
|
type: String,
|
|
value: '',
|
|
},
|
|
diffDrafts: {
|
|
type: Object,
|
|
observer: '_handleHeightChanged',
|
|
},
|
|
/** @type {!Function} */
|
|
filterReviewerSuggestion: {
|
|
type: Function,
|
|
value() {
|
|
return this._filterReviewerSuggestionGenerator(false);
|
|
},
|
|
},
|
|
/** @type {!Function} */
|
|
filterCCSuggestion: {
|
|
type: Function,
|
|
value() {
|
|
return this._filterReviewerSuggestionGenerator(true);
|
|
},
|
|
},
|
|
permittedLabels: Object,
|
|
/**
|
|
* @type {{ note_db_enabled: boolean }}
|
|
*/
|
|
serverConfig: Object,
|
|
/**
|
|
* @type {{ commentlinks: Array }}
|
|
*/
|
|
projectConfig: Object,
|
|
knownLatestState: String,
|
|
underReview: {
|
|
type: Boolean,
|
|
value: true,
|
|
},
|
|
|
|
_account: Object,
|
|
_ccs: Array,
|
|
/** @type {?Object} */
|
|
_ccPendingConfirmation: {
|
|
type: Object,
|
|
observer: '_reviewerPendingConfirmationUpdated',
|
|
},
|
|
_messagePlaceholder: {
|
|
type: String,
|
|
computed: '_computeMessagePlaceholder(canBeStarted)',
|
|
},
|
|
_owner: Object,
|
|
/** @type {?} */
|
|
_pendingConfirmationDetails: Object,
|
|
_includeComments: {
|
|
type: Boolean,
|
|
value: true,
|
|
},
|
|
_reviewers: Array,
|
|
/** @type {?Object} */
|
|
_reviewerPendingConfirmation: {
|
|
type: Object,
|
|
observer: '_reviewerPendingConfirmationUpdated',
|
|
},
|
|
_previewFormatting: {
|
|
type: Boolean,
|
|
value: false,
|
|
observer: '_handleHeightChanged',
|
|
},
|
|
_reviewersPendingRemove: {
|
|
type: Object,
|
|
value: {
|
|
CC: [],
|
|
REVIEWER: [],
|
|
},
|
|
},
|
|
_sendButtonLabel: {
|
|
type: String,
|
|
computed: '_computeSendButtonLabel(canBeStarted)',
|
|
},
|
|
_ccsEnabled: {
|
|
type: Boolean,
|
|
computed: '_computeCCsEnabled(serverConfig)',
|
|
},
|
|
_savingComments: Boolean,
|
|
_reviewersMutated: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
_labelsChanged: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
_saveTooltip: {
|
|
type: String,
|
|
value: ButtonTooltips.SAVE,
|
|
readOnly: true,
|
|
},
|
|
_pluginMessage: {
|
|
type: String,
|
|
value: '',
|
|
},
|
|
_sendDisabled: {
|
|
type: Boolean,
|
|
computed: '_computeSendButtonDisabled(_sendButtonLabel, diffDrafts, ' +
|
|
'draft, _reviewersMutated, _labelsChanged, _includeComments, ' +
|
|
'disabled)',
|
|
observer: '_sendDisabledChanged',
|
|
},
|
|
},
|
|
|
|
FocusTarget,
|
|
|
|
// TODO(logan): Remove once the fix for issue 6841 is stable on
|
|
// googlesource.com.
|
|
START_REVIEW_MESSAGE,
|
|
|
|
behaviors: [
|
|
Gerrit.BaseUrlBehavior,
|
|
Gerrit.KeyboardShortcutBehavior,
|
|
Gerrit.PatchSetBehavior,
|
|
Gerrit.RESTClientBehavior,
|
|
],
|
|
|
|
keyBindings: {
|
|
'esc': '_handleEscKey',
|
|
'ctrl+enter meta+enter': '_handleEnterKey',
|
|
},
|
|
|
|
observers: [
|
|
'_changeUpdated(change.reviewers.*, change.owner, serverConfig)',
|
|
'_ccsChanged(_ccs.splices)',
|
|
'_reviewersChanged(_reviewers.splices)',
|
|
],
|
|
|
|
attached() {
|
|
this._getAccount().then(account => {
|
|
this._account = account || {};
|
|
});
|
|
},
|
|
|
|
ready() {
|
|
this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
|
|
},
|
|
|
|
open(opt_focusTarget) {
|
|
this.knownLatestState = LatestPatchState.CHECKING;
|
|
this.fetchChangeUpdates(this.change, this.$.restAPI)
|
|
.then(result => {
|
|
this.knownLatestState = result.isLatest ?
|
|
LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
|
|
});
|
|
|
|
this._focusOn(opt_focusTarget);
|
|
if (this.quote && this.quote.length) {
|
|
// If a reply quote has been provided, use it and clear the property.
|
|
this.draft = this.quote;
|
|
this.quote = '';
|
|
} else {
|
|
// Otherwise, check for an unsaved draft in localstorage.
|
|
this.draft = this._loadStoredDraft();
|
|
}
|
|
if (this.$.restAPI.hasPendingDiffDrafts()) {
|
|
this._savingComments = true;
|
|
this.$.restAPI.awaitPendingDiffDrafts().then(() => {
|
|
this.fire('comment-refresh');
|
|
this._savingComments = false;
|
|
});
|
|
}
|
|
},
|
|
|
|
focus() {
|
|
this._focusOn(FocusTarget.ANY);
|
|
},
|
|
|
|
getFocusStops() {
|
|
const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
|
|
return {
|
|
start: this.$.reviewers.focusStart,
|
|
end,
|
|
};
|
|
},
|
|
|
|
setLabelValue(label, value) {
|
|
const selectorEl =
|
|
this.$.labelScores.$$(`gr-label-score-row[name="${label}"]`);
|
|
if (!selectorEl) { return; }
|
|
selectorEl.setSelectedValue(value);
|
|
},
|
|
|
|
getLabelValue(label) {
|
|
const selectorEl =
|
|
this.$.labelScores.$$(`gr-label-score-row[name="${label}"]`);
|
|
if (!selectorEl) { return null; }
|
|
|
|
return selectorEl.selectedValue;
|
|
},
|
|
|
|
_handleEscKey(e) {
|
|
this.cancel();
|
|
},
|
|
|
|
_handleEnterKey(e) {
|
|
this._submit();
|
|
},
|
|
|
|
_ccsChanged(splices) {
|
|
if (splices && splices.indexSplices) {
|
|
this._reviewersMutated = true;
|
|
this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC);
|
|
}
|
|
},
|
|
|
|
_reviewersChanged(splices) {
|
|
if (splices && splices.indexSplices) {
|
|
this._reviewersMutated = true;
|
|
this._processReviewerChange(splices.indexSplices,
|
|
ReviewerTypes.REVIEWER);
|
|
let key;
|
|
let index;
|
|
let account;
|
|
// Remove any accounts that already exist as a CC.
|
|
for (const splice of splices.indexSplices) {
|
|
for (const addedKey of splice.addedKeys) {
|
|
account = this.get(`_reviewers.${addedKey}`);
|
|
key = this._accountOrGroupKey(account);
|
|
index = this._ccs.findIndex(
|
|
account => this._accountOrGroupKey(account) === key);
|
|
if (index >= 0) {
|
|
this.splice('_ccs', index, 1);
|
|
const message = (account.name || account.email || key) +
|
|
' moved from CC to reviewer.';
|
|
this.fire('show-alert', {message});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_processReviewerChange(indexSplices, type) {
|
|
for (const splice of indexSplices) {
|
|
for (const account of splice.removed) {
|
|
if (!this._reviewersPendingRemove[type]) {
|
|
console.err('Invalid type ' + type + ' for reviewer.');
|
|
return;
|
|
}
|
|
this._reviewersPendingRemove[type].push(account);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Resets the state of the _reviewersPendingRemove object, and removes
|
|
* accounts if necessary.
|
|
*
|
|
* @param {boolean} isCancel true if the action is a cancel.
|
|
* @param {Object=} opt_accountIdsTransferred map of account IDs that must
|
|
* not be removed, because they have been readded in another state.
|
|
*/
|
|
_purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) {
|
|
let reviewerArr;
|
|
const keep = opt_accountIdsTransferred || {};
|
|
for (const type in this._reviewersPendingRemove) {
|
|
if (this._reviewersPendingRemove.hasOwnProperty(type)) {
|
|
if (!isCancel) {
|
|
reviewerArr = this._reviewersPendingRemove[type];
|
|
for (let i = 0; i < reviewerArr.length; i++) {
|
|
if (!keep[reviewerArr[i]._account_id]) {
|
|
this._removeAccount(reviewerArr[i], type);
|
|
}
|
|
}
|
|
}
|
|
this._reviewersPendingRemove[type] = [];
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes an account from the change, both on the backend and the client.
|
|
* Does nothing if the account is a pending addition.
|
|
*
|
|
* @param {!Object} account
|
|
* @param {string} type
|
|
*
|
|
* * TODO(beckysiegel) submit Polymer PR
|
|
* @suppress {checkTypes}
|
|
*/
|
|
_removeAccount(account, type) {
|
|
if (account._pendingAdd) { return; }
|
|
|
|
return this.$.restAPI.removeChangeReviewer(this.change._number,
|
|
account._account_id).then(response => {
|
|
if (!response.ok) { return response; }
|
|
|
|
const reviewers = this.change.reviewers[type] || [];
|
|
for (let i = 0; i < reviewers.length; i++) {
|
|
if (reviewers[i]._account_id == account._account_id) {
|
|
this.splice(['change', 'reviewers', type], i, 1);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
_mapReviewer(reviewer) {
|
|
let reviewerId;
|
|
let confirmed;
|
|
if (reviewer.account) {
|
|
reviewerId = reviewer.account._account_id || reviewer.account.email;
|
|
} else if (reviewer.group) {
|
|
reviewerId = reviewer.group.id;
|
|
confirmed = reviewer.group.confirmed;
|
|
}
|
|
return {reviewer: reviewerId, confirmed};
|
|
},
|
|
|
|
send(includeComments, startReview) {
|
|
this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
|
|
const labels = this.$.labelScores.getLabelValues();
|
|
|
|
const obj = {
|
|
drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP',
|
|
labels,
|
|
};
|
|
|
|
if (startReview) {
|
|
obj.ready = true;
|
|
}
|
|
|
|
if (this.draft != null) {
|
|
obj.message = this.draft;
|
|
}
|
|
|
|
const accountAdditions = {};
|
|
obj.reviewers = this.$.reviewers.additions().map(reviewer => {
|
|
if (reviewer.account) {
|
|
accountAdditions[reviewer.account._account_id] = true;
|
|
}
|
|
return this._mapReviewer(reviewer);
|
|
});
|
|
const ccsEl = this.$$('#ccs');
|
|
if (ccsEl) {
|
|
for (let reviewer of ccsEl.additions()) {
|
|
if (reviewer.account) {
|
|
accountAdditions[reviewer.account._account_id] = true;
|
|
}
|
|
reviewer = this._mapReviewer(reviewer);
|
|
reviewer.state = 'CC';
|
|
obj.reviewers.push(reviewer);
|
|
}
|
|
}
|
|
|
|
this.disabled = true;
|
|
|
|
if (obj.ready && !obj.message) {
|
|
// TODO(logan): The server currently doesn't send email in this case.
|
|
// Insert a dummy message to force an email to be sent. Remove this
|
|
// once the fix for issue 6841 is stable on googlesource.com.
|
|
obj.message = START_REVIEW_MESSAGE;
|
|
}
|
|
|
|
const errFn = this._handle400Error.bind(this);
|
|
return this._saveReview(obj, errFn).then(response => {
|
|
if (!response) {
|
|
// Null or undefined response indicates that an error handler
|
|
// took responsibility, so just return.
|
|
return {};
|
|
}
|
|
if (!response.ok) {
|
|
this.fire('server-error', {response});
|
|
return {};
|
|
}
|
|
|
|
// TODO(logan): Remove once the required API changes are live and stable
|
|
// on googlesource.com.
|
|
return this._maybeSetReady(startReview, response).catch(err => {
|
|
// We catch error here because we still want to treat this as a
|
|
// successful review.
|
|
console.error('error setting ready:', err);
|
|
}).then(() => {
|
|
this.draft = '';
|
|
this._includeComments = true;
|
|
this.fire('send', null, {bubbles: false});
|
|
return accountAdditions;
|
|
});
|
|
}).then(result => {
|
|
this.disabled = false;
|
|
return result;
|
|
}).catch(err => {
|
|
this.disabled = false;
|
|
throw err;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns a promise resolving to true if review was successfully posted,
|
|
* false otherwise.
|
|
*
|
|
* TODO(logan): Remove this once the required API changes are live and
|
|
* stable on googlesource.com.
|
|
*/
|
|
_maybeSetReady(startReview, response) {
|
|
return this.$.restAPI.getResponseObject(response).then(result => {
|
|
if (!startReview || result.ready) {
|
|
return Promise.resolve();
|
|
}
|
|
// We don't have confirmation that review was started, so attempt to
|
|
// start review explicitly.
|
|
return this.$.restAPI.startReview(
|
|
this.change._number, null, response => {
|
|
// If we see a 409 response code, then that means the server
|
|
// *does* support moving from WIP->ready when posting a
|
|
// review. Only alert user for non-409 failures.
|
|
if (response.status !== 409) {
|
|
this.fire('server-error', {response});
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
_focusOn(section) {
|
|
// Safeguard- always want to focus on something.
|
|
if (!section || section === FocusTarget.ANY) {
|
|
section = this._chooseFocusTarget();
|
|
}
|
|
if (section === FocusTarget.BODY) {
|
|
const textarea = this.$.textarea;
|
|
textarea.async(textarea.getNativeTextarea()
|
|
.focus.bind(textarea.getNativeTextarea()));
|
|
} else if (section === FocusTarget.REVIEWERS) {
|
|
const reviewerEntry = this.$.reviewers.focusStart;
|
|
reviewerEntry.async(reviewerEntry.focus);
|
|
} else if (section === FocusTarget.CCS) {
|
|
const ccEntry = this.$$('#ccs').focusStart;
|
|
ccEntry.async(ccEntry.focus);
|
|
}
|
|
},
|
|
|
|
_chooseFocusTarget() {
|
|
// If we are the owner and the reviewers field is empty, focus on that.
|
|
if (this._account && this.change && 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(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});
|
|
return response;
|
|
}
|
|
|
|
// Process the response body, format a better error message, and fire
|
|
// an event for gr-event-manager to display.
|
|
const jsonPromise = this.$.restAPI.getResponseObject(response);
|
|
return jsonPromise.then(result => {
|
|
const errors = [];
|
|
for (const state of ['reviewers', 'ccs']) {
|
|
if (!result.hasOwnProperty(state)) { continue; }
|
|
for (const reviewer of Object.values(result[state])) {
|
|
if (reviewer.error) {
|
|
errors.push(reviewer.error);
|
|
}
|
|
}
|
|
}
|
|
response = {
|
|
ok: false,
|
|
status: response.status,
|
|
text() { return Promise.resolve(errors.join(', ')); },
|
|
};
|
|
this.fire('server-error', {response});
|
|
});
|
|
},
|
|
|
|
_computeHideDraftList(drafts) {
|
|
return Object.keys(drafts || {}).length == 0;
|
|
},
|
|
|
|
_computeDraftsTitle(drafts) {
|
|
let total = 0;
|
|
for (const file in drafts) {
|
|
if (drafts.hasOwnProperty(file)) {
|
|
total += drafts[file].length;
|
|
}
|
|
}
|
|
if (total == 0) { return ''; }
|
|
if (total == 1) { return '1 Draft'; }
|
|
if (total > 1) { return total + ' Drafts'; }
|
|
},
|
|
|
|
_computeMessagePlaceholder(canBeStarted) {
|
|
return canBeStarted ?
|
|
'Add a note for your reviewers...' :
|
|
'Say something nice...';
|
|
},
|
|
|
|
_changeUpdated(changeRecord, owner, serverConfig) {
|
|
this._rebuildReviewerArrays(changeRecord.base, owner, serverConfig);
|
|
},
|
|
|
|
_rebuildReviewerArrays(change, owner, serverConfig) {
|
|
this._owner = owner;
|
|
|
|
let reviewers = [];
|
|
const ccs = [];
|
|
|
|
for (const key in change) {
|
|
if (change.hasOwnProperty(key)) {
|
|
if (key !== 'REVIEWER' && key !== 'CC') {
|
|
console.warn('unexpected reviewer state:', key);
|
|
continue;
|
|
}
|
|
for (const entry of change[key]) {
|
|
if (entry._account_id === owner._account_id) {
|
|
continue;
|
|
}
|
|
switch (key) {
|
|
case 'REVIEWER':
|
|
reviewers.push(entry);
|
|
break;
|
|
case 'CC':
|
|
ccs.push(entry);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._ccsEnabled) {
|
|
this._ccs = ccs;
|
|
} else {
|
|
this._ccs = [];
|
|
reviewers = reviewers.concat(ccs);
|
|
}
|
|
this._reviewers = reviewers;
|
|
},
|
|
|
|
_accountOrGroupKey(entry) {
|
|
return entry.id || entry._account_id;
|
|
},
|
|
|
|
/**
|
|
* Generates a function to filter out reviewer/CC entries. When isCCs is
|
|
* truthy, the function filters out entries that already exist in this._ccs.
|
|
* When falsy, the function filters entries that exist in this._reviewers.
|
|
* @param {boolean} isCCs
|
|
* @return {!Function}
|
|
*/
|
|
_filterReviewerSuggestionGenerator(isCCs) {
|
|
return suggestion => {
|
|
let 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;
|
|
}
|
|
|
|
const key = this._accountOrGroupKey(entry);
|
|
const finder = entry => this._accountOrGroupKey(entry) === key;
|
|
if (isCCs) {
|
|
return this._ccs.find(finder) === undefined;
|
|
}
|
|
return this._reviewers.find(finder) === undefined;
|
|
};
|
|
},
|
|
|
|
_getAccount() {
|
|
return this.$.restAPI.getAccount();
|
|
},
|
|
|
|
_cancelTapHandler(e) {
|
|
e.preventDefault();
|
|
this.cancel();
|
|
},
|
|
|
|
cancel() {
|
|
this.fire('cancel', null, {bubbles: false});
|
|
this.$.textarea.closeDropdown();
|
|
this._purgeReviewersPendingRemove(true);
|
|
this._rebuildReviewerArrays(this.change.reviewers, this._owner,
|
|
this.serverConfig);
|
|
},
|
|
|
|
_saveTapHandler(e) {
|
|
e.preventDefault();
|
|
if (this._ccsEnabled && !this.$$('#ccs').submitEntryText()) {
|
|
// Do not proceed with the save if there is an invalid email entry in
|
|
// the text field of the CC entry.
|
|
return;
|
|
}
|
|
this.send(this._includeComments, false).then(keepReviewers => {
|
|
this._purgeReviewersPendingRemove(false, keepReviewers);
|
|
});
|
|
},
|
|
|
|
_sendTapHandler(e) {
|
|
e.preventDefault();
|
|
this._submit();
|
|
},
|
|
|
|
_submit() {
|
|
if (this._ccsEnabled && !this.$$('#ccs').submitEntryText()) {
|
|
// Do not proceed with the send if there is an invalid email entry in
|
|
// the text field of the CC entry.
|
|
return;
|
|
}
|
|
if (this._sendDisabled) {
|
|
this.dispatchEvent(new CustomEvent('show-alert', {
|
|
bubbles: true,
|
|
detail: {message: EMPTY_REPLY_MESSAGE},
|
|
}));
|
|
return;
|
|
}
|
|
return this.send(this._includeComments, this.canBeStarted)
|
|
.then(keepReviewers => {
|
|
this._purgeReviewersPendingRemove(false, keepReviewers);
|
|
});
|
|
},
|
|
|
|
_saveReview(review, opt_errFn) {
|
|
return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
|
|
review, opt_errFn);
|
|
},
|
|
|
|
_reviewerPendingConfirmationUpdated(reviewer) {
|
|
if (reviewer === null) {
|
|
this.$.reviewerConfirmationOverlay.close();
|
|
} else {
|
|
this._pendingConfirmationDetails =
|
|
this._ccPendingConfirmation || this._reviewerPendingConfirmation;
|
|
this.$.reviewerConfirmationOverlay.open();
|
|
}
|
|
},
|
|
|
|
_confirmPendingReviewer() {
|
|
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() {
|
|
this._ccPendingConfirmation = null;
|
|
this._reviewerPendingConfirmation = null;
|
|
|
|
const target =
|
|
this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
|
|
this._focusOn(target);
|
|
},
|
|
|
|
_getStorageLocation() {
|
|
// Tests trigger this method without setting change.
|
|
if (!this.change) { return {}; }
|
|
return {
|
|
changeNum: this.change._number,
|
|
patchNum: '@change',
|
|
path: '@change',
|
|
};
|
|
},
|
|
|
|
_loadStoredDraft() {
|
|
const draft = this.$.storage.getDraftComment(this._getStorageLocation());
|
|
return draft ? draft.message : '';
|
|
},
|
|
|
|
_handleAccountTextEntry() {
|
|
// When either of the account entries has input added to the autocomplete,
|
|
// it should trigger the save button to enable/
|
|
//
|
|
// Note: if the text is removed, the save button will not get disabled.
|
|
this._reviewersMutated = true;
|
|
},
|
|
|
|
_draftChanged(newDraft, oldDraft) {
|
|
this.debounce('store', () => {
|
|
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);
|
|
},
|
|
|
|
_handleHeightChanged(e) {
|
|
this.fire('autogrow');
|
|
},
|
|
|
|
_handleLabelsChanged() {
|
|
this._labelsChanged = Object.keys(
|
|
this.$.labelScores.getLabelValues()).length !== 0;
|
|
},
|
|
|
|
_isState(knownLatestState, value) {
|
|
return knownLatestState === value;
|
|
},
|
|
|
|
_reload() {
|
|
// Load the current change without any patch range.
|
|
location.href = this.getBaseUrl() + '/c/' + this.change._number;
|
|
},
|
|
|
|
_computeSendButtonLabel(canBeStarted) {
|
|
return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND;
|
|
},
|
|
|
|
_computeSendButtonTooltip(canBeStarted) {
|
|
return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND;
|
|
},
|
|
|
|
_computeCCsEnabled(serverConfig) {
|
|
return serverConfig && serverConfig.note_db_enabled;
|
|
},
|
|
|
|
_computeSavingLabelClass(savingComments) {
|
|
return savingComments ? 'saving' : '';
|
|
},
|
|
|
|
_computeSendButtonDisabled(buttonLabel, drafts, text, reviewersMutated,
|
|
labelsChanged, includeComments, disabled) {
|
|
if (disabled) { return true; }
|
|
if (buttonLabel === ButtonLabels.START_REVIEW) { return false; }
|
|
const hasDrafts = includeComments && Object.keys(drafts).length;
|
|
return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged;
|
|
},
|
|
|
|
_computePatchSetWarning(patchNum, labelsChanged) {
|
|
let str = `Patch ${patchNum} is not latest.`;
|
|
if (labelsChanged) {
|
|
str += ' Voting on a non-latest patch will have no effect.';
|
|
}
|
|
return str;
|
|
},
|
|
|
|
setPluginMessage(message) {
|
|
this._pluginMessage = message;
|
|
},
|
|
|
|
_sendDisabledChanged(sendDisabled) {
|
|
this.dispatchEvent(new CustomEvent('send-disabled-changed'));
|
|
},
|
|
});
|
|
})();
|