]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js
allow pass-thru of filteringtreeselect disableQuery as constructor param
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / AutoFieldWidget.js
1 if(!dojo._hasResource['openils.widget.AutoFieldWidget']) {
2     dojo.provide('openils.widget.AutoFieldWidget');
3     dojo.require('openils.Util');
4     dojo.require('openils.User');
5     dojo.require('fieldmapper.IDL');
6     dojo.require('openils.PermaCrud');
7         dojo.requireLocalization("openils.widget", "AutoFieldWidget");
8
9     dojo.declare('openils.widget.AutoFieldWidget', null, {
10
11         async : false,
12
13         /**
14          * args:
15          *  idlField -- Field description object from fieldmapper.IDL.fmclasses
16          *  fmObject -- If available, the object being edited.  This will be used 
17          *      to set the value of the widget.
18          *  fmClass -- Class name (not required if idlField or fmObject is set)
19          *  fmField -- Field name (not required if idlField)
20          *  parentNode -- If defined, the widget will be appended to this DOM node
21          *  dijitArgs -- Optional parameters object, passed directly to the dojo widget
22          *  orgLimitPerms -- If this field defines a set of org units and an orgLimitPerms 
23          *      is defined, the code will limit the org units in the set to those
24          *      allowed by the permission
25          *  orgDefaultsToWs -- If this is an org unit field and the widget has no value,
26          *      set the value equal to the users's workstation org unit.  Othwerwise, leave it null
27          *  selfReference -- The primary purpose of an AutoFieldWidget is to render the value
28          *      or widget for a field on an object (that may or may not link to another object).
29          *      selfReference allows you to sidestep the indirection and create a selector widget
30          *      based purely on an fmClass.  To get a dropdown of all of the 'abc'
31          *      objects, pass in {selfReference : true, fmClass : 'abc'}.  
32          *  labelFormat -- For widgets that are displayed as remote object filtering selects,
33          *      this provides a mechanism for overriding the label format in the filtering select.
34          *      It must be an array, whose first value is a format string, compliant with
35          *      dojo.string.substitute.  The remaining array items are the arguments to the format
36          *      represented as field names on the remote linked object.
37          *      E.g.
38          *      labelFormat : [ '${0} (${1})', 'obj_field_1', 'obj_field_2' ]
39          *      Note: this does not control the final display value.  Only values in the drop-down.
40          *      See searchFormat for controlling the display value
41          *  searchFormat -- This format controls the structure of the search attribute which
42          *      controls the text used during type-ahead searching and the displayed value in 
43          *      the filtering select.  See labelFormat for the structure.  
44          *  dataLoader : Bypass the default PermaCrud linked data fetcher and use this function instead.
45          *      Function arguments are (link class name, search filter, callback)
46          *      The fetched objects should be passed to the callback as an array
47          *  disableQuery : dojo.data query passed to FilteringTreeSelect-based widgets to disable
48          *      (but leave visible) certain options.  
49          */
50         constructor : function(args) {
51             for(var k in args)
52                 this[k] = args[k];
53
54             if (!this.dijitArgs) {
55                 this.dijitArgs = {};
56             }
57             this.dijitArgs['scrollOnFocus'] = false;
58
59             // find the field description in the IDL if not provided
60             if(this.fmObject) 
61                 this.fmClass = this.fmObject.classname;
62             this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
63             this.suppressLinkedFields = args.suppressLinkedFields || [];
64
65             if(this.selfReference) {
66                 this.fmField = fieldmapper.IDL.fmclasses[this.fmClass].pkey;
67                 
68                 // create a mock-up of the idlField object.  
69                 this.idlField = {
70                     datatype : 'link',
71                     'class' : this.fmClass,
72                     reltype : 'has_a',
73                     key : this.fmField,
74                     name : this.fmField
75                 };
76
77             } else {
78
79                 if(!this.idlField) {
80                     this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
81                     var fields = this.fmIDL.fields;
82                     for(var f in fields) 
83                         if(fields[f].name == this.fmField)
84                             this.idlField = fields[f];
85                 }
86             }
87
88             if(!this.idlField) 
89                 throw new Error("AutoFieldWidget could not determine which " +
90                     "field to render.  We need more information. fmClass=" + 
91                     this.fmClass + ' fmField=' + this.fmField + ' fmObject=' + js2JSON(this.fmObject));
92
93             this.auth = openils.User.authtoken;
94             this.cache = openils.widget.AutoFieldWidget.cache;
95             this.cache[this.auth] = this.cache[this.auth] || {};
96             this.cache[this.auth].single = this.cache[this.auth].single || {};
97             this.cache[this.auth].list = this.cache[this.auth].list || {};
98         },
99
100         /**
101          * Turn the widget-stored value into a value oils understands
102          */
103         getFormattedValue : function() {
104             var value = this.baseWidgetValue();
105             switch(this.idlField.datatype) {
106                 case 'bool':
107                     switch(value) {
108                         case 'true': return 't';
109                         case 'on': return 't';
110                         case 'false' : return 'f';
111                         case 'unset' : return null;
112                         case true : return 't';
113                         default: return 'f';
114                     }
115                 case 'timestamp':
116                     if(!value) return null;
117                     return dojo.date.stamp.toISOString(value);
118                 case 'int':
119                 case 'float':
120                 case 'money':
121                     if(isNaN(value)) value = null;
122                 default:
123                     return (value === '') ? null : value;
124             }
125         },
126
127         baseWidgetValue : function(value) {
128             var attr = (this.readOnly) ? 'content' : 'value';
129             if(arguments.length) this.widget.attr(attr, value);
130             return this.widget.attr(attr);
131         },
132         
133         /**
134          * Turn the widget-stored value into something visually suitable
135          */
136         getDisplayString : function() {
137             var value = this.widgetValue;
138             switch(this.idlField.datatype) {
139                 case 'bool':
140                     switch(value) {
141                         case 't': 
142                         case 'true': 
143                             return openils.widget.AutoFieldWidget.localeStrings.TRUE; 
144                         case 'f' : 
145                         case 'false' : 
146                             return openils.widget.AutoFieldWidget.localeStrings.FALSE;
147                         case  null :
148                         case 'unset' : return openils.widget.AutoFieldWidget.localeStrings.UNSET;
149                         case true : return openils.widget.AutoFieldWidget.localeStrings.TRUE; 
150                         default: return openils.widget.AutoFieldWidget.localeStrings.FALSE;
151                     }
152                 case 'timestamp':
153                     if (!value) return '';
154                     dojo.require('dojo.date.locale');
155                     dojo.require('dojo.date.stamp');
156                     var date = dojo.date.stamp.fromISOString(value);
157                     return dojo.date.locale.format(date, {formatLength:'short'});
158                 case 'org_unit':
159                     if(value === null || value === undefined) return '';
160                     return fieldmapper.aou.findOrgUnit(value).shortname();
161                 case 'int':
162                 case 'float':
163                     if(isNaN(value)) value = 0;
164                 default:
165                     if(value === undefined || value === null)
166                         value = '';
167                     return value+'';
168             }
169         },
170
171         build : function(onload) {
172
173             if(this.widgetValue == null)
174                 this.widgetValue = (this.fmObject) ? this.fmObject[this.idlField.name]() : null;
175
176             if(this.widget) {
177                 // core widget provided for us, attach and move on
178                 if(this.parentNode) // may already be in the "right" place
179                     this.parentNode.appendChild(this.widget.domNode);
180                 if(this.widget.attr('value') == null)
181                     this._widgetLoaded();
182                 return;
183             }
184             
185             if(!this.parentNode) // give it somewhere to live so that dojo won't complain
186                 this.parentNode = dojo.create('div');
187
188             this.onload = onload;
189
190             if(this.readOnly) {
191                 dojo.require('dijit.layout.ContentPane');
192                 this.widget = new dijit.layout.ContentPane(this.dijitArgs, this.parentNode);
193                 if(this.widgetValue !== null)
194                     this._tryLinkedDisplayField();
195
196             } else if(this.widgetClass) {
197                 dojo.require(this.widgetClass);
198                 eval('this.widget = new ' + this.widgetClass + '(this.dijitArgs, this.parentNode);');
199
200             } else {
201
202                 switch(this.idlField.datatype) {
203                     
204                     case 'id':
205                         dojo.require('dijit.form.TextBox');
206                         this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
207                         break;
208
209                     case 'org_unit':
210                         this._buildOrgSelector();
211                         break;
212
213                     case 'money':
214                         dojo.require('dijit.form.CurrencyTextBox');
215                         this.widget = new dijit.form.CurrencyTextBox(this.dijitArgs, this.parentNode);
216                         break;
217
218                     case 'int':
219                         dojo.require('dijit.form.NumberTextBox');
220                         this.dijitArgs = dojo.mixin({constraints:{places:0}}, this.dijitArgs || {});
221                         this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
222                         break;
223
224                     case 'float':
225                         dojo.require('dijit.form.NumberTextBox');
226                         this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
227                         break;
228
229                     case 'timestamp':
230                         dojo.require('dijit.form.DateTextBox');
231                         dojo.require('dojo.date.stamp');
232                         this.widget = new dijit.form.DateTextBox(this.dijitArgs, this.parentNode);
233                         if(this.widgetValue != null) 
234                             this.widgetValue = dojo.date.stamp.fromISOString(
235                                 // Kludge until the ML returning ISO timestamps with a colon in the timezone offset,
236                                 // which dojo.date.stamp.fromISOString requires
237                                 this.widgetValue.replace( /^(\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d-\d\d)(\d\d)$/, '$1:$2') 
238                             );
239                         break;
240
241                     case 'bool':
242                         if(this.ternary) {
243                             dojo.require('dijit.form.FilteringSelect');
244                             var store = new dojo.data.ItemFileReadStore({
245                                 data:{
246                                     identifier : 'value',
247                                     items:[
248                                         {label : openils.widget.AutoFieldWidget.localeStrings.UNSET, value : 'unset'},
249                                         {label : openils.widget.AutoFieldWidget.localeStrings.TRUE, value : 'true'},
250                                         {label : openils.widget.AutoFieldWidget.localeStrings.FALSE, value : 'false'}
251                                     ]
252                                 }
253                             });
254                             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
255                             this.widget.searchAttr = this.widget.labelAttr = 'label';
256                             this.widget.valueAttr = 'value';
257                             this.widget.store = store;
258                             this.widget.startup();
259                             this.widgetValue = (this.widgetValue === null) ? 'unset' : 
260                                 (openils.Util.isTrue(this.widgetValue)) ? 'true' : 'false';
261                         } else {
262                             dojo.require('dijit.form.CheckBox');
263                             this.widget = new dijit.form.CheckBox(this.dijitArgs, this.parentNode);
264                             this.widgetValue = openils.Util.isTrue(this.widgetValue);
265                         }
266                         break;
267
268                     case 'link':
269                         if(this._buildLinkSelector()) break;
270
271                     default:
272                         if(this.dijitArgs && (this.dijitArgs.required || this.dijitArgs.regExp)) {
273                             dojo.require('dijit.form.ValidationTextBox');
274                             this.widget = new dijit.form.ValidationTextBox(this.dijitArgs, this.parentNode);
275                         } else {
276                             dojo.require('dijit.form.TextBox');
277                             this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
278                         }
279                 }
280             }
281
282             if(!this.async) this._widgetLoaded();
283             return this.widget;
284         },
285
286         // we want to display the value for our widget.  However, instead of displaying
287         // an ID, for exmaple, display the value for the 'selector' field on the object
288         // the ID points to
289         _tryLinkedDisplayField : function(noAsync) {
290
291             if(this.idlField.datatype == 'org_unit')
292                 return false; // we already handle org_units, no need to re-fetch
293
294             // user opted to bypass fetching this linked data
295             if(this.suppressLinkedFields.indexOf(this.idlField.name) > -1)
296                 return false;
297
298             var linkInfo = this._getLinkSelector();
299             if(!(linkInfo && linkInfo.vfield && linkInfo.vfield.selector)) 
300                 return false;
301             var lclass = linkInfo.linkClass;
302
303             if(lclass == 'aou') 
304                 return false;
305
306             // first try the store cache
307             var self = this;
308             if(this.cache[this.auth].list[lclass]) {
309                 var store = this.cache[this.auth].list[lclass];
310                 var query = {};
311                 query[linkInfo.vfield.name] = ''+this.widgetValue;
312                 var found = false;
313                 store.fetch({query:query, onComplete:
314                     function(list) {
315                         if(list[0]) {
316                             self.widgetValue = store.getValue(list[0], linkInfo.vfield.selector);
317                             found = true;
318                         }
319                     }
320                 });
321
322                 if(found) return;
323             }
324
325             // then try the single object cache
326             if(this.cache[this.auth].single[lclass] && this.cache[this.auth].single[lclass][this.widgetValue]) {
327                 this.widgetValue = this.cache[this.auth].single[lclass][this.widgetValue];
328                 return;
329             }
330
331             console.log("Fetching sync object " + lclass + " : " + this.widgetValue);
332
333             // if those fail, fetch the linked object
334             this.async = true;
335             var self = this;
336             new openils.PermaCrud().retrieve(lclass, this.widgetValue, {   
337                 async : !this.forceSync,
338                 oncomplete : function(r) {
339                     var item = openils.Util.readResponse(r);
340                     var newvalue = item[linkInfo.vfield.selector]();
341
342                     if(!self.cache[self.auth].single[lclass])
343                         self.cache[self.auth].single[lclass] = {};
344                     self.cache[self.auth].single[lclass][self.widgetValue] = newvalue;
345
346                     self.widgetValue = newvalue;
347                     self.widget.startup();
348                     self._widgetLoaded();
349                 }
350             });
351         },
352
353         _getLinkSelector : function() {
354             var linkClass = this.idlField['class'];
355             if(this.idlField.reltype != 'has_a')  return false;
356             if(!fieldmapper.IDL.fmclasses[linkClass].permacrud) return false;
357             if(!fieldmapper.IDL.fmclasses[linkClass].permacrud.retrieve) return false;
358
359             var vfield;
360             var rclassIdl = fieldmapper.IDL.fmclasses[linkClass];
361
362             for(var f in rclassIdl.fields) {
363                 if(this.idlField.key == rclassIdl.fields[f].name) {
364                     vfield = rclassIdl.fields[f];
365                     break;
366                 }
367             }
368
369             if(!vfield) 
370                 throw new Error("'" + linkClass + "' has no '" + this.idlField.key + "' field!");
371
372             return {
373                 linkClass : linkClass,
374                 vfield : vfield
375             };
376         },
377
378         _buildLinkSelector : function() {
379             var self = this;
380             var selectorInfo = this._getLinkSelector();
381             if(!selectorInfo) return false;
382
383             var linkClass = selectorInfo.linkClass;
384             var vfield = selectorInfo.vfield;
385
386             this.async = true;
387
388             if(linkClass == 'pgt')
389                 return this._buildPermGrpSelector();
390             if(linkClass == 'aou')
391                 return this._buildOrgSelector();
392             if(linkClass == 'acpl')
393                 return this._buildCopyLocSelector();
394
395
396             dojo.require('dojo.data.ItemFileReadStore');
397             dojo.require('dijit.form.FilteringSelect');
398
399             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
400             this.widget.searchAttr = this.widget.labelAttr = vfield.selector || vfield.name;
401             this.widget.valueAttr = vfield.name;
402
403             var oncomplete = function(list) {
404
405                 if(self.labelFormat) 
406                     self.widget.labelAttr = '_label';
407
408                 if(self.searchFormat)
409                     self.widget.searchAttr = '_search';
410
411                 function formatString(item, formatList) {
412
413                     try {
414
415                         // formatList[1..*] are names of fields.  Pull the field
416                         // values from each object to determine the values for string substitution
417                         var values = [];
418                         var format = formatList[0];
419                         for(var i = 1; i< formatList.length; i++) 
420                             values.push(item[formatList[i]]);
421
422                         return dojo.string.substitute(format, values);
423
424                     } catch(E) {
425                         throw new Error(
426                             "openils.widget.AutoFieldWidget: Invalid formatList ["+formatList+"] : "+E);
427                     }
428
429                 }
430
431                 if(list) {
432                     var storeData = {data:fieldmapper[linkClass].toStoreData(list)};
433
434                     if(self.labelFormat) {
435                         dojo.forEach(storeData.data.items, 
436                             function(item) {
437                                 item._label = formatString(item, self.labelFormat);
438                             }
439                         );
440                     }
441
442                     if(self.searchFormat) {
443                         dojo.forEach(storeData.data.items, 
444                             function(item) {
445                                 item._search = formatString(item, self.searchFormat);
446                             }
447                         );
448                     }
449
450                     self.widget.store = new dojo.data.ItemFileReadStore(storeData);
451                     self.cache[self.auth].list[linkClass] = self.widget.store;
452
453                 } else {
454                     self.widget.store = self.cache[self.auth].list[linkClass];
455                 }
456
457                 self.widget.startup();
458                 self._widgetLoaded();
459             };
460
461             if(!this.noCache && this.cache[self.auth].list[linkClass]) {
462                 oncomplete();
463
464             } else {
465
466                 if(this.dataLoader) {
467
468                     // caller provided an external function for retrieving the data
469                     this.dataLoader(linkClass, this.searchFilter, oncomplete);
470
471                 } else {
472
473                     var _cb = function(r) {
474                         oncomplete(openils.Util.readResponse(r, false, true));
475                     };
476
477                     if (this.searchFilter) {
478                         new openils.PermaCrud().search(linkClass, this.searchFilter, {
479                             async : !this.forceSync, oncomplete : _cb
480                         });
481                     } else {
482                         new openils.PermaCrud().retrieveAll(linkClass, {
483                             async : !this.forceSync, oncomplete : _cb
484                         });
485                     }
486                 }
487             }
488
489             return true;
490         },
491
492         /**
493          * For widgets that run asynchronously, provide a callback for finishing up
494          */
495         _widgetLoaded : function(value) {
496             
497             if(this.readOnly) {
498
499                 /* -------------------------------------------------------------
500                    when using widgets in a grid, the cell may dissapear, which 
501                    kills the underlying DOM node, which causes this to fail.
502                    For now, back out gracefully and let grid getters use
503                    getDisplayString() instead
504                   -------------------------------------------------------------*/
505                 try { 
506                     this.baseWidgetValue(this.getDisplayString());
507                 } catch (E) {};
508
509             } else {
510
511                 this.baseWidgetValue(this.widgetValue);
512                 if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && (!this.selfReference && !this.noDisablePkey))
513                     this.widget.attr('disabled', true); 
514                 if(this.disableWidgetTest && this.disableWidgetTest(this.idlField.name, this.fmObject))
515                     this.widget.attr('disabled', true); 
516             }
517             if(this.onload)
518                 this.onload(this.widget, this);
519
520             if(!this.readOnly && (this.idlField.required || (this.dijitArgs && this.dijitArgs.required))) {
521                 // a required dijit is not given any styling to indicate the value
522                 // is invalid until the user has focused the widget then left it with
523                 // invalid data.  This change tells dojo to pretend this focusing has 
524                 // already happened so we can style required widgets during page render.
525                 this.widget._hasBeenBlurred = true;
526                 this.widget.validate();
527             }
528         },
529
530         _buildOrgSelector : function() {
531             dojo.require('fieldmapper.OrgUtils');
532             dojo.require('openils.widget.FilteringTreeSelect');
533             this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
534             this.widget.searchAttr = 'shortname';
535             this.widget.labelAttr = 'shortname';
536             this.widget.parentField = 'parent_ou';
537             var user = new openils.User();
538
539             if(this.widgetValue == null && this.orgDefaultsToWs) 
540                 this.widgetValue = user.user.ws_ou();
541             
542             // if we have a limit perm, find the relevent orgs (async)
543             if(this.orgLimitPerms && this.orgLimitPerms.length > 0) {
544                 this.async = true;
545                 var self = this;
546                 user.getPermOrgList(this.orgLimitPerms, 
547                     function(orgList) {
548                         self.widget.tree = orgList;
549                         self.widget.startup();
550                         self._widgetLoaded();
551                     }
552                 );
553
554             } else {
555                 this.widget.tree = fieldmapper.aou.globalOrgTree;
556                 this.widget.startup();
557             }
558
559             return true;
560         },
561
562         _buildPermGrpSelector : function() {
563             dojo.require('openils.widget.FilteringTreeSelect');
564             this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
565             this.widget.disableQuery = this.disableQuery;
566             this.widget.searchAttr = 'name';
567
568             if(this.cache.permGrpTree) {
569                 this.widget.tree = this.cache.permGrpTree;
570                 this.widget.startup();
571                 this._widgetLoaded();
572                 return true;
573             } 
574
575             var self = this;
576             this.async = true;
577             new openils.PermaCrud().retrieveAll('pgt', {
578                 async : !this.forceSync,
579                 oncomplete : function(r) {
580                     var list = openils.Util.readResponse(r, false, true);
581                     if(!list) return;
582                     var map = {};
583                     var root = null;
584                     for(var l in list)
585                         map[list[l].id()] = list[l];
586                     for(var l in list) {
587                         var node = list[l];
588                         var pnode = map[node.parent()];
589                         if(!pnode) {root = node; continue;}
590                         if(!pnode.children()) pnode.children([]);
591                         pnode.children().push(node);
592                     }
593                     self.widget.tree = self.cache.permGrpTree = root;
594                     self.widget.startup();
595                     self._widgetLoaded();
596                 }
597             });
598
599             return true;
600         },
601
602         _buildCopyLocSelector : function() {
603             dojo.require('dijit.form.FilteringSelect');
604             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
605             this.widget.searchAttr = this.widget.labalAttr = 'name';
606             this.widget.valueAttr = 'id';
607
608             if(this.cache.copyLocStore) {
609                 this.widget.store = this.cache.copyLocStore;
610                 this.widget.startup();
611                 this.async = false;
612                 return true;
613             } 
614
615             // my orgs
616             var ws_ou = openils.User.user.ws_ou();
617             var orgs = fieldmapper.aou.findOrgUnit(ws_ou).orgNodeTrail().map(function (i) { return i.id() });
618             orgs = orgs.concat(fieldmapper.aou.descendantNodeList(ws_ou).map(function (i) { return i.id() }));
619
620             var self = this;
621             new openils.PermaCrud().search('acpl', {owning_lib : orgs}, {
622                 async : !this.forceSync,
623                 oncomplete : function(r) {
624                     var list = openils.Util.readResponse(r, false, true);
625                     if(!list) return;
626                     self.widget.store = 
627                         new dojo.data.ItemFileReadStore({data:fieldmapper.acpl.toStoreData(list)});
628                     self.cache.copyLocStore = self.widget.store;
629                     self.widget.startup();
630                     self._widgetLoaded();
631                 }
632             });
633
634             return true;
635         }
636     });
637
638     openils.widget.AutoFieldWidget.localeStrings = dojo.i18n.getLocalization("openils.widget", "AutoFieldWidget");
639     openils.widget.AutoFieldWidget.cache = {};
640 }
641