From 26f1217aab212750d5f5ef8447a835119ae0cefd Mon Sep 17 00:00:00 2001 From: Lebbeous Fogle-Weekley Date: Fri, 18 May 2012 12:12:55 -0400 Subject: [PATCH] Trigger Event Log A better, more browsy/filtery way to browse Action Trigger Events related to holds and circs, which staff users sometimes want to filter by patron barcode. This is accessed from various menus around the staff client, notably the patron interface and the item status interface. This has better printing capabilities than previous versions of this interface, too. There's also a new org unit setting to go with this, "circ.staff.max_visible_event_age", which if set, should hide any events older than the configured age. One new permission: VIEW_TRIGGER_EVENT. This does what it sounds like it does. Signed-off-by: Lebbeous Fogle-Weekley Signed-off-by: Bill Erickson Conflicts (resolved): Open-ILS/src/sql/Pg/950.data.seed-values.sql Open-ILS/web/js/dojo/openils/widget/PCrudFilterDialog.js Signed-off-by: Mike Rylander --- Open-ILS/examples/fm_IDL.xml | 85 +- Open-ILS/src/sql/Pg/950.data.seed-values.sql | 35 +- .../XXXX.data.actor-event-log-settings.sql | 46 ++ .../src/templates/actor/user/event_log.tt2 | 282 +++++++ .../templates/actor/user/trigger_events.tt2 | 29 - Open-ILS/web/css/skin/default.css | 7 + .../openils/widget/FlattenerFilterDialog.js | 56 +- .../openils/widget/FlattenerFilterPane.js | 57 ++ .../js/dojo/openils/widget/FlattenerGrid.js | 89 ++- .../dojo/openils/widget/PCrudFilterDialog.js | 633 +-------------- .../js/dojo/openils/widget/PCrudFilterPane.js | 756 ++++++++++++++++++ ...CrudFilterDialog.js => PCrudFilterPane.js} | 1 + .../ui/default/actor/user/trigger_events.js | 87 -- .../chrome/content/main/constants.js | 4 +- .../staff_client/server/circ/copy_status.js | 6 +- .../xul/staff_client/server/patron/display.js | 6 +- .../xul/staff_client/server/patron/items.js | 12 +- .../server/patron/trigger_events.js | 234 ------ .../server/patron/trigger_events.xul | 88 -- 19 files changed, 1362 insertions(+), 1151 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.data.actor-event-log-settings.sql create mode 100644 Open-ILS/src/templates/actor/user/event_log.tt2 delete mode 100644 Open-ILS/src/templates/actor/user/trigger_events.tt2 create mode 100644 Open-ILS/web/js/dojo/openils/widget/FlattenerFilterPane.js create mode 100644 Open-ILS/web/js/dojo/openils/widget/PCrudFilterPane.js rename Open-ILS/web/js/dojo/openils/widget/nls/{PCrudFilterDialog.js => PCrudFilterPane.js} (92%) delete mode 100644 Open-ILS/web/js/ui/default/actor/user/trigger_events.js delete mode 100644 Open-ILS/xul/staff_client/server/patron/trigger_events.js delete mode 100644 Open-ILS/xul/staff_client/server/patron/trigger_events.xul diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 7aabb798ff..9c0ce4af9c 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -1101,6 +1101,84 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + NOW() - (SELECT MAX(value) FROM ( + SELECT value::INTERVAL FROM actor.org_unit_ancestor_setting( + 'circ.staff.max_visible_event_age', + COALESCE(targ_circ.circ_lib, targ_ahr.pickup_lib) + ) UNION + SELECT '1000 YEARS'::INTERVAL AS value + ) ous) + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3442,7 +3520,7 @@ SELECT usr, - + @@ -3509,6 +3587,11 @@ SELECT usr, + + + + + diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index d988a09e91..c41aa9df39 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -1557,7 +1557,10 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES ( 533, 'ADMIN_COPY_LOCATION_GROUP', oils_i18n_gettext( 533, 'Allows a user to create/retrieve/update/delete copy location groups', 'ppl', 'description' )), ( 534, 'ADMIN_USER_ACTIVITY_TYPE', oils_i18n_gettext( 534, - 'Allows a user to create/retrieve/update/delete user activity types', 'ppl', 'description' )); + 'Allows a user to create/retrieve/update/delete user activity types', 'ppl', 'description' )), +( 535, 'VIEW_TRIGGER_EVENT', oils_i18n_gettext( 535, + 'Allows a user to view circ- and hold-related action/trigger events', 'ppl', 'description')) +; SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000); @@ -10175,7 +10178,24 @@ INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,dat 'description' ), 'string' -); +), ( + 'ui.grid_columns.actor.user.event_log', + 'gui', + FALSE, + oils_i18n_gettext( + 'ui.grid_columns.actor.user.event_log', + 'User Event Log', + 'cust', + 'label' + ), + oils_i18n_gettext( + 'ui.grid_columns.actor.user.event_log', + 'User Event Log Saved Column Settings', + 'cust', + 'description' + ), + 'string' +) ; SELECT setval( 'config.sms_carrier_id_seq', 1000 ); INSERT INTO config.sms_carrier VALUES @@ -11695,3 +11715,14 @@ VALUES ( ), 'integer' ); + +INSERT INTO config.org_unit_setting_type ( + name, grp, label, description, datatype +) VALUES ( + 'circ.staff.max_visible_event_age', + 'circ', + 'Maximum visible age of User Trigger Events in Staff Interfaces', + 'If this is unset, staff can view User Trigger Events regardless of age. When this is set to an interval, it represents the age of the oldest possible User Trigger Event that can be viewed.', + 'interval' +); + diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.actor-event-log-settings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.actor-event-log-settings.sql new file mode 100644 index 0000000000..17a6d9b94a --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.actor-event-log-settings.sql @@ -0,0 +1,46 @@ +BEGIN; + +SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version); + +INSERT INTO config.org_unit_setting_type ( + name, label, grp, description, datatype +) VALUES ( + 'circ.staff.max_visible_event_age', + 'Maximum visible age of User Trigger Events in Staff Interfaces', + 'circ', + 'If this is unset, staff can view User Trigger Events regardless of age. When this is set to an interval, it represents the age of the oldest possible User Trigger Event that can be viewed.', + 'interval' +); + +INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,datatype) VALUES ( + 'ui.grid_columns.actor.user.event_log', + 'gui', + FALSE, + oils_i18n_gettext( + 'ui.grid_columns.actor.user.event_log', + 'User Event Log', + 'cust', + 'label' + ), + oils_i18n_gettext( + 'ui.grid_columns.actor.user.event_log', + 'User Event Log Saved Column Settings', + 'cust', + 'description' + ), + 'string' +); + +INSERT INTO permission.perm_list ( id, code, description ) + VALUES ( + 535, + 'VIEW_TRIGGER_EVENT', + oils_i18n_gettext( + 535, + 'Allows a user to view circ- and hold-related action/trigger events', + 'ppl', + 'description' + ) + ); + +COMMIT; diff --git a/Open-ILS/src/templates/actor/user/event_log.tt2 b/Open-ILS/src/templates/actor/user/event_log.tt2 new file mode 100644 index 0000000000..2756c7b4a0 --- /dev/null +++ b/Open-ILS/src/templates/actor/user/event_log.tt2 @@ -0,0 +1,282 @@ +[% WRAPPER base.tt2 %] +[% ctx.page_title = "Triggered Event Log" %] + +
+
+
+ [% ctx.page_title %] + + +
+
+ + + + +
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + +
Event NameTarget Circulation - Copy BarcodeTarget Circulation - TitleTarget Circulation - AuthorTarget Circulation - Patron BarcodeTarget Hold - Patron Barcode
+
+
+
+
+
+
+
+
+[% END %] diff --git a/Open-ILS/src/templates/actor/user/trigger_events.tt2 b/Open-ILS/src/templates/actor/user/trigger_events.tt2 deleted file mode 100644 index ec0c2f4c59..0000000000 --- a/Open-ILS/src/templates/actor/user/trigger_events.tt2 +++ /dev/null @@ -1,29 +0,0 @@ -[% ctx.page_title = 'Events' %] -[% WRAPPER base.tt2 %] - - - -
-
User Events
-
- -
-
- - -
- - - - - - - - - - - -
Event Def.HookReactorValidatorTargetState
-
- -[% END %] diff --git a/Open-ILS/web/css/skin/default.css b/Open-ILS/web/css/skin/default.css index 209832950a..af96f071e7 100644 --- a/Open-ILS/web/css/skin/default.css +++ b/Open-ILS/web/css/skin/default.css @@ -92,6 +92,13 @@ table { border-collapse: collapse; } } .oils-fm-edit-dialog td { border:1px solid #999;} .oils-pcrudfilterdialog-table tr td { padding: 0.75ex 0.5em; } +.oils-pcrudfilterdialog-table tr:nth-child(event) { + background-color: #d7d7d7; +} +.oils-pcrudfilterdialog-remover-holder { + text-align: left; vertical-align: middle; + width: 33%; +} .oils-pcrudfilterdialog-remover { background-color: #ccc; color: #f00; padding: 0.25em; border: 1px outset #000; diff --git a/Open-ILS/web/js/dojo/openils/widget/FlattenerFilterDialog.js b/Open-ILS/web/js/dojo/openils/widget/FlattenerFilterDialog.js index af6b7df181..187b69f00f 100644 --- a/Open-ILS/web/js/dojo/openils/widget/FlattenerFilterDialog.js +++ b/Open-ILS/web/js/dojo/openils/widget/FlattenerFilterDialog.js @@ -1,57 +1,11 @@ if (!dojo._hasResource["openils.widget.FlattenerFilterDialog"]) { - dojo._hasResource["openils.widget.FlattenerFilterDialog"] = true; - dojo.provide("openils.widget.FlattenerFilterDialog"); - dojo.require("openils.widget.PCrudFilterDialog"); + dojo.require("openils.widget.FlattenerFilterPane"); + dojo.require("dijit.Dialog"); dojo.declare( - "openils.widget.FlattenerFilterDialog", - [openils.widget.PCrudFilterDialog], { - "mapTerminii": null, - - "constructor": function(args) { - dojo.mixin(this, args); - }, - - "_buildFieldStore": function() { - var self = this; - - if (!this.mapTerminii) - throw new Error("No mapTerminii list; can't proceed"); - - var realFieldList = dojo.clone(this.mapTerminii).filter( - function(o) { - if (self.suppressFilterFields && - dojo.indexOf( - self.suppressFilterFields, o.simple_name - ) >= -1 - ) { - return false; - } - - return o.isfilter; - } - ); - - this.fieldStore = new dojo.data.ItemFileReadStore({ - "data": { - "identifier": "simple_name", - "name": "label", - "items": realFieldList.map( - function(item) { - return { - "label": item.label, - "name": item.name, - "type": item.datatype, - "fmClass": item.fmClass, - "simple_name": item.simple_name, - "indirect": item.indirect - }; - } - ) - } - }); - } - } + "openils.widget.FlattenerFilterDialog", [ + dijit.Dialog, openils.widget.FlattenerFilterPane + ] ); } diff --git a/Open-ILS/web/js/dojo/openils/widget/FlattenerFilterPane.js b/Open-ILS/web/js/dojo/openils/widget/FlattenerFilterPane.js new file mode 100644 index 0000000000..5b74a999bf --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/widget/FlattenerFilterPane.js @@ -0,0 +1,57 @@ +if (!dojo._hasResource["openils.widget.FlattenerFilterPane"]) { + dojo._hasResource["openils.widget.FlattenerFilterPane"] = true; + + dojo.provide("openils.widget.FlattenerFilterPane"); + dojo.require("openils.widget.PCrudFilterPane"); + + dojo.declare( + "openils.widget.FlattenerFilterPane", + [openils.widget.PCrudFilterPane], { + "mapTerminii": null, + + "constructor": function(args) { + dojo.mixin(this, args); + }, + + "_buildFieldStore": function() { + var self = this; + + if (!this.mapTerminii) + throw new Error("No mapTerminii list; can't proceed"); + + var realFieldList = dojo.clone(this.mapTerminii).filter( + function(o) { + if (self.suppressFilterFields && + dojo.indexOf( + self.suppressFilterFields, o.simple_name + ) >= -1 + ) { + return false; + } + + return o.isfilter; + } + ); + + this.fieldStore = new dojo.data.ItemFileReadStore({ + "data": { + "identifier": "simple_name", + "name": "label", + "items": realFieldList.map( + function(item) { + return { + "label": item.label, + "name": item.name, + "type": item.datatype, + "fmClass": item.fmClass, + "simple_name": item.simple_name, + "indirect": item.indirect + }; + } + ) + } + }); + } + } + ); +} diff --git a/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js b/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js index 769178962e..9a350c52ba 100644 --- a/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js +++ b/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js @@ -18,10 +18,16 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { "columnPersistKey": null, "autoCoreFields": false, "autoFieldFields": null, - "showLoadFilter": false, /* use FlattenerFilterDialog */ + "showLoadFilter": false, /* use FlattenerFilter(Dialog|Pane) */ + "filterAlwaysInDiv": null, /* use FlattenerFilterPane and put its + content in this HTML element */ "fetchLock": false, + "filterInitializers": null, + "filterWidgetBuilders": null, + "filterSemaphore": null, + "filterSemaphoreCallback": null, - /* These potential constructor arguments maybe useful to + /* These potential constructor arguments may be useful to * FlattenerGrid in their own right, and are passed to * FlattenerStore. */ "fmClass": null, @@ -346,7 +352,6 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { c.fpath.match(wbp_re); } ).length) { - console.info("adding auto field" + would_be_path); self.structure[0].cells[0].push({ "field": "AUTO_" + beginning.name + "_" + field.name, @@ -479,14 +484,21 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { dojo.place(this.linkHolder.domNode, this.domNode, "before"); if (this.showLoadFilter) { - dojo.require("openils.widget.FlattenerFilterDialog"); - this.filterDialog = - new openils.widget.FlattenerFilterDialog({ + var which_filter_ui = this.filterAlwaysInDiv ? + "FlattenerFilterPane" : "FlattenerFilterDialog"; + + dojo.require("openils.widget." + which_filter_ui); + this.filterUi = + new openils.widget[which_filter_ui]({ "fmClass": this.fmClass, - "mapTerminii": this.mapTerminii + "mapTerminii": this.mapTerminii, + "useDiv": this.filterAlwaysInDiv, + "compact": true, + "initializers": this.filterInitializers, + "widgetBuilders": this.filterWidgetBuilders }); - this.filterDialog.onApply = dojo.hitch( + this.filterUi.onApply = dojo.hitch( this, function(filter) { this.filter( dojo.mixin(filter, this._baseQuery), @@ -495,16 +507,23 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { } ); - this.filterDialog.startup(); - dojo.create( - "a", { - "innerHTML": "Filter", /* XXX i18n */ - "href": "javascript:void(0);", - "onclick": dojo.hitch(this, function() { - this.filterDialog.show(); - }) - }, this.linkHolder.domNode - ); + this.filterUi.startup(); + + if (this.filterSemaphore && this.filterSemaphore()) { + if (this.filterSemaphoreCallback) + this.filterSemaphoreCallback(); + } + if (!this.filterAlwaysInDiv) { + dojo.create( + "a", { + "innerHTML": "Filter", /* XXX i18n */ + "href": "javascript:void(0);", + "onclick": dojo.hitch(this, function() { + this.filterUi.show(); + }) + }, this.linkHolder.domNode + ); + } } }, @@ -832,15 +851,24 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { ); }, + "getSelectedIDs": function() { + return this.getSelectedItems().map( + dojo.hitch( + this, + function(item) { return this.store.getIdentity(item); } + ) + ); + }, + /* Print the same data that the Flattener is feeding to the - * grid, sorted the same way too. remove limit and offset (i.e., - * print it all. */ - "print": function() { + * grid, sorted the same way too. Remove limit and offset (i.e., + * print it all) unless those are passed in to the print() method. + */ + "print": function(limit, offset, query_mixin) { var coal = this._columnOrderingAndLabels(); var req = { - "query": this.query, + "query": dojo.mixin({}, this.query, query_mixin), "queryOptions": { - "all": true, "columns": coal.columns, "labels": coal.labels }, @@ -849,7 +877,22 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { } }; + if (limit) { + req.count = limit; + req.start = offset || 0; + } else { + req.queryOptions.all = true; + } + this.store.fetchToPrint(req); + }, + + "printSelected": function() { + var id_blob = {}; + id_blob[this.store.getIdentityAttributes()[0]] = + this.getSelectedIDs(); + + this.print(null, null, id_blob); } } ); diff --git a/Open-ILS/web/js/dojo/openils/widget/PCrudFilterDialog.js b/Open-ILS/web/js/dojo/openils/widget/PCrudFilterDialog.js index 09a26fb83f..6adf45c6f8 100644 --- a/Open-ILS/web/js/dojo/openils/widget/PCrudFilterDialog.js +++ b/Open-ILS/web/js/dojo/openils/widget/PCrudFilterDialog.js @@ -1,630 +1,11 @@ -if (!dojo._hasResource['openils.widget.PCrudFilterDialog']) { - - /* openils.widget.PCrudFilterDialog is a dijit that, given a fieldmapper - * class, provides a dialog in which users can define inclusionary - * filters based on fields selected from the fieldmapper class and values - * for those fields. Operators can be selected so that not only equality - * comparisons are possible in the filter, but also inequality filters, - * likeness (for text fields only) betweenness, and nullity tests. - * - * The dijit yields its result in the form of a JSON query suitable for - * use as the where clause of a pcrud search, via the onApply callback. - * - * In addition to its fmClass paramter, note the useful parameter - * suppressFilterFields. Say for instance you're using this dijit - * on an fmClass like "brt" which has a field "record" that points to the - * bre class. The AutoWidget provided for users to enter values for - * comparisons on the record field would be a dropdown containing all - * the bre ID's in the system! That would be unusable in any realistic - * system, unless/until we teach AutoWidget to use a lazy-loading store - * for dropdowns. - * - * The comparisons in each filter row are "and-ed" together in the JSON - * query yielded, except for repetitions of the same field, which are - * "or-ed" together /within/ the overall "and" group. Look at comments - * within PCrudFilterRowManager.compile() for more information. - * - * AutoGrid has some ability to use this dijit to offer a filtering dialog, - * but be aware that the filtering dialog is /not/ aware of other - * fitering measures in place in a given AutoGrid-based interface, such as - * (typically) context org unit selectors, and therefore using the context - * org unit selector will not respect selected filters in this dijit, and - * vice-versa. - */ - - dojo.provide('openils.widget.PCrudFilterDialog'); - dojo.require('openils.widget.AutoFieldWidget'); - dojo.require('dijit.form.FilteringSelect'); - dojo.require('dijit.form.Button'); - dojo.require('dojo.data.ItemFileReadStore'); - dojo.require('dijit.Dialog'); - dojo.require('openils.Util'); - - dojo.requireLocalization("openils.widget", "PCrudFilterDialog"); - - var pcFilterLocaleStrings = dojo.i18n.getLocalization( - "openils.widget", "PCrudFilterDialog" - ); - - /* These are the operators that make up the central dropdown in each - * row of the widget. When fields of different datatypes are selected, - * some of these operators may be masked via the "minimal" and "strict" - * properties. - */ - var _operator_store = new dojo.data.ItemFileReadStore( - { - "data": { - "identifier": "name", - "items": [ - { - "name": "=", - "label": pcFilterLocaleStrings.OPERATOR_EQ, - "param_count": 1, - "minimal": true, - "strict": true - }, { - "name": "!=", - "label": pcFilterLocaleStrings.OPERATOR_NE, - "param_count": 1, - "minimal": true, - "strict": true - }, { - "name": "null", - "label": pcFilterLocaleStrings.OPERATOR_IS_NULL, - "param_count": 0, - "minimal": true, - "strict": true - }, { - "name": "not null", - "label": pcFilterLocaleStrings.OPERATOR_IS_NOT_NULL, - "param_count": 0, - "minimal": true, - "strict": true - }, { - "name": ">", - "label": pcFilterLocaleStrings.OPERATOR_GT, - "param_count": 1, - "strict": true - }, { - "name": "<", - "label": pcFilterLocaleStrings.OPERATOR_LT, - "param_count": 1, - "strict": true - }, { - "name": ">=", - "label": pcFilterLocaleStrings.OPERATOR_GTE, - "param_count": 1, - "strict": true - }, { - "name": "<=", - "label": pcFilterLocaleStrings.OPERATOR_LTE, - "param_count": 1, - "strict": true - }, { - "name": "between", - "label": pcFilterLocaleStrings.OPERATOR_BETWEEN, - "param_count": 2, - "strict": true - }, { - "name": "not between", - "label": pcFilterLocaleStrings.OPERATOR_NOT_BETWEEN, - "param_count": 2, - "strict": true - }, { - "name": "like", - "label": pcFilterLocaleStrings.OPERATOR_LIKE, - "param_count": 1 - }, { - "name": "not like", - "label": pcFilterLocaleStrings.OPERATOR_NOT_LIKE, - "param_count": 1 - } - ] - } - } - ); - - /* The text datatype supports all the above operators for comparisons. */ - var _store_query_by_datatype = {"text": {}}; - - /* These three datatypes support only minimal operators. */ - ["bool", "link", "org_unit"].forEach( - function(type) { - _store_query_by_datatype[type] = {"minimal": true}; - } - ); - - /* These datatypes support strict operators (everything save [not] like). */ - ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach( - function(type) { - _store_query_by_datatype[type] = {"strict": true}; - } - ); - - /* This helps convert things that pcrud won't accept ("not between", "not - * like") into proper JSON query expressions. - * It returns false if a clause doesn't have any such negative operator, - * or it returns true AND gets rid of the "not " part in the clause - * object itself. It's up to the caller to wrap it in {"-not": {}} in - * the right place. */ - function _clause_was_negative(clause) { - /* clause objects really only ever have one property */ - var ops = openils.Util.objectProperties(clause); - var op = ops.pop(); - var matches = op.match(/^not (\w+)$/); - if (matches) { - clause[matches[1]] = clause[op]; - delete clause[op]; - return true; - } - return false; - } - - /* This is not the dijit per se. Search further in this file for - * "dojo.declare" for the beginning of the dijit. - * - * This is, however, the object that represents a collection of filter - * rows and knows how to compile a filter from those rows. */ - function PCrudFilterRowManager() { - var self = this; - - this._init = function(container, field_store, fm_class) { - this.container = container; - this.field_store = field_store; - this.fm_class = fm_class; - - this.rows = {}; - this.row_index = 0; - - this._build_table(); - }; - - this._build_table = function() { - this.table = dojo.create( - "table", { - "className": "oils-pcrudfilterdialog-table" - }, this.container - ); - - var tr = dojo.create( - "tr", { - "id": "pcrudfilterdialog-empty", - "className": "hidden" - }, this.table - ); - - dojo.create( - "td", { - "colspan": 4, - "innerHTML": pcFilterLocaleStrings.EMPTY_CASE - }, tr - ); - - this.add_row(); - }; - - this._compile_second_pass = function(first_pass) { - var and = []; - var result = {"-and": and}; - - for (var field in first_pass) { - var list = first_pass[field]; - if (list.length == 1) { - var obj = {}; - var clause = list.pop(); - if (_clause_was_negative(clause)) { - obj["-not"] = {}; - obj["-not"][field] = clause; - } else { - obj[field] = clause; - } - and.push(obj); - } else { - var or = list.map( - function(clause) { - var obj = {}; - if (_clause_was_negative(clause)) { - obj["-not"] = {}; - obj["-not"][field] = clause; - } else { - obj[field] = clause; - } - return obj; - } - ); - and.push({"-or": or}); - } - } - - return result; - }; - - this.add_row = function() { - this.hide_empty_placeholder(); - var row_id = this.row_index++; - this.rows[row_id] = new PCrudFilterRow(this, row_id); - }; - - this.remove_row = function(row_id) { - this.rows[row_id].destroy(); - delete this.rows[row_id]; - - if (openils.Util.objectProperties(this.rows).length < 1) - this.show_empty_placeholder(); - }; - - this.hide_empty_placeholder = function() { - openils.Util.hide("pcrudfilterdialog-empty"); - }; - - this.show_empty_placeholder = function() { - openils.Util.show("pcrudfilterdialog-empty"); - }; - - this.compile = function() { - /* We'll prepare a first-pass data structure that looks like: - * { - * field1: [{"op": "one value"}], - * field2: [{"op": "a value"}, {"op": "b value"}], - * field3: [{"op": "first value"}, {"op": ["range start", "range end"]}] - * } - * - * which will be passed to _compile_second_pass() to yield an - * actual filter suitable for pcrud (with -and and -or in all the - * right places) so the above example would come out like: - * - * { "-and": [ - * {"field1": {"op": "one value"}}, - * {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] }, - * {"-or": [ - * {"field3": {"op": "first value"}}, - * {"field3": {"op": ["range start", "range end"]}} - * ] } - * ] } - */ - var first_pass = {}; - - for (var row_id in this.rows) { - var row = this.rows[row_id]; - var value = row.compile(); - var field = row.selected_field; - - if (typeof(value) != "undefined" && - typeof(field) != "undefined") { - if (!first_pass[field]) - first_pass[field] = []; - first_pass[field].push(value); - } - } - - /* Don't return an empty filter: pcrud can't use that. */ - if (openils.Util.objectProperties(first_pass).length < 1) { - var result = {}; - result[fieldmapper[this.fm_class].Identifier] = {"!=": null}; - return result; - } else { - return this._compile_second_pass(first_pass); - } - }; - - this._init.apply(this, arguments); - } - - /* As the name implies, objects of this class manage a single row of the - * query. Therefore they know about their own field dropdown, their own - * selector dropdown, and their own value widget (or widgets in the case - * of between searches, which call for two widgets to define a range), - * and not much else. */ - function PCrudFilterRow() { - var self = this; - - this._init = function(filter_row_manager, row_id) { - this.filter_row_manager = filter_row_manager; - this.row_id = row_id; - - this._build(); - }; - - this._build = function() { - this.tr = dojo.create("tr", {}, this.filter_row_manager.table); - - this._create_field_selector(); - this._create_operator_selector(); - this._create_value_slot(); - this._create_remover(); - }; - - this._create_field_selector = function() { - var td = dojo.create("td", {}, this.tr); - this.field_selector = new dijit.form.FilteringSelect( - { - "labelAttr": "label", - "searchAttr": "label", - "scrollOnFocus": false, - "onChange": function(value) { - self.update_selected_field(value); - }, - "store": this.filter_row_manager.field_store - }, dojo.create("span", {}, td) - ); - }; - - this._create_operator_selector = function() { - var td = dojo.create("td", {}, this.tr); - this.operator_selector = new dijit.form.FilteringSelect( - { - "labelAttr": "label", - "searchAttr": "label", - "scrollOnFocus": false, - "onChange": function(value) { - self.update_selected_operator(value); - }, - "store": _operator_store - }, dojo.create("span", {}, td) - ); - }; - - this._adjust_operator_selector = function() { - this.operator_selector.attr( - "query", _store_query_by_datatype[this.selected_field_type] - ); - this.operator_selector.reset(); - }; - - this._create_value_slot = function() { - this.value_slot = dojo.create("td", {"innerHTML": "-"}, this.tr); - }; - - this._create_remover = function() { - var td = dojo.create("td", {}, this.tr); - var anchor = dojo.create( - "a", { - "className": "oils-pcrudfilterdialog-remover", - "innerHTML": "X", - "href": "#", - "onclick": function() { - self.filter_row_manager.remove_row(self.row_id); - } - }, td - ); - }; - - this._clear_value_slot = function() { - if (this.value_widgets) { - this.value_widgets.forEach( - function(autowidg) { autowidg.widget.destroy(); } - ); - delete this.value_widgets; - } - - dojo.empty(this.value_slot); - }; - - this._rebuild_value_widgets = function() { - this._clear_value_slot(); - - if (!this.get_selected_operator() || !this.selected_field) - return; - - this.value_widgets = []; - - var param_count = this.operator_selector.item.param_count; - - for (var i = 0; i < param_count; i++) { - var widg = new openils.widget.AutoFieldWidget({ - "fmClass": this.selected_field_fm_class, - "fmField": this.selected_field_fm_field, - "parentNode": dojo.create("span", {}, this.value_slot), - "dijitArgs": {"scrollOnFocus": false} - }); - - widg.build(); - this.value_widgets.push(widg); - } - }; - - /* for ugly special cases in compliation */ - this._null_clause = function() { - var opname = this.get_selected_operator_name(); - if (opname == "not null") - return {"!=": null}; - else if (opname == "null") - return null; - else - return; - }; - - this.get_selected_operator = function() { - if (this.operator_selector) - return this.operator_selector.item; - }; - - this.get_selected_operator_name = function() { - var op = this.get_selected_operator(); - return op ? op.name : null; - }; - - this.update_selected_operator = function(value) { - this._rebuild_value_widgets(); - }; - - this.update_selected_field = function(value) { - if (this.field_selector.item) { - this.selected_field = value; - this.selected_field_type = this.field_selector.item.type; - - /* This is really about supporting flattenergrid, of which - * we're in the superclass (in a sloppy sad way). From now - * on I won't mix this kind of lazy object with Dojo modules. */ - //console.log(dojo.toJson(this.field_selector.item)); - this.selected_field_fm_field = this.field_selector.item.name; - this.selected_field_is_indirect = - this.field_selector.item.indirect || false; - this.selected_field_fm_class = - this.field_selector.item.fmClass || - this.filter_row_manager.fm_class; - - this._adjust_operator_selector(); - this._rebuild_value_widgets(); - } - }; - - this.compile = function() { - if (this.value_widgets) { - var values = this.value_widgets.map( - function(widg) { - return self.selected_field_is_indirect ? - widg.widget.attr('displayedValue') : - widg.getFormattedValue(); - } - ); - - if (!values.length) { - return this._null_clause(); /* null/not null */ - } else { - var clause = {}; - var op = this.get_selected_operator_name(); - if (values.length == 1) - clause[op] = values.pop(); - else - clause[op] = values; - return clause; - } - } else { - return; - } - }; - - this.destroy = function() { - this._clear_value_slot(); - this.field_selector.destroy(); - if (this.operator_selector) - this.operator_selector.destroy(); - - dojo.destroy(this.tr); - }; - - this._init.apply(this, arguments); - } +if (!dojo._hasResource["openils.widget.PCrudFilterDialog"]) { + dojo.provide("openils.widget.PCrudFilterDialog"); + dojo.require("openils.widget.PCrudFilterPane"); + dojo.require("dijit.Dialog"); dojo.declare( - 'openils.widget.PCrudFilterDialog', - [dijit.Dialog, openils.widget.AutoWidget], - { - - constructor : function(args) { - for(var k in args) - this[k] = args[k]; - this.title = this.title || pcFilterLocaleStrings.DEFAULT_DIALOG_TITLE; - this.widgetIndex = 0; - this.widgetCache = {}; - }, - - _buildButtons : function() { - var self = this; - - var button_holder = dojo.create( - "div", { - "className": "oils-pcrudfilterdialog-buttonholder" - }, this.domNode - ); - - new dijit.form.Button( - { - "label": pcFilterLocaleStrings.ADD_ROW, - "scrollOnFocus": false, /* almost always better */ - "onClick": function() { - self.filter_row_manager.add_row(); - } - }, dojo.create("span", {}, button_holder) - ); - - new dijit.form.Button( - { - "label": pcFilterLocaleStrings.APPLY, - "scrollOnFocus": false, - "onClick": function() { - if (self.onApply) - self.onApply(self.filter_row_manager.compile()); - self.hide(); - } - }, dojo.create("span", {}, button_holder) - ); - - new dijit.form.Button( - { - "label": pcFilterLocaleStrings.CANCEL, - "scrollOnFocus": false, - "onClick": function() { - if (self.onCancel) - self.onCancel(); - self.hide(); - } - }, dojo.create("span", {}, button_holder) - ); - }, - - _buildFieldStore : function() { - var self = this; - var realFieldList = this.sortedFieldList.filter( - function(item) { return !(item.virtual || item.nonIdl); } - ); - - /* Prevent any explicitly unwanted fields from being available - * in our field dropdowns. */ - if (dojo.isArray(this.suppressFilterFields)) { - realFieldList = realFieldList.filter( - function(item) { - for ( - var i = 0; - i < self.suppressFilterFields.length; - i++ - ) { - if (item.name == self.suppressFilterFields[i]) - return false; - } - return true; - } - ); - } - - this.fieldStore = new dojo.data.ItemFileReadStore({ - "data": { - "identifier": "name", - "name": "label", - "items": realFieldList.map( - function(item) { - return { - "label": item.label, - "name": item.name, - "type": item.datatype - }; - } - ) - } - }); - }, - - /* All we really do here is create a data store out of the fields - * from the IDL for our given class, place a few buttons at the - * bottom of the dialog, and hand off to PCrudFilterRowManager to - * do the actual work. - */ - - startup : function() { - var self = this; - this.inherited(arguments); - this.initAutoEnv(); - - this._buildFieldStore(); - - this.filter_row_manager = new PCrudFilterRowManager( - dojo.create("div", {}, this.domNode), - this.fieldStore, this.fmClass - ); - - this._buildButtons(); - } - } + "openils.widget.PCrudFilterDialog", [ + dijit.Dialog, openils.widget.PCrudFilterPane + ] ); } diff --git a/Open-ILS/web/js/dojo/openils/widget/PCrudFilterPane.js b/Open-ILS/web/js/dojo/openils/widget/PCrudFilterPane.js new file mode 100644 index 0000000000..eb34315076 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/widget/PCrudFilterPane.js @@ -0,0 +1,756 @@ +if (!dojo._hasResource['openils.widget.PCrudFilterPane']) { + + /* openils.widget.PCrudFilterPane is a dijit that, given a fieldmapper + * class, provides a pane in which users can define inclusionary + * filters based on fields selected from the fieldmapper class and values + * for those fields. Operators can be selected so that not only equality + * comparisons are possible in the filter, but also inequality filters, + * likeness (for text fields only) betweenness, and nullity tests. * + * The dijit yields its result in the form of a JSON query suitable for + * use as the where clause of a pcrud search, via the onApply callback. + * + * In addition to its fmClass paramter, note the useful parameter + * suppressFilterFields. Say for instance you're using this dijit + * on an fmClass like "brt" which has a field "record" that points to the + * bre class. The AutoWidget provided for users to enter values for + * comparisons on the record field would be a dropdown containing all + * the bre ID's in the system! That would be unusable in any realistic + * system, unless/until we teach AutoWidget to use a lazy-loading store + * for dropdowns. + * + * The comparisons in each filter row are "and-ed" together in the JSON + * query yielded, except for repetitions of the same field, which are + * "or-ed" together /within/ the overall "and" group. Look at comments + * within PCrudFilterRowManager.compile() for more information. + * + * AutoGrid has some ability to use this dijits based on this to offer a + * filtering dialog, but be aware that the filtering dialog is /not/ aware + * of other fitering measures in place in a given AutoGrid-based interface, + * such as (typically) context org unit selectors, and therefore using the + * context org unit selector will not respect selected filters in this + * dijit, and vice-versa. + */ + + dojo.provide('openils.widget.PCrudFilterPane'); + dojo.require('openils.widget.AutoFieldWidget'); + dojo.require('dijit.form.FilteringSelect'); + dojo.require('dijit.form.Button'); + dojo.require('dojo.data.ItemFileReadStore'); + dojo.require('openils.Util'); + + dojo.requireLocalization("openils.widget", "PCrudFilterPane"); + + /* XXX namespace pollution! arg! Fix this whole module sometime. */ + var localeStrings = dojo.i18n.getLocalization( + "openils.widget", "PCrudFilterPane" + ); + + /* These are the operators that make up the central dropdown in each + * row of the widget. When fields of different datatypes are selected, + * some of these operators may be masked via the "minimal" and "strict" + * properties. + */ + var _operator_store = new dojo.data.ItemFileReadStore( + { + "data": { + "identifier": "name", + "items": [ + { + "name": "=", + "label": localeStrings.OPERATOR_EQ, + "param_count": 1, + "minimal": true, + "strict": true + }, { + "name": "!=", + "label": localeStrings.OPERATOR_NE, + "param_count": 1, + "minimal": true, + "strict": true + }, { + "name": "null", + "label": localeStrings.OPERATOR_IS_NULL, + "param_count": 0, + "minimal": true, + "strict": true + }, { + "name": "not null", + "label": localeStrings.OPERATOR_IS_NOT_NULL, + "param_count": 0, + "minimal": true, + "strict": true + }, { + "name": ">", + "label": localeStrings.OPERATOR_GT, + "param_count": 1, + "strict": true + }, { + "name": "<", + "label": localeStrings.OPERATOR_LT, + "param_count": 1, + "strict": true + }, { + "name": ">=", + "label": localeStrings.OPERATOR_GTE, + "param_count": 1, + "strict": true + }, { + "name": "<=", + "label": localeStrings.OPERATOR_LTE, + "param_count": 1, + "strict": true + }, { + "name": "between", + "label": localeStrings.OPERATOR_BETWEEN, + "param_count": 2, + "strict": true + }, { + "name": "not between", + "label": localeStrings.OPERATOR_NOT_BETWEEN, + "param_count": 2, + "strict": true + }, { + "name": "like", + "label": localeStrings.OPERATOR_LIKE, + "param_count": 1 + }, { + "name": "not like", + "label": localeStrings.OPERATOR_NOT_LIKE, + "param_count": 1 + } + ] + } + } + ); + + /* The text datatype supports all the above operators for comparisons. */ + var _store_query_by_datatype = {"text": {}}; + + /* These three datatypes support only minimal operators. */ + ["bool", "link", "org_unit"].forEach( + function(type) { + _store_query_by_datatype[type] = {"minimal": true}; + } + ); + + /* These datatypes support strict operators (everything save [not] like). */ + ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach( + function(type) { + _store_query_by_datatype[type] = {"strict": true}; + } + ); + + /* This helps convert things that pcrud won't accept ("not between", "not + * like") into proper JSON query expressions. + * It returns false if a clause doesn't have any such negative operator, + * or it returns true AND gets rid of the "not " part in the clause + * object itself. It's up to the caller to wrap it in {"-not": {}} in + * the right place. */ + function _clause_was_negative(clause) { + /* clause objects really only ever have one property */ + var ops = openils.Util.objectProperties(clause); + var op = ops.pop(); + var matches = op.match(/^not (\w+)$/); + if (matches) { + clause[matches[1]] = clause[op]; + delete clause[op]; + return true; + } + return false; + } + + /* This is not the dijit per se. Search further in this file for + * "dojo.declare" for the beginning of the dijit. + * + * This is, however, the object that represents a collection of filter + * rows and knows how to compile a filter from those rows. */ + function PCrudFilterRowManager() { + var self = this; + + this._init = function( + container, field_store, fm_class, compact, widget_builders, + skip_first_add_row, do_apply + ) { + this.container = container; + this.field_store = field_store; + this.fm_class = fm_class; + this.compact = compact; + this.widget_builders = widget_builders || {}; + this.skip_first_add_row = skip_first_add_row; + this.do_apply = do_apply; + + this.rows = {}; + this.row_index = 0; + + this._build_table(); + }; + + this._build_table = function() { + this.table = dojo.create( + "table", { + "className": "oils-pcrudfilterdialog-table" + }, this.container + ); + + var tr = dojo.create( + "tr", { + "id": "pcrudfilterdialog-empty", + "className": "hidden" + }, this.table + ); + + dojo.create( + "td", { + "colspan": 4, + "innerHTML": localeStrings[ + this.compact ? "EMPTY_CASE_COMPACT" : "EMPTY_CASE" + ] + }, tr + ); + + if (!this.skip_first_add_row) + this.add_row(); + }; + + this._compile_second_pass = function(first_pass) { + var and = []; + var result = {"-and": and}; + + for (var field in first_pass) { + var list = first_pass[field]; + if (list.length == 1) { + var obj = {}; + var clause = list.pop(); + if (_clause_was_negative(clause)) { + obj["-not"] = {}; + obj["-not"][field] = clause; + } else { + obj[field] = clause; + } + and.push(obj); + } else { + var or = list.map( + function(clause) { + var obj = {}; + if (_clause_was_negative(clause)) { + obj["-not"] = {}; + obj["-not"][field] = clause; + } else { + obj[field] = clause; + } + return obj; + } + ); + and.push({"-or": or}); + } + } + + return result; + }; + + this.add_row = function(initializer) { + this.hide_empty_placeholder(); + var row_id = this.row_index++; + this.rows[row_id] = new PCrudFilterRow(this, row_id, initializer); + }; + + this.remove_row = function(row_id) { + this.rows[row_id].destroy(); + delete this.rows[row_id]; + + if (openils.Util.objectProperties(this.rows).length < 1) + this.show_empty_placeholder(); + + if (this.compact) + this.do_apply(); + }; + + this.hide_empty_placeholder = function() { + openils.Util.hide("pcrudfilterdialog-empty"); + }; + + this.show_empty_placeholder = function() { + openils.Util.show("pcrudfilterdialog-empty"); + }; + + this.compile = function() { + /* We'll prepare a first-pass data structure that looks like: + * { + * field1: [{"op": "one value"}], + * field2: [{"op": "a value"}, {"op": "b value"}], + * field3: [{"op": "first value"}, {"op": ["range start", "range end"]}] + * } + * + * which will be passed to _compile_second_pass() to yield an + * actual filter suitable for pcrud (with -and and -or in all the + * right places) so the above example would come out like: + * + * { "-and": [ + * {"field1": {"op": "one value"}}, + * {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] }, + * {"-or": [ + * {"field3": {"op": "first value"}}, + * {"field3": {"op": ["range start", "range end"]}} + * ] } + * ] } + */ + var first_pass = {}; + + for (var row_id in this.rows) { + var row = this.rows[row_id]; + var value = row.compile(); + var field = row.selected_field; + + if (typeof(value) != "undefined" && + typeof(field) != "undefined") { + if (!first_pass[field]) + first_pass[field] = []; + first_pass[field].push(value); + } + } + + /* Don't return an empty filter: pcrud can't use that. */ + if (openils.Util.objectProperties(first_pass).length < 1) { + var result = {}; + result[fieldmapper[this.fm_class].Identifier] = {"!=": null}; + return result; + } else { + return this._compile_second_pass(first_pass); + } + }; + + this._init.apply(this, arguments); + } + + /* As the name implies, objects of this class manage a single row of the + * query. Therefore they know about their own field dropdown, their own + * selector dropdown, and their own value widget (or widgets in the case + * of between searches, which call for two widgets to define a range), + * and not much else. */ + function PCrudFilterRow() { + var self = this; + + this._init = function(filter_row_manager, row_id, initializer) { + this.filter_row_manager = filter_row_manager; + this.row_id = row_id; + + if (this.filter_row_manager.compact) + this._build_compact(); + else + this._build(); + + if (initializer) + this.initialize(initializer); + }; + + this._build = function() { + this.tr = dojo.create("tr", {}, this.filter_row_manager.table); + + this._create_field_selector(); + this._create_operator_selector(); + this._create_value_slot(); + this._create_remover(); + }; + + this._build_compact = function() { + this.tr = dojo.create("tr", {}, this.filter_row_manager.table); + + var td = dojo.create("td", {}, this.tr); + + this._create_field_selector(td); + this._create_operator_selector(td); + + dojo.create("br", {}, td); + this._create_value_slot(td); + + td = dojo.create( + "td", + {"className": "oils-pcrudfilterdialog-remover-holder"}, + this.tr + ); + + this._create_remover(td); + }; + + this._create_field_selector = function(use_element) { + var td = use_element || dojo.create("td", {}, this.tr); + + this.field_selector = new dijit.form.FilteringSelect( + { + "labelAttr": "label", + "searchAttr": "label", + "scrollOnFocus": false, + "onChange": function(value) { + self.update_selected_field(value); + }, + "store": this.filter_row_manager.field_store + }, dojo.create("span", {}, td) + ); + }; + + this._create_operator_selector = function(use_element) { + var td = use_element || dojo.create("td", {}, this.tr); + + this.operator_selector = new dijit.form.FilteringSelect( + { + "labelAttr": "label", + "searchAttr": "label", + "scrollOnFocus": false, + "onChange": function(value) { + self.update_selected_operator(value); + }, + "store": _operator_store + }, dojo.create("span", {}, td) + ); + }; + + this._adjust_operator_selector = function() { + this.operator_selector.attr( + "query", _store_query_by_datatype[this.selected_field_type] + ); + this.operator_selector.reset(); + }; + + this._create_value_slot = function(use_element) { + if (use_element) + this.value_slot = dojo.create( + "span", {"innerHTML": "-"}, use_element + ); + else + this.value_slot = dojo.create("td",{"innerHTML":"-"},this.tr); + }; + + this._create_remover = function(use_element) { + var td = use_element || dojo.create("td", {}, this.tr); + var anchor = dojo.create( + "a", { + "className": "oils-pcrudfilterdialog-remover", + "innerHTML": "X", + "href": "#", + "onclick": function() { + self.filter_row_manager.remove_row(self.row_id); + } + }, td + ); + }; + + this._clear_value_slot = function() { + if (this.value_widgets) { + this.value_widgets.forEach( + function(autowidg) { autowidg.widget.destroy(); } + ); + delete this.value_widgets; + } + + dojo.empty(this.value_slot); + }; + + this._rebuild_value_widgets = function() { + this._clear_value_slot(); + + if (!this.get_selected_operator() || !this.selected_field) + return; + + this.value_widgets = []; + + var param_count = this.operator_selector.item.param_count; + + /* This is where find and deploy custom widget builders. */ + var widget_builder_key = this.selected_field_fm_class + ":" + + this.selected_field_fm_field; + var constr = + this.filter_row_manager.widget_builders[widget_builder_key] || + openils.widget.AutoFieldWidget; + + for (var i = 0; i < param_count; i++) { + var widg = new constr({ + "fmClass": this.selected_field_fm_class, + "fmField": this.selected_field_fm_field, + "parentNode": dojo.create("span", {}, this.value_slot), + "dijitArgs": {"scrollOnFocus": false} + }); + + widg.build(); + this.value_widgets.push(widg); + } + }; + + /* for ugly special cases in compliation */ + this._null_clause = function() { + var opname = this.get_selected_operator_name(); + if (opname == "not null") + return {"!=": null}; + else if (opname == "null") + return null; + else + return; + }; + + this.get_selected_operator = function() { + if (this.operator_selector) + return this.operator_selector.item; + }; + + this.get_selected_operator_name = function() { + var op = this.get_selected_operator(); + return op ? op.name : null; + }; + + this.update_selected_operator = function(value) { + this._rebuild_value_widgets(); + }; + + this.update_selected_field = function(value) { + if (this.field_selector.item) { + this.selected_field = value; + this.selected_field_type = this.field_selector.item.type; + + /* This is really about supporting flattenergrid, of which + * we're in the superclass (in a sloppy sad way). From now + * on I won't mix this kind of lazy object with Dojo modules. */ + this.selected_field_fm_field = this.field_selector.item.name; + this.selected_field_is_indirect = + this.field_selector.item.indirect || false; + this.selected_field_fm_class = + this.field_selector.item.fmClass || + this.filter_row_manager.fm_class; + + this._adjust_operator_selector(); + this._rebuild_value_widgets(); + } + }; + + this.compile = function() { + if (this.value_widgets) { + var values = this.value_widgets.map( + function(widg) { + if (widg.useCorrectly) + return widg.widget.attr("value"); + else if (self.selected_field_is_indirect) + return widg.widget.attr("displayedValue"); + else + return widg.getFormattedValue(); + } + ); + + if (!values.length) { + return this._null_clause(); /* null/not null */ + } else { + var clause = {}; + var op = this.get_selected_operator_name(); + if (values.length == 1) + clause[op] = values.pop(); + else + clause[op] = values; + return clause; + } + } else { + return; + } + }; + + this.destroy = function() { + this._clear_value_slot(); + this.field_selector.destroy(); + if (this.operator_selector) + this.operator_selector.destroy(); + + dojo.destroy(this.tr); + }; + + this.initialize = function(initializer) { + this.field_selector.attr("value", initializer.field); + this.operator_selector.attr("value", initializer.operator); + + /* Caller supplies value for one value, values (array) for + * multiple. */ + if (!initializer.values || !dojo.isArray(initializer.values)) + initializer.values = [initializer.values || initializer.value]; + + for (var i = 0; i < initializer.values.length; i++) { + this.value_widgets[i].widget.attr( + "value", initializer.values[i] + ); + } + }; + + this._init.apply(this, arguments); + } + + dojo.declare( + "openils.widget.PCrudFilterPane", [openils.widget.AutoWidget], + { + "useDiv": null, /* should always be null for subclass dialogs */ + "initializers": null, + "compact": false, + "widgetBuilders": null, + + "constructor": function(args) { + for(var k in args) + this[k] = args[k]; + this.widgetIndex = 0; + this.widgetCache = {}; + + /* Meaningless in a pane, but better here than in + * PCrudFilterDialog so that we don't need to load i18n + * strings there: */ + this.title = this.title || localeStrings.DEFAULT_DIALOG_TITLE; + }, + + "_buildButtons": function() { + var self = this; + + var button_holder = dojo.create( + "div", { + "className": "oils-pcrudfilterdialog-buttonholder" + }, this.domNode + ); + + new dijit.form.Button( + { + "label": localeStrings.ADD_ROW, + "scrollOnFocus": false, /* almost always better */ + "onClick": function() { + self.filter_row_manager.add_row(); + } + }, dojo.create("span", {}, button_holder) + ); + + this._apply_button = new dijit.form.Button( + { + "label": localeStrings.APPLY, + "scrollOnFocus": false, + "onClick": function() { self.doApply(); } + }, dojo.create("span", {}, button_holder) + ); + + if (!this.useDiv) { + new dijit.form.Button( + { + "label": localeStrings.CANCEL, + "scrollOnFocus": false, + "onClick": function() { + if (self.onCancel) + self.onCancel(); + self.hide(); + } + }, dojo.create("span", {}, button_holder) + ); + } + }, + + "_buildFieldStore": function() { + var self = this; + var realFieldList = this.sortedFieldList.filter( + function(item) { return !(item.virtual || item.nonIdl); } + ); + + /* Prevent any explicitly unwanted fields from being available + * in our field dropdowns. */ + if (dojo.isArray(this.suppressFilterFields)) { + realFieldList = realFieldList.filter( + function(item) { + for ( + var i = 0; + i < self.suppressFilterFields.length; + i++ + ) { + if (item.name == self.suppressFilterFields[i]) + return false; + } + return true; + } + ); + } + + this.fieldStore = new dojo.data.ItemFileReadStore({ + "data": { + "identifier": "name", + "name": "label", + "items": realFieldList.map( + function(item) { + return { + "label": item.label, + "name": item.name, + "type": item.datatype + }; + } + ) + } + }); + }, + + "hide": function() { + try { + this.inherited(arguments); + } catch (E) { + /* When using *FilterPane directly (without a *Dialog + * subclass), do nothing. */ + void(0); + } + }, + + /* All we really do here is create a data store out of the fields + * from the IDL for our given class, place a few buttons at the + * bottom of the dialog, and hand off to PCrudFilterRowManager to + * do the actual work. + */ + + "startup": function() { + if (this.useDiv) + this.domNode = this.useDiv; + + try { + this.inherited(arguments); + } catch (E) { + /* When using *FilterPane directly (without a *Dialog + * subclass), there is no startup method in any ancestor + * class. XXX Refactor? + */ + void(0); + } + + this.initAutoEnv(); + + this._buildFieldStore(); + + this.filter_row_manager = new PCrudFilterRowManager( + dojo.create("div", {}, this.domNode), + this.fieldStore, this.fmClass, this.compact, + this.widgetBuilders, + Boolean(this.initializers) /* avoid adding empty row */, + dojo.hitch(this, function() { this.doApply(); }) + ); + + this._buildButtons(); + + if (this.initializers) { + this.initializers.forEach( + dojo.hitch(this, function(initializer) { + this.filter_row_manager.add_row(initializer); + }) + ); + } + }, + + /* This should just be named 'apply', but that is kind of a special + * word in Javascript, no? */ + "doApply": function() { + this._apply_button.attr("disabled", true); + + var _E; /* Try pretty hard not to leave the apply button + disabled forever, even if 'apply' blows up. */ + try { + if (this.onApply) + this.onApply(this.filter_row_manager.compile()); + } catch (E) { + _E = E; + } + this.hide(); + this._apply_button.attr("disabled", false); + + if (_E) throw _E; + } + } + ); +} diff --git a/Open-ILS/web/js/dojo/openils/widget/nls/PCrudFilterDialog.js b/Open-ILS/web/js/dojo/openils/widget/nls/PCrudFilterPane.js similarity index 92% rename from Open-ILS/web/js/dojo/openils/widget/nls/PCrudFilterDialog.js rename to Open-ILS/web/js/dojo/openils/widget/nls/PCrudFilterPane.js index 9ad4e97d31..8e76dd59de 100644 --- a/Open-ILS/web/js/dojo/openils/widget/nls/PCrudFilterDialog.js +++ b/Open-ILS/web/js/dojo/openils/widget/nls/PCrudFilterPane.js @@ -12,6 +12,7 @@ "OPERATOR_LIKE": "is like", "OPERATOR_NOT_LIKE": "is not like", "EMPTY_CASE": "Add rows to filter results, or just click Apply to see unfiltered results.", + "EMPTY_CASE_COMPACT": "Add rows to filter results.", "DEFAULT_DIALOG_TITLE": "Filter Results", "ADD_ROW": "Add Row", "APPLY": "Apply", diff --git a/Open-ILS/web/js/ui/default/actor/user/trigger_events.js b/Open-ILS/web/js/ui/default/actor/user/trigger_events.js deleted file mode 100644 index 1f734df627..0000000000 --- a/Open-ILS/web/js/ui/default/actor/user/trigger_events.js +++ /dev/null @@ -1,87 +0,0 @@ -dojo.require('dojox.grid.DataGrid'); -dojo.require('dojo.data.ItemFileWriteStore'); -dojo.require('openils.Util'); -dojo.require('openils.User'); - -// need these to represent the event def name -dojo.requireLocalization('openils.conify', 'conify'); -var localeStrings = dojo.i18n.getLocalization('openils.conify', 'conify'); - -var evtCache = {}; - -function init() { - var store = new dojo.data.ItemFileWriteStore({data:acqf.initStoreData()}); - evtGrid.setStore(store); - evtGrid.render(); - - function onResponse(r) { - var evt = openils.Util.readResponse(r); - evtCache[evt.id()] = evt; - evtGrid.store.newItem(evt.toStoreItem()); - } - - fieldmapper.standardRequest( - ['open-ils.actor', 'open-ils.actor.user.events.circ'], - { async: true, - params: [openils.User.authtoken, patronId], - onresponse : onResponse - } - ); - - fieldmapper.standardRequest( - ['open-ils.actor', 'open-ils.actor.user.events.ahr'], - { async: true, - params: [openils.User.authtoken, patronId], - onresponse : onResponse - } - ); -} - -function getField(rowIdx, item) { - if(!item) return ''; - var evt = evtCache[this.grid.store.getValue(item, 'id')]; - - switch(this.field) { - case 'event_def': - return dojo.string.substitute( - localeStrings.EVENT_DEF_LABEL, [ - fieldmapper.aou.findOrgUnit(evt.event_def().owner()).shortname(), - evt.event_def().name() - ]); - case 'reactor': - return evt.event_def().reactor().module(); - case 'validator': - return evt.event_def().validator().module(); - case 'hook': - return evt.event_def().hook(); - case 'target': - switch(evt.target().classname) { - case 'circ': - return evt.target().target_copy().barcode(); - case 'ahr': - if(evt.target().currrent_copy()) - return evt.target().currrent_copy().barcode(); - } - - } - - return this.grid.store.getValue(item, this.field) || ''; -} - -function evtCancelSelected() { - var selected = evtGrid.selection.getSelected(); - if(selected.length == 0) return; - var eventIds = selected.map( - function(item) { return evtGrid.store.getValue(item, 'id') } ); - alert(eventIds); - fieldmapper.standardRequest( - ['open-ils.actor', 'open-ils.actor.user.event.cancel.batch'], - { async: true, - params: [openils.User.authtoken, eventIds], - oncomplete : init - } - ); -} - -openils.Util.addOnLoad(init); - diff --git a/Open-ILS/xul/staff_client/chrome/content/main/constants.js b/Open-ILS/xul/staff_client/chrome/content/main/constants.js index 6bc4d44868..870f4308d0 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/constants.js +++ b/Open-ILS/xul/staff_client/chrome/content/main/constants.js @@ -492,7 +492,6 @@ var urls = { 'XUL_SURVEY_WIZARD' : 'chrome://open_ils_staff_client/content/admin/survey_wizard.xul', 'XUL_TIMESTAMP_DIALOG' : '/xul/server/util/timestamp.xul', 'XUL_TOOLBAR_CONFIG' : '/xul/server/admin/toolbar.xul', - 'XUL_TRIGGER_EVENTS' : '/xul/server/patron/trigger_events.xul', 'XUL_USER_BUCKETS' : '/xul/server/patron/user_buckets.xul', 'XUL_VERIFY_CREDENTIALS' : '/xul/server/main/verify_credentials.xul', 'XUL_VOLUME_BUCKETS' : '/xul/server/cat/volume_buckets.xul', @@ -512,7 +511,8 @@ var urls = { 'XUL_REPORTS' : '/reports/oils_rpt.xhtml', 'EG_ACQ_PO_VIEW' : '/eg/acq/po/view', 'EG_ACQ_USER_REQUESTS' : '/eg/acq/picklist/user_request', - 'XUL_SERIAL_BATCH_RECEIVE': '/xul/server/serial/batch_receive.xul' + 'XUL_SERIAL_BATCH_RECEIVE': '/xul/server/serial/batch_receive.xul', + 'EG_TRIGGER_EVENTS' : '/eg/actor/user/event_log' } if(use_tpac) { diff --git a/Open-ILS/xul/staff_client/server/circ/copy_status.js b/Open-ILS/xul/staff_client/server/circ/copy_status.js index af1c9b16f9..b19477a337 100644 --- a/Open-ILS/xul/staff_client/server/circ/copy_status.js +++ b/Open-ILS/xul/staff_client/server/circ/copy_status.js @@ -187,12 +187,14 @@ circ.copy_status.prototype = { try { for (var i = 0; i < obj.selection_list.length; i++) { xulG.new_tab( - urls.XUL_TRIGGER_EVENTS, + xulG.url_prefix(urls.XUL_REMOTE_BROWSER), { 'tab_name' : document.getElementById('commonStrings').getFormattedString('tab.label.triggered_events_for_copy',[ obj.selection_list[i].barcode ]) }, { - 'copy_id' : obj.selection_list[i].copy_id + 'url': urls.EG_TRIGGER_EVENTS + "?copy_id=" + obj.selection_list[i].copy_id, + 'show_print_button': false, + 'show_nav_buttons': false } ); } diff --git a/Open-ILS/xul/staff_client/server/patron/display.js b/Open-ILS/xul/staff_client/server/patron/display.js index a383082dcc..9608ebdb6c 100644 --- a/Open-ILS/xul/staff_client/server/patron/display.js +++ b/Open-ILS/xul/staff_client/server/patron/display.js @@ -344,10 +344,12 @@ patron.display.prototype = { ['command'], function(ev) { obj.right_deck.set_iframe( - urls.XUL_TRIGGER_EVENTS, + xulG.url_prefix(urls.XUL_REMOTE_BROWSER), {}, { - 'patron_id' : obj.patron.id() + 'url': urls.EG_TRIGGER_EVENTS + "?patron_id=" + obj.patron.id(), + 'show_print_button': false, + 'show_nav_buttons': false } ); } diff --git a/Open-ILS/xul/staff_client/server/patron/items.js b/Open-ILS/xul/staff_client/server/patron/items.js index a38f7284dd..ac0f2df6bc 100644 --- a/Open-ILS/xul/staff_client/server/patron/items.js +++ b/Open-ILS/xul/staff_client/server/patron/items.js @@ -44,12 +44,14 @@ patron.items.prototype = { var barcodes = util.functional.map_list( obj.retrieve_ids, function(o) { return o.barcode; } ); for (var i = 0; i < copy_ids.length; i++) { xulG.new_tab( - urls.XUL_TRIGGER_EVENTS, + xulG.url_prefix(urls.XUL_REMOTE_BROWSER), { 'tab_name' : document.getElementById('commonStrings').getFormattedString('tab.label.triggered_events_for_copy',[ barcodes[i] ]) }, { - 'copy_id' : copy_ids[i] + 'url': urls.EG_TRIGGER_EVENTS + "?copy_id=" + copy_ids[i], + 'show_nav_buttons': false, + 'show_print_button': false } ); } @@ -65,10 +67,12 @@ patron.items.prototype = { var copy_ids = util.functional.map_list( obj.retrieve_ids2, function(o) { return o.copy_id; } ); for (var i = 0; i < copy_ids.length; i++) { xulG.new_tab( - urls.XUL_TRIGGER_EVENTS, + xulG.url_prefix(urls.XUL_REMOTE_BROWSER), {}, { - 'copy_id' : copy_ids[i] + 'url': urls.EG_TRIGGER_EVENTS + "?copy_id=" + copy_ids[i], + 'show_nav_buttons': false, + 'show_print_button': false } ); } diff --git a/Open-ILS/xul/staff_client/server/patron/trigger_events.js b/Open-ILS/xul/staff_client/server/patron/trigger_events.js deleted file mode 100644 index 29c3fa21bf..0000000000 --- a/Open-ILS/xul/staff_client/server/patron/trigger_events.js +++ /dev/null @@ -1,234 +0,0 @@ -var list; var error; var net; var rows; - -function $(id) { return document.getElementById(id); } - -//// parent interfaces often call these -function default_focus() { $('atev_list').focus(); } -function refresh() { populate_list(); } -//// - -function trigger_event_init() { - try { - netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); - - commonStrings = $('commonStrings'); - patronStrings = $('patronStrings'); - - if (typeof JSAN == 'undefined') { - throw( - commonStrings.getString('common.jsan.missing') - ); - } - - JSAN.errorLevel = "die"; // none, warn, or die - JSAN.addRepository('..'); - - JSAN.use('OpenILS.data'); data = new OpenILS.data(); data.stash_retrieve(); - XML_HTTP_SERVER = data.server_unadorned; - - JSAN.use('util.error'); error = new util.error(); - JSAN.use('util.network'); net = new util.network(); - JSAN.use('patron.util'); - JSAN.use('util.list'); - JSAN.use('util.functional'); - JSAN.use('util.widgets'); - - dojo.require('openils.Util'); - - init_list(); - $('list_actions').appendChild( list.render_list_actions() ); - list.set_list_actions(); - $('cmd_cancel_event').addEventListener('command', gen_event_handler('cancel'), false); - $('cmd_reset_event').addEventListener('command', gen_event_handler('reset'), false); - $('circ').addEventListener('command', function() { populate_list(); }, false); - $('ahr').addEventListener('command', function() { populate_list(); }, false); - $('pending').addEventListener('command', function() { populate_list(); }, false); - $('complete').addEventListener('command', function() { populate_list(); }, false); - $('error').addEventListener('command', function() { populate_list(); }, false); - populate_list(); - default_focus(); - - } catch(E) { - var err_prefix = 'trigger_events.js -> trigger_event_init() : '; - if (error) error.standard_unexpected_error_alert(err_prefix,E); else alert(err_prefix + E); - } -} - -function gen_event_handler(method) { // cancel or reset? - return function(ev) { - try { - var sel = list.retrieve_selection(); - var ids = util.functional.map_list( sel, function(o) { return JSON2js( o.getAttribute('retrieve_id') ); } ); - - var pm = $('progress'); pm.value = 0; pm.hidden = false; - var idx = -1; - - var i = method == 'cancel' ? 'FM_ATEV_CANCEL' : 'FM_ATEV_RESET'; - fieldmapper.standardRequest( - [ api[i].app, api[i].method ], - { async: true, - params: [ses(), ids], - onresponse: function(r) { - try { - idx++; pm.value = Number( pm.value ) + 100/ids.length; - var result = openils.Util.readResponse(r); - if (typeof result.ilsevent != 'undefined') { throw(result); } - } catch(E) { - error.standard_unexpected_error_alert('In patron/trigger_events.js, handle_'+i+'_event onresponse.',E); - } - }, - onerror: function(r) { - try { - var result = openils.Util.readResponse(r); - throw(result); - } catch(E) { - error.standard_unexpected_error_alert('In patron/trigger_events.js, handle_'+i+'_event onerror.',E); - } - pm.hidden = true; pm.value = 0; populate_list(); - }, - oncomplete: function(r) { - try { - var result = openils.Util.readResponse(r); - } catch(E) { - error.standard_unexpected_error_alert('In patron/trigger_events.js, handle_'+i+'_event oncomplete.',E); - } - pm.hidden = true; pm.value = 0; populate_list(); - } - } - ); - - } catch(E) { - alert('Error in patron/trigger_events.js, handle_???_event(): ' + E); - } - }; -} - -function init_list() { - try { - - list = new util.list( 'atev_list' ); - list.init( - { - 'columns' : [].concat( - list.fm_columns('atev', { - 'atev_target' : { 'render' : function(my) { return fieldmapper.IDL.fmclasses[my.atev.target().classname].label; } } - }) - ).concat( - list.fm_columns('atevdef', { - '*' : { 'expanded_label' : true, 'hidden' : true }, - 'atevdef_name' : { 'hidden' : false }, - 'atevdef_reactor' : { 'render' : function(my) { return my.atevdef.reactor().id(); } }, - 'atevdef_validator' : { 'render' : function(my) { return my.atevdef.validator().id(); } } - }) - ).concat( - list.fm_columns('atreact', { - '*' : { 'expanded_label' : true, 'hidden' : true }, - 'atreact_module' : { 'hidden' : false } - }) - ).concat( - list.fm_columns('atval', { - '*' : { 'expanded_label' : true, 'hidden' : true }, - 'atval_module' : { 'hidden' : false } - }) - ).concat( - list.fm_columns('circ', { - '*' : { 'expanded_label' : true, 'hidden' : true }, - 'circ_due_date' : { 'hidden' : false } - }) - ).concat( - list.fm_columns('acp', { - '*' : { 'expanded_label' : true, 'hidden' : true }, - 'acp_barcode' : { 'hidden' : false } - }) - ).concat( - list.fm_columns('ahr', { - '*' : { 'expanded_label' : true, 'hidden' : true }, - 'ahr_id' : { 'hidden' : false } - }) - ), - 'retrieve_row' : retrieve_row, - 'on_select' : handle_selection - } - ); - - } catch(E) { - var err_prefix = 'trigger_events.js -> init_list() : '; - if (error) error.standard_unexpected_error_alert(err_prefix,E); else alert(err_prefix + E); - } -} - -function retrieve_row(params) { // callback function for fleshing rows in a list - params.treeitem_node.setAttribute('retrieve_id',params.row.my.atev.id()); - params.on_retrieve(params.row); - return params.row; -} - -function handle_selection(ev) { // handler for list row selection event - var sel = list.retrieve_selection(); - if (sel.length > 0) { - $('cmd_cancel_event').setAttribute('disabled','false'); - $('cmd_reset_event').setAttribute('disabled','false'); - } else { - $('cmd_cancel_event').setAttribute('disabled','true'); - $('cmd_reset_event').setAttribute('disabled','true'); - } -}; - -function populate_list() { - try { - - $('circ').disabled = true; $('ahr').disabled = true; $('pending').disabled = true; $('complete').disabled = true; $('error').disabled = true; - - rows = {}; - list.clear(); - - function onResponse(r) { - var evt = openils.Util.readResponse(r); - var row_params = { - 'row' : { - 'my' : { - 'atev' : evt, - 'atevdef' : evt.event_def(), - 'atreact' : evt.event_def().reactor(), - 'atval' : evt.event_def().validator(), - 'circ' : evt.target().classname == 'circ' ? evt.target() : null, - 'ahr' : evt.target().classname == 'ahr' ? evt.target() : null, - 'acp' : evt.target().classname == 'circ' ? evt.target().target_copy() : evt.target().current_copy() - } - } - }; - rows[ evt.id() ] = list.append( row_params ); - - } - - function onError(r) { - var evt = openils.Util.readResponse(r); - alert('error, evt = ' + js2JSON(evt)); - $('circ').disabled = false; $('ahr').disabled = false; $('pending').disabled = false; $('complete').disabled = false; $('error').disabled = false; - } - - var method = $('circ').checked ? 'FM_ATEV_APROPOS_CIRC' : 'FM_ATEV_APROPOS_AHR'; - if (xul_param('copy_id')) { method += '_VIA_COPY'; } - - var filter = {"event":{"state":"complete"}, "order_by":[{"class":"atev", "field":"run_time", "direction":"desc"}]}; - - if ($('pending').checked) { filter.event.state = 'pending'; filter.order_by[0].direction = 'asc'; } - if ($('error').checked) { filter.event.state = 'error'; } - - fieldmapper.standardRequest( - [api[method].app, api[method].method ], - { async: true, - params: [ses(), xul_param('copy_id') || xul_param('patron_id'), filter], - onresponse : onResponse, - onerror : onError, - oncomplete : function() { - $('circ').disabled = false; $('ahr').disabled = false; $('pending').disabled = false; $('complete').disabled = false; $('error').disabled = false; - } - } - ); - - } catch(E) { - var err_prefix = 'trigger_events.js -> populate_list() : '; - if (error) error.standard_unexpected_error_alert(err_prefix,E); else alert(err_prefix + E); - } -} diff --git a/Open-ILS/xul/staff_client/server/patron/trigger_events.xul b/Open-ILS/xul/staff_client/server/patron/trigger_events.xul deleted file mode 100644 index 368d80c3c1..0000000000 --- a/Open-ILS/xul/staff_client/server/patron/trigger_events.xul +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - -]> - - - - - - - - - - - - -