Initial vmsp tree editor
authorLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Fri, 8 Apr 2011 21:48:57 +0000 (17:48 -0400)
committerBill Erickson <berick@esilibrary.com>
Wed, 6 Jul 2011 18:50:48 +0000 (14:50 -0400)
__ 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

Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm
Open-ILS/src/sql/Pg/012.schema.vandelay.sql
Open-ILS/web/js/dojo/fieldmapper/Fieldmapper.js
Open-ILS/web/js/dojo/openils/vandelay/TreeDndSource.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/vandelay/TreeStoreModel.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/vandelay/nls/match_set.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/vandelay/match_set.js [new file with mode: 0644]
Open-ILS/web/templates/default/vandelay/match_set.tt2 [new file with mode: 0644]

index 4070fa3..c89957b 100644 (file)
@@ -541,10 +541,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
-                               <create permission="CREATE_IMPORT_MATCH_SET ADMIN_IMPORT_IMPORT_MATCH_SET" context_field="owner"/>
-                               <retrieve permission="CREATE_IMPORT_MATCH_SET UPDATE_IMPORT_MATCH_SET DELETE_IMPORT_MATCH_SET" context_field="owner"/>
-                               <update permission="UPDATE_IMPORT_MATCH_SET" context_field="owner"/>
-                               <delete permission="DELETE_IMPORT_MATCH_SET" context_field="owner"/>
+                               <create permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <retrieve permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <update permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <delete permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
                        </actions>
                </permacrud>
        </class>
@@ -558,25 +558,28 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Coded Field" name="svf" reporter:datatype="link"/>
                        <field reporter:label="Tag" name="tag" reporter:datatype="text"/>
                        <field reporter:label="Subfield" name="subfield" reporter:datatype="text"/>
+            <field reporter:label="Negate" name="negate"  reporter:datatype="bool"/>
                        <field reporter:label="Importance" name="quality" reporter:datatype="int"/>
+                       <field reporter:label="Expression Tree Children" name="children" oils_persist:virtual="true" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="parent" reltype="has_a" key="id" map="" class="vmsp"/>
                        <link field="match_set" reltype="has_a" key="id" map="" class="vms"/>
                        <link field="svf" reltype="has_a" key="id" map="" class="crad"/>
+                       <link field="children" reltype="has_many" key="parent" map="" class="vmsp"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
-                               <create permission="CREATE_IMPORT_MATCH_SET ADMIN_IMPORT_IMPORT_MATCH_SET">
+                               <create permission="ADMIN_IMPORT_MATCH_SET">
                     <context link="match_set" field="owner"/>
                                </create>
-                               <retrieve permission="CREATE_IMPORT_MATCH_SET UPDATE_IMPORT_MATCH_SET DELETE_IMPORT_MATCH_SET">
+                               <retrieve permission="ADMIN_IMPORT_MATCH_SET">
                     <context link="match_set" field="owner"/>
                                </retrieve>
-                               <update permission="UPDATE_IMPORT_MATCH_SET">
+                               <update permission="ADMIN_IMPORT_MATCH_SET">
                     <context link="match_set" field="owner"/>
                                </update>
-                               <delete permission="DELETE_IMPORT_MATCH_SET">
+                               <delete permission="ADMIN_IMPORT_MATCH_SET">
                     <context link="match_set" field="owner"/>
                                </delete>
                        </actions>
@@ -598,10 +601,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
-                               <create permission="CREATE_IMPORT_MATCH_SET ADMIN_IMPORT_IMPORT_MATCH_SET" context_field="owner"/>
-                               <retrieve permission="CREATE_IMPORT_MATCH_SET UPDATE_IMPORT_MATCH_SET DELETE_IMPORT_MATCH_SET" context_field="owner"/>
-                               <update permission="UPDATE_IMPORT_MATCH_SET" context_field="owner"/>
-                               <delete permission="DELETE_IMPORT_MATCH_SET" context_field="owner"/>
+                               <create permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <retrieve permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <update permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <delete permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
                        </actions>
                </permacrud>
        </class>
index 5c48282..aa830ad 100644 (file)
@@ -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;
index fd3f818..a9506d1 100644 (file)
@@ -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 (
index bb369c6..df8b73b 100644 (file)
@@ -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 (file)
index 0000000..6438132
--- /dev/null
@@ -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 (file)
index 0000000..61d2821
--- /dev/null
@@ -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 (file)
index 0000000..1b9f01b
--- /dev/null
@@ -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 (file)
index 0000000..7d6a74f
--- /dev/null
@@ -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 = "<strong>N</strong>" + s;
+                        else
+                            s = "<strong>NOT</strong> " + 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() + "<br /><em>" +
+                        (our_crad.description() || "") + "</em><br />";
+                }
+            }
+        );
+        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 (file)
index 0000000..fe5def0
--- /dev/null
@@ -0,0 +1,59 @@
+[% WRAPPER 'default/base.tt2' %]
+[% ctx.page_title = 'Vandelay Match Set' %]
+<style type="text/css">
+    h1 { margin: 0.5em 0; }
+    .outer { clear: both; }
+    #vmsp-buttons button { padding: 0 1.5em; }
+    .node-editor { margin-bottom: 2em; }
+    .node-editor td { padding: 0.5ex; }
+    li { background-color: #ddd; }
+</style>
+<h1>[% ctx.page_title %]</h1>
+<table class="hidden">
+    <tr consistent-controls="1">
+        <td>
+            <label for="quality-input"
+                title="A relative number representing the impact of this expression on the quality of the overall record match"><!-- XXX tooltipize -->
+                Quality:
+            </label>
+        </td>
+        <td>
+            <input id="quality-input" type="text" value="1"
+                size="4" maxlength="3" fmfield="quality" />
+        </td>
+    </tr>
+    <tr consistent-controls="1">
+        <td>
+            <label for="negate-input">Negate?</label>
+        </td>
+        <td>
+            <input id="negate-input" type="checkbox" fmfield="negate" />
+        </td>
+    </tr>
+</table>
+<div class="outer">
+    <div><!-- XXX TODO: consider a read-only display here of the query as built
+        so far from the treet --></div>
+    <div id="vmsp-buttons">
+        <button onclick="node_editor.add('svf');">New Single-Value-Field</button>
+        <button onclick="node_editor.add('tag');">New MARC Tag and Subfield</button>
+        <button onclick="node_editor.add('bool_op');">New Boolean Operator</button>
+    </div>
+</div>
+<div class="outer" style="margin-top: 2ex;">
+    <div style="float: left; width: 49%">
+        <div>
+            <form id="node-editor-container" onsubmit="return false;"></form>
+        </div>
+        <ul id="src_here"></ul>
+    </div>
+
+    <div style="float: right; width: 50%">
+        <div><big>Your Expression</big></div>
+        <div id="tree_here"></div>
+    </div>
+</div>
+<div jsId="progress_dialog" dojoType="openils.widget.ProgressDialog"></div>
+<script type="text/javascript"
+    src="[% ctx.media_prefix %]/js/ui/default/vandelay/match_set.js"></script>
+[% END %]