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