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', 'egMarcMod'])
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 service.deleteRecordFromCatalog = function(recordId) {
199 var deferred = $q.defer();
203 'open-ils.cat.biblio.record_entry.delete',
204 egCore.auth.token(), recordId
205 ).then(function(resp) {
206 // rather than rejecting the promise in the
207 // case of a failure, we'll let the caller
208 // look for errors -- doing this because AngularJS
209 // does not have a native $q.allSettled() yet.
210 deferred.resolve(resp);
213 return deferred.promise;
216 // delete bucket by ID.
217 // resolved w/ response on successful delete,
218 // rejected otherwise.
219 service.deleteBucket = function(id) {
220 var deferred = $q.defer();
223 'open-ils.actor.container.full_delete',
224 egCore.auth.token(), 'biblio', id
225 ).then(function(resp) {
226 var evt = egCore.evt.parse(resp);
229 deferred.reject(evt);
232 deferred.resolve(resp);
234 return deferred.promise;
241 * Top-level controller.
242 * Hosts functions needed by all controllers.
244 .controller('RecordBucketCtrl',
245 ['$scope','$location','$q','$timeout','$modal',
246 '$window','egCore','bucketSvc',
247 function($scope, $location, $q, $timeout, $modal,
248 $window, egCore, bucketSvc) {
250 $scope.bucketSvc = bucketSvc;
251 $scope.bucket = function() { return bucketSvc.currentBucket }
253 // tabs: search, pending, view
254 $scope.setTab = function(tab) {
257 // for bucket selector; must be called after route resolve
258 bucketSvc.fetchUserBuckets();
261 $scope.loadBucketFromMenu = function(item, bucket) {
262 if (bucket) return $scope.loadBucket(bucket.id());
265 $scope.loadBucket = function(id) {
267 '/cat/bucket/record/' +
268 $scope.tab + '/' + encodeURIComponent(id));
271 $scope.addToBucket = function(recs) {
272 if (recs.length == 0) return;
273 bucketSvc.bucketNeedsRefresh = true;
275 angular.forEach(recs,
277 var item = new egCore.idl.cbrebi();
278 item.bucket(bucketSvc.currentBucket.id());
279 item.target_biblio_record_entry(rec.id);
282 'open-ils.actor.container.item.create',
283 egCore.auth.token(), 'biblio', item
284 ).then(function(resp) {
286 // HACK: add the IDs of the added items so that the size
287 // of the view list will grow (and update any UI looking at
288 // the list size). The data stored is inconsistent, but since
289 // we are forcing a bucket refresh on the next rendering of
290 // the view pane, the list will be repaired.
291 bucketSvc.currentBucket.items().push(resp);
297 $scope.openCreateBucketDialog = function() {
299 templateUrl: './cat/bucket/record/t_bucket_create',
301 ['$scope', '$modalInstance', function($scope, $modalInstance) {
302 $scope.focusMe = true;
303 $scope.ok = function(args) { $modalInstance.close(args) }
304 $scope.cancel = function () { $modalInstance.dismiss() }
306 }).result.then(function (args) {
307 if (!args || !args.name) return;
308 bucketSvc.createBucket(args.name, args.desc).then(
311 bucketSvc.viewList = [];
312 bucketSvc.allBuckets = []; // reset
313 bucketSvc.currentBucket = null;
315 '/cat/bucket/record/' + $scope.tab + '/' + id);
321 $scope.openEditBucketDialog = function() {
323 templateUrl: './cat/bucket/record/t_bucket_edit',
325 ['$scope', '$modalInstance', function($scope, $modalInstance) {
326 $scope.focusMe = true;
328 name : bucketSvc.currentBucket.name(),
329 desc : bucketSvc.currentBucket.description(),
330 pub : bucketSvc.currentBucket.pub() == 't'
332 $scope.ok = function(args) {
334 $scope.actionPending = true;
335 args.pub = args.pub ? 't' : 'f';
336 // close the dialog after edit has completed
337 bucketSvc.editBucket(args).then(
338 function() { $modalInstance.close() });
340 $scope.cancel = function () { $modalInstance.dismiss() }
346 // opens the delete confirmation and deletes the current
347 // bucket if the user confirms.
348 $scope.openDeleteBucketDialog = function() {
350 templateUrl: './cat/bucket/record/t_bucket_delete',
352 ['$scope', '$modalInstance', function($scope, $modalInstance) {
353 $scope.bucket = function() { return bucketSvc.currentBucket }
354 $scope.ok = function() { $modalInstance.close() }
355 $scope.cancel = function() { $modalInstance.dismiss() }
357 }).result.then(function () {
358 bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
360 bucketSvc.allBuckets = [];
361 $location.path('/cat/bucket/record/view');
366 // retrieves the requested bucket by ID
367 $scope.openSharedBucketDialog = function() {
369 templateUrl: './cat/bucket/record/t_load_shared',
371 ['$scope', '$modalInstance', function($scope, $modalInstance) {
372 $scope.focusMe = true;
373 $scope.ok = function(args) {
374 if (args && args.id) {
375 $modalInstance.close(args.id)
378 $scope.cancel = function() { $modalInstance.dismiss() }
380 }).result.then(function(id) {
381 // RecordBucketCtrl $scope is not inherited by the
382 // modal, so we need to call loadBucket from the
384 $scope.loadBucket(id);
388 // opens the record export dialog
389 $scope.openExportBucketDialog = function() {
391 templateUrl: './cat/bucket/record/t_bucket_export',
393 ['$scope', '$modalInstance', function($scope, $modalInstance) {
394 $scope.args = {format : 'XML', encoding : 'UTF-8'}; // defaults
395 $scope.ok = function(args) { $modalInstance.close(args) }
396 $scope.cancel = function() { $modalInstance.dismiss() }
398 }).result.then(function (args) {
400 args.containerid = bucketSvc.currentBucket.id();
402 var url = '/exporter?containerid=' + args.containerid +
403 '&format=' + args.format + '&encoding=' + args.encoding;
405 if (args.holdings) url += '&holdings=1';
407 // TODO: improve auth cookie handling so this isn't necessary.
408 // today the cookie path is too specific (/eg/staff) for non-staff
409 // UIs to access it. See services/auth.js
410 url += '&ses=' + egCore.auth.token();
412 $timeout(function() { $window.open(url) });
417 .controller('SearchCtrl',
418 ['$scope','$routeParams','egCore','bucketSvc',
419 function($scope, $routeParams, egCore , bucketSvc) {
421 $scope.setTab('search');
422 $scope.focusMe = true;
423 var idQueryHash = {};
425 function generateQuery() {
426 if (bucketSvc.queryRecords.length)
427 return {id : bucketSvc.queryRecords};
432 $scope.gridControls = {
433 setQuery : function() {return generateQuery()},
434 setSort : function() {return ['id']}
437 // add selected items directly to the pending list
438 $scope.addToPending = function(recs) {
439 angular.forEach(recs, function(rec) {
440 if (bucketSvc.pendingList.filter( // remove dupes
441 function(r) {return r.id == rec.id}).length) return;
442 bucketSvc.pendingList.push(rec);
446 $scope.search = function() {
447 $scope.searchList = [];
448 $scope.searchInProgress = true;
449 bucketSvc.queryRecords = [];
453 'open-ils.search.biblio.multiclass.query.staff', {
455 }, bucketSvc.queryString, true
456 ).then(function(resp) {
457 bucketSvc.queryRecords =
458 resp.ids.map(function(id){return id[0]});
459 $scope.gridControls.setQuery(generateQuery());
460 })['finally'](function() {
461 $scope.searchInProgress = false;
465 if ($routeParams.id &&
466 (!bucketSvc.currentBucket ||
467 bucketSvc.currentBucket.id() != $routeParams.id)) {
468 // user has accessed this page cold with a bucket ID.
469 // fetch the bucket for display, then set the totalCount
470 // (also for display), but avoid fully fetching the bucket,
471 // since it's premature, in this UI.
472 bucketSvc.fetchBucket($routeParams.id);
476 .controller('PendingCtrl',
477 ['$scope','$routeParams','bucketSvc','egGridDataProvider',
478 function($scope, $routeParams, bucketSvc , egGridDataProvider) {
479 $scope.setTab('pending');
481 var provider = egGridDataProvider.instance({});
482 provider.get = function(offset, count) {
483 return provider.arrayNotifier(
484 bucketSvc.pendingList, offset, count);
486 $scope.gridDataProvider = provider;
488 $scope.resetPendingList = function() {
489 bucketSvc.pendingList = [];
490 $scope.gridDataProvider.refresh();
494 if ($routeParams.id &&
495 (!bucketSvc.currentBucket ||
496 bucketSvc.currentBucket.id() != $routeParams.id)) {
497 // user has accessed this page cold with a bucket ID.
498 // fetch the bucket for display, then set the totalCount
499 // (also for display), but avoid fully fetching the bucket,
500 // since it's premature, in this UI.
501 bucketSvc.fetchBucket($routeParams.id);
505 .controller('ViewCtrl',
506 ['$scope','$q','$routeParams','bucketSvc', 'egCore', '$window',
507 '$timeout', 'egConfirmDialog', '$modal',
508 function($scope, $q , $routeParams, bucketSvc, egCore, $window,
509 $timeout, egConfirmDialog, $modal) {
511 $scope.setTab('view');
512 $scope.bucketId = $routeParams.id;
515 $scope.gridControls = {
516 setQuery : function(q) {
522 function drawBucket() {
523 return bucketSvc.fetchBucket($scope.bucketId).then(
525 var ids = bucket.items().map(
526 function(i){return i.target_biblio_record_entry()}
529 $scope.gridControls.setQuery({id : ids});
531 $scope.gridControls.setQuery({});
537 // opens the record merge dialog
538 $scope.openRecordMergeDialog = function(records) {
540 templateUrl: './cat/bucket/record/t_merge_records',
543 ['$scope', '$modalInstance', function($scope, $modalInstance) {
546 angular.forEach(records, function(rec) {
547 $scope.records.push({ id : rec.id });
549 $scope.ok = function() {
550 $modalInstance.close({
551 lead_id : $scope.lead_id,
552 records : $scope.records
555 $scope.cancel = function () { $modalInstance.dismiss() }
556 $scope.use_as_lead = function(rec) {
557 if ($scope.lead_id) {
558 $scope.records.push({ id : $scope.lead_id });
560 $scope.lead_id = rec.id;
563 $scope.drop = function(rec) {
564 angular.forEach($scope.records, function(val, i) {
565 if (rec == $scope.records[i]) {
566 $scope.records.splice(i, 1);
570 $scope.edit_lead = function() {
571 var lead_id = $scope.lead_id;
573 templateUrl: './cat/bucket/record/t_edit_lead_record',
576 ['$scope', '$modalInstance', function($scope, $modalInstance) {
577 $scope.focusMe = true;
578 $scope.record_id = lead_id;
579 $scope.dirty_flag = false;
580 $scope.ok = function() { $modalInstance.close() }
581 $scope.cancel = function () { $modalInstance.dismiss() }
583 }).result.then(function() {
584 // TODO: need a way to force a refresh of the egRecordHtml, as
585 // the record ID does not change
589 }).result.then(function (args) {
590 if (!args.lead_id) return;
591 if (!args.records.length) return;
594 'open-ils.cat.biblio.records.merge',
597 args.records.map(function(val) { return val.id; })
604 $scope.showAllRecords = function() {
605 // TODO: maybe show selected would be better?
606 // TODO: probably want to set a limit on the number of
607 // new tabs one could choose to open at once
608 angular.forEach(bucketSvc.currentBucket.items(), function(rec) {
609 var url = egCore.env.basePath +
610 'cat/catalog/record/' +
611 rec.target_biblio_record_entry();
612 $timeout(function() { $window.open(url, '_blank') });
616 $scope.batchEdit = function() {
617 var url = egCore.env.basePath +
618 'cat/catalog/batchEdit/bucket/' + $scope.bucketId;
619 $timeout(function() { $window.open(url, '_blank') });
622 $scope.detachRecords = function(records) {
624 angular.forEach(records, function(rec) {
625 var item = bucketSvc.currentBucket.items().filter(
627 return (i.target_biblio_record_entry() == rec.id)
631 promises.push(bucketSvc.detachRecord(item[0].id()));
634 bucketSvc.bucketNeedsRefresh = true;
635 return $q.all(promises).then(drawBucket);
638 $scope.deleteRecordsFromCatalog = function(records) {
639 egConfirmDialog.open(
640 egCore.strings.CONFIRM_DELETE_RECORD_BUCKET_ITEMS_FROM_CATALOG,
643 ).result.then(function() {
645 angular.forEach(records, function(rec) {
646 promises.push(bucketSvc.deleteRecordFromCatalog(rec.id));
648 bucketSvc.bucketNeedsRefresh = true;
649 return $q.all(promises).then(function(results) {
650 var failures = results.filter(function(result) {
651 return egCore.evt.parse(result);
652 }).map(function(result) {
653 var evt = egCore.evt.parse(result);
655 return { recordId: evt.payload, desc: evt.desc };
658 if (failures.length) {
660 templateUrl: './cat/bucket/record/t_records_not_deleted',
662 ['$scope', '$modalInstance', function($scope, $modalInstance) {
663 $scope.failures = failures;
664 $scope.ok = function() { $modalInstance.close() }
665 $scope.cancel = function() { $modalInstance.dismiss() }
674 // fetch the bucket; on error show the not-allowed message
676 drawBucket()['catch'](function() { $scope.forbidden = true });