Merge branch 'master' into gh-pages

This commit is contained in:
Rufus Pollock 2012-02-10 03:17:53 +00:00
commit 7003338c9d
10 changed files with 1945 additions and 476 deletions

View File

@ -16,7 +16,7 @@ Live demo: http://okfnlabs.org/recline/demo/
* Bulk update/clean your data using an easy scripting UI
* Visualize your data
![screenshot](http://i.imgur.com/XDSRe.png)
![screenshot](http://farm8.staticflickr.com/7020/6847468031_0f474de5f7_b.jpg)
## Demo App

View File

@ -150,7 +150,7 @@
* Data Table Menus
*********************************************************/
a.column-header-menu {
a.column-header-menu, a.root-header-menu {
float: right;
display: block;
margin: 0 4px 0 0;
@ -160,7 +160,7 @@ a.column-header-menu {
background-repeat: no-repeat;
}
a.row-header-menu:hover {
a.row-header-menu:hover, a.root-header-menu:hover {
background-position: -17px 0px;
text-decoration: none;
}
@ -175,6 +175,10 @@ a.row-header-menu {
background-repeat: no-repeat;
}
.read-only a.row-header-menu {
display: none;
}
a.column-header-menu:hover {
background-position: -17px 0px;
text-decoration: none;
@ -511,14 +515,14 @@ td.expression-preview-value {
* Read-only mode
*********************************************************/
.read-only .data-table tr td:first-child,
/*.read-only .data-table tr td:first-child,
.read-only .data-table tr th:first-child
{
display: none;
}
*/
.read-only .column-header-menu,
.read-only .row-header-menu,
.read-only .write-op,
.read-only a.data-table-cell-edit
{
display: none;

View File

@ -84,15 +84,24 @@
<li><a href="demo/index.html">Demo</a></li>
</ul>
<h2 id="downloads">Downloads & Dependencies <small>(Right-click, and use 'Save As')</small></h2>
<p><a href="recline.js" class="btn">Development Version (v0.2)</a></p>
<h2 id="using">Using It</h2>
<p>Check out the the <a href="demo/">Demo</a> and view source. The
javascript you want for actual integration is in: <a
href="demo/js/app.js">app.js</a>.</p>
<h2 id="docs">Docs</h2>
<p>Want to see how to embed this in your own application. Check out the the
<a href="demo/">Demo</a> and view source.</p>
<ul>
<li><a href="docs/model.html">Models</a></li>
<li><a href="docs/backend.html">Backends</a></li>
<li><a href="docs/view.html">Views including the main Data Explorer</a></li>
</ul>
<h2 id="tests">Tests</h2>
<p><a href="test/index.html">Run the tests online</a>.</p>
<h2 id="history">History</h2>
<p>Max Ogden was developing Recline as the frontend data browser and editor
for his <a href="http://datacouch.com/">http://datacouch.com/</a> project.

1680
recline.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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:
@ -78,16 +106,19 @@ this.recline.Model = this.recline.Model || {};
alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model);
}
},
getDocuments: function(model, numRows, start) {
if (start === undefined) {
start = 0;
}
if (numRows === undefined) {
numRows = 10;
}
query: function(model, queryObj) {
var numRows = queryObj.size;
var start = queryObj.offset;
var dfd = $.Deferred();
rows = model.backendConfig.data.rows;
var results = rows.slice(start, start+numRows);
results = model.backendConfig.data.rows;
// not complete sorting!
_.each(queryObj.sort, function(item) {
results = _.sortBy(results, function(row) {
var _out = row[item[0]];
return (item[1] == 'asc') ? _out : -1*_out;
});
});
var results = results.slice(start, start+numRows);
dfd.resolve(results);
return dfd.promise();
}
@ -119,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;
});
@ -128,27 +159,29 @@ this.recline.Model = this.recline.Model || {};
});
dataset.docCount = schema.count;
dfd.resolve(dataset, jqxhr);
})
.fail(function(arguments) {
dfd.reject(arguments);
});
return dfd.promise();
}
}
},
getDocuments: function(model, numRows, start) {
if (start === undefined) {
start = 0;
}
if (numRows === undefined) {
numRows = 10;
}
query: function(model, queryObj) {
var base = model.backendConfig.url;
var data = {
_limit: queryObj.size
, _offset: queryObj.offset
};
var jqxhr = $.ajax({
url: base + '.json?_limit=' + numRows,
dataType: 'jsonp',
jsonp: '_callback',
cache: true
url: base + '.json',
data: data,
dataType: 'jsonp',
jsonp: '_callback',
cache: true
});
var dfd = $.Deferred();
jqxhr.then(function(results) {
jqxhr.done(function(results) {
dfd.resolve(results.data);
});
return dfd.promise();
@ -194,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();
}
@ -206,17 +242,11 @@ this.recline.Model = this.recline.Model || {};
alert('This backend only supports read operations');
}
},
getDocuments: function(dataset, numRows, start) {
if (start === undefined) {
start = 0;
}
if (numRows === undefined) {
numRows = 10;
}
query: function(dataset, queryObj) {
var base = my.backends['dataproxy'].get('dataproxy');
var data = {
url: dataset.backendConfig.url
, 'max-results': numRows
, 'max-results': queryObj.size
, type: dataset.backendConfig.format
};
var jqxhr = $.ajax({
@ -225,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) {
@ -260,7 +290,7 @@ this.recline.Model = this.recline.Model || {};
return dfd.promise(); }
},
getDocuments: function(dataset, start, numRows) {
query: function(dataset, queryObj) {
var dfd = $.Deferred();
var fields = dataset.get('headers');

View File

@ -60,110 +60,9 @@ 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
};
}();

View File

@ -15,6 +15,11 @@ this.recline.Model = this.recline.Model || {};
this.currentDocuments = new my.DocumentList();
this.docCount = null;
this.backend = null;
this.defaultQuery = {
size: 100
, offset: 0
};
// this.queryState = {};
},
// ### getDocuments
@ -29,11 +34,13 @@ 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
getDocuments: function(numRows, start) {
query: function(queryObj) {
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.getDocuments(this, numRows, start).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;
@ -42,6 +49,9 @@ this.recline.Model = this.recline.Model || {};
});
self.currentDocuments.reset(docs);
dfd.resolve(self.currentDocuments);
})
.fail(function(arguments) {
dfd.reject(arguments);
});
return dfd.promise();
},

