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 "showLoadFilter": false, /* use FlattenerFilterDialog */
21 /* These potential constructor arguments maybe useful to
22 * FlattenerGrid in their own right, and are passed to
27 "defaultSort": null, /* whatever any part of the UI says will
29 "baseSort": null, /* will contains what the columnpicker
30 dictates, and precedes whatever the column
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 */
40 /* _generateMap() lives to interpret the attributes of the
41 * FlattenerGrid dijit itself plus those definined in
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.
49 "_generateMap": function() {
50 var map = this.mapClause = {};
51 var fields = this.structure[0].cells[0];
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
60 * If you need extra fields in the map for which display
61 * or sort should *not* be true, use mapExtras.
64 fields, function(field) {
65 if (field.field.match(/^\+/))
66 return; /* special fields e.g. checkbox/line # */
70 "filter": (field.ffilter || false),
72 "path": field.fpath || field.field
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.*/
78 map[field.field]._label = field.name;
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
90 for (var key in this.mapExtras) {
91 if (typeof this.mapExtras[key] != "object") {
92 this.mapExtras[key] = {
93 "path": this.mapExtras[key],
100 dojo.mixin(map, this.mapExtras);
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();
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. */
122 "_cleanMapForStore": function(map) {
123 var clean = dojo.clone(map);
125 for (var column in clean) {
126 openils.Util.objectProperties(clean[column]).filter(
127 function(k) { return k.match(/^_/); }
129 function(k) { delete clean[column][k]; }
136 /* The FlattenerStore doesn't need this, but it has at least two
137 * uses: 1) FlattenerFilterDialog, 2) setting column header labels
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);
149 function _follow_to_end(hint, path) {
150 var last_field, last_hint;
151 var orig_path = dojo.clone(path);
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. */
159 fieldmapper.IDL.fmclasses[hint].field_map[field];
161 if (field_def["class"] && path.length) {
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. */
170 "_calculateMapTerminii can't parse path " +
171 orig_path + " (at " + field + ")"
174 break; /* keeps field defined after loop */
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.
184 if (orig_path.length > 1 &&
185 _fm_is_selector_for_class(hint, field)) {
195 "label": field_def.label,
196 "datatype": datatype,
201 this.mapTerminii = [];
202 for (var column in this.mapClause) {
203 var terminus = dojo.mixin(
206 this.mapClause[column].path.split(/\./)
208 "simple_name": column,
209 "isfilter": this.mapClause[column].filter
212 if (this.mapClause[column]._label)
213 terminus.label = this.mapClause[column]._label;
215 this.mapTerminii.push(terminus);
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. */
227 this.structure[0].cells[0].forEach(
230 header.name = self.mapTerminii.filter(
232 return t.simple_name == header.field;
240 "constructor": function(args) {
241 dojo.mixin(this, args);
243 this.fmIdentifier = this.fmIdentifier ||
244 fieldmapper.IDL.fmclasses[this.fmClass].pkey;
247 "startup": function() {
249 /* Save original query for further filtering later */
250 this._baseQuery = dojo.clone(this.query);
251 this._startupGridHelperColumns();
253 if (!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) });
260 this.columnPicker.onSortChange = dojo.hitch(this,
261 /* directly after, this.update() is called by the
262 column picker, causing a re-fetch */
264 this.store.baseSort = this._mapCPSortFields(fields)
268 this.columnPicker.load();
271 this.inherited(arguments);
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) {
281 a[f.field] = f.direction;
288 "_finishStartup": function(sortFields) {
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)
301 // pick up any column label changes
302 this.columnPicker.reloadStructure();
304 this._showing_create_pane = false;
306 this.overrideEditWidgets = {};
307 this.overrideEditWidgetClass = {};
308 this.overrideWidgetArgs = {};
310 if (this.editOnEnter)
311 this._applyEditOnEnter();
312 else if (this.singleEditStyle)
313 this._applySingleEditStyle();
315 /* Like AutoGrid's paginator, but we'll never have Back/Next
316 * links. Just a place to hold misc links */
321 "_setupLinks": function() {
322 this.linkHolder = new dijit.layout.ContentPane();
323 dojo.place(this.linkHolder.domNode, this.domNode, "before");
325 if (this.showLoadFilter) {
326 dojo.require("openils.widget.FlattenerFilterDialog");
328 new openils.widget.FlattenerFilterDialog({
329 "fmClass": this.fmClass,
330 "mapTerminii": this.mapTerminii
333 this.filterDialog.onApply = dojo.hitch(
334 this, function(filter) {
336 dojo.mixin(filter, this._baseQuery),
342 this.filterDialog.startup();
345 "innerHTML": "Filter", /* XXX i18n */
346 "href": "javascript:void(0);",
347 "onclick": dojo.hitch(this, function() {
348 this.filterDialog.show();
350 }, this.linkHolder.domNode
355 /* ******** below are methods mostly copied but
356 * slightly changed from AutoGrid ******** */
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);
367 /* capture keydown and launch edit dialog on enter */
368 "_applyEditOnEnter": function() {
369 this._applySingleEditStyle();
372 this, "onRowDblClick", function(e) {
373 if (this.editStyle == "pane")
375 this.selection.getFirstSelected(),
379 this._drawEditDialog(
380 this.selection.getFirstSelected(),
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")
393 this.selection.getFirstSelected(),
397 this._drawEditDialog(
398 this.selection.getFirstSelected(),
406 "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
408 var fmObject = (new openils.PermaCrud()).retrieve(
410 this.store.getIdentity(storeItem)
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});
428 if (grid.onPostUpdate)
429 grid.onPostUpdate(storeItem, rowIndex);
434 grid.views.views[0].getCellNode(
443 "onCancel": function() {
446 grid.views.views[0].getCellNode(
456 if (typeof this.editPaneOnSubmit == "function")
457 pane.onSubmit = this.editPaneOnSubmit;
459 pane.fieldOrder = this.fieldOrder;
460 pane.mode = "update";
464 "_makeCreatePane": function(onPostSubmit, onCancel) {
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);
479 grid.store.fetchItemByIdentity({
480 "identity": fmObject[grid.fmIdentifier](),
481 "onItem": function(item) {
482 grid.store.onNew(item);
490 grid.selection.select(grid.rowCount - 1);
491 grid.views.views[0].getCellNode(
499 onPostSubmit(fmObject);
501 "onCancel": function() { if (onCancel) onCancel(); }
504 if (typeof this.createPaneOnSubmit == "function")
505 pane.onSubmit = this.createPaneOnSubmit;
506 pane.fieldOrder = this.fieldOrder;
507 pane.mode = "create";
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
520 "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
521 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
522 var origPane = this._makeEditPane(storeItem, rowIndex);
526 origPane.fieldList, function(field) {
527 if (field.widget.widget.attr('disabled'))
530 var w = clonePane.fieldList.filter(
531 function(i) { return (i.name == field.name) }
535 w.widget.baseWidgetValue(field.widget.widget.attr('value'));
538 w.widget.onload = function() {
539 w.widget.baseWidgetValue(
540 field.widget.widget.attr('value')
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();
559 * Generates an EditDialog for object creation and displays it to the user
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();
569 "_drawEditPane": function(storeItem, rowIndex) {
570 var done = dojo.hitch(this, function() { this.hidePane(); });
572 dojo.style(this.domNode, "display", "none");
574 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
575 this.editPane.startup();
576 dojo.place(this.editPane.domNode, this.domNode, "before");
579 this.onEditPane(this.editPane);
582 "showClonePane": function(onPostSubmit) {
583 var done = dojo.hitch(this, function() { this.hidePane(); });
584 var row = this.getFirstSelectedRow();
590 postSubmit = dojo.hitch(
591 this, function(result) {
592 onPostSubmit(this.getItem(row), result);
600 dojo.style(this.domNode, "display", "none");
601 this.editPane = this._makeClonePane(
602 this.getItem(row), row, postSubmit, done
604 dojo.place(this.editPane.domNode, this.domNode, "before");
606 this.onEditPane(this.editPane);
609 "showCreatePane": function() {
610 if (this._showing_create_pane)
612 this._showing_create_pane = true;
614 var done = dojo.hitch(
616 this._showing_create_pane = false;
621 dojo.style(this.domNode, "display", "none");
623 this.editPane = this._makeCreatePane(done, done);
624 this.editPane.startup();
626 dojo.place(this.editPane.domNode, this.domNode, "before");
629 this.onEditPane(this.editPane);
632 "hideDialog": function() {
633 this.editDialog.hide();
634 this.editDialog.destroy();
635 delete this.editDialog;
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");
647 "deleteSelected": function() {
650 this.getSelectedItems().forEach(
652 var fmobj = new fieldmapper[self.fmClass]();
653 fmobj[self.fmIdentifier](
654 self.store.getIdentity(item)
656 (new openils.PermaCrud()).eliminate(
658 "oncomplete": function() {
659 self.store.deleteItem(item);
669 /* monkey patch so we can get more attributes from each column in the
670 * markup that specifies grid columns (table->thead->tr->[td,...])
673 var b = dojox.grid.cells._Base;
674 var orig_mf = b.markupFactory;
676 b.markupFactory = function(node, cellDef) {
677 orig_mf(node, cellDef);
680 ["fpath", "ffilter"], function(a) {
681 var value = dojo.attr(node, a);
689 /* the secret to successfully subclassing dojox.grid.DataGrid */
690 openils.widget.FlattenerGrid.markupFactory =
691 dojox.grid.DataGrid.markupFactory;