Custom Org Tree : Admin UI
authorBill Erickson <berick@esilibrary.com>
Tue, 20 Mar 2012 19:16:09 +0000 (15:16 -0400)
committerMike Rylander <mrylander@gmail.com>
Mon, 2 Apr 2012 17:39:02 +0000 (13:39 -0400)
New UI for managing custom org unit trees.  (Only OPAC is supported for
now).  Custom trees default to the same shape as the full org unit tree.
Staff can remove nodes and re-order nodes.  Staff can also replace
deleted nodes by dragging them from the reference tree on the left.

Admin -> Server Admin -> Custom Org Unit Trees

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/src/templates/conify/global/actor/org_unit_custom_tree.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/conify/global/actor/org_unit_custom_tree.js [new file with mode: 0644]
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul

diff --git a/Open-ILS/src/templates/conify/global/actor/org_unit_custom_tree.tt2 b/Open-ILS/src/templates/conify/global/actor/org_unit_custom_tree.tt2
new file mode 100644 (file)
index 0000000..9d259b0
--- /dev/null
@@ -0,0 +1,73 @@
+[% WRAPPER base.tt2 %]
+[% ctx.page_title = l('Org Unit Custom Tree') %]
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/conify/global/actor/org_unit_custom_tree.js'> </script>
+<link rel='stylesheet' type='text/css' href='[% ctx.media_prefix %]/js/dojo/dojo/resources/dnd.css'/>
+<link rel='stylesheet' type='text/css' href='[% ctx.media_prefix %]/js/dojo/dojo/resources/dndDefault.css'/>
+
+<style>
+    #wrapper     {width : 100%; height: 100%}
+    .block       {height: 100%; float : left; vertical-align : top; text-align: left; overflow: auto;}
+    #left-block  {border-right: 2px solid #333; margin-right: 20px; padding-right: 40px; max-width 40%}
+    #right-block  {max-width: 58%}
+    .action-wrapper { padding-right: 5px; margin-right: 5px; border-right: 2px dotted #333; }
+    .tree-actions {
+        width : 98%; 
+        padding: 5px; 
+        margin: 5px; 
+        background-color:#E7A555;
+        border-bottom: 2px solid #4A4747;
+    }
+</style>
+
+<h2>[% l('Org Unit Custom Tree') %]</h2>
+<ul>
+    <li>[% l('To add new nodes to the custom tree, drag them from the full tree on the left') %]</li>
+    <li>[% l('Changes to custom org trees may require web server (apache) reload before taking effect') %]</li>
+</ul>
+<hr/>
+
+<div id='wrapper'>
+    <div id='left-block' class='block'>
+        <h3>[% l('Full Org Unit Tree') %]</h3>
+        <div class='tree-actions'>
+            <a href='javascript:;' onClick='realTree.expandAll()'>[% l('Expand') %]</a>&nbsp;/&nbsp;<a 
+                href='javascript:;' onClick='realTree.collapseAll()'>[% l('Collapse') %]</a>
+        </div>
+        <div id='real-tree'></div>
+    </div>
+    <div id='right-block' class='block'>
+        <table>
+            <td style='padding-right: 10px;'>
+                <h3>[% l('Custom Unit Tree: ') %]</h3>
+            </td>
+            <td style='vertical-align: bottom'>
+                <!-- 'opac' is currently the only purpose -->
+                <select jsId='treePurposeSelector' dojoType='dijit.form.FilteringSelect' disabled='disabled' onChange='drawMagicTree()'>
+                    <option value='opac'>[% l('OPAC') %]</option>
+                </select>
+            </td>
+        </tr></table>
+        <div class='tree-actions'>
+            <span class='action-wrapper'>
+                <a href='javascript:;' onClick='magicTree.expandAll()'>[% l('Expand') %]</a>&nbsp;/&nbsp;<a 
+                    href='javascript:;' onClick='magicTree.collapseAll()'>[% l('Collapse') %]</a>
+            </span>
+            <span class='action-wrapper'>
+                <a href='javascript:;' onClick='deleteSelected()'>[% l('Delete Selected') %]</a>
+            </span>
+            <span class='action-wrapper'>
+                <a id='activate-tree'   href='javascript:;' onClick='activateTree()'>[% l('Activate Tree') %]</a>
+                <a id='deactivate-tree' href='javascript:;' onClick='deactivateTree()' class='hidden'>[% l('Deactivate Tree') %]</a>
+            </span>
+            <span>
+                <a href='javascript:;' onClick='applyChanges()'>[% l('Apply Changes') %]</a>
+            </span>
+        </div>
+        <div id='magic-tree'></div>
+    </div>
+</div>
+
+<div jsId="progressDialog" dojoType="openils.widget.ProgressDialog"></div>
+[% END %]
+
+
diff --git a/Open-ILS/web/js/ui/default/conify/global/actor/org_unit_custom_tree.js b/Open-ILS/web/js/ui/default/conify/global/actor/org_unit_custom_tree.js
new file mode 100644 (file)
index 0000000..cc835f7
--- /dev/null
@@ -0,0 +1,284 @@
+dojo.require('dijit.form.Button');
+dojo.require('dijit.form.FilteringSelect');
+dojo.require('dojo.data.ItemFileReadStore');
+dojo.require('dojo.data.ItemFileWriteStore');
+dojo.require('dijit.Tree');
+dojo.require('dijit.tree.TreeStoreModel');
+dojo.require("dijit._tree.dndSource");
+dojo.require('fieldmapper.Fieldmapper');
+dojo.require('fieldmapper.OrgUtils');
+dojo.require('openils.User');
+dojo.require('openils.Util');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.ProgressDialog');
+
+var realTree; // dijit.Tree
+var magicTree; // dijit.Tree
+var mTree; // aouct object
+var pcrud;
+var virtId = -1;
+var realOrgList = [];
+var ctNodes = [];
+
+dojo.declare(
+    'openils.actor.OrgUnitCustomTreeSource', dijit._tree.dndSource, {
+    itemCreator : function(nodes, etc) {
+        var items = this.inherited(arguments);
+        dojo.forEach(items, function(item) {item.shortname = item.name});
+        return items;
+    }
+});
+
+dojo.declare(
+    'openils.actor.OrgUnitCustomTreeStoreModel', dijit.tree.TreeStoreModel, {
+    mayHaveChildren : function(item) { return true },
+});
+
+function drawPage() {
+    pcrud = new openils.PermaCrud({authtoken : openils.User.authtoken});
+
+    // real org unit list.  Not write-able.  Used only as a source.
+    realOrgList = openils.Util.objectValues(
+        fieldmapper.aou.OrgCache).map(function(obj) { return obj.org });
+
+    var store = new dojo.data.ItemFileReadStore(
+        {data : fieldmapper.aou.toStoreData(realOrgList)});
+            
+    var model = new dijit.tree.TreeStoreModel({
+        store: store,
+        query: {_top : 'true'}
+    });
+
+    realTree = new dijit.Tree(
+        {   model: model,
+            expandAll: function() {treeDoAll(this)},
+            collapseAll: function() {treeDoAll(this, true)},
+            dndController : dijit._tree.dndSource,
+            persist : false,
+        }, 
+        'real-tree'
+    );
+
+    realTree.expandAll();
+    drawMagicTree();
+}
+
+
+// composed of org units.  Write-able.
+function drawMagicTree() {
+    var orgList = realOrgList;
+    var query = {_top : 'true'};
+
+    var mTreeRes = pcrud.search('aouct', 
+        {purpose : treePurposeSelector.attr('value')});
+
+    if (mTreeRes.length) {
+        mTree = mTreeRes[0];
+        if (openils.Util.isTrue(mTree.active())) {
+            openils.Util.hide(dojo.byId('activate-tree'));
+            openils.Util.show(dojo.byId('deactivate-tree'), 'inline');
+        }
+        ctNodes = pcrud.search('aouctn', {tree : mTree.id()});
+        if (ctNodes.length) {
+            orgList = [];
+            // create an org tree from the custom tree nodes
+           
+            dojo.forEach(ctNodes, 
+                function(node) {
+                    // deep clone to avoid globalOrgTree clobbering
+                    var org = JSON2js(js2JSON( 
+                        fieldmapper.aou.findOrgUnit(node.org_unit())
+                    ));
+                    org.parent_ou(null);
+                    org.children([]);
+                    if (node.parent_node()) {
+                        org.parent_ou(
+                            ctNodes.filter(
+                                function(n) {return n.id() == node.parent_node()}
+                            )[0].org_unit()
+                        );
+                    }
+                    orgList.push(org);
+                }
+            );
+            var root = ctNodes.filter(function(n) {return n.parent_node() == null})[0];
+            query = {id : root.org_unit()+''}
+        }
+    } else {
+
+        mTree = new fieldmapper.aouct();
+        mTree.isnew(true);
+        mTree.purpose(treePurposeSelector.attr('value'));
+        mTree.active(false);
+    }
+
+    var store = new dojo.data.ItemFileWriteStore(
+        {data : fieldmapper.aou.toStoreData(orgList)});
+
+    var model = new openils.actor.OrgUnitCustomTreeStoreModel({
+        store : store,
+        query : query
+    });
+
+    magicTree = new dijit.Tree(
+        {   model: model,
+            expandAll: function() {treeDoAll(this)},
+            collapseAll: function() {treeDoAll(this, true)},
+            dndController : openils.actor.OrgUnitCustomTreeSource,
+            dragThreshold : 8,
+            betweenThreshold : 5,
+            persist : false,
+        }, 
+        'magic-tree'
+    );
+
+    magicTree.expandAll();
+}
+
+// 1. create the tree if necessary
+// 2. translate the dijit.tree nodes into aouctn's
+// 3. delete the existing aouctn's
+// 4. create the new aouctn's
+function applyChanges() {
+    progressDialog.show();
+
+    if (mTree.isnew()) {
+
+        pcrud.create(mTree, {
+            oncomplete : function(r, objs) {
+                mTree = objs[0];
+                applyChanges2();
+            }
+        });
+    
+    } else {
+        if (ctNodes.length) { 
+            console.log('Deleting ' + ctNodes.length + ' nodes');
+            pcrud.eliminate(ctNodes, {oncomplete : applyChanges2});
+        } else {
+            applyChanges2();
+        }
+    }
+}
+
+function applyChanges2() {
+    ctNodes = [];
+    var newCtNodes = [];
+    var nodeList = [];
+    var sorder = 0;
+    var prevTn;
+    var progress = 0;
+
+    function flatten(node) {
+        nodeList.push(node);
+        dojo.forEach(node.getChildren(), flatten);
+    }
+    flatten(magicTree.rootNode);
+
+    // traverse the nodes, creating new aoucnt's as we go
+    function traverseAndCreate(node) {
+        var item = node.item;
+
+        var tn = new fieldmapper.aouctn();
+        tn.tree(mTree.id());
+        tn.org_unit(item.id[0])
+
+        var pnode = node.getParent();
+        if (pnode) {
+            // find the newly created parent node and extract the ID 
+            var ptn = ctNodes.filter(function(n) {
+                return n.org_unit() == pnode.item.id[0]})[0];
+            tn.parent_node(ptn.id());
+        }
+
+        // if the last node was our previous sibling
+        if (prevTn && prevTn.parent_node() == tn.parent_node()) {
+            tn.sibling_order(++sorder);
+        } else { sorder = 0; }
+
+        console.log("Creating new node for org unit " + tn.org_unit());
+
+        // create the new node, then process the children
+        pcrud.create(tn, {
+            oncomplete : function(r, objs) {
+                var newTn = objs[0];
+                ctNodes.push(newTn);
+                prevTn = newTn;
+                if (nodeList.length == 0) {
+                    progressDialog.hide();
+                    location.href = location.href;
+                } else {
+                    progressDialog.update({maximum : nodeList.length, progress : ++progress});
+                    traverseAndCreate(nodeList.shift());
+                }
+            }
+        });
+    }
+    traverseAndCreate(nodeList.shift());
+}
+
+function deleteSelected() {
+    var toDelete = [];
+
+    function collectChildren(item) {
+        toDelete.push(item);
+        magicTree.model.store.fetch({
+            query : {parent_ou : item.id[0]+''},
+            onComplete : function(list) { 
+                dojo.forEach(list, collectChildren) 
+            }
+        });
+    }
+
+    magicTree.dndController.getSelectedItems().forEach(
+        function(item) {
+            if (item === magicTree.model.root) return
+            collectChildren(item);
+            // delete node plus children, starting at the leaf nodes
+            dojo.forEach(toDelete.reverse(),
+                function(i) {
+                    console.log('Deleting item ' + i.id);
+                    magicTree.model.store.deleteItem(i)
+                }
+            );
+        }
+    );
+}
+
+function activateTree() {
+    mTree.active('t');
+    if (mTree.isnew()) return;
+    pcrud.update(mTree, {
+        oncomplete : function() {
+            openils.Util.hide(dojo.byId('activate-tree'));
+            openils.Util.show(dojo.byId('deactivate-tree'), 'inline');
+        }
+    });
+}
+
+function deactivateTree() {
+    mTree.active('f');
+    if (mTree.isnew()) return;
+    pcrud.update(mTree, {
+        oncomplete : function() {
+            openils.Util.hide(dojo.byId('deactivate-tree'));
+            openils.Util.show(dojo.byId('activate-tree'), 'inline');
+        }
+    });
+}
+
+// modified from 
+// http://stackoverflow.com/questions/2161032/expanding-all-nodes-in-dijit-tree
+function treeDoAll(tree, collapse) {
+    function expand(node) {
+        if (collapse) tree._collapseNode(node);
+        else tree._expandNode(node);
+        var childBranches = dojo.filter(node.getChildren() || [], 
+            function(node) { return node.isExpandable });
+        var def = new dojo.Deferred();
+        defs = dojo.map(childBranches, expand);
+    }
+    return expand(tree.rootNode);
+}
+
+openils.Util.addOnLoad(drawPage);
index 024419d..e9385dd 100644 (file)
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_actor_sip_fields "Actor Stat Cat Sip Fields">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_asset_sip_fields "Asset Stat Cat Sip Fields">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_usr_activity_type "User Activity Types">
+<!ENTITY staff.main.menu.admin.server_admin.conify.actor.org_unit_custom_tree "Custom Org Unit Trees">
 <!ENTITY staff.main.menu.admin.server_admin.conify.global_flag.label "Global Flags">
 <!ENTITY staff.main.menu.admin.server_admin.conify.circulation_limit_group.label "Circulation Limit Groups">
 
index 4ca96d8..113ea93 100644 (file)
@@ -750,6 +750,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/usr_activity_type', null, event); }
             ],