View File

@ -2,10 +2,17 @@ var util = function() {
var templates = {
transformActions: '<li><a data-action="transform" class="menuAction" href="JavaScript:void(0);">Global transform...</a></li>'
, columnActions: ' \
<li><a data-action="bulkEdit" class="menuAction" href="JavaScript:void(0);">Transform...</a></li> \
<li><a data-action="deleteColumn" class="menuAction" href="JavaScript:void(0);">Delete this column</a></li> \
<li class="write-op"><a data-action="bulkEdit" class="menuAction" href="JavaScript:void(0);">Transform...</a></li> \
<li class="write-op"><a data-action="deleteColumn" class="menuAction" href="JavaScript:void(0);">Delete this column</a></li> \
<li><a data-action="sortAsc" class="menuAction" href="JavaScript:void(0);">Sort ascending</a></li> \
<li><a data-action="sortDesc" class="menuAction" href="JavaScript:void(0);">Sort descending</a></li> \
<li><a data-action="hideColumn" class="menuAction" href="JavaScript:void(0);">Hide this column</a></li> \
'
, rowActions: '<li><a data-action="deleteRow" class="menuAction" href="JavaScript:void(0);">Delete this row</a></li>'
, rowActions: '<li><a data-action="deleteRow" class="menuAction write-op" href="JavaScript:void(0);">Delete this row</a></li>'
, rootActions: ' \
{{#columns}} \
<li><a data-action="showColumn" data-column="{{.}}" class="menuAction" href="JavaScript:void(0);">Add column: {{.}}</a></li> \
{{/columns}}'
, cellEditor: ' \
<div class="menu-container data-table-cell-editor"> \
<textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
@ -63,14 +70,6 @@ var util = function() {
return o;
};
function inURL(url, str) {
var exists = false;
if ( url.indexOf( str ) > -1 ) {
exists = true;
}
return exists;
}
function registerEmitter() {
var Emitter = function(obj) {
this.emit = function(obj, channel) {
@ -151,295 +150,13 @@ 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 = ' \
<div class="alert-message {{category}} fade in" data-alert="alert"><a class="close" href="#">×</a> \
<p>{{msg}} \
{{#loader}} \
<img src="images/small-spinner.gif" class="notification-loader"> \
{{/loader}} \
</p> \
</div>';
var _templated = $.mustache(_template, tmplData);
_templated = $(_templated).appendTo($('.data-explorer .alert-messages'));
if (!options.persist) {
setTimeout(function() {
$(_templated).remove();
}, 3000);
}
}
function formatMetadata(data) {
out = '<dl>';
$.each(data, function(key, val) {
if (typeof(val) == 'string' && key[0] != '_') {
out = out + '<dt>' + key + '<dd>' + val;
} else if (typeof(val) == 'object' && key != "geometry" && val != null) {
if (key == 'properties') {
$.each(val, function(attr, value){
out = out + '<dt>' + attr + '<dd>' + value;
})
} else {
out = out + '<dt>' + key + '<dd>' + val.join(', ');
}
}
});
out = out + '</dl>';
return out;
}
function getBaseURL(url) {
var baseURL = "";
if ( inURL(url, '_design') ) {
if (inURL(url, '_rewrite')) {
var path = url.split("#")[0];
if (path[path.length - 1] === "/") {
baseURL = "";
} else {
baseURL = '_rewrite/';
}
} else {
baseURL = '_rewrite/';
}
}
return baseURL;
}
var persist = {
restore: function() {
$('.persist').each(function(i, el) {
var inputId = $(el).attr('id');
if(localStorage.getItem(inputId)) $('#' + inputId).val(localStorage.getItem(inputId));
})
},
save: function(id) {
localStorage.setItem(id, $('#' + id).val());
},
clear: function() {
$('.persist').each(function(i, el) {
localStorage.removeItem($(el).attr('id'));
})
}
}
// simple debounce adapted from underscore.js
function delay(func, wait) {
return function() {
var context = this, args = arguments;
var throttler = function() {
delete app.timeout;
func.apply(context, args);
};
if (!app.timeout) app.timeout = setTimeout(throttler, wait);
};
};
function resetForm(form) {
$(':input', form)
.not(':button, :submit, :reset, :hidden')
.val('')
.removeAttr('checked')
.removeAttr('selected');
}
function largestWidth(selector, min) {
var min_width = min || 0;
$(selector).each(function(i, n){
var this_width = $(n).width();
if (this_width > min_width) {
min_width = this_width;
}
});
return min_width;
}
function getType(obj) {
if (obj === null) {
return 'null';
}
if (typeof obj === 'object') {
if (obj.constructor.toString().indexOf("Array") !== -1) {
return 'array';
} else {
return 'object';
}
} else {
return typeof obj;
}
}
function lookupPath(path) {
var docs = app.apiDocs;
try {
_.each(path, function(node) {
docs = docs[node];
})
} catch(e) {
util.notify("Error selecting documents" + e);
docs = [];
}
return docs;
}
function nodePath(docField) {
if (docField.children('.object-key').length > 0) return docField.children('.object-key').text();
if (docField.children('.array-key').length > 0) return docField.children('.array-key').text();
if (docField.children('.doc-key').length > 0) return docField.children('.doc-key').text();
return "";
}
function selectedTreePath() {
var nodes = []
, parent = $('.chosen');
while (parent.length > 0) {
nodes.push(nodePath(parent));
parent = parent.parents('.doc-field:first');
}
return _.compact(nodes).reverse();
}
// TODO refactor handlers so that they dont stack up as the tree gets bigger
function handleTreeClick(e) {
var clicked = $(e.target);
if(clicked.hasClass('expand')) return;
if (clicked.children('.array').length > 0) {
var field = clicked;
} else if (clicked.siblings('.array').length > 0) {
var field = clicked.parents('.doc-field:first');
} else {
var field = clicked.parents('.array').parents('.doc-field:first');
}
$('.chosen').removeClass('chosen');
field.addClass('chosen');
return false;
}
var createTreeNode = {
"string": function (obj, key) {
var val = $('<div class="doc-value string-type"></div>');
if (obj[key].length > 45) {
val.append($('<span class="string-type"></span>')
.text(obj[key].slice(0, 45)))
.append(
$('<span class="expand">...</span>')
.click(function () {
val.html('')
.append($('<span class="string-type"></span>')
.text(obj[key].length ? obj[key] : " ")
)
})
)
}
else {
var val = $('<div class="doc-value string-type"></div>');
val.append(
$('<span class="string-type"></span>')
.text(obj[key].length ? obj[key] : " ")
)
}
return val;
}
, "number": function (obj, key) {
var val = $('<div class="doc-value number"></div>')
val.append($('<span class="number-type">' + obj[key] + '</span>'))
return val;
}
, "null": function (obj, key) {
var val = $('<div class="doc-value null"></div>')
val.append($('<span class="null-type">' + obj[key] + '</span>'))
return val;
}
, "boolean": function (obj, key) {
var val = $('<div class="fue null"></div>')
val.append($('<span class="null-type">' + obj[key] + '</span>'))
return val;
}
, "array": function (obj, key, indent) {
if (!indent) indent = 1;
var val = $('<div class="doc-value array"></div>')
$('<span class="array-type">[</span><span class="expand" style="float:left">...</span><span class="array-type">]</span>')
.click(function (e) {
var n = $(this).parent();
var cls = 'sub-'+key+'-'+indent
n.html('')
n.append('<span style="padding-left:'+((indent - 1) * 10)+'px" class="array-type">[</span>')
for (i in obj[key]) {
var field = $('<div class="doc-field"></div>').click(handleTreeClick);
n.append(
field
.append('<div class="array-key '+cls+'" >'+i+'</div>')
.append(createTreeNode[getType(obj[key][i])](obj[key], i, indent + 1))
)
}
n.append('<span style="padding-left:'+((indent - 1) * 10)+'px" class="array-type">]</span>')
$('div.'+cls).width(largestWidth('div.'+cls))
})
.appendTo($('<div class="array-type"></div>').appendTo(val))
return val;
}
, "object": function (obj, key, indent) {
if (!indent) indent = 1;
var val = $('<div class="doc-value object"></div>')
$('<span class="object-type">{</span><span class="expand" style="float:left">...</span><span class="object-type">}</span>')
.click(function (e) {
var n = $(this).parent();
n.html('')
n.append('<span style="padding-left:'+((indent - 1) * 10)+'px" class="object-type">{</span>')
for (i in obj[key]) {
var field = $('<div class="doc-field"></div>').click(handleTreeClick);
var p = $('<div class="id-space" style="margin-left:'+(indent * 10)+'px"/>');
var di = $('<div class="object-key">'+i+'</div>')
field.append(p)
.append(di)
.append(createTreeNode[getType(obj[key][i])](obj[key], i, indent + 1))
n.append(field)
}
n.append('<span style="padding-left:'+((indent - 1) * 10)+'px" class="object-type">}</span>')
di.width(largestWidth('div.object-key'))
})
.appendTo($('<div class="object-type"></div>').appendTo(val))
return val;
}
}
function renderTree(doc) {
var d = $('div#document-editor');
for (i in doc) {
var field = $('<div class="doc-field"></div>').click(handleTreeClick);
$('<div class="id-space" />').appendTo(field);
field.append('<div class="doc-key doc-key-base">'+i+'</div>')
field.append(createTreeNode[getType(doc[i])](doc, i));
d.append(field);
}
$('div.doc-key-base').width(largestWidth('div.doc-key-base'))
}
return {
inURL: inURL,
registerEmitter: registerEmitter,
listenFor: listenFor,
show: show,
hide: hide,
position: position,
render: render,
notify: notify,
observeExit: observeExit,
formatMetadata:formatMetadata,
getBaseURL:getBaseURL,
resetForm: resetForm,
delay: delay,
persist: persist,
lookupPath: lookupPath,
selectedTreePath: selectedTreePath,
renderTree: renderTree
observeExit: observeExit
};
}();

View File

@ -23,6 +23,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 = ' \
<div class="alert-message {{category}} fade in" data-alert="alert"><a class="close" href="#">×</a> \
<p>{{msg}} \
{{#loader}} \
<img src="images/small-spinner.gif" class="notification-loader"> \
{{/loader}} \
</p> \
</div>';
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
@ -101,17 +142,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
self.model.getDocuments(self.config.displayCount);
});
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});
});
},
query: function() {
this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
var queryObj = {
size: this.config.displayCount
};
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.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
this.model.getDocuments(this.config.displayCount);
this.query();
},
setReadOnly: function() {
@ -180,11 +240,13 @@ my.DataTable = Backbone.View.extend({
this.model.currentDocuments.bind('reset', this.render);
this.model.currentDocuments.bind('remove', this.render);
this.state = {};
this.hiddenHeaders = [];
},
events: {
'click .column-header-menu': 'onColumnHeaderClick'
, 'click .row-header-menu': 'onRowHeaderClick'
, 'click .root-header-menu': 'onRootHeaderClick'
, 'click .data-table-menu li a': 'onMenuClick'
},
@ -214,6 +276,11 @@ my.DataTable = Backbone.View.extend({
util.position('data-table-menu', e);
util.render('rowActions', 'data-table-menu');
},
onRootHeaderClick: function(e) {
util.position('data-table-menu', e);
util.render('rootActions', 'data-table-menu', {'columns': this.hiddenHeaders});
},
onMenuClick: function(e) {
var self = this;
@ -221,6 +288,10 @@ my.DataTable = Backbone.View.extend({
var actions = {
bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
transform: function() { self.showTransformDialog('transform') },
sortAsc: function() { self.setColumnSort('asc') },
sortDesc: function() { self.setColumnSort('desc') },
hideColumn: function() { self.hideColumn() },
showColumn: function() { self.showColumn(e) },
// TODO: Delete or re-implement ...
csv: function() { window.location.href = app.csvUrl },
json: function() { window.location.href = "_rewrite/api/json" },
@ -243,10 +314,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)
})
}
}
@ -284,6 +355,20 @@ my.DataTable = Backbone.View.extend({
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
},
setColumnSort: function(order) {
var query = _.extend(this.model.queryState, {sort: [[this.state.currentColumn, order]]});
this.model.query(query);
},
hideColumn: function() {
this.hiddenHeaders.push(this.state.currentColumn);
this.render();
},
showColumn: function(e) {
this.hiddenHeaders = _.without(this.hiddenHeaders, $(e.target).data('column'));
this.render();
},
// ======================================================
// Core Templating
@ -293,7 +378,14 @@ my.DataTable = Backbone.View.extend({
<table class="data-table" cellspacing="0"> \
<thead> \
<tr> \
{{#notEmpty}}<th class="column-header"></th>{{/notEmpty}} \
{{#notEmpty}} \
<th class="column-header"> \
<div class="column-header-title"> \
<a class="root-header-menu"></a> \
<span class="column-header-name"></span> \
</div> \
</th> \
{{/notEmpty}} \
{{#headers}} \
<th class="column-header"> \
<div class="column-header-title"> \
@ -311,11 +403,15 @@ my.DataTable = Backbone.View.extend({
toTemplateJSON: function() {
var modelData = this.model.toJSON()
modelData.notEmpty = ( modelData.headers.length > 0 )
modelData.notEmpty = ( this.headers.length > 0 )
modelData.headers = this.headers;
return modelData;
},
render: function() {
var self = this;
this.headers = _.filter(this.model.get('headers'), function(header) {
return _.indexOf(self.hiddenHeaders, header) == -1;
});
var htmls = $.mustache(this.template, this.toTemplateJSON());
this.el.html(htmls);
this.model.currentDocuments.forEach(function(doc) {
@ -324,10 +420,11 @@ my.DataTable = Backbone.View.extend({
var newView = new my.DataTableRow({
model: doc,
el: tr,
headers: self.model.get('headers')
headers: self.headers,
});
newView.render();
});
$(".root-header-menu").toggle((self.hiddenHeaders.length > 0));
return this;
}
});
@ -343,6 +440,7 @@ my.DataTableRow = Backbone.View.extend({
this.el = $(this.el);
this.model.bind('change', this.render);
},
template: ' \
<td><a class="row-header-menu"></a></td> \
{{#cells}} \
@ -398,12 +496,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
});
@ -501,11 +599,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();
});
@ -515,7 +613,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();
}

View File

@ -26,21 +26,33 @@
// deep copy so we do not touch original data ...
, data: $.extend(true, {}, indata)
};
expect(9);
expect(10);
dataset.fetch().then(function(dataset) {
equal(dataset.get('name'), metadata.name);
deepEqual(dataset.get('headers'), indata.headers);
equal(dataset.docCount, 6);
dataset.getDocuments(4, 2).then(function(documentList) {
var queryObj = {
size: 4
, offset: 2
};
dataset.query(queryObj).then(function(documentList) {
deepEqual(indata.rows[2], documentList.models[0].toJSON());
});
dataset.getDocuments().then(function(docList) {
// Test getDocuments
equal(docList.length, Math.min(10, indata.rows.length));
var queryObj = {
sort: [
['y', 'desc']
]
};
dataset.query(queryObj).then(function(docs) {
var doc0 = dataset.currentDocuments.models[0].toJSON();
equal(doc0.x, 6);
});
dataset.query().then(function(docList) {
equal(docList.length, Math.min(100, indata.rows.length));
var doc1 = docList.models[0];
deepEqual(doc1.toJSON(), indata.rows[0]);
// Test UPDATA
// Test UPDATE
var newVal = 10;
doc1.set({x: newVal});
doc1.save().then(function() {
@ -142,26 +154,32 @@
var stub = sinon.stub($, 'ajax', function(options) {
if (options.url.indexOf('schema.json') != -1) {
return {
then: function(callback) {
done: function(callback) {
callback(webstoreSchema);
return this;
},
fail: function() {
return this;
}
}
} else {
return {
then: function(callback) {
done: function(callback) {
callback(webstoreData);
},
fail: function() {
}
}
}
});
dataset.fetch().then(function(dataset) {
dataset.fetch().done(function(dataset) {
deepEqual(['__id__', 'date', 'geometry', 'amount'], dataset.get('headers'));
equal(3, dataset.docCount)
dataset.getDocuments().then(function(docList) {
equal(3, docList.length)
equal("2009-01-01", docList.models[0].get('date'));
});
// dataset.query().done(function(docList) {
// equal(3, docList.length)
// equal("2009-01-01", docList.models[0].get('date'));
// });
});
$.ajax.restore();
});
@ -243,21 +261,25 @@
var partialUrl = 'jsonpdataproxy.appspot.com';
if (options.url.indexOf(partialUrl) != -1) {
return {
then: function(callback) {
done: function(callback) {
callback(dataProxyData);
return this;
},
fail: function() {
return this;
}
}
}
});
dataset.fetch().then(function(dataset) {
dataset.fetch().done(function(dataset) {
deepEqual(['__id__', 'date', 'price'], dataset.get('headers'));
equal(null, dataset.docCount)
dataset.getDocuments().then(function(docList) {
dataset.query().done(function(docList) {
equal(10, docList.length)
equal("1950-01", docList.models[0].get('date'));
// needed only if not stubbing
start();
// needed only if not stubbing
start();
});
});
$.ajax.restore();
@ -455,7 +477,7 @@
console.log('inside dataset:', dataset, dataset.get('headers'), dataset.get('data'));
deepEqual(['column-2', 'column-1'], dataset.get('headers'));
//equal(null, dataset.docCount)
dataset.getDocuments().then(function(docList) {
dataset.query().then(function(docList) {
equal(3, docList.length);
console.log(docList.models[0]);
equal("A", docList.models[0].get('column-1'));