[build,make][s]: build latest files plus a new recline.dataset.js file.
This commit is contained in:
parent
731e6093dd
commit
8bc5154dda
127
dist/recline.css
vendored
127
dist/recline.css
vendored
@ -204,94 +204,6 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
|
||||
/**********************************************************
|
||||
* Transform Dialog
|
||||
*********************************************************/
|
||||
|
||||
textarea.expression-preview-code {
|
||||
font-family: monospace;
|
||||
height: 5em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.expression-preview-parsing-status {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.expression-preview-parsing-status.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#expression-preview-tabs-preview,
|
||||
#expression-preview-tabs-help,
|
||||
#expression-preview-tabs-history,
|
||||
#expression-preview-tabs-starred {
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#expression-preview-tabs-preview > div,
|
||||
#expression-preview-tabs-help > div,
|
||||
#expression-preview-tabs-history > div,
|
||||
#expression-preview-tabs-starred {
|
||||
height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#expression-preview-tabs-preview td, #expression-preview-tabs-preview th,
|
||||
#expression-preview-tabs-help td, #expression-preview-tabs-help th,
|
||||
#expression-preview-tabs-history td, #expression-preview-tabs-history th,
|
||||
#expression-preview-tabs-starred td, #expression-preview-tabs-starred th {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.expression-preview-table-wrapper {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.expression-preview-container td {
|
||||
padding: 2px 5px;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
td.expression-preview-heading {
|
||||
border-top: none;
|
||||
background: #ddd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td.expression-preview-value {
|
||||
max-width: 250px !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.expression-preview-special-value {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.expression-preview-help-container h3 {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 7px;
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
.expression-preview-doc-item-title {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.expression-preview-doc-item-params {
|
||||
}
|
||||
|
||||
.expression-preview-doc-item-returns {
|
||||
}
|
||||
|
||||
.expression-preview-doc-item-desc {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
/**********************************************************
|
||||
* Read-only mode
|
||||
*********************************************************/
|
||||
@ -691,3 +603,42 @@ classes should alter those!
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.recline-transform .script {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.recline-transform .script textarea {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.recline-transform h2 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.recline-transform h2 .okButton {
|
||||
margin-left: 10px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.recline-transform .preview {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.expression-preview-parsing-status {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.expression-preview-parsing-status.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.recline-transform .before-after .after {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.recline-transform .before-after .after.different {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
748
dist/recline.dataset.js
vendored
Normal file
748
dist/recline.dataset.js
vendored
Normal file
@ -0,0 +1,748 @@
|
||||
// # Recline Backbone Models
|
||||
this.recline = this.recline || {};
|
||||
this.recline.Model = this.recline.Model || {};
|
||||
|
||||
(function($, my) {
|
||||
|
||||
// ## <a id="dataset">Dataset</a>
|
||||
my.Dataset = Backbone.Model.extend({
|
||||
__type__: 'Dataset',
|
||||
|
||||
// ### initialize
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'query');
|
||||
this.backend = null;
|
||||
if (this.get('backend')) {
|
||||
this.backend = this._backendFromString(this.get('backend'));
|
||||
} else { // try to guess backend ...
|
||||
if (this.get('records')) {
|
||||
this.backend = recline.Backend.Memory;
|
||||
}
|
||||
}
|
||||
this.fields = new my.FieldList();
|
||||
this.currentRecords = new my.RecordList();
|
||||
this._changes = {
|
||||
deletes: [],
|
||||
updates: [],
|
||||
creates: []
|
||||
};
|
||||
this.facets = new my.FacetList();
|
||||
this.recordCount = null;
|
||||
this.queryState = new my.Query();
|
||||
this.queryState.bind('change', this.query);
|
||||
this.queryState.bind('facet:add', this.query);
|
||||
// store is what we query and save against
|
||||
// store will either be the backend or be a memory store if Backend fetch
|
||||
// tells us to use memory store
|
||||
this._store = this.backend;
|
||||
if (this.backend == recline.Backend.Memory) {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
// ### fetch
|
||||
//
|
||||
// Retrieve dataset and (some) records from the backend.
|
||||
fetch: function() {
|
||||
var self = this;
|
||||
var dfd = $.Deferred();
|
||||
|
||||
if (this.backend !== recline.Backend.Memory) {
|
||||
this.backend.fetch(this.toJSON())
|
||||
.done(handleResults)
|
||||
.fail(function(arguments) {
|
||||
dfd.reject(arguments);
|
||||
});
|
||||
} else {
|
||||
// special case where we have been given data directly
|
||||
handleResults({
|
||||
records: this.get('records'),
|
||||
fields: this.get('fields'),
|
||||
useMemoryStore: true
|
||||
});
|
||||
}
|
||||
|
||||
function handleResults(results) {
|
||||
var out = self._normalizeRecordsAndFields(results.records, results.fields);
|
||||
if (results.useMemoryStore) {
|
||||
self._store = new recline.Backend.Memory.Store(out.records, out.fields);
|
||||
}
|
||||
|
||||
self.set(results.metadata);
|
||||
self.fields.reset(out.fields);
|
||||
self.query()
|
||||
.done(function() {
|
||||
dfd.resolve(self);
|
||||
})
|
||||
.fail(function(arguments) {
|
||||
dfd.reject(arguments);
|
||||
});
|
||||
}
|
||||
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
// ### _normalizeRecordsAndFields
|
||||
//
|
||||
// Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects
|
||||
//
|
||||
// e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] =>
|
||||
// fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]
|
||||
_normalizeRecordsAndFields: function(records, fields) {
|
||||
// if no fields get them from records
|
||||
if (!fields && records && records.length > 0) {
|
||||
// records is array then fields is first row of records ...
|
||||
if (records[0] instanceof Array) {
|
||||
fields = records[0];
|
||||
records = records.slice(1);
|
||||
} else {
|
||||
fields = _.map(_.keys(records[0]), function(key) {
|
||||
return {id: key};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// fields is an array of strings (i.e. list of field headings/ids)
|
||||
if (fields && fields.length > 0 && typeof fields[0] === 'string') {
|
||||
// Rename duplicate fieldIds as each field name needs to be
|
||||
// unique.
|
||||
var seen = {};
|
||||
fields = _.map(fields, function(field, index) {
|
||||
// cannot use trim as not supported by IE7
|
||||
var fieldId = field.replace(/^\s+|\s+$/g, '');
|
||||
if (fieldId === '') {
|
||||
fieldId = '_noname_';
|
||||
field = fieldId;
|
||||
}
|
||||
while (fieldId in seen) {
|
||||
seen[field] += 1;
|
||||
fieldId = field + seen[field];
|
||||
}
|
||||
if (!(field in seen)) {
|
||||
seen[field] = 0;
|
||||
}
|
||||
// TODO: decide whether to keep original name as label ...
|
||||
// return { id: fieldId, label: field || fieldId }
|
||||
return { id: fieldId };
|
||||
});
|
||||
}
|
||||
// records is provided as arrays so need to zip together with fields
|
||||
// NB: this requires you to have fields to match arrays
|
||||
if (records && records.length > 0 && records[0] instanceof Array) {
|
||||
records = _.map(records, function(doc) {
|
||||
var tmp = {};
|
||||
_.each(fields, function(field, idx) {
|
||||
tmp[field.id] = doc[idx];
|
||||
});
|
||||
return tmp;
|
||||
});
|
||||
}
|
||||
return {
|
||||
fields: fields,
|
||||
records: records
|
||||
};
|
||||
},
|
||||
|
||||
save: function() {
|
||||
var self = this;
|
||||
// TODO: need to reset the changes ...
|
||||
return this._store.save(this._changes, this.toJSON());
|
||||
},
|
||||
|
||||
transform: function(editFunc) {
|
||||
var self = this;
|
||||
if (!this._store.transform) {
|
||||
alert('Transform is not supported with this backend: ' + this.get('backend'));
|
||||
return;
|
||||
}
|
||||
this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
|
||||
this._store.transform(editFunc).done(function() {
|
||||
// reload data as records have changed
|
||||
self.query();
|
||||
self.trigger('recline:flash', {message: "Records updated successfully"});
|
||||
});
|
||||
},
|
||||
|
||||
// ### query
|
||||
//
|
||||
// AJAX method with promise API to get records from the backend.
|
||||
//
|
||||
// It will query based on current query state (given by this.queryState)
|
||||
// updated by queryObj (if provided).
|
||||
//
|
||||
// Resulting RecordList are used to reset this.currentRecords and are
|
||||
// also returned.
|
||||
query: function(queryObj) {
|
||||
var self = this;
|
||||
var dfd = $.Deferred();
|
||||
this.trigger('query:start');
|
||||
|
||||
if (queryObj) {
|
||||
this.queryState.set(queryObj, {silent: true});
|
||||
}
|
||||
var actualQuery = this.queryState.toJSON();
|
||||
|
||||
this._store.query(actualQuery, this.toJSON())
|
||||
.done(function(queryResult) {
|
||||
self._handleQueryResult(queryResult);
|
||||
self.trigger('query:done');
|
||||
dfd.resolve(self.currentRecords);
|
||||
})
|
||||
.fail(function(arguments) {
|
||||
self.trigger('query:fail', arguments);
|
||||
dfd.reject(arguments);
|
||||
});
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
_handleQueryResult: function(queryResult) {
|
||||
var self = this;
|
||||
self.recordCount = queryResult.total;
|
||||
var docs = _.map(queryResult.hits, function(hit) {
|
||||
var _doc = new my.Record(hit);
|
||||
_doc.bind('change', function(doc) {
|
||||
self._changes.updates.push(doc.toJSON());
|
||||
});
|
||||
_doc.bind('destroy', function(doc) {
|
||||
self._changes.deletes.push(doc.toJSON());
|
||||
});
|
||||
return _doc;
|
||||
});
|
||||
self.currentRecords.reset(docs);
|
||||
if (queryResult.facets) {
|
||||
var facets = _.map(queryResult.facets, function(facetResult, facetId) {
|
||||
facetResult.id = facetId;
|
||||
return new my.Facet(facetResult);
|
||||
});
|
||||
self.facets.reset(facets);
|
||||
}
|
||||
},
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var data = this.toJSON();
|
||||
data.recordCount = this.recordCount;
|
||||
data.fields = this.fields.toJSON();
|
||||
return data;
|
||||
},
|
||||
|
||||
// ### getFieldsSummary
|
||||
//
|
||||
// Get a summary for each field in the form of a `Facet`.
|
||||
//
|
||||
// @return null as this is async function. Provides deferred/promise interface.
|
||||
getFieldsSummary: function() {
|
||||
var self = this;
|
||||
var query = new my.Query();
|
||||
query.set({size: 0});
|
||||
this.fields.each(function(field) {
|
||||
query.addFacet(field.id);
|
||||
});
|
||||
var dfd = $.Deferred();
|
||||
this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
|
||||
if (queryResult.facets) {
|
||||
_.each(queryResult.facets, function(facetResult, facetId) {
|
||||
facetResult.id = facetId;
|
||||
var facet = new my.Facet(facetResult);
|
||||
// TODO: probably want replace rather than reset (i.e. just replace the facet with this id)
|
||||
self.fields.get(facetId).facets.reset(facet);
|
||||
});
|
||||
}
|
||||
dfd.resolve(queryResult);
|
||||
});
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
// ### recordSummary
|
||||
//
|
||||
// Get a simple html summary of a Dataset record in form of key/value list
|
||||
recordSummary: function(record) {
|
||||
var html = '<div class="recline-record-summary">';
|
||||
this.fields.each(function(field) {
|
||||
if (field.id != 'id') {
|
||||
html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + record.getFieldValue(field) + '</div>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
},
|
||||
|
||||
// ### _backendFromString(backendString)
|
||||
//
|
||||
// See backend argument to initialize for details
|
||||
_backendFromString: function(backendString) {
|
||||
var parts = backendString.split('.');
|
||||
// walk through the specified path xxx.yyy.zzz to get the final object which should be backend class
|
||||
var current = window;
|
||||
for(ii=0;ii<parts.length;ii++) {
|
||||
if (!current) {
|
||||
break;
|
||||
}
|
||||
current = current[parts[ii]];
|
||||
}
|
||||
if (current) {
|
||||
return current;
|
||||
}
|
||||
|
||||
// alternatively we just had a simple string
|
||||
var backend = null;
|
||||
if (recline && recline.Backend) {
|
||||
_.each(_.keys(recline.Backend), function(name) {
|
||||
if (name.toLowerCase() === backendString.toLowerCase()) {
|
||||
backend = recline.Backend[name];
|
||||
}
|
||||
});
|
||||
}
|
||||
return backend;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ### Dataset.restore
|
||||
//
|
||||
// Restore a Dataset instance from a serialized state. Serialized state for a
|
||||
// Dataset is an Object like:
|
||||
//
|
||||
// <pre>
|
||||
// {
|
||||
// backend: {backend type - i.e. value of dataset.backend.__type__}
|
||||
// dataset: {dataset info needed for loading -- result of dataset.toJSON() would be sufficient but can be simpler }
|
||||
// // convenience - if url provided and dataste not this be used as dataset url
|
||||
// url: {dataset url}
|
||||
// ...
|
||||
// }
|
||||
my.Dataset.restore = function(state) {
|
||||
var dataset = null;
|
||||
// hack-y - restoring a memory dataset does not mean much ...
|
||||
if (state.backend === 'memory') {
|
||||
var datasetInfo = {
|
||||
records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
|
||||
};
|
||||
} else {
|
||||
var datasetInfo = {
|
||||
url: state.url,
|
||||
backend: state.backend
|
||||
};
|
||||
}
|
||||
dataset = new recline.Model.Dataset(datasetInfo);
|
||||
return dataset;
|
||||
};
|
||||
|
||||
// ## <a id="record">A Record (aka Row)</a>
|
||||
//
|
||||
// A single entry or row in the dataset
|
||||
my.Record = Backbone.Model.extend({
|
||||
__type__: 'Record',
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'getFieldValue');
|
||||
},
|
||||
|
||||
// ### getFieldValue
|
||||
//
|
||||
// For the provided Field get the corresponding rendered computed data value
|
||||
// for this record.
|
||||
getFieldValue: function(field) {
|
||||
val = this.getFieldValueUnrendered(field);
|
||||
if (field.renderer) {
|
||||
val = field.renderer(val, field, this.toJSON());
|
||||
}
|
||||
return val;
|
||||
},
|
||||
|
||||
// ### getFieldValueUnrendered
|
||||
//
|
||||
// For the provided Field get the corresponding computed data value
|
||||
// for this record.
|
||||
getFieldValueUnrendered: function(field) {
|
||||
var val = this.get(field.id);
|
||||
if (field.deriver) {
|
||||
val = field.deriver(val, field, this);
|
||||
}
|
||||
return val;
|
||||
},
|
||||
|
||||
// Override Backbone save, fetch and destroy so they do nothing
|
||||
// Instead, Dataset object that created this Record should take care of
|
||||
// handling these changes (discovery will occur via event notifications)
|
||||
// WARNING: these will not persist *unless* you call save on Dataset
|
||||
fetch: function() {},
|
||||
save: function() {},
|
||||
destroy: function() { this.trigger('destroy', this); }
|
||||
});
|
||||
|
||||
// ## A Backbone collection of Records
|
||||
my.RecordList = Backbone.Collection.extend({
|
||||
__type__: 'RecordList',
|
||||
model: my.Record
|
||||
});
|
||||
|
||||
// ## <a id="field">A Field (aka Column) on a Dataset</a>
|
||||
my.Field = Backbone.Model.extend({
|
||||
// ### defaults - define default values
|
||||
defaults: {
|
||||
label: null,
|
||||
type: 'string',
|
||||
format: null,
|
||||
is_derived: false
|
||||
},
|
||||
// ### initialize
|
||||
//
|
||||
// @param {Object} data: standard Backbone model attributes
|
||||
//
|
||||
// @param {Object} options: renderer and/or deriver functions.
|
||||
initialize: function(data, options) {
|
||||
// if a hash not passed in the first argument throw error
|
||||
if ('0' in data) {
|
||||
throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
|
||||
}
|
||||
if (this.attributes.label === null) {
|
||||
this.set({label: this.id});
|
||||
}
|
||||
if (options) {
|
||||
this.renderer = options.renderer;
|
||||
this.deriver = options.deriver;
|
||||
}
|
||||
if (!this.renderer) {
|
||||
this.renderer = this.defaultRenderers[this.get('type')];
|
||||
}
|
||||
this.facets = new my.FacetList();
|
||||
},
|
||||
defaultRenderers: {
|
||||
object: function(val, field, doc) {
|
||||
return JSON.stringify(val);
|
||||
},
|
||||
geo_point: function(val, field, doc) {
|
||||
return JSON.stringify(val);
|
||||
},
|
||||
'float': function(val, field, doc) {
|
||||
var format = field.get('format');
|
||||
if (format === 'percentage') {
|
||||
return val + '%';
|
||||
}
|
||||
return val;
|
||||
},
|
||||
'string': function(val, field, doc) {
|
||||
var format = field.get('format');
|
||||
if (format === 'markdown') {
|
||||
if (typeof Showdown !== 'undefined') {
|
||||
var showdown = new Showdown.converter();
|
||||
out = showdown.makeHtml(val);
|
||||
return out;
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
} else if (format == 'plain') {
|
||||
return val;
|
||||
} else {
|
||||
// as this is the default and default type is string may get things
|
||||
// here that are not actually strings
|
||||
if (val && typeof val === 'string') {
|
||||
val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
my.FieldList = Backbone.Collection.extend({
|
||||
model: my.Field
|
||||
});
|
||||
|
||||
// ## <a id="query">Query</a>
|
||||
my.Query = Backbone.Model.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
size: 100,
|
||||
from: 0,
|
||||
q: '',
|
||||
facets: {},
|
||||
filters: []
|
||||
};
|
||||
},
|
||||
_filterTemplates: {
|
||||
term: {
|
||||
type: 'term',
|
||||
field: '',
|
||||
term: ''
|
||||
},
|
||||
geo_distance: {
|
||||
distance: 10,
|
||||
unit: 'km',
|
||||
point: {
|
||||
lon: 0,
|
||||
lat: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
// ### addFilter
|
||||
//
|
||||
// Add a new filter (appended to the list of filters)
|
||||
//
|
||||
// @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates
|
||||
addFilter: function(filter) {
|
||||
// crude deep copy
|
||||
var ourfilter = JSON.parse(JSON.stringify(filter));
|
||||
// not full specified so use template and over-write
|
||||
if (_.keys(filter).length <= 2) {
|
||||
ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
|
||||
}
|
||||
var filters = this.get('filters');
|
||||
filters.push(ourfilter);
|
||||
this.trigger('change:filters:new-blank');
|
||||
},
|
||||
updateFilter: function(index, value) {
|
||||
},
|
||||
// ### removeFilter
|
||||
//
|
||||
// Remove a filter from filters at index filterIndex
|
||||
removeFilter: function(filterIndex) {
|
||||
var filters = this.get('filters');
|
||||
filters.splice(filterIndex, 1);
|
||||
this.set({filters: filters});
|
||||
this.trigger('change');
|
||||
},
|
||||
// ### addFacet
|
||||
//
|
||||
// Add a Facet to this query
|
||||
//
|
||||
// See <http://www.elasticsearch.org/guide/reference/api/search/facets/>
|
||||
addFacet: function(fieldId) {
|
||||
var facets = this.get('facets');
|
||||
// Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)
|
||||
if (_.contains(_.keys(facets), fieldId)) {
|
||||
return;
|
||||
}
|
||||
facets[fieldId] = {
|
||||
terms: { field: fieldId }
|
||||
};
|
||||
this.set({facets: facets}, {silent: true});
|
||||
this.trigger('facet:add', this);
|
||||
},
|
||||
addHistogramFacet: function(fieldId) {
|
||||
var facets = this.get('facets');
|
||||
facets[fieldId] = {
|
||||
date_histogram: {
|
||||
field: fieldId,
|
||||
interval: 'day'
|
||||
}
|
||||
};
|
||||
this.set({facets: facets}, {silent: true});
|
||||
this.trigger('facet:add', this);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ## <a id="facet">A Facet (Result)</a>
|
||||
my.Facet = Backbone.Model.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
_type: 'terms',
|
||||
total: 0,
|
||||
other: 0,
|
||||
missing: 0,
|
||||
terms: []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// ## A Collection/List of Facets
|
||||
my.FacetList = Backbone.Collection.extend({
|
||||
model: my.Facet
|
||||
});
|
||||
|
||||
// ## Object State
|
||||
//
|
||||
// Convenience Backbone model for storing (configuration) state of objects like Views.
|
||||
my.ObjectState = Backbone.Model.extend({
|
||||
});
|
||||
|
||||
|
||||
// ## Backbone.sync
|
||||
//
|
||||
// Override Backbone.sync to hand off to sync function in relevant backend
|
||||
Backbone.sync = function(method, model, options) {
|
||||
return model.backend.sync(method, model, options);
|
||||
};
|
||||
|
||||
}(jQuery, this.recline.Model));
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.Backend = this.recline.Backend || {};
|
||||
this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
|
||||
(function($, my) {
|
||||
my.__type__ = 'memory';
|
||||
|
||||
// ## Data Wrapper
|
||||
//
|
||||
// Turn a simple array of JS objects into a mini data-store with
|
||||
// functionality like querying, faceting, updating (by ID) and deleting (by
|
||||
// ID).
|
||||
//
|
||||
// @param data list of hashes for each record/row in the data ({key:
|
||||
// value, key: value})
|
||||
// @param fields (optional) list of field hashes (each hash defining a field
|
||||
// as per recline.Model.Field). If fields not specified they will be taken
|
||||
// from the data.
|
||||
my.Store = function(data, fields) {
|
||||
var self = this;
|
||||
this.data = data;
|
||||
if (fields) {
|
||||
this.fields = fields;
|
||||
} else {
|
||||
if (data) {
|
||||
this.fields = _.map(data[0], function(value, key) {
|
||||
return {id: key};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.update = function(doc) {
|
||||
_.each(self.data, function(internalDoc, idx) {
|
||||
if(doc.id === internalDoc.id) {
|
||||
self.data[idx] = doc;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.delete = function(doc) {
|
||||
var newdocs = _.reject(self.data, function(internalDoc) {
|
||||
return (doc.id === internalDoc.id);
|
||||
});
|
||||
this.data = newdocs;
|
||||
};
|
||||
|
||||
this.save = function(changes, dataset) {
|
||||
var self = this;
|
||||
var dfd = $.Deferred();
|
||||
// TODO _.each(changes.creates) { ... }
|
||||
_.each(changes.updates, function(record) {
|
||||
self.update(record);
|
||||
});
|
||||
_.each(changes.deletes, function(record) {
|
||||
self.delete(record);
|
||||
});
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
this.query = function(queryObj) {
|
||||
var dfd = $.Deferred();
|
||||
var numRows = queryObj.size || this.data.length;
|
||||
var start = queryObj.from || 0;
|
||||
var results = this.data;
|
||||
results = this._applyFilters(results, queryObj);
|
||||
results = this._applyFreeTextQuery(results, queryObj);
|
||||
// not complete sorting!
|
||||
_.each(queryObj.sort, function(sortObj) {
|
||||
var fieldName = _.keys(sortObj)[0];
|
||||
results = _.sortBy(results, function(doc) {
|
||||
var _out = doc[fieldName];
|
||||
return _out;
|
||||
});
|
||||
if (sortObj[fieldName].order == 'desc') {
|
||||
results.reverse();
|
||||
}
|
||||
});
|
||||
var facets = this.computeFacets(results, queryObj);
|
||||
var out = {
|
||||
total: results.length,
|
||||
hits: results.slice(start, start+numRows),
|
||||
facets: facets
|
||||
};
|
||||
dfd.resolve(out);
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// in place filtering
|
||||
this._applyFilters = function(results, queryObj) {
|
||||
_.each(queryObj.filters, function(filter) {
|
||||
// if a term filter ...
|
||||
if (filter.type === 'term') {
|
||||
results = _.filter(results, function(doc) {
|
||||
return (doc[filter.field] == filter.term);
|
||||
});
|
||||
}
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
// we OR across fields but AND across terms in query string
|
||||
this._applyFreeTextQuery = function(results, queryObj) {
|
||||
if (queryObj.q) {
|
||||
var terms = queryObj.q.split(' ');
|
||||
results = _.filter(results, function(rawdoc) {
|
||||
var matches = true;
|
||||
_.each(terms, function(term) {
|
||||
var foundmatch = false;
|
||||
_.each(self.fields, function(field) {
|
||||
var value = rawdoc[field.id];
|
||||
if (value !== null) {
|
||||
value = value.toString();
|
||||
} else {
|
||||
// value can be null (apparently in some cases)
|
||||
value = '';
|
||||
}
|
||||
// TODO regexes?
|
||||
foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase());
|
||||
// TODO: early out (once we are true should break to spare unnecessary testing)
|
||||
// if (foundmatch) return true;
|
||||
});
|
||||
matches = matches && foundmatch;
|
||||
// TODO: early out (once false should break to spare unnecessary testing)
|
||||
// if (!matches) return false;
|
||||
});
|
||||
return matches;
|
||||
});
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
this.computeFacets = function(records, queryObj) {
|
||||
var facetResults = {};
|
||||
if (!queryObj.facets) {
|
||||
return facetResults;
|
||||
}
|
||||
_.each(queryObj.facets, function(query, facetId) {
|
||||
// TODO: remove dependency on recline.Model
|
||||
facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
|
||||
facetResults[facetId].termsall = {};
|
||||
});
|
||||
// faceting
|
||||
_.each(records, function(doc) {
|
||||
_.each(queryObj.facets, function(query, facetId) {
|
||||
var fieldId = query.terms.field;
|
||||
var val = doc[fieldId];
|
||||
var tmp = facetResults[facetId];
|
||||
if (val) {
|
||||
tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
|
||||
} else {
|
||||
tmp.missing = tmp.missing + 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
_.each(queryObj.facets, function(query, facetId) {
|
||||
var tmp = facetResults[facetId];
|
||||
var terms = _.map(tmp.termsall, function(count, term) {
|
||||
return { term: term, count: count };
|
||||
});
|
||||
tmp.terms = _.sortBy(terms, function(item) {
|
||||
// want descending order
|
||||
return -item.count;
|
||||
});
|
||||
tmp.terms = tmp.terms.slice(0, 10);
|
||||
});
|
||||
return facetResults;
|
||||
};
|
||||
|
||||
this.transform = function(editFunc) {
|
||||
var toUpdate = costco.mapDocs(this.data, editFunc);
|
||||
// TODO: very inefficient -- could probably just walk the documents and updates in tandem and update
|
||||
_.each(toUpdate.updates, function(record, idx) {
|
||||
self.data[idx] = record;
|
||||
});
|
||||
return this.save(toUpdate);
|
||||
};
|
||||
};
|
||||
|
||||
}(jQuery, this.recline.Backend.Memory));
|
||||
255
dist/recline.js
vendored
255
dist/recline.js
vendored
@ -188,6 +188,9 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
|
||||
my.__type__ = 'dataproxy';
|
||||
// URL for the dataproxy
|
||||
my.dataproxy_url = 'http://jsonpdataproxy.appspot.com';
|
||||
// Timeout for dataproxy (after this time if no response we error)
|
||||
// Needed because use JSONP so do not receive e.g. 500 errors
|
||||
my.timeout = 5000;
|
||||
|
||||
// ## load
|
||||
//
|
||||
@ -230,12 +233,11 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
|
||||
// a crude way to catch those errors.
|
||||
var _wrapInTimeout = function(ourFunction) {
|
||||
var dfd = $.Deferred();
|
||||
var timeout = 5000;
|
||||
var timer = setTimeout(function() {
|
||||
dfd.reject({
|
||||
message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
|
||||
message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
|
||||
});
|
||||
}, timeout);
|
||||
}, my.timeout);
|
||||
ourFunction.done(function(arguments) {
|
||||
clearTimeout(timer);
|
||||
dfd.resolve(arguments);
|
||||
@ -743,7 +745,12 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
var foundmatch = false;
|
||||
_.each(self.fields, function(field) {
|
||||
var value = rawdoc[field.id];
|
||||
if (value !== null) { value = value.toString(); }
|
||||
if (value !== null) {
|
||||
value = value.toString();
|
||||
} else {
|
||||
// value can be null (apparently in some cases)
|
||||
value = '';
|
||||
}
|
||||
// TODO regexes?
|
||||
foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase());
|
||||
// TODO: early out (once we are true should break to spare unnecessary testing)
|
||||
@ -795,6 +802,15 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
});
|
||||
return facetResults;
|
||||
};
|
||||
|
||||
this.transform = function(editFunc) {
|
||||
var toUpdate = costco.mapDocs(this.data, editFunc);
|
||||
// TODO: very inefficient -- could probably just walk the documents and updates in tandem and update
|
||||
_.each(toUpdate.updates, function(record, idx) {
|
||||
self.data[idx] = record;
|
||||
});
|
||||
return this.save(toUpdate);
|
||||
};
|
||||
};
|
||||
|
||||
}(jQuery, this.recline.Backend.Memory));
|
||||
@ -820,9 +836,9 @@ var costco = function() {
|
||||
;
|
||||
if (!after) after = {};
|
||||
if (currentColumn) {
|
||||
preview.push({before: JSON.stringify(before[currentColumn]), after: JSON.stringify(after[currentColumn])});
|
||||
preview.push({before: before[currentColumn], after: after[currentColumn]});
|
||||
} else {
|
||||
preview.push({before: JSON.stringify(before), after: JSON.stringify(after)});
|
||||
preview.push({before: before, after: after});
|
||||
}
|
||||
}
|
||||
return preview;
|
||||
@ -853,9 +869,9 @@ var costco = function() {
|
||||
});
|
||||
|
||||
return {
|
||||
edited: edited,
|
||||
updates: edited,
|
||||
docs: updatedDocs,
|
||||
deleted: deleted,
|
||||
deletes: deleted,
|
||||
failed: failed
|
||||
};
|
||||
}
|
||||
@ -895,7 +911,7 @@ my.Dataset = Backbone.Model.extend({
|
||||
creates: []
|
||||
};
|
||||
this.facets = new my.FacetList();
|
||||
this.docCount = null;
|
||||
this.recordCount = null;
|
||||
this.queryState = new my.Query();
|
||||
this.queryState.bind('change', this.query);
|
||||
this.queryState.bind('facet:add', this.query);
|
||||
@ -1017,6 +1033,20 @@ my.Dataset = Backbone.Model.extend({
|
||||
return this._store.save(this._changes, this.toJSON());
|
||||
},
|
||||
|
||||
transform: function(editFunc) {
|
||||
var self = this;
|
||||
if (!this._store.transform) {
|
||||
alert('Transform is not supported with this backend: ' + this.get('backend'));
|
||||
return;
|
||||
}
|
||||
this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
|
||||
this._store.transform(editFunc).done(function() {
|
||||
// reload data as records have changed
|
||||
self.query();
|
||||
self.trigger('recline:flash', {message: "Records updated successfully"});
|
||||
});
|
||||
},
|
||||
|
||||
// ### query
|
||||
//
|
||||
// AJAX method with promise API to get records from the backend.
|
||||
@ -1032,7 +1062,7 @@ my.Dataset = Backbone.Model.extend({
|
||||
this.trigger('query:start');
|
||||
|
||||
if (queryObj) {
|
||||
this.queryState.set(queryObj);
|
||||
this.queryState.set(queryObj, {silent: true});
|
||||
}
|
||||
var actualQuery = this.queryState.toJSON();
|
||||
|
||||
@ -1051,7 +1081,7 @@ my.Dataset = Backbone.Model.extend({
|
||||
|
||||
_handleQueryResult: function(queryResult) {
|
||||
var self = this;
|
||||
self.docCount = queryResult.total;
|
||||
self.recordCount = queryResult.total;
|
||||
var docs = _.map(queryResult.hits, function(hit) {
|
||||
var _doc = new my.Record(hit);
|
||||
_doc.bind('change', function(doc) {
|
||||
@ -1074,11 +1104,13 @@ my.Dataset = Backbone.Model.extend({
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var data = this.toJSON();
|
||||
data.docCount = this.docCount;
|
||||
data.recordCount = this.recordCount;
|
||||
data.fields = this.fields.toJSON();
|
||||
return data;
|
||||
},
|
||||
|
||||
// ### getFieldsSummary
|
||||
//
|
||||
// Get a summary for each field in the form of a `Facet`.
|
||||
//
|
||||
// @return null as this is async function. Provides deferred/promise interface.
|
||||
@ -1104,6 +1136,20 @@ my.Dataset = Backbone.Model.extend({
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
// ### recordSummary
|
||||
//
|
||||
// Get a simple html summary of a Dataset record in form of key/value list
|
||||
recordSummary: function(record) {
|
||||
var html = '<div class="recline-record-summary">';
|
||||
this.fields.each(function(field) {
|
||||
if (field.id != 'id') {
|
||||
html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + record.getFieldValue(field) + '</div>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
},
|
||||
|
||||
// ### _backendFromString(backendString)
|
||||
//
|
||||
// See backend argument to initialize for details
|
||||
@ -1198,16 +1244,6 @@ my.Record = Backbone.Model.extend({
|
||||
return val;
|
||||
},
|
||||
|
||||
summary: function(fields) {
|
||||
var html = '';
|
||||
for (key in this.attributes) {
|
||||
if (key != 'id') {
|
||||
html += '<div><strong>' + key + '</strong>: '+ this.attributes[key] + '</div>';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
},
|
||||
|
||||
// Override Backbone save, fetch and destroy so they do nothing
|
||||
// Instead, Dataset object that created this Record should take care of
|
||||
// handling these changes (discovery will occur via event notifications)
|
||||
@ -1258,6 +1294,9 @@ my.Field = Backbone.Model.extend({
|
||||
object: function(val, field, doc) {
|
||||
return JSON.stringify(val);
|
||||
},
|
||||
geo_point: function(val, field, doc) {
|
||||
return JSON.stringify(val);
|
||||
},
|
||||
'float': function(val, field, doc) {
|
||||
var format = field.get('format');
|
||||
if (format === 'percentage') {
|
||||
@ -2254,7 +2293,7 @@ my.Map = Backbone.View.extend({
|
||||
// If not found, the user will need to define the fields via the editor.
|
||||
latitudeFieldNames: ['lat','latitude'],
|
||||
longitudeFieldNames: ['lon','longitude'],
|
||||
geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location'],
|
||||
geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
@ -2856,7 +2895,7 @@ my.MultiView = Backbone.View.extend({
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="recline-results-info"> \
|
||||
Results found <span class="doc-count">{{docCount}}</span> \
|
||||
<span class="doc-count">{{recordCount}}</span> records\
|
||||
</div> \
|
||||
<div class="menu-right"> \
|
||||
<div class="btn-group" data-toggle="buttons-checkbox"> \
|
||||
@ -2887,7 +2926,7 @@ my.MultiView = Backbone.View.extend({
|
||||
this.pageViews = [{
|
||||
id: 'grid',
|
||||
label: 'Grid',
|
||||
view: new my.Grid({
|
||||
view: new my.SlickGrid({
|
||||
model: this.model,
|
||||
state: this.state.get('view-grid')
|
||||
}),
|
||||
@ -2912,6 +2951,12 @@ my.MultiView = Backbone.View.extend({
|
||||
model: this.model,
|
||||
state: this.state.get('view-timeline')
|
||||
}),
|
||||
}, {
|
||||
id: 'transform',
|
||||
label: 'Transform',
|
||||
view: new my.Transform({
|
||||
model: this.model
|
||||
})
|
||||
}];
|
||||
}
|
||||
// these must be called after pageViews are created
|
||||
@ -2933,7 +2978,7 @@ my.MultiView = Backbone.View.extend({
|
||||
});
|
||||
this.model.bind('query:done', function() {
|
||||
self.clearNotifications();
|
||||
self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
|
||||
self.el.find('.doc-count').text(self.model.recordCount || 'Unknown');
|
||||
});
|
||||
this.model.bind('query:fail', function(error) {
|
||||
self.clearNotifications();
|
||||
@ -3037,6 +3082,8 @@ my.MultiView = Backbone.View.extend({
|
||||
this.$filterEditor.toggle();
|
||||
} else if (action === 'fields') {
|
||||
this.$fieldsView.toggle();
|
||||
} else if (action === 'transform') {
|
||||
this.transformView.el.toggle();
|
||||
}
|
||||
},
|
||||
|
||||
@ -3256,6 +3303,8 @@ this.recline.View = this.recline.View || {};
|
||||
// https://github.com/mleibman/SlickGrid
|
||||
//
|
||||
// Initialize it with a `recline.Model.Dataset`.
|
||||
//
|
||||
// NB: you need an explicit height on the element for slickgrid to work
|
||||
my.SlickGrid = Backbone.View.extend({
|
||||
tagName: "div",
|
||||
className: "recline-slickgrid",
|
||||
@ -3263,6 +3312,7 @@ my.SlickGrid = Backbone.View.extend({
|
||||
initialize: function(modelEtc) {
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
this.el.addClass('recline-slickgrid');
|
||||
_.bindAll(this, 'render');
|
||||
this.model.currentRecords.bind('add', this.render);
|
||||
this.model.currentRecords.bind('reset', this.render);
|
||||
@ -3301,7 +3351,6 @@ my.SlickGrid = Backbone.View.extend({
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
|
||||
var options = {
|
||||
enableCellNavigation: true,
|
||||
@ -3648,7 +3697,7 @@ my.Timeline = Backbone.View.extend({
|
||||
"startDate": start,
|
||||
"endDate": end,
|
||||
"headline": String(record.get('title') || ''),
|
||||
"text": record.get('description') || record.summary()
|
||||
"text": record.get('description') || this.model.recordSummary(record)
|
||||
};
|
||||
return tlEntry;
|
||||
} else {
|
||||
@ -3733,61 +3782,23 @@ this.recline.View = this.recline.View || {};
|
||||
|
||||
// ## ColumnTransform
|
||||
//
|
||||
// View (Dialog) for doing data transformations (on columns of data).
|
||||
my.ColumnTransform = Backbone.View.extend({
|
||||
className: 'transform-column-view modal fade in',
|
||||
// View (Dialog) for doing data transformations
|
||||
my.Transform = Backbone.View.extend({
|
||||
className: 'recline-transform',
|
||||
template: ' \
|
||||
<div class="modal-header"> \
|
||||
<a class="close" data-dismiss="modal">×</a> \
|
||||
<h3>Functional transform on column {{name}}</h3> \
|
||||
<div class="script"> \
|
||||
<h2> \
|
||||
Transform Script \
|
||||
<button class="okButton btn btn-primary">Run on all records</button> \
|
||||
</h2> \
|
||||
<textarea class="expression-preview-code"></textarea> \
|
||||
</div> \
|
||||
<div class="modal-body"> \
|
||||
<div class="grid-layout layout-tight layout-full"> \
|
||||
<table> \
|
||||
<tbody> \
|
||||
<tr> \
|
||||
<td colspan="4"> \
|
||||
<div class="grid-layout layout-tight layout-full"> \
|
||||
<table rows="4" cols="4"> \
|
||||
<tbody> \
|
||||
<tr style="vertical-align: bottom;"> \
|
||||
<td colspan="4"> \
|
||||
Expression \
|
||||
</td> \
|
||||
</tr> \
|
||||
<tr> \
|
||||
<td colspan="3"> \
|
||||
<div class="input-container"> \
|
||||
<textarea class="expression-preview-code"></textarea> \
|
||||
</div> \
|
||||
</td> \
|
||||
<td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
|
||||
No syntax error. \
|
||||
</td> \
|
||||
</tr> \
|
||||
<tr> \
|
||||
<td colspan="4"> \
|
||||
<div id="expression-preview-tabs"> \
|
||||
<span>Preview</span> \
|
||||
<div id="expression-preview-tabs-preview"> \
|
||||
<div class="expression-preview-container"> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
</td> \
|
||||
</tr> \
|
||||
</tbody> \
|
||||
</table> \
|
||||
</div> \
|
||||
</td> \
|
||||
</tr> \
|
||||
</tbody> \
|
||||
</table> \
|
||||
</div> \
|
||||
<div class="expression-preview-parsing-status"> \
|
||||
No syntax error. \
|
||||
</div> \
|
||||
<div class="modal-footer"> \
|
||||
<button class="okButton btn primary"> Update All </button> \
|
||||
<button class="cancelButton btn danger">Cancel</button> \
|
||||
<div class="preview"> \
|
||||
<h3>Preview</h3> \
|
||||
<div class="expression-preview-container"></div> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
@ -3796,19 +3807,23 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
'keydown .expression-preview-code': 'onEditorKeydown'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
initialize: function(options) {
|
||||
this.el = $(this.el);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var htmls = Mustache.render(this.template,
|
||||
{name: this.state.currentColumn}
|
||||
);
|
||||
var htmls = Mustache.render(this.template);
|
||||
this.el.html(htmls);
|
||||
// Put in the basic (identity) transform script
|
||||
// TODO: put this into the template?
|
||||
var editor = this.el.find('.expression-preview-code');
|
||||
editor.val("function(doc) {\n doc['"+ this.state.currentColumn+"'] = doc['"+ this.state.currentColumn+"'];\n return doc;\n}");
|
||||
if (this.model.fields.length > 0) {
|
||||
var col = this.model.fields.models[0].id;
|
||||
} else {
|
||||
var col = 'unknown';
|
||||
}
|
||||
editor.val("function(doc) {\n doc['"+ col +"'] = doc['"+ col +"'];\n return doc;\n}");
|
||||
editor.focus().get(0).setSelectionRange(18, 18);
|
||||
editor.keydown();
|
||||
},
|
||||
@ -3821,58 +3836,34 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
this.trigger('recline:flash', {message: "Error with function! " + editFunc.errorMessage});
|
||||
return;
|
||||
}
|
||||
this.el.modal('hide');
|
||||
this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
|
||||
var docs = self.model.currentRecords.map(function(doc) {
|
||||
return doc.toJSON();
|
||||
});
|
||||
// TODO: notify about failed docs?
|
||||
var toUpdate = costco.mapDocs(docs, editFunc).edited;
|
||||
var totalToUpdate = toUpdate.length;
|
||||
function onCompletedUpdate() {
|
||||
totalToUpdate += -1;
|
||||
if (totalToUpdate === 0) {
|
||||
self.trigger('recline:flash', {message: toUpdate.length + " records updated successfully"});
|
||||
alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
|
||||
self.remove();
|
||||
}
|
||||
}
|
||||
// TODO: Very inefficient as we search through all docs every time!
|
||||
_.each(toUpdate, function(editedDoc) {
|
||||
var realDoc = self.model.currentRecords.get(editedDoc.id);
|
||||
realDoc.set(editedDoc);
|
||||
realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate);
|
||||
});
|
||||
this.el.remove();
|
||||
this.model.transform(editFunc);
|
||||
},
|
||||
|
||||
editPreviewTemplate: ' \
|
||||
<div class="expression-preview-table-wrapper"> \
|
||||
<table class="table table-condensed"> \
|
||||
<table class="table table-condensed table-bordered before-after"> \
|
||||
<thead> \
|
||||
<tr> \
|
||||
<th class="expression-preview-heading"> \
|
||||
before \
|
||||
</th> \
|
||||
<th class="expression-preview-heading"> \
|
||||
after \
|
||||
</th> \
|
||||
<th>Field</th> \
|
||||
<th>Before</th> \
|
||||
<th>After</th> \
|
||||
</tr> \
|
||||
</thead> \
|
||||
<tbody> \
|
||||
{{#rows}} \
|
||||
{{#row}} \
|
||||
<tr> \
|
||||
<td class="expression-preview-value"> \
|
||||
<td> \
|
||||
{{field}} \
|
||||
</td> \
|
||||
<td class="before {{#different}}different{{/different}}"> \
|
||||
{{before}} \
|
||||
</td> \
|
||||
<td class="expression-preview-value"> \
|
||||
<td class="after {{#different}}different{{/different}}"> \
|
||||
{{after}} \
|
||||
</td> \
|
||||
</tr> \
|
||||
{{/rows}} \
|
||||
{{/row}} \
|
||||
</tbody> \
|
||||
</table> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
onEditorKeydown: function(e) {
|
||||
@ -3886,10 +3877,26 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
var docs = self.model.currentRecords.map(function(doc) {
|
||||
return doc.toJSON();
|
||||
});
|
||||
var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn);
|
||||
var previewData = costco.previewTransform(docs, editFunc);
|
||||
var $el = self.el.find('.expression-preview-container');
|
||||
var templated = Mustache.render(self.editPreviewTemplate, {rows: previewData.slice(0,4)});
|
||||
$el.html(templated);
|
||||
var fields = self.model.fields.toJSON();
|
||||
var rows = _.map(previewData.slice(0,4), function(row) {
|
||||
return _.map(fields, function(field) {
|
||||
return {
|
||||
field: field.id,
|
||||
before: row.before[field.id],
|
||||
after: row.after[field.id],
|
||||
different: !_.isEqual(row.before[field.id], row.after[field.id])
|
||||
}
|
||||
});
|
||||
});
|
||||
$el.html('');
|
||||
_.each(rows, function(row) {
|
||||
var templated = Mustache.render(self.editPreviewTemplate, {
|
||||
row: row
|
||||
});
|
||||
$el.append(templated);
|
||||
});
|
||||
} else {
|
||||
errors.text(editFunc.errorMessage);
|
||||
}
|
||||
|
||||
4
make
4
make
@ -7,6 +7,10 @@ def cat():
|
||||
print("** Combining js files")
|
||||
cmd = 'ls src/*.js | grep -v couchdb | xargs cat > dist/recline.js'
|
||||
os.system(cmd)
|
||||
|
||||
cmd = 'cat src/model.js src/backend.memory.js > dist/recline.dataset.js'
|
||||
os.system(cmd)
|
||||
|
||||
print("** Combining css files")
|
||||
cmd = 'cat css/*.css > dist/recline.css'
|
||||
os.system(cmd)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user