Added Data editor

This commit is contained in:
DallonF
2012-07-26 16:59:30 -07:00
parent 7998c7ba43
commit 8bf93ba4f5
7 changed files with 343 additions and 11 deletions

View File

@@ -75,7 +75,7 @@
if (obj) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
if (typeof obj[k] === 'object') {
if (typeof obj[k] !== 'string') {
return true;
}
}

View File

@@ -1 +1,74 @@
<h1>Data</h1>
<div id="data" class="well">
<h3>Data</h3>
<div id="current-data">
<div class="table-container">
<table class="table table-bordered table-striped">
<thead class="type-icons">
<tr>
<td class="code">
<i class="icon-white icon-hack string" data-bind="tooltip: 'string'"></i>
id
</td>
<!-- ko foreach: properties -->
<td class="code">
<i class="icon-white icon-hack" data-bind="tooltip: typeLabel, cssNamed: type"></i>
<span data-bind="text: name"></span>
</td>
<!-- /ko -->
<td></td>
</tr>
<tbody data-bind="foreach: collection">
<tr>
<td data-bind="text: id"></td>
<!-- ko foreach: $root.properties -->
<td data-bind="event: {dblclick: $parent._onDoubleclick}">
<span data-bind="text: $parent.c_formatted(name, type), visible: !$parent.c_editing()"></span>
<div data-bind="visible: $parent.c_editing, template: 'data-cell-edit-template'"></div>
</td>
<!-- /ko -->
<td>
<a href="#" class="btn" data-bind="click: c_toggleEditing, visible: !c_editing()"><i class="icon-edit"></i> edit</a>
<a href="#" class="btn" data-bind="click: $root.deleteRow, visible: !c_editing()"><i class="icon-remove-sign"></i> delete</a>
<a href="#" class="btn btn-primary" data-bind="click: $root.saveRow, visible: c_editing"><i class="icon-white icon-ok-sign"></i> done</a>
<a href="#" class="btn" data-bind="click: c_toggleEditing, visible: c_editing">cancel</a>
</td>
</tr>
</tbody>
<tfoot>
<tr data-bind="visible: !collection().length">
<td data-bind="attr: {colspan: properties().length + 2}">
<div class="alert alert-info" style="margin: 40px auto; width: 120px;">This collection is empty.</div>
</td>
</tr>
<tr data-bind="with: newRow">
<td>...</td>
<!-- ko foreach: $root.properties -->
<td>
<div data-bind="template: 'data-cell-edit-template'"></div>
</td>
<!-- /ko -->
<td>
<a href="#" class="btn btn-success" data-bind="click: $root.saveRow"><i class="icon-white icon-plus"></i> Add</a>
</td>
</tr>
</tfoot>
</table>
</div>
<input id="current-data-querystring" data-bind="value: queryString, valueUpdate: 'afterkeydown', tooltip: {placement: 'left', title: queryError, trigger: 'focus'}" type="text" placeholder="Query string" />
</div>
<script type="text/html" id="data-cell-edit-template">
<div class="data-cell-input">
<i class="icon-white icon-warning-sign" data-bind="tooltip: {title: $parent.c_errors()[name]}, visible: $parent.c_errors()[name]"></i>
<!-- ko if: type === 'string' --> <input type="text" data-bind="value: $parent[name], hasfocus: $parent.c_focus() === name, event: {keydown: $parent._onKeypress}", /> <!-- /ko -->
<!-- ko if: type === 'boolean' --> <input type="checkbox" data-bind="checked: $parent[name]" /> <!-- /ko -->
<!-- ko if: type === 'date' --> <input type="datetime" data-bind="value: $parent[name], hasfocus: $parent.c_focus() === name, event: {keydown: $parent._onKeypress}" /> <!-- /ko -->
<!-- ko if: type === 'number' --> <input type="number" data-bind="value: $parent[name], hasfocus: $parent.c_focus() === name, event: {keydown: $parent._onKeypress}" /> <!-- /ko -->
<!-- ko if: type === 'password' --> <input type="password" placeholder="*******" data-bind="value: $parent[name], hasfocus: $parent.c_focus() === name, event: {keydown: $parent._onKeypress}" /> <!-- /ko -->
<!-- ko if: type === 'object' --> {...} <!-- /ko -->
<!-- ko if: type === 'array' --> [...] <!-- /ko -->
</div>
</script>
</div>

View File

