Merge branch 'master' into gh-pages
This commit is contained in:
commit
7003338c9d
@ -16,7 +16,7 @@ Live demo: http://okfnlabs.org/recline/demo/
|
||||
* Bulk update/clean your data using an easy scripting UI
|
||||
* Visualize your data
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Demo App
|
||||
|
||||
@ -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;
|
||||
|
||||
13
index.html
13
index.html
@ -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
1680
recline.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||
|
||||
|
||||
103
src/costco.js
103
src/costco.js
@ -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
|
||||
};
|
||||
}();
|
||||
|
||||
14
src/model.js
14
src/model.js
@ -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();
|
||||
},
|
||||
|
||||
305
src/util.js
305
src/util.js
@ -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
|
||||
};
|
||||
}();
|
||||
|
||||
134
src/view.js
134
src/view.js
@ -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();
|
||||
}
|
||||
|
||||
@ -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'));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user