[bugzilla-gnome-org-extensions] Add saving of drafts to HTML5 localStorage



commit 38a04c8465c4975943568e69b6adfde7053f8968
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Sat Sep 12 18:31:28 2009 -0400

    Add saving of drafts to HTML5 localStorage
    
    Add an abstract ReviewStorage "interface" for saving information about
    in-progress reviews and an implementation of in terms of the
    "Web Storage" spec as implemented in very recent browsers.
    
    Hook that up in the web interface to auto-save on changes (with a
    "Saving Draft..." indicator that is at least useful while debugging
    the facility.)
    
    The "Save" button is retitled to "Publish" to reduce confusion between
    saving drafts and publishing to the bug.

 Makefile            |    1 +
 js/reviewStorage.js |  104 +++++++++++++++++++++++++++++++++++++++++++++++++++
 js/splinter.js      |   71 ++++++++++++++++++++++++++++++++---
 web/index.html      |    3 +-
 web/splinter.css    |    9 ++++
 5 files changed, 181 insertions(+), 7 deletions(-)
---
diff --git a/Makefile b/Makefile
index 19f81ca..667d2a9 100644
--- a/Makefile
+++ b/Makefile
@@ -11,6 +11,7 @@ JS_FILES =                                    \
        js/bug.js                               \
        js/patch.js                             \
        js/review.js                            \
+       js/reviewStorage.js                     \
        js/splinter.js                          \
        js/testUtils.js                         \
        js/utils.js
diff --git a/js/reviewStorage.js b/js/reviewStorage.js
new file mode 100644
index 0000000..cfc146c
--- /dev/null
+++ b/js/reviewStorage.js
@@ -0,0 +1,104 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+include('Review');
+
+/* The ReviewStorage 'interface' has the following methods:
+ *
+ *  listReviews()
+ *    Returns an array of objects with the following properties:
+ *      bugId
+ *      bugShortDesc
+ *      attachmentId
+ *      attachmentDescription
+ *      creationTime
+ *      modificationTime
+ *      isDraft
+ *  loadDraft(bug, attachment, patch)
+ *  saveDraft(bug, attachment, review)
+ *  draftPublished(bug, attachment)
+ */
+
+function LocalReviewStorage() {
+    this._init();
+}
+
+LocalReviewStorage.available = function() {
+    return 'localStorage' in window;
+};
+
+LocalReviewStorage.prototype = {
+    _init : function() {
+        var reviewInfosText = localStorage.splinterReviews;
+        if (reviewInfosText == null)
+            this._reviewInfos = [];
+        else
+            this._reviewInfos = JSON.parse(reviewInfosText);
+    },
+
+    listReviews : function() {
+        return this._reviewInfos;
+    },
+
+    _reviewPropertyName : function(bug, attachment) {
+        return 'splinterReview_' + bug.id + '_' + attachment.id;
+    },
+
+    loadDraft : function(bug, attachment, patch) {
+        var propertyName = this._reviewPropertyName(bug, attachment);
+        var reviewText = localStorage[propertyName];
+        if (reviewText != null) {
+            var review = new Review.Review(patch);
+            review.parse(reviewText);
+            return review;
+        } else {
+            return null;
+        }
+    },
+
+    _findReview : function(bug, attachment) {
+        for (var i = 0 ; i < this._reviewInfos.length; i++)
+            if (this._reviewInfos[i].bugId == bug.id && this._reviewInfos[i].attachmentId == attachment.id)
+                return i;
+
+        return -1;
+    },
+
+    _updateOrCreateReviewInfo : function(bug, attachment, props) {
+        var reviewIndex = this._findReview(bug, attachment);
+        var reviewInfo;
+
+        var nowTime = Date.now();
+        if (reviewIndex >= 0) {
+            reviewInfo = this._reviewInfos[reviewIndex];
+            this._reviewInfos.splice(reviewIndex, 1);
+        } else {
+            reviewInfo = {
+                bugId: bug.id,
+                bugShortDesc: bug.shortDesc,
+                attachmentId: attachment.id,
+                attachmentDescription: attachment.description,
+                creationTime: nowTime
+            };
+        }
+
+        reviewInfo.modificationTime = nowTime;
+        for (var prop in props)
+            reviewInfo[prop] = props[prop];
+
+        this._reviewInfos.push(reviewInfo);
+        localStorage.splinterReviews = JSON.stringify(this._reviewInfos);
+    },
+
+    saveDraft : function(bug, attachment, review) {
+        var propertyName = this._reviewPropertyName(bug, attachment);
+
+        this._updateOrCreateReviewInfo(bug, attachment, { isDraft: true });
+        localStorage[propertyName] = "" + review;
+    },
+
+    draftPublished : function(bug, attachment) {
+        var propertyName = this._reviewPropertyName(bug, attachment);
+
+        this._updateOrCreateReviewInfo(bug, attachment, { isDraft: false });
+        delete localStorage[propertyName];
+    }
+};
diff --git a/js/splinter.js b/js/splinter.js
index 5313faf..ed50b44 100644
--- a/js/splinter.js
+++ b/js/splinter.js
@@ -2,13 +2,19 @@
 include('Bug');
 include('Patch');
 include('Review');
+include('ReviewStorage');
 
+var reviewStorage;
 var attachmentId;
 var theBug;
 var theAttachment;
 var thePatch;
 var theReview;
 
+var saveDraftTimeoutId;
+var saveDraftNoticeTimeoutId;
+var savingDraft = false;
+
 const ADD_COMMENT_SUCCESS = /<title>\s*Bug[\S\s]*processed\s*<\/title>/;
 const UPDATE_ATTACHMENT_SUCCESS = /<title>\s*Changes\s+Submitted/;
 
@@ -80,7 +86,7 @@ function addComment(bug, comment, success, failure) {
            });
 }
 
