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