Merge branch 'master' of git.evergreen-ils.org:Evergreen
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / FlattenerGrid.js
1 if (!dojo._hasResource["openils.widget.FlattenerGrid"]) {
2     dojo.provide("openils.widget.FlattenerGrid");
3
4     dojo.require("DojoSRF");
5     dojo.require("dojox.grid.DataGrid");
6     dojo.require("openils.FlattenerStore");
7     dojo.require("openils.PermaCrud");
8     dojo.require("openils.widget.GridColumnPicker");
9     dojo.require("openils.widget.EditDialog");  /* includes EditPane */
10     dojo.require("openils.widget._GridHelperColumns");
11
12     dojo.declare(
13         "openils.widget.FlattenerGrid",
14         [dojox.grid.DataGrid, openils.widget._GridHelperColumns], {
15             /* These potential constructor arguments are useful to
16              * FlattenerGrid in their own right */
17             "columnReordering": true,
18             "columnPersistKey": null,
19             "showLoadFilter": false,    /* use FlattenerFilterDialog */
20
21             /* These potential constructor arguments maybe useful to
22              * FlattenerGrid in their own right, and are passed to
23              * FlattenerStore. */
24             "fmClass": null,
25             "fmIdentifier": null,
26             "mapExtras": null,
27             "defaultSort": null,  /* whatever any part of the UI says will
28                                      /replace/ this */
29             "baseSort": null,     /* will contains what the columnpicker
30                                      dictates, and precedes whatever the column
31                                      headers provide. */
32
33             /* These potential constructor arguments are for functionality
34              * copied from AutoGrid */
35             "editOnEnter": false,       /* also implies edit-on-dblclick */
36             "editStyle": "dialog",      /* "dialog" or "pane" */
37             "requiredFields": null,     /* affects create/edit dialogs */
38             "suppressEditFields": null, /* affects create/edit dialogs */
39
40             /* _generateMap() lives to interpret the attributes of the
41              * FlattenerGrid dijit itself plus those definined in
42              * <table>
43              *  <thead>
44              *   <tr>
45              *    <th field="foo" ...>
46              * to build the map to hand to the FlattenerStore, which in turn
47              * uses it to query the flattener service.
48              */
49             "_generateMap": function() {
50                 var map = this.mapClause = {};
51                 var fields = this.structure[0].cells[0];
52
53                 /* These are the fields defined in thead -> tr -> [th,th,...].
54                  * For purposes of building the map, where each field has
55                  * three boolean attributes "display", "sort" and "filter",
56                  * assume "display" and "sort" are always true for these.
57                  * That doesn't mean that at the UI level we can't hide a
58                  * column later.
59                  *
60                  * If you need extra fields in the map for which display
61                  * or sort should *not* be true, use mapExtras.
62                  */
63                 dojo.forEach(
64                     fields, function(field) {
65                         if (field.field.match(/^\+/))
66                             return; /* special fields e.g. checkbox/line # */
67
68                         map[field.field] = {
69                             "display": true,
70                             "filter": (field.ffilter || false),
71                             "sort": true,
72                             "path": field.fpath || field.field
73                         };
74                         /* The following attribute is not for the flattener
75                          * service's benefit, but for other uses. We capture
76                          * the hardcoded <th> value (the header label) if any.*/
77                         if (field.name)
78                             map[field.field]._label = field.name;
79                     }
80                 );
81
82                 if (this.mapExtras) {
83                     /* It's not particularly useful to add simple fields, i.e.
84                      *  circ_lib: "circ_lib.name"
85                      * to mapExtras, because by convention used elsewhere in
86                      * Flattener, that gives all attributes, including
87                      * display, a true value. Still, be consistent to avoid
88                      * stumping users.
89                      */
90                     for (var key in this.mapExtras) {
91                         if (typeof this.mapExtras[key] != "object") {
92                             this.mapExtras[key] = {
93                                 "path": this.mapExtras[key],
94                                 "sort": true,
95                                 "filter": true,
96                                 "display": true
97                             };
98                         }
99                     }
100                     dojo.mixin(map, this.mapExtras);
101                 }
102
103                 /* Do this now, since we don't want a silently added
104                  * identifier attribute in the terminii list (see its uses). */
105                 this._calculateMapTerminii();
106                 this._supplementHeaderNames();
107
108                 /* make sure we always have a field for fm identifier */
109                 if (!map[this.fmIdentifier]) {
110                     map[this.fmIdentifier] = {
111                         "path": this.fmIdentifier,
112                         "display": true,    /* Flattener displays it to us,
113                                                but we don't display to user. */
114                         "sort": false,
115                         "filter": true
116                     };
117                 }
118
119                 return map;
120             },
121
122             "_cleanMapForStore": function(map) {
123                 var clean = dojo.clone(map);
124
125                 for (var column in clean) {
126                     openils.Util.objectProperties(clean[column]).filter(
127                         function(k) { return k.match(/^_/); }
128                     ).forEach(
129                         function(k) { delete clean[column][k]; }
130                     );
131                 }
132
133                 return clean;
134             },
135
136             /* The FlattenerStore doesn't need this, but it has at least two
137              * uses: 1) FlattenerFilterDialog, 2) setting column header labels
138              * to IDL defaults.
139              *
140              * To call these 'Terminii' can be misleading. In certain
141              * (actually probably common) cases, they won't really be the last
142              * field in a path, but the next-to-last. Read on. */
143             "_calculateMapTerminii": function() {
144                 function _fm_is_selector_for_class(hint, field) {
145                     var cl = fieldmapper.IDL.fmclasses[hint];
146                     return (cl.field_map[cl.pkey].selector == field);
147                 }
148
149                 function _follow_to_end(hint, path) {
150                     var last_field, last_hint;
151                     var orig_path = dojo.clone(path);
152                     var field;
153
154                     while (field = path.shift()) {
155                         /* XXX this assumes we have the whole IDL loaded. I
156                          * guess we could teach this to work by loading classes
157                          * on demand when we don't have the whole IDL loaded. */
158                         var field_def =
159                             fieldmapper.IDL.fmclasses[hint].field_map[field];
160
161                         if (field_def["class"] && path.length) {
162                             last_field = field;
163                             last_hint = hint;
164
165                             hint = field_def["class"];
166                         } else if (path.length) {
167                             /* There are more fields left but we can't follow
168                              * the chain via IDL any further. */
169                             throw new Error(
170                                 "_calculateMapTerminii can't parse path " +
171                                 orig_path + " (at " + field + ")"
172                             );
173                         } else {
174                             break;  /* keeps field defined after loop */
175                         }
176                     }
177
178                     var datatype = field_def.datatype;
179                     var indirect = false;
180                     /* Back off the last field in the path if it's a selector
181                      * for its class, because the preceding field will be
182                      * a better thing to hand to AutoFieldWidget.
183                      */
184                     if (orig_path.length > 1 &&
185                             _fm_is_selector_for_class(hint, field)) {
186                         hint = last_hint;
187                         field = last_field;
188                         datatype = "link";
189                         indirect = true;
190                     }
191
192                     return {
193                         "fmClass": hint,
194                         "name": field,
195                         "label": field_def.label,
196                         "datatype": datatype,
197                         "indirect": indirect
198                     };
199                 }
200
201                 this.mapTerminii = [];
202                 for (var column in this.mapClause) {
203                     var terminus = dojo.mixin(
204                         _follow_to_end(
205                             this.fmClass,
206                             this.mapClause[column].path.split(/\./)
207                         ), {
208                             "simple_name": column,
209                             "isfilter": this.mapClause[column].filter
210                         }
211                     );
212                     if (this.mapClause[column]._label)
213                         terminus.label = this.mapClause[column]._label;
214
215                     this.mapTerminii.push(terminus);
216                 }
217             },
218
219             "_supplementHeaderNames": function() {
220                 /* You'd be surprised how rarely this make sense in Flattener
221                  * use cases, but if we didn't give a particular header cell
222                  * (<th>) a display name (the innerHTML of that <th>), then
223                  * use the IDL to provide the label of the terminus of the
224                  * flattener path for that column. It may be better than using
225                  * the raw field name. */
226                 var self = this;
227                 this.structure[0].cells[0].forEach(
228                     function(header) {
229                         if (!header.name) {
230                             header.name = self.mapTerminii.filter(
231                                 function(t) {
232                                     return t.simple_name == header.field;
233                                 }
234                             )[0].label;
235                         }
236                     }
237                 );
238             },
239
240             "constructor": function(args) {
241                 dojo.mixin(this, args);
242
243                 this.fmIdentifier = this.fmIdentifier ||
244                     fieldmapper.IDL.fmclasses[this.fmClass].pkey;
245             },
246
247             "startup": function() {
248
249                 /* Save original query for further filtering later */
250                 this._baseQuery = dojo.clone(this.query);
251                 this._startupGridHelperColumns();
252
253                 if (!this.columnPicker) {
254                     this.columnPicker =
255                         new openils.widget.GridColumnPicker(
256                             null, this.columnPersistKey, this);
257                     this.columnPicker.onLoad = dojo.hitch(
258                         this, function(opts) { this._finishStartup(opts.sortFields) });
259
260                     this.columnPicker.onSortChange = dojo.hitch(this,
261                         /* directly after, this.update() is called by the
262                            column picker, causing a re-fetch */
263                         function(fields) {
264                             this.store.baseSort = this._mapCPSortFields(fields)
265                         }
266                     );
267
268                     this.columnPicker.load();
269                 }
270
271                 this.inherited(arguments);
272             },
273
274             /*  Maps ColumnPicker sort fields to the correct format.
275                 If no sort fields specified, falls back to defaultSort */
276             "_mapCPSortFields": function(sortFields) {
277                 var sort = this.defaultSort;
278                 if (sortFields.length) {
279                     sort = sortFields.map(function(f) {
280                         a = {};
281                         a[f.field] = f.direction;
282                         return a;
283                     });
284                 }
285                 return sort;
286             },
287
288             "_finishStartup": function(sortFields) {
289
290                 this.setStore(
291                     new openils.FlattenerStore({
292                         "fmClass": this.fmClass,
293                         "fmIdentifier": this.fmIdentifier,
294                         "mapClause": (this.mapClause ||
295                             this._cleanMapForStore(this._generateMap())),
296                         "baseSort": this.baseSort,
297                         "defaultSort": this._mapCPSortFields(sortFields)
298                     }), this.query
299                 );
300
301                 // pick up any column label changes
302                 this.columnPicker.reloadStructure();
303
304                 this._showing_create_pane = false;
305
306                 this.overrideEditWidgets = {};
307                 this.overrideEditWidgetClass = {};
308                 this.overrideWidgetArgs = {};
309
310                 if (this.editOnEnter)
311                     this._applyEditOnEnter();
312                 else if (this.singleEditStyle)
313                     this._applySingleEditStyle();
314
315                 /* Like AutoGrid's paginator, but we'll never have Back/Next
316                  * links.  Just a place to hold misc links */
317                 this._setupLinks();
318             },
319
320
321             "_setupLinks": function() {
322                 this.linkHolder = new dijit.layout.ContentPane();
323                 dojo.place(this.linkHolder.domNode, this.domNode, "before");
324
325                 if (this.showLoadFilter) {
326                     dojo.require("openils.widget.FlattenerFilterDialog");
327                     this.filterDialog =
328                         new openils.widget.FlattenerFilterDialog({
329                             "fmClass": this.fmClass,
330                             "mapTerminii": this.mapTerminii
331                         });
332
333                     this.filterDialog.onApply = dojo.hitch(
334                         this, function(filter) {
335                             this.filter(
336                                 dojo.mixin(filter, this._baseQuery),
337                                 true    /* re-render */
338                             );
339                         }
340                     );
341
342                     this.filterDialog.startup();
343                     dojo.create(
344                         "a", {
345                             "innerHTML": "Filter",  /* XXX i18n */
346                             "href": "javascript:void(0);",
347                             "onclick": dojo.hitch(this, function() {
348                                 this.filterDialog.show();
349                             })
350                         }, this.linkHolder.domNode
351                     );
352                 }
353             },
354
355             /* ******** below are methods mostly copied but
356              * slightly changed from AutoGrid ******** */
357
358             "_applySingleEditStyle": function() {
359                 this.onMouseOverRow = function(e) {};
360                 this.onMouseOutRow = function(e) {};
361                 this.onCellFocus = function(cell, rowIndex) {
362                     this.selection.deselectAll();
363                     this.selection.select(this.focus.rowIndex);
364                 };
365             },
366
367             /* capture keydown and launch edit dialog on enter */
368             "_applyEditOnEnter": function() {
369                 this._applySingleEditStyle();
370
371                 dojo.connect(
372                     this, "onRowDblClick", function(e) {
373                         if (this.editStyle == "pane")
374                             this._drawEditPane(
375                                 this.selection.getFirstSelected(),
376                                 this.focus.rowIndex
377                             );
378                         else
379                             this._drawEditDialog(
380                                 this.selection.getFirstSelected(),
381                                 this.focus.rowIndex
382                             );
383                     }
384                 );
385
386                 dojo.connect(
387                     this, "onKeyDown", function(e) {
388                         if (e.keyCode == dojo.keys.ENTER) {
389                             this.selection.deselectAll();
390                             this.selection.select(this.focus.rowIndex);
391                             if (this.editStyle == "pane")
392                                 this._drawEditPane(
393                                     this.selection.getFirstSelected(),
394                                     this.focus.rowIndex
395                                 );
396                             else
397                                 this._drawEditDialog(
398                                     this.selection.getFirstSelected(),
399                                     this.focus.rowIndex
400                                 );
401                         }
402                     }
403                 );
404             },
405
406             "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
407                 var grid = this;
408                 var fmObject = (new openils.PermaCrud()).retrieve(
409                     this.fmClass,
410                     this.store.getIdentity(storeItem)
411                 );
412
413                 var pane = new openils.widget.EditPane({
414                     "fmObject": fmObject,
415                     "hideSaveButton": this.editReadOnly,
416                     "readOnly": this.editReadOnly,
417                     "overrideWidgets": this.overrideEditWidgets,
418                     "overrideWidgetClass": this.overrideEditWidgetClass,
419                     "overrideWidgetArgs": this.overrideWidgetArgs,
420                     "disableWidgetTest": this.disableWidgetTest,
421                     "requiredFields": this.requiredFields,
422                     "suppressFields": this.suppressEditFields,
423                     "onPostSubmit": function() {
424                         /* ask the store to call flattener specially to get
425                          * the flat row related to only this fmobj */
426                         grid.store.loadItem({"force": true, "item": storeItem});
427
428                         if (grid.onPostUpdate)
429                             grid.onPostUpdate(storeItem, rowIndex);
430
431                         setTimeout(
432                             function() {
433                                 try {
434                                     grid.views.views[0].getCellNode(
435                                         rowIndex, 0
436                                     ).focus();
437                                 } catch (E) { }
438                             }, 200
439                         );
440                         if (onPostSubmit)
441                             onPostSubmit();
442                     },
443                     "onCancel": function() {
444                         setTimeout(
445                             function() {
446                                 grid.views.views[0].getCellNode(
447                                     rowIndex, 0
448                                 ).focus();
449                             }, 200
450                         );
451                         if (onCancel)
452                             onCancel();
453                     }
454                 });
455
456                 if (typeof this.editPaneOnSubmit == "function")
457                     pane.onSubmit = this.editPaneOnSubmit;
458
459                 pane.fieldOrder = this.fieldOrder;
460                 pane.mode = "update";
461                 return pane;
462             },
463
464             "_makeCreatePane": function(onPostSubmit, onCancel) {
465                 var grid = this;
466                 var pane = new openils.widget.EditPane({
467                     "fmClass": this.fmClass,
468                     "overrideWidgets": this.overrideEditWidgets,
469                     "overrideWidgetClass": this.overrideEditWidgetClass,
470                     "overrideWidgetArgs": this.overrideWidgetArgs,
471                     "disableWidgetTest": this.disableWidgetTest,
472                     "requiredFields": this.requiredFields,
473                     "suppressFields": this.suppressEditFields,
474                     "onPostSubmit": function(req, cudResults) {
475                         var fmObject = cudResults[0];
476                         if (grid.onPostCreate)
477                             grid.onPostCreate(fmObject);
478                         if (fmObject) {
479                             grid.store.fetchItemByIdentity({
480                                 "identity": fmObject[grid.fmIdentifier](),
481                                 "onItem": function(item) {
482                                     grid.store.onNew(item);
483                                 }
484                             });
485                         }
486
487                         setTimeout(
488                             function() {
489                                 try {
490                                     grid.selection.select(grid.rowCount - 1);
491                                     grid.views.views[0].getCellNode(
492                                         grid.rowCount - 1, 1
493                                     ).focus();
494                                 } catch (E) { }
495                             }, 200
496                         );
497
498                         if (onPostSubmit)
499                             onPostSubmit(fmObject);
500                     },
501                     "onCancel": function() { if (onCancel) onCancel(); }
502                 });
503
504                 if (typeof this.createPaneOnSubmit == "function")
505                     pane.onSubmit = this.createPaneOnSubmit;
506                 pane.fieldOrder = this.fieldOrder;
507                 pane.mode = "create";
508                 return pane;
509             },
510
511             /**
512              * Creates an EditPane with a copy of the data from the provided store
513              * item for cloning said item
514              * @param {Object} storeItem Dojo data item
515              * @param {Number} rowIndex The Grid row index of the item to be cloned
516              * @param {Function} onPostSubmit Optional callback for post-submit behavior
517              * @param {Function} onCancel Optional callback for clone cancelation
518              * @return {Object} The clone EditPane
519              */
520             "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
521                 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
522                 var origPane = this._makeEditPane(storeItem, rowIndex);
523                 clonePane.startup();
524                 origPane.startup();
525                 dojo.forEach(
526                     origPane.fieldList, function(field) {
527                         if (field.widget.widget.attr('disabled'))
528                             return;
529
530                         var w = clonePane.fieldList.filter(
531                             function(i) { return (i.name == field.name) }
532                         )[0];
533
534                         // sync widgets
535                         w.widget.baseWidgetValue(field.widget.widget.attr('value'));
536
537                         // async widgets
538                         w.widget.onload = function() {
539                             w.widget.baseWidgetValue(
540                                 field.widget.widget.attr('value')
541                             )
542                         };
543                     }
544                 );
545                 origPane.destroy();
546                 return clonePane;
547             },
548
549
550             "_drawEditDialog": function(storeItem, rowIndex) {
551                 var done = dojo.hitch(this, function() { this.hideDialog(); });
552                 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
553                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
554                 this.editDialog.startup();
555                 this.editDialog.show();
556             },
557
558             /**
559              * Generates an EditDialog for object creation and displays it to the user
560              */
561             "showCreateDialog": function() {
562                 var done = dojo.hitch(this, function() { this.hideDialog(); });
563                 var pane = this._makeCreatePane(done, done);
564                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
565                 this.editDialog.startup();
566                 this.editDialog.show();
567             },
568
569             "_drawEditPane": function(storeItem, rowIndex) {
570                 var done = dojo.hitch(this, function() { this.hidePane(); });
571
572                 dojo.style(this.domNode, "display", "none");
573
574                 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
575                 this.editPane.startup();
576                 dojo.place(this.editPane.domNode, this.domNode, "before");
577
578                 if (this.onEditPane)
579                     this.onEditPane(this.editPane);
580             },
581
582             "showClonePane": function(onPostSubmit) {
583                 var done = dojo.hitch(this, function() { this.hidePane(); });
584                 var row = this.getFirstSelectedRow();
585
586                 if (!row)
587                     return;
588
589                 if (onPostSubmit) {
590                     postSubmit = dojo.hitch(
591                         this, function(result) {
592                             onPostSubmit(this.getItem(row), result);
593                             this.hidePane();
594                         }
595                     );
596                 } else {
597                     postSubmit = done;
598                 }
599
600                 dojo.style(this.domNode, "display", "none");
601                 this.editPane = this._makeClonePane(
602                     this.getItem(row), row, postSubmit, done
603                 );
604                 dojo.place(this.editPane.domNode, this.domNode, "before");
605                 if (this.onEditPane)
606                     this.onEditPane(this.editPane);
607             },
608
609             "showCreatePane": function() {
610                 if (this._showing_create_pane)
611                     return;
612                 this._showing_create_pane = true;
613
614                 var done = dojo.hitch(
615                     this, function() {
616                         this._showing_create_pane = false;
617                         this.hidePane();
618                     }
619                 );
620
621                 dojo.style(this.domNode, "display", "none");
622
623                 this.editPane = this._makeCreatePane(done, done);
624                 this.editPane.startup();
625
626                 dojo.place(this.editPane.domNode, this.domNode, "before");
627
628                 if (this.onEditPane)
629                     this.onEditPane(this.editPane);
630             },
631
632             "hideDialog": function() {
633                 this.editDialog.hide();
634                 this.editDialog.destroy();
635                 delete this.editDialog;
636                 this.update();
637             },
638
639             "hidePane": function() {
640                 this.domNode.parentNode.removeChild(this.editPane.domNode);
641                 this.editPane.destroy();
642                 delete this.editPane;
643                 dojo.style(this.domNode, "display", "block");
644                 this.update();
645             },
646
647             "deleteSelected": function() {
648                 var self = this;
649
650                 this.getSelectedItems().forEach(
651                     function(item) {
652                         var fmobj = new fieldmapper[self.fmClass]();
653                         fmobj[self.fmIdentifier](
654                             self.store.getIdentity(item)
655                         );
656                         (new openils.PermaCrud()).eliminate(
657                             fmobj, {
658                                 "oncomplete": function() {
659                                     self.store.deleteItem(item);
660                                 }
661                             }
662                         );
663                     }
664                 );
665             }
666         }
667     );
668
669     /* monkey patch so we can get more attributes from each column in the
670      * markup that specifies grid columns (table->thead->tr->[td,...])
671      */
672     (function() {
673         var b = dojox.grid.cells._Base;
674         var orig_mf = b.markupFactory;
675
676         b.markupFactory = function(node, cellDef) {
677             orig_mf(node, cellDef);
678
679             dojo.forEach(
680                 ["fpath", "ffilter"], function(a) {
681                     var value = dojo.attr(node, a);
682                     if (value)
683                         cellDef[a] = value;
684                 }
685             );
686         };
687     })();
688
689     /* the secret to successfully subclassing dojox.grid.DataGrid */
690     openils.widget.FlattenerGrid.markupFactory =
691         dojox.grid.DataGrid.markupFactory;
692 }