]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
LP#1350042 Browser client templates/scripts (phase 1)
[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'])
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'
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             service.currentBucket = bucket;
170             deferred.resolve(bucket);
171         });
172
173         return deferred.promise;
174     }
175
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();
180         egCore.net.request(
181             'open-ils.actor',
182             'open-ils.actor.container.item.delete',
183             egCore.auth.token(), 'biblio', itemId
184         ).then(function(resp) { 
185             var evt = egCore.evt.parse(resp);
186             if (evt) {
187                 console.error(evt);
188                 deferred.reject(evt);
189                 return;
190             }
191             console.log('detached bucket item ' + itemId);
192             deferred.resolve(resp);
193         });
194
195         return deferred.promise;
196     }
197
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();
203         egCore.net.request(
204             'open-ils.actor',
205             'open-ils.actor.container.full_delete',
206             egCore.auth.token(), 'biblio', id
207         ).then(function(resp) {
208             var evt = egCore.evt.parse(resp);
209             if (evt) {
210                 console.error(evt);
211                 deferred.reject(evt);
212                 return;
213             }
214             deferred.resolve(resp);
215         });
216         return deferred.promise;
217     }
218
219     return service;
220 }])
221
222 /**
223  * Top-level controller.  
224  * Hosts functions needed by all controllers.
225  */
226 .controller('RecordBucketCtrl',
227        ['$scope','$location','$q','$timeout','$modal',
228         '$window','egCore','bucketSvc',
229 function($scope,  $location,  $q,  $timeout,  $modal,  
230          $window,  egCore,  bucketSvc) {
231
232     $scope.bucketSvc = bucketSvc;
233     $scope.bucket = function() { return bucketSvc.currentBucket }
234
235     // tabs: search, pending, view
236     $scope.setTab = function(tab) { 
237         $scope.tab = tab;
238
239         // for bucket selector; must be called after route resolve
240         bucketSvc.fetchUserBuckets(); 
241     };
242
243     $scope.loadBucketFromMenu = function(item, bucket) {
244         if (bucket) return $scope.loadBucket(bucket.id());
245     }
246
247     $scope.loadBucket = function(id) {
248         $location.path(
249             '/cat/bucket/record/' + 
250                 $scope.tab + '/' + encodeURIComponent(id));
251     }
252
253     $scope.addToBucket = function(recs) {
254         if (recs.length == 0) return;
255         bucketSvc.bucketNeedsRefresh = true;
256
257         angular.forEach(recs,
258             function(rec) {
259                 var item = new egCore.idl.cbrebi();
260                 item.bucket(bucketSvc.currentBucket.id());
261                 item.target_biblio_record_entry(rec.id);
262                 egCore.net.request(
263                     'open-ils.actor',
264                     'open-ils.actor.container.item.create', 
265                     egCore.auth.token(), 'biblio', item
266                 ).then(function(resp) {
267
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);
274                 });
275             }
276         );
277     }
278
279     $scope.openCreateBucketDialog = function() {
280         $modal.open({
281             templateUrl: './cat/bucket/record/t_bucket_create',
282             controller: 
283                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
284                 $scope.focusMe = true;
285                 $scope.ok = function(args) { $modalInstance.close(args) }
286                 $scope.cancel = function () { $modalInstance.dismiss() }
287             }]
288         }).result.then(function (args) {
289             if (!args || !args.name) return;
290             bucketSvc.createBucket(args.name, args.desc).then(
291                 function(id) {
292                     if (!id) return;
293                     bucketSvc.viewList = [];
294                     bucketSvc.allBuckets = []; // reset
295                     bucketSvc.currentBucket = null;
296                     $location.path(
297                         '/cat/bucket/record/' + $scope.tab + '/' + id);
298                 }
299             );
300         });
301     }
302
303     $scope.openEditBucketDialog = function() {
304         $modal.open({
305             templateUrl: './cat/bucket/record/t_bucket_edit',
306             controller: 
307                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
308                 $scope.focusMe = true;
309                 $scope.args = {
310                     name : bucketSvc.currentBucket.name(),
311                     desc : bucketSvc.currentBucket.description(),
312                     pub : bucketSvc.currentBucket.pub() == 't'
313                 };
314                 $scope.ok = function(args) { 
315                     if (!args) return;
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() });
321                 }
322                 $scope.cancel = function () { $modalInstance.dismiss() }
323             }]
324         })
325     }
326
327
328     // opens the delete confirmation and deletes the current
329     // bucket if the user confirms.
330     $scope.openDeleteBucketDialog = function() {
331         $modal.open({
332             templateUrl: './cat/bucket/record/t_bucket_delete',
333             controller : 
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() }
338             }]
339         }).result.then(function () {
340             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
341             .then(function() {
342                 bucketSvc.allBuckets = [];
343                 $location.path('/cat/bucket/record/view');
344             });
345         });
346     }
347
348     // retrieves the requested bucket by ID
349     $scope.openSharedBucketDialog = function() {
350         $modal.open({
351             templateUrl: './cat/bucket/record/t_load_shared',
352             controller : 
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) 
358                     }
359                 }
360                 $scope.cancel = function() { $modalInstance.dismiss() }
361             }]
362         }).result.then(function(id) {
363             // RecordBucketCtrl $scope is not inherited by the
364             // modal, so we need to call loadBucket from the 
365             // promise resolver.
366             $scope.loadBucket(id);
367         });
368     }
369
370     // opens the record export dialog
371     $scope.openExportBucketDialog = function() {
372         $modal.open({
373             templateUrl: './cat/bucket/record/t_bucket_export',
374             controller : 
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() }
379             }]
380         }).result.then(function (args) {
381             if (!args) return;
382             args.containerid = bucketSvc.currentBucket.id();
383
384             var url = '/exporter?containerid=' + args.containerid + 
385                 '&format=' + args.format + '&encoding=' + args.encoding;
386
387             if (args.holdings) url += '&holdings=1';
388
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(); 
393
394             $timeout(function() { $window.open(url) });
395         });
396     }
397 }])
398
399 .controller('SearchCtrl',
400        ['$scope','$routeParams','egCore','bucketSvc',
401 function($scope,  $routeParams,  egCore , bucketSvc) {
402
403     $scope.setTab('search');
404     $scope.focusMe = true;
405     var idQueryHash = {};
406
407     function generateQuery() {
408         if (bucketSvc.queryRecords.length)
409             return {id : bucketSvc.queryRecords};
410         else 
411             return null;
412     }
413
414     $scope.gridControls = {
415         setQuery : function() {return generateQuery()},
416         setSort : function() {return ['id']}
417     }
418
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);
425         });
426     }
427
428     $scope.search = function() {
429         $scope.searchList = [];
430         $scope.searchInProgress = true;
431         bucketSvc.queryRecords = [];
432
433         egCore.net.request(
434             'open-ils.search',
435             'open-ils.search.biblio.multiclass.query', {   
436                 limit : 500 // meh
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;
444         });
445     }
446
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);
455     }
456 }])
457
458 .controller('PendingCtrl',
459        ['$scope','$routeParams','bucketSvc','egGridDataProvider',
460 function($scope,  $routeParams,  bucketSvc , egGridDataProvider) {
461     $scope.setTab('pending');
462
463     var provider = egGridDataProvider.instance({});
464     provider.get = function(offset, count) {
465         return provider.arrayNotifier(
466             bucketSvc.pendingList, offset, count);
467     }
468     $scope.gridDataProvider = provider;
469
470     $scope.resetPendingList = function() {
471         bucketSvc.pendingList = [];
472     }
473     
474
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);
483     }
484 }])
485
486 .controller('ViewCtrl',
487        ['$scope','$q','$routeParams','bucketSvc',
488 function($scope,  $q , $routeParams,  bucketSvc) {
489
490     $scope.setTab('view');
491     $scope.bucketId = $routeParams.id;
492
493     var query;
494     $scope.gridControls = {
495         setQuery : function(q) {
496             if (q) query = q;
497             return query;
498         }
499     };
500
501     function drawBucket() {
502         return bucketSvc.fetchBucket($scope.bucketId).then(
503             function(bucket) {
504                 var ids = bucket.items().map(
505                     function(i){return i.target_biblio_record_entry()}
506                 );
507                 if (ids.length) {
508                     $scope.gridControls.setQuery({id : ids});
509                 } else {
510                     $scope.gridControls.setQuery({});
511                 }
512             }
513         );
514     }
515
516     $scope.detachRecords = function(records) {
517         var promises = [];
518         angular.forEach(records, function(rec) {
519             var item = bucketSvc.currentBucket.items().filter(
520                 function(i) {
521                     return (i.target_biblio_record_entry() == rec.id)
522                 }
523             );
524             if (item.length)
525                 promises.push(bucketSvc.detachRecord(item[0].id()));
526         });
527
528         bucketSvc.bucketNeedsRefresh = true;
529         return $q.all(promises).then(drawBucket);
530     }
531
532     // fetch the bucket;  on error show the not-allowed message
533     if ($scope.bucketId) 
534         drawBucket()['catch'](function() { $scope.forbidden = true });
535 }])