%]
[% BLOCK APP_JS %]
-<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/list.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/list.js"></script><!-- remove me -->
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/app.js"></script>
</div>
</form>
</div>
- <div class="col-md-1 text-right">
- [% INCLUDE 'staff/circ/patron/t_search_actions.tt2' %]
- </div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
+<!--
<div class="alert alert-info alert-dismissable"
ng-hide="tips.dismissed('circ.patron.search')">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true"
<li>Middle-click to open a patron in a new browser tab.</li>
</ol>
</div>
+-->
[% INCLUDE 'staff/circ/patron/t_search_results.tt2' %]
</div>
</div>
-[%
-# Default / available display columns
-# Since there will be demand for configurable columns in this UI,
-# experiment with automagic column creation.
-#
-# We could autogenerate much of this from the IDL. However, since there
-# are special cases to handle (e.g. billing vs mailing address) and
-# because table autogeneration will likely evolve over time, go ahead
-# and list the columns explicitly for now.
-#
-# the 'name' field doubles as the path to the value and as a unique
-# key for the column picker
-COLUMNS = [
+<eg-grid
+ idl-class="au" sort="[]" id-field="id"
+ main-label="[% l('Patron Search Resuts') %]"
+ items-provider="patronSearchGridProvider"
+ persist-key="eg.staff.circ.patron.search">
+ <eg-grid-field label="[% ('ID') %]" path='id'></eg-grid-field>
+ <eg-grid-field label="[% ('Card') %]" path='card.barcode'></eg-grid-field>
+ <eg-grid-field label="[% ('Last Name') %]" path='family_name'></eg-grid-field>
+ <eg-grid-field label="[% ('First Name') %]" path='first_given_name'></eg-grid-field>
+ <eg-grid-field label="[% ('Middle Name') %]" path='second_given_name'></eg-grid-field>
+ <eg-grid-field label="[% ('DoB') %]" path='dob'></eg-grid-field>
+ <eg-grid-field label="[% ('Home Library') %]" path='home_ou.shortname'></eg-grid-field>
+ <eg-grid-field label="[% ('Created On') %]" path='create_date'></eg-grid-field>
-{label => l('ID'), name => 'id', display => 1},
-{label => l('Card'), name => 'card.barcode', display => 1},
-{label => l('Last Name'), name => 'family_name', display => 1},
-{label => l('First Name'), name => 'first_given_name', display => 1},
-{label => l('Middle Name'), name => 'second_given_name',display => 1},
-{label => l('DoB'), name => 'dob', display => 1},
-{label => l('Home Library'),name => 'home_ou.shortname',display => 1},
-{label => l('Created On'), name => 'create_date', display => 1},
-
-{label => l('Mailing:Street 1'), name => 'mailing_address.street1', display => 1},
-{label => l('Mailing:Street 2'), name => 'mailing_address.street2'},
-{label => l('Mailing:City'), name => 'mailing_address.city'},
-{label => l('Mailing:County'), name => 'mailing_address.county'},
-{label => l('Mailing:State'), name => 'mailing_address.state'},
-{label => l('Mailing:Zip'), name => 'mailing_address.post_code'},
-
-{label => l('Billing:Street 1'), name => 'billing_address.street1'},
-{label => l('Billing:Street 2'), name => 'billing_address.street2'},
-{label => l('Billing:City'), name => 'billing_address.city'},
-{label => l('Billing:County'), name => 'billing_address.county'},
-{label => l('Billing:State'), name => 'billing_address.state'},
-{label => l('Billing:Zip'), name => 'billing_address.post_code'}
-
-]
-%]
-
-<!-- tell JS about our columns so they can be dynamically managed -->
-<div ng-init="
-patrons.setColumns([
-[%- FOR col IN COLUMNS %]
-{label:'[% col.label %]',name:'[% col.name %]'[% IF col.display %],display:true[% END %]}[% IF !loop.last; ','; END -%]
-[% END %]
-])">
-</div>
-
-<table class="list table table-hover table-condensed">
- <thead>
- <tr>
- <th>#</th>
- <th><a href='' ng-click="patrons.toggleSelectAll()">✓</a></th>
- <th ng-repeat="col in patrons.allColumns"
- ng-show="patrons.displayColumns[col.name]">
- {{col.label}}
- </th>
- </tr>
- </thead>
- <!-- giving tbody a tabindex allows the keyup event to fire.
- requires outline:none to prevent selected element CSS bordering -->
- <tbody _tabindex="-1" _style="outline:none" _ng-keyup="navigateResults($event)">
- <tr ng-repeat="user in patrons.items track by $index"
- ng-click="onPatronClick($event, user)"
- ng-dblclick="onPatronDblClick($event, user)"
- ng-class="{selected : patrons.selected[user.id()]}">
- <td>{{$index + 1}}</td>
- <td><span ng-if="patrons.selected[user.id()]">✓</span>
- <td ng-repeat="col in patrons.allColumns"
- ng-show="patrons.displayColumns[col.name]">
- {{patrons.fieldValue(user, col.name)}}
- </td>
- </tr>
- </tbody>
-</table>
+ <eg-grid-field label="[% ('Mailing:Street 1') %]" path='mailing_address.street1'></eg-grid-field>
+ <eg-grid-field label="[% ('Mailing:Street 2') %]" path='mailing_address.street2' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Mailing:City') %]" path='mailing_address.city' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Mailing:County') %]" path='mailing_address.county' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Mailing:State') %]" path='mailing_address.state' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Mailing:Zip') %]" path='mailing_address.post_code' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Billing:Street 1') %]" path='billing_address.street1' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Billing:Street 2') %]" path='billing_address.street2' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Billing:City') %]" path='billing_address.city' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Billing:County') %]" path='billing_address.county' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Billing:State') %]" path='billing_address.state' display="false"></eg-grid-field>
+ <eg-grid-field label="[% ('Billing:Zip') %]" path='billing_address.post_code' display="false"></eg-grid-field>
+</eg-grid>
*/
angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap',
- 'egCoreMod', 'egUiMod', 'egListMod', 'egUserMod'])
+ 'egCoreMod', 'egUiMod', 'egGridMod', 'egListMod', 'egUserMod'])
-.config(function($routeProvider, $locationProvider) {
+.config(function($routeProvider, $locationProvider, $compileProvider) {
$locationProvider.html5Mode(true);
+ $compileProvider.aHrefSanitizationWhitelist(/^\s*(blob):/); // grid export
// data loaded at startup which only requires an authtoken goes
// here. this allows the requests to be run in parallel instead of
.controller('PatronSearchCtrl',
['$scope','$q','$routeParams','$timeout','$window','$location','egEnv',
'$filter','egIDL','egNet','egAuth','egEvent','egList','egUser','patronSvc',
+ 'egGridFlatDataProvider',
function($scope, $q, $routeParams, $timeout, $window, $location, egEnv,
- $filter, egIDL, egNet, egAuth, egEvent, egList, egUser, patronSvc) {
+ $filter, egIDL, egNet, egAuth, egEvent, egList, egUser, patronSvc,
+ egGridFlatDataProvider) {
$scope.initTab('search');
$scope.focusMe = true;
$scope.patrons = patronSvc.patrons;
+
+ // our data provider is a modified flat data provider
+ var provider = egGridFlatDataProvider.instance({});
+ provider.get = function(index, count, onitem) {
+ angular.forEach(
+ $scope.patrons.items.slice(index, index + count),
+ function(item) { onitem(item) }
+ );
+ };
+ provider.itemFieldValue = function(item, column) {
+ return provider.nestedItemFieldValue(item, column);
+ };
+ $scope.patronSearchGridProvider = provider;
// typeahead doesn't filter correctly with full hash objects, so
// trim them down to just name and id. This would allow us to use
).then(null, null, function(user) {
patronSvc.localFlesh(user);
$scope.patrons.items[$scope.patrons.items.length] = user;
+ $scope.patronSearchGridProvider.increment();
});
};
// egList containting our tabular data is provided for us
// and managed externally.
- dataProvider : '=',
-
+ itemsProvider : '=',
+
// 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
},
controller : [
- '$scope','egIDL','egAuth','egNet',
- 'egGridFlatDataProvider','egGridColumnsProvider',
- '$filter','$window',
- function($scope, egIDL, egAuth, egNet,
- egGridFlatDataProvider, egGridColumnsProvider,
- $filter, $window) {
+ '$scope','egIDL','egAuth','egNet', 'egGridFlatDataProvider',
+ 'egGridColumnsProvider', '$filter','$window',
+ function($scope, egIDL, egAuth, egNet, egGridFlatDataProvider,
+ egGridColumnsProvider, $filter, $window) {
var grid = this;
grid.items = [];
grid.selected = {}; // idField-based
grid.totalCount = -1;
- grid.dataProvider = $scope.dataProvider;
+ grid.dataProvider = $scope.itemsProvider;
grid.idlClass = $scope.idlClass;
grid.mainLabel = $scope.mainLabel;
grid.indexField = $scope.idField;
grid.columnsProvider.compileAutoColumns();
}
- if (!grid.dataProvider) {
+ if (grid.dataProvider) {
+
+ // refresh the grid contents each time the data
+ // provider's revision changes
+ $scope.$watch(
+ function() { return grid.dataProvider.revision() },
+ function() { grid.collect() }
+ );
+
+ } else {
+
grid.selfManagedData = true;
grid.dataProvider = egGridFlatDataProvider.instance({
idlClass : grid.idlClass,
grid.indexValue = function(item) {
if (angular.isObject(item)) {
if (item !== null) {
- if (grid.indexFieldAsFunction)
+ if (angular.isFunction(item[grid.indexField]))
return item[grid.indexField]();
- return item[grid.indexField];
+ return item[grid.indexField]; // flat data
}
}
// passed a non-object; assume it's an index
name : '@', // required; unique name
path : '@', // optional; flesh path
label : '@', // optional; display label
- flex : '@', // optoinal; default flex width
+ flex : '@', // optional; default flex width
+ display : '=' // optional; hide column by default
},
template : '<div></div>', // NOOP template
link : function(scope, element, attrs, egGridCtrl) {
- egGridCtrl.addColumn(scope);
+ egGridCtrl.columnsProvider.add(scope);
}
};
})
// Factory service for egGridDataManager instances, which are
// responsible for collecting flattened grid data.
.factory('egGridFlatDataProvider',
- ['egNet','egAuth',
- function(egNet, egAuth) {
+ ['$filter','egNet','egAuth','egIDL',
+ function($filter , egNet , egAuth , egIDL) {
function FlatDataProvider(args) {
var gridData = this;
gridData.query = args.query;
gridData.columnsProvider = args.columnsProvider;
gridData.sort = [];
+ gridData._revision = 0;
+
+ gridData.revision = function() {
+ return gridData._revision;
+ }
+
+ gridData.increment = function() {
+ gridData._revision++;
+ }
gridData.get = function(index, count, onresponse) {
}
gridData.itemFieldValue = function(item, column) {
- // all of our data is flattened
+ // all of our data is flat
return item[column.name];
}
+
+ // utility function which may be useful for other grid data
+ // providers.
+ // given an object and a dot-separated path to a field,
+ // extract the value of the field. The path can refer
+ // to function names or object attributes. If the final
+ // value is an IDL field, run the value through its
+ // corresponding output filter.
+ gridData.nestedItemFieldValue = function(obj, column) {
+ if (obj === null || obj === undefined || obj === '') return '';
+ if (!column.path) return obj;
+
+ var idlField, cls, clsobj;
+ var parts = column.path.split('.');
+
+ angular.forEach(parts, function(step, idx) {
+ // object is not fleshed to the expected extent
+ if (!obj || typeof obj != 'object') {
+ obj = '';
+ return;
+ }
+
+ cls = obj.classname;
+ if (cls && (clsobj = egIDL.classes[cls])) {
+ idlField = clsobj.fields.filter(
+ function(f) { return f.name == step })[0];
+ obj = obj[step]();
+ } else {
+ if (angular.isFunction(obj[step])) {
+ obj = obj[step]();
+ } else {
+ obj = obj[step];
+ }
+ }
+ });
+
+ if (obj === null || obj === undefined || obj === '') return '';
+ if (!idlField) return obj;
+ return $filter('egGridValueFilter')(obj, column);
+ }
}
return {