+            'cmd_server_admin_actor_org_unit_custom_tree' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/actor/org_unit_custom_tree', null, event); }
+            ],
             'cmd_local_admin_external_text_editor' : [
                 ['oncommand'],
                 function() {
index bd98d4b..5316bbf 100644 (file)
     <command id="cmd_server_admin_config_usr_activity_type"
              perm="ADMIN_USER_ACTIVITY_TYPE VIEW_USER_ACTIVITY_TYPE"
              />
+    <command id="cmd_server_admin_actor_org_unit_custom_tree"
+             perm="ADMIN_ORG_UNIT_CUSTOM_TREE VIEW_ORG_UNIT_CUSTOM_TREE"
+             />
 
     <command id="cmd_hotkeys_toggle" />
     <command id="cmd_hotkeys_set" />
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_actor_sip_fields;" command="cmd_server_admin_config_actor_sip_fields"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_asset_sip_fields;" command="cmd_server_admin_config_asset_sip_fields"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_usr_activity_type;" command="cmd_server_admin_config_usr_activity_type"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.actor.org_unit_custom_tree;" command="cmd_server_admin_actor_org_unit_custom_tree"/>
                 <menu id="main.menu.admin.server.acq" label="&staff.main.menu.admin.server_admin.acq.label;" accesskey="&staff.main.menu.admin.server_admin.acq.accesskey;">
                     <menupopup id="main.menu.admin.server.acq.popup">
                         <menuitem label="&staff.main.menu.admin.server_admin.acq.fund.label;" accesskey="&staff.main.menu.admin.server_admin.acq.fund.accesskey;" command="cmd_server_admin_acq_fund" />