From: Bill Erickson Date: Wed, 2 Apr 2014 14:07:43 +0000 (-0400) Subject: web staff : deprecating old grid X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=27f9b22f58372b5999dacd18285ef86b1720528b;p=working%2FEvergreen.git web staff : deprecating old grid Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 b/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 index db9b271618..b9f9499392 100644 --- a/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 +++ b/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 @@ -3,36 +3,107 @@ Actions row. This sits above the grid and contains the column picker, etc. --> +
+
-
{{gridLabel}}
+
{{grid.mainLabel}}
- +
- + + + + + + + + +
+ + +
+ +
+ + +
+ +
- - -
+
- +
- -
-
-
[% l('#') %]
+
+
[% l('#') %]
-
-
- +
+
+
- {{column.label}} + eg-grid-column-drag-source + eg-grid-column-drag-dest + column="{{col.name}}" + eg-right-click="grid.onContextMenu($event)" + ng-repeat="col in grid.columnsProvider.columns" + style="flex:{{col.flex}}" + ng-show="grid.columnsProvider.visible[col.name]"> + {{col.label}}
- -
+
+ style="flex:{{grid.indexFlex + grid.selectorFlex}}">
[% l('Expand') %]
[% l('Shrink') %]
[% l('Sort') %]
+ ng-repeat="col in grid.columnsProvider.columns" + style="flex:{{col.flex}}" + ng-show="grid.columnsProvider.visible[col.name]">
+ ng-click="grid.modifyColumnFlex(col,1)">
+ ng-click="grid.modifyColumnFlex(col,-1)">
- +
- - - -
- - -
+
[% l('No Items To Display') %]
-
- {{$index + gridDataManager.offset}} + ng-repeat="item in grid.items" + ng-show="grid.count() > 0" + ng-class="{'eg-grid-row-selected' : grid.selected[grid.indexValue(item)]}"> +
+ {{$index + grid.offset + 1}}
-
+
+ ng-model="grid.selected[grid.indexValue(item)]"/>
- {{fieldValue(item, column.name) | egGridvalueFilter:column}} + ng-click="grid.handleRowClick($event, item)" + ng-repeat="col in grid.columnsProvider.columns" + style="flex:{{col.flex}}" + ng-show="grid.columnsProvider.visible[col.name]"> + {{grid.dataProvider.itemFieldValue(item, col) | egGridvalueFilter:col}}
+ +
diff --git a/Open-ILS/src/templates/staff/parts/t_autogrid2.tt2 b/Open-ILS/src/templates/staff/parts/t_autogrid2.tt2 deleted file mode 100644 index b9f9499392..0000000000 --- a/Open-ILS/src/templates/staff/parts/t_autogrid2.tt2 +++ /dev/null @@ -1,207 +0,0 @@ - - - -
- -
-
{{grid.mainLabel}}
-
- - -
- - - - - - - - - - - -
- - -
- -
- - -
- - - -
-
- - -
- - -
- -
-
-
[% l('#') %]
-
-
-
- -
-
- -
- - -
-
-
[% l('Expand') %]
-
[% l('Shrink') %]
-
[% l('Sort') %]
-
-
-
- - - -
-
- - - -
-
- -
-
-
- -
-
[% l('No Items To Display') %]
- -
-
- {{$index + grid.offset + 1}} -
-
- -
- -
-
-
- {{grid.dataProvider.itemFieldValue(item, col) | egGridvalueFilter:col}} -
-
-
- - -
- diff --git a/Open-ILS/src/templates/staff/parts/t_autogrid_deprecated.tt2 b/Open-ILS/src/templates/staff/parts/t_autogrid_deprecated.tt2 new file mode 100644 index 0000000000..db9b271618 --- /dev/null +++ b/Open-ILS/src/templates/staff/parts/t_autogrid_deprecated.tt2 @@ -0,0 +1,158 @@ + + + + + + + +
+ + +
+ + + +
+
+
[% l('#') %]
+
+
+
+ +
+
+ +
+ + + +
+
+
[% l('Expand') %]
+
[% l('Shrink') %]
+
[% l('Sort') %]
+
+
+
+ + + +
+
+ + + +
+
+ +
+
+
+ + + + +
+ + +
[% l('No Items To Display') %]
+ +
+
+ {{$index + gridDataManager.offset}} +
+
+ +
+ +
+
+
+ {{fieldValue(item, column.name) | egGridvalueFilter:column}} +
+
+
+
+ diff --git a/Open-ILS/src/templates/staff/test/index.tt2 b/Open-ILS/src/templates/staff/test/index.tt2 index 7ebbcbd000..0f360bdb1c 100644 --- a/Open-ILS/src/templates/staff/test/index.tt2 +++ b/Open-ILS/src/templates/staff/test/index.tt2 @@ -8,7 +8,7 @@ [% BLOCK APP_JS %] - + [% END %] diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js index c96733f279..81eb00e5f0 100644 --- a/Open-ILS/web/js/ui/default/staff/services/grid.js +++ b/Open-ILS/web/js/ui/default/staff/services/grid.js @@ -1,12 +1,12 @@ - angular.module('egGridMod', - ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap', 'ui.scroll.jqlite', 'ui.scroll']) + ['egCoreMod', 'egUiMod', 'ui.bootstrap']) -.directive('egGrid', function($window) { +.directive('egGrid', function() { return { restrict : 'AE', transclude : true, scope : { + // IDL class hint (e.g. "aou") idlClass : '@', @@ -21,10 +21,6 @@ angular.module('egGridMod', // grid preferences will be stored / retrieved with this key persistKey : '@', - // if true, use the scroll CSS to force a vertical height - // and scroll bar - isScroll : '@', - // field whose value is unique and may be used for item // reference / lookup. This will usually be someting like // "id". This is not needed when using autoFields, since we @@ -33,7 +29,7 @@ angular.module('egGridMod', // egList containting our tabular data is provided for us // and managed externally. - egList : '=', + dataProvider : '=', // if true, hide the sortPriority options in the // grid configuration UI. This is primarily used by @@ -45,319 +41,327 @@ angular.module('egGridMod', mainLabel : '@' }, + // TODO: avoid hard-coded url + templateUrl : '/eg/staff/parts/t_autogrid', + link : function(scope, element, attrs) { // link() is called after page compilation, which means our // eg-grid-field's have been parsed and loaded. Now it's // safe to perform our initial page load. - //scope.fetchData(); + scope.grid.collect(); }, - templateUrl : '/eg/staff/parts/t_autogrid', // TODO: avoid abs url + controller : [ + '$scope','egIDL','egAuth','egNet', + 'egGridFlatDataProvider','egGridColumnsProvider', + function($scope, egIDL, egAuth, egNet, + egGridFlatDataProvider, egGridColumnsProvider) { + + var grid = this; + + grid.init = function() { + grid.offset = 0; + grid.limit = 25; + grid.items = []; + grid.selected = {}; // idField-based + grid.totalCount = -1; + grid.dataProvider = $scope.dataProvider; + grid.idlClass = $scope.idlClass; + grid.mainLabel = $scope.mainLabel; + grid.indexField = $scope.idField; + grid.showGridConf = false; + + // default flex values for the index and selector columns + grid.indexFlex = 1; + grid.selectorFlex = 1; + + grid.columnsProvider = egGridColumnsProvider.instance({ + idlClass : grid.idlClass + }); - controller : // TODO: reqs list - function($scope, $timeout, $location, egIDL, egAuth, egNet, egList, egGridData) { - var self = this; + if ($scope.autoFields) { + grid.indexField = egIDL.classes[grid.idlClass].pkey; + if (!grid.mainLabel) + grid.mainLabel = egIDL.classes[grid.idlClass].label; + grid.columnsProvider.compileAutoColumns(); + } - // setup function. called at the end of the controller - this.init = function() { - self.limit = 10; - self.offset = 0; + if (!grid.dataProvider) { + grid.selfManagedData = true; + grid.dataProvider = egGridFlatDataProvider.instance({ + idlClass : grid.idlClass, + columnsProvider : grid.columnsProvider, + query : $scope.query + }); + } - $scope.indexFlex = 1; - $scope.selectorFlex = 1; - $scope.gridLabel = $scope.mainLabel; + grid.compileSort(); + $scope.grid = grid; + } - if (!$scope.query) { - console.error("egGrid requires a query"); - return; - } + grid.onContextMenu = function($event) { + var col = angular.element($event.target).attr('column'); + } - if (!$scope.idlClass) { - console.error("egGrid requires an idlClass"); - return; - } + grid.page = function() { + return (grid.offset / grid.limit) + 1; + } - if ($scope.egList) { - $scope.list = $scope.egList; - } else { - self.selfManaged = true; - $scope.list = egList.create(); + grid.goToPage = function(page) { + page = Number(page); + if (angular.isNumber(page) && page > 0) { + grid.offset = (page - 1) * grid.limit; + grid.collect(); } + } - if ($scope.autoFields) - self.compileAutoFields(); + grid.onFirstPage = function() { + return grid.offset == 0; + } - $scope.list.indexField = $scope.idField; + grid.hasNextPage = function() { + // we have less data than requested, there must + // not be any more pages + if (grid.count() < grid.limit) return false; - $scope.gridDataManager = egGridData.create({ - idlClass : $scope.idlClass, - query : $scope.query, - list : $scope.list, - // TODO: eg_grid_offset assumes one grid per page. - offset : parseInt($location.search().eg_grid_offset) - }); - } + // if the total count is not known, assume that a full + // page of data implies more pages are available. + if (grid.totalCount == -1) return true; - // clicking on a column header performs a quick, single-column - // sort. Sorts on the same column toggle between ascending - // and descending sort. - $scope.sortOn = function(col_name) { - var sort = $scope.gridDataManager.sort; - if (sort && sort.length && - sort[0] == col_name) { - var blob = {}; - blob[col_name] = 'desc'; - sort = [blob]; - } else { - sort = [col_name]; - } - $scope.gridDataManager.applySort(sort); + // we have a full page of data, but is there more? + return grid.totalCount > (grid.offset + grid.count()); } - $scope.modifyColumnFlex = function(column, val) { - column.flex += val; - // prevent flex:0; use hiding instead - if (column.flex < 1) - column.flex = 1; + grid.incrementPage = function() { + grid.offset += grid.limit; + grid.collect(); } - $scope.toggleGridConf = function() { - if ($scope.showGridConf) { - $scope.showGridConf = false; - $scope.gridDataManager.applySort(); + grid.decrementPage = function() { + if (grid.offset < grid.limit) { + grid.offset = 0; } else { - $scope.showGridConf = true; + grid.offset -= grid.limit; } + grid.collect(); } - /** - * Adds a column from an eg-grid-field or directly from - * an IDL field via compileAutoFields. - */ - this.addColumn = function(fieldSpec) { - - var field = { - name : fieldSpec.name, - label : fieldSpec.label, - path : fieldSpec.path, - flex : fieldSpec.flex, - datatype : fieldSpec.datatype, - display : (fieldSpec.display !== false) - }; - - if (!field.name) field.name = field.path; - if (!field.path) field.path = field.name; - - field = self.absorbField(field); - $scope.list.addColumn(field); + // number of items loaded for the current page of results + grid.count = function() { + return grid.items.length; } - $scope.fieldValue = function(item, key) { - if ($scope.egList) - return $scope.list.fieldValue(item, key); - // if we are managing the data, then our data is flat - return item ? item[key] : ''; + // returns the unique identifier value for the provided item + grid.indexValue = function(item) { + if (angular.isObject(item)) { + if (item !== null) { + if (grid.indexFieldAsFunction) + return item[grid.indexField](); + return item[grid.indexField]; + } + } + // passed a non-object; assume it's an index + return item; } - /** - * Caller wants to display all fields for the selected IDL class - * Find the fields and, when a field is a link, fetch the label - * from the "selector" field as well. - */ - this.compileAutoFields = function() { - if ($scope.list.allColumns.length) return; - var idlClass = egIDL.classes[$scope.idlClass]; - - $scope.idField = $scope.idField || idlClass.pkey; + // selects one row after deselecting all of the others + grid.selectOneItem = function(index) { + grid.selected = {}; + grid.selected[index] = true; + } - if (!$scope.gridLabel) { - $scope.gridLabel = idlClass.label; + // selects or deselects an item, without affecting the others. + // returns true if the item is selected; false if de-selected. + grid.toggleSelectOneItem = function(index) { + if (grid.selected[index]) { + delete grid.selected[index]; + return false; + } else { + return grid.selected[index] = true; } - - angular.forEach( - idlClass.fields.sort( - function(a, b) { return a.name < b.name ? -1 : 1 }), - function(field) { - if (field.virtual) return; - if (field.datatype == 'link' || field.datatype == 'org_unit') { - // if the field is a link and the linked class has a - // "selector" field specified, use the selector field - // as the display field for the grid. - // flattener will take care of the fleshing. - if (field['class']) { - var selectorField = egIDL.classes[field['class']].fields - .filter(function(f) { return Boolean(f.selector) })[0]; - if (selectorField) { - field.path = field.name + '.' + selectorField.selector; - } - } - } - self.addColumn(field); - } - ); } - // given a base class and a dotpath, find the IDL field - this.getIDLFieldFromPath = function(idlClass, path) { - var class_obj = egIDL.classes[idlClass]; - var path_parts = path.split(/\./); - - // note: use of for() is intentional for early exit - var idl_field; - for (var path_idx in path_parts) { - var part = path_parts[path_idx]; - - // find the field object matching the path component - for (var field_idx in class_obj.fields) { - if (class_obj.fields[field_idx].name == part) { - idl_field = class_obj.fields[field_idx]; - break; - } - } + grid.selectAllItems = function() { + angular.forEach(grid.items, function(item) { + grid.selected[grid.indexValue(item)] = true + }); + } - // unless we're at the end of the list, this field should - // link to another class. - - if (idl_field && idl_field['class'] && ( - idl_field.datatype == 'link' || - idl_field.datatype == 'org_unit')) { - class_obj = egIDL.classes[idl_field['class']]; - } else { - if (path_idx < (path_parts.length - 1)) { - // we ran out of classes to hop through before - // we ran out of path components - console.error("egGrid: invalid IDL path: " + path); - } - } + // if all are selected, deselect all, otherwise select all + grid.toggleSelectAllItems = function() { + if (Object.keys(grid.selected).length == grid.items.length) { + grid.selected = {}; + } else { + grid.selectAllItems(); } + } - return idl_field; + // returns true if item1 appears in the list before item2; + // false otherwise. this is slightly more efficient that + // finding the position of each then comparing them. + // item1 / item2 may be an item or an item index + grid.comesBefore = function(itemOrIndex1, itemOrIndex2) { + var idx1 = grid.indexValue(itemOrIndex1); + var idx2 = grid.indexValue(itemOrIndex2); + + // use for() for early exit + for (var i = 0; i < grid.items.length; i++) { + var idx = grid.indexValue(grid.items[i]); + if (idx == idx1) return true; + if (idx == idx2) return false; + } + return false; } - /** - * Looks for the matching IDL field to extract the label - * and datattype as needed. - * Creates a local copy of the field for our internal - * machinations. - */ - this.absorbField = function(field) { - - // start by cloning the field so we can flesh it out. - // note: aungular.copy won't work, because 'field' may - // be a $scope object. - var new_field = { - name : field.name, - label : field.label, - path : field.path, - flex : Number(field.flex) || 2, - display : (field.display === false) ? false : true - }; - - // lookup the matching IDL field - var idl_field = field.datatype ? field : - self.getIDLFieldFromPath($scope.idlClass, field.path); - - // No matching IDL field. Caller has gone commando. - // Nothing left to do. - if (!idl_field) return new_field; - - new_field.datatype = idl_field.datatype; - - if (field.label) { - // caller-provided label - new_field.label = field.label; - } else { - if (idl_field.label) { - new_field.label = idl_field.label; - } else { - new_field.label = new_field.name; - } + // 0-based position of item in the current data set + grid.indexOf = function(item) { + var idx = grid.indexValue(item); + for (var i = 0; i < grid.items.length; i++) { + if (grid.indexValue(grid.items[i]) == idx) + return i; } + return -1; + } - return new_field; + grid.modifyColumnFlex = function(column, val) { + column.flex += val; + // prevent flex:0; use hiding instead + if (column.flex < 1) + column.flex = 1; } - $scope.handleRowClick = function($event, item) { - var index = $scope.list.indexValue(item); + // handles click, control-click, and shift-click + grid.handleRowClick = function($event, item) { + var index = grid.indexValue(item); if ($event.ctrlKey || $event.metaKey /* mac command */) { // control-click - if ($scope.list.toggleOneSelection(index)) - self.lastSelectedRowIndex = index; + if (grid.toggleSelectOneItem(index)) + grid.lastSelectedItemIndex = index; } else if ($event.shiftKey) { // shift-click - if (!self.lastSelectedRowIndex || - index == self.lastSelectedRowIndex) { + if (!grid.lastSelectedItemIndex || + index == grid.lastSelectedItemIndex) { // no source row, just do a simple select - $scope.list.selectOne(index); - self.lastSelectedRowIndex = index; + grid.selectOneItem(index); + grid.lastSelectedItemIndex = index; return; } var selecting = false; var ascending = - $scope.list.comesBefore(self.lastSelectedRowIndex, item); + grid.comesBefore(grid.lastSelectedItemIndex, item); var startPos = - $scope.list.indexOf(self.lastSelectedRowIndex); + grid.indexOf(grid.lastSelectedItemIndex); // update to new last-selected - self.lastSelectedRowIndex = index; + grid.lastSelectedItemIndex = index; // select each row between the last selected and // currently selected items while (true) { startPos += ascending ? 1 : -1; - var curItem = $scope.list.items[startPos]; + var curItem = grid.items[startPos]; if (!curItem) break; - var curIdx = $scope.list.indexValue(curItem); - $scope.list.selected[curIdx] = true; + var curIdx = grid.indexValue(curItem); + grid.selected[curIdx] = true; if (curIdx == index) break; // all done } } else { - $scope.list.selectOne(index); - self.lastSelectedRowIndex = index; + grid.selectOneItem(index); + grid.lastSelectedItemIndex = index; } } - $scope.itemIsSelected = function(item) { - return $scope.list.selected[ - $scope.list.indexValue(item) - ]; + // Builds a sort expression from column sort priorities. + // called on page load and any time the priorities are modified. + grid.compileSort = function() { + var sortList = grid.columnsProvider.columns.filter( + function(col) { return Number(col.sort) != 0 } + ).sort( + function(a, b) { + if (Math.abs(a.sort) < Math.abs(b.sort)) + return -1; + return 1; + } + ); + + if (sortList.length) { + grid.dataProvider.sort = sortList.map(function(col) { + var blob = {}; + blob[col.name] = col.sort < 0 ? 'desc' : 'asc'; + return blob; + }); + } } - this.onColumnDrag = function(col) { - // track which column we're dragging - self.dragColumn = col; + // builds a sort expression using a single column, + // toggling between ascending and descending sort. + grid.quickSort = function(col_name) { + var sort = grid.dataProvider.sort; + if (sort && sort.length && + sort[0] == col_name) { + var blob = {}; + blob[col_name] = 'desc'; + grid.dataProvider.sort = [blob]; + } else { + grid.dataProvider.sort = [col_name]; + } + + grid.collect(); } - // if the target column does not match the source column, - // increase the size of the source column. - this.onColumnDragOver = function(target) { - if (angular.isUndefined(target)) return; - if (target == self.dragColumn) return; - if (self.dragColumn == '+index') { - $scope.indexFlex += 1; - } else if (self.dragColumn == '+selector') { - $scope.selectorFlex += 1; + grid.toggleConfDisplay = function() { + if (grid.showGridConf) { + grid.showGridConf = false; + grid.compileSort(); + grid.collect(); } else { - var column = $scope.list.findColumn(self.dragColumn); - $scope.modifyColumnFlex(column, 1); + grid.showGridConf = true; } - $scope.$apply(); // needed } - /* - $scope.fetchMoreData = function() { - console.log('fetchMoreData'); - self.offset += self.limit; - $scope.fetchData(); + grid.onColumnDrop = function(target) { + if (angular.isUndefined(target)) return; + if (target == grid.dragColumn) return; + var srcIdx, targetIdx, srcCol; + angular.forEach(grid.columnsProvider.columns, + function(col, idx) { + if (col.name == grid.dragColumn) { + srcIdx = idx; + srcCol = col; + } else if (col.name == target) { + targetIdx = idx; + } + } + ); + + if (srcIdx < targetIdx) targetIdx--; + + // move src column from old location to new location in + // the columns array, then force a page refresh + grid.columnsProvider.columns.splice(srcIdx, 1); + grid.columnsProvider.columns.splice(targetIdx, 0, srcCol); + $scope.$apply(); } - */ - this.init(); - } + // asks the dataProvider for a page of data + grid.collect = function() { + grid.items = []; + grid.selectedItems = {}; + grid.dataProvider.get( + grid.offset, + grid.limit, + function(item) { + if (item) grid.items.push(item) + } + ); + } + + grid.init(); + }] }; }) @@ -384,147 +388,251 @@ angular.module('egGridMod', }; }) -/** - * Factory service for egGridDataManager instances, which are - * responsible for collecting flattened grid data. - */ -.factory('egGridData', ['egNet','egAuth', +.factory('egGridColumnsProvider', ['egIDL', function(egIDL) { - function(egNet, egAuth) { + function ColumnsProvider(args) { + var cols = this; + cols.columns = []; + cols.visible = {}; + cols.idlClass = args.idlClass; - // per-grid data manager class - function EgGridDataManager(args) { - var self = this; + cols.showAllColumns = function() { + angular.forEach(cols.columns, function(column) { + cols.visible[column.name] = true; + }); + } - this.idlClass = args.idlClass; - this.query = args.query; - this.list = args.list; - this.offset = args.offset || 0; - this.version = 1; + cols.hideAllColumns = function() { + cols.visible = {}; + } - this.reset = function() { - self.list.resetPageData(); - self.version++; + cols.indexOf = function(name) { + for (var i = 0; i < cols.columns.length; i++) { + if (cols.columns[i].name == name) + return i; } + return -1; + } + + cols.findColumn = function(name) { + return cols.columns[cols.indexOf(name)]; + } + + cols.compileAutoColumns = function() { + + var idl_class = egIDL.classes[cols.idlClass]; + + angular.forEach( + idl_class.fields.sort( + function(a, b) { return a.name < b.name ? -1 : 1 }), + function(field) { + if (field.virtual) return; + if (field.datatype == 'link' || field.datatype == 'org_unit') { + // if the field is a link and the linked class has a + // "selector" field specified, use the selector field + // as the display field for the columns. + // flattener will take care of the fleshing. + if (field['class']) { + var selector_field = egIDL.classes[field['class']].fields + .filter(function(f) { return Boolean(f.selector) })[0]; + if (selector_field) { + field.path = field.name + '.' + selector_field.selector; + } + } + } + cols.add(field, true); + } + ); + } + + // Add a column to the columns collection. + // Columns may come from a slim eg-columns-field or + // directly from the IDL. + cols.add = function(colSpec, fromIDL) { - this.revision = function() { - return self.version; + var column = { + name : colSpec.name, + label : colSpec.label, + path : colSpec.path, + flex : Number(colSpec.flex) || 2, + sort : Number(colSpec.sort) || 0, + datatype : colSpec.datatype, + }; + + if (!column.name) column.name = column.path; + if (!column.path) column.path = column.name; + + if (colSpec.display !== false) + cols.visible[column.name] = true; + + cols.columns.push(column); + + if (fromIDL) return; + + // lookup the matching IDL field + var idl_field = cols.idlFieldFromPath(column.path); + + if (!idl_field) return; // ad-hoc field + + column.datatype = idl_field.datatype; + + if (!column.label) { + column.label = idl_field.label || column.name; } + }, - this.get = function(index, count, success) { - index -= 1; // make it zero-based + // finds the IDL field from the dotpath, using the columns + // idlClass as the base. + cols.idlFieldFromPath = function(dotpath) { + var class_obj = egIDL.classes[cols.idlClass]; + var path_parts = dotpath.split(/\./); + + // for() == early exit + var idl_field; + for (var path_idx in path_parts) { + var part = path_parts[path_idx]; + + // find the field object matching the path component + for (var field_idx in class_obj.fields) { + if (class_obj.fields[field_idx].name == part) { + idl_field = class_obj.fields[field_idx]; + break; + } + } - if (self.offset) { index += self.offset } - - if (index < 0) return success([]); + // unless we're at the end of the list, this field should + // link to another class. - console.log('index = ' + index + ' : count = ' + count); + if (idl_field && idl_field['class'] && ( + idl_field.datatype == 'link' || + idl_field.datatype == 'org_unit')) { + class_obj = egIDL.classes[idl_field['class']]; + } else { + if (path_idx < (path_parts.length - 1)) { + // we ran out of classes to hop through before + // we ran out of path components + console.error("egGrid: invalid IDL path: " + path); + } + } + } - // we may already have the data - var slice = self.list.items.slice(index, index + count); - if (slice.length && slice[0]) return success(slice); + return idl_field; + } + } + return { + instance : function(args) { return new ColumnsProvider(args) } + } +}]) + +// Factory service for egGridDataManager instances, which are +// responsible for collecting flattened grid data. +.factory('egGridFlatDataProvider', + ['egNet','egAuth', + function(egNet, egAuth) { + + function FlatDataProvider(args) { + var gridData = this; + + gridData.idlClass = args.idlClass; + gridData.query = args.query; + gridData.columnsProvider = args.columnsProvider; + gridData.sort = []; + + gridData.get = function(index, count, onresponse) { + + // fetch data for all currently visible columns var queryFields = {} - angular.forEach(self.list.allColumns, function(field) { - if (self.list.displayColumns[field.name]) - queryFields[field.name] = field.path || field.name; + angular.forEach(gridData.columnsProvider.columns, function(col) { + if (gridData.columnsProvider.visible[col.name]) + queryFields[col.name] = col.path; }); - var respIndex = index; egNet.request( 'open-ils.fielder', 'open-ils.fielder.flattened_search', - egAuth.token(), self.idlClass, queryFields, - self.query, - { sort : self.sort, + egAuth.token(), gridData.idlClass, queryFields, + gridData.query, + { sort : gridData.sort, limit : count, offset : index } ).then( - function() { // oncomplete - success(self.list.items.slice(index, index + count)); - }, - null, // onerror - function(item) { // onmessage - self.list.items[respIndex++] = item; - } + null, null, + function(item) { onresponse(item) } ); } - this.applySort = function(sortBlob) { - self.offset = 0; - - if (sortBlob) { - self.sort = sortBlob; - - } else { - // no sort provided, compile the sort blob - // from the column sort priority values - - var sortList = self.list.allColumns.filter( - function(col) { return Number(col.sortPriority) != 0 } - ).sort( - function(a, b) { - if (Math.abs(a.sortPriority) < Math.abs(b.sortPriority)) - return -1; - return 1; - } - ); - - self.sort = sortList.map(function(col) { - var blob = {}; - blob[col.name] = col.sortPriority < 0 ? 'desc' : 'asc'; - return blob; - }); - } - - self.reset(); + gridData.itemFieldValue = function(item, column) { + // all of our data is flattened + return item[column.name]; } } return { - create : function(args) { - return new EgGridDataManager(args); + instance : function(args) { + return new FlatDataProvider(args); } }; } ]) -/** Simplified dnd directives for grid column controls. - * Extract these out if the can be made generic enough - */ - -.directive('egDragSource', function() { +.directive('egGridColumnDragSource', function() { return { restrict : 'A', require : '^egGrid', link : function(scope, element, attrs, egGridCtrl) { angular.element(element).attr('draggable', 'true'); + element.bind('dragstart', function(e) { - var col = angular.element(e.target).attr('column'); - egGridCtrl.onColumnDrag(col); + egGridCtrl.dragColumn = attrs.column; + angular.element(e.target).addClass('eg-grid-col-drag'); + }); + + element.bind('dragend', function(e) { + console.log('dragend'); + angular.element(e.target).removeClass('eg-grid-col-drag'); }); } }; }) -.directive('egDragDest', function() { +.directive('egGridColumnDragDest', function() { return { restrict : 'A', require : '^egGrid', link : function(scope, element, attrs, egGridCtrl) { - element.bind('dragover', function(e) { + + element.bind('dragover', function(e) { // required for drop + e.stopPropagation(); + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + }); + + element.bind('dragenter', function(e) { + e.stopPropagation(); + e.preventDefault(); + angular.element(e.target).addClass('eg-grid-col-hover'); + }); + + element.bind('dragleave', function(e) { e.stopPropagation(); e.preventDefault(); - //e.dataTransfer.dropEffect = 'copy'; - var col = angular.element(e.target).attr('column'); - console.log('dragover ' + col); - egGridCtrl.onColumnDragOver(col); + angular.element(e.target).removeClass('eg-grid-col-hover'); + }); + + element.bind('drop', function(e) { + e.stopPropagation(); + e.preventDefault(); + angular.element(e.target).removeClass('eg-grid-col-hover'); + egGridCtrl.onColumnDrop(attrs.column); // move the column }); } }; }) + + /** * Translates bare IDL object values into display values. * 1. Passes dates through the angular date filter @@ -547,4 +655,3 @@ angular.module('egGridMod', } }]); - diff --git a/Open-ILS/web/js/ui/default/staff/services/grid2.js b/Open-ILS/web/js/ui/default/staff/services/grid2.js deleted file mode 100644 index f72278ca48..0000000000 --- a/Open-ILS/web/js/ui/default/staff/services/grid2.js +++ /dev/null @@ -1,657 +0,0 @@ -angular.module('egGridMod', - ['egCoreMod', 'egUiMod', 'ui.bootstrap']) - -.directive('egGrid', function() { - return { - restrict : 'AE', - transclude : true, - scope : { - - // IDL class hint (e.g. "aou") - idlClass : '@', - - // points to a structure in the calling scope which defines - // a PCRUD-compliant query. - query : '=', - - // if true, grid columns are derived from all non-virtual - // fields on the base idlClass - autoFields : '@', - - // grid preferences will be stored / retrieved with this key - persistKey : '@', - - // field whose value is unique and may be used for item - // reference / lookup. This will usually be someting like - // "id". This is not needed when using autoFields, since we - // can determine the primary key directly from the IDL. - idField : '@', - - // egList containting our tabular data is provided for us - // and managed externally. - dataProvider : '=', - - // if true, hide the sortPriority options in the - // grid configuration UI. This is primarily used by - // UIs where the data is ephemeral and can only be - // single-display-column sorted. - disableSortPriority : '@', - - // optional primary grid label - mainLabel : '@' - }, - - // TODO: avoid hard-coded url - templateUrl : '/eg/staff/parts/t_autogrid2', - - link : function(scope, element, attrs) { - // link() is called after page compilation, which means our - // eg-grid-field's have been parsed and loaded. Now it's - // safe to perform our initial page load. - scope.grid.collect(); - }, - - controller : [ - '$scope','egIDL','egAuth','egNet', - 'egGridFlatDataProvider','egGridColumnsProvider', - function($scope, egIDL, egAuth, egNet, - egGridFlatDataProvider, egGridColumnsProvider) { - - var grid = this; - - grid.init = function() { - grid.offset = 0; - grid.limit = 25; - grid.items = []; - grid.selected = {}; // idField-based - grid.totalCount = -1; - grid.dataProvider = $scope.dataProvider; - grid.idlClass = $scope.idlClass; - grid.mainLabel = $scope.mainLabel; - grid.indexField = $scope.idField; - grid.showGridConf = false; - - // default flex values for the index and selector columns - grid.indexFlex = 1; - grid.selectorFlex = 1; - - grid.columnsProvider = egGridColumnsProvider.instance({ - idlClass : grid.idlClass - }); - - if ($scope.autoFields) { - grid.indexField = egIDL.classes[grid.idlClass].pkey; - if (!grid.mainLabel) - grid.mainLabel = egIDL.classes[grid.idlClass].label; - grid.columnsProvider.compileAutoColumns(); - } - - if (!grid.dataProvider) { - grid.selfManagedData = true; - grid.dataProvider = egGridFlatDataProvider.instance({ - idlClass : grid.idlClass, - columnsProvider : grid.columnsProvider, - query : $scope.query - }); - } - - grid.compileSort(); - $scope.grid = grid; - } - - grid.onContextMenu = function($event) { - var col = angular.element($event.target).attr('column'); - } - - grid.page = function() { - return (grid.offset / grid.limit) + 1; - } - - grid.goToPage = function(page) { - page = Number(page); - if (angular.isNumber(page) && page > 0) { - grid.offset = (page - 1) * grid.limit; - grid.collect(); - } - } - - grid.onFirstPage = function() { - return grid.offset == 0; - } - - grid.hasNextPage = function() { - // we have less data than requested, there must - // not be any more pages - if (grid.count() < grid.limit) return false; - - // if the total count is not known, assume that a full - // page of data implies more pages are available. - if (grid.totalCount == -1) return true; - - // we have a full page of data, but is there more? - return grid.totalCount > (grid.offset + grid.count()); - } - - grid.incrementPage = function() { - grid.offset += grid.limit; - grid.collect(); - } - - grid.decrementPage = function() { - if (grid.offset < grid.limit) { - grid.offset = 0; - } else { - grid.offset -= grid.limit; - } - grid.collect(); - } - - // number of items loaded for the current page of results - grid.count = function() { - return grid.items.length; - } - - // returns the unique identifier value for the provided item - grid.indexValue = function(item) { - if (angular.isObject(item)) { - if (item !== null) { - if (grid.indexFieldAsFunction) - return item[grid.indexField](); - return item[grid.indexField]; - } - } - // passed a non-object; assume it's an index - return item; - } - - // selects one row after deselecting all of the others - grid.selectOneItem = function(index) { - grid.selected = {}; - grid.selected[index] = true; - } - - // selects or deselects an item, without affecting the others. - // returns true if the item is selected; false if de-selected. - grid.toggleSelectOneItem = function(index) { - if (grid.selected[index]) { - delete grid.selected[index]; - return false; - } else { - return grid.selected[index] = true; - } - } - - grid.selectAllItems = function() { - angular.forEach(grid.items, function(item) { - grid.selected[grid.indexValue(item)] = true - }); - } - - // if all are selected, deselect all, otherwise select all - grid.toggleSelectAllItems = function() { - if (Object.keys(grid.selected).length == grid.items.length) { - grid.selected = {}; - } else { - grid.selectAllItems(); - } - } - - // returns true if item1 appears in the list before item2; - // false otherwise. this is slightly more efficient that - // finding the position of each then comparing them. - // item1 / item2 may be an item or an item index - grid.comesBefore = function(itemOrIndex1, itemOrIndex2) { - var idx1 = grid.indexValue(itemOrIndex1); - var idx2 = grid.indexValue(itemOrIndex2); - - // use for() for early exit - for (var i = 0; i < grid.items.length; i++) { - var idx = grid.indexValue(grid.items[i]); - if (idx == idx1) return true; - if (idx == idx2) return false; - } - return false; - } - - // 0-based position of item in the current data set - grid.indexOf = function(item) { - var idx = grid.indexValue(item); - for (var i = 0; i < grid.items.length; i++) { - if (grid.indexValue(grid.items[i]) == idx) - return i; - } - return -1; - } - - grid.modifyColumnFlex = function(column, val) { - column.flex += val; - // prevent flex:0; use hiding instead - if (column.flex < 1) - column.flex = 1; - } - - // handles click, control-click, and shift-click - grid.handleRowClick = function($event, item) { - var index = grid.indexValue(item); - - if ($event.ctrlKey || $event.metaKey /* mac command */) { - // control-click - if (grid.toggleSelectOneItem(index)) - grid.lastSelectedItemIndex = index; - - } else if ($event.shiftKey) { - // shift-click - if (!grid.lastSelectedItemIndex || - index == grid.lastSelectedItemIndex) { - // no source row, just do a simple select - grid.selectOneItem(index); - grid.lastSelectedItemIndex = index; - return; - } - - var selecting = false; - var ascending = - grid.comesBefore(grid.lastSelectedItemIndex, item); - var startPos = - grid.indexOf(grid.lastSelectedItemIndex); - - // update to new last-selected - grid.lastSelectedItemIndex = index; - - // select each row between the last selected and - // currently selected items - while (true) { - startPos += ascending ? 1 : -1; - var curItem = grid.items[startPos]; - if (!curItem) break; - var curIdx = grid.indexValue(curItem); - grid.selected[curIdx] = true; - if (curIdx == index) break; // all done - } - - } else { - grid.selectOneItem(index); - grid.lastSelectedItemIndex = index; - } - } - - // Builds a sort expression from column sort priorities. - // called on page load and any time the priorities are modified. - grid.compileSort = function() { - var sortList = grid.columnsProvider.columns.filter( - function(col) { return Number(col.sort) != 0 } - ).sort( - function(a, b) { - if (Math.abs(a.sort) < Math.abs(b.sort)) - return -1; - return 1; - } - ); - - if (sortList.length) { - grid.dataProvider.sort = sortList.map(function(col) { - var blob = {}; - blob[col.name] = col.sort < 0 ? 'desc' : 'asc'; - return blob; - }); - } - } - - // builds a sort expression using a single column, - // toggling between ascending and descending sort. - grid.quickSort = function(col_name) { - var sort = grid.dataProvider.sort; - if (sort && sort.length && - sort[0] == col_name) { - var blob = {}; - blob[col_name] = 'desc'; - grid.dataProvider.sort = [blob]; - } else { - grid.dataProvider.sort = [col_name]; - } - - grid.collect(); - } - - grid.toggleConfDisplay = function() { - if (grid.showGridConf) { - grid.showGridConf = false; - grid.compileSort(); - grid.collect(); - } else { - grid.showGridConf = true; - } - } - - grid.onColumnDrop = function(target) { - if (angular.isUndefined(target)) return; - if (target == grid.dragColumn) return; - var srcIdx, targetIdx, srcCol; - angular.forEach(grid.columnsProvider.columns, - function(col, idx) { - if (col.name == grid.dragColumn) { - srcIdx = idx; - srcCol = col; - } else if (col.name == target) { - targetIdx = idx; - } - } - ); - - if (srcIdx < targetIdx) targetIdx--; - - // move src column from old location to new location in - // the columns array, then force a page refresh - grid.columnsProvider.columns.splice(srcIdx, 1); - grid.columnsProvider.columns.splice(targetIdx, 0, srcCol); - $scope.$apply(); - } - - // asks the dataProvider for a page of data - grid.collect = function() { - grid.items = []; - grid.selectedItems = {}; - grid.dataProvider.get( - grid.offset, - grid.limit, - function(item) { - if (item) grid.items.push(item) - } - ); - } - - grid.init(); - }] - }; -}) - -/** - * eg-grid-field : used for collecting custom field data from the templates. - * This directive does not direct display, it just passes data up to the - * parent grid. - */ -.directive('egGridField', function() { - return { - require : '^egGrid', - restrict : 'AE', - transclude : true, - scope : { - name : '@', // required; unique name - path : '@', // optional; flesh path - label : '@', // optional; display label - flex : '@', // optoinal; default flex width - }, - template : '
', // NOOP template - link : function(scope, element, attrs, egGridCtrl) { - egGridCtrl.addColumn(scope); - } - }; -}) - -.factory('egGridColumnsProvider', ['egIDL', function(egIDL) { - - function ColumnsProvider(args) { - var cols = this; - cols.columns = []; - cols.visible = {}; - cols.idlClass = args.idlClass; - - cols.showAllColumns = function() { - angular.forEach(cols.columns, function(column) { - cols.visible[column.name] = true; - }); - } - - cols.hideAllColumns = function() { - cols.visible = {}; - } - - cols.indexOf = function(name) { - for (var i = 0; i < cols.columns.length; i++) { - if (cols.columns[i].name == name) - return i; - } - return -1; - } - - cols.findColumn = function(name) { - return cols.columns[cols.indexOf(name)]; - } - - cols.compileAutoColumns = function() { - - var idl_class = egIDL.classes[cols.idlClass]; - - angular.forEach( - idl_class.fields.sort( - function(a, b) { return a.name < b.name ? -1 : 1 }), - function(field) { - if (field.virtual) return; - if (field.datatype == 'link' || field.datatype == 'org_unit') { - // if the field is a link and the linked class has a - // "selector" field specified, use the selector field - // as the display field for the columns. - // flattener will take care of the fleshing. - if (field['class']) { - var selector_field = egIDL.classes[field['class']].fields - .filter(function(f) { return Boolean(f.selector) })[0]; - if (selector_field) { - field.path = field.name + '.' + selector_field.selector; - } - } - } - cols.add(field, true); - } - ); - } - - // Add a column to the columns collection. - // Columns may come from a slim eg-columns-field or - // directly from the IDL. - cols.add = function(colSpec, fromIDL) { - - var column = { - name : colSpec.name, - label : colSpec.label, - path : colSpec.path, - flex : Number(colSpec.flex) || 2, - sort : Number(colSpec.sort) || 0, - datatype : colSpec.datatype, - }; - - if (!column.name) column.name = column.path; - if (!column.path) column.path = column.name; - - if (colSpec.display !== false) - cols.visible[column.name] = true; - - cols.columns.push(column); - - if (fromIDL) return; - - // lookup the matching IDL field - var idl_field = cols.idlFieldFromPath(column.path); - - if (!idl_field) return; // ad-hoc field - - column.datatype = idl_field.datatype; - - if (!column.label) { - column.label = idl_field.label || column.name; - } - }, - - // finds the IDL field from the dotpath, using the columns - // idlClass as the base. - cols.idlFieldFromPath = function(dotpath) { - var class_obj = egIDL.classes[cols.idlClass]; - var path_parts = dotpath.split(/\./); - - // for() == early exit - var idl_field; - for (var path_idx in path_parts) { - var part = path_parts[path_idx]; - - // find the field object matching the path component - for (var field_idx in class_obj.fields) { - if (class_obj.fields[field_idx].name == part) { - idl_field = class_obj.fields[field_idx]; - break; - } - } - - // unless we're at the end of the list, this field should - // link to another class. - - if (idl_field && idl_field['class'] && ( - idl_field.datatype == 'link' || - idl_field.datatype == 'org_unit')) { - class_obj = egIDL.classes[idl_field['class']]; - } else { - if (path_idx < (path_parts.length - 1)) { - // we ran out of classes to hop through before - // we ran out of path components - console.error("egGrid: invalid IDL path: " + path); - } - } - } - - return idl_field; - } - } - - return { - instance : function(args) { return new ColumnsProvider(args) } - } -}]) - -// Factory service for egGridDataManager instances, which are -// responsible for collecting flattened grid data. -.factory('egGridFlatDataProvider', - ['egNet','egAuth', - function(egNet, egAuth) { - - function FlatDataProvider(args) { - var gridData = this; - - gridData.idlClass = args.idlClass; - gridData.query = args.query; - gridData.columnsProvider = args.columnsProvider; - gridData.sort = []; - - gridData.get = function(index, count, onresponse) { - - // fetch data for all currently visible columns - var queryFields = {} - angular.forEach(gridData.columnsProvider.columns, function(col) { - if (gridData.columnsProvider.visible[col.name]) - queryFields[col.name] = col.path; - }); - - egNet.request( - 'open-ils.fielder', - 'open-ils.fielder.flattened_search', - egAuth.token(), gridData.idlClass, queryFields, - gridData.query, - { sort : gridData.sort, - limit : count, - offset : index - } - ).then( - null, null, - function(item) { onresponse(item) } - ); - } - - gridData.itemFieldValue = function(item, column) { - // all of our data is flattened - return item[column.name]; - } - } - - return { - instance : function(args) { - return new FlatDataProvider(args); - } - }; - } -]) - -.directive('egGridColumnDragSource', function() { - return { - restrict : 'A', - require : '^egGrid', - link : function(scope, element, attrs, egGridCtrl) { - angular.element(element).attr('draggable', 'true'); - - element.bind('dragstart', function(e) { - egGridCtrl.dragColumn = attrs.column; - angular.element(e.target).addClass('eg-grid-col-drag'); - }); - - element.bind('dragend', function(e) { - console.log('dragend'); - angular.element(e.target).removeClass('eg-grid-col-drag'); - }); - } - }; -}) - -.directive('egGridColumnDragDest', function() { - return { - restrict : 'A', - require : '^egGrid', - link : function(scope, element, attrs, egGridCtrl) { - - element.bind('dragover', function(e) { // required for drop - e.stopPropagation(); - e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; - }); - - element.bind('dragenter', function(e) { - e.stopPropagation(); - e.preventDefault(); - angular.element(e.target).addClass('eg-grid-col-hover'); - }); - - element.bind('dragleave', function(e) { - e.stopPropagation(); - e.preventDefault(); - angular.element(e.target).removeClass('eg-grid-col-hover'); - }); - - element.bind('drop', function(e) { - e.stopPropagation(); - e.preventDefault(); - angular.element(e.target).removeClass('eg-grid-col-hover'); - egGridCtrl.onColumnDrop(attrs.column); // move the column - }); - } - }; -}) - - - -/** - * Translates bare IDL object values into display values. - * 1. Passes dates through the angular date filter - * 2. Translates bools to Booleans so the browser can display translated - * value. (Though we could manually translate instead..) - * Others likely to follow... - */ -.filter('egGridvalueFilter', ['$filter', function($filter) { - return function(value, item) { - switch(item.datatype) { - case 'bool': - // Browser will translate true/false for us - return Boolean(value == 't'); - case 'timestamp': - // canned angular date filter FTW - return $filter('date')(value); - default: - return value; - } - } -}]); - diff --git a/Open-ILS/web/js/ui/default/staff/services/grid_deprecated.js b/Open-ILS/web/js/ui/default/staff/services/grid_deprecated.js new file mode 100644 index 0000000000..c96733f279 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/grid_deprecated.js @@ -0,0 +1,550 @@ + +angular.module('egGridMod', + ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap', 'ui.scroll.jqlite', 'ui.scroll']) + +.directive('egGrid', function($window) { + return { + restrict : 'AE', + transclude : true, + scope : { + // IDL class hint (e.g. "aou") + idlClass : '@', + + // points to a structure in the calling scope which defines + // a PCRUD-compliant query. + query : '=', + + // if true, grid columns are derived from all non-virtual + // fields on the base idlClass + autoFields : '@', + + // grid preferences will be stored / retrieved with this key + persistKey : '@', + + // if true, use the scroll CSS to force a vertical height + // and scroll bar + isScroll : '@', + + // field whose value is unique and may be used for item + // reference / lookup. This will usually be someting like + // "id". This is not needed when using autoFields, since we + // can determine the primary key directly from the IDL. + idField : '@', + + // egList containting our tabular data is provided for us + // and managed externally. + egList : '=', + + // if true, hide the sortPriority options in the + // grid configuration UI. This is primarily used by + // UIs where the data is ephemeral and can only be + // single-display-column sorted. + disableSortPriority : '@', + + // optional primary grid label + mainLabel : '@' + }, + + link : function(scope, element, attrs) { + // link() is called after page compilation, which means our + // eg-grid-field's have been parsed and loaded. Now it's + // safe to perform our initial page load. + //scope.fetchData(); + }, + + templateUrl : '/eg/staff/parts/t_autogrid', // TODO: avoid abs url + + controller : // TODO: reqs list + function($scope, $timeout, $location, egIDL, egAuth, egNet, egList, egGridData) { + var self = this; + + // setup function. called at the end of the controller + this.init = function() { + self.limit = 10; + self.offset = 0; + + $scope.indexFlex = 1; + $scope.selectorFlex = 1; + $scope.gridLabel = $scope.mainLabel; + + if (!$scope.query) { + console.error("egGrid requires a query"); + return; + } + + if (!$scope.idlClass) { + console.error("egGrid requires an idlClass"); + return; + } + + if ($scope.egList) { + $scope.list = $scope.egList; + } else { + self.selfManaged = true; + $scope.list = egList.create(); + } + + if ($scope.autoFields) + self.compileAutoFields(); + + $scope.list.indexField = $scope.idField; + + $scope.gridDataManager = egGridData.create({ + idlClass : $scope.idlClass, + query : $scope.query, + list : $scope.list, + // TODO: eg_grid_offset assumes one grid per page. + offset : parseInt($location.search().eg_grid_offset) + }); + } + + // clicking on a column header performs a quick, single-column + // sort. Sorts on the same column toggle between ascending + // and descending sort. + $scope.sortOn = function(col_name) { + var sort = $scope.gridDataManager.sort; + if (sort && sort.length && + sort[0] == col_name) { + var blob = {}; + blob[col_name] = 'desc'; + sort = [blob]; + } else { + sort = [col_name]; + } + $scope.gridDataManager.applySort(sort); + } + + $scope.modifyColumnFlex = function(column, val) { + column.flex += val; + // prevent flex:0; use hiding instead + if (column.flex < 1) + column.flex = 1; + } + + $scope.toggleGridConf = function() { + if ($scope.showGridConf) { + $scope.showGridConf = false; + $scope.gridDataManager.applySort(); + } else { + $scope.showGridConf = true; + } + } + + /** + * Adds a column from an eg-grid-field or directly from + * an IDL field via compileAutoFields. + */ + this.addColumn = function(fieldSpec) { + + var field = { + name : fieldSpec.name, + label : fieldSpec.label, + path : fieldSpec.path, + flex : fieldSpec.flex, + datatype : fieldSpec.datatype, + display : (fieldSpec.display !== false) + }; + + if (!field.name) field.name = field.path; + if (!field.path) field.path = field.name; + + field = self.absorbField(field); + $scope.list.addColumn(field); + } + + $scope.fieldValue = function(item, key) { + if ($scope.egList) + return $scope.list.fieldValue(item, key); + // if we are managing the data, then our data is flat + return item ? item[key] : ''; + } + + /** + * Caller wants to display all fields for the selected IDL class + * Find the fields and, when a field is a link, fetch the label + * from the "selector" field as well. + */ + this.compileAutoFields = function() { + if ($scope.list.allColumns.length) return; + var idlClass = egIDL.classes[$scope.idlClass]; + + $scope.idField = $scope.idField || idlClass.pkey; + + if (!$scope.gridLabel) { + $scope.gridLabel = idlClass.label; + } + + angular.forEach( + idlClass.fields.sort( + function(a, b) { return a.name < b.name ? -1 : 1 }), + function(field) { + if (field.virtual) return; + if (field.datatype == 'link' || field.datatype == 'org_unit') { + // if the field is a link and the linked class has a + // "selector" field specified, use the selector field + // as the display field for the grid. + // flattener will take care of the fleshing. + if (field['class']) { + var selectorField = egIDL.classes[field['class']].fields + .filter(function(f) { return Boolean(f.selector) })[0]; + if (selectorField) { + field.path = field.name + '.' + selectorField.selector; + } + } + } + self.addColumn(field); + } + ); + } + + // given a base class and a dotpath, find the IDL field + this.getIDLFieldFromPath = function(idlClass, path) { + var class_obj = egIDL.classes[idlClass]; + var path_parts = path.split(/\./); + + // note: use of for() is intentional for early exit + var idl_field; + for (var path_idx in path_parts) { + var part = path_parts[path_idx]; + + // find the field object matching the path component + for (var field_idx in class_obj.fields) { + if (class_obj.fields[field_idx].name == part) { + idl_field = class_obj.fields[field_idx]; + break; + } + } + + // unless we're at the end of the list, this field should + // link to another class. + + if (idl_field && idl_field['class'] && ( + idl_field.datatype == 'link' || + idl_field.datatype == 'org_unit')) { + class_obj = egIDL.classes[idl_field['class']]; + } else { + if (path_idx < (path_parts.length - 1)) { + // we ran out of classes to hop through before + // we ran out of path components + console.error("egGrid: invalid IDL path: " + path); + } + } + } + + return idl_field; + } + + /** + * Looks for the matching IDL field to extract the label + * and datattype as needed. + * Creates a local copy of the field for our internal + * machinations. + */ + this.absorbField = function(field) { + + // start by cloning the field so we can flesh it out. + // note: aungular.copy won't work, because 'field' may + // be a $scope object. + var new_field = { + name : field.name, + label : field.label, + path : field.path, + flex : Number(field.flex) || 2, + display : (field.display === false) ? false : true + }; + + // lookup the matching IDL field + var idl_field = field.datatype ? field : + self.getIDLFieldFromPath($scope.idlClass, field.path); + + // No matching IDL field. Caller has gone commando. + // Nothing left to do. + if (!idl_field) return new_field; + + new_field.datatype = idl_field.datatype; + + if (field.label) { + // caller-provided label + new_field.label = field.label; + } else { + if (idl_field.label) { + new_field.label = idl_field.label; + } else { + new_field.label = new_field.name; + } + } + + return new_field; + } + + $scope.handleRowClick = function($event, item) { + var index = $scope.list.indexValue(item); + + if ($event.ctrlKey || $event.metaKey /* mac command */) { + // control-click + if ($scope.list.toggleOneSelection(index)) + self.lastSelectedRowIndex = index; + + } else if ($event.shiftKey) { + // shift-click + if (!self.lastSelectedRowIndex || + index == self.lastSelectedRowIndex) { + // no source row, just do a simple select + $scope.list.selectOne(index); + self.lastSelectedRowIndex = index; + return; + } + + var selecting = false; + var ascending = + $scope.list.comesBefore(self.lastSelectedRowIndex, item); + var startPos = + $scope.list.indexOf(self.lastSelectedRowIndex); + + // update to new last-selected + self.lastSelectedRowIndex = index; + + // select each row between the last selected and + // currently selected items + while (true) { + startPos += ascending ? 1 : -1; + var curItem = $scope.list.items[startPos]; + if (!curItem) break; + var curIdx = $scope.list.indexValue(curItem); + $scope.list.selected[curIdx] = true; + if (curIdx == index) break; // all done + } + + } else { + $scope.list.selectOne(index); + self.lastSelectedRowIndex = index; + } + } + + $scope.itemIsSelected = function(item) { + return $scope.list.selected[ + $scope.list.indexValue(item) + ]; + } + + this.onColumnDrag = function(col) { + // track which column we're dragging + self.dragColumn = col; + } + + // if the target column does not match the source column, + // increase the size of the source column. + this.onColumnDragOver = function(target) { + if (angular.isUndefined(target)) return; + if (target == self.dragColumn) return; + if (self.dragColumn == '+index') { + $scope.indexFlex += 1; + } else if (self.dragColumn == '+selector') { + $scope.selectorFlex += 1; + } else { + var column = $scope.list.findColumn(self.dragColumn); + $scope.modifyColumnFlex(column, 1); + } + $scope.$apply(); // needed + } + + /* + $scope.fetchMoreData = function() { + console.log('fetchMoreData'); + self.offset += self.limit; + $scope.fetchData(); + } + */ + + this.init(); + } + }; +}) + +/** + * eg-grid-field : used for collecting custom field data from the templates. + * This directive does not direct display, it just passes data up to the + * parent grid. + */ +.directive('egGridField', function() { + return { + require : '^egGrid', + restrict : 'AE', + transclude : true, + scope : { + name : '@', // required; unique name + path : '@', // optional; flesh path + label : '@', // optional; display label + flex : '@', // optoinal; default flex width + }, + template : '
', // NOOP template + link : function(scope, element, attrs, egGridCtrl) { + egGridCtrl.addColumn(scope); + } + }; +}) + +/** + * Factory service for egGridDataManager instances, which are + * responsible for collecting flattened grid data. + */ +.factory('egGridData', ['egNet','egAuth', + + function(egNet, egAuth) { + + // per-grid data manager class + function EgGridDataManager(args) { + var self = this; + + this.idlClass = args.idlClass; + this.query = args.query; + this.list = args.list; + this.offset = args.offset || 0; + this.version = 1; + + this.reset = function() { + self.list.resetPageData(); + self.version++; + } + + this.revision = function() { + return self.version; + } + + this.get = function(index, count, success) { + index -= 1; // make it zero-based + + if (self.offset) { index += self.offset } + + if (index < 0) return success([]); + + console.log('index = ' + index + ' : count = ' + count); + + // we may already have the data + var slice = self.list.items.slice(index, index + count); + if (slice.length && slice[0]) return success(slice); + + var queryFields = {} + angular.forEach(self.list.allColumns, function(field) { + if (self.list.displayColumns[field.name]) + queryFields[field.name] = field.path || field.name; + }); + + var respIndex = index; + egNet.request( + 'open-ils.fielder', + 'open-ils.fielder.flattened_search', + egAuth.token(), self.idlClass, queryFields, + self.query, + { sort : self.sort, + limit : count, + offset : index + } + ).then( + function() { // oncomplete + success(self.list.items.slice(index, index + count)); + }, + null, // onerror + function(item) { // onmessage + self.list.items[respIndex++] = item; + } + ); + } + + this.applySort = function(sortBlob) { + self.offset = 0; + + if (sortBlob) { + self.sort = sortBlob; + + } else { + // no sort provided, compile the sort blob + // from the column sort priority values + + var sortList = self.list.allColumns.filter( + function(col) { return Number(col.sortPriority) != 0 } + ).sort( + function(a, b) { + if (Math.abs(a.sortPriority) < Math.abs(b.sortPriority)) + return -1; + return 1; + } + ); + + self.sort = sortList.map(function(col) { + var blob = {}; + blob[col.name] = col.sortPriority < 0 ? 'desc' : 'asc'; + return blob; + }); + } + + self.reset(); + } + } + + return { + create : function(args) { + return new EgGridDataManager(args); + } + }; + } +]) + +/** Simplified dnd directives for grid column controls. + * Extract these out if the can be made generic enough + */ + +.directive('egDragSource', function() { + return { + restrict : 'A', + require : '^egGrid', + link : function(scope, element, attrs, egGridCtrl) { + angular.element(element).attr('draggable', 'true'); + element.bind('dragstart', function(e) { + var col = angular.element(e.target).attr('column'); + egGridCtrl.onColumnDrag(col); + }); + } + }; +}) + +.directive('egDragDest', function() { + return { + restrict : 'A', + require : '^egGrid', + link : function(scope, element, attrs, egGridCtrl) { + element.bind('dragover', function(e) { + e.stopPropagation(); + e.preventDefault(); + //e.dataTransfer.dropEffect = 'copy'; + var col = angular.element(e.target).attr('column'); + console.log('dragover ' + col); + egGridCtrl.onColumnDragOver(col); + }); + } + }; +}) + +/** + * Translates bare IDL object values into display values. + * 1. Passes dates through the angular date filter + * 2. Translates bools to Booleans so the browser can display translated + * value. (Though we could manually translate instead..) + * Others likely to follow... + */ +.filter('egGridvalueFilter', ['$filter', function($filter) { + return function(value, item) { + switch(item.datatype) { + case 'bool': + // Browser will translate true/false for us + return Boolean(value == 't'); + case 'timestamp': + // canned angular date filter FTW + return $filter('date')(value); + default: + return value; + } + } +}]); + +