@@ -0,0 +1,245 @@
(function() {
var resource = Context.resourceId;
var createRow = function(data, props, vm) {
var self = {};
function map(data, props) {
var defaults = {id: null};
props.forEach(function(prop) {
defaults[prop.name] = undefined;
});
data = _.defaults(data, defaults);
ko.mapping.fromJS(data, {}, self);
}
map(data, props);
self.c_editing = ko.observable(false);
self.c_focus = ko.observable();
self.c_errors = ko.observable({});
self.c_formatted = function(name, type) {
var name = ko.utils.unwrapObservable(name);
var type = ko.utils.unwrapObservable(type);
var value = ko.utils.unwrapObservable(self[name]);
if (type === 'password' || typeof value === 'undefined') {
return '...';
} else {
return value;
}
}
self.c_toggleEditing = function() {
if (self.c_editing() && !self.isNew) {
vm.revertRow(self);
} else {
self.c_editing(true);
}
};
self.c_remapProps = function(props) {
var data = ko.mapping.toJS(self);
map(data, props);
};
self._onKeypress = function(data, e) {
if (e.which == 13) {
setTimeout(function() {
vm.saveRow(self);
}, 1);
} else if (e.which == 27 && !self.isNew) {
setTimeout(function() {
vm.revertRow(self);
}, 1);
}
return true;
};
self._onDoubleclick = function(data, e) {
if (!self.c_editing()) {
self.c_editing(true);
self.c_focus(data.name);
} else {
return true;
}
};
return self;
};
var create = function() {
var self = {
properties: ko.observableArray()
, collection: ko.observableArray()
, queryString: ko.observable()
, queryError: ko.observable("")
, queryObj: ko.observable({})
, isUser: Context.resourceType === "UserCollection"
};
var collectionMapping = {
'collection': {
key: function(data) {
return ko.utils.unwrapObservable(data.id);
}
, create: function(options) {
return createRow(options.data, ko.mapping.toJS(self.properties), self)
}
, update: function(options) {
if (!options.target.c_editing()) {
return options.target;
} else {
return options.data;
}
}
}
};
self.newRow = createRow({}, [], self);
self.newRow.isNew = true;
self.deleteRow = function(data) {
var row = data.id();
dpd(resource).del(row, function(res, err) {
if (err) return ui.error("Could not save", err.message).sticky();
ui.notify("Deleted row", $('<a href="#">Undo</a>').click(function() {
self.saveRow(data);
}));
});
};
self.saveRow = function(data) {
var rowData = ko.mapping.toJS(data);
dpd(resource).post(rowData, function(res, err) {
if (err) {
if (err.errors) {
data.c_errors(err.errors);
} else {
ui.error("Could not save", err.message).sticky();
}
} else {
if (!data.id()) {
self.properties().forEach(function(prop) {
if (data[prop.name]) {
data[prop.name](null);
}
});
} else {
data.c_editing(false);
}
}
});
};
self.revertRow = function(data) {
var row = data.id();
dpd(resource).get(row, function(res, err) {
if (err) return;
ko.mapping.fromJS(res, {}, data);
data.c_editing(false);
});
};
function loadProperties() {
dpd('__resources').get(resource, function(res, err) {
var props = CollectionUtil.propsToArray(res.properties);
if (self.isUser) {
props.unshift({
name: 'email'
, type: 'string'
, typeLabel: 'string'
}, {
name: 'password'
, type: 'password'
, typeLabel: 'password'
});
}
self.collection().forEach(function(row) {
row.c_remapProps(props);
});
self.newRow.c_remapProps(props);
self.properties(props);
});
}
function loadCollection() {
dpd(resource).get(self.queryObj(), function(res, err) {
ko.mapping.fromJS({
collection: res
}
, collectionMapping, self
);
});
}
dpd.on(resource + ':changed', loadCollection);
loadProperties();
ko.computed(function() {
var queryString = self.queryString() || ''
, queryObj = {};
self.queryError("");
if (queryString.indexOf('?') === 0) queryString = queryString.slice(1);
if (queryString.indexOf('{') === 0) {
try {
queryObj = JSON.parse(queryString);
} catch (ex) {
self.queryError(ex);
return;
}
} else {
var vars = queryString.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
queryObj[pair[0]] = unescape(pair[1]);
}
}
self.queryObj(queryObj);
}, self).extend({throttle: 500});
//Hack - this would take a fairly complex custom binding to do the right way
setTimeout(function() {
ko.computed(function() {
var error = self.queryError();
var $field = $('#current-data-querystring');
if (error) {
$field.attr('data-original-title', error).tooltip('fixTitle')
.tooltip('show');
} else {
$field.attr('data-original-title', '').tooltip('fixTitle')
.tooltip('hide');
}
}, self);
}, 1);
loadCollection();
self.queryObj.subscribe(function() {
loadCollection();
});
ko.applyBindings(self);
return self;
};
create();
})();

View File

@@ -224,12 +224,7 @@ function create() {
var propertiesJson = res.properties
, propertiesArray;
propertiesArray = Object.keys(propertiesJson || {}).reduce(function(prev, cur) {
prev.push(propertiesJson[cur]);
return prev;
}, []).sort(function(a, b) {
return a.order - b.order;
});
propertiesArray = CollectionUtil.propsToArray(propertiesJson);
ko.mapping.fromJS({
properties: propertiesArray

View File

@@ -0,0 +1,18 @@
window.CollectionUtil = (function() {
var exports = {};
exports.propsToArray = function(propertiesJson) {
var propertiesArray = Object.keys(propertiesJson || {}).reduce(function(prev, cur) {
prev.push(propertiesJson[cur]);
return prev;
}, []).sort(function(a, b) {
return a.order - b.order;
});
return propertiesArray;
}
return exports;
})();

View File

@@ -1,5 +1,5 @@
<div id="properties" class="container tab-pane active">
<div class="panel well stacked">
<div id="properties">
<div class="panel well">
<h3>
Properties
</h3>
@@ -72,7 +72,7 @@
</div>
<div id="property-now-what" class="alert alert-info hide" data-bind="visible: properties().length && !otherItems()">
When you're done adding properties, you should <a href="../data" class="cta-link">add some data</a> to this collection.
When you're done adding properties, you should <a data-bind="attr: {href: '/dashboard/' + Context.resourceId + '/data'}" class="cta-link">add some data</a> to this collection.
</div>
</div>

View File

@@ -54,6 +54,7 @@ Collection.dashboard = {
'/js/lib/knockout-2.1.0.js'
, '/js/lib/knockout.mapping.js'
, '/js/lib/knockout-util.js'
, '/js/util.js'
]
}