MozReview: Add autosave feature, update layout for dropdown dialog (Bug 1246617). r?glob draft
authorDavid Walsh <dwalsh@mozilla.com>
Mon, 21 Nov 2016 15:53:11 -0600
changeset 234 204b13534320e65e1a9b982e399224a5bcb5ee25
parent 233 bb7398d02b086984a5678a5d5628bedf43232408
push idunknown
push userunknown
push dateunknown
reviewersglob
bugs1246617
MozReview: Add autosave feature, update layout for dropdown dialog (Bug 1246617). r?glob MozReview-Commit-ID: FQ5L5i6iEtY
reviewboard/reviewboard/static/rb/css/common_mozreview.less
reviewboard/reviewboard/static/rb/js/ui/views/textEditorView_mozreview.js
reviewboard/reviewboard/static/rb/js/utils/apiUtils_mozreview.js
reviewboard/reviewboard/static/rb/js/views/draftReviewBannerView_mozreview.js
reviewboard/reviewboard/static/rb/js/views/reviewDialogView_mozreview.js
--- 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) {