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