]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/PCrudFilterDialog.js
9ed23ce7dde43c5273ddd1c78448f05f4bf40a47
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / PCrudFilterDialog.js
1 if (!dojo._hasResource['openils.widget.PCrudFilterDialog']) {
2
3     /* openils.widget.PCrudFilterDialog is a dijit that, given a fieldmapper
4      * class, provides a dialog in which users can define inclusionary
5      * filters based on fields selected from the fieldmapper class and values
6      * for those fields.  Operators can be selected so that not only equality
7      * comparisons are possible in the filter, but also inequality filters,
8      * likeness (for text fields only) betweenness, and nullity tests.
9      *
10      * The dijit yields its result in the form of a JSON query suitable for
11      * use as the where clause of a pcrud search, via the onApply callback.
12      *
13      * In addition to its fmClass paramter, note the useful parameter
14      * suppressFilterFields.  Say for instance you're using this dijit
15      * on an fmClass like "brt" which has a field "record" that points to the
16      * bre class.  The AutoWidget provided for users to enter values for
17      * comparisons on the record field would be a dropdown containing all
18      * the bre ID's in the system!  That would be unusable in any realistic
19      * system, unless/until we teach AutoWidget to use a lazy-loading store
20      * for dropdowns.
21      *
22      * The comparisons in each filter row are "and-ed" together in the JSON
23      * query yielded, except for repetitions of the same field, which are
24      * "or-ed" together /within/ the overall "and" group.  Look at comments
25      * within PCrudFilterRowManager.compile() for more information.
26      *
27      * AutoGrid has some ability to use this dijit to offer a filtering dialog,
28      * but be aware that the filtering dialog is /not/ aware of other
29      * fitering measures in place in a given AutoGrid-based interface, such as
30      * (typically) context org unit selectors, and therefore using the context
31      * org unit selector will not respect selected filters in this dijit, and
32      * vice-versa.
33      */
34     dojo.provide('openils.widget.PCrudFilterDialog');
35     dojo.require('openils.widget.AutoFieldWidget');
36     dojo.require('dijit.form.FilteringSelect');
37     dojo.require('dijit.form.Button');
38     dojo.require('dojo.data.ItemFileReadStore');
39     dojo.require('dijit.Dialog');
40     dojo.require('openils.Util');
41
42     dojo.requireLocalization("openils.widget", "PCrudFilterDialog");
43     var localeStrings = dojo.i18n.getLocalization(
44         "openils.widget", "PCrudFilterDialog"
45     );
46
47     /* These are the operators that make up the central dropdown in each
48      * row of the widget.  When fields of different datatypes are selected,
49      * some of these operators may be masked via the "minimal" and "strict"
50      * properties.
51      */
52     var _operator_store = new dojo.data.ItemFileReadStore(
53         {
54             "data": {
55                 "identifier": "name",
56                 "items": [
57                     {
58                         "name": "=",
59                         "label": localeStrings.OPERATOR_EQ,
60                         "param_count": 1,
61                         "minimal": true,
62                         "strict": true
63                     }, {
64                         "name": "!=",
65                         "label": localeStrings.OPERATOR_NE,
66                         "param_count": 1,
67                         "minimal": true,
68                         "strict": true
69                     }, {
70                         "name": "null",
71                         "label": localeStrings.OPERATOR_IS_NULL,
72                         "param_count": 0,
73                         "minimal": true,
74                         "strict": true
75                     }, {
76                         "name": "not null",
77                         "label": localeStrings.OPERATOR_IS_NOT_NULL,
78                         "param_count": 0,
79                         "minimal": true,
80                         "strict": true
81                     }, {
82                         "name": ">",
83                         "label": localeStrings.OPERATOR_GT,
84                         "param_count": 1,
85                         "strict": true
86                     }, {
87                         "name": "<",
88                         "label": localeStrings.OPERATOR_LT,
89                         "param_count": 1,
90                         "strict": true
91                     }, {
92                         "name": ">=",
93                         "label": localeStrings.OPERATOR_GTE,
94                         "param_count": 1,
95                         "strict": true
96                     }, {
97                         "name": "<=",
98                         "label": localeStrings.OPERATOR_LTE,
99                         "param_count": 1,
100                         "strict": true
101                     }, {
102                         "name": "between",
103                         "label": localeStrings.OPERATOR_BETWEEN,
104                         "param_count": 2,
105                         "strict": true
106                     }, {
107                         "name": "not between",
108                         "label": localeStrings.OPERATOR_NOT_BETWEEN,
109                         "param_count": 2,
110                         "strict": true
111                     }, {
112                         "name": "like",
113                         "label": localeStrings.OPERATOR_LIKE,
114                         "param_count": 1
115                     }, {
116                         "name": "not like",
117                         "label": localeStrings.OPERATOR_NOT_LIKE,
118                         "param_count": 1
119                     }
120                 ]
121             }
122         }
123     );
124
125     /* The text datatype supports all the above operators for comparisons. */
126     var _store_query_by_datatype = {"text": {}};
127
128     /* These three datatypes support only minimal operators. */
129     ["bool", "link", "org_unit"].forEach(
130         function(type) {
131             _store_query_by_datatype[type] = {"minimal": true};
132         }
133     );
134
135     /* These datatypes support strict operators (everything save [not] like). */
136     ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach(
137         function(type) {
138             _store_query_by_datatype[type] = {"strict": true};
139         }
140     );
141
142     /* This helps convert things that pcrud won't accept ("not between", "not
143      * like") into proper JSON query expressions.
144      * It returns false if a clause doesn't have any such negative operator,
145      * or it returns true AND gets rid of the "not " part in the clause
146      * object itself.  It's up to the caller to wrap it in {"-not": {}} in
147      * the right place. */
148     function _clause_was_negative(clause) {
149         /* clause objects really only ever have one property */
150         var ops = openils.Util.objectProperties(clause);
151         var op = ops.pop();
152         var matches = op.match(/^not (\w+)$/);
153         if (matches) {
154             clause[matches[1]] = clause[op];
155             delete clause[op];
156             return true;
157         }
158         return false;
159     }
160
161     /* This is not the dijit per se. Search further in this file for
162      * "dojo.declare" for the beginning of the dijit.
163      *
164      * This is, however, the object that represents a collection of filter
165      * rows and knows how to compile a filter from those rows. */
166     function PCrudFilterRowManager() {
167         var self = this;
168
169         this._init = function(container, field_store, fm_class) {
170             this.container = container;
171             this.field_store = field_store;
172             this.fm_class = fm_class;
173
174             this.rows = {};
175             this.row_index = 0;
176
177             this._build_table();
178         };
179
180         this._build_table = function() {
181             this.table = dojo.create(
182                 "table", {
183                     "className": "oils-pcrudfilterdialog-table"
184                 }, this.container
185             );
186
187             var tr = dojo.create(
188                 "tr", {
189                     "id": "pcrudfilterdialog-empty",
190                     "className": "hidden"
191                 }, this.table
192             );
193
194             dojo.create(
195                 "td", {
196                     "colspan": 4,
197                     "innerHTML": localeStrings.EMPTY_CASE
198                 }, tr
199             );
200
201             this.add_row();
202         };
203
204         this._compile_second_pass = function(first_pass) {
205             var and = [];
206             var result = {"-and": and};
207
208             for (var field in first_pass) {
209                 var list = first_pass[field];
210                 if (list.length == 1) {
211                     var obj = {};
212                     var clause = list.pop();
213                     if (_clause_was_negative(clause)) {
214                         obj["-not"] = {};
215                         obj["-not"][field] = clause;
216                     } else {
217                         obj[field] = clause;
218                     }
219                     and.push(obj);
220                 } else {
221                     var or = list.map(
222                         function(clause) {
223                             var obj = {};
224                             if (_clause_was_negative(clause)) {
225                                 obj["-not"] = {};
226                                 obj["-not"][field] = clause;
227                             } else {
228                                 obj[field] = clause;
229                             }
230                             return obj;
231                         }
232                     );
233                     and.push({"-or": or});
234                 }
235             }
236
237             return result;
238         };
239
240         this.add_row = function() {
241             this.hide_empty_placeholder();
242             var row_id = this.row_index++;
243             this.rows[row_id] = new PCrudFilterRow(this, row_id);
244         };
245
246         this.remove_row = function(row_id) {
247             this.rows[row_id].destroy();
248             delete this.rows[row_id];
249
250             if (openils.Util.objectProperties(this.rows).length < 1)
251                 this.show_empty_placeholder();
252         };
253
254         this.hide_empty_placeholder = function() {
255             openils.Util.hide("pcrudfilterdialog-empty");
256         };
257
258         this.show_empty_placeholder = function() {
259             openils.Util.show("pcrudfilterdialog-empty");
260         };
261
262         this.compile = function() {
263             /* We'll prepare a first-pass data structure that looks like:
264              * {
265              *  field1: [{"op": "one value"}],
266              *  field2: [{"op": "a value"}, {"op": "b value"}],
267              *  field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
268              * }
269              *
270              * which will be passed to _compile_second_pass() to yield an
271              * actual filter suitable for pcrud (with -and and -or in all the
272              * right places) so the above example would come out like:
273              *
274              * { "-and": [
275              *   {"field1": {"op": "one value"}},
276              *   {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
277              *   {"-or": [
278              *     {"field3": {"op": "first value"}},
279              *     {"field3": {"op": ["range start", "range end"]}}
280              *   ] }
281              * ] }
282              */
283             var first_pass = {};
284
285             for (var row_id in this.rows) {
286                 var row = this.rows[row_id];
287                 var value = row.compile();
288                 var field = row.selected_field;
289
290                 if (typeof(value) != "undefined" &&
291                     typeof(field) != "undefined") {
292                     if (!first_pass[field])
293                         first_pass[field] = [];
294                     first_pass[field].push(value);
295                 }
296             }
297
298             /* Don't return an empty filter: pcrud can't use that. */
299             if (openils.Util.objectProperties(first_pass).length < 1) {
300                 var result = {};
301                 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
302                 return result;
303             } else {
304                 return this._compile_second_pass(first_pass);
305             }
306         };
307
308         this._init.apply(this, arguments);
309     }
310
311     /* As the name implies, objects of this class manage a single row of the
312      * query.  Therefore they know about their own field dropdown, their own
313      * selector dropdown, and their own value widget (or widgets in the case
314      * of between searches, which call for two widgets to define a range),
315      * and not much else. */
316     function PCrudFilterRow() {
317         var self = this;
318
319         this._init = function(filter_row_manager, row_id) {
320             this.filter_row_manager = filter_row_manager;
321             this.row_id = row_id;
322
323             this._build();
324         };
325
326         this._build = function() {
327             this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
328
329             this._create_field_selector();
330             this._create_operator_selector();
331             this._create_value_slot();
332             this._create_remover();
333         };
334
335         this._create_field_selector = function() {
336             var td = dojo.create("td", {}, this.tr);
337             this.field_selector = new dijit.form.FilteringSelect(
338                 {
339                     "labelAttr": "label",
340                     "searchAttr": "label",
341                     "scrollOnFocus": false,
342                     "onChange": function(value) {
343                         self.update_selected_field(value);
344                     },
345                     "store": this.filter_row_manager.field_store
346                 }, dojo.create("span", {}, td)
347             );
348         };
349
350         this._create_operator_selector = function() {
351             var td = dojo.create("td", {}, this.tr);
352             this.operator_selector = new dijit.form.FilteringSelect(
353                 {
354                     "labelAttr": "label",
355                     "searchAttr": "label",
356                     "scrollOnFocus": false,
357                     "onChange": function(value) {
358                         self.update_selected_operator(value);
359                     },
360                     "store": _operator_store
361                 }, dojo.create("span", {}, td)
362             );
363         };
364
365         this._adjust_operator_selector = function() {
366             this.operator_selector.attr(
367                 "query", _store_query_by_datatype[this.selected_field_type]
368             );
369             this.operator_selector.reset();
370         };
371
372         this._create_value_slot = function() {
373             this.value_slot = dojo.create("td", {"innerHTML": "-"}, this.tr);
374         };
375
376         this._create_remover = function() {
377             var td = dojo.create("td", {}, this.tr);
378             var anchor = dojo.create(
379                 "a", {
380                     "className": "oils-pcrudfilterdialog-remover",
381                     "innerHTML": "X",
382                     "href": "#",
383                     "onclick": function() {
384                         self.filter_row_manager.remove_row(self.row_id);
385                     }
386                 }, td
387             );
388         };
389
390         this._clear_value_slot = function() {
391             if (this.value_widgets) {
392                 this.value_widgets.forEach(
393                     function(autowidg) { autowidg.widget.destroy(); }
394                 );
395                 delete this.value_widgets;
396             }
397
398             dojo.empty(this.value_slot);
399         };
400
401         this._rebuild_value_widgets = function() {
402             this._clear_value_slot();
403
404             if (!this.get_selected_operator() || !this.selected_field)
405                 return;
406
407             this.value_widgets = [];
408
409             var param_count = this.operator_selector.item.param_count;
410
411             for (var i = 0; i < param_count; i++) {
412                 var widg = new openils.widget.AutoFieldWidget({
413                     "fmClass": this.filter_row_manager.fm_class,
414                     "fmField": this.selected_field,
415                     "parentNode": dojo.create("span", {}, this.value_slot),
416                     "dijitArgs": {"scrollOnFocus": false}
417                 });
418
419                 widg.build();
420                 this.value_widgets.push(widg);
421             }
422         };
423
424         /* for ugly special cases in compliation */
425         this._null_clause = function() {
426             var opname = this.get_selected_operator_name();
427             if (opname == "not null")
428                 return {"!=": null};
429             else if (opname == "null")
430                 return null;
431             else
432                 return;
433         };
434
435         this.get_selected_operator = function() {
436             if (this.operator_selector)
437                 return this.operator_selector.item;
438         };
439
440         this.get_selected_operator_name = function() {
441             var op = this.get_selected_operator();
442             return op ? op.name : null;
443         };
444
445         this.update_selected_operator = function(value) {
446             this._rebuild_value_widgets();
447         };
448
449         this.update_selected_field = function(value) {
450             if (this.field_selector.item) {
451                 this.selected_field = value;
452                 this.selected_field_type = this.field_selector.item.type;
453                 this._adjust_operator_selector();
454                 this._rebuild_value_widgets();
455             }
456         };
457
458         this.compile = function() {
459             if (this.value_widgets) {
460                 var values = this.value_widgets.map(
461                     function(widg) { return widg.getFormattedValue(); }
462                 );
463
464                 if (!values.length) {
465                     return this._null_clause(); /* null/not null */
466                 } else {
467                     var clause = {};
468                     var op = this.get_selected_operator_name();
469                     if (values.length == 1)
470                         clause[op] = values.pop();
471                     else
472                         clause[op] = values;
473                     return clause;
474                 }
475             } else {
476                 return;
477             }
478         };
479
480         this.destroy = function() {
481             this._clear_value_slot();
482             this.field_selector.destroy();
483             if (this.operator_selector)
484                 this.operator_selector.destroy();
485
486             dojo.destroy(this.tr);
487         };
488
489         this._init.apply(this, arguments);
490     }
491
492     dojo.declare(
493         'openils.widget.PCrudFilterDialog',
494         [dijit.Dialog, openils.widget.AutoWidget],
495         {
496
497             constructor : function(args) {
498                 for(var k in args)
499                     this[k] = args[k];
500                 this.title = this.title || localeStrings.DEFAULT_DIALOG_TITLE;
501                 this.widgetIndex = 0;
502                 this.widgetCache = {};
503             },
504
505             /* All we really do here is create a data store out of the fields
506              * from the IDL for our given class, place a few buttons at the
507              * bottom of the dialog, and hand off to PCrudFilterRowManager to
508              * do the actual work.
509              */
510
511             startup : function() {
512                 var self = this;
513                 this.inherited(arguments);
514                 this.initAutoEnv();
515                 var realFieldList = this.sortedFieldList.filter(
516                     function(item) { return !(item.virtual || item.nonIdl); }
517                 );
518
519                 /* Prevent any explicitly unwanted fields from being available
520                  * in our field dropdowns. */
521                 if (dojo.isArray(this.suppressFilterFields)) {
522                     realFieldList = realFieldList.filter(
523                         function(item) {
524                             for (
525                                 var i = 0;
526                                 i < self.suppressFilterFields.length;
527                                 i++
528                             ) {
529                                 if (item.name == self.suppressFilterFields[i])
530                                     return false;
531                             }
532                             return true;
533                         }
534                     );
535                 }
536
537                 this.fieldStore = new dojo.data.ItemFileReadStore({
538                     "data": {
539                         "identifier": "name",
540                         "name": "label",
541                         "items": realFieldList.map(
542                             function(item) {
543                                 return {
544                                     "label": item.label,
545                                     "name": item.name,
546                                     "type": item.datatype
547                                 };
548                             }
549                         )
550                     }
551                 });
552
553                 this.filter_row_manager = new PCrudFilterRowManager(
554                     dojo.create("div", {}, this.domNode),
555                     this.fieldStore, this.fmClass
556                 );
557
558                 var button_holder = dojo.create(
559                     "div", {
560                         "className": "oils-pcrudfilterdialog-buttonholder"
561                     }, this.domNode
562                 );
563
564                 new dijit.form.Button(
565                     {
566                         "label": localeStrings.ADD_ROW,
567                         "scrollOnFocus": false, /* almost always better */
568                         "onClick": function() {
569                             self.filter_row_manager.add_row();
570                         }
571                     }, dojo.create("span", {}, button_holder)
572                 );
573
574                 new dijit.form.Button(
575                     {
576                         "label": localeStrings.APPLY,
577                         "scrollOnFocus": false,
578                         "onClick": function() {
579                             if (self.onApply)
580                                 self.onApply(self.filter_row_manager.compile());
581                             self.hide();
582                         }
583                     }, dojo.create("span", {}, button_holder)
584                 );
585
586                 new dijit.form.Button(
587                     {
588                         "label": localeStrings.CANCEL,
589                         "scrollOnFocus": false,
590                         "onClick": function() {
591                             if (self.onCancel)
592                             self.onCancel();
593                             self.hide();
594                         }
595                     }, dojo.create("span", {}, button_holder)
596                 );
597             }
598         }
599     );
600 }