From: Lebbeous Fogle-Weekley Date: Fri, 8 Apr 2011 21:48:57 +0000 (-0400) Subject: Initial vmsp tree editor X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=6795405f13dead5dff39cf884fa727c01d6f4636;p=contrib%2FConifer.git Initial vmsp tree editor __ notes __ now we can retrieve a tree from the server and use it as the basis of our dijit.Tree widget. Still work to be done. Can't save anything yet. Note to self: borrow dojo dnd's "copy" operation (as opposed to move) to mean replacing a node in the tree, rather than adding to the tree. Re the permissions I changed, actual users of Evergreen hate having as much granularity as there was before, and it just confuses people trying to figure out what perms to give to whom. Note to self 2: add ADMIN_IMPORT_MATCH_SET to ppl Usability 1) the tree editor will only let bool_op nodes have children 2) you can't put the unset "dummy" node from the leftside onto the tree incidentally, gave fm objects a toString method that identifies their classname hint, as an aid to debugging in general --- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 4070fa3526..c89957bc85 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -541,10 +541,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - - - - + + + + @@ -558,25 +558,28 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + - + - + - + - + @@ -598,10 +601,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - - - - + + + + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm index 5c48282809..aa830adb81 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm @@ -1107,5 +1107,34 @@ sub respond_with_status { success_count => $success_count, %args }) if $err or ($try_count % 5 == 0); } +__PACKAGE__->register_method( + api_name => "open-ils.vandelay.match_set.get_tree", + method => "match_set_get_tree", + api_level => 1, + argc => 1 +); + +sub match_set_get_tree { + my ($self, $conn, $authtoken, $match_set_id) = @_; + + $match_set_id = int($match_set_id) or return; + + my $e = new_editor("authtoken" => $authtoken); + $e->checkauth or return $e->die_event; + + my $set = $e->retrieve_vandelay_match_set($match_set_id) or + return $e->die_event; + + $e->allowed("ADMIN_IMPORT_MATCH_SET", $set->owner) or + return $e->die_event; + + my $tree = $e->search_vandelay_match_set_point([ + {"match_set" => $match_set_id, "parent" => undef}, + {"flesh" => -1, "flesh_fields" => {"vmsp" => ["children"]}} + ]) or return $e->die_event; + + return pop @$tree; +} + 1; diff --git a/Open-ILS/src/sql/Pg/012.schema.vandelay.sql b/Open-ILS/src/sql/Pg/012.schema.vandelay.sql index fd3f818bfb..a9506d15d2 100644 --- a/Open-ILS/src/sql/Pg/012.schema.vandelay.sql +++ b/Open-ILS/src/sql/Pg/012.schema.vandelay.sql @@ -23,6 +23,7 @@ CREATE TABLE vandelay.match_set_point ( svf TEXT REFERENCES config.record_attr_definition (name), tag TEXT, subfield TEXT, + negate BOOL DEFAULT FALSE, quality INT NOT NULL DEFAULT 1, -- higher is better CONSTRAINT vmsp_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL), CONSTRAINT vmsp_need_a_tag_or_a_ff_or_a_bo CHECK ( diff --git a/Open-ILS/web/js/dojo/fieldmapper/Fieldmapper.js b/Open-ILS/web/js/dojo/fieldmapper/Fieldmapper.js index bb369c60ac..df8b73bd14 100644 --- a/Open-ILS/web/js/dojo/fieldmapper/Fieldmapper.js +++ b/Open-ILS/web/js/dojo/fieldmapper/Fieldmapper.js @@ -87,6 +87,14 @@ if(!dojo._hasResource["fieldmapper.Fieldmapper"]){ return true; } return; + }, + + toString : function() { + /* ever so slightly aid debugging */ + if (this.classname) + return "[object fieldmapper." + this.classname + "]"; + else + return Object.prototype.toString(); } }); diff --git a/Open-ILS/web/js/dojo/openils/vandelay/TreeDndSource.js b/Open-ILS/web/js/dojo/openils/vandelay/TreeDndSource.js new file mode 100644 index 0000000000..6438132b83 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/vandelay/TreeDndSource.js @@ -0,0 +1,29 @@ +dojo.provide("openils.vandelay.TreeDndSource"); +dojo.require("dijit._tree.dndSource"); + +/* This class specifically serves the eg/vandelay/match_set interface + * for editing Vandelay Match Set trees. It should probably have a more + * specific name that reflects that. + */ +dojo.declare( + "openils.vandelay.TreeDndSource", dijit._tree.dndSource, { + "checkItemAcceptance": function(target, source, position) { + return ( + source._ready && ( + position != "over" || + this.tree.model.mayHaveChildren( + dijit.getEnclosingWidget(target).item + ) + ) + ); + /* code in match_set.js makes sure that source._ready gets set true + * only when we want the item to be draggable */ + }, + "itemCreator": function(nodes) { + var default_items = this.inherited(arguments); + for (var i = 0; i < default_items.length; i++) + default_items[i].match_point = nodes[i].match_point; + return default_items; + } + } +); diff --git a/Open-ILS/web/js/dojo/openils/vandelay/TreeStoreModel.js b/Open-ILS/web/js/dojo/openils/vandelay/TreeStoreModel.js new file mode 100644 index 0000000000..61d28214e4 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/vandelay/TreeStoreModel.js @@ -0,0 +1,55 @@ +dojo.provide("openils.vandelay.TreeStoreModel"); +dojo.require("dijit.tree.TreeStoreModel"); +dojo.require("openils.Util"); + +/* This class specifically serves the eg/vandelay/match_set interface + * for editing Vandelay Match Set trees. It should probably have a more + * specific name that reflects that. + */ + +function _simple_item(model, item) { + /* Instead of model.getLabel(), could do + * model.store.getValue(item, "blah") or something like that ... */ + return { + "label": model.getLabel(item), + "match_point": String(model.store.getValue(item, "match_point")), + "children": {} + }; +} + +dojo.declare( + "openils.vandelay.TreeStoreModel", dijit.tree.TreeStoreModel, { + "getSimpleTree": function(item, oncomplete, result) { + var self = this; + if (!result) result = {}; + + var mykey = this.getIdentity(item); + result[mykey] = _simple_item(this, item); + var child_collector = result[mykey].children; + + if (this.mayHaveChildren(item)) { + this.getChildren( + item, function(children) { + for (var i = 0; i < children.length; i++) { + self.getSimpleTree( + children[i], null, child_collector + ); + } + if (oncomplete) oncomplete(result); + } + ); + } + }, + "mayHaveChildren": function(item) { + var match_point = this.store.getValue(item, "match_point"); + if (match_point) + return openils.Util.isTrue(match_point.bool_op()); + else + return true; + } +// "newItem": function(args, parent) { +// if (!this.mayHaveChildren(parent)) return; +// return this.inherited(arguments); +// } + } +); diff --git a/Open-ILS/web/js/dojo/openils/vandelay/nls/match_set.js b/Open-ILS/web/js/dojo/openils/vandelay/nls/match_set.js new file mode 100644 index 0000000000..1b9f01bca3 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/vandelay/nls/match_set.js @@ -0,0 +1,3 @@ +{ + "DEFINE_MP": "Define this match point using the above fields, then drag me to the tree on the right." +} diff --git a/Open-ILS/web/js/ui/default/vandelay/match_set.js b/Open-ILS/web/js/ui/default/vandelay/match_set.js new file mode 100644 index 0000000000..7d6a74f569 --- /dev/null +++ b/Open-ILS/web/js/ui/default/vandelay/match_set.js @@ -0,0 +1,359 @@ +dojo.require("dijit.Tree"); +dojo.require("dijit.form.Button"); +dojo.require("dojo.data.ItemFileWriteStore"); +//dojo.require("openils.vandelay.DndSource"); +dojo.require("dojo.dnd.Source"); +dojo.require("openils.vandelay.TreeDndSource"); +dojo.require("openils.vandelay.TreeStoreModel"); +dojo.require("openils.CGI"); +dojo.require("openils.User"); +dojo.require("openils.Util"); +dojo.require("openils.PermaCrud"); +dojo.require("openils.widget.ProgressDialog"); + +var localeStrings; +var node_editor; +var _crads; +var CGI; + +function _find_crad_by_name(name) { + for (var i = 0; i < _crads.length; i++) { + if (_crads[i].name() == name) + return _crads[i]; + } + return null; +} + +function NodeEditor() { + var self = this; + + var _svf_select_template = null; + var _factories_by_type = { + "svf": function() { + if (!_svf_select_template) { + _svf_select_template = dojo.create( + "select", {"fmfield": "svf"} + ); + for (var i=0; i<_crads.length; i++) { + dojo.create( + "option", { + "value": _crads[i].name(), + "innerHTML": _crads[i].label() + }, _svf_select_template + ); + } + } + + var select = dojo.clone(_svf_select_template); + dojo.attr(select, "id", "svf-select"); + var label = dojo.create( + "label", { + "for": "svf-select", "innerHTML": "Single-Value-Field:" + } + ); + + var tr = dojo.create("tr"); + dojo.place(label, dojo.create("td", null, tr)); + dojo.place(select, dojo.create("td", null, tr)); + + return [tr]; + }, + "tag": function() { + var rows = [dojo.create("tr"), dojo.create("tr")]; + dojo.create( + "label", { + "for": "tag-input", "innerHTML": "Tag:" + }, dojo.create("td", null, rows[0]) + ); + dojo.create( + "input", { + "id": "tag-input", + "type": "text", + "size": 4, + "maxlength": 3, + "fmfield": "tag" + }, dojo.create("td", null, rows[0]) + ); + dojo.create( + "label", { + "for": "subfield-input", "innerHTML": "Subfield: \u2021" + }, dojo.create("td", null, rows[1]) + ); + dojo.create( + "input", { + "id": "subfield-input", + "type": "text", + "size": 2, + "maxlength": 1, + "fmfield": "subfield" + }, dojo.create("td", null, rows[1]) + ); + return rows; + }, + "bool_op": function() { + var tr = dojo.create("tr"); + dojo.create( + "label", + {"for": "operator-select", "innerHTML": "Operator:"}, + dojo.create("td", null, tr) + ); + var select = dojo.create( + "select", {"fmfield": "bool_op", "id": "operator-select"}, + dojo.create("td", null, tr) + ); + dojo.create("option", {"value": "AND", "innerHTML": "AND"}, select); + dojo.create("option", {"value": "OR", "innerHTML": "OR"}, select); + + return [tr]; + } + }; + + function _simple_value_getter(control) { + if (typeof control.selectedIndex != "undefined") + return control.options[control.selectedIndex].value; + else if (dojo.attr(control, "type") == "checkbox") + return control.checked; + else + return control.value; + }; + + this._init = function(dnd_source, node_editor_container) { + this.dnd_source = dnd_source; + this.node_editor_container = dojo.byId(node_editor_container); + }; + + this.clear = function() { + this.dnd_source.selectAll().deleteSelectedNodes(); + dojo.empty(this.node_editor_container); + }; + + this.update_draggable = function(draggable) { + var s = ""; + draggable.match_point = new vmsp(); + var had_op = false; + dojo.query("[fmfield]", this.node_editor_container).forEach( + function(control) { + var used_svf = null; + var field = dojo.attr(control, "fmfield"); + var value = _simple_value_getter(control); + draggable.match_point[field](value); + + if (field == "subfield") + s += " \u2021"; + if (field == "svf") + used_svf = value; + if (field == "quality") + return; + if (field == "bool_op") + had_op = true; + if (field == "negate") { + if (value) { + if (had_op) + s = "N" + s; + else + s = "NOT " + s; + } + } else { + s += value; + } + + if (used_svf !== null) { + var our_crad = _find_crad_by_name(used_svf); + /* XXX i18n, use fmtted strings */ + s += " / " + our_crad.label() + "
" + + (our_crad.description() || "") + "
"; + } + } + ); + dojo.attr(draggable, "innerHTML", s); + this.dnd_source._ready = true; + }; + + this._add_consistent_controls = function(tgt) { + if (!this._consistent_controls) { + var trs = dojo.query("[consistent-controls]"); + this._consistent_controls = []; + for (var i = 0; i < trs.length; i++) + this._consistent_controls[i] = dojo.clone(trs[i]); + dojo.empty(trs[0].parentNode); + } + + this._consistent_controls.forEach( + function(node) { dojo.place(dojo.clone(node), tgt); } + ); + }; + + this.add = function(type) { + this.clear(); + + /* a representation, not the editing widgets, but will also carry + * the fieldmapper object when dragged to the tree */ + var draggable = dojo.create( + "li", {"innerHTML": localeStrings.DEFINE_MP} + ); + + /* these are the editing widgets */ + var table = dojo.create("table", {"className": "node-editor"}); + + var nodes = _factories_by_type[type](); + for (var i = 0; i < nodes.length; i++) dojo.place(nodes[i], table); + + this._add_consistent_controls(table); + + dojo.create( + "input", { + "type": "submit", "value": "Ok", + "onclick": function() { self.update_draggable(draggable); } + }, dojo.create( + "td", {"colspan": 2, "align": "center"}, + dojo.create("tr", null, table) + ) + ); + + dojo.place(table, this.node_editor_container, "only"); + /* XXX around here attach other data structures to the node */ + this.dnd_source.insertNodes(false, [draggable]); + this.dnd_source._ready = false; + }; + + this._init.apply(this, arguments); +} + +/* XXX replace later with code that will suit this function's purpose + * as well as that of update_draggable. */ +function display_name_from_point(point) { + /* quick and dirty */ + if (point.bool_op()) { + return (point.negate() == "t" ? "N" : "") + point.bool_op(); + } else if (point.svf()) { + return (point.negate() == "t" ? "NOT " : "") + point.svf(); + } else { + return (point.negate() == "t" ? "NOT " : "") + point.tag() + + "\u2021" + point.subfield(); + } +} + +/* dojoize_match_set_tree() takes an argument, "point", that is actually a + * vmsp fieldmapper object with descendants fleshed hierarchically. It turns + * that into a syntactically flat array but preserving the hierarchy + * semantically in the language used by dojo data stores, i.e., + * + * [ + * {'id': 'root', children:[{'_reference': '0'}, {'_reference': '1'}]}, + * {'id': '0', children:[]}, + * {'id': '1', children:[]} + * ], + * + */ +function dojoize_match_set_tree(point, refgen) { + /* XXX TODO test with deeper trees! */ + var root = false; + if (!refgen) { + refgen = 0; + root = true; + } + + var bathwater = point.children(); + point.children([]); + var item = { + "id": (root ? "root" : refgen), + "name": display_name_from_point(point), + "match_point": point.clone(), + "children": [] + }; + point.children(bathwater); + + var results = [item]; + + if (point.children()) { + for (var i = 0; i < point.children().length; i++) { + var child = point.children()[i]; + item.children.push({"_reference": ++refgen}); + results = results.concat( + dojoize_match_set_tree(child, refgen) + ); + } + } + + return results; +} + +function init_test() { + progress_dialog.show(true); + + dojo.requireLocalization("openils.vandelay", "match_set"); + localeStrings = dojo.i18n.getLocalization("openils.vandelay", "match_set"); + + CGI = new openils.CGI(); + + /* XXX No-one should have hundreds of these or anything, but theoretically + * this could be problematic with a big enough list of crad objects. */ + _crads = new openils.PermaCrud().retrieveAll( + "crad", {"order_by": {"crad": "label"}} + ); + + var match_set_tree = fieldmapper.standardRequest( + ["open-ils.vandelay", "open-ils.vandelay.match_set.get_tree"], + [openils.User.authtoken, CGI.param("match_set")] + ); + +// { +// "identifier": "id", "label": "name", "items": [ +// { +// "id": "root", "name": "AND", +// "children": [ +// {"_reference": "leaf0"}, {"_reference": "leaf1"} +// ] +// }, +// {"id": "leaf0", "name": "nonsense test"}, +// {"id": "leaf1", "name": "more nonsense"} +// ] +// } + + var store = new dojo.data.ItemFileWriteStore({ + "data": { + "identifier": "id", + "label": "name", + "items": dojoize_match_set_tree(match_set_tree) + } + }); + + var treeModel = new openils.vandelay.TreeStoreModel({ + store: store, "query": {"id": "root"} + }); + + var src = new dojo.dnd.Source("src_here"); + var tree = new dijit.Tree( + { + "model": treeModel, + "dndController": openils.vandelay.TreeDndSource, + "dragThreshold": 8, + "betweenThreshold": 5, + "persist": false + }, "tree_here" + ); + + node_editor = new NodeEditor(src, "node-editor-container"); + + dojo.connect( + src, "onDndDrop", null, + function(source, nodes, copy, target) { + if (source == this) { + var model = target.tree.model; + model.getRoot( + function(root) { + model.getSimpleTree( + root, function(results) { alert(js2JSON(results)); } + ); + } + ); + node_editor.clear(); /* because otherwise this acts like a copy! */ + } else { + alert("XXX [src] nodes length is " + nodes.length); /* XXX DEBUG */ + } + } + ); + progress_dialog.hide(); +} + +openils.Util.addOnLoad(init_test); diff --git a/Open-ILS/web/templates/default/vandelay/match_set.tt2 b/Open-ILS/web/templates/default/vandelay/match_set.tt2 new file mode 100644 index 0000000000..fe5def0109 --- /dev/null +++ b/Open-ILS/web/templates/default/vandelay/match_set.tt2 @@ -0,0 +1,59 @@ +[% WRAPPER 'default/base.tt2' %] +[% ctx.page_title = 'Vandelay Match Set' %] + +

[% ctx.page_title %]

+ + + + + + + + + + +
+
+
+ + + +
+
+
+
+
+
+
+
    +
    + +
    +
    Your Expression
    +
    +
    +
    +
    + +[% END %]