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 var pcFilterLocaleStrings = dojo.i18n.getLocalization(
46 "openils.widget", "PCrudFilterDialog"
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"
54 var _operator_store = new dojo.data.ItemFileReadStore(
61 "label": pcFilterLocaleStrings.OPERATOR_EQ,
67 "label": pcFilterLocaleStrings.OPERATOR_NE,
73 "label": pcFilterLocaleStrings.OPERATOR_IS_NULL,
79 "label": pcFilterLocaleStrings.OPERATOR_IS_NOT_NULL,
85 "label": pcFilterLocaleStrings.OPERATOR_GT,
90 "label": pcFilterLocaleStrings.OPERATOR_LT,
95 "label": pcFilterLocaleStrings.OPERATOR_GTE,
100 "label": pcFilterLocaleStrings.OPERATOR_LTE,
105 "label": pcFilterLocaleStrings.OPERATOR_BETWEEN,
109 "name": "not between",
110 "label": pcFilterLocaleStrings.OPERATOR_NOT_BETWEEN,
115 "label": pcFilterLocaleStrings.OPERATOR_LIKE,
119 "label": pcFilterLocaleStrings.OPERATOR_NOT_LIKE,
127 /* The text datatype supports all the above operators for comparisons. */
128 var _store_query_by_datatype = {"text": {}};
130 /* These three datatypes support only minimal operators. */
131 ["bool", "link", "org_unit"].forEach(
133 _store_query_by_datatype[type] = {"minimal": true};
137 /* These datatypes support strict operators (everything save [not] like). */
138 ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach(
140 _store_query_by_datatype[type] = {"strict": true};
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);
154 var matches = op.match(/^not (\w+)$/);
156 clause[matches[1]] = clause[op];
163 /* This is not the dijit per se. Search further in this file for
164 * "dojo.declare" for the beginning of the dijit.
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() {
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;
182 this._build_table = function() {
183 this.table = dojo.create(
185 "className": "oils-pcrudfilterdialog-table"
189 var tr = dojo.create(
191 "id": "pcrudfilterdialog-empty",
192 "className": "hidden"
199 "innerHTML": pcFilterLocaleStrings.EMPTY_CASE
206 this._compile_second_pass = function(first_pass) {
208 var result = {"-and": and};
210 for (var field in first_pass) {
211 var list = first_pass[field];
212 if (list.length == 1) {
214 var clause = list.pop();
215 if (_clause_was_negative(clause)) {
217 obj["-not"][field] = clause;
226 if (_clause_was_negative(clause)) {
228 obj["-not"][field] = clause;
235 and.push({"-or": or});
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);
248 this.remove_row = function(row_id) {
249 this.rows[row_id].destroy();
250 delete this.rows[row_id];
252 if (openils.Util.objectProperties(this.rows).length < 1)
253 this.show_empty_placeholder();
256 this.hide_empty_placeholder = function() {
257 openils.Util.hide("pcrudfilterdialog-empty");
260 this.show_empty_placeholder = function() {
261 openils.Util.show("pcrudfilterdialog-empty");
264 this.compile = function() {
265 /* We'll prepare a first-pass data structure that looks like:
267 * field1: [{"op": "one value"}],
268 * field2: [{"op": "a value"}, {"op": "b value"}],
269 * field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
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:
277 * {"field1": {"op": "one value"}},
278 * {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
280 * {"field3": {"op": "first value"}},
281 * {"field3": {"op": ["range start", "range end"]}}
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;
292 if (typeof(value) != "undefined" &&
293 typeof(field) != "undefined") {
294 if (!first_pass[field])
295 first_pass[field] = [];
296 first_pass[field].push(value);
300 /* Don't return an empty filter: pcrud can't use that. */
301 if (openils.Util.objectProperties(first_pass).length < 1) {
303 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
306 return this._compile_second_pass(first_pass);
310 this._init.apply(this, arguments);
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() {
321 this._init = function(filter_row_manager, row_id) {
322 this.filter_row_manager = filter_row_manager;
323 this.row_id = row_id;
328 this._build = function() {
329 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
331 this._create_field_selector();
332 this._create_operator_selector();
333 this._create_value_slot();
334 this._create_remover();
337 this._create_field_selector = function() {
338 var td = dojo.create("td", {}, this.tr);
339 this.field_selector = new dijit.form.FilteringSelect(
341 "labelAttr": "label",
342 "searchAttr": "label",
343 "scrollOnFocus": false,
344 "onChange": function(value) {
345 self.update_selected_field(value);
347 "store": this.filter_row_manager.field_store
348 }, dojo.create("span", {}, td)
352 this._create_operator_selector = function() {
353 var td = dojo.create("td", {}, this.tr);
354 this.operator_selector = new dijit.form.FilteringSelect(
356 "labelAttr": "label",
357 "searchAttr": "label",
358 "scrollOnFocus": false,
359 "onChange": function(value) {
360 self.update_selected_operator(value);
362 "store": _operator_store
363 }, dojo.create("span", {}, td)
367 this._adjust_operator_selector = function() {
368 this.operator_selector.attr(
369 "query", _store_query_by_datatype[this.selected_field_type]
371 this.operator_selector.reset();
374 this._create_value_slot = function() {
375 this.value_slot = dojo.create("td", {"innerHTML": "-"}, this.tr);
378 this._create_remover = function() {
379 var td = dojo.create("td", {}, this.tr);
380 var anchor = dojo.create(
382 "className": "oils-pcrudfilterdialog-remover",
385 "onclick": function() {
386 self.filter_row_manager.remove_row(self.row_id);
392 this._clear_value_slot = function() {
393 if (this.value_widgets) {
394 this.value_widgets.forEach(
395 function(autowidg) { autowidg.widget.destroy(); }
397 delete this.value_widgets;
400 dojo.empty(this.value_slot);
403 this._rebuild_value_widgets = function() {
404 this._clear_value_slot();
406 if (!this.get_selected_operator() || !this.selected_field)
409 this.value_widgets = [];
411 var param_count = this.operator_selector.item.param_count;
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}
422 this.value_widgets.push(widg);
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")
431 else if (opname == "null")
437 this.get_selected_operator = function() {
438 if (this.operator_selector)
439 return this.operator_selector.item;
442 this.get_selected_operator_name = function() {
443 var op = this.get_selected_operator();
444 return op ? op.name : null;
447 this.update_selected_operator = function(value) {
448 this._rebuild_value_widgets();
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;
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;
467 this._adjust_operator_selector();
468 this._rebuild_value_widgets();
472 this.compile = function() {
473 if (this.value_widgets) {
474 var values = this.value_widgets.map(
476 return self.selected_field_is_indirect ?
477 widg.widget.attr('displayedValue') :
478 widg.getFormattedValue();
482 if (!values.length) {
483 return this._null_clause(); /* null/not null */
486 var op = this.get_selected_operator_name();
487 if (values.length == 1)
488 clause[op] = values.pop();
498 this.destroy = function() {
499 this._clear_value_slot();
500 this.field_selector.destroy();
501 if (this.operator_selector)
502 this.operator_selector.destroy();
504 dojo.destroy(this.tr);
507 this._init.apply(this, arguments);
511 'openils.widget.PCrudFilterDialog',
512 [dijit.Dialog, openils.widget.AutoWidget],
515 constructor : function(args) {
518 this.title = this.title || pcFilterLocaleStrings.DEFAULT_DIALOG_TITLE;
519 this.widgetIndex = 0;
520 this.widgetCache = {};
523 _buildButtons : function() {
526 var button_holder = dojo.create(
528 "className": "oils-pcrudfilterdialog-buttonholder"
532 new dijit.form.Button(
534 "label": pcFilterLocaleStrings.ADD_ROW,
535 "scrollOnFocus": false, /* almost always better */
536 "onClick": function() {
537 self.filter_row_manager.add_row();
539 }, dojo.create("span", {}, button_holder)
542 new dijit.form.Button(
544 "label": pcFilterLocaleStrings.APPLY,
545 "scrollOnFocus": false,
546 "onClick": function() {
548 self.onApply(self.filter_row_manager.compile());
551 }, dojo.create("span", {}, button_holder)
554 new dijit.form.Button(
556 "label": pcFilterLocaleStrings.CANCEL,
557 "scrollOnFocus": false,
558 "onClick": function() {
563 }, dojo.create("span", {}, button_holder)
567 _buildFieldStore : function() {
569 var realFieldList = this.sortedFieldList.filter(
570 function(item) { return !(item.virtual || item.nonIdl); }
573 /* Prevent any explicitly unwanted fields from being available
574 * in our field dropdowns. */
575 if (dojo.isArray(this.suppressFilterFields)) {
576 realFieldList = realFieldList.filter(
580 i < self.suppressFilterFields.length;
583 if (item.name == self.suppressFilterFields[i])
591 this.fieldStore = new dojo.data.ItemFileReadStore({
593 "identifier": "name",
595 "items": realFieldList.map(
600 "type": item.datatype
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.
614 startup : function() {
616 this.inherited(arguments);
619 this._buildFieldStore();
621 this.filter_row_manager = new PCrudFilterRowManager(
622 dojo.create("div", {}, this.domNode),
623 this.fieldStore, this.fmClass
626 this._buildButtons();