From 36382be3da5adba63b051efdf333666c70d3aea0 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Thu, 26 Oct 2017 17:56:36 -0400 Subject: [PATCH] Angular-based staff catalog experiment WIP Signed-off-by: Bill Erickson --- Open-ILS/examples/fm_IDL.xml | 2 +- .../lib/OpenILS/Application/Search/Biblio.pm | 44 ++ .../src/templates/staff/cat/staffcat/index.tt2 | 25 + .../templates/staff/cat/staffcat/search_form.tt2 | 216 ++++++ .../staff/cat/staffcat/search_result_facets.tt2 | 37 + .../cat/staffcat/search_result_pagination.tt2 | 24 + .../staff/cat/staffcat/search_result_record.tt2 | 113 ++++ .../src/templates/staff/cat/staffcat/t_record.tt2 | 6 + .../src/templates/staff/cat/staffcat/t_search.tt2 | 37 + Open-ILS/src/templates/staff/css/style.css.tt2 | 4 + Open-ILS/src/templates/staff/navbar.tt2 | 6 + .../web/js/ui/default/staff/cat/staffcat/app.js | 751 +++++++++++++++++++++ 12 files changed, 1264 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/templates/staff/cat/staffcat/index.tt2 create mode 100644 Open-ILS/src/templates/staff/cat/staffcat/search_form.tt2 create mode 100644 Open-ILS/src/templates/staff/cat/staffcat/search_result_facets.tt2 create mode 100644 Open-ILS/src/templates/staff/cat/staffcat/search_result_pagination.tt2 create mode 100644 Open-ILS/src/templates/staff/cat/staffcat/search_result_record.tt2 create mode 100644 Open-ILS/src/templates/staff/cat/staffcat/t_record.tt2 create mode 100644 Open-ILS/src/templates/staff/cat/staffcat/t_search.tt2 create mode 100644 Open-ILS/web/js/ui/default/staff/cat/staffcat/app.js diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index edd70544c8..0738f9cdcc 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3157,7 +3157,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm index 06ec97d92d..5c9eafc950 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm @@ -2623,6 +2623,50 @@ sub copies_by_cn_label { return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ]; } +__PACKAGE__->register_method( + method => 'staffcat_bib_extras', + api_name => 'open-ils.search.biblio.catalog.extras.staff', + stream => 1, + # disable opensrf bundling/chunking for timely responses. + max_chunk_size => 0, + max_bundle_size => 0, + max_bundle_count => 0, + signaturn => { + desc => q/ +Collect various APIs needed for bib list display in the staff catalog into +a single API to streamline./ + }, +); + +sub staffcat_bib_extras { + my ($self, $client, $auth, $org_id, $bre_ids) = @_; + + # No auth checks are needed at time of writing. + # Reserving for possible future use of $auth. + my $e = new_editor(authtoken => $auth); + + for my $bre_id (@$bre_ids) { + + # Fire hold counts in parallel. Response is collected below. + my $hold_req = OpenSRF::AppSession->create('open-ils.circ') + ->request('open-ils.circ.bre.holds.count', $bre_id); + + my $copy_counts = $e->json_query({from => + ['asset.record_copy_count', $org_id, $bre_id, 1]}); + + $copy_counts = [sort { $a->{depth} <=> $b->{depth} } @$copy_counts]; + + $client->respond({ + id => $bre_id, + copy_counts => $copy_counts, + hold_count => $hold_req->gather(1) + }); + } + + return undef; +} + + 1; diff --git a/Open-ILS/src/templates/staff/cat/staffcat/index.tt2 b/Open-ILS/src/templates/staff/cat/staffcat/index.tt2 new file mode 100644 index 0000000000..a24b1db05a --- /dev/null +++ b/Open-ILS/src/templates/staff/cat/staffcat/index.tt2 @@ -0,0 +1,25 @@ +[% + WRAPPER "staff/base.tt2"; + ctx.page_title = l("Staff Catalog"); + ctx.page_app = "egStaffCatApp"; + ctx.page_ctrl = "StaffCatBaseCtrl"; +%] + +[% BLOCK APP_JS %] + + + + +[% END %] + + +[% INCLUDE 'staff/cat/staffcat/search_form.tt2' %] + + +
+ +[% END %] + diff --git a/Open-ILS/src/templates/staff/cat/staffcat/search_form.tt2 b/Open-ILS/src/templates/staff/cat/staffcat/search_form.tt2 new file mode 100644 index 0000000000..839424b9ad --- /dev/null +++ b/Open-ILS/src/templates/staff/cat/staffcat/search_form.tt2 @@ -0,0 +1,216 @@ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
+
+
+ + + + +
+
+
+
+
+
+ + +
+
+ +
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ [% l('Searching..') %] +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ diff --git a/Open-ILS/src/templates/staff/cat/staffcat/search_result_facets.tt2 b/Open-ILS/src/templates/staff/cat/staffcat/search_result_facets.tt2 new file mode 100644 index 0000000000..448f48a5ef --- /dev/null +++ b/Open-ILS/src/templates/staff/cat/staffcat/search_result_facets.tt2 @@ -0,0 +1,37 @@ + +
+
+
+
+
+
+

+ {{context.result().facet_data[facet_conf.facet_class][name].cmf_label}} +

+
+
+
+ +
{{value.count}}
+
+
+
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/cat/staffcat/search_result_pagination.tt2 b/Open-ILS/src/templates/staff/cat/staffcat/search_result_pagination.tt2 new file mode 100644 index 0000000000..c885d20f9d --- /dev/null +++ b/Open-ILS/src/templates/staff/cat/staffcat/search_result_pagination.tt2 @@ -0,0 +1,24 @@ + + + diff --git a/Open-ILS/src/templates/staff/cat/staffcat/search_result_record.tt2 b/Open-ILS/src/templates/staff/cat/staffcat/search_result_record.tt2 new file mode 100644 index 0000000000..db9acb65b0 --- /dev/null +++ b/Open-ILS/src/templates/staff/cat/staffcat/search_result_record.tt2 @@ -0,0 +1,113 @@ +
+
+
+ + + +
+
+ + +
+
+ #{{$index + 1 + context.number(context.search_args.offset)}} + + + {{context.icon_ccvm_map()[record.icon_attr.value()].value()}} + + {{record.display_fields.edition.value}} + {{record.display_fields.pubdate.value}} +
+
+
+
+
+
+ [% l( + '[_1] / [_2] items @ [_3]', + '{{copy_count.available}}', + '{{copy_count.visible}}', + '{{context.org_name(copy_count.org_unit)}}' + ) + %] +
+
+
+
+
+
+
+ [% l('Holds: [_1]', '{{record.hold_count}}') %] +
+
+
+
+ [% l('Created [_1] by [_2]', + '{{record.bre.create_date() | date:$root.egDateFormat}}', + '{{record.bre.creator().usrname()}}' + ) %] +
+
+
+
+
+
+ [% l('TCN: [_1]', '{{record.bre.tcn_value()}}') %] +
+
+
+
+ [% l('Edited [_1] by [_2]', + '{{record.bre.edit_date() | date:$root.egDateFormat}}', + '{{record.bre.editor().usrname()}}' + ) %] +
+
+
+
+
+
+ [% l('ID: [_1]', '{{record.bre.id()}}') %] +
+
+
+
+ + + + + + +
+
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/cat/staffcat/t_record.tt2 b/Open-ILS/src/templates/staff/cat/staffcat/t_record.tt2 new file mode 100644 index 0000000000..7526eb8c33 --- /dev/null +++ b/Open-ILS/src/templates/staff/cat/staffcat/t_record.tt2 @@ -0,0 +1,6 @@ +

[% l('Record Details') %]

+ + +
+

Tabs and actions and stuff

diff --git a/Open-ILS/src/templates/staff/cat/staffcat/t_search.tt2 b/Open-ILS/src/templates/staff/cat/staffcat/t_search.tt2 new file mode 100644 index 0000000000..b79fa72fea --- /dev/null +++ b/Open-ILS/src/templates/staff/cat/staffcat/t_search.tt2 @@ -0,0 +1,37 @@ +
+ +
+ +
+
+
+

+ [% l('Search Results [_1]', + '{{context.result_count()}}') %] +

+
+
+
+
+
+ [% INCLUDE 'staff/cat/staffcat/search_result_pagination.tt2' %] +
+
+
+
+ +
+ [% INCLUDE 'staff/cat/staffcat/search_result_facets.tt2' %] +
+
+
+ [% INCLUDE 'staff/cat/staffcat/search_result_record.tt2' %] +
+
+
+
diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2 index 74cf439656..cbc22de5d4 100644 --- a/Open-ILS/src/templates/staff/css/style.css.tt2 +++ b/Open-ILS/src/templates/staff/css/style.css.tt2 @@ -108,6 +108,7 @@ table.list tr.selected td { /* deprecated? */ .pad-horiz {padding : 0px 10px 0px 10px; } .pad-vert {padding : 20px 0px 10px 0px;} +.pad-vert-min {padding : 5px 0px 2px 0px;} .pad-left {padding-left: 10px;} .pad-right {padding-right: 10px;} .pad-right-min {padding-right: 5px;} @@ -171,6 +172,9 @@ table.list tr.selected td { /* deprecated? */ font-size: 140%; font-weight: bold; } +.weak-text-1 { + font-size: 90%; +} .currency-input { width: 8em; diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2 index a6a65ad58c..d2a4db8771 100644 --- a/Open-ILS/src/templates/staff/navbar.tt2 +++ b/Open-ILS/src/templates/staff/navbar.tt2 @@ -252,6 +252,12 @@
  • + + + [% l('Staff Catalog (Experimental)') %] + +
  • +
  • [% l('Record Buckets') %] diff --git a/Open-ILS/web/js/ui/default/staff/cat/staffcat/app.js b/Open-ILS/web/js/ui/default/staff/cat/staffcat/app.js new file mode 100644 index 0000000000..e8058a538b --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/cat/staffcat/app.js @@ -0,0 +1,751 @@ +angular.module('egStaffCatApp', + ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod']) + +.config(function($routeProvider, $locationProvider, $compileProvider) { + $locationProvider.html5Mode(true); + $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export + + + var resolver = {delay : ['egCore','egStartup', + function(egCore, egStartup) { + + // Batch load universally required org unit settings + egCore.env.classLoaders.aous = function() { + // Settings are cached in egCore.org.settings(...) + return egCore.org.settings([ + 'opac.default_search_location', + // Add more catalog display settings here + ]); + } + + egCore.env.loadClasses.push('aous'); + return egStartup.go(); + } + ]}; + + $routeProvider.when('/cat/staffcat/search', { + templateUrl: './cat/staffcat/t_search', + controller: 'StaffCatSearchResultsCtrl', + resolve : resolver + }); + + $routeProvider.when('/cat/staffcat/record/:record_id', { + templateUrl: './cat/staffcat/t_record', + controller: 'StaffCatRecordCtrl', + resolve : resolver + }); + + $routeProvider.otherwise({redirectTo : '/cat/staffcat/search'}); +}) + +.factory('staffCatSvc', [ + '$q','egCore','egBibDisplay', +function($q , egCore , egBibDisplay) { + + var service = { + + search_state : 'pending', + context_org : null, + default_context_org : null, + search_result : null, + ccvm_list_keys : [ + 'item_type', + 'item_form', + 'item_lang', + 'audience', + 'audience_group', // TODO + 'vr_format', + 'bib_level', + 'lit_form' + ], + + // ctype => value-alphabetically sorted list of ccvms + ccvm_lists : {}, + icon_ccvm_map : {}, + cmf_map : {}, + + // Some data must be fetched on all pages, before any actions + // have occurred, but after startup is complete. Bundle those + // here so page-specific controllers can be sure the data is + // loaded before kicking off any auto-load ations. + post_startup_init : function() { + return $q.all([ + service.set_default_context_org(), + service.fetch_ccvms() + ]) + }, + + /** + * Perform the search and collect data related to each bib record. + */ + search : function(params) { + var deferred = $q.defer(); + + service.search_state = 'searching'; + service.context_org = params.context_org; + + var full_query = service.compile_search_query(params) + + console.debug('search query: ' + full_query); + + egCore.net.request( + 'open-ils.search', + 'open-ils.search.biblio.multiclass.query.staff', + {limit : params.limit, offset : params.offset}, + full_query, true + + ).then(function(resp) { + console.debug(resp); + + service.search_result = resp; + service.search_result.records = []; + + // Build the record stub in order of ID response. + var bre_ids = []; + angular.forEach(resp.ids, function(bre_blob) { + var bre_id = bre_blob[0]; + bre_ids.push(bre_id); + + // Create the result record on which all record- + // specific data hangs. Add stub data so the UI + // will shuffle less as real data arrives. + var record = { + id : bre_id, + hold_count : 0, + copy_counts : [] + } + + angular.forEach(egCore.org.ancestors( + service.context_org.id(), true).reverse(), + function(org_id) { + record.copy_counts.push({ + available : 0, + visible : 0, + org_unit : org_id + }); + } + ); + + service.search_result.records.push(record); + }); + + if (bre_ids.length == 0) return; + + // Collect display data. + // These each run 1 API, so OK to run them in parallel. + return $q.all([ + service.fetch_bres(bre_ids), + service.fetch_facet_data(resp.facet_key), + service.fetch_bib_extras(bre_ids) + ]); + + }).then(function() { + service.search_state = 'complete'; + deferred.resolve(); + }); + + return deferred.promise; + }, + + fetch_bres : function(bre_ids) { + return egCore.pcrud.search('bre', {id : bre_ids}, { + flesh : 2, + flesh_fields : { + bre : + ['flat_display_entries','creator','editor','mattrs'], + }, + // Avoid fetching the MARC blob. + // Add other bre fields as needed. + select : {bre : + ['id','tcn_value','creator','editor','create_date','edit_date'] + }, + + }).then( + function() { + service.search_state = 'records'; + }, + null, + function(bre) { + var record = service.get_result_record(bre.id()); + record.bre = bre; + record.display_fields = + egBibDisplay.mfdeToMetaHash(bre.flat_display_entries()) + record.icon_attr = bre.mattrs().filter( + function(a) {return a.attr() == 'icon_format'})[0]; + } + ); + }, + + /** + * Compiles one clause in a multi-clause boolean search + */ + compile_one_query_set : function(params, idx) { + var query = params.query[idx]; + var joiner = params.joiner[idx]; + var match = params.match[idx]; + var type = params.type[idx]; + + var str = ''; + if (!query) return str; + + if (idx > 0) str += ' ' + joiner; + + str += ' ('; + if (type) str += type + ':'; + + function strip_quotes(query) {return query.replace(/"/g, ''); } + function strip_anchors(query) {return query.replace(/[\^\$]/g, ''); } + function add_quotes(query) { + if (query.match(/ /)) + return '"' + query + '"' + return query; + }; + + switch(match) { + case 'phrase': + query = add_quotes(strip_quotes(query)); + break; + case 'nocontains': + query = '-' + add_quotes(strip_quotes(query)); + break; + case 'exact': + query = '^' + strip_anchors(query) + '$'; + break; + case 'starts': + query = add_quotes('^' + strip_anchors(strip_quotes(query))); + break; + } + + return str + query + ')'; + }, + + /** + * Turn the form parameters into a single query string + */ + compile_search_query : function(params) { + var str = ''; + + var qcount = params.query.length; + if (qcount > 1) str += '('; + angular.forEach(params.query, function(q, idx) { + str += service.compile_one_query_set(params, idx); + }); + if (qcount > 1) str += ')'; + + if (params.format) { + str += ' format(' + params.format + ')'; + } + + str += ' site(' + service.context_org.shortname() + ')'; + + if (params.available) str += ' #available'; + if (params.global) { + str += ' depth(' + + egCore.org.root().ou_type().depth() + ')'; + } + + if (params.sort) { + // e.g. title, title.descending + var parts = params.sort.split(/\./); + str += ' sort(' + parts[0] + ')'; + if (parts[1]) str += ' #descending'; + } + + angular.forEach(service.ccvm_list_keys, function(field) { + if (params[field]) { // comma-separated string + str += ' ' + field + '(' + params[field] + ')'; + } + }); + + angular.forEach(params.facets, function(facet) { + str += ' ' + facet.cls + '|' + + facet.name + '[' + facet.value + ']'; + }); + + return str; + }, + + fetch_bib_extras : function(bre_ids) { + return egCore.net.request( + 'open-ils.search', + 'open-ils.search.biblio.catalog.extras.staff', + egCore.auth.token(), service.context_org.id(), bre_ids + ).then(null, null, function(bib_info) { + var record = service.get_result_record(bib_info.id); + // null if a new search was started in the meantime. + if (!record) return; + record.copy_counts = bib_info.copy_counts; + record.hold_count = bib_info.hold_count; + }); + }, + + get_result_record : function(bre_id) { + // Old-timey for loop so we can exit early + var recs = service.search_result.records; + for (var idx = 0; idx < recs.length; idx++) { + if (recs[idx].id == bre_id) + return recs[idx]; + } + }, + + fetch_facet_data : function(facet_key) { + return egCore.net.request( + 'open-ils.search', + 'open-ils.search.facet_cache.retrieve', facet_key + ).then(function(facets) { + // Translate the facet data into something a little + // more digestable by the template. + var facet_data = {}; + angular.forEach(facets, function(facet_hash, cmf_id) { + var cmf_data = []; + var cmf = service.cmf_map[cmf_id]; + + angular.forEach(facet_hash, function(count, value) { + cmf_data.push({value : value, count : count}); + }); + + if (!facet_data[cmf.field_class()]) + facet_data[cmf.field_class()] = {}; + + facet_data[cmf.field_class()][cmf.name()] = { + cmf_label : cmf.label(), + value_list : cmf_data.sort(function(a, b) { + if (a.count > b.count) return -1; + if (a.count < b.count) return 1; + // secondary alpha sort on display value + return a.value < b.value ? -1 : 1; + }) + }; + }); + + service.search_result.facet_data = facet_data; + }); + }, + + fetch_ccvms : function() { + // May already be cached + if (service.ccvm_lists.search_format) return $q.when(); + + return egCore.pcrud.search('ccvm', + {ctype : service.ccvm_list_keys.concat( + ['search_format','icon_format'])}, + {}, {atomic : true} + ).then(function(list) { + angular.forEach(list, function(ccvm) { + if (ccvm.ctype() == 'icon_format') { + service.icon_ccvm_map[ccvm.code()] = ccvm; + } else { + if (!service.ccvm_lists[ccvm.ctype()]) + service.ccvm_lists[ccvm.ctype()] = []; + service.ccvm_lists[ccvm.ctype()].push(ccvm); + } + }); + + // CCVM lists are all sorted alphabetically by "value" (label). + angular.forEach( + service.ccvm_list_keys.concat(['search_format']), + function(key) { + if (!service.ccvm_lists[key]) + service.ccvm_lists[key] = []; + service.ccvm_lists[key] = service.ccvm_lists[key].sort( + function(a, b) { + return a.value() < b.value() ? -1 : 1 + } + ); + } + ); + }); + }, + + fetch_cmfs : function() { + // At the moment, we only need facet CMFs. + // May need other later. + if (Object.keys(service.cmf_map).length) return $q.when(); + return egCore.pcrud.search('cmf', + {facet_field : 't'}, {}, {atomic : true}) + .then(function(cmfs) { + angular.forEach(cmfs, function(cmf) { + service.cmf_map[cmf.id()] = cmf; + }); + }); + }, + + set_default_context_org : function() { + // This setting is load-cached during startup. + if (service.default_context_org) return $q.when(); + + return egCore.org.settings('opac.default_search_location') + .then(function (setting) { + var def = setting['opac.default_search_location']; + service.default_context_org = + def ? egCore.org.get(def) : egCore.org.root() + }) + }, + }; + + return service; +}]) + +/** Top-level controller for the catalog app. + * Tracks scope data needed by all child controllers, including + * (primarily) the search form. + * This controller runs on page load before startup has completed. + */ +.controller('StaffCatBaseCtrl', + ['$scope','$q','$window','$location','$timeout','egCore','staffCatSvc', + 'egBibDisplay', +function($scope , $q , $window , $location , $timeout , egCore , staffCatSvc, + egBibDisplay) { + + var scs = staffCatSvc; // For tidiness + var ctx = $scope.context = { + search_args : {limit : 15}, // see reset_form() + focus_query : [true], + icon_ccvm_map : function() {return scs.icon_ccvm_map}, + ccvm_lists : function(type) { + return (scs.ccvm_lists && scs.ccvm_lists[type]) + ? scs.ccvm_lists[type] : []; + }, + result : function() {return scs.search_result}, + search_state : function() {return scs.search_state}, + org_name : function(org_id) {return egCore.org.get(org_id).shortname()}, + result_count : function() { + return scs.search_result ? scs.search_result.count : 0; + }, + reset_form : reset_form, + search_by_form : search_by_form, + search_by_url : search_by_url, + check_enter : function($event) { + if ($event.keyCode == 13) { + ctx.search_args.offset = 0; + ctx.search_by_form(); + } + }, + first_page : function() { return page_info('first') }, + last_page : function() { return page_info('last') }, + current_page : function() { return page_info('current') }, + page_count : function() { return page_info('count') }, + page_list : function() { return page_info('list') }, + go_to_page : function(page) { + ctx.search_args.offset = (ctx.search_args.limit * (page - 1)); + search_by_form(); + }, + // Useful for inline addition, avoid string concat. + number : function(n) { return Number(n || 0) } + }; + + ctx.add_search_row = function(index) { + ctx.search_args.query.splice(index, 0, ''); + ctx.search_args.type.splice(index, 0, 'keyword'); + ctx.search_args.joiner.splice(index, 0, '&&'); + ctx.search_args.match.splice(index, 0, 'contains'); + } + + ctx.del_search_row = function(index) { + ctx.search_args.query.splice(index, 1); + ctx.search_args.type.splice(index, 1); + ctx.search_args.joiner.splice(index, 1); + ctx.search_args.match.splice(index, 1); + } + + function reset_form() { + ctx.search_args.offset = 0, + ctx.search_args.format = '', + ctx.search_args.sort = '', + ctx.search_args.query = ['']; + ctx.search_args.type = ['keyword']; + ctx.search_args.match = ['contains']; + ctx.search_args.joiner = ['']; + ctx.search_args.facets = []; + ctx.search_args.available = false; + ctx.search_args.global = false; + + // Default to empty string values so sane defaults can be + // applied in the UI for various CCVM-based widgets. + angular.forEach(scs.ccvm_list_keys, function(key) { + ctx.search_args[key] = ['']; + }); + } + + function page_info(which) { + var active = Boolean(scs.search_result); + switch(which) { + case 'first': + return active && ctx.search_args.offset == 0; + case 'last': + return active && page_info('current') == page_info('count'); + case 'current': + return !active ? 0 : + Math.floor(ctx.search_args.offset / ctx.search_args.limit) + 1 + case 'count': + if (!active) return 0; + var pages = scs.search_result.count / ctx.search_args.limit; + if (Math.floor(pages) < pages) + pages = Math.floor(pages) + 1; + return pages; + case 'list': + var list = []; + for(var i = 1; i <= page_info('count'); i++) + list.push(i); + return list; + } + } + + ctx.place_hold = function(result) { + alert('Place hold for ID ' + result.bre.id()); + } + + ctx.add_to_list = function(result) { + alert('Add to list for ID: ' + result.bre.id()); + } + + ctx.search_author = function(result) { + reset_form(); + ctx.search_args.type = ['author']; + ctx.search_args.query = [result.display_fields.author.value]; + search_by_form(); + } + + /** + * Add a new facet to the set of facets used for filtering. + * If the facet is already applied, remove it. + */ + ctx.apply_facet = function(cls, name, value) { + if (ctx.facet_is_applied(cls, name, value)) { + ctx.search_args.facets = ctx.search_args.facets.filter( + function(f) { + return !( + f.cls == cls && + f.name == name && + f.value == value + ); + } + ); + } else { + ctx.search_args.facets.push({ + cls : cls, + name : name, + value : value + }); + } + + ctx.search_args.offset = 0; + search_by_form(); + } + + ctx.facet_is_applied = function(cls, name, value) { + return ctx.search_args.facets.filter(function(f) { + return ( + f.cls == cls && + f.name == name && + f.value == value + ); + })[0]; + } + + // Migrate form data into the URL, the activate the new URL. + function search_by_form() { + + // Avoid propagating search args to the URL if the search + // will ultimately fail on an empty query. + if (!ctx.search_args.query[0]) return; + + propagate_form_to_url(); + } + + // Encode search parameters from all form values into the URL. + // Remove previously encoded values that have been cleared from + // the search form since page load. + function propagate_form_to_url() { + + // Copy form data to URL + var url_search = $location.search(); + + // Propagate scalar values + // Avoid poluting the URL with unset values. + angular.forEach( + ['limit','offset','format','sort','available','global'], + function(field) { + if (ctx.search_args[field]) { + url_search[field] = ctx.search_args[field]; + } else { + delete url_search[field]; + } + } + ); + + url_search.query = [], + url_search.type = [], + url_search.joiner = []; + url_search.match = []; + + angular.forEach(ctx.search_args.query, function(q, idx) { + if (!ctx.search_args.query[idx]) return; + angular.forEach( + ['query', 'type','joiner','match'], + function(field) { + url_search[field][idx] = ctx.search_args[field][idx]; + } + ); + }); + + // Encode and propagate array-based values + // Avoid propagating list values that have no user selection. + angular.forEach(scs.ccvm_list_keys, function(field) { + if (ctx.search_args[field][0] != '') { + url_search[field] = ctx.search_args[field].join(','); + } else { + delete url_search[field]; + } + }); + + // Facets are encoded as a multi-param value. + // Each value is a JSON encoded blob of class, name, and value + url_search.facet = []; + angular.forEach(ctx.search_args.facets, function(facet) { + url_search.facet.push(JSON.stringify(facet)); + }); + + // Handle special case values + if (ctx.search_args.context_org.id() != scs.default_context_org.id()) { + url_search.org = ctx.search_args.context_org.id(); + } else { + delete url_search.org; + } + + $location.path('/cat/staffcat/search').search(url_search); + } + + // Reset the form to its default state, then copy values from + // the URL into the form. + function propagate_url_to_form() { + reset_form(); + + var url_search = $location.search(); + + // Propagate scalar values + angular.forEach( + ['limit','offset','format','sort','available','global'], + function(field) { + if (url_search[field] != undefined) { + ctx.search_args[field] = url_search[field]; + } + } + ); + + if (typeof url_search.query == 'string') { + // If there is only one query set, each parameter + // will be represented as a string instead of an array. + url_search.query = [url_search.query]; + url_search.type = [url_search.type]; + url_search.joiner = [url_search.joiner]; + url_search.match = [url_search.match]; + } + + angular.forEach(url_search.query, function(q, idx) { + ctx.search_args.query[idx] = url_search.query[idx]; + ctx.search_args.type[idx] = url_search.type[idx]; + ctx.search_args.match[idx] = url_search.match[idx]; + ctx.search_args.joiner[idx] = url_search.joiner[idx]; + }); + + // Decode and propagate array-based values + angular.forEach(scs.ccvm_list_keys, function(field) { + if (url_search[field]) { + ctx.search_args[field] = url_search[field].split(/,/); + + // Any of these fields means advanced search filters + // are active. Make them visible. + ctx.show_adv_search = true; + } + }); + + // if there's only a single value it will be a string. + if (typeof url_search.facet == 'string') + url_search.facet = [url_search.facet]; + + angular.forEach(url_search.facet, function(facet) { + console.log('parsing: ' + facet); + ctx.search_args.facets.push(JSON.parse(facet)); + }); + + // Handle special-case values + if (url_search.org) + ctx.search_args.context_org = egCore.org.get(url_search.org); + + } + + // All searches are ultimately search-by-URL. Migrate URL data into + // the form then activate the current parameters by running a new + // search or loading a new page of results. + function search_by_url() { + + propagate_url_to_form(); + + // Searches rerun controllers but do not reload the page. + // Force the browser back to the top of the page w/ each search. + $window.scrollTo(0, 0); + + // Finally exit if there is no search to execute. Do this after + // any other form data has been propagated so the URL and form + // will be in sync. + if (!ctx.search_args.query[0]) return; + + var params = angular.copy(ctx.search_args); + angular.forEach(scs.ccvm_list_keys, function(key) { + params[key] = params[key].join(','); // Stringify + }); + + return staffCatSvc.search(params); + } + +}]) + +/** Controller for main search page */ +.controller('StaffCatSearchResultsCtrl', + ['$scope','$q','$window','$location','egCore','staffCatSvc', +function($scope , $q , $window , $location , egCore , staffCatSvc) { + + staffCatSvc.post_startup_init().then( + function() { + // will be overridden by URL params when present + $scope.context.search_args.context_org + = staffCatSvc.default_context_org; + + // currently we only need CMF's for facets on the results page. + staffCatSvc.fetch_cmfs() + .then($scope.context.search_by_url); + } + ); + + $scope.context.facets = { + display : [ + {facet_class : 'author', facet_order : ['personal', 'corporate']}, + {facet_class : 'subject', facet_order : ['topic']}, + {facet_class : 'identifier', facet_order : ['genre']}, + {facet_class : 'series', facet_order : ['seriestitle']}, + {facet_class : 'subject', facet_order : ['name', 'geographic']} + ], + default_display_count : 5 + } + +}]) + +/** Controller for bib detail page */ +.controller('StaffCatRecordCtrl', + ['$scope','$q','$window','$location','$routeParams','egCore','staffCatSvc', +function($scope , $q , $window , $location , $routeParams , egCore , staffCatSvc) { + + $scope.context.record_id = $routeParams.record_id; + + staffCatSvc.post_startup_init().then( + function() { + // TODO: + // will be overridden by URL params when present + $scope.context.search_args.context_org = + staffCatSvc.default_context_org; + } + ); +}]) + + -- 2.11.0