1 if (!dojo._hasResource['openils.widget.PCrudFilterDialog']) {
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.
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.
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
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.
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
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');
42 dojo.requireLocalization("openils.widget", "PCrudFilterDialog");
43 var localeStrings = dojo.i18n.getLocalization(
44 "openils.widget", "PCrudFilterDialog"
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"
52 var _operator_store = new dojo.data.ItemFileReadStore(
59 "label": localeStrings.OPERATOR_EQ,
65 "label": localeStrings.OPERATOR_NE,
71 "label": localeStrings.OPERATOR_IS_NULL,
77 "label": localeStrings.OPERATOR_IS_NOT_NULL,
83 "label": localeStrings.OPERATOR_GT,
88 "label": localeStrings.OPERATOR_LT,
93 "label": localeStrings.OPERATOR_GTE,
98 "label": localeStrings.OPERATOR_LTE,
103 "label": localeStrings.OPERATOR_BETWEEN,
107 "name": "not between",
108 "label": localeStrings.OPERATOR_NOT_BETWEEN,
113 "label": localeStrings.OPERATOR_LIKE,
117 "label": localeStrings.OPERATOR_NOT_LIKE,
125 /* The text datatype supports all the above operators for comparisons. */
126 var _store_query_by_datatype = {"text": {}};
128 /* These three datatypes support only minimal operators. */
129 ["bool", "link", "org_unit"].forEach(
131 _store_query_by_datatype[type] = {"minimal": true};
135 /* These datatypes support strict operators (everything save [not] like). */
136 ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach(
138 _store_query_by_datatype[type] = {"strict": true};
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);
152 var matches = op.match(/^not (\w+)$/);
154 clause[matches[1]] = clause[op];
161 /* This is not the dijit per se. Search further in this file for
162 * "dojo.declare" for the beginning of the dijit.
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() {
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;
180 this._build_table = function() {
181 this.table = dojo.create(
183 "className": "oils-pcrudfilterdialog-table"
187 var tr = dojo.create(
189 "id": "pcrudfilterdialog-empty",
190 "className": "hidden"
197 "innerHTML": localeStrings.EMPTY_CASE
204 this._compile_second_pass = function(first_pass) {
206 var result = {"-and": and};
208 for (var field in first_pass) {
209 var list = first_pass[field];
210 if (list.length == 1) {
212 var clause = list.pop();
213 if (_clause_was_negative(clause)) {
215 obj["-not"][field] = clause;
224 if (_clause_was_negative(clause)) {
226 obj["-not"][field] = clause;
233 and.push({"-or": or});
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);
246 this.remove_row = function(row_id) {
247 this.rows[row_id].destroy();
248 delete this.rows[row_id];
250 if (openils.Util.objectProperties(this.rows).length < 1)
251 this.show_empty_placeholder();
254 this.hide_empty_placeholder = function() {
255 openils.Util.hide("pcrudfilterdialog-empty");
258 this.show_empty_placeholder = function() {
259 openils.Util.show("pcrudfilterdialog-empty");
262 this.compile = function() {
263 /* We'll prepare a first-pass data structure that looks like:
265 * field1: [{"op": "one value"}],
266 * field2: [{"op": "a value"}, {"op": "b value"}],
267 * field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
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:
275 * {"field1": {"op": "one value"}},
276 * {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
278 * {"field3": {"op": "first value"}},
279 * {"field3": {"op": ["range start", "range end"]}}
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;
290 if (typeof(value) != "undefined" &&
291 typeof(field) != "undefined") {
292 if (!first_pass[field])
293 first_pass[field] = [];
294 first_pass[field].push(value);
298 /* Don't return an empty filter: pcrud can't use that. */
299 if (openils.Util.objectProperties(first_pass).length < 1) {
301 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
304 return this._compile_second_pass(first_pass);
308 this._init.apply(this, arguments);
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() {
319 this._init = function(filter_row_manager, row_id) {
320 this.filter_row_manager = filter_row_manager;
321 this.row_id = row_id;
326 this._build = function() {
327 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
329 this._create_field_selector();
330 this._create_operator_selector();
331 this._create_value_slot();
332 this._create_remover();
335 this._create_field_selector = function() {
336 var td = dojo.create("td", {}, this.tr);
337 this.field_selector = new dijit.form.FilteringSelect(
339 "labelAttr": "label",
340 "searchAttr": "label",
341 "scrollOnFocus": false,
342 "onChange": function(value) {
343 self.update_selected_field(value);
345 "store": this.filter_row_manager.field_store
346 }, dojo.create("span", {}, td)
350 this._create_operator_selector = function() {
351 var td = dojo.create("td", {}, this.tr);
352 this.operator_selector = new dijit.form.FilteringSelect(
354 "labelAttr": "label",
355 "searchAttr": "label",
356 "scrollOnFocus": false,
357 "onChange": function(value) {
358 self.update_selected_operator(value);
360 "store": _operator_store
361 }, dojo.create("span", {}, td)
365 this._adjust_operator_selector = function() {
366 this.operator_selector.attr(
367 "query", _store_query_by_datatype[this.selected_field_type]
369 this.operator_selector.reset();
372 this._create_value_slot = function() {
373 this.value_slot = dojo.create("td", {"innerHTML": "-"}, this.tr);
376 this._create_remover = function() {
377 var td = dojo.create("td", {}, this.tr);
378 var anchor = dojo.create(
380 "className": "oils-pcrudfilterdialog-remover",
383 "onclick": function() {
384 self.filter_row_manager.remove_row(self.row_id);
390 this._clear_value_slot = function() {
391 if (this.value_widgets) {
392 this.value_widgets.forEach(
393 function(autowidg) { autowidg.widget.destroy(); }
395 delete this.value_widgets;
398 dojo.empty(this.value_slot);
401 this._rebuild_value_widgets = function() {
402 this._clear_value_slot();
404 if (!this.get_selected_operator() || !this.selected_field)
407 this.value_widgets = [];
409 var param_count = this.operator_selector.item.param_count;
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}
420 this.value_widgets.push(widg);
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")
429 else if (opname == "null")
435 this.get_selected_operator = function() {
436 if (this.operator_selector)
437 return this.operator_selector.item;
440 this.get_selected_operator_name = function() {
441 var op = this.get_selected_operator();
442 return op ? op.name : null;
445 this.update_selected_operator = function(value) {
446 this._rebuild_value_widgets();
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();
458 this.compile = function() {
459 if (this.value_widgets) {
460 var values = this.value_widgets.map(
461 function(widg) { return widg.getFormattedValue(); }
464 if (!values.length) {
465 return this._null_clause(); /* null/not null */
468 var op = this.get_selected_operator_name();
469 if (values.length == 1)
470 clause[op] = values.pop();
480 this.destroy = function() {
481 this._clear_value_slot();
482 this.field_selector.destroy();
483 if (this.operator_selector)
484 this.operator_selector.destroy();
486 dojo.destroy(this.tr);
489 this._init.apply(this, arguments);
493 'openils.widget.PCrudFilterDialog',
494 [dijit.Dialog, openils.widget.AutoWidget],
497 constructor : function(args) {
500 this.title = this.title || localeStrings.DEFAULT_DIALOG_TITLE;
501 this.widgetIndex = 0;
502 this.widgetCache = {};
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.
511 startup : function() {
513 this.inherited(arguments);
515 var realFieldList = this.sortedFieldList.filter(
516 function(item) { return !(item.virtual || item.nonIdl); }
519 /* Prevent any explicitly unwanted fields from being available
520 * in our field dropdowns. */
521 if (dojo.isArray(this.suppressFilterFields)) {
522 realFieldList = realFieldList.filter(
526 i < self.suppressFilterFields.length;
529 if (item.name == self.suppressFilterFields[i])
537 this.fieldStore = new dojo.data.ItemFileReadStore({
539 "identifier": "name",
541 "items": realFieldList.map(
546 "type": item.datatype
553 this.filter_row_manager = new PCrudFilterRowManager(
554 dojo.create("div", {}, this.domNode),
555 this.fieldStore, this.fmClass
558 var button_holder = dojo.create(
560 "className": "oils-pcrudfilterdialog-buttonholder"
564 new dijit.form.Button(
566 "label": localeStrings.ADD_ROW,
567 "scrollOnFocus": false, /* almost always better */
568 "onClick": function() {
569 self.filter_row_manager.add_row();
571 }, dojo.create("span", {}, button_holder)
574 new dijit.form.Button(
576 "label": localeStrings.APPLY,
577 "scrollOnFocus": false,
578 "onClick": function() {
580 self.onApply(self.filter_row_manager.compile());
583 }, dojo.create("span", {}, button_holder)
586 new dijit.form.Button(
588 "label": localeStrings.CANCEL,
589 "scrollOnFocus": false,
590 "onClick": function() {
595 }, dojo.create("span", {}, button_holder)