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