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
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');
43 dojo.requireLocalization("openils.widget", "PCrudFilterDialog");
45 /* XXX namespace pollution! arg! Fix this whole module sometime. */
46 var localeStrings = dojo.i18n.getLocalization(
47 "openils.widget", "PCrudFilterDialog"
50 /* These are the operators that make up the central dropdown in each
51 * row of the widget. When fields of different datatypes are selected,
52 * some of these operators may be masked via the "minimal" and "strict"
55 var _operator_store = new dojo.data.ItemFileReadStore(
62 "label": localeStrings.OPERATOR_EQ,
68 "label": localeStrings.OPERATOR_NE,
74 "label": localeStrings.OPERATOR_IS_NULL,
80 "label": localeStrings.OPERATOR_IS_NOT_NULL,
86 "label": localeStrings.OPERATOR_GT,
91 "label": localeStrings.OPERATOR_LT,
96 "label": localeStrings.OPERATOR_GTE,
101 "label": localeStrings.OPERATOR_LTE,
106 "label": localeStrings.OPERATOR_BETWEEN,
110 "name": "not between",
111 "label": localeStrings.OPERATOR_NOT_BETWEEN,
116 "label": localeStrings.OPERATOR_LIKE,
120 "label": localeStrings.OPERATOR_NOT_LIKE,
128 /* The text datatype supports all the above operators for comparisons. */
129 var _store_query_by_datatype = {"text": {}};
131 /* These three datatypes support only minimal operators. */
132 ["bool", "link", "org_unit"].forEach(
134 _store_query_by_datatype[type] = {"minimal": true};
138 /* These datatypes support strict operators (everything save [not] like). */
139 ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach(
141 _store_query_by_datatype[type] = {"strict": true};
145 /* This helps convert things that pcrud won't accept ("not between", "not
146 * like") into proper JSON query expressions.
147 * It returns false if a clause doesn't have any such negative operator,
148 * or it returns true AND gets rid of the "not " part in the clause
149 * object itself. It's up to the caller to wrap it in {"-not": {}} in
150 * the right place. */
151 function _clause_was_negative(clause) {
152 /* clause objects really only ever have one property */
153 var ops = openils.Util.objectProperties(clause);
155 var matches = op.match(/^not (\w+)$/);
157 clause[matches[1]] = clause[op];
164 /* This is not the dijit per se. Search further in this file for
165 * "dojo.declare" for the beginning of the dijit.
167 * This is, however, the object that represents a collection of filter
168 * rows and knows how to compile a filter from those rows. */
169 function PCrudFilterRowManager() {
172 this._init = function(container, field_store, fm_class) {
173 this.container = container;
174 this.field_store = field_store;
175 this.fm_class = fm_class;
183 this._build_table = function() {
184 this.table = dojo.create(
186 "className": "oils-pcrudfilterdialog-table"
190 var tr = dojo.create(
192 "id": "pcrudfilterdialog-empty",
193 "className": "hidden"
200 "innerHTML": localeStrings.EMPTY_CASE
207 this._compile_second_pass = function(first_pass) {
209 var result = {"-and": and};
211 for (var field in first_pass) {
212 var list = first_pass[field];
213 if (list.length == 1) {
215 var clause = list.pop();
216 if (_clause_was_negative(clause)) {
218 obj["-not"][field] = clause;
227 if (_clause_was_negative(clause)) {
229 obj["-not"][field] = clause;
236 and.push({"-or": or});
243 this.add_row = function() {
244 this.hide_empty_placeholder();
245 var row_id = this.row_index++;
246 this.rows[row_id] = new PCrudFilterRow(this, row_id);
249 this.remove_row = function(row_id) {
250 this.rows[row_id].destroy();
251 delete this.rows[row_id];
253 if (openils.Util.objectProperties(this.rows).length < 1)
254 this.show_empty_placeholder();
257 this.hide_empty_placeholder = function() {
258 openils.Util.hide("pcrudfilterdialog-empty");
261 this.show_empty_placeholder = function() {
262 openils.Util.show("pcrudfilterdialog-empty");
265 this.compile = function() {
266 /* We'll prepare a first-pass data structure that looks like:
268 * field1: [{"op": "one value"}],
269 * field2: [{"op": "a value"}, {"op": "b value"}],
270 * field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
273 * which will be passed to _compile_second_pass() to yield an
274 * actual filter suitable for pcrud (with -and and -or in all the
275 * right places) so the above example would come out like:
278 * {"field1": {"op": "one value"}},
279 * {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
281 * {"field3": {"op": "first value"}},
282 * {"field3": {"op": ["range start", "range end"]}}
288 for (var row_id in this.rows) {
289 var row = this.rows[row_id];
290 var value = row.compile();
291 var field = row.selected_field;
293 if (typeof(value) != "undefined" &&
294 typeof(field) != "undefined") {
295 if (!first_pass[field])
296 first_pass[field] = [];
297 first_pass[field].push(value);
301 /* Don't return an empty filter: pcrud can't use that. */
302 if (openils.Util.objectProperties(first_pass).length < 1) {
304 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
307 return this._compile_second_pass(first_pass);
311 this._init.apply(this, arguments);
314 /* As the name implies, objects of this class manage a single row of the
315 * query. Therefore they know about their own field dropdown, their own
316 * selector dropdown, and their own value widget (or widgets in the case
317 * of between searches, which call for two widgets to define a range),
318 * and not much else. */
319 function PCrudFilterRow() {
322 this._init = function(filter_row_manager, row_id) {
323 this.filter_row_manager = filter_row_manager;
324 this.row_id = row_id;
329 this._build = function() {
330 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
332 this._create_field_selector();
333 this._create_operator_selector();
334 this._create_value_slot();
335 this._create_remover();
338 this._create_field_selector = function() {
339 var td = dojo.create("td", {}, this.tr);
340 this.field_selector = new dijit.form.FilteringSelect(
342 "labelAttr": "label",
343 "searchAttr": "label",
344 "scrollOnFocus": false,
345 "onChange": function(value) {
346 self.update_selected_field(value);
348 "store": this.filter_row_manager.field_store
349 }, dojo.create("span", {}, td)
353 this._create_operator_selector = function() {
354 var td = dojo.create("td", {}, this.tr);
355 this.operator_selector = new dijit.form.FilteringSelect(
357 "labelAttr": "label",
358 "searchAttr": "label",
359 "scrollOnFocus": false,
360 "onChange": function(value) {
361 self.update_selected_operator(value);
363 "store": _operator_store
364 }, dojo.create("span", {}, td)
368 this._adjust_operator_selector = function() {
369 this.operator_selector.attr(
370 "query", _store_query_by_datatype[this.selected_field_type]
372 this.operator_selector.reset();
375 this._create_value_slot = function() {
376 this.value_slot = dojo.create("td", {"innerHTML": "-"}, this.tr);
379 this._create_remover = function() {
380 var td = dojo.create("td", {}, this.tr);
381 var anchor = dojo.create(
383 "className": "oils-pcrudfilterdialog-remover",
386 "onclick": function() {
387 self.filter_row_manager.remove_row(self.row_id);
393 this._clear_value_slot = function() {
394 if (this.value_widgets) {
395 this.value_widgets.forEach(
396 function(autowidg) { autowidg.widget.destroy(); }
398 delete this.value_widgets;
401 dojo.empty(this.value_slot);
404 this._rebuild_value_widgets = function() {
405 this._clear_value_slot();
407 if (!this.get_selected_operator() || !this.selected_field)
410 this.value_widgets = [];
412 var param_count = this.operator_selector.item.param_count;
414 for (var i = 0; i < param_count; i++) {
415 var widg = new openils.widget.AutoFieldWidget({
416 "fmClass": this.selected_field_fm_class,
417 "fmField": this.selected_field_fm_field,
418 "parentNode": dojo.create("span", {}, this.value_slot),
419 "dijitArgs": {"scrollOnFocus": false}
423 this.value_widgets.push(widg);
427 /* for ugly special cases in compliation */
428 this._null_clause = function() {
429 var opname = this.get_selected_operator_name();
430 if (opname == "not null")
432 else if (opname == "null")
438 this.get_selected_operator = function() {
439 if (this.operator_selector)
440 return this.operator_selector.item;
443 this.get_selected_operator_name = function() {
444 var op = this.get_selected_operator();
445 return op ? op.name : null;
448 this.update_selected_operator = function(value) {
449 this._rebuild_value_widgets();
452 this.update_selected_field = function(value) {
453 if (this.field_selector.item) {
454 this.selected_field = value;
455 this.selected_field_type = this.field_selector.item.type;
457 /* This is really about supporting flattenergrid, of which
458 * we're in the superclass (in a sloppy sad way). From now
459 * on I won't mix this kind of lazy object with Dojo modules. */
460 //console.log(dojo.toJson(this.field_selector.item));
461 this.selected_field_fm_field = this.field_selector.item.name;
462 this.selected_field_is_indirect =
463 this.field_selector.item.indirect || false;
464 this.selected_field_fm_class =
465 this.field_selector.item.fmClass ||
466 this.filter_row_manager.fm_class;
468 this._adjust_operator_selector();
469 this._rebuild_value_widgets();
473 this.compile = function() {
474 if (this.value_widgets) {
475 var values = this.value_widgets.map(
477 return self.selected_field_is_indirect ?
478 widg.widget.attr('displayedValue') :
479 widg.getFormattedValue();
483 if (!values.length) {
484 return this._null_clause(); /* null/not null */
487 var op = this.get_selected_operator_name();
488 if (values.length == 1)
489 clause[op] = values.pop();
499 this.destroy = function() {
500 this._clear_value_slot();
501 this.field_selector.destroy();
502 if (this.operator_selector)
503 this.operator_selector.destroy();
505 dojo.destroy(this.tr);
508 this._init.apply(this, arguments);
512 'openils.widget.PCrudFilterDialog',
513 [dijit.Dialog, openils.widget.AutoWidget],
516 constructor : function(args) {
519 this.title = this.title || localeStrings.DEFAULT_DIALOG_TITLE;
520 this.widgetIndex = 0;
521 this.widgetCache = {};
524 _buildButtons : function() {
527 var button_holder = dojo.create(
529 "className": "oils-pcrudfilterdialog-buttonholder"
533 new dijit.form.Button(
535 "label": localeStrings.ADD_ROW,
536 "scrollOnFocus": false, /* almost always better */
537 "onClick": function() {
538 self.filter_row_manager.add_row();
540 }, dojo.create("span", {}, button_holder)
543 new dijit.form.Button(
545 "label": localeStrings.APPLY,
546 "scrollOnFocus": false,
547 "onClick": function() {
549 self.onApply(self.filter_row_manager.compile());
552 }, dojo.create("span", {}, button_holder)
555 new dijit.form.Button(
557 "label": localeStrings.CANCEL,
558 "scrollOnFocus": false,
559 "onClick": function() {
564 }, dojo.create("span", {}, button_holder)
568 _buildFieldStore : function() {
570 var realFieldList = this.sortedFieldList.filter(
571 function(item) { return !(item.virtual || item.nonIdl); }
574 /* Prevent any explicitly unwanted fields from being available
575 * in our field dropdowns. */
576 if (dojo.isArray(this.suppressFilterFields)) {
577 realFieldList = realFieldList.filter(
581 i < self.suppressFilterFields.length;
584 if (item.name == self.suppressFilterFields[i])
592 this.fieldStore = new dojo.data.ItemFileReadStore({
594 "identifier": "name",
596 "items": realFieldList.map(
601 "type": item.datatype
609 /* All we really do here is create a data store out of the fields
610 * from the IDL for our given class, place a few buttons at the
611 * bottom of the dialog, and hand off to PCrudFilterRowManager to
612 * do the actual work.
615 startup : function() {
617 this.inherited(arguments);
620 this._buildFieldStore();
622 this.filter_row_manager = new PCrudFilterRowManager(
623 dojo.create("div", {}, this.domNode),
624 this.fieldStore, this.fmClass
627 this._buildButtons();