LP#1832897: add miscellaneous carousels functionality to staff interface
[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?|mailto|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             backdrop: 'static',
313             controller: 
314                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
315                 $scope.focusMe = true;
316                 $scope.ok = function(args) { $uibModalInstance.close(args) }
317                 $scope.cancel = function () { $uibModalInstance.dismiss() }
318             }]
319         }).result.then(function (args) {
320             if (!args || !args.name) return;
321             bucketSvc.createBucket(args.name, args.desc).then(
322                 function(id) {
323                     if (!id) return;
324                     bucketSvc.viewList = [];
325                     bucketSvc.allBuckets = []; // reset
326                     bucketSvc.currentBucket = null;
327                     $location.path(
328                         '/cat/bucket/record/' + $scope.tab + '/' + id);
329                 }
330             );
331         });
332     }
333
334     $scope.openEditBucketDialog = function() {
335         $uibModal.open({
336             templateUrl: './cat/bucket/share/t_bucket_edit',
337             backdrop: 'static',
338             controller: 
339                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
340                 $scope.focusMe = true;
341                 $scope.args = {
342                     name : bucketSvc.currentBucket.name(),
343                     desc : bucketSvc.currentBucket.description(),
344                     pub : bucketSvc.currentBucket.pub() == 't'
345                 };
346                 $scope.ok = function(args) { 
347                     if (!args) return;
348                     $scope.actionPending = true;
349                     args.pub = args.pub ? 't' : 'f';
350                     // close the dialog after edit has completed
351                     bucketSvc.editBucket(args).then(
352                         function() { $uibModalInstance.close() });
353                 }
354                 $scope.cancel = function () { $uibModalInstance.dismiss() }
355             }]
356         })
357     }
358
359
360     // opens the delete confirmation and deletes the current
361     // bucket if the user confirms.
362     $scope.openDeleteBucketDialog = function() {
363         $uibModal.open({
364             templateUrl: './cat/bucket/share/t_bucket_delete',
365             backdrop: 'static',
366             controller : 
367                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
368                 $scope.bucket = function() { return bucketSvc.currentBucket }
369                 $scope.ok = function() { $uibModalInstance.close() }
370                 $scope.cancel = function() { $uibModalInstance.dismiss() }
371             }]
372         }).result.then(function () {
373             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
374             .then(function() {
375                 bucketSvc.allBuckets = [];
376                 $location.path('/cat/bucket/record/view');
377             });
378         });
379     }
380
381     // retrieves the requested bucket by ID
382     $scope.openSharedBucketDialog = function() {
383         $uibModal.open({
384             templateUrl: './cat/bucket/share/t_load_shared',
385             backdrop: 'static',
386             controller : 
387                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
388                 $scope.focusMe = true;
389                 $scope.ok = function(args) { 
390                     if (args && args.id) {
391                         $uibModalInstance.close(args.id) 
392                     }
393                 }
394                 $scope.cancel = function() { $uibModalInstance.dismiss() }
395             }]
396         }).result.then(function(id) {
397             // RecordBucketCtrl $scope is not inherited by the
398             // modal, so we need to call loadBucket from the 
399             // promise resolver.
400             $scope.loadBucket(id);
401         });
402     }
403
404     // allows user to create a carousel from the selected bucket
405     $scope.openCreateCarouselDialog = function() {
406         if (!bucketSvc.currentBucket || !bucketSvc.currentBucket.id()) {
407             return;
408         }
409         $uibModal.open({
410             templateUrl: './cat/bucket/record/t_create_carousel',
411             backdrop: 'static',
412             controller :
413                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
414                 $scope.focusMe = true;
415                 $scope.ok = function(args) {
416                     if (args && args.name) {
417                         return egCore.net.request(
418                             'open-ils.actor',
419                             'open-ils.actor.carousel.create.from_bucket',
420                             egCore.auth.token(), args.name, bucketSvc.currentBucket.id()
421                         ).then(function(carouselId) { $uibModalInstance.close(carouselId) });
422                     }
423                 }
424                 $scope.cancel = function() { $uibModalInstance.dismiss() }
425             }]
426         }).result.then(function(carouselId) {
427             // bouncing outside of AngularJS
428             $window.location.href = '/eg2/en-US/staff/admin/server/container/carousel';
429         });
430     }
431
432     // opens the record export dialog
433     $scope.openExportBucketDialog = function() {
434         $uibModal.open({
435             templateUrl: './cat/bucket/record/t_bucket_export',
436             backdrop: 'static',
437             controller : 
438                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
439                 $scope.args = {format : 'XML', encoding : 'UTF-8'}; // defaults
440                 $scope.ok = function(args) { $uibModalInstance.close(args) }
441                 $scope.cancel = function() { $uibModalInstance.dismiss() }
442             }]
443         }).result.then(function (args) {
444             if (!args) return;
445             args.containerid = bucketSvc.currentBucket.id();
446
447             var url = '/exporter?containerid=' + args.containerid + 
448                 '&format=' + args.format + '&encoding=' + args.encoding;
449
450             if (args.holdings) url += '&holdings=1';
451
452             // TODO: improve auth cookie handling so this isn't necessary.
453             // today the cookie path is too specific (/eg/staff) for non-staff
454             // UIs to access it.  See services/auth.js
455             url += '&ses=' + egCore.auth.token(); 
456
457             $timeout(function() { $window.open(url) });
458         });
459     }
460 }])
461
462 .controller('SearchCtrl',
463        ['$scope','$routeParams','egCore','bucketSvc',
464 function($scope,  $routeParams,  egCore , bucketSvc) {
465
466     $scope.setTab('search');
467     $scope.focusMe = true;
468     var idQueryHash = {};
469
470     function generateQuery() {
471         if (bucketSvc.queryRecords.length)
472             return {id : bucketSvc.queryRecords};
473         else 
474             return null;
475     }
476
477     $scope.gridControls = {
478         setQuery : function() {return generateQuery()},
479         setSort : function() {return ['id']}
480     }
481
482     // add selected items directly to the pending list
483     $scope.addToPending = function(recs) {
484         angular.forEach(recs, function(rec) {
485             if (bucketSvc.pendingList.filter( // remove dupes
486                 function(r) {return r.id == rec.id}).length) return;
487             bucketSvc.pendingList.push(rec);
488         });
489     }
490
491     $scope.search = function() {
492         $scope.searchList = [];
493         $scope.searchInProgress = true;
494         bucketSvc.queryRecords = [];
495
496         egCore.net.request(
497             'open-ils.search',
498             'open-ils.search.biblio.multiclass.query.staff', {   
499                 limit : 500 // meh
500             }, bucketSvc.queryString, true
501         ).then(function(resp) {
502             bucketSvc.queryRecords = 
503                 resp.ids.map(function(id){return id[0]});
504             $scope.gridControls.setQuery(generateQuery());
505         })['finally'](function() {
506             $scope.searchInProgress = false;
507         });
508     }
509
510     if ($routeParams.id && 
511         (!bucketSvc.currentBucket || 
512             bucketSvc.currentBucket.id() != $routeParams.id)) {
513         // user has accessed this page cold with a bucket ID.
514         // fetch the bucket for display, then set the totalCount
515         // (also for display), but avoid fully fetching the bucket,
516         // since it's premature, in this UI.
517         bucketSvc.fetchBucket($routeParams.id);
518     }
519 }])
520
521 .controller('PendingCtrl',
522        ['$scope','$routeParams','bucketSvc','egGridDataProvider',
523 function($scope,  $routeParams,  bucketSvc , egGridDataProvider) {
524     $scope.setTab('pending');
525
526     var provider = egGridDataProvider.instance({});
527     provider.get = function(offset, count) {
528         return provider.arrayNotifier(
529             bucketSvc.pendingList, offset, count);
530     }
531     $scope.gridDataProvider = provider;
532
533     $scope.resetPendingList = function() {
534         bucketSvc.pendingList = [];
535         $scope.gridDataProvider.refresh();
536     }
537     
538
539     if ($routeParams.id && 
540         (!bucketSvc.currentBucket || 
541             bucketSvc.currentBucket.id() != $routeParams.id)) {
542         // user has accessed this page cold with a bucket ID.
543         // fetch the bucket for display, then set the totalCount
544         // (also for display), but avoid fully fetching the bucket,
545         // since it's premature, in this UI.
546         bucketSvc.fetchBucket($routeParams.id);
547     }
548 }])
549
550 .controller('ViewCtrl',
551        ['$scope','$q','$routeParams','bucketSvc','egCore','$window',
552         '$timeout','egConfirmDialog','$uibModal','egHolds',
553 function($scope,  $q , $routeParams,  bucketSvc,  egCore,  $window,
554          $timeout,  egConfirmDialog,  $uibModal,  egHolds) {
555
556     $scope.setTab('view');
557     $scope.bucketId = $routeParams.id;
558
559     var query;
560     $scope.gridControls = {
561         setQuery : function(q) {
562             if (q) query = q;
563             return query;
564         }
565     };
566
567     function drawBucket() {
568         return bucketSvc.fetchBucket($scope.bucketId).then(
569             function(bucket) {
570                 var ids = bucket.items().map(
571                     function(i){return i.target_biblio_record_entry()}
572                 );
573                 if (ids.length) {
574                     $scope.gridControls.setQuery({id : ids});
575                 } else {
576                     $scope.gridControls.setQuery({});
577                 }
578             }
579         );
580     }
581
582     // runs the transfer title holds action
583     $scope.transfer_holds_to_marked = function(records) {
584         var bib_ids = records.map(function(val) { return val.id; })
585         egHolds.transfer_all_bib_holds_to_marked_title(bib_ids);
586     }
587
588     // Refresh and update a single bib record.
589     // Returns a promise.
590     function updateOneRecord(recId, marcXml) {
591
592         return egCore.net.request(
593             'open-ils.cat',
594             'open-ils.cat.biblio.record.xml.update',
595             egCore.auth.token(), recId, marcXml
596         ).then(function(result) {
597             var evt = egCore.evt.parse(result);
598             if (evt) {
599                 alert(evt);
600                 return $q.reject(evt);
601             } else {
602                 return result; // bib record
603             }
604         });
605     }
606
607     // opens the record merge dialog
608     $scope.openRecordMergeDialog = function(records) {
609         $uibModal.open({
610             templateUrl: './cat/bucket/record/t_merge_records',
611             backdrop: 'static',
612             size: 'lg',
613             windowClass: 'eg-wide-modal',
614             controller:
615                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
616                 $scope.records = [];
617                 $scope.lead_id = 0;
618                 $scope.merge_profile = null;
619                 $scope.lead = { marc_xml : null };
620                 $scope.editing_inplace = false;
621                 $scope.showHoldings = false;
622                 angular.forEach(records, function(rec) {
623                     $scope.records.push({ id : rec.id });
624                 });
625                 $scope.ok = function() {
626                     $uibModalInstance.close({
627                         lead_id : $scope.lead_id,
628                         records : $scope.records,
629                         merge_profile : $scope.merge_profile,
630                         lead : $scope.lead
631                     });
632                 }
633                 $scope.cancel = function () { $uibModalInstance.dismiss() }
634
635                 $scope.merge_marc = function() {
636                     // need lead, at least one sub, and a merge profile
637                     if (!$scope.lead_id) return;
638                     if (!$scope.merge_profile) return;
639
640                     if (!$scope.records.length) {
641                         // if we got here, the last subordinate record
642                         // was likely removed, so let's refresh the
643                         // lead for the sake of a consistent display
644                         egCore.pcrud.retrieve('bre', $scope.lead_id)
645                         .then(function(rec) {
646                             $scope.lead.marc_xml = rec.marc();
647                         });
648                         return;
649                     }
650
651                     var recs = $scope.records.map(function(val) { return val.id; });
652                     recs.unshift($scope.lead_id);
653                     egCore.net.request(
654                         'open-ils.cat',
655                         'open-ils.cat.merge.biblio.per_profile',
656                         egCore.auth.token(),
657                         $scope.merge_profile,
658                         recs
659                     ).then(function(merged) {
660                         if (merged) $scope.lead.marc_xml = merged;
661                     });
662                 }
663                 $scope.$watch('merge_profile', function(newVal, oldVal) {
664                     if (newVal && newVal !== oldVal) {
665                         $scope.merge_marc();
666                     }
667                 });
668
669                 $scope.use_as_lead = function(rec) {
670                     if ($scope.lead_id) {
671                         $scope.records.push({ id : $scope.lead_id });
672                     }
673                     $scope.lead_id = rec.id;
674                     $scope.drop(rec);
675
676                     egCore.pcrud.retrieve('bre', $scope.lead_id)
677                     .then(function(rec) {
678                         $scope.lead.marc_xml = rec.marc();
679                         $scope.merge_marc();
680                     });
681                 }
682                 $scope.drop = function(rec) {
683                     angular.forEach($scope.records, function(val, i) {
684                         if (rec == $scope.records[i]) {
685                             $scope.records.splice(i, 1);
686                         }
687                     });
688                     $scope.merge_marc();
689                 }
690                 $scope.post_edit_inplace = function() {
691                     $scope.editing_inplace = false;
692                     updateOneRecord($scope.lead_id, $scope.lead.marc_xml);
693                 }
694
695                 $scope.cancel_edit_lead_inplace = function() {
696                     $scope.editing_inplace = false;
697                     $scope.lead.marc_xml = $scope.lead.orig_marc_xml;
698                 }
699
700                 $scope.edit_lead_inplace = function() {
701                     $scope.editing_inplace = true;
702                     let lead = { orig_marc_xml : $scope.lead.marc_xml };
703                     $scope.lead = lead;
704                 }
705                 $scope.edit_lead = function() {
706                     var lead = { marc_xml : $scope.lead.marc_xml };
707                     var parentScope = $scope;
708
709                     $uibModal.open({
710                         templateUrl: './cat/bucket/record/t_edit_lead_record',
711                         backdrop: 'static',
712                         size: 'lg',
713                         controller:
714                             ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
715                             $scope.focusMe = true;
716                             $scope.lead = lead;
717                             $scope.dirty_flag = false;
718                             $scope.ok = function() { $uibModalInstance.close() }
719                             $scope.cancel = function () { $uibModalInstance.dismiss() }
720                             $scope.on_save = function() {
721                                 parentScope.lead.marc_xml = $scope.lead.marc_xml;
722                                 parentScope.post_edit_inplace();
723                             }
724                         }]
725                     }).result.then(function() {
726                         $scope.lead.marc_xml = lead.marc_xml;
727                     });
728                 };
729             }]
730         }).result.then(function (args) {
731             if (!args.lead_id) return;
732             if (!args.records.length) return;
733
734             function update_bib() {
735                 if (args.merge_profile) {
736                     return updateOneRecord(args.lead_id, args.lead.marc_xml);
737                 } else {
738                     return $q.when();
739                 }
740             }
741
742             update_bib().then(function() {
743                 egCore.net.request(
744                     'open-ils.cat',
745                     'open-ils.cat.biblio.records.merge',
746                     egCore.auth.token(),
747                     args.lead_id,
748                     args.records.map(function(val) { return val.id; })
749                 ).then(function() {
750                     $window.open(egCore.env.basePath + 'cat/catalog/record/' + args.lead_id);
751                 });
752             });
753         });
754     }
755
756     $scope.showRecords = function(records) {
757         // TODO: probably want to set a limit on the number of
758         //       new tabs one could choose to open at once
759         angular.forEach(records, function(rec) {
760             var url = egCore.env.basePath +
761                       'cat/catalog/record/' +
762                       rec.id;
763             $timeout(function() { $window.open(url, '_blank') });
764         });
765     }
766
767     $scope.batchEdit = function() {
768         var url = egCore.env.basePath +
769                   'cat/catalog/batchEdit/bucket/' + $scope.bucketId;
770         $timeout(function() { $window.open(url, '_blank') });
771     }
772
773     $scope.detachRecords = function(records) {
774         var promises = [];
775         angular.forEach(records, function(rec) {
776             var item = bucketSvc.currentBucket.items().filter(
777                 function(i) {
778                     return (i.target_biblio_record_entry() == rec.id)
779                 }
780             );
781             if (item.length)
782                 promises.push(bucketSvc.detachRecord(item[0].id()));
783         });
784
785         bucketSvc.bucketNeedsRefresh = true;
786         return $q.all(promises).then(drawBucket);
787     }
788
789     $scope.moveToPending = function(records) {
790         angular.forEach(records, function(rec) {
791             if (bucketSvc.pendingList.filter( // remove dupes
792                 function(r) {return r.id == rec.id}).length) return;
793             bucketSvc.pendingList.push(rec);
794         });
795         $scope.detachRecords(records);
796     }
797
798     $scope.deleteRecordsFromCatalog = function(records) {
799         egConfirmDialog.open(
800             egCore.strings.CONFIRM_DELETE_RECORD_BUCKET_ITEMS_FROM_CATALOG,
801             '',
802             {}
803         ).result.then(function() {
804             var promises = [];
805             angular.forEach(records, function(rec) {
806                 promises.push(bucketSvc.deleteRecordFromCatalog(rec.id));
807             });
808             bucketSvc.bucketNeedsRefresh = true;
809             return $q.all(promises).then(function(results) {
810                 var failures = results.filter(function(result) {
811                     return egCore.evt.parse(result);
812                 }).map(function(result) {
813                     var evt = egCore.evt.parse(result);
814                     if (evt) {
815                         return { recordId: evt.payload, desc: evt.desc };
816                     }
817                 });
818                 if (failures.length) {
819                     $uibModal.open({
820                         templateUrl: './cat/bucket/record/t_records_not_deleted',
821                         backdrop: 'static',
822                         controller :
823                             ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
824                             $scope.failures = failures;
825                             $scope.ok = function() { $uibModalInstance.close() }
826                             $scope.cancel = function() { $uibModalInstance.dismiss() }
827                             }]
828                     });
829                 }
830                 drawBucket();
831             });
832         });
833     }
834
835     $scope.need_multiple_selected = function() {
836         var items = $scope.gridControls.selectedItems();
837         if (items.length > 1) return false;
838         return true;
839     }
840
841     // fetch the bucket;  on error show the not-allowed message
842     if ($scope.bucketId) 
843         drawBucket()['catch'](function() { $scope.forbidden = true });
844 }])