MozReview: Add autosave feature, update layout for dropdown dialog (
Bug 1246617). r?glob
MozReview-Commit-ID: FQ5L5i6iEtY
--- a/reviewboard/reviewboard/static/rb/css/common_mozreview.less
+++ b/reviewboard/reviewboard/static/rb/css/common_mozreview.less
@@ -107,16 +107,20 @@
label {
color: black;
font-size: 8pt !important;
font-weight: normal;
}
}
}
+#review-form-comments .inline-editor-form .enable-markdown {
+ margin-left: 0;
+}
+
.loading-indicator {
display: inline;
}
.text-editor {
margin: @textarea-editor-margin;
textarea, .CodeMirror {
--- a/reviewboard/reviewboard/static/rb/js/ui/views/textEditorView_mozreview.js
+++ b/reviewboard/reviewboard/static/rb/js/ui/views/textEditorView_mozreview.js
@@ -1,10 +1,14 @@
(function() {
+/*
+ Adding a method to inlineEditor instances to get the root form element
+*/
+$.ui.inlineEditor.prototype.form = function() { return this._form; };
var CodeMirrorWrapper,
TextAreaWrapper;
/*
* Wraps CodeMirror, providing a standard interface for TextEditorView's usage.
*/
@@ -102,17 +106,19 @@ CodeMirrorWrapper = Backbone.View.extend
this._codeMirror.refresh();
},
/*
* Focuses the editor.
*/
focus: function() {
this._codeMirror.focus();
- }
+ // Set the cursor at the end of existing content
+ this._codeMirror.setCursor(this._codeMirror.lineCount(), 0);
+ },
});
/*
* Wraps <textarea>, providing a standard interface for TextEditorView's usage.
*/
TextAreaWrapper = Backbone.View.extend({
tagName: 'textarea',
@@ -473,34 +479,45 @@ RB.TextEditorView = Backbone.View.extend
* Hides the actual editor wrapper.
*
* The last value from the editor will be stored for later retrieval.
*/
_hideEditor: function() {
if (this._editor) {
this._value = this._editor.getText();
this._richTextDirty = false;
+
this._editor.remove();
this._editor = null;
this.$el.empty();
}
+ },
+
+ /*
+ * Updates the stored value of the editor without closing
+ */
+ _updateStoredValue: function() {
+ if (this._editor) {
+ this._value = this._editor.getText();
+ this._richTextDirty = false;
+ }
}
}, {
/*
* Returns options used to display a TextEditorView in an inlineEditor.
*
* This will return an options dictionary that can be used with an
* inlineEditor. The inlineEditor will make use of the TextEditorView
* instead of a textarea.
*
* This can take options for the TextEditorView to change the default
* behavior.
*/
- getInlineEditorOptions: function(options) {
+ getInlineEditorOptions: function(options, isProgressSave) {
var textEditor;
return {
matchHeight: false,
multiline: true,
createMultilineField: function(editor) {
var $editor = editor.element,
@@ -525,16 +542,17 @@ RB.TextEditorView = Backbone.View.extend
})
.appendTo($span);
textEditor.bindRichTextCheckbox($checkbox);
$span.append($('<label/>')
.attr('for', $checkbox[0].id)
.text(gettext('Enable Markdown')));
+ $buttons = $buttons || $('<div/>').addClass('buttons').appendTo($editor.inlineEditor('form'));
$buttons.append($span);
$markdownRef = $('<a/>')
.addClass('markdown-info')
.attr({
href: MANUAL_URL + 'users/markdown/',
target: '_blank'
})
@@ -550,17 +568,22 @@ RB.TextEditorView = Backbone.View.extend
});
$editor.on('cancel', function() {
textEditor._hideEditor();
textEditor.setRichText(origRichText);
});
$editor.on('complete', function() {
- textEditor._hideEditor();
+ if (isProgressSave) {
+ textEditor._updateStoredValue();
+ }
+ else {
+ textEditor._hideEditor();
+ }
});
textEditor.$el.data('text-editor', textEditor);
return textEditor.$el;
},
setFieldValue: function(editor, value) {
--- a/reviewboard/reviewboard/static/rb/js/utils/apiUtils_mozreview.js
+++ b/reviewboard/reviewboard/static/rb/js/utils/apiUtils_mozreview.js
@@ -11,17 +11,17 @@
*/
RB.setActivityIndicator = function(status, options) {
var $activityIndicator = $("#activity-indicator"),
$indicatorText = $activityIndicator.children('.indicator-text');
if (status) {
if (RB.ajaxOptions.enableIndicator && !options.noActivityIndicator) {
$indicatorText
- .text((options.type || options.type === "GET")
+ .text((options.type && options.type === "GET")
? gettext("Loading...") : gettext("Saving..."));
$activityIndicator
.removeClass("error")
.show();
}
} else if (RB.ajaxOptions.enableIndicator &&
!options.noActivityIndicator &&
--- a/reviewboard/reviewboard/static/rb/js/views/draftReviewBannerView_mozreview.js
+++ b/reviewboard/reviewboard/static/rb/js/views/draftReviewBannerView_mozreview.js
@@ -195,37 +195,47 @@ RB.DraftReviewBannerView = Backbone.View
*/
_onHideDialog: function() {
$(window).off('resize', this._setDialogHeight);
this._editButton.value = this._editLabel;
this._$dialogContainer.removeClass('show-dialog');
// Save the partial review contents without publishing
- if(this._RDVInstance) {
+ if (this._RDVInstance) {
this._RDVInstance._saveReview(false);
+ if (this._saveInterval) {
+ clearInterval(this._saveInterval);
+ }
}
this._RDVInstance.close();
},
/*
* MozReview: Shows dropdown and creates RDV instance
*/
_onShowDialog: function() {
$(window).on('resize', this._setDialogHeight); /* ToDo: Implement debouncing */
this._setDialogHeight();
this._editButton.value = this._editButton.getAttribute('data-alt-value');
this._$dialogContainer.addClass('show-dialog');
- this._RDVInstance = RB.ReviewDialogView.create({
+ var _RDVInstance = this._RDVInstance = RB.ReviewDialogView.create({
review: this.model,
reviewRequestEditor: this.options.reviewRequestEditor
});
+
+ // Review comment will auto-save every 10 seconds
+ this._saveInterval = setInterval(function() {
+ if(_RDVInstance && _RDVInstance._bodyTopView.needsSave()) {
+ _RDVInstance._saveReview(false, true);
+ }
+ }, 10000);
},
/*
* Handler for the Publish button.
*
* Publishes the review.
*/
_onPublishClicked: function() {
--- a/reviewboard/reviewboard/static/rb/js/views/reviewDialogView_mozreview.js
+++ b/reviewboard/reviewboard/static/rb/js/views/reviewDialogView_mozreview.js
@@ -1,11 +1,10 @@
(function() {
-
var BaseCommentView,
DiffCommentView,
FileAttachmentCommentView,
ScreenshotCommentView,
HeaderFooterCommentView;
function _getRawValueFieldsName() {
@@ -190,17 +189,16 @@ BaseCommentView = Backbone.View.extend({
return $(this.thumbnailTemplate(this.model.attributes));
},
/*
* Renders the text for this comment.
*/
renderText: function() {
var reviewRequest = this.model.get('parentObject').get('parentObject');
-
if (this.$editor) {
RB.formatText(this.$editor, {
newText: this.model.get('text'),
richText: this.model.get('richText'),
isHTMLEncoded: true,
bugTrackerURL: reviewRequest.get('bugTrackerURL')
});
}
@@ -412,23 +410,25 @@ HeaderFooterCommentView = Backbone.View.
.timesince()
.end();
this.$editor = this.$('pre.reviewtext')
.inlineEditor(_.extend({
cls: 'inline-comment-editor',
editIconClass: 'rb-icon rb-icon-edit',
notifyUnchangedCompletion: true,
+ showButtons: false,
+ showEditIcon: false,
multiline: true
}, RB.TextEditorView.getInlineEditorOptions({
bindRichText: {
model: this.model,
attrName: this.richTextPropertyName
}
- })))
+ }, true)))
.on({
complete: _.bind(function(e, value) {
this.model.set(this.propertyName, value);
this.model.set(this.richTextPropertyName,
this.textEditor.richText);
this.model.save({
attrs: [this.propertyName, this.richTextPropertyName,
'forceTextType', 'includeTextTypes']
@@ -446,18 +446,27 @@ HeaderFooterCommentView = Backbone.View.
this._$editorContainer = this.$('.comment-text-field');
this._$linkContainer = this.$('.add-link-container');
this.listenTo(this.model, 'change:' + _getRawValueFieldsName(),
this._updateRawValue);
this._updateRawValue();
+
this.listenTo(this.model, 'saved', this.renderText);
this.renderText();
+
+ // Opening the editor first prevents HTML from displaying as
+ // plain text within the editor
+ this.openEditor();
+ // Updating this option will prevent ESC from closing the editor
+ this.$editor.inlineEditor('option', 'forceOpen', true);
+ // Ensures the editor gets focus upon open
+ this.$editor.inlineEditor('showEditor', true);
},
/*
* Renders the text for this comment.
*/
renderText: function() {
var reviewRequest = this.model.get('parentObject'),
text = this.model.get(this.propertyName);
@@ -562,17 +571,16 @@ RB.ReviewDialogView = Backbone.View.exte
*/
_$bannerContentsContainer: $('#review-banner-dialog-contents'),
/*
* Initializes the review dialog.
*/
initialize: function() {
var reviewRequest = this.model.get('parentObject');
-
this._$comments = null;
this._$dlg = null;
this._$buttons = null;
this._$spinner = null;
this._$shipIt = null;
this._commentViews = [];
this._hookViews = [];
@@ -711,41 +719,45 @@ RB.ReviewDialogView = Backbone.View.exte
hookView.render();
}, this);
this._bodyTopView = new HeaderFooterCommentView({
model: this.model,
el: this.$('.body-top'),
propertyName: 'bodyTop',
richTextPropertyName: 'bodyTopRichText',
- linkText: gettext('Add header'),
- editText: gettext('Edit header')
+ linkText: gettext('Comments'),
+ editText: gettext('Comments')
});
-
+ /* MozReview: We don't want the footer to display; additionally, it
+ causes focus issues with the header
this._bodyBottomView = new HeaderFooterCommentView({
model: this.model,
el: this.$('.body-bottom'),
propertyName: 'bodyBottom',
richTextPropertyName: 'bodyBottomRichText',
linkText: gettext('Add footer'),
editText: gettext('Edit footer')
});
+ */
/*
* Even if the model is already loaded, we may not have the right text
* type data. Force it to reload.
*/
this.model.set('loaded', false);
this.model.ready({
data: this._queryData,
ready: function() {
this._renderDialog();
this._bodyTopView.render();
+ /* MozReview: We don't want this displaying
this._bodyBottomView.render();
+ */
if (this.model.isNew() || this.model.get('bodyTop') === '') {
this._bodyTopView.openEditor();
}
if (this.model.isNew()) {
this._$spinner.remove();
this._$spinner = null;
@@ -801,17 +813,19 @@ RB.ReviewDialogView = Backbone.View.exte
*/
_handleEmptyReview: function() {
/*
* We only display the bottom textarea if we have comments or the user
* has previously set the bottom textarea -- we don't want the user to
* not be able to remove their text.
*/
if (this._commentViews.length === 0 && !this.model.get('bodyBottom')) {
+ /* MozReview: We don't want this displaying
this._bodyBottomView.$el.hide();
+ */
this._bodyTopView.setLinkText(gettext('Add text'));
}
},
/*
* Loads the comments from a collection.
*
* This is part of the load comments flow. The list of remaining
@@ -945,22 +959,22 @@ RB.ReviewDialogView = Backbone.View.exte
* Saves the review.
*
* First, this loops over all the comment editors and saves any which are
* still in the editing phase.
*
* If requested, this will also publish the review (saving with
* public=true).
*/
- _saveReview: function(publish) {
+ _saveReview: function(publish, isProgressSave) {
var madeChanges = false;
/* MozReview: These buttons are no longer used since no dialog
- this._$buttons.prop('disabled');
- */
+ this._$buttons.prop('disabled'); */
+
$.funcQueue('reviewForm').clear();
function maybeSave(view) {
if (view.needsSave()) {
$.funcQueue('reviewForm').add(function() {
madeChanges = true;
view.save({
@@ -968,17 +982,19 @@ RB.ReviewDialogView = Backbone.View.exte
$.funcQueue('reviewForm').next();
}
});
});
}
}
maybeSave(this._bodyTopView);
+ /* MozReview: We don't want this displaying
maybeSave(this._bodyBottomView);
+ */
_.each(this._commentViews, maybeSave);
$.funcQueue('reviewForm').add(function() {
var shipIt = this._$shipIt.prop('checked'),
saveFunc = publish ? this.model.publish : this.model.save;
if (this.model.get('public') === publish &&
this.model.get('shipIt') === shipIt) {
@@ -997,36 +1013,35 @@ RB.ReviewDialogView = Backbone.View.exte
},
error: function() {
console.log(arguments);
}
});
}
}, this);
- $.funcQueue('reviewForm').add(function() {
- var reviewBanner = RB.DraftReviewBannerView.instance;
+ if(!isProgressSave) {
+ $.funcQueue('reviewForm').add(function() {
+ var reviewBanner = RB.DraftReviewBannerView.instance;
- this.close();
+ this.close();
+
+ if (reviewBanner) {
+ if (publish) {
+ this._$dlg.remove();
- if (reviewBanner) {
- if (publish) {
- /* MozReview: Removes the container for the comments and
- review, thus preventing Firefox and Chrome.
- From saying "Do you really want to leave this screen?" */
- this._$dlg.remove();
-
- reviewBanner.hideAndReload();
- } else if (this.model.isNew() && !madeChanges) {
- reviewBanner.hide();
- } else {
- reviewBanner.show();
+ reviewBanner.hideAndReload();
+ } else if (this.model.isNew() && !madeChanges) {
+ reviewBanner.hide();
+ } else {
+ reviewBanner.show();
+ }
}
- }
- }, this);
+ }, this);
+ }
$.funcQueue('reviewForm').start();
},
/*
* Sets the text attributes on a model for forcing and including types.
*/
_setTextTypeAttributes: function(model) {