Implemented tag editing; added modal for delete operation. * Closes #11.
h3rald h3rald@h3rald.com
Sat, 11 Apr 2015 19:57:01 +0200
6 files changed,
131 insertions(+),
64 deletions(-)
M
admin/js/models.js
→
admin/js/models.js
@@ -65,4 +65,37 @@ serialize: function(data){return data;},
config: xhrcfg }); }; + + Doc.patch = function(id, updatedTags){ + console.log(updatedTags); + return Doc.get(id).then(function(doc){ + var tags = doc.tags; + console.log(tags); + var count = 0; + var ops = []; + tags.forEach(function(tag){ + if (updatedTags[count]){ + if (updatedTags[count] != tag){ + // update tag + ops.push({"op": "replace", "path": "/tags/"+count, "value": updatedTags[count]}); + } + } else { + // delete tag + ops.push({"op": "remove", "path": "/tags/"+count}); + } + count++; + }); + if (updatedTags.length > tags.length) { + for (i = tags.length; i< updatedTags.length; i++){ + // add tag + ops.push({"op": "add", "path": "/tags/"+i, "value": updatedTags[i]}); + } + } + return m.request({ + method: "PATCH", + url: "/v1/docs/"+id, + data: ops + }); + }); + }; }());
M
admin/js/modules/document.js
→
admin/js/modules/document.js
@@ -13,27 +13,25 @@ vm.action = m.route.param("action");
vm.readOnly = true; vm.contentType = m.prop(""); vm.updatedTags = m.prop(""); + vm.content = ""; + vm.tags = []; try { vm.ext = vm.id().match(/\.(.+)$/)[1]; } catch(e) { vm.ext = ""; } + + // Retrieve single document & populate vm.getDoc = function(cb){ vm.doc = Doc.get(vm.id()); vm.doc.then(function(doc){ vm.content = doc.data; vm.tags = doc.tags; + vm.updatedTags(vm.tags.filter(function(t){return !/^\$/.test(t)}).join(", ")); }, vm.flashError); }; - vm.viewDocument = function(){ - if (vm.ext === "md" && vm.id().match(new Regexp("^"+vm.dir+"\/md\/"))) { - // If editing a documentation page, go back to the guide. - m.route("/guide/"+vm.id().replace(/\.md$/, "").replace(new Regexp("^"+vm.dir+"\/md\/"), "")); - } else { - m.route("/document/view/"+vm.id()); - } - }; - vm.tags = []; + + // Reset some properties based on action switch (vm.action) { case 'create': vm.readOnly = false;@@ -47,26 +45,25 @@ case 'view':
vm.getDoc(); break; } - vm.editTagsDialogCfg = { - title: "Edit Tags", - id: "edit-tags-modal", - action: function(){ - // TODO - console.log(vm.updatedTags()); - }, - actionText: "Update", - content: m("input", { - type: "text", - class:"form-control", - onchange: m.withAttr("value", vm.updatedTags), - placeholder: "Enter comma-separated tags..." - }) + + // View document in editor + vm.viewDocument = function(){ + if (vm.ext === "md" && vm.id().match(new RegExp("^"+vm.dir+"\/md\/"))) { + // If editing a documentation page, go back to the guide. + m.route("/guide/"+vm.id().replace(/\.md$/, "").replace(new Regexp("^"+vm.dir+"\/md\/"), "")); + } else { + m.route("/document/view/"+vm.id()); + } }; + + // Set current document editable vm.edit = function(){ vm.editor.setReadOnly(false); vm.action = "edit"; vm.flash(""); }; + + // Save document vm.save = function(){ var doc = {}; doc.id = vm.id();@@ -79,7 +76,7 @@ vm.viewDocument();
}, vm.flashError); }; if (vm.action === "create") { - doc.id = vm.dir+"/"+vm.id(); + doc.id = vm.id(); vm.id(doc.id); Doc.get(doc.id) .then(function(){@@ -91,17 +88,16 @@ } else {
put(); } }; + + // Delete Document vm.delete = function(){ - var msg = "Do you want to delete document '"+vm.id()+"'?"; - if (confirm(msg)) { - Doc.delete(vm.id()).then(function(){ - LS.flash({type: "success", content: "Document '"+vm.id()+"' deleted successfully."}); - m.route("/info"); - }, vm.flashError); - } else { - m.route("/document/view/"+vm.id()); - } + Doc.delete(vm.id()).then(function(){ + LS.flash({type: "success", content: "Document '"+vm.id()+"' deleted successfully."}); + m.route("/info"); + }, vm.flashError); }; + + // Cancel editing vm.cancel = function(){ if (vm.action === "create"){ m.route("/info");@@ -109,6 +105,21 @@ } else {
vm.viewDocument(); } }; + + // Patch document (update tags) + vm.patch = function(){ + var sysTags = vm.tags.filter(function(t){return /^\$/.test(t)}); + var newTags = sysTags.concat(vm.updatedTags().split(/,\s*/)); + Doc.patch(vm.id(), newTags).then(function(){ + LS.flash({type: "success", content: "Tags for document '"+vm.id()+"' updated successfully."}); + Info.get().then(function(info){ + app.system = info; + vm.viewDocument(); + }); + }, vm.flashError); + }; + + // Populate tools based on current action vm.tools = function(){ if (app.system.read_only) { return [];@@ -121,8 +132,8 @@ switch (vm.action){
case "view": return [ {title: "Edit Content", icon: "edit", action: vm.edit}, - {title: "Edit Tags", icon: "tags", action: function(){$("#edit-tags-modal").modal()}}, - {title: "Delete", icon: "trash", action: vm.delete} + {title: "Edit Tags", icon: "tags", action: u.showModal("#edit-tags-modal")}, + {title: "Delete", icon: "trash", action: u.showModal("#delete-document-modal")} ]; default: return [@@ -132,12 +143,43 @@ ];
} }; }; + + // Module main view app.document.main = function(){ var vm = app.document.vm; var titleLeft = vm.id(); var titleRight = m("span.pull-right", vm.tags.map(function(t){return u.taglink(t);})); + // Delete confirmation dialog + var deleteDialogCfg = { + title: "Delete Document", + id: "delete-document-modal", + action: vm.delete, + actionText: "Delete", + content: m("p", "Do you want to delete document '"+vm.id()+"'?") + }; + // Configuration for the Edit Tags dialog + var editTagsDialogCfg = { + title: "Add/Edit User Tags", + id: "edit-tags-modal", + action: vm.patch, + actionText: "Update", + content: m("div", [ + m("input", { + type: "text", + class:"form-control", + onchange: m.withAttr("value", vm.updatedTags), + value: vm.updatedTags(), + placeholder: "Enter comma-separated tags..." + }), + m("div.tip", [ + m("p", "Tip") , + m("p", "Each user tag can contain letters, numbers, and any of the following special characters:"), + m("p", "?, ~, :, ., @, #, ^, !, +, _, or -") + ]) + ]) + }; if (vm.action === "create"){ - titleLeft = m("span", [vm.dir+"/",m("input", { + titleLeft = m("span", [m("input", { placeholder: "Document ID", onchange: m.withAttr("value", function(value){ vm.id(value);@@ -155,11 +197,12 @@ })]);
} var title = m("span",[titleLeft, titleRight]); return m("div", [ - u.modal(vm.editTagsDialogCfg), + u.modal(deleteDialogCfg), + u.modal(editTagsDialogCfg), m(".row", [u.toolbar({links: vm.tools()})]), m(".row", [u.panel({title: title, content:app.editor.view(vm)})]) ]); }; - u.layout(app.document); + u.layout(app.document); }());
M
admin/js/utils.js
→
admin/js/utils.js
@@ -165,7 +165,8 @@ ]);
}; u.taglink = function(tag) { - return m("span.label.label-primary", + var color = /^\$/.test(tag) ? "warning" : "primary"; + return m("span.tag-label.label.label-"+color, [m("i.fa.fa-tag"), " ", m("a", {href: "/tags/"+tag, config:m.route}, tag)]); };@@ -204,13 +205,9 @@ return "";
} }; - /** - * obj: - * - id - */ - u.showModal = function(id){ - return function(el, isInitialized){ - $(id).modal(); + u.showModal = function(sel){ + return function(){ + $(sel).modal(); }; };@@ -236,7 +233,7 @@ ]),
m(".modal-body", [obj.content]), m(".modal-footer", [ m("button.btn.btn-default[data-dismiss='modal']", "Close"), - m("button.btn.btn-default[data-dismiss='modal']", {onclick: obj.action}, obj.actionText) + m("button.btn.btn-primary[data-dismiss='modal']", {onclick: obj.action}, obj.actionText) ]) ]) ])
M
admin/styles/elements.less
→
admin/styles/elements.less
@@ -122,7 +122,7 @@ span.ext, span.kwd {
font-weight: bold; } -span.label-primary { +span.tag-label { margin: auto 2px; font-weight: normal; &:hover {
M
admin/styles/litestore.css
→
admin/styles/litestore.css
@@ -1867,14 +1867,14 @@ span.ext,
span.kwd { font-weight: bold; } -span.label-primary { +span.tag-label { margin: auto 2px; font-weight: normal; } -span.label-primary a { +span.tag-label a { color: #efefef; } -span.label-primary a:hover { +span.tag-label a:hover { color: #fff; text-decoration: none; }
M
lib/utils.nim
→
lib/utils.nim
@@ -1,10 +1,6 @@
import json, db_sqlite, strutils, pegs, asyncdispatch, asynchttpserver2, times, logging, math, sqlite3 import types, queries, contenttypes -proc dbg*(args: varargs[string, `$`]) = - if logging.level <= lvlDebug: - echo "DEBUG - "&args.join(" ") - proc dbQuote*(s: string): string = result = "'" for c in items(s):@@ -48,7 +44,7 @@ if options.limit > 0:
result = result & "LIMIT " & $options.limit & " " if options.offset > 0: result = result & "OFFSET " & $options.offset & " " - dbg(result) + debug(result.replace("$", "$$")) proc prepareSelectTagsQuery*(options: QueryOptions): string = result = "SELECT tag_id, COUNT(document_ID) "@@ -60,7 +56,7 @@ if options.orderby.len > 0:
result = result & "ORDER BY " & options.orderby&" " if options.limit > 0: result = result & "LIMIT " & $options.limit & " " - dbg(result) + debug(result.replace("$", "$$")) proc prepareJsonDocument*(store:Datastore, doc: TRow, cols:seq[string]): JsonNode = var raw_tags = store.db.getAllRows(SQL_SELECT_DOCUMENT_TAGS, doc[0])@@ -83,11 +79,9 @@ res.add(("tags", %tags))
return %res proc toPlainText*(s: string): string = - let subs = @[ - (pattern: peg"""\<\/?[^<>]+\>""", repl: ""), - (pattern: peg"""{[_*/+!=?%$^-]+} {(!$1 .)+} $1""", repl: "$2") - ] - return s.parallelReplace(subs) + var tags = peg"""'<' [^<>]+ '>'""" + var markup = peg"""{[_*/+!=?%$^~]+} {(!$1 .)+} $1""" + return s.replace(tags).replacef(markup, "$2") proc checkIfBinary*(binary:int, contenttype:string): int = if binary == -1 and contenttype.isBinary:@@ -116,9 +110,9 @@ stderr.writeln(msg)
quit(code) proc resError*(code: HttpCode, message: string, trace = ""): Response = - warn(message) + warn(message.replace("$", "$$")) if trace.len > 0: - dbg(trace) + debug(trace.replace("$", "$$")) result.code = code result.content = """{"error":"$1"}""" % message result.headers = ctJsonHeader()