From 7d06754a2f36b4b182cf3f62e9d9f82ed4be21d2 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 10 Feb 2012 03:17:25 +0000 Subject: [PATCH] [build][xs]: build recline.js. --- recline.js | 272 +++++++++++++++++++++++------------------------------ 1 file changed, 118 insertions(+), 154 deletions(-) diff --git a/recline.js b/recline.js index 596ebcf8..51a91b0e 100644 --- a/recline.js +++ b/recline.js @@ -11,10 +11,38 @@ this.recline.Model = this.recline.Model || {}; (function($, my) { my.backends = {}; + // ## Backbone.sync + // + // Override Backbone.sync to hand off to sync function in relevant backend Backbone.sync = function(method, model, options) { return my.backends[model.backendConfig.type].sync(method, model, options); } + // ## wrapInTimeout + // + // Crude way to catch backend errors + // Many of backends use JSONP and so will not get error messages and this is + // a crude way to catch those errors. + function wrapInTimeout(ourFunction) { + var dfd = $.Deferred(); + var timeout = 5000; + var timer = setTimeout(function() { + dfd.reject({ + message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds' + }); + }, timeout); + ourFunction.done(function(arguments) { + clearTimeout(timer); + dfd.resolve(arguments); + }) + .fail(function(arguments) { + clearTimeout(timer); + dfd.reject(arguments); + }) + ; + return dfd.promise(); + } + // ## BackendMemory - uses in-memory data // // To use you should: @@ -122,7 +150,7 @@ this.recline.Model = this.recline.Model || {}; jsonp: '_callback' }); var dfd = $.Deferred(); - jqxhr.then(function(schema) { + wrapInTimeout(jqxhr).done(function(schema) { headers = _.map(schema.data, function(item) { return item.name; }); @@ -131,6 +159,9 @@ this.recline.Model = this.recline.Model || {}; }); dataset.docCount = schema.count; dfd.resolve(dataset, jqxhr); + }) + .fail(function(arguments) { + dfd.reject(arguments); }); return dfd.promise(); } @@ -150,7 +181,7 @@ this.recline.Model = this.recline.Model || {}; cache: true }); var dfd = $.Deferred(); - jqxhr.then(function(results) { + jqxhr.done(function(results) { dfd.resolve(results.data); }); return dfd.promise(); @@ -196,11 +227,14 @@ this.recline.Model = this.recline.Model || {}; , dataType: 'jsonp' }); var dfd = $.Deferred(); - jqxhr.then(function(results) { + wrapInTimeout(jqxhr).done(function(results) { dataset.set({ headers: results.fields }); dfd.resolve(dataset, jqxhr); + }) + .fail(function(arguments) { + dfd.reject(arguments); }); return dfd.promise(); } @@ -221,7 +255,7 @@ this.recline.Model = this.recline.Model || {}; , dataType: 'jsonp' }); var dfd = $.Deferred(); - jqxhr.then(function(results) { + jqxhr.done(function(results) { var _out = _.map(results.data, function(row) { var tmp = {}; _.each(results.fields, function(key, idx) { @@ -453,111 +487,10 @@ var costco = function() { }; } - function updateDocs(editFunc) { - var dfd = $.Deferred(); - util.notify("Download entire database into Recline. This could take a while...", {persist: true, loader: true}); - couch.request({url: app.baseURL + "api/json"}).then(function(docs) { - util.notify("Updating " + docs.docs.length + " documents. This could take a while...", {persist: true, loader: true}); - var toUpdate = costco.mapDocs(docs.docs, editFunc).edited; - costco.uploadDocs(toUpdate).then( - function(updatedDocs) { - util.notify(updatedDocs.length + " documents updated successfully"); - recline.initializeTable(app.offset); - dfd.resolve(updatedDocs); - }, - function(err) { - util.notify("Errorz! " + err); - dfd.reject(err); - } - ); - }); - return dfd.promise(); - } - - function updateDoc(doc) { - return couch.request({type: "PUT", url: app.baseURL + "api/" + doc._id, data: JSON.stringify(doc)}) - } - - function uploadDocs(docs) { - var dfd = $.Deferred(); - if(!docs.length) dfd.resolve("Failed: No docs specified"); - couch.request({url: app.baseURL + "api/_bulk_docs", type: "POST", data: JSON.stringify({docs: docs})}) - .then( - function(resp) {ensureCommit().then(function() { - var error = couch.responseError(resp); - if (error) { - dfd.reject(error); - } else { - dfd.resolve(resp); - } - })}, - function(err) { dfd.reject(err.responseText) } - ); - return dfd.promise(); - } - - function ensureCommit() { - return couch.request({url: app.baseURL + "api/_ensure_full_commit", type:'POST', data: "''"}); - } - - function deleteColumn(name) { - var deleteFunc = function(doc) { - delete doc[name]; - return doc; - } - return updateDocs(deleteFunc); - } - - function uploadCSV() { - var file = $('#file')[0].files[0]; - if (file) { - var reader = new FileReader(); - reader.readAsText(file); - reader.onload = function(event) { - var payload = { - url: window.location.href + "/api/_bulk_docs", // todo more robust url composition - data: event.target.result - }; - var worker = new Worker('script/costco-csv-worker.js'); - worker.onmessage = function(event) { - var message = event.data; - if (message.done) { - var error = couch.responseError(JSON.parse(message.response)) - console.log('e',error) - if (error) { - app.emitter.emit(error, 'error'); - } else { - util.notify("Data uploaded successfully!"); - recline.initializeTable(app.offset); - } - util.hide('dialog'); - } else if (message.percent) { - if (message.percent === 100) { - util.notify("Waiting for CouchDB...", {persist: true, loader: true}) - } else { - util.notify("Uploading... " + message.percent + "%"); - } - } else { - util.notify(JSON.stringify(message)); - } - }; - worker.postMessage(payload); - }; - } else { - util.notify('File not selected. Please try again'); - } - }; - return { evalFunction: evalFunction, previewTransform: previewTransform, - mapDocs: mapDocs, - updateDocs: updateDocs, - updateDoc: updateDoc, - uploadDocs: uploadDocs, - deleteColumn: deleteColumn, - ensureCommit: ensureCommit, - uploadCSV: uploadCSV + mapDocs: mapDocs }; }(); // # Recline Backbone Models @@ -597,13 +530,12 @@ this.recline.Model = this.recline.Model || {}; // this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here. // This also illustrates the limitations of separating the Dataset and the Backend query: function(queryObj) { - this.queryState = queryObj || this.defaultQuery; - this.queryState = _.extend({size: 100, offset: 0}, this.queryState); - var self = this; var backend = my.backends[this.backendConfig.type]; + this.queryState = queryObj || this.defaultQuery; + this.queryState = _.extend({size: 100, offset: 0}, this.queryState); var dfd = $.Deferred(); - backend.query(this, this.queryState).then(function(rows) { + backend.query(this, this.queryState).done(function(rows) { var docs = _.map(rows, function(row) { var _doc = new my.Document(row); _doc.backendConfig = self.backendConfig; @@ -612,6 +544,9 @@ this.recline.Model = this.recline.Model || {}; }); self.currentDocuments.reset(docs); dfd.resolve(self.currentDocuments); + }) + .fail(function(arguments) { + dfd.reject(arguments); }); return dfd.promise(); }, @@ -789,30 +724,6 @@ var util = function() { // if (template in app.after) app.after[template](); } - function notify(message, options) { - if (!options) var options = {}; - var tmplData = _.extend({ - msg: message, - category: 'warning' - }, - options); - var _template = ' \ -
× \ -

{{msg}} \ - {{#loader}} \ - \ - {{/loader}} \ -

\ -
'; - var _templated = $.mustache(_template, tmplData); - _templated = $(_templated).appendTo($('.data-explorer .alert-messages')); - if (!options.persist) { - setTimeout(function() { - $(_templated).remove(); - }, 3000); - } - } - return { registerEmitter: registerEmitter, listenFor: listenFor, @@ -820,7 +731,6 @@ var util = function() { hide: hide, position: position, render: render, - notify: notify, observeExit: observeExit }; }(); @@ -849,6 +759,47 @@ function parseQueryString(q) { return urlParams; } +// ## notify +// +// Create a notification (a div.alert-message in div.alert-messsages) using provide messages and options. Options are: +// +// * category: warning (default), success, error +// * persist: if true alert is persistent, o/w hidden after 3s (default = false) +// * loader: if true show loading spinner +my.notify = function(message, options) { + if (!options) var options = {}; + var tmplData = _.extend({ + msg: message, + category: 'warning' + }, + options); + var _template = ' \ +
× \ +

{{msg}} \ + {{#loader}} \ + \ + {{/loader}} \ +

\ +
'; + var _templated = $.mustache(_template, tmplData); + _templated = $(_templated).appendTo($('.data-explorer .alert-messages')); + if (!options.persist) { + setTimeout(function() { + $(_templated).fadeOut(1000, function() { + $(this).remove(); + }); + }, 1000); + } +} + +// ## clearNotifications +// +// Clear all existing notifications +my.clearNotifications = function() { + var $notifications = $('.data-explorer .alert-message'); + $notifications.remove(); +} + // The primary view for the entire application. // // It should be initialized with a recline.Model.Dataset object and an existing @@ -927,23 +878,36 @@ my.DataExplorer = Backbone.View.extend({ // retrieve basic data like headers etc // note this.model and dataset returned are the same - this.model.fetch().then(function(dataset) { - self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - // initialize of dataTable calls render - var queryObj = { - size: self.config.displayCount - }; - self.model.query(queryObj); - }); + this.model.fetch() + .done(function(dataset) { + self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); + self.query(); + }) + .fail(function(error) { + my.notify(error.message, {category: 'error', persist: true}); + }); }, - onDisplayCountUpdate: function(e) { - e.preventDefault(); + query: function() { this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val()); var queryObj = { size: this.config.displayCount }; - this.model.query(queryObj); + my.notify('Loading data', {loader: true}); + this.model.query(queryObj) + .done(function() { + my.clearNotifications(); + my.notify('Data loaded', {category: 'success'}); + }) + .fail(function(error) { + my.clearNotifications(); + my.notify(error.message, {category: 'error', persist: true}); + }); + }, + + onDisplayCountUpdate: function(e) { + e.preventDefault(); + this.query(); }, setReadOnly: function() { @@ -1086,10 +1050,10 @@ my.DataTable = Backbone.View.extend({ }); doc.destroy().then(function() { self.model.currentDocuments.remove(doc); - util.notify("Row deleted successfully"); + my.notify("Row deleted successfully"); }) .fail(function(err) { - util.notify("Errorz! " + err) + my.notify("Errorz! " + err) }) } } @@ -1268,12 +1232,12 @@ my.DataTableRow = Backbone.View.extend({ var newData = {}; newData[header] = newValue; this.model.set(newData); - util.notify("Updating row...", {loader: true}); + my.notify("Updating row...", {loader: true}); this.model.save().then(function(response) { - util.notify("Row updated successfully", {category: 'success'}); + my.notify("Row updated successfully", {category: 'success'}); }) .fail(function() { - util.notify('Error saving row', { + my.notify('Error saving row', { category: 'error', persist: true }); @@ -1371,11 +1335,11 @@ my.ColumnTransform = Backbone.View.extend({ var funcText = this.el.find('.expression-preview-code').val(); var editFunc = costco.evalFunction(funcText); if (editFunc.errorMessage) { - util.notify("Error with function! " + editFunc.errorMessage); + my.notify("Error with function! " + editFunc.errorMessage); return; } util.hide('dialog'); - util.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true}); + my.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true}); var docs = self.model.currentDocuments.map(function(doc) { return doc.toJSON(); }); @@ -1385,7 +1349,7 @@ my.ColumnTransform = Backbone.View.extend({ function onCompletedUpdate() { totalToUpdate += -1; if (totalToUpdate === 0) { - util.notify(toUpdate.length + " documents updated successfully"); + my.notify(toUpdate.length + " documents updated successfully"); alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)'); self.remove(); }