diff options
author | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2013-05-31 23:11:04 +0300 |
---|---|---|
committer | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2013-05-31 23:42:02 +0300 |
commit | 6659899825b9f56e636771715d06b899823f7895 (patch) | |
tree | 9ab1d9ae023fecdd03e115289033443a40ff9463 /web | |
parent | bf259b67a6287e46d2bfb07dec08b6a8a9b2b690 (diff) | |
download | aconf-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.js | 525 |
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("#/"); }); }); |