Viktar Donich b2198e8233 Ranged comments integration
- gr-file-list recognizes local preferences (for hasRangedComments flag)
- gr-file-list reacts to cursor hotkey only if there is no range
  selected (currently always false).
- Remove dead code from GrDiffBuilderSideBySide, GrDiffBuilder,
  gr-diff-builder.html
- Bugfix: GrDiffBuilder.prototype.getGroupsByLineRange handles one-line
  BOTH code sections correctly. Test updated as well.
- Added utitily methods added to gr-diff-builder.html to reduce
  dependency on DOM structure and reduce amount of code copy-pasting:
  - renderLineRange, getContentByLine, etc
- For gr-diff.js and gr-diff-comment-thread.js addDraft renamed to
  addOrEditDraft because that's what it does.
- For both, addDraft method always creates a draft comment.
- Added support for ranged comments in gr-diff, gr-diff-comment-thread.
- Added mouseenter and mouseout events to gr-comment.js
- Refactored gr-comment.js to reduce code copy-paste, unify event
  payload, and to eliminate need of accessing component instance for
  patchNum. Tests updated as well.
- Refactored gr-diff.js UI data model update using gr-diff-builder.html
  utility methods to make code more readable.
- Added support for creating ranged comments to gr-diff.js.
- gr-selection-action-box now reacts to click and tap to create a 
  comment.

Change-Id: I01480a4c6f460774a8b2826915702800b3f81d25
2016-06-13 22:44:47 +00:00

