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