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