-function saveReview() {
+function publishReview() {
     theReview.setIntro($("#myComment").val());
 
     var comment = "Review of attachment " + attachmentId + ":\n\n" + theReview;
@@ -92,6 +98,8 @@ function saveReview() {
 
     function success() {
         alert("Succesfully published the review.");
+        if (reviewStorage)
+            reviewStorage.draftPublished(theBug, theAttachment);
     }
 
     addComment(theBug, comment,
@@ -110,6 +118,39 @@ function saveReview() {
                });
 }
 
+function hideSaveDraftNotice() {
+    saveDraftNoticeTimeoutId = null;
+    $("#saveDraftNotice").hide();
+}
+
+function saveDraft() {
+    theReview.setIntro($("#myComment").val());
+
+    if (reviewStorage == null)
+        return;
+
+    clearTimeout(saveDraftTimeoutId);
+    saveDraftTimeoutId = null;
+
+    savingDraft = true;
+    $("#saveDraftNotice")
+        .text("Saving Draft...")
+        .show();
+    clearTimeout(saveDraftNoticeTimeoutId);
+    setTimeout(hideSaveDraftNotice, 3000);
+
+    reviewStorage.saveDraft(theBug, theAttachment, theReview);
+
+    savingDraft = false;
+    $("#saveDraftNotice")
+        .text("Saved Draft");
+}
+
+function queueSaveDraft() {
+    if (saveDraftTimeoutId == null)
+        saveDraftTimeoutId = setTimeout(saveDraft, 10000);
+}
+
 function getQueryParams() {
     var query = window.location.search.substring(1);
     if (query == null || query == "")
@@ -162,9 +203,15 @@ function getSeparatorClass(type) {
     return null;
 }
 
-function addCommentDisplay(row, comment, commentorIndex) {
+function addCommentDisplay(row, comment) {
     var commentArea = ensureCommentArea(row);
 
+    var commentorIndex;
+    if (comment.file.review == theReview)
+        commentorIndex = 0;
+    else
+        commentorIndex = 1;
+
     var separatorClass = getSeparatorClass(comment.type);
     if (separatorClass)
         $("<div></div>")
@@ -198,7 +245,7 @@ function saveComment(row, file, location, type) {
         else
             comment = reviewFile.addComment(location, type, value);
 
-        addCommentDisplay(row, comment, 0);
+        addCommentDisplay(row, comment);
     } else {
         if (comment)
             comment.remove();
@@ -209,6 +256,8 @@ function saveComment(row, file, location, type) {
     } else {
         $(commentArea).find(".comment-editor").remove();
     }
+
+    saveDraft();
 }
 
 function insertCommentEditor(clickRow, clickType) {
@@ -334,7 +383,7 @@ function addPatchFile(file) {
 
                          if (line.reviewComments != null)
                              for (var k = 0; k < line.reviewComments.length; k++)
-                                 addCommentDisplay(tr, line.reviewComments[k], 1);
+                                 addCommentDisplay(tr, line.reviewComments[k]);
                      });
     }
 }
@@ -342,7 +391,10 @@ function addPatchFile(file) {
 var REVIEW_RE = /^\s*review\s+of\s+attachment\s+(\d+)\s*:\s*/i;
 
 function start(xml) {
-    theReview = new Review.Review(thePatch);
+    if (reviewStorage)
+        theReview = reviewStorage.loadDraft(theBug, theAttachment, thePatch);
+    if (!theReview)
+        theReview = new Review.Review(thePatch);
 
     $("#loading").hide();
     $("#headers").show();
@@ -367,6 +419,10 @@ function start(xml) {
     else
         $("#patchIntro").hide();
 
+    $("#myComment")
+        .val(theReview.intro)
+        .keypress(queueSaveDraft);
+
     $("#attachmentId").text(theAttachment.id);
     $("#attachmentDesc").text(theAttachment.description);
     $("#attachmentDate").text(Utils.formatDate(theAttachment.date));
@@ -400,7 +456,7 @@ function start(xml) {
     for (i = 0; i < thePatch.files.length; i++)
         addPatchFile(thePatch.files[i]);
 
-    $("#saveButton").click(publishReview);
+    $("#publishButton").click(publishReview);
 }
 
 function gotBug(xml) {
@@ -492,6 +548,9 @@ function init() {
     var params = getQueryParams();
     var bugId;
 
+    if (ReviewStorage.LocalReviewStorage.available())
+        reviewStorage = new ReviewStorage.LocalReviewStorage();
+
     if (params.bug)
         bugId = isDigits(params.bug) ? parseInt(params.bug) : NaN;
 
diff --git a/web/index.html b/web/index.html
index ea8693b..193d3bf 100644
--- a/web/index.html
+++ b/web/index.html
@@ -52,11 +52,12 @@
          <span id="attachmentStatusSpan">Patch Status:
            <select id="attachmentStatus"> </select>
          </span>
-         <input id="saveButton" type="button" value="Save"></input>
+         <input id="publishButton" type="button" value="Publish"></input>
          <input id="cancelButton" type="button" value="Cancel"></input>
        </div>
        <div id="buttonSeparator"></div>
       </div>
     <div id="files" style="display: none;"></div>
   </body>
+  <div id="saveDraftNotice" style="display: none;"></div>
 </html>
diff --git a/web/splinter.css b/web/splinter.css
index 1ae9185..60f0f39 100644
--- a/web/splinter.css
+++ b/web/splinter.css
@@ -166,3 +166,12 @@ body {
 .comment-separator-added {
     clear: right;
 }
+
+#saveDraftNotice {
+    border: 1px solid black;
+    padding: 0.5em;
+    background: #ffccaa;
+    position: fixed;
+    bottom: 0px;
+    right: 0px;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]