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