summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
authorKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2013-05-31 23:11:04 +0300
committerKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2013-05-31 23:42:02 +0300
commit6659899825b9f56e636771715d06b899823f7895 (patch)
tree9ab1d9ae023fecdd03e115289033443a40ff9463 /web
parentbf259b67a6287e46d2bfb07dec08b6a8a9b2b690 (diff)
downloadaconf-6659899825b9f56e636771715d06b899823f7895.tar.bz2
aconf-6659899825b9f56e636771715d06b899823f7895.tar.xz
web client: refactoring
transaction state handling separated from UI logic start new transaction on view change if there are no uncommitted changes minor usability improvements
Diffstat (limited to 'web')
-rw-r--r--web/client.js525
1 files changed, 284 insertions, 241 deletions
diff --git a/web/client.js b/web/client.js
index 5a740f8..c152339 100644
--- a/web/client.js
+++ b/web/client.js
@@ -9,47 +9,175 @@ $(function() {
data: JSON.stringify({username: "admin", password: "admin"})
}).done(function(data, status, xhr) {
- var token = xhr.getResponseHeader("X-ACF-Auth-Token");
- var txn;
-
- function request(url, options) {
- options = options || {};
- options.headers = {"X-ACF-Auth-Token": token};
- if (txn) options.headers["X-ACF-Transaction-ID"] = txn;
- return $.ajax(url, options);
+ function split(path) {
+ var res = [];
+ while (path && path != "/") {
+ var comp = path.match(/^\/([^\\\/]|\\.)+/)[0];
+ res.push(comp.substring(1));
+ path = path.substring(comp.length);
+ }
+ return res;
}
- function objRequest(path, options) {
- return request("/config" + path, options);
+ function join(path, name) {
+ if (_.isString(name)) {
+ name = name.replace(/([\\\/])/g, "\\$1");
+ if (!isNaN(Number(name))) name = "\\" + name;
+ }
+ return (path == "/" ? "" : path) + "/" + name;
}
- var changed, invalid;
- var statusBar = $("#status p");
- var buttons = $("#status div");
+
+ var txnMgr = (function(token) {
+ var txn, changed, invalid;
- function startTxn() {
- var def = $.Deferred();
- txn = null;
- request("/", {type: "POST"}).done(function(data, status, xhr) {
- txn = xhr.getResponseHeader("X-ACF-Transaction-ID");
+ function reset() {
+ txn = null;
changed = {};
invalid = {};
- $("#status").prop("class", null);
- statusBar.empty();
- buttons.prop("class", "hidden");
- def.resolve();
- });
- return def;
- }
+ }
+ reset();
- function isTxnValid() { return !(_.size(invalid)); }
+ function isValid() { return !(_.size(invalid)); }
+ function request(url, options) {
+ options = options || {};
+ options.headers = {"X-ACF-Auth-Token": token};
+ if (txn) options.headers["X-ACF-Transaction-ID"] = txn;
+ return $.ajax(url, options);
+ }
- function showError(el, msg, xhr) {
- msg += " " + xhr.statusCode().status;
- if (xhr.responseText) msg += ': ' + xhr.responseText;
- el.text(msg);
- }
+ function abort() {
+ var def = request("/", {type: "DELETE"});
+ reset();
+ return def;
+ }
+
+ function objRequest(path, options) {
+ return request("/config" + path, options);
+ }
+
+ return {
+ start: function() {
+ var def = $.Deferred();
+ if (txn && isValid() && !(_.size(changed))) abort();
+
+ if (txn)
+ def.resolve();
+
+ else request("/", {type: "POST"})
+ .done(function(data, status, xhr) {
+ txn = xhr.getResponseHeader("X-ACF-Transaction-ID");
+ def.resolve();
+ })
+ .fail(function() { def.reject(); });
+
+ return def;
+ },
+
+ commit: function() {
+ var def = $.Deferred();
+ request("/", {type: "PUT"}).done(function() {
+ reset();
+ def.resolve();
+ }).fail(function(xhr) { def.reject(xhr); });
+ return def;
+ },
+
+ abort: abort,
+
+ query: function(path) {
+ var def = $.Deferred();
+
+ objRequest(path).done(function(data) {
+ data.get = function(name) {
+ var p = join(path, name);
+ if (_.isArray(data.data)) name--;
+ return p in invalid ?
+ invalid[p][0] : data.data[name];
+ };
+
+ data.status = function(name) {
+ var p = join(path, name);
+ function scan(objs) {
+ return _.size(_.filter(
+ _.keys(objs), function(obj) {
+ return obj == p ||
+ !obj.indexOf(p + "/");
+ }
+ ));
+ }
+
+ if (scan(invalid)) return "invalid";
+ if (scan(changed)) return "changed";
+ return null;
+ }
+
+ data.set = function(name, newValue) {
+ var def = $.Deferred();
+
+ function ensureTask(task) {
+ if (task) return task;
+ task = $.Deferred();
+ task.resolve();
+ return task;
+ }
+
+ var mpath = join(path, name);
+ var value = data.data[name];
+
+ var prevTask;
+ if (mpath in invalid)
+ prevTask = invalid[mpath][1];
+ prevTask = ensureTask(prevTask);
+
+ prevTask.always(function() {
+ var options;
+ if (newValue != null)
+ options = {
+ type: "PUT",
+ data: JSON.stringify(newValue)
+ };
+ else {
+ var savedValue = (mpath in changed) ?
+ changed[mpath][1] : value;
+ if (savedValue != null)
+ options = {type: "DELETE"};
+ }
+
+ var task;
+ if (options)
+ task = objRequest(mpath, options);
+ task = ensureTask(task);
+
+ if (_.isObject(newValue)) newValue = mpath;
+ invalid[mpath] = [newValue, task];
+
+ task.done(function() {
+ if (!(mpath in changed))
+ changed[mpath] = [value];
+ if (newValue == changed[mpath][0])
+ delete changed[mpath];
+ else changed[mpath][1] = newValue;
+
+ delete invalid[mpath];
+
+ def.resolve(isValid());
+
+ }).fail(function(xhr) { def.reject(xhr); });
+ });
+
+ return def;
+ };
+
+ def.resolve(data);
+ }).fail(function() { def.reject(); });
+
+ return def;
+ }
+ };
+ })(xhr.getResponseHeader("X-ACF-Auth-Token"));
+
var Field = {
@@ -152,6 +280,25 @@ $(function() {
}
+ var statusBar = $("#status p");
+ var buttons = $("#status div");
+
+ function setStatus(status, msg, commit) {
+ $("#status").prop("class", status);
+ statusBar.text(msg);
+ $("#commit").prop("disabled", !commit);
+ buttons.prop("class", null);
+ }
+
+ function setErrorStatus(msg) { setStatus("invalid", msg, false); }
+
+ function formatError(msg, xhr) {
+ msg += " " + xhr.statusCode().status;
+ if (xhr.responseText) msg += ': ' + xhr.responseText;
+ return msg;
+ }
+
+
function render() {
var path = $.param.fragment();
@@ -161,41 +308,10 @@ $(function() {
);
}
- function split(path) {
- var res = [];
- while (path && path != "/") {
- var comp = path.match(/^\/([^\\\/]|\\.)+/)[0];
- res.push(comp.substring(1));
- path = path.substring(comp.length);
- }
- return res;
- }
-
- function getStatus(path, data) {
- if (path == "/") path = "";
-
- var res = {};
- _.map(data.data, function(path) { res[path] = ""; });
-
- function scan(objs, label) {
- var level = split(path).length;
- _.map(_.filter(_.keys(objs), function(obj) {
- return !obj.indexOf(path + "/");
- }), function(obj) {
- res[path + "/" + split(obj)[level]] = label;
- });
- }
-
- scan(changed, "changed");
- scan(invalid, "invalid");
-
- return res;
- }
-
function renderMenu(target, path, current, selectFirst) {
var def = $.Deferred();
- objRequest(path).done(function(data) {
+ txnMgr.query(path).done(function(data) {
if (data.meta.type != "model" || _.filter(
data.meta.fields, function(field) {
return !isTreeNode(field);
@@ -205,19 +321,15 @@ $(function() {
return;
}
- var status = getStatus(path, data);
- var first = data.data[data.meta.fields[0].name];
- if (current)
- status[
- (path == "/" ? "" : path) + "/" + current
- ] = "current";
- else if (selectFirst) status[first] = "current";
+ var first = data.meta.fields[0].name;
+ if (!current && selectFirst) current = first;
_.each(data.meta.fields, function(field) {
var el = $("<li>");
var link = Path.format(
- data.data[field.name],
- status[data.data[field.name]]
+ data.get(field.name),
+ field.name == current ?
+ "current" : data.status(field.name)
);
link.text(field["ui-name"]);
el.append(link);
@@ -229,37 +341,37 @@ $(function() {
return def;
}
-
+
var content = $("#content").empty();
function renderContent(path, data) {
content.html($("<h1>").text(data.meta["ui-name"]));
-
+
if (!isTreeNode(data.meta)) {
content.append(JSON.stringify(data));
return;
}
-
- var status = getStatus(path, data);
- var table = $("<table>");
- function join(path, name) {
- if (_.isString(name)) {
- name = name.replace(/([\\\/])/g, "\\$1");
- if (!isNaN(Number(name))) name = "\\" + name;
- }
- return path + "/" + name;
+
+ function validated(txnValid) {
+ if (txnValid)
+ setStatus(
+ "changed",
+ "You have uncommitted changes",
+ true
+ );
}
+
+
+ var table = $("<table>");
- function renderField(
- path, name, value, meta, label, editable
- ) {
- path = join(path, name);
+ function renderField(name, value, meta, label, editable) {
+ var status = data.status(name);
var row = $("<tr>");
row.append($("<td>").text(label));
-
+
var td = $("<td>");
var msg = $("<div>");
@@ -271,114 +383,50 @@ $(function() {
if (editable) {
widget = Object.create(widget);
- widget.init(
- path in invalid ? invalid[path][0] : value,
- meta
- );
+ widget.init(value, meta);
el = widget.el;
el.change(function() {
-
- function ensureTask(task) {
- if (task) return task;
- task = $.Deferred();
- task.resolve();
- return task;
- }
-
- var prevTask;
- if (path in invalid)
- prevTask = invalid[path][1];
- prevTask = ensureTask(prevTask);
-
- prevTask.always(function() {
- var options;
- var newValue = widget.get();
- if (newValue != null)
- options = {
- type: "PUT",
- data: JSON.stringify(newValue)
- };
- else {
- var savedValue = (path in changed) ?
- changed[path][1] : value;
- if (savedValue != null)
- options = {type: "DELETE"};
- }
-
- var task;
- if (options)
- task = objRequest(path, options);
- task = ensureTask(task);
-
- invalid[path] = [newValue, task];
-
- msg.text("[checking]");
- if (
- $("#status").prop("class") != "invalid"
- ) statusBar.text("Validating changes");
-
-
- function setStatus(status, msg, commit) {
- $("#status").prop("class", status);
- statusBar.text(msg);
- $("#commit").prop("disabled", !commit);
- buttons.prop("class", null);
- }
-
- task.done(function() {
- if (!(path in changed))
- changed[path] = [value];
- if (newValue == changed[path][0])
- delete changed[path];
- else changed[path][1] = newValue;
+ msg.text("[checking]");
+ if (
+ $("#status").prop("class") != "invalid"
+ ) statusBar.text("Validating changes");
+
+ data.set(name, widget.get()).done(function(
+ txnValid
+ ) {
+ msg.empty();
+ widget.setStatus(data.status(name));
+ validated(txnValid);
- msg.empty();
- widget.setStatus(
- path in changed ? "changed" : null
- );
-
- delete invalid[path];
- if (isTxnValid())
- setStatus(
- "changed",
- "You have uncommitted changes",
- true
- );
-
- }).fail(function(xhr) {
- if (xhr.statusCode().status == 422)
- msg.html(_.reduce(
- _.map(
- $.parseJSON(
- xhr.responseText
- ),
- _.escape
+ }).fail(function(xhr) {
+ if (xhr.statusCode().status == 422)
+ msg.html(_.reduce(
+ _.map(
+ $.parseJSON(
+ xhr.responseText
),
- function(a, b) {
- return a + "<br/>" + b;
- }
- ));
- else showError(msg, "Error", xhr);
-
- widget.setStatus("invalid");
- setStatus(
- "invalid",
- "Some values need checking",
- false
- );
- });
+ _.escape
+ ),
+ function(a, b) {
+ return a + "<br/>" + b;
+ }
+ ));
+ else msg.text(formatError("Error", xhr));
+
+ widget.setStatus("invalid");
+ setErrorStatus("Some values need checking");
});
});
- if (path in invalid) el.trigger("change");
+ if (status == "invalid") el.trigger("change");
}
- else el = widget.format(value, status[path]);
+ else el = widget.format(value, status);
td.append(el);
- if (editable) widget.setStatus(status[path]);
+ if (editable) widget.setStatus(status);
}
else td.text(value);
@@ -388,9 +436,8 @@ $(function() {
table.append(row);
}
- function renderCollectionMember(path, name, value, meta) {
+ function renderCollectionMember(name, value, meta) {
renderField(
- path,
name,
value,
meta.members,
@@ -402,9 +449,8 @@ $(function() {
if (data.meta.type == "model")
_.each(data.meta.fields, function(field) {
renderField(
- path,
field.name,
- data.data[field.name],
+ data.get(field.name),
field,
field["ui-name"],
true
@@ -412,47 +458,45 @@ $(function() {
});
else _.each(data.data, function(value, name) {
- name = _.isArray(data.data) ? name + 1 : name;
- renderCollectionMember(path, name, value, data.meta);
+ if (_.isArray(data.data)) name++;
+ renderCollectionMember(name, data.get(name), data.meta);
});
content.append(table);
if (_.contains(["collection", "list"], data.meta.type)) {
var len = data.data.length;
+ var keys = _.clone(_.keys(data.data));
var button = $("<input>").attr(
{type: "submit", value: "Insert"}
).click(function() {
-
+
var getter;
function insert() {
+ var name = getter();
- function whenValidated(task) {
- return $.when.apply(
- $, _.pluck(invalid, 1)
- ).always(task);
+ if (_.contains(keys, name)) {
+ button.prop("class", null);
+ return;
}
+ keys.push(name);
- if (isTxnValid()) {
- var name = getter();
- var jpath = join(path, name);
-
- invalid[jpath] = [
- isTreeNode(data.meta.members) ? jpath : null
- ];
+ var tn = isTreeNode(data.meta.members);
+ data.set(
+ name, tn ? {} : null
+ ).done(function(txnValid) {
renderCollectionMember(
- path, name, null, data.meta
+ name,
+ tn ? join(path, name) : null,
+ data.meta
);
-
- whenValidated(function() {
- button.prop("class", null);
- });
- }
- else whenValidated(insert);
+ button.prop("class", null);
+ validated(txnValid);
+ });
}
-
+
button.prop("class", "hidden");
if (data.meta.type == "collection") {
@@ -474,50 +518,49 @@ $(function() {
content.append(button);
}
}
-
+
function renderObj(path) {
- objRequest(path).done(function(data) {
+ txnMgr.query(path).done(function(data) {
renderContent(path, data);
});
}
- var comps = split(path);
- renderMenu($("#modules").empty(), "/", comps[0], false);
- var tabs = $("#tabs").empty();
+ txnMgr.start().done(function() {
+ var comps = split(path);
+ renderMenu($("#modules").empty(), "/", comps[0], false);
+ var tabs = $("#tabs").empty();
- if (path == "/") return;
- var topLevel = comps.length == 1;
-
- renderMenu(tabs, "/" + comps[0], comps[1], true)
- .done(function(first) { renderObj(topLevel ? first : path); })
- .fail(function(data) {
- if (topLevel) renderContent(path, data)
- else renderObj(path);
- });
+ if (path == "/") return;
+ var topLevel = comps.length == 1;
+
+ renderMenu(tabs, "/" + comps[0], comps[1], true)
+ .done(function(first) {
+ renderObj(topLevel ? join(path, first) : path);
+ })
+ .fail(function(data) {
+ if (topLevel) renderContent(path, data)
+ else renderObj(path);
+ });
+ });
}
-
- function newTxn() { return startTxn().done(render); }
-
- function abortTxn() {
- request("/", {type: "DELETE"});
- return newTxn();
+
+ function clearState() {
+ $("#status").prop("class", null);
+ statusBar.empty();
+ buttons.prop("class", "hidden");
+ render();
}
$("#commit").click(function() {
- request("/", {type: "PUT"}).done(newTxn).fail(function(xhr) {
- abortTxn().done(function() {
- showError(statusBar, "Commit failed", xhr);
- });
- });
+ txnMgr.commit().done(clearState).fail(function(xhr) {
+ setErrorStatus(formatError("Commit failed", xhr));
+ })
});
- $("#revert").click(abortTxn);
-
-
- startTxn().done(function() {
- $(window).bind("hashchange", render);
- $.bbq.pushState("#/");
- });
+ $("#revert").click(function() { txnMgr.abort().always(clearState); });
+
+ $(window).bind("hashchange", render);
+ $.bbq.pushState("#/");
});
});