1 if (!dojo._hasResource["openils.widget.FlattenerGrid"]) {
2 dojo.provide("openils.widget.FlattenerGrid");
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");
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 "autoCoreFields": false,
20 "autoFieldFields": null,
21 "showLoadFilter": false, /* use FlattenerFilterDialog */
24 /* These potential constructor arguments maybe useful to
25 * FlattenerGrid in their own right, and are passed to
30 "sortFieldReMap": null,
31 "defaultSort": null, /* whatever any part of the UI says will
33 "baseSort": null, /* will contains what the columnpicker
34 dictates, and precedes whatever the column
37 /* These potential constructor arguments are for functionality
38 * copied from AutoGrid */
39 "editOnEnter": false, /* also implies edit-on-dblclick */
40 "editStyle": "dialog", /* "dialog" or "pane" */
41 "requiredFields": null, /* affects create/edit dialogs */
42 "suppressEditFields": null, /* affects create/edit dialogs */
44 /* _generateMap() lives to interpret the attributes of the
45 * FlattenerGrid dijit itself plus those definined in
49 * <th field="foo" ...>
50 * to build the map to hand to the FlattenerStore, which in turn
51 * uses it to query the flattener service.
53 "_generateMap": function() {
54 var map = this.mapClause = {};
55 var fields = this.structure[0].cells[0];
57 /* These are the fields defined in thead -> tr -> [th,th,...].
58 * For purposes of building the map, where each field has
59 * three boolean attributes "display", "sort" and "filter",
60 * assume "display" is always true for these.
61 * That doesn't mean that at the UI level we can't hide a
64 * If you need extra fields in the map for which display
65 * should *not* be true, use mapExtras.
68 fields, function(field) {
69 if (field.field.match(/^\+/))
70 return; /* special fields e.g. checkbox/line # */
74 "filter": (field.ffilter || false),
76 "path": field.fpath || field.field
78 /* The following attribute is not for the flattener
79 * service's benefit, but for other uses. We capture
80 * the hardcoded <th> value (the header label) if any.*/
82 map[field.field]._label = field.name;
87 /* It's not particularly useful to add simple fields, i.e.
88 * circ_lib: "circ_lib.name"
89 * to mapExtras, because by convention used elsewhere in
90 * Flattener, that gives all attributes, including
91 * display, a true value. Still, be consistent to avoid
94 for (var key in this.mapExtras) {
95 if (typeof this.mapExtras[key] != "object") {
96 this.mapExtras[key] = {
97 "path": this.mapExtras[key],
104 dojo.mixin(map, this.mapExtras);
107 /* Do this now, since we don't want a silently added
108 * identifier attribute in the terminii list (see its uses). */
109 this._calculateMapTerminii();
110 this._supplementHeaderNames();
112 /* make sure we always have a field for fm identifier */
113 if (!map[this.fmIdentifier]) {
114 map[this.fmIdentifier] = {
115 "path": this.fmIdentifier,
116 "display": true, /* Flattener displays it to us,
117 but we don't display to user. */
126 "_cleanMapForStore": function(map) {
127 var clean = dojo.clone(map);
129 for (var column in clean) {
130 openils.Util.objectProperties(clean[column]).filter(
131 function(k) { return k.match(/^_/); }
133 function(k) { delete clean[column][k]; }
140 /* Given the hint of a class to start at, follow path to the end
141 * and return information on the last field. */
142 "_followPathToEnd": function(hint, path, allow_selector_backoff) {
143 function _fm_is_selector_for_class(h, field) {
144 var cl = fieldmapper.IDL.fmclasses[h];
145 return (cl.field_map[cl.pkey].selector == field);
148 var last_field, last_hint;
149 var orig_path = dojo.clone(path);
150 var field, field_def;
152 while (field = path.shift()) {
153 /* XXX this assumes we have the whole IDL loaded. I
154 * guess we could teach this to work by loading classes
155 * on demand when we don't have the whole IDL loaded. */
157 fieldmapper.IDL.fmclasses[hint].field_map[field];
160 /* This can be ok in some cases. Columns following
161 * IDL paths involving links with a nonempty "map"
162 * attribute can be used for display only (no
163 * sort, no filter). */
165 "Lost our way in IDL at hint " + hint +
166 ", field " + field + "; may be ok"
171 if (field_def["class"]) {
175 hint = field_def["class"];
176 } else if (path.length) {
177 /* There are more fields left but we can't follow
178 * the chain via IDL any further. */
180 "_calculateMapTerminii can't parse path " +
181 orig_path + " (at " + field + ")"
186 var datatype = field_def.datatype;
187 var indirect = false;
188 /* If allowed, back off the last field in the path if it's a
189 * selector for its class, because the preceding field will be
190 * a better thing to hand to AutoFieldWidget.
192 if (orig_path.length > 1 && allow_selector_backoff &&
193 _fm_is_selector_for_class(hint, field_def.name)) {
199 field = field_def.name;
205 "label": field_def.label,
206 "datatype": datatype,
211 /* The FlattenerStore doesn't need this, but it has at least two
212 * uses: 1) FlattenerFilterDialog, 2) setting column header labels
215 * To call these 'Terminii' can be misleading. In certain
216 * (actually probably common) cases, they won't really be the last
217 * field in a path, but the next-to-last. Read on. */
218 "_calculateMapTerminii": function() {
219 this.mapTerminii = [];
220 for (var column in this.mapClause) {
221 var end = this._followPathToEnd(
223 this.mapClause[column].path.split(/\./),
224 true /* allow selector backoff */
228 var terminus = dojo.mixin(
230 "simple_name": column,
231 "isfilter": this.mapClause[column].filter
234 if (this.mapClause[column]._label)
235 terminus.label = this.mapClause[column]._label;
237 this.mapTerminii.push(terminus);
241 "_supplementHeaderNames": function() {
242 /* If we didn't give a particular header cell
243 * (<th>) a display name (the innerHTML of that <th>), then
244 * use the IDL to provide the label of the terminus of the
245 * flattener path for that column. It may be better than using
246 * the raw field name. */
248 this.structure[0].cells[0].forEach(
251 header.name = self.mapTerminii.filter(
253 return t.simple_name == header.field;
261 "_columnOrderingAndLabels": function() {
265 this.views.views[0].structure.cells[0].forEach(
267 if (!c.field.match(/^\+/)) {
269 columns.push(c.field);
274 return {"labels": labels, "columns": columns};
277 "_getAutoFieldFields": function(fmclass) {
279 fieldmapper.IDL.fmclasses[fmclass].fields)
282 return !field.virtual && field.datatype != "link";
285 function(a, b) { return a.label > b.label ? 1 : -1; }
289 /* Take our core class (this.fmClass) and add table columns for
290 * any field we don't already have covered by actual hard-coded
292 "_addAutoCoreFields": function() {
293 var cell_list = this.structure[0].cells[0];
294 var fields = dojo.clone(
295 fieldmapper.IDL.fmclasses[this.fmClass].fields
297 function(a, b) { return a.label > b.label ? 1 : -1; }
301 fields, function(f) {
302 if (f.datatype == "link" || f.virtual)
305 if (cell_list.filter(
307 if (!c.fpath) return false;
308 return c.fpath.split(/\./)[0] == f.name;
323 "_addAutoFieldFields": function(paths) {
328 paths, function(path) {
329 /* The beginning is the end. */
330 var beginning = self._followPathToEnd(
331 self.fmClass, path.split(/\./), false
337 self._getAutoFieldFields(beginning.fmClass),
340 path + "." + field.name;
342 new RegExp("^" + would_be_path);
343 if (!self.structure[0].cells[0].filter(
346 c.fpath.match(wbp_re);
349 console.info("adding auto field" + would_be_path);
350 self.structure[0].cells[0].push({
351 "field": "AUTO_" + beginning.name +
353 "name": beginning.label + " - " +
356 "fpath": would_be_path,
367 "_addAutoFields": function() {
368 if (this.autoCoreFields)
369 this._addAutoCoreFields();
371 if (dojo.isArray(this.autoFieldFields))
372 this._addAutoFieldFields(this.autoFieldFields);
374 this.setStructure(this.structure);
377 "constructor": function(args) {
378 dojo.mixin(this, args);
380 this.fmIdentifier = this.fmIdentifier ||
381 fieldmapper.IDL.fmclasses[this.fmClass].pkey;
384 "startup": function() {
385 /* Save original query for further filtering later */
386 this._baseQuery = dojo.clone(this.query);
388 this._addAutoFields();
390 this._startupGridHelperColumns();
394 if (!this.columnPicker) {
396 new openils.widget.GridColumnPicker(
397 null, this.columnPersistKey, this);
398 this.columnPicker.onLoad = dojo.hitch(
399 this, function(opts) { this._finishStartup(opts.sortFields) });
401 this.columnPicker.onSortChange = dojo.hitch(this,
402 /* directly after, this.update() is called by the
403 column picker, causing a re-fetch */
405 this.store.baseSort = this._mapCPSortFields(fields)
409 this.columnPicker.load();
412 this.inherited(arguments);
415 "canSort": function(idx, skip_structure /* API abuse */) {
416 var initial = this.inherited(arguments);
418 /* idx is one-based instead of zero-based for a reason. */
419 var view_idx = Math.abs(idx) - 1;
422 this.views.views[0].structure.cells[0][view_idx].fsort
426 /* Maps ColumnPicker sort fields to the correct format.
427 If no sort fields specified, falls back to defaultSort */
428 "_mapCPSortFields": function(sortFields) {
429 var sort = this.defaultSort;
430 if (sortFields.length) {
431 sort = sortFields.map(function(f) {
433 a[f.field] = f.direction;
440 "_finishStartup": function(sortFields) {
442 this._setStore( /* Seriously, let's leave this as _setStore. */
443 new openils.FlattenerStore({
444 "fmClass": this.fmClass,
445 "fmIdentifier": this.fmIdentifier,
446 "mapClause": this._cleanMapForStore(this.mapClause),
447 "baseSort": this.baseSort,
448 "defaultSort": this._mapCPSortFields(sortFields),
449 "sortFieldReMap": this.sortFieldReMap
454 // pick up any column label changes
455 this.columnPicker.reloadStructure();
460 this._showing_create_pane = false;
462 this.overrideEditWidgets = {};
463 this.overrideEditWidgetClass = {};
464 this.overrideWidgetArgs = {};
466 if (this.editOnEnter)
467 this._applyEditOnEnter();
468 else if (this.singleEditStyle)
469 this._applySingleEditStyle();
471 /* Like AutoGrid's paginator, but we'll never have Back/Next
472 * links. Just a place to hold misc links */
477 "_setupLinks": function() {
478 this.linkHolder = new dijit.layout.ContentPane();
479 dojo.place(this.linkHolder.domNode, this.domNode, "before");
481 if (this.showLoadFilter) {
482 dojo.require("openils.widget.FlattenerFilterDialog");
484 new openils.widget.FlattenerFilterDialog({
485 "fmClass": this.fmClass,
486 "mapTerminii": this.mapTerminii
489 this.filterDialog.onApply = dojo.hitch(
490 this, function(filter) {
492 dojo.mixin(filter, this._baseQuery),
498 this.filterDialog.startup();
501 "innerHTML": "Filter", /* XXX i18n */
502 "href": "javascript:void(0);",
503 "onclick": dojo.hitch(this, function() {
504 this.filterDialog.show();
506 }, this.linkHolder.domNode
511 "refresh": function() {
512 this.fetchLock = false;
513 this._refresh(/* isRender */ true);
516 "_fetch": function() {
520 return this.inherited(arguments);
523 /* ******** below are methods mostly copied but
524 * slightly changed from AutoGrid ******** */
526 "_applySingleEditStyle": function() {
527 this.onMouseOverRow = function(e) {};
528 this.onMouseOutRow = function(e) {};
529 this.onCellFocus = function(cell, rowIndex) {
530 this.selection.deselectAll();
531 this.selection.select(this.focus.rowIndex);
535 /* capture keydown and launch edit dialog on enter */
536 "_applyEditOnEnter": function() {
537 this._applySingleEditStyle();
540 this, "onRowDblClick", function(e) {
541 if (this.editStyle == "pane")
543 this.selection.getFirstSelected(),
547 this._drawEditDialog(
548 this.selection.getFirstSelected(),
555 this, "onKeyDown", function(e) {
556 if (e.keyCode == dojo.keys.ENTER) {
557 this.selection.deselectAll();
558 this.selection.select(this.focus.rowIndex);
559 if (this.editStyle == "pane")
561 this.selection.getFirstSelected(),
565 this._drawEditDialog(
566 this.selection.getFirstSelected(),
574 "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
576 var fmObject = (new openils.PermaCrud()).retrieve(
578 this.store.getIdentity(storeItem)
581 var pane = new openils.widget.EditPane({
582 "fmObject": fmObject,
583 "hideSaveButton": this.editReadOnly,
584 "readOnly": this.editReadOnly,
585 "overrideWidgets": this.overrideEditWidgets,
586 "overrideWidgetClass": this.overrideEditWidgetClass,
587 "overrideWidgetArgs": this.overrideWidgetArgs,
588 "disableWidgetTest": this.disableWidgetTest,
589 "requiredFields": this.requiredFields,
590 "suppressFields": this.suppressEditFields,
591 "onPostSubmit": function() {
592 /* ask the store to call flattener specially to get
593 * the flat row related to only this fmobj */
594 grid.store.loadItem({"force": true, "item": storeItem});
596 if (grid.onPostUpdate)
597 grid.onPostUpdate(storeItem, rowIndex);
602 grid.views.views[0].getCellNode(
611 "onCancel": function() {
614 grid.views.views[0].getCellNode(
624 if (typeof this.editPaneOnSubmit == "function")
625 pane.onSubmit = this.editPaneOnSubmit;
627 pane.fieldOrder = this.fieldOrder;
628 pane.mode = "update";
632 "_makeCreatePane": function(onPostSubmit, onCancel) {
634 var pane = new openils.widget.EditPane({
635 "fmClass": this.fmClass,
636 "overrideWidgets": this.overrideEditWidgets,
637 "overrideWidgetClass": this.overrideEditWidgetClass,
638 "overrideWidgetArgs": this.overrideWidgetArgs,
639 "disableWidgetTest": this.disableWidgetTest,
640 "requiredFields": this.requiredFields,
641 "suppressFields": this.suppressEditFields,
642 "onPostSubmit": function(req, cudResults) {
643 var fmObject = cudResults[0];
644 if (grid.onPostCreate)
645 grid.onPostCreate(fmObject);
647 grid.store.fetchItemByIdentity({
648 "identity": fmObject[grid.fmIdentifier](),
649 "onItem": function(item) {
650 grid.store.onNew(item);
658 grid.selection.select(grid.rowCount - 1);
659 grid.views.views[0].getCellNode(
667 onPostSubmit(fmObject);
669 "onCancel": function() { if (onCancel) onCancel(); }
672 if (typeof this.createPaneOnSubmit == "function")
673 pane.onSubmit = this.createPaneOnSubmit;
674 pane.fieldOrder = this.fieldOrder;
675 pane.mode = "create";
680 * Creates an EditPane with a copy of the data from the provided store
681 * item for cloning said item
682 * @param {Object} storeItem Dojo data item
683 * @param {Number} rowIndex The Grid row index of the item to be cloned
684 * @param {Function} onPostSubmit Optional callback for post-submit behavior
685 * @param {Function} onCancel Optional callback for clone cancelation
686 * @return {Object} The clone EditPane
688 "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
689 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
690 var origPane = this._makeEditPane(storeItem, rowIndex);
694 origPane.fieldList, function(field) {
695 if (field.widget.widget.attr('disabled'))
698 var w = clonePane.fieldList.filter(
699 function(i) { return (i.name == field.name) }
703 w.widget.baseWidgetValue(field.widget.widget.attr('value'));
706 w.widget.onload = function() {
707 w.widget.baseWidgetValue(
708 field.widget.widget.attr('value')
718 "_drawEditDialog": function(storeItem, rowIndex) {
719 var done = dojo.hitch(this, function() { this.hideDialog(); });
720 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
721 this.editDialog = new openils.widget.EditDialog({editPane:pane});
722 this.editDialog.startup();
723 this.editDialog.show();
727 * Generates an EditDialog for object creation and displays it to the user
729 "showCreateDialog": function() {
730 var done = dojo.hitch(this, function() { this.hideDialog(); });
731 var pane = this._makeCreatePane(done, done);
732 this.editDialog = new openils.widget.EditDialog({editPane:pane});
733 this.editDialog.startup();
734 this.editDialog.show();
737 "_drawEditPane": function(storeItem, rowIndex) {
738 var done = dojo.hitch(this, function() { this.hidePane(); });
740 dojo.style(this.domNode, "display", "none");
742 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
743 this.editPane.startup();
744 dojo.place(this.editPane.domNode, this.domNode, "before");
747 this.onEditPane(this.editPane);
750 "showClonePane": function(onPostSubmit) {
751 var done = dojo.hitch(this, function() { this.hidePane(); });
752 var row = this.getFirstSelectedRow();
758 postSubmit = dojo.hitch(
759 this, function(result) {
760 onPostSubmit(this.getItem(row), result);
768 dojo.style(this.domNode, "display", "none");
769 this.editPane = this._makeClonePane(
770 this.getItem(row), row, postSubmit, done
772 dojo.place(this.editPane.domNode, this.domNode, "before");
774 this.onEditPane(this.editPane);
777 "showCreatePane": function() {
778 if (this._showing_create_pane)
780 this._showing_create_pane = true;
782 var done = dojo.hitch(
784 this._showing_create_pane = false;
789 dojo.style(this.domNode, "display", "none");
791 this.editPane = this._makeCreatePane(done, done);
792 this.editPane.startup();
794 dojo.place(this.editPane.domNode, this.domNode, "before");
797 this.onEditPane(this.editPane);
800 "hideDialog": function() {
801 this.editDialog.hide();
802 this.editDialog.destroy();
803 delete this.editDialog;
807 "hidePane": function() {
808 this.domNode.parentNode.removeChild(this.editPane.domNode);
809 this.editPane.destroy();
810 delete this.editPane;
811 dojo.style(this.domNode, "display", "block");
815 "deleteSelected": function() {
818 this.getSelectedItems().forEach(
820 var fmobj = new fieldmapper[self.fmClass]();
821 fmobj[self.fmIdentifier](
822 self.store.getIdentity(item)
824 (new openils.PermaCrud()).eliminate(
826 "oncomplete": function() {
827 self.store.deleteItem(item);
835 /* Print the same data that the Flattener is feeding to the
836 * grid, sorted the same way too. remove limit and offset (i.e.,
838 "print": function() {
839 var coal = this._columnOrderingAndLabels();
844 "columns": coal.columns,
845 "labels": coal.labels
847 "onComplete": function(text) {
848 openils.Util.printHtmlString(text);
852 this.store.fetchToPrint(req);
857 /* monkey patch so we can get more attributes from each column in the
858 * markup that specifies grid columns (table->thead->tr->[td,...])
861 var b = dojox.grid.cells._Base;
862 var orig_mf = b.markupFactory;
864 b.markupFactory = function(node, cellDef) {
865 orig_mf(node, cellDef);
868 ["fpath", "ffilter"], function(a) {
869 var value = dojo.attr(node, a);
875 /* fsort and _visible are different. Assume true unless defined. */
877 ["fsort", "_visible"], function(a) {
878 var val = dojo.attr(node, a);
879 cellDef[a] = (typeof val == "undefined" || val === null) ?
880 true : dojo.fromJson(val);
886 /* the secret to successfully subclassing dojox.grid.DataGrid */
887 openils.widget.FlattenerGrid.markupFactory =
888 dojox.grid.DataGrid.markupFactory;