2018-03-26 10:04:27 -04:00
|
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2016-03-04 17:48:22 -05:00
|
|
|
|
(function() {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
const STORAGE_DEBOUNCE_INTERVAL = 400;
|
2017-09-12 17:01:42 -07:00
|
|
|
|
const TOAST_DEBOUNCE_INTERVAL = 200;
|
|
|
|
|
|
|
|
|
|
const SAVING_MESSAGE = 'Saving';
|
|
|
|
|
const DRAFT_SINGULAR = 'draft...';
|
|
|
|
|
const DRAFT_PLURAL = 'drafts...';
|
|
|
|
|
const SAVED_MESSAGE = 'All changes saved';
|
2016-05-18 12:37:53 -07:00
|
|
|
|
|
2018-05-14 12:59:23 -07:00
|
|
|
|
const REPORT_CREATE_DRAFT = 'CreateDraftComment';
|
|
|
|
|
const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
|
|
|
|
|
const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
|
|
|
|
|
|
2016-03-04 17:48:22 -05:00
|
|
|
|
Polymer({
|
|
|
|
|
is: 'gr-diff-comment',
|
|
|
|
|
|
2016-12-07 13:36:49 -08:00
|
|
|
|
/**
|
|
|
|
|
* Fired when the create fix comment action is triggered.
|
|
|
|
|
*
|
|
|
|
|
* @event create-fix-comment
|
2016-03-04 17:48:22 -05:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fired when this comment is discarded.
|
|
|
|
|
*
|
2016-03-25 11:29:36 -04:00
|
|
|
|
* @event comment-discard
|
2016-03-04 17:48:22 -05:00
|
|
|
|
*/
|
|
|
|
|
|
2016-05-19 17:24:45 -07:00
|
|
|
|
/**
|
|
|
|
|
* Fired when this comment is saved.
|
|
|
|
|
*
|
|
|
|
|
* @event comment-save
|
|
|
|
|
*/
|
|
|
|
|
|
2016-05-23 15:24:05 -07:00
|
|
|
|
/**
|
|
|
|
|
* Fired when this comment is updated.
|
|
|
|
|
*
|
|
|
|
|
* @event comment-update
|
|
|
|
|
*/
|
|
|
|
|
|
2016-06-09 16:08:04 -07:00
|
|
|
|
/**
|
|
|
|
|
* @event comment-mouse-over
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @event comment-mouse-out
|
|
|
|
|
*/
|
|
|
|
|
|
2016-03-04 17:48:22 -05:00
|
|
|
|
properties: {
|
|
|
|
|
changeNum: String,
|
2017-08-11 16:32:47 -07:00
|
|
|
|
/** @type {?} */
|
2016-03-04 17:48:22 -05:00
|
|
|
|
comment: {
|
|
|
|
|
type: Object,
|
|
|
|
|
notify: true,
|
2016-05-23 15:24:05 -07:00
|
|
|
|
observer: '_commentChanged',
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
2016-12-07 13:36:49 -08:00
|
|
|
|
isRobotComment: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false,
|
|
|
|
|
reflectToAttribute: true,
|
|
|
|
|
},
|
2016-03-04 17:48:22 -05:00
|
|
|
|
disabled: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false,
|
|
|
|
|
reflectToAttribute: true,
|
|
|
|
|
},
|
|
|
|
|
draft: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false,
|
|
|
|
|
observer: '_draftChanged',
|
|
|
|
|
},
|
|
|
|
|
editing: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false,
|
|
|
|
|
observer: '_editingChanged',
|
|
|
|
|
},
|
2018-06-12 16:18:34 -07:00
|
|
|
|
discarding: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false,
|
|
|
|
|
reflectToAttribute: true,
|
|
|
|
|
},
|
2016-12-07 13:36:49 -08:00
|
|
|
|
hasChildren: Boolean,
|
2016-03-04 17:48:22 -05:00
|
|
|
|
patchNum: String,
|
|
|
|
|
showActions: Boolean,
|
2016-12-07 13:36:49 -08:00
|
|
|
|
_showHumanActions: Boolean,
|
|
|
|
|
_showRobotActions: Boolean,
|
2016-10-10 17:31:56 -07:00
|
|
|
|
collapsed: {
|
2016-10-06 10:18:32 -07:00
|
|
|
|
type: Boolean,
|
|
|
|
|
value: true,
|
|
|
|
|
observer: '_toggleCollapseClass',
|
|
|
|
|
},
|
2017-08-11 16:32:47 -07:00
|
|
|
|
/** @type {?} */
|
2016-03-04 17:48:22 -05:00
|
|
|
|
projectConfig: Object,
|
Move reply buttons to comment thread
Move all buttons that generate a reply of some sort (done, ack, reply,
quote) to the comment thread instead of the comment [1].
When there is a draft for a particular comment thread, all reply buttons
are hidden [2]. For example, if you click reply, you cannot click done
on the same thread, unless you remove the draft.
Each thread can have up to 1 draft. It's also worth noting that if a
thread has a draft, and the user clicks on the line or 'c' at the same
range, the existing draft will switch to 'editing' form.
[1] With the exception of "please fix" for robot comments.
[2] In this case, The please fix button will be disabled when other
reply buttons are hidden.
Feature: Issue 5410
Change-Id: Id847ee0cba0d0ce4e5b6476f58141866d41ffdad
2017-02-09 16:07:32 -08:00
|
|
|
|
robotButtonDisabled: Boolean,
|
2017-05-08 10:40:06 -07:00
|
|
|
|
_isAdmin: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false,
|
|
|
|
|
},
|
2016-03-04 17:48:22 -05:00
|
|
|
|
|
2017-07-05 11:17:26 -07:00
|
|
|
|
_xhrPromise: Object, // Used for testing.
|
2016-05-23 13:53:10 -07:00
|
|
|
|
_messageText: {
|
2016-05-18 12:37:53 -07:00
|
|
|
|
type: String,
|
2016-05-23 19:03:11 -04:00
|
|
|
|
value: '',
|
2016-05-23 13:53:10 -07:00
|
|
|
|
observer: '_messageTextChanged',
|
2016-05-18 12:37:53 -07:00
|
|
|
|
},
|
2017-01-30 12:03:13 -08:00
|
|
|
|
commentSide: String,
|
2017-01-04 16:45:21 -08:00
|
|
|
|
|
2018-03-08 17:29:09 -08:00
|
|
|
|
resolved: Boolean,
|
2017-09-12 17:01:42 -07:00
|
|
|
|
|
2017-10-26 14:11:24 -07:00
|
|
|
|
_numPendingDraftRequests: {
|
2017-09-12 17:01:42 -07:00
|
|
|
|
type: Object,
|
|
|
|
|
value: {number: 0}, // Intentional to share the object across instances.
|
|
|
|
|
},
|
2017-11-02 18:17:52 -07:00
|
|
|
|
|
2018-04-05 10:48:03 -07:00
|
|
|
|
_enableOverlay: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Property for storing references to overlay elements. When the overlays
|
|
|
|
|
* are moved to Gerrit.getRootElement() to be shown they are no-longer
|
|
|
|
|
* children, so they can't be queried along the tree, so they are stored
|
|
|
|
|
* here.
|
|
|
|
|
*/
|
|
|
|
|
_overlays: {
|
|
|
|
|
type: Object,
|
|
|
|
|
value: () => ({}),
|
|
|
|
|
},
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
|
|
|
|
|
2016-05-23 19:03:11 -04:00
|
|
|
|
observers: [
|
|
|
|
|
'_commentMessageChanged(comment.message)',
|
|
|
|
|
'_loadLocalDraft(changeNum, patchNum, comment)',
|
2016-12-07 13:36:49 -08:00
|
|
|
|
'_isRobotComment(comment)',
|
|
|
|
|
'_calculateActionstoShow(showActions, isRobotComment)',
|
2016-05-23 19:03:11 -04:00
|
|
|
|
],
|
2016-03-04 17:48:22 -05:00
|
|
|
|
|
2017-06-19 11:27:22 -07:00
|
|
|
|
behaviors: [
|
|
|
|
|
Gerrit.KeyboardShortcutBehavior,
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
keyBindings: {
|
|
|
|
|
'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
|
|
|
|
|
'esc': '_handleEsc',
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
attached() {
|
2016-10-06 10:18:32 -07:00
|
|
|
|
if (this.editing) {
|
2016-10-10 17:31:56 -07:00
|
|
|
|
this.collapsed = false;
|
2017-01-06 11:33:42 -08:00
|
|
|
|
} else if (this.comment) {
|
|
|
|
|
this.collapsed = this.comment.collapsed;
|
2016-10-06 10:18:32 -07:00
|
|
|
|
}
|
2017-05-16 13:50:27 -07:00
|
|
|
|
this._getIsAdmin().then(isAdmin => {
|
2017-05-08 10:40:06 -07:00
|
|
|
|
this._isAdmin = isAdmin;
|
2017-05-16 13:50:27 -07:00
|
|
|
|
});
|
2016-10-06 10:18:32 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
detached() {
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this.cancelDebouncer('fire-update');
|
2018-04-05 10:48:03 -07:00
|
|
|
|
if (this.textarea) {
|
|
|
|
|
this.textarea.closeDropdown();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
get textarea() {
|
|
|
|
|
return this.$$('#editTextarea');
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
get confirmDeleteOverlay() {
|
|
|
|
|
if (!this._overlays.confirmDelete) {
|
|
|
|
|
this._enableOverlay = true;
|
|
|
|
|
Polymer.dom.flush();
|
|
|
|
|
this._overlays.confirmDelete = this.$$('#confirmDeleteOverlay');
|
|
|
|
|
}
|
|
|
|
|
return this._overlays.confirmDelete;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
get confirmDiscardOverlay() {
|
|
|
|
|
if (!this._overlays.confirmDiscard) {
|
|
|
|
|
this._enableOverlay = true;
|
|
|
|
|
Polymer.dom.flush();
|
|
|
|
|
this._overlays.confirmDiscard = this.$$('#confirmDiscardOverlay');
|
|
|
|
|
}
|
|
|
|
|
return this._overlays.confirmDiscard;
|
2016-05-23 15:24:05 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_computeShowHideText(collapsed) {
|
2016-10-06 10:18:32 -07:00
|
|
|
|
return collapsed ? '◀' : '▼';
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_calculateActionstoShow(showActions, isRobotComment) {
|
2016-12-07 13:36:49 -08:00
|
|
|
|
this._showHumanActions = showActions && !isRobotComment;
|
|
|
|
|
this._showRobotActions = showActions && isRobotComment;
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_isRobotComment(comment) {
|
2016-12-07 13:36:49 -08:00
|
|
|
|
this.isRobotComment = !!comment.robot_id;
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
isOnParent() {
|
2017-03-21 14:56:19 -07:00
|
|
|
|
return this.side === 'PARENT';
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_getIsAdmin() {
|
2017-05-08 10:40:06 -07:00
|
|
|
|
return this.$.restAPI.getIsAdmin();
|
|
|
|
|
},
|
|
|
|
|
|
2017-12-06 11:25:30 -08:00
|
|
|
|
/**
|
|
|
|
|
* @param {*=} opt_comment
|
|
|
|
|
*/
|
|
|
|
|
save(opt_comment) {
|
|
|
|
|
let comment = opt_comment;
|
2018-06-12 16:18:34 -07:00
|
|
|
|
if (!comment) { comment = this.comment; }
|
2017-03-13 16:40:01 -07:00
|
|
|
|
|
2018-06-12 16:18:34 -07:00
|
|
|
|
this.set('comment.message', this._messageText);
|
|
|
|
|
this.editing = false;
|
2016-03-04 17:48:22 -05:00
|
|
|
|
this.disabled = true;
|
2016-05-18 12:37:53 -07:00
|
|
|
|
|
2017-11-27 16:13:06 -08:00
|
|
|
|
if (!this._messageText) {
|
|
|
|
|
return this._discardDraft();
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-06 11:25:30 -08:00
|
|
|
|
this._xhrPromise = this._saveDraft(comment).then(response => {
|
2016-03-04 17:48:22 -05:00
|
|
|
|
this.disabled = false;
|
2016-05-02 15:59:59 -04:00
|
|
|
|
if (!response.ok) { return response; }
|
|
|
|
|
|
2017-10-26 14:11:24 -07:00
|
|
|
|
this._eraseDraftComment();
|
2017-05-16 13:50:27 -07:00
|
|
|
|
return this.$.restAPI.getResponseObject(response).then(obj => {
|
2017-12-06 11:25:30 -08:00
|
|
|
|
const resComment = obj;
|
|
|
|
|
resComment.__draft = true;
|
2016-05-03 15:14:57 -04:00
|
|
|
|
// Maintain the ephemeral draft ID for identification by other
|
|
|
|
|
// elements.
|
|
|
|
|
if (this.comment.__draftID) {
|
2017-12-06 11:25:30 -08:00
|
|
|
|
resComment.__draftID = this.comment.__draftID;
|
2016-05-03 15:14:57 -04:00
|
|
|
|
}
|
2017-12-06 11:25:30 -08:00
|
|
|
|
resComment.__commentSide = this.commentSide;
|
|
|
|
|
this.comment = resComment;
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this._fireSave();
|
2016-05-03 15:14:57 -04:00
|
|
|
|
return obj;
|
2017-05-16 13:50:27 -07:00
|
|
|
|
});
|
|
|
|
|
}).catch(err => {
|
2016-03-04 17:48:22 -05:00
|
|
|
|
this.disabled = false;
|
2016-05-02 15:59:59 -04:00
|
|
|
|
throw err;
|
2017-05-16 13:50:27 -07:00
|
|
|
|
});
|
2017-10-26 14:11:24 -07:00
|
|
|
|
|
|
|
|
|
return this._xhrPromise;
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_eraseDraftComment() {
|
2017-05-16 15:12:08 -07:00
|
|
|
|
// Prevents a race condition in which removing the draft comment occurs
|
|
|
|
|
// prior to it being saved.
|
|
|
|
|
this.cancelDebouncer('store');
|
2017-05-16 13:50:27 -07:00
|
|
|
|
|
2016-12-01 10:55:14 -08:00
|
|
|
|
this.$.storage.eraseDraftComment({
|
|
|
|
|
changeNum: this.changeNum,
|
2017-03-13 11:42:42 -07:00
|
|
|
|
patchNum: this._getPatchNum(),
|
2016-12-01 10:55:14 -08:00
|
|
|
|
path: this.comment.path,
|
|
|
|
|
line: this.comment.line,
|
2017-01-31 16:57:14 -08:00
|
|
|
|
range: this.comment.range,
|
2016-12-01 10:55:14 -08:00
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_commentChanged(comment) {
|
2016-05-23 15:24:05 -07:00
|
|
|
|
this.editing = !!comment.__editing;
|
2017-01-04 16:45:21 -08:00
|
|
|
|
this.resolved = !comment.unresolved;
|
2016-06-09 16:08:04 -07:00
|
|
|
|
if (this.editing) { // It's a new draft/reply, notify.
|
|
|
|
|
this._fireUpdate();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2017-08-11 16:32:47 -07:00
|
|
|
|
/**
|
|
|
|
|
* @param {!Object=} opt_mixin
|
|
|
|
|
*
|
|
|
|
|
* @return {!Object}
|
|
|
|
|
*/
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_getEventPayload(opt_mixin) {
|
|
|
|
|
return Object.assign({}, opt_mixin, {
|
2016-06-09 16:08:04 -07:00
|
|
|
|
comment: this.comment,
|
|
|
|
|
patchNum: this.patchNum,
|
2017-05-16 13:50:27 -07:00
|
|
|
|
});
|
2016-06-09 16:08:04 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_fireSave() {
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this.fire('comment-save', this._getEventPayload());
|
2016-05-23 15:24:05 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_fireUpdate() {
|
|
|
|
|
this.debounce('fire-update', () => {
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this.fire('comment-update', this._getEventPayload());
|
2016-07-14 12:31:09 -07:00
|
|
|
|
});
|
2016-05-23 15:24:05 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_draftChanged(draft) {
|
2016-03-04 17:48:22 -05:00
|
|
|
|
this.$.container.classList.toggle('draft', draft);
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_editingChanged(editing, previousValue) {
|
2016-03-04 17:48:22 -05:00
|
|
|
|
this.$.container.classList.toggle('editing', editing);
|
|
|
|
|
if (this.comment && this.comment.id) {
|
|
|
|
|
this.$$('.cancel').hidden = !editing;
|
|
|
|
|
}
|
2016-05-23 15:24:05 -07:00
|
|
|
|
if (this.comment) {
|
|
|
|
|
this.comment.__editing = this.editing;
|
|
|
|
|
}
|
2016-06-09 16:08:04 -07:00
|
|
|
|
if (editing != !!previousValue) {
|
|
|
|
|
// To prevent event firing on comment creation.
|
|
|
|
|
this._fireUpdate();
|
|
|
|
|
}
|
2018-04-05 10:48:03 -07:00
|
|
|
|
if (editing) {
|
|
|
|
|
this.async(() => {
|
|
|
|
|
Polymer.dom.flush();
|
|
|
|
|
this.textarea.putCursorAtEnd();
|
|
|
|
|
}, 1);
|
|
|
|
|
}
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_computeLinkToComment(comment) {
|
2016-03-04 17:48:22 -05:00
|
|
|
|
return '#' + comment.line;
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-18 16:34:17 -07:00
|
|
|
|
_computeDeleteButtonClass(isAdmin, draft) {
|
|
|
|
|
return isAdmin && !draft ? 'showDeleteButtons' : '';
|
2017-05-18 13:02:14 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-12-05 11:03:52 -08:00
|
|
|
|
_computeSaveDisabled(draft, comment, resolved) {
|
|
|
|
|
// If resolved state has changed and a msg exists, save should be enabled.
|
|
|
|
|
if (comment.unresolved === resolved && draft) { return false; }
|
2017-11-27 16:13:06 -08:00
|
|
|
|
return !draft || draft.trim() === '';
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
|
|
|
|
|
2017-06-19 11:27:22 -07:00
|
|
|
|
_handleSaveKey(e) {
|
2017-12-05 11:03:52 -08:00
|
|
|
|
if (!this._computeSaveDisabled(this._messageText, this.comment,
|
|
|
|
|
this.resolved)) {
|
2017-06-19 11:27:22 -07:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
this._handleSave(e);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_handleEsc(e) {
|
|
|
|
|
if (!this._messageText.length) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this._handleCancel(e);
|
2016-03-04 17:48:22 -05:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleToggleCollapsed() {
|
2016-10-10 17:31:56 -07:00
|
|
|
|
this.collapsed = !this.collapsed;
|
2016-10-06 10:18:32 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_toggleCollapseClass(collapsed) {
|
2016-10-10 17:31:56 -07:00
|
|
|
|
if (collapsed) {
|
2016-10-06 10:18:32 -07:00
|
|
|
|
this.$.container.classList.add('collapsed');
|
|
|
|
|
} else {
|
|
|
|
|
this.$.container.classList.remove('collapsed');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_commentMessageChanged(message) {
|
2016-05-23 19:03:11 -04:00
|
|
|
|
this._messageText = message || '';
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_messageTextChanged(newValue, oldValue) {
|
2016-05-23 19:03:11 -04:00
|
|
|
|
if (!this.comment || (this.comment && this.comment.id)) { return; }
|
2016-05-18 12:37:53 -07:00
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
this.debounce('store', () => {
|
|
|
|
|
const message = this._messageText;
|
|
|
|
|
const commentLocation = {
|
2016-05-23 13:53:10 -07:00
|
|
|
|
changeNum: this.changeNum,
|
2017-03-13 11:42:42 -07:00
|
|
|
|
patchNum: this._getPatchNum(),
|
2016-05-23 13:53:10 -07:00
|
|
|
|
path: this.comment.path,
|
|
|
|
|
line: this.comment.line,
|
2017-01-31 16:57:14 -08:00
|
|
|
|
range: this.comment.range,
|
2016-05-23 13:53:10 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ((!this._messageText || !this._messageText.length) && oldValue) {
|
|
|
|
|
// If the draft has been modified to be empty, then erase the storage
|
|
|
|
|
// entry.
|
|
|
|
|
this.$.storage.eraseDraftComment(commentLocation);
|
|
|
|
|
} else {
|
|
|
|
|
this.$.storage.setDraftComment(commentLocation, message);
|
2016-05-18 12:37:53 -07:00
|
|
|
|
}
|
2016-05-23 13:53:10 -07:00
|
|
|
|
}, STORAGE_DEBOUNCE_INTERVAL);
|
2016-05-18 12:37:53 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleLinkTap(e) {
|
2016-03-04 17:48:22 -05:00
|
|
|
|
e.preventDefault();
|
2017-05-16 13:50:27 -07:00
|
|
|
|
const hash = this._computeLinkToComment(this.comment);
|
2016-03-04 17:48:22 -05:00
|
|
|
|
// Don't add the hash to the window history if it's already there.
|
|
|
|
|
// Otherwise you mess up expected back button behavior.
|
|
|
|
|
if (window.location.hash == hash) { return; }
|
|
|
|
|
// Change the URL but don’t trigger a nav event. Otherwise it will
|
|
|
|
|
// reload the page.
|
|
|
|
|
page.show(window.location.pathname + hash, null, false);
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleEdit(e) {
|
2016-11-15 17:01:15 -08:00
|
|
|
|
e.preventDefault();
|
2016-05-23 13:53:10 -07:00
|
|
|
|
this._messageText = this.comment.message;
|
2016-03-04 17:48:22 -05:00
|
|
|
|
this.editing = true;
|
2018-06-19 17:03:43 -07:00
|
|
|
|
this.$.reporting.recordDraftInteraction();
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleSave(e) {
|
2016-11-15 17:01:15 -08:00
|
|
|
|
e.preventDefault();
|
2017-10-24 16:34:10 -07:00
|
|
|
|
|
|
|
|
|
// Ignore saves started while already saving.
|
|
|
|
|
if (this.disabled) { return; }
|
2018-05-14 12:59:23 -07:00
|
|
|
|
const timingLabel = this.comment.id ?
|
|
|
|
|
REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
|
2018-06-13 14:14:38 -07:00
|
|
|
|
const timer = this.$.reporting.getTimer(timingLabel);
|
2017-01-04 16:45:21 -08:00
|
|
|
|
this.set('comment.__editing', false);
|
2018-06-13 14:14:38 -07:00
|
|
|
|
return this.save().then(() => { timer.end(); });
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleCancel(e) {
|
2016-11-15 17:01:15 -08:00
|
|
|
|
e.preventDefault();
|
Clean up comment editing UX
The removal of the 'Discard' button from comments in the editing state
complicated the UX of the 'Cancel' button a bit.
Previously:
- Discard deleted the draft completely.
- Cancel cancelled whatever edits were made, and reverted to the prior
draft state. Cancel was also hidden on new drafts.
Currently:
- Cancel cancels whatever edits were made, and reverts to whatever edit
was last stored.
This behavior is problematic, for a few reasons:
- Due to some vestigial code, the actual value of the comment's message
(not just the buffer value '_messageText') was updated any time the
draft comment was edited. This was done to prevent multiple drafts
from being created on the same thread -- that issue was made obsolete
by the removal of buttons from individual comments, and their
consolidation to the thread level.
- If a user cancels a draft, then restarts it (having their text
repopulated from localstorage), then cancels it again, the draft is
rendered as a saved draft comment with the localstorage value, despite
having never been saved at all.
So, with this change:
- Cancel cancels whatever edits were made.
- If a user tries to add a draft in the same place...
- ...and the draft has been saved on the server, the textarea is
prepopulated with the value from the comment received from the
server.
- ...and the draft has NOT been saved on the server, the textarea is
rehydrated from localstorage.
This UX makes sense because...
- Case 1 occurs when a user clicks "Edit" on an existing draft comment -
thus expecting to edit the content shown in the UI.
- Case 2 occurs when a user cancels a draft that they started
constructing, but did not save -- thus there is no suggestion of any
prepopulated value by the UI, _and_ the case in which a draft was
erroneously cancelled during editing can be solved.
Bug: Issue 7709
Change-Id: Iacf9a91fbb0226aba3a518f56edd61f28c15a8ef
2017-11-09 11:38:59 -08:00
|
|
|
|
|
|
|
|
|
if (!this.comment.message ||
|
|
|
|
|
this.comment.message.trim().length === 0 ||
|
|
|
|
|
!this.comment.id) {
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this._fireDiscard();
|
2016-03-04 17:48:22 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
2016-05-23 13:53:10 -07:00
|
|
|
|
this._messageText = this.comment.message;
|
2016-03-04 17:48:22 -05:00
|
|
|
|
this.editing = false;
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_fireDiscard() {
|
2016-08-05 15:05:50 -07:00
|
|
|
|
this.cancelDebouncer('fire-update');
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this.fire('comment-discard', this._getEventPayload());
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleDiscard(e) {
|
2016-11-15 17:01:15 -08:00
|
|
|
|
e.preventDefault();
|
2018-06-19 17:03:43 -07:00
|
|
|
|
this.$.reporting.recordDraftInteraction();
|
2017-12-05 11:40:43 -08:00
|
|
|
|
|
|
|
|
|
if (!this._messageText) {
|
2017-10-18 13:22:43 -07:00
|
|
|
|
this._discardDraft();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-07-10 18:04:44 -07:00
|
|
|
|
|
|
|
|
|
this._openOverlay(this.confirmDiscardOverlay).then(() => {
|
|
|
|
|
this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
|
|
|
|
|
.resetFocus();
|
|
|
|
|
});
|
2017-10-18 13:22:43 -07:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_handleConfirmDiscard(e) {
|
|
|
|
|
e.preventDefault();
|
2018-06-13 14:14:38 -07:00
|
|
|
|
const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
|
2017-10-18 13:22:43 -07:00
|
|
|
|
this._closeConfirmDiscardOverlay();
|
2018-06-13 14:14:38 -07:00
|
|
|
|
return this._discardDraft().then(() => { timer.end(); });
|
2017-10-18 13:22:43 -07:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_discardDraft() {
|
2016-03-04 17:48:22 -05:00
|
|
|
|
if (!this.comment.__draft) {
|
|
|
|
|
throw Error('Cannot discard a non-draft comment.');
|
|
|
|
|
}
|
2018-06-12 16:18:34 -07:00
|
|
|
|
this.discarding = true;
|
2016-05-23 19:03:11 -04:00
|
|
|
|
this.editing = false;
|
2016-03-04 17:48:22 -05:00
|
|
|
|
this.disabled = true;
|
2016-12-01 10:55:14 -08:00
|
|
|
|
this._eraseDraftComment();
|
|
|
|
|
|
2016-05-03 15:14:57 -04:00
|
|
|
|
if (!this.comment.id) {
|
2016-05-23 19:03:11 -04:00
|
|
|
|
this.disabled = false;
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this._fireDiscard();
|
2016-03-04 17:48:22 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
2016-05-03 15:14:57 -04:00
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
this._xhrPromise = this._deleteDraft(this.comment).then(response => {
|
|
|
|
|
this.disabled = false;
|
2018-06-12 16:18:34 -07:00
|
|
|
|
if (!response.ok) {
|
|
|
|
|
this.discarding = false;
|
|
|
|
|
return response;
|
|
|
|
|
}
|
2016-05-02 15:59:59 -04:00
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
this._fireDiscard();
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
this.disabled = false;
|
|
|
|
|
throw err;
|
|
|
|
|
});
|
2017-11-27 16:13:06 -08:00
|
|
|
|
|
|
|
|
|
return this._xhrPromise;
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
|
|
|
|
|
2017-10-18 13:22:43 -07:00
|
|
|
|
_closeConfirmDiscardOverlay() {
|
2018-04-05 10:48:03 -07:00
|
|
|
|
this._closeOverlay(this.confirmDiscardOverlay);
|
2017-10-18 13:22:43 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-09-12 17:01:42 -07:00
|
|
|
|
_getSavingMessage(numPending) {
|
|
|
|
|
if (numPending === 0) { return SAVED_MESSAGE; }
|
|
|
|
|
return [
|
|
|
|
|
SAVING_MESSAGE,
|
|
|
|
|
numPending,
|
|
|
|
|
numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
|
|
|
|
|
].join(' ');
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_showStartRequest() {
|
2017-10-26 14:11:24 -07:00
|
|
|
|
const numPending = ++this._numPendingDraftRequests.number;
|
2017-09-12 17:01:42 -07:00
|
|
|
|
this._updateRequestToast(numPending);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_showEndRequest() {
|
2017-10-26 14:11:24 -07:00
|
|
|
|
const numPending = --this._numPendingDraftRequests.number;
|
2017-09-12 17:01:42 -07:00
|
|
|
|
this._updateRequestToast(numPending);
|
|
|
|
|
},
|
|
|
|
|
|
2017-10-26 14:11:24 -07:00
|
|
|
|
_handleFailedDraftRequest() {
|
|
|
|
|
this._numPendingDraftRequests.number--;
|
|
|
|
|
|
|
|
|
|
// Cancel the debouncer so that error toasts from the error-manager will
|
|
|
|
|
// not be overridden.
|
|
|
|
|
this.cancelDebouncer('draft-toast');
|
|
|
|
|
},
|
|
|
|
|
|
2017-09-12 17:01:42 -07:00
|
|
|
|
_updateRequestToast(numPending) {
|
|
|
|
|
const message = this._getSavingMessage(numPending);
|
|
|
|
|
this.debounce('draft-toast', () => {
|
|
|
|
|
// Note: the event is fired on the body rather than this element because
|
|
|
|
|
// this element may not be attached by the time this executes, in which
|
|
|
|
|
// case the event would not bubble.
|
|
|
|
|
document.body.dispatchEvent(new CustomEvent('show-alert',
|
|
|
|
|
{detail: {message}, bubbles: true}));
|
|
|
|
|
}, TOAST_DEBOUNCE_INTERVAL);
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_saveDraft(draft) {
|
2017-09-12 17:01:42 -07:00
|
|
|
|
this._showStartRequest();
|
|
|
|
|
return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
|
|
|
|
|
.then(result => {
|
2017-10-26 14:11:24 -07:00
|
|
|
|
if (result.ok) {
|
|
|
|
|
this._showEndRequest();
|
|
|
|
|
} else {
|
|
|
|
|
this._handleFailedDraftRequest();
|
|
|
|
|
}
|
2017-09-12 17:01:42 -07:00
|
|
|
|
return result;
|
|
|
|
|
});
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_deleteDraft(draft) {
|
2017-09-12 17:01:42 -07:00
|
|
|
|
this._showStartRequest();
|
2016-05-03 15:14:57 -04:00
|
|
|
|
return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
|
2017-09-12 17:01:42 -07:00
|
|
|
|
draft).then(result => {
|
2017-10-26 14:11:24 -07:00
|
|
|
|
if (result.ok) {
|
|
|
|
|
this._showEndRequest();
|
|
|
|
|
} else {
|
|
|
|
|
this._handleFailedDraftRequest();
|
|
|
|
|
}
|
2017-09-12 17:01:42 -07:00
|
|
|
|
return result;
|
|
|
|
|
});
|
2016-03-04 17:48:22 -05:00
|
|
|
|
},
|
2016-05-18 12:37:53 -07:00
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_getPatchNum() {
|
2017-03-21 14:56:19 -07:00
|
|
|
|
return this.isOnParent() ? 'PARENT' : this.patchNum;
|
2017-03-13 11:42:42 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_loadLocalDraft(changeNum, patchNum, comment) {
|
2016-05-23 19:03:11 -04:00
|
|
|
|
// Only apply local drafts to comments that haven't been saved
|
|
|
|
|
// remotely, and haven't been given a default message already.
|
2016-12-22 15:04:11 -08:00
|
|
|
|
//
|
|
|
|
|
// Don't get local draft if there is another comment that is currently
|
|
|
|
|
// in an editing state.
|
|
|
|
|
if (!comment || comment.id || comment.message || comment.__otherEditing) {
|
|
|
|
|
delete comment.__otherEditing;
|
2016-05-23 19:03:11 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
2016-05-18 12:37:53 -07:00
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
const draft = this.$.storage.getDraftComment({
|
|
|
|
|
changeNum,
|
2017-03-13 11:42:42 -07:00
|
|
|
|
patchNum: this._getPatchNum(),
|
2016-05-23 19:03:11 -04:00
|
|
|
|
path: comment.path,
|
|
|
|
|
line: comment.line,
|
2017-01-31 16:57:14 -08:00
|
|
|
|
range: comment.range,
|
2016-05-23 19:03:11 -04:00
|
|
|
|
});
|
2016-05-18 12:37:53 -07:00
|
|
|
|
|
2016-05-23 19:03:11 -04:00
|
|
|
|
if (draft) {
|
|
|
|
|
this.set('comment.message', draft.message);
|
|
|
|
|
}
|
2016-05-18 12:37:53 -07:00
|
|
|
|
},
|
2016-06-09 16:08:04 -07:00
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleMouseEnter(e) {
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this.fire('comment-mouse-over', this._getEventPayload());
|
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleMouseLeave(e) {
|
2016-06-09 16:08:04 -07:00
|
|
|
|
this.fire('comment-mouse-out', this._getEventPayload());
|
|
|
|
|
},
|
2017-01-04 16:45:21 -08:00
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleToggleResolved() {
|
2018-06-19 17:03:43 -07:00
|
|
|
|
this.$.reporting.recordDraftInteraction();
|
2017-01-04 16:45:21 -08:00
|
|
|
|
this.resolved = !this.resolved;
|
2017-12-05 11:03:52 -08:00
|
|
|
|
// Modify payload instead of this.comment, as this.comment is passed from
|
|
|
|
|
// the parent by ref.
|
|
|
|
|
const payload = this._getEventPayload();
|
2018-03-08 17:29:09 -08:00
|
|
|
|
payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
|
2017-12-05 11:03:52 -08:00
|
|
|
|
this.fire('comment-update', payload);
|
|
|
|
|
if (!this.editing) {
|
|
|
|
|
// Save the resolved state immediately.
|
2017-12-06 11:25:30 -08:00
|
|
|
|
this.save(payload.comment);
|
2017-12-05 11:03:52 -08:00
|
|
|
|
}
|
2017-01-04 16:45:21 -08:00
|
|
|
|
},
|
2017-05-08 10:40:06 -07:00
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleCommentDelete() {
|
2018-04-05 10:48:03 -07:00
|
|
|
|
this._openOverlay(this.confirmDeleteOverlay);
|
2017-10-18 13:22:43 -07:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_handleCancelDeleteComment() {
|
2018-04-05 10:48:03 -07:00
|
|
|
|
this._closeOverlay(this.confirmDeleteOverlay);
|
2017-10-18 13:22:43 -07:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_openOverlay(overlay) {
|
|
|
|
|
Polymer.dom(Gerrit.getRootElement()).appendChild(overlay);
|
2018-07-10 18:04:44 -07:00
|
|
|
|
return overlay.open();
|
2017-05-08 10:40:06 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-10-18 13:22:43 -07:00
|
|
|
|
_closeOverlay(overlay) {
|
|
|
|
|
Polymer.dom(Gerrit.getRootElement()).removeChild(overlay);
|
|
|
|
|
overlay.close();
|
2017-05-08 10:40:06 -07:00
|
|
|
|
},
|
|
|
|
|
|
2017-05-16 13:50:27 -07:00
|
|
|
|
_handleConfirmDeleteComment() {
|
2018-04-05 10:48:03 -07:00
|
|
|
|
const dialog =
|
|
|
|
|
this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
|
2017-05-08 10:40:06 -07:00
|
|
|
|
this.$.restAPI.deleteComment(
|
2018-04-05 10:48:03 -07:00
|
|
|
|
this.changeNum, this.patchNum, this.comment.id, dialog.message)
|
|
|
|
|
.then(newComment => {
|
2017-05-16 13:50:27 -07:00
|
|
|
|
this._handleCancelDeleteComment();
|
|
|
|
|
this.comment = newComment;
|
|
|
|
|
});
|
2017-05-08 10:40:06 -07:00
|
|
|
|
},
|
2016-03-04 17:48:22 -05:00
|
|
|
|
});
|
|
|
|
|
})();
|