]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
10b26104ff9771b4cedb07388dcdbbebd66f94e0
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / bucket / record / app.js
1 /**
2  * Catalog Record Buckets
3  *
4  * Known Issues
5  *
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.
13  */
14
15 angular.module('egCatRecordBuckets', 
16     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egMarcMod', 'egHoldingsMod'])
17
18 .config(function($routeProvider, $locationProvider, $compileProvider) {
19     $locationProvider.html5Mode(true);
20     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
21
22     var resolver = {delay : function(egStartup) {return egStartup.go()}};
23
24     $routeProvider.when('/cat/bucket/record/search/:id', {
25         templateUrl: './cat/bucket/record/t_search',
26         controller: 'SearchCtrl',
27         resolve : resolver
28     });
29     
30     $routeProvider.when('/cat/bucket/record/search', {
31         templateUrl: './cat/bucket/record/t_search',
32         controller: 'SearchCtrl',
33         resolve : resolver
34     });
35
36     $routeProvider.when('/cat/bucket/record/pending/:id', {
37         templateUrl: './cat/bucket/record/t_pending',
38         controller: 'PendingCtrl',
39         resolve : resolver
40     });
41
42     $routeProvider.when('/cat/bucket/record/pending', {
43         templateUrl: './cat/bucket/record/t_pending',
44         controller: 'PendingCtrl',
45         resolve : resolver
46     });
47
48     $routeProvider.when('/cat/bucket/record/view/:id', {
49         templateUrl: './cat/bucket/record/t_view',
50         controller: 'ViewCtrl',
51         resolve : resolver
52     });
53
54     $routeProvider.when('/cat/bucket/record/view', {
55         templateUrl: './cat/bucket/record/t_view',
56         controller: 'ViewCtrl',
57         resolve : resolver
58     });
59
60     // default page / bucket view
61     $routeProvider.otherwise({redirectTo : '/cat/bucket/record/view'});
62 })
63
64 /**
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).
69  */
70 .factory('bucketSvc', ['$q','egCore', function($q,  egCore) { 
71
72     var service = {
73         allBuckets : [], // un-fleshed user buckets
74         queryString : '', // last run query
75         queryRecords : [], // last run query results
76         currentBucket : null, // currently viewed bucket
77
78         // per-page list collections
79         searchList  : [],
80         pendingList : [],
81         viewList  : [],
82
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;
87             var self = this;
88             return egCore.net.request(
89                 'open-ils.actor',
90                 'open-ils.actor.container.retrieve_by_class.authoritative',
91                 egCore.auth.token(), egCore.auth.user().id(), 
92                 'biblio', ['staff_client', 'vandelay_queue']
93             ).then(function(buckets) { self.allBuckets = buckets });
94         },
95
96         createBucket : function(name, desc) {
97             var deferred = $q.defer();
98             var bucket = new egCore.idl.cbreb();
99             bucket.owner(egCore.auth.user().id());
100             bucket.name(name);
101             bucket.description(desc || '');
102             bucket.btype('staff_client');
103
104             egCore.net.request(
105                 'open-ils.actor',
106                 'open-ils.actor.container.create',
107                 egCore.auth.token(), 'biblio', bucket
108             ).then(function(resp) {
109                 if (resp) {
110                     if (typeof resp == 'object') {
111                         console.error('bucket create error: ' + js2JSON(resp));
112                         deferred.reject();
113                     } else {
114                         deferred.resolve(resp);
115                     }
116                 }
117             });
118
119             return deferred.promise;
120         },
121
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(
130                 'open-ils.actor',
131                 'open-ils.actor.container.update',
132                 egCore.auth.token(), 'biblio', bucket
133             );
134         }
135     }
136
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;
144             return 1;
145         }
146         if (service.currentBucket.id() != id) return 1;
147         return 2;
148     }
149
150     // returns a promise, resolved with bucket, rejected if bucket is
151     // not fetch-able
152     service.fetchBucket = function(id) {
153         var refresh = service.bucketRefreshLevel(id);
154         if (refresh == 2) return $q.when(service.currentBucket);
155
156         var deferred = $q.defer();
157
158         egCore.net.request(
159             'open-ils.actor',
160             'open-ils.actor.container.flesh.authoritative',
161             egCore.auth.token(), 'biblio', id
162         ).then(function(bucket) {
163             var evt = egCore.evt.parse(bucket);
164             if (evt) {
165                 console.debug(evt);
166                 deferred.reject(evt);
167                 return;
168             }
169             egCore.pcrud.retrieve(
170                 'au', bucket.owner(),
171                 {flesh : 1, flesh_fields : {au : ["card"]}}
172             ).then(function(patron) {
173                 // On the off chance no barcode is present (it's not 
174                 // required) use the patron username as the identifier.
175                 bucket._owner_ident = patron.card() ? 
176                     patron.card().barcode() : patron.usrname();
177                 bucket._owner_name = patron.family_name();
178                 bucket._owner_ou = egCore.org.get(patron.home_ou()).shortname();
179             });
180
181             service.currentBucket = bucket;
182             deferred.resolve(bucket);
183         });
184
185         return deferred.promise;
186     }
187
188     // deletes a single container item from a bucket by container item ID.
189     // promise is rejected on failure
190     service.detachRecord = function(itemId) {
191         var deferred = $q.defer();
192         egCore.net.request(
193             'open-ils.actor',
194             'open-ils.actor.container.item.delete',
195             egCore.auth.token(), 'biblio', itemId
196         ).then(function(resp) { 
197             var evt = egCore.evt.parse(resp);
198             if (evt) {
199                 console.error(evt);
200                 deferred.reject(evt);
201                 return;
202             }
203             console.log('detached bucket item ' + itemId);
204             deferred.resolve(resp);
205         });
206
207         return deferred.promise;
208     }
209
210     service.deleteRecordFromCatalog = function(recordId) {
211         var deferred = $q.defer();
212
213         egCore.net.request(
214             'open-ils.cat',
215             'open-ils.cat.biblio.record_entry.delete',
216             egCore.auth.token(), recordId
217         ).then(function(resp) { 
218             // rather than rejecting the promise in the
219             // case of a failure, we'll let the caller
220             // look for errors -- doing this because AngularJS
221             // does not have a native $q.allSettled() yet.
222             deferred.resolve(resp);
223         });
224         
225         return deferred.promise;
226     }
227
228     // delete bucket by ID.
229     // resolved w/ response on successful delete,
230     // rejected otherwise.
231     service.deleteBucket = function(id) {
232         var deferred = $q.defer();
233         egCore.net.request(
234             'open-ils.actor',
235             'open-ils.actor.container.full_delete',
236             egCore.auth.token(), 'biblio', id
237         ).then(function(resp) {
238             var evt = egCore.evt.parse(resp);
239             if (evt) {
240                 console.error(evt);
241                 deferred.reject(evt);
242                 return;
243             }
244             deferred.resolve(resp);
245         });
246         return deferred.promise;
247     }
248
249     return service;
250 }])
251
252 /**
253  * Top-level controller.  
254  * Hosts functions needed by all controllers.
255  */
256 .controller('RecordBucketCtrl',
257        ['$scope','$location','$q','$timeout','$uibModal',
258         '$window','egCore','bucketSvc',
259 function($scope,  $location,  $q,  $timeout,  $uibModal,  
260          $window,  egCore,  bucketSvc) {
261
262     $scope.bucketSvc = bucketSvc;
263     $scope.bucket = function() { return bucketSvc.currentBucket }
264
265     // tabs: search, pending, view
266     $scope.setTab = function(tab) { 
267         $scope.tab = tab;
268
269         // for bucket selector; must be called after route resolve
270         bucketSvc.fetchUserBuckets(); 
271     };
272
273     $scope.loadBucketFromMenu = function(item, bucket) {
274         if (bucket) return $scope.loadBucket(bucket.id());
275     }
276
277     $scope.loadBucket = function(id) {
278         $location.path(
279             '/cat/bucket/record/' + 
280                 $scope.tab + '/' + encodeURIComponent(id));
281     }
282
283     $scope.addToBucket = function(recs) {
284         if (recs.length == 0) return;
285         bucketSvc.bucketNeedsRefresh = true;
286
287         angular.forEach(recs,
288             function(rec) {
289                 var item = new egCore.idl.cbrebi();
290                 item.bucket(bucketSvc.currentBucket.id());
291                 item.target_biblio_record_entry(rec.id);
292                 egCore.net.request(
293                     'open-ils.actor',
294                     'open-ils.actor.container.item.create', 
295                     egCore.auth.token(), 'biblio', item
296                 ).then(function(resp) {
297
298                     // HACK: add the IDs of the added items so that the size
299                     // of the view list will grow (and update any UI looking at
300                     // the list size).  The data stored is inconsistent, but since
301                     // we are forcing a bucket refresh on the next rendering of 
302                     // the view pane, the list will be repaired.
303                     bucketSvc.currentBucket.items().push(resp);
304                 });
305             }
306         );
307     }
308
309     $scope.openCreateBucketDialog = function() {
310         $uibModal.open({
311             templateUrl: './cat/bucket/share/t_bucket_create',
312             controller: 
313                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
314                 $scope.focusMe = true;
315                 $scope.ok = function(args) { $uibModalInstance.close(args) }
316                 $scope.cancel = function () { $uibModalInstance.dismiss() }
317             }]
318         }).result.then(function (args) {
319             if (!args || !args.name) return;
320             bucketSvc.createBucket(args.name, args.desc).then(
321                 function(id) {
322                     if (!id) return;
323                     bucketSvc.viewList = [];
324                     bucketSvc.allBuckets = []; // reset
325                     bucketSvc.currentBucket = null;
326                     $location.path(
327                         '/cat/bucket/record/' + $scope.tab + '/' + id);
328                 }
329             );
330         });
331     }
332
333     $scope.openEditBucketDialog = function() {
334         $uibModal.open({
335             templateUrl: './cat/bucket/share/t_bucket_edit',
336             controller: 
337                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
338                 $scope.focusMe = true;
339                 $scope.args = {
340                     name : bucketSvc.currentBucket.name(),
341                     desc : bucketSvc.currentBucket.description(),
342                     pub : bucketSvc.currentBucket.pub() == 't'
343                 };
344                 $scope.ok = function(args) { 
345                     if (!args) return;
346                     $scope.actionPending = true;
347                     args.pub = args.pub ? 't' : 'f';
348                     // close the dialog after edit has completed
349                     bucketSvc.editBucket(args).then(
350                         function() { $uibModalInstance.close() });
351                 }
352                 $scope.cancel = function () { $uibModalInstance.dismiss() }
353             }]
354         })
355     }
356
357
358     // opens the delete confirmation and deletes the current
359     // bucket if the user confirms.
360     $scope.openDeleteBucketDialog = function() {
361         $uibModal.open({
362             templateUrl: './cat/bucket/share/t_bucket_delete',
363             controller : 
364                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
365                 $scope.bucket = function() { return bucketSvc.currentBucket }
366                 $scope.ok = function() { $uibModalInstance.close() }
367                 $scope.cancel = function() { $uibModalInstance.dismiss() }
368             }]
369         }).result.then(function () {
370             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
371             .then(function() {
372                 bucketSvc.allBuckets = [];
373                 $location.path('/cat/bucket/record/view');
374             });
375         });
376     }
377
378     // retrieves the requested bucket by ID
379     $scope.openSharedBucketDialog = function() {
380         $uibModal.open({
381             templateUrl: './cat/bucket/share/t_load_shared',
382             controller : 
383                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
384                 $scope.focusMe = true;
385                 $scope.ok = function(args) { 
386                     if (args && args.id) {
387                         $uibModalInstance.close(args.id) 
388                     }
389                 }
390                 $scope.cancel = function() { $uibModalInstance.dismiss() }
391             }]
392         }).result.then(function(id) {
393             // RecordBucketCtrl $scope is not inherited by the
394             // modal, so we need to call loadBucket from the 
395             // promise resolver.
396             $scope.loadBucket(id);
397         });
398     }
399
400     // opens the record export dialog
401     $scope.openExportBucketDialog = function() {
402         $uibModal.open({
403             templateUrl: './cat/bucket/record/t_bucket_export',
404             controller : 
405                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
406                 $scope.args = {format : 'XML', encoding : 'UTF-8'}; // defaults
407                 $scope.ok = function(args) { $uibModalInstance.close(args) }
408                 $scope.cancel = function() { $uibModalInstance.dismiss() }
409             }]
410         }).result.then(function (args) {
411             if (!args) return;
412             args.containerid = bucketSvc.currentBucket.id();
413
414             var url = '/exporter?containerid=' + args.containerid + 
415                 '&format=' + args.format + '&encoding=' + args.encoding;
416
417             if (args.holdings) url += '&holdings=1';
418
419             // TODO: improve auth cookie handling so this isn't necessary.
420             // today the cookie path is too specific (/eg/staff) for non-staff
421             // UIs to access it.  See services/auth.js
422             url += '&ses=' + egCore.auth.token(); 
423
424             $timeout(function() { $window.open(url) });
425         });
426     }
427 }])
428
429 .controller('SearchCtrl',
430        ['$scope','$routeParams','egCore','bucketSvc',
431 function($scope,  $routeParams,  egCore , bucketSvc) {
432
433     $scope.setTab('search');
434     $scope.focusMe = true;
435     var idQueryHash = {};
436
437     function generateQuery() {
438         if (bucketSvc.queryRecords.length)
439             return {id : bucketSvc.queryRecords};
440         else 
441             return null;
442     }
443
444     $scope.gridControls = {
445         setQuery : function() {return generateQuery()},
446         setSort : function() {return ['id']}
447     }
448
449     // add selected items directly to the pending list
450     $scope.addToPending = function(recs) {
451         angular.forEach(recs, function(rec) {
452             if (bucketSvc.pendingList.filter( // remove dupes
453                 function(r) {return r.id == rec.id}).length) return;
454             bucketSvc.pendingList.push(rec);
455         });
456     }
457
458     $scope.search = function() {
459         $scope.searchList = [];
460         $scope.searchInProgress = true;
461         bucketSvc.queryRecords = [];
462
463         egCore.net.request(
464             'open-ils.search',
465             'open-ils.search.biblio.multiclass.query.staff', {   
466                 limit : 500 // meh
467             }, bucketSvc.queryString, true
468         ).then(function(resp) {
469             bucketSvc.queryRecords = 
470                 resp.ids.map(function(id){return id[0]});
471             $scope.gridControls.setQuery(generateQuery());
472         })['finally'](function() {
473             $scope.searchInProgress = false;
474         });
475     }
476
477     if ($routeParams.id && 
478         (!bucketSvc.currentBucket || 
479             bucketSvc.currentBucket.id() != $routeParams.id)) {
480         // user has accessed this page cold with a bucket ID.
481         // fetch the bucket for display, then set the totalCount
482         // (also for display), but avoid fully fetching the bucket,
483         // since it's premature, in this UI.
484         bucketSvc.fetchBucket($routeParams.id);
485     }
486 }])
487
488 .controller('PendingCtrl',
489        ['$scope','$routeParams','bucketSvc','egGridDataProvider',
490 function($scope,  $routeParams,  bucketSvc , egGridDataProvider) {
491     $scope.setTab('pending');
492
493     var provider = egGridDataProvider.instance({});
494     provider.get = function(offset, count) {
495         return provider.arrayNotifier(
496             bucketSvc.pendingList, offset, count);
497     }
498     $scope.gridDataProvider = provider;
499
500     $scope.resetPendingList = function() {
501         bucketSvc.pendingList = [];
502         $scope.gridDataProvider.refresh();
503     }
504     
505
506     if ($routeParams.id && 
507         (!bucketSvc.currentBucket || 
508             bucketSvc.currentBucket.id() != $routeParams.id)) {
509         // user has accessed this page cold with a bucket ID.
510         // fetch the bucket for display, then set the totalCount
511         // (also for display), but avoid fully fetching the bucket,
512         // since it's premature, in this UI.
513         bucketSvc.fetchBucket($routeParams.id);
514     }
515 }])
516
517 .controller('ViewCtrl',
518        ['$scope','$q','$routeParams','bucketSvc','egCore','$window',
519         '$timeout','egConfirmDialog','$uibModal','egHolds',
520 function($scope,  $q , $routeParams,  bucketSvc,  egCore,  $window,
521          $timeout,  egConfirmDialog,  $uibModal,  egHolds) {
522
523     $scope.setTab('view');
524     $scope.bucketId = $routeParams.id;
525
526     var query;
527     $scope.gridControls = {
528         setQuery : function(q) {
529             if (q) query = q;
530             return query;
531         }
532     };
533
534     function drawBucket() {
535         return bucketSvc.fetchBucket($scope.bucketId).then(
536             function(bucket) {
537                 var ids = bucket.items().map(
538                     function(i){return i.target_biblio_record_entry()}
539                 );
540                 if (ids.length) {
541                     $scope.gridControls.setQuery({id : ids});
542                 } else {
543                     $scope.gridControls.setQuery({});
544                 }
545             }
546         );
547     }
548
549     // runs the transfer title holds action
550     $scope.transfer_holds_to_marked = function(records) {
551         var bib_ids = records.map(function(val) { return val.id; })
552         egHolds.transfer_all_bib_holds_to_marked_title(bib_ids);
553     }
554
555     // opens the record merge dialog
556     $scope.openRecordMergeDialog = function(records) {
557         $uibModal.open({
558             templateUrl: './cat/bucket/record/t_merge_records',
559             size: 'lg',
560             windowClass: 'eg-wide-modal',
561             controller:
562                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
563                 $scope.records = [];
564                 $scope.lead_id = 0;
565                 $scope.merge_profile = null;
566                 $scope.lead = { marc_xml : null };
567                 $scope.editing_inplace = false;
568                 angular.forEach(records, function(rec) {
569                     $scope.records.push({ id : rec.id });
570                 });
571                 $scope.ok = function() {
572                     $uibModalInstance.close({
573                         lead_id : $scope.lead_id,
574                         records : $scope.records,
575                         merge_profile : $scope.merge_profile,
576                         lead : $scope.lead
577                     });
578                 }
579                 $scope.cancel = function () { $uibModalInstance.dismiss() }
580
581                 $scope.merge_marc = function() {
582                     // need lead, at least one sub, and a merge profile
583                     if (!$scope.lead_id) return;
584                     if (!$scope.merge_profile) return;
585
586                     if (!$scope.records.length) {
587                         // if we got here, the last subordinate record
588                         // was likely removed, so let's refresh the
589                         // lead for the sake of a consistent display
590                         egCore.pcrud.retrieve('bre', $scope.lead_id)
591                         .then(function(rec) {
592                             $scope.lead.marc_xml = rec.marc();
593                         });
594                         return;
595                     }
596
597                     var recs = $scope.records.map(function(val) { return val.id; });
598                     recs.unshift($scope.lead_id);
599                     egCore.net.request(
600                         'open-ils.cat',
601                         'open-ils.cat.merge.biblio.per_profile',
602                         egCore.auth.token(),
603                         $scope.merge_profile,
604                         recs
605                     ).then(function(merged) {
606                         if (merged) $scope.lead.marc_xml = merged;
607                     });
608                 }
609                 $scope.$watch('merge_profile', function(newVal, oldVal) {
610                     if (newVal && newVal !== oldVal) {
611                         $scope.merge_marc();
612                     }
613                 });
614
615                 $scope.use_as_lead = function(rec) {
616                     if ($scope.lead_id) {
617                         $scope.records.push({ id : $scope.lead_id });
618                     }
619                     $scope.lead_id = rec.id;
620                     $scope.drop(rec);
621
622                     egCore.pcrud.retrieve('bre', $scope.lead_id)
623                     .then(function(rec) {
624                         $scope.lead.marc_xml = rec.marc();
625                         $scope.merge_marc();
626                     });
627                 }
628                 $scope.drop = function(rec) {
629                     angular.forEach($scope.records, function(val, i) {
630                         if (rec == $scope.records[i]) {
631                             $scope.records.splice(i, 1);
632                         }
633                     });
634                     $scope.merge_marc();
635                 }
636                 $scope.post_edit_inplace = function() {
637                     $scope.editing_inplace = false;
638                 }
639                 $scope.edit_lead_inplace = function() {
640                     $scope.editing_inplace = true;
641                 }
642                 $scope.edit_lead = function() {
643                     var lead = { marc_xml : $scope.lead.marc_xml };
644
645                     // passing the on-save callback this way is a
646                     // hack - this invocation of the MARC editor doesn't
647                     // need it, but for some reason using this stomps
648                     // over the callback set by the other MARC editor
649                     // instance
650                     var callback = $scope.post_edit_inplace;
651
652                     $uibModal.open({
653                         templateUrl: './cat/bucket/record/t_edit_lead_record',
654                         size: 'lg',
655                         controller:
656                             ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
657                             $scope.focusMe = true;
658                             $scope.lead = lead;
659                             $scope.dirty_flag = false;
660                             $scope.ok = function() { $uibModalInstance.close() }
661                             $scope.cancel = function () { $uibModalInstance.dismiss() }
662                             $scope.on_save = callback;
663                         }]
664                     }).result.then(function() {
665                         $scope.lead.marc_xml = lead.marc_xml;
666                     });
667                 };
668             }]
669         }).result.then(function (args) {
670             if (!args.lead_id) return;
671             if (!args.records.length) return;
672
673             function update_bib() {
674                 if (args.merge_profile) {
675                     return egCore.pcrud.retrieve('bre', args.lead_id)
676                     .then(function(rec) {
677                         rec.marc(args.lead.marc_xml);
678                         rec.edit_date('now');
679                         rec.editor(egCore.auth.user().id());
680                         return egCore.pcrud.update(rec);
681                     });
682                 } else {
683                     return $q.when();
684                 }
685             }
686
687             update_bib().then(function() {
688                 egCore.net.request(
689                     'open-ils.cat',
690                     'open-ils.cat.biblio.records.merge',
691                     egCore.auth.token(),
692                     args.lead_id,
693                     args.records.map(function(val) { return val.id; })
694                 ).then(function() {
695                     $window.location.href =
696                         egCore.env.basePath + 'cat/catalog/record/' + args.lead_id;
697                 });
698             });
699         });
700     }
701
702     $scope.showRecords = function(records) {
703         // TODO: probably want to set a limit on the number of
704         //       new tabs one could choose to open at once
705         angular.forEach(records, function(rec) {
706             var url = egCore.env.basePath +
707                       'cat/catalog/record/' +
708                       rec.id;
709             $timeout(function() { $window.open(url, '_blank') });
710         });
711     }
712
713     $scope.batchEdit = function() {
714         var url = egCore.env.basePath +
715                   'cat/catalog/batchEdit/bucket/' + $scope.bucketId;
716         $timeout(function() { $window.open(url, '_blank') });
717     }
718
719     $scope.detachRecords = function(records) {
720         var promises = [];
721         angular.forEach(records, function(rec) {
722             var item = bucketSvc.currentBucket.items().filter(
723                 function(i) {
724                     return (i.target_biblio_record_entry() == rec.id)
725                 }
726             );
727             if (item.length)
728                 promises.push(bucketSvc.detachRecord(item[0].id()));
729         });
730
731         bucketSvc.bucketNeedsRefresh = true;
732         return $q.all(promises).then(drawBucket);
733     }
734
735     $scope.deleteRecordsFromCatalog = function(records) {
736         egConfirmDialog.open(
737             egCore.strings.CONFIRM_DELETE_RECORD_BUCKET_ITEMS_FROM_CATALOG,
738             '',
739             {}
740         ).result.then(function() {
741             var promises = [];
742             angular.forEach(records, function(rec) {
743                 promises.push(bucketSvc.deleteRecordFromCatalog(rec.id));
744             });
745             bucketSvc.bucketNeedsRefresh = true;
746             return $q.all(promises).then(function(results) {
747                 var failures = results.filter(function(result) {
748                     return egCore.evt.parse(result);
749                 }).map(function(result) {
750                     var evt = egCore.evt.parse(result);
751                     if (evt) {
752                         return { recordId: evt.payload, desc: evt.desc };
753                     }
754                 });
755                 if (failures.length) {
756                     $uibModal.open({
757                         templateUrl: './cat/bucket/record/t_records_not_deleted',
758                         controller :
759                             ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
760                             $scope.failures = failures;
761                             $scope.ok = function() { $uibModalInstance.close() }
762                             $scope.cancel = function() { $uibModalInstance.dismiss() }
763                             }]
764                     });
765                 }
766                 drawBucket();
767             });
768         });
769     }
770
771     // fetch the bucket;  on error show the not-allowed message
772     if ($scope.bucketId) 
773         drawBucket()['catch'](function() { $scope.forbidden = true });
774 }])