2 * Catalog Record Buckets
6 * add-all actions only add visible/fetched items.
7 * remove all from bucket UI leaves busted pagination
8 * -- apply a refresh after item removal?
9 * problems with bucket view fetching by record ID instead of bucket item:
10 * -- dupe bibs always sort to the bottom
11 * -- dupe bibs result in more records displayed per page than requested
12 * -- item 'pos' ordering is not honored on initial load.
15 angular.module('egCatRecordBuckets',
16 ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
18 .config(function($routeProvider, $locationProvider, $compileProvider) {
19 $locationProvider.html5Mode(true);
20 $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
22 var resolver = {delay : function(egStartup) {return egStartup.go()}};
24 $routeProvider.when('/cat/bucket/record/search/:id', {
25 templateUrl: './cat/bucket/record/t_search',
26 controller: 'SearchCtrl',
30 $routeProvider.when('/cat/bucket/record/search', {
31 templateUrl: './cat/bucket/record/t_search',
32 controller: 'SearchCtrl',
36 $routeProvider.when('/cat/bucket/record/pending/:id', {
37 templateUrl: './cat/bucket/record/t_pending',
38 controller: 'PendingCtrl',
42 $routeProvider.when('/cat/bucket/record/pending', {
43 templateUrl: './cat/bucket/record/t_pending',
44 controller: 'PendingCtrl',
48 $routeProvider.when('/cat/bucket/record/view/:id', {
49 templateUrl: './cat/bucket/record/t_view',
50 controller: 'ViewCtrl',
54 $routeProvider.when('/cat/bucket/record/view', {
55 templateUrl: './cat/bucket/record/t_view',
56 controller: 'ViewCtrl',
60 // default page / bucket view
61 $routeProvider.otherwise({redirectTo : '/cat/bucket/record/view'});
65 * bucketSvc allows us to communicate between the search,
66 * pending, and view controllers. It also allows us to cache
67 * data for each so that data reloads are not needed on every
68 * tab click (i.e. route persistence).
70 .factory('bucketSvc', ['$q','egCore', function($q, egCore) {
73 allBuckets : [], // un-fleshed user buckets
74 queryString : '', // last run query
75 queryRecords : [], // last run query results
76 currentBucket : null, // currently viewed bucket
78 // per-page list collections
83 // fetches all staff/biblio buckets for the authenticated user
84 // this function may only be called after startup.
85 fetchUserBuckets : function(force) {
86 if (this.allBuckets.length && !force) return;
88 return egCore.net.request(
90 'open-ils.actor.container.retrieve_by_class.authoritative',
91 egCore.auth.token(), egCore.auth.user().id(),
92 'biblio', 'staff_client'
93 ).then(function(buckets) { self.allBuckets = buckets });
96 createBucket : function(name, desc) {
97 var deferred = $q.defer();
98 var bucket = new egCore.idl.cbreb();
99 bucket.owner(egCore.auth.user().id());
101 bucket.description(desc || '');
102 bucket.btype('staff_client');
106 'open-ils.actor.container.create',
107 egCore.auth.token(), 'biblio', bucket
108 ).then(function(resp) {
110 if (typeof resp == 'object') {
111 console.error('bucket create error: ' + js2JSON(resp));
114 deferred.resolve(resp);
119 return deferred.promise;
122 // edit the current bucket. since we edit the
123 // local object, there's no need to re-fetch.
124 editBucket : function(args) {
125 var bucket = service.currentBucket;
126 bucket.name(args.name);
127 bucket.description(args.desc);
128 bucket.pub(args.pub);
129 return egCore.net.request(
131 'open-ils.actor.container.update',
132 egCore.auth.token(), 'biblio', bucket
137 // returns 1 if full refresh is needed
138 // returns 2 if list refresh only is needed
139 service.bucketRefreshLevel = function(id) {
140 if (!service.currentBucket) return 1;
141 if (service.bucketNeedsRefresh) {
142 service.bucketNeedsRefresh = false;
143 service.currentBucket = null;
146 if (service.currentBucket.id() != id) return 1;
150 // returns a promise, resolved with bucket, rejected if bucket is
152 service.fetchBucket = function(id) {
153 var refresh = service.bucketRefreshLevel(id);
154 if (refresh == 2) return $q.when(service.currentBucket);
156 var deferred = $q.defer();
160 'open-ils.actor.container.flesh.authoritative',
161 egCore.auth.token(), 'biblio', id
162 ).then(function(bucket) {
163 var evt = egCore.evt.parse(bucket);
166 deferred.reject(evt);
169 service.currentBucket = bucket;
170 deferred.resolve(bucket);
173 return deferred.promise;
176 // deletes a single container item from a bucket by container item ID.
177 // promise is rejected on failure
178 service.detachRecord = function(itemId) {
179 var deferred = $q.defer();
182 'open-ils.actor.container.item.delete',
183 egCore.auth.token(), 'biblio', itemId
184 ).then(function(resp) {
185 var evt = egCore.evt.parse(resp);
188 deferred.reject(evt);
191 console.log('detached bucket item ' + itemId);
192 deferred.resolve(resp);
195 return deferred.promise;
198 // delete bucket by ID.
199 // resolved w/ response on successful delete,
200 // rejected otherwise.
201 service.deleteBucket = function(id) {
202 var deferred = $q.defer();
205 'open-ils.actor.container.full_delete',
206 egCore.auth.token(), 'biblio', id
207 ).then(function(resp) {
208 var evt = egCore.evt.parse(resp);
211 deferred.reject(evt);
214 deferred.resolve(resp);
216 return deferred.promise;
223 * Top-level controller.
224 * Hosts functions needed by all controllers.
226 .controller('RecordBucketCtrl',
227 ['$scope','$location','$q','$timeout','$modal',
228 '$window','egCore','bucketSvc',
229 function($scope, $location, $q, $timeout, $modal,
230 $window, egCore, bucketSvc) {
232 $scope.bucketSvc = bucketSvc;
233 $scope.bucket = function() { return bucketSvc.currentBucket }
235 // tabs: search, pending, view
236 $scope.setTab = function(tab) {
239 // for bucket selector; must be called after route resolve
240 bucketSvc.fetchUserBuckets();
243 $scope.loadBucketFromMenu = function(item, bucket) {
244 if (bucket) return $scope.loadBucket(bucket.id());
247 $scope.loadBucket = function(id) {
249 '/cat/bucket/record/' +
250 $scope.tab + '/' + encodeURIComponent(id));
253 $scope.addToBucket = function(recs) {
254 if (recs.length == 0) return;
255 bucketSvc.bucketNeedsRefresh = true;
257 angular.forEach(recs,
259 var item = new egCore.idl.cbrebi();
260 item.bucket(bucketSvc.currentBucket.id());
261 item.target_biblio_record_entry(rec.id);
264 'open-ils.actor.container.item.create',
265 egCore.auth.token(), 'biblio', item
266 ).then(function(resp) {
268 // HACK: add the IDs of the added items so that the size
269 // of the view list will grow (and update any UI looking at
270 // the list size). The data stored is inconsistent, but since
271 // we are forcing a bucket refresh on the next rendering of
272 // the view pane, the list will be repaired.
273 bucketSvc.currentBucket.items().push(resp);
279 $scope.openCreateBucketDialog = function() {
281 templateUrl: './cat/bucket/record/t_bucket_create',
283 ['$scope', '$modalInstance', function($scope, $modalInstance) {
284 $scope.focusMe = true;
285 $scope.ok = function(args) { $modalInstance.close(args) }
286 $scope.cancel = function () { $modalInstance.dismiss() }
288 }).result.then(function (args) {
289 if (!args || !args.name) return;
290 bucketSvc.createBucket(args.name, args.desc).then(
293 bucketSvc.viewList = [];
294 bucketSvc.allBuckets = []; // reset
295 bucketSvc.currentBucket = null;
297 '/cat/bucket/record/' + $scope.tab + '/' + id);
303 $scope.openEditBucketDialog = function() {
305 templateUrl: './cat/bucket/record/t_bucket_edit',
307 ['$scope', '$modalInstance', function($scope, $modalInstance) {
308 $scope.focusMe = true;
310 name : bucketSvc.currentBucket.name(),
311 desc : bucketSvc.currentBucket.description(),
312 pub : bucketSvc.currentBucket.pub() == 't'
314 $scope.ok = function(args) {
316 $scope.actionPending = true;
317 args.pub = args.pub ? 't' : 'f';
318 // close the dialog after edit has completed
319 bucketSvc.editBucket(args).then(
320 function() { $modalInstance.close() });
322 $scope.cancel = function () { $modalInstance.dismiss() }
328 // opens the delete confirmation and deletes the current
329 // bucket if the user confirms.
330 $scope.openDeleteBucketDialog = function() {
332 templateUrl: './cat/bucket/record/t_bucket_delete',
334 ['$scope', '$modalInstance', function($scope, $modalInstance) {
335 $scope.bucket = function() { return bucketSvc.currentBucket }
336 $scope.ok = function() { $modalInstance.close() }
337 $scope.cancel = function() { $modalInstance.dismiss() }
339 }).result.then(function () {
340 bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
342 bucketSvc.allBuckets = [];
343 $location.path('/cat/bucket/record/view');
348 // retrieves the requested bucket by ID
349 $scope.openSharedBucketDialog = function() {
351 templateUrl: './cat/bucket/record/t_load_shared',
353 ['$scope', '$modalInstance', function($scope, $modalInstance) {
354 $scope.focusMe = true;
355 $scope.ok = function(args) {
356 if (args && args.id) {
357 $modalInstance.close(args.id)
360 $scope.cancel = function() { $modalInstance.dismiss() }
362 }).result.then(function(id) {
363 // RecordBucketCtrl $scope is not inherited by the
364 // modal, so we need to call loadBucket from the
366 $scope.loadBucket(id);
370 // opens the record export dialog
371 $scope.openExportBucketDialog = function() {
373 templateUrl: './cat/bucket/record/t_bucket_export',
375 ['$scope', '$modalInstance', function($scope, $modalInstance) {
376 $scope.args = {format : 'XML', encoding : 'UTF-8'}; // defaults
377 $scope.ok = function(args) { $modalInstance.close(args) }
378 $scope.cancel = function() { $modalInstance.dismiss() }
380 }).result.then(function (args) {
382 args.containerid = bucketSvc.currentBucket.id();
384 var url = '/exporter?containerid=' + args.containerid +
385 '&format=' + args.format + '&encoding=' + args.encoding;
387 if (args.holdings) url += '&holdings=1';
389 // TODO: improve auth cookie handling so this isn't necessary.
390 // today the cookie path is too specific (/eg/staff) for non-staff
391 // UIs to access it. See services/auth.js
392 url += '&ses=' + egCore.auth.token();
394 $timeout(function() { $window.open(url) });
399 .controller('SearchCtrl',
400 ['$scope','$routeParams','egCore','bucketSvc',
401 function($scope, $routeParams, egCore , bucketSvc) {
403 $scope.setTab('search');
404 $scope.focusMe = true;
405 var idQueryHash = {};
407 function generateQuery() {
408 if (bucketSvc.queryRecords.length)
409 return {id : bucketSvc.queryRecords};
414 $scope.gridControls = {
415 setQuery : function() {return generateQuery()},
416 setSort : function() {return ['id']}
419 // add selected items directly to the pending list
420 $scope.addToPending = function(recs) {
421 angular.forEach(recs, function(rec) {
422 if (bucketSvc.pendingList.filter( // remove dupes
423 function(r) {return r.id == rec.id}).length) return;
424 bucketSvc.pendingList.push(rec);
428 $scope.search = function() {
429 $scope.searchList = [];
430 $scope.searchInProgress = true;
431 bucketSvc.queryRecords = [];
435 'open-ils.search.biblio.multiclass.query.staff', {
437 }, bucketSvc.queryString, true
438 ).then(function(resp) {
439 bucketSvc.queryRecords =
440 resp.ids.map(function(id){return id[0]});
441 $scope.gridControls.setQuery(generateQuery());
442 })['finally'](function() {
443 $scope.searchInProgress = false;
447 if ($routeParams.id &&
448 (!bucketSvc.currentBucket ||
449 bucketSvc.currentBucket.id() != $routeParams.id)) {
450 // user has accessed this page cold with a bucket ID.
451 // fetch the bucket for display, then set the totalCount
452 // (also for display), but avoid fully fetching the bucket,
453 // since it's premature, in this UI.
454 bucketSvc.fetchBucket($routeParams.id);
458 .controller('PendingCtrl',
459 ['$scope','$routeParams','bucketSvc','egGridDataProvider',
460 function($scope, $routeParams, bucketSvc , egGridDataProvider) {
461 $scope.setTab('pending');
463 var provider = egGridDataProvider.instance({});
464 provider.get = function(offset, count) {
465 return provider.arrayNotifier(
466 bucketSvc.pendingList, offset, count);
468 $scope.gridDataProvider = provider;
470 $scope.resetPendingList = function() {
471 bucketSvc.pendingList = [];
475 if ($routeParams.id &&
476 (!bucketSvc.currentBucket ||
477 bucketSvc.currentBucket.id() != $routeParams.id)) {
478 // user has accessed this page cold with a bucket ID.
479 // fetch the bucket for display, then set the totalCount
480 // (also for display), but avoid fully fetching the bucket,
481 // since it's premature, in this UI.
482 bucketSvc.fetchBucket($routeParams.id);
486 .controller('ViewCtrl',
487 ['$scope','$q','$routeParams','bucketSvc',
488 function($scope, $q , $routeParams, bucketSvc) {
490 $scope.setTab('view');
491 $scope.bucketId = $routeParams.id;
494 $scope.gridControls = {
495 setQuery : function(q) {
501 function drawBucket() {
502 return bucketSvc.fetchBucket($scope.bucketId).then(
504 var ids = bucket.items().map(
505 function(i){return i.target_biblio_record_entry()}
508 $scope.gridControls.setQuery({id : ids});
510 $scope.gridControls.setQuery({});
516 $scope.detachRecords = function(records) {
518 angular.forEach(records, function(rec) {
519 var item = bucketSvc.currentBucket.items().filter(
521 return (i.target_biblio_record_entry() == rec.id)
525 promises.push(bucketSvc.detachRecord(item[0].id()));
528 bucketSvc.bucketNeedsRefresh = true;
529 return $q.all(promises).then(drawBucket);
532 // fetch the bucket; on error show the not-allowed message
534 drawBucket()['catch'](function() { $scope.forbidden = true });