354 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = 400;
var UPDATE_DEBOUNCE_INTERVAL = 500;
Polymer({
is: 'gr-diff-comment',
/**
* Fired when the Reply action is triggered.
*
* @event reply
*/
/**
* Fired when the Done action is triggered.
*
* @event done
*/
/**
* Fired when this comment is discarded.
*
* @event comment-discard
*/
/**
* Fired when this comment is saved.
*
* @event comment-save
*/
/**
* Fired when this comment is updated.
*
* @event comment-update
*/
/**
* @event comment-mouse-over
*/
/**
* @event comment-mouse-out
*/
properties: {
changeNum: String,
comment: {
type: Object,
notify: true,
observer: '_commentChanged',
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
draft: {
type: Boolean,
value: false,
observer: '_draftChanged',
},
editing: {
type: Boolean,
value: false,
observer: '_editingChanged',
},
patchNum: String,
showActions: Boolean,
projectConfig: Object,
_xhrPromise: Object, // Used for testing.
_messageText: {
type: String,
value: '',
observer: '_messageTextChanged',
},
},
observers: [
'_commentMessageChanged(comment.message)',
'_loadLocalDraft(changeNum, patchNum, comment)',
],
detached: function() {
this.cancelDebouncer('fire-update');
},
save: function() {
this.comment.message = this._messageText;
this.disabled = true;
this.$.storage.eraseDraftComment({
changeNum: this.changeNum,
patchNum: this.patchNum,
path: this.comment.path,
line: this.comment.line,
});
this._xhrPromise = this._saveDraft(this.comment).then(function(response) {
this.disabled = false;
if (!response.ok) { return response; }
return this.$.restAPI.getResponseObject(response).then(function(obj) {
var comment = obj;
comment.__draft = true;
// Maintain the ephemeral draft ID for identification by other
// elements.
if (this.comment.__draftID) {
comment.__draftID = this.comment.__draftID;
}
this.comment = comment;
this.editing = false;
this._fireSave();
return obj;
}.bind(this));
}.bind(this)).catch(function(err) {
this.disabled = false;
throw err;
}.bind(this));
},
_commentChanged: function(comment) {
this.editing = !!comment.__editing;
if (this.editing) { // It's a new draft/reply, notify.
this._fireUpdate();
}
},
_getEventPayload: function(opt_mixin) {
var payload = {
comment: this.comment,
patchNum: this.patchNum,
};
for (var k in opt_mixin) {
payload[k] = opt_mixin[k];
}
return payload;
},
_fireSave: function() {
this.fire('comment-save', this._getEventPayload());
},
_fireUpdate: function() {
this.debounce('fire-update', function() {
this.fire('comment-update', this._getEventPayload());
}, UPDATE_DEBOUNCE_INTERVAL);
},
_draftChanged: function(draft) {
this.$.container.classList.toggle('draft', draft);
},
_editingChanged: function(editing, previousValue) {
this.$.container.classList.toggle('editing', editing);
if (editing) {
var textarea = this.$.editTextarea.textarea;
// Put the cursor at the end always.
textarea.selectionStart = textarea.value.length;
textarea.selectionEnd = textarea.selectionStart;
this.async(function() {
textarea.focus();
}.bind(this));
}
if (this.comment && this.comment.id) {
this.$$('.cancel').hidden = !editing;
}
if (this.comment) {
this.comment.__editing = this.editing;
}
if (editing != !!previousValue) {
// To prevent event firing on comment creation.
this._fireUpdate();
}
},
_computeLinkToComment: function(comment) {
return '#' + comment.line;
},
_computeSaveDisabled: function(draft) {
return draft == null || draft.trim() == '';
},
_handleTextareaKeydown: function(e) {
if (e.keyCode == 27) { // 'esc'
this._handleCancel(e);
}
},
_commentMessageChanged: function(message) {
this._messageText = message || '';
},
_messageTextChanged: function(newValue, oldValue) {
if (!this.comment || (this.comment && this.comment.id)) { return; }
this.debounce('store', function() {
var message = this._messageText;
var commentLocation = {
changeNum: this.changeNum,
patchNum: this.patchNum,
path: this.comment.path,
line: this.comment.line,
};
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);
}
this._fireUpdate();
}, STORAGE_DEBOUNCE_INTERVAL);
},
_handleLinkTap: function(e) {
e.preventDefault();
var hash = this._computeLinkToComment(this.comment);
// 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 dont trigger a nav event. Otherwise it will
// reload the page.
page.show(window.location.pathname + hash, null, false);
},
_handleReply: function(e) {
this._preventDefaultAndBlur(e);
this.fire('reply', this._getEventPayload(), {bubbles: false});
},
_handleQuote: function(e) {
this._preventDefaultAndBlur(e);
this.fire(
'reply', this._getEventPayload({quote: true}), {bubbles: false});
},
_handleDone: function(e) {
this._preventDefaultAndBlur(e);
this.fire('done', this._getEventPayload(), {bubbles: false});
},
_handleEdit: function(e) {
this._preventDefaultAndBlur(e);
this._messageText = this.comment.message;
this.editing = true;
},
_handleSave: function(e) {
this._preventDefaultAndBlur(e);
this.save();
},
_handleCancel: function(e) {
this._preventDefaultAndBlur(e);
if (this.comment.message == null || this.comment.message.length == 0) {
this._fireDiscard();
return;
}
this._messageText = this.comment.message;
this.editing = false;
},
_fireDiscard: function() {
this.fire('comment-discard', this._getEventPayload());
},
_handleDiscard: function(e) {
this._preventDefaultAndBlur(e);
if (!this.comment.__draft) {
throw Error('Cannot discard a non-draft comment.');
}
this.editing = false;
this.disabled = true;
if (!this.comment.id) {
this.disabled = false;
this._fireDiscard();
return;
}
this._xhrPromise = this._deleteDraft(this.comment).then(
function(response) {
this.disabled = false;
if (!response.ok) { return response; }
this._fireDiscard();
}.bind(this)).catch(function(err) {
this.disabled = false;
throw err;
}.bind(this));
},
_preventDefaultAndBlur: function(e) {
e.preventDefault();
Polymer.dom(e).rootTarget.blur();
},
_saveDraft: function(draft) {
return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft);
},
_deleteDraft: function(draft) {
return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
draft);
},
_loadLocalDraft: function(changeNum, patchNum, comment) {
// Only apply local drafts to comments that haven't been saved
// remotely, and haven't been given a default message already.
if (!comment || comment.id || comment.message) {
return;
}
var draft = this.$.storage.getDraftComment({
changeNum: changeNum,
patchNum: patchNum,
path: comment.path,
line: comment.line,
});
if (draft) {
this.set('comment.message', draft.message);
}
},
_handleMouseEnter: function(e) {
this.fire('comment-mouse-over', this._getEventPayload());
},
_handleMouseLeave: function(e) {
this.fire('comment-mouse-out', this._getEventPayload());
},
});
})();