--- /dev/null
+<style>
+ /** TODO: clean up some inline styles */
+
+ #staffcat-search-form .eg-org-selector,
+ #staffcat-search-form .eg-org-selector button {
+ width: 100%;
+ text-align: left
+ }
+ .flex-cell {
+ /* override style.css padding:4px */
+ padding: 0px 0px 0px 8px;
+ }
+ .flex-row:first-child {
+ padding-left: 0px;
+ }
+</style>
+<div id='staffcat-search-form'>
+ <div class="row"
+ ng-repeat="q in context.search_args.query track by $index">
+ <div class="col-md-9 flex-row">
+ <div class="flex-cell">
+ <div ng-if="$index == 0">
+ <select class="form-control" ng-model="context.search_args.format">
+ <option value=''>[% l('All Formats') %]</option>
+ <option ng-repeat="fmt in context.ccvm_lists('search_format')"
+ value="{{fmt.code()}}">{{fmt.value()}}</option>
+ </select>
+ </div>
+ <div ng-if="$index > 0">
+ <select class="form-control"
+ ng-model="context.search_args.joiner[$index]">
+ <option value='&&'>[% l('And') %]</option>
+ <option value='||'>[% l('Or') %]</option>
+ </select>
+ </div>
+ </div>
+ <div class="flex-cell">
+ <select class="form-control"
+ ng-model="context.search_args.type[$index]">
+ <option value='keyword'>[% l('Keyword') %]</option>
+ <option value='title'>[% l('Title') %]</option>
+ <option value='jtitle'>[% l('Journal Title') %]</option>
+ <option value='author'>[% l('Author') %]</option>
+ <option value='subject'>[% l('Subject') %]</option>
+ <option value='series'>[% l('Series') %]</option>
+ <!-- TODO: bookplate -->
+ </select>
+ </div>
+ <div class="flex-cell">
+ <select class="form-control"
+ ng-model="context.search_args.match[$index]">
+ <option value='contains'>[% l('Contains') %]</option>
+ <option value='nocontains'>[% l('Does not contain') %]</option>
+ <option value='phrase'>[% l('Contains phrase') %]</option>
+ <option value='exact'>[% l('Matches exactly') %]</option>
+ <option value='starts'>[% l('Starts with') %]</option>
+ </select>
+ </div>
+ <div class="flex-cell flex-2">
+ <div class="form-group">
+ <input type="text" class="form-control"
+ focus-me="context.focus_query[$index]"
+ ng-model="context.search_args.query[$index]"
+ ng-keyup="context.check_enter($event)"
+ placeholder="[% l('Query...') %]"/>
+ </div>
+ </div>
+ <div class="flex-cell">
+ <button class="btn btn-sm btn-default"
+ ng-click="context.add_search_row($index + 1)">
+ <span class="glyphicon glyphicon-plus"></span>
+ </button>
+ <button class="btn btn-sm btn-default"
+ ng-disabled="context.search_args.query.length < 2"
+ ng-click="context.del_search_row($index)">
+ <span class="glyphicon glyphicon-minus"></span>
+ </button>
+ </div>
+ </div>
+ <div class="col-md-3">
+ <div ng-if="$index == 0" class="pull-right">
+ <button class="btn btn-success" type="button"
+ ng-click="context.search_args.offset=0;context.search_by_form()">
+ [% l('Search') %]
+ </button>
+ <button class="btn btn-warning" type="button"
+ ng-click="context.reset_form()">
+ [% l('Clear Form') %]
+ </button>
+ <button class="btn btn-default" type="button"
+ ng-if="!context.show_adv_search"
+ ng-click="context.show_adv_search=true">
+ [% l('More Filters...') %]
+ </button>
+ <button class="btn btn-default" type="button"
+ ng-if="context.show_adv_search"
+ ng-click="context.show_adv_search=false">
+ [% l('Hide Filters') %]
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-9 flex-row">
+ <div class="flex-cell">
+ <eg-org-selector nodefault
+ selected="context.search_args.context_org">
+ </eg-org-selector>
+ </div>
+ <div class="flex-cell flex-3">
+ <select class="form-control" ng-model="context.search_args.sort">
+ <option value=''>[% l('Sort by Relevance') %]</option>
+ <optgroup label="[% l('Sort by Title') %]">
+ <option value='titlesort'>[% l('Title: A to Z') %]</option>
+ <option value='titlesort.descending'>[% l('Title: Z to A') %]</option>
+ </optgroup>
+ <optgroup label="[% l('Sort by Author') %]">
+ <option value='authorsort'>[% l('Author: A to Z') %]</option>
+ <option value='authorsort.descending'>[% l('Author: Z to A') %]</option>
+ </optgroup>
+ <optgroup label="[% l('Sort by Publication Date') %]">
+ <option value='pubdate'>[% l('Date: A to Z') %]</option>
+ <option value='pubdate.descending'>[% l('Date: Z to A') %]</option>
+ </optgroup>
+ <optgroup label="[% l('Sort by Popularity') %]">
+ <option value='popularity'>[% l('Most Popular') %]</option>
+ <option value='poprel'>[% l('Popularity Adjusted Relevance') %]</option>
+ </optgroup>
+ </select>
+ </div>
+ <div class="flex-cell flex-2">
+ <div class="checkbox">
+ <label>
+ <input type="checkbox" ng-model="context.search_args.available"/>
+ [% l('Limit to Available') %]
+ </label>
+ </div>
+ </div>
+ <div class="flex-cell flex-4">
+ <div class="checkbox">
+ <label>
+ <input type="checkbox" ng-model="context.search_args.global"/>
+ [% l('Show Results from All Libraries') %]
+ </label>
+ </div>
+ </div>
+ <!-- balance out the flow -->
+ <div class="flex-cell flex-2">
+ <div ng-if="context.search_state() == 'searching'">
+ <div class="progress">
+ <div class="progress-bar progress-bar-striped active"
+ role="progressbar" aria-valuenow="100" aria-valuemin="0"
+ aria-valuemax="100" style="width: 100%">
+ <span class="sr-only">[% l('Searching..') %]</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row pad-vert-min" ng-show="context.show_adv_search">
+ <div class="col-md-2">
+ <select class="form-control" ng-model="context.search_args.item_type" multiple="true">
+ <option value=''>[% l('All Item Types') %]</option>
+ <option ng-repeat="item_type in context.ccvm_lists('item_type')"
+ value="{{item_type.code()}}">{{item_type.value()}}</option>
+ </select>
+ </div>
+ <div class="col-md-2">
+ <select class="form-control" ng-model="context.search_args.item_form" multiple="true">
+ <option value=''>[% l('All Item Forms') %]</option>
+ <option ng-repeat="item_form in context.ccvm_lists('item_form')"
+ value="{{item_form.code()}}">{{item_form.value()}}</option>
+ </select>
+ </div>
+ <div class="col-md-2">
+ <select class="form-control" ng-model="context.search_args.item_lang" multiple="true">
+ <option value=''>[% l('All Languages') %]</option>
+ <option ng-repeat="item_lang in context.ccvm_lists('item_lang')"
+ value="{{item_lang.code()}}">{{item_lang.value()}}</option>
+ </select>
+ </div>
+ <div class="col-md-2">
+ <select class="form-control" ng-model="context.search_args.audience" multiple="true">
+ <option value=''>[% l('All Audiences') %]</option>
+ <option ng-repeat="audience in context.ccvm_lists('audience')"
+ value="{{audience.code()}}">{{audience.value()}}</option>
+ </select>
+ </div>
+ </div>
+ <div class="row pad-vert-min" ng-show="context.show_adv_search">
+ <div class="col-md-2">
+ <select class="form-control" ng-model="context.search_args.vr_format" multiple="true">
+ <option value=''>[% l('All Video Formats') %]</option>
+ <option ng-repeat="vr_format in context.ccvm_lists('vr_format')"
+ value="{{vr_format.code()}}">{{vr_format.value()}}</option>
+ </select>
+ </div>
+ <div class="col-md-2">
+ <select class="form-control" ng-model="context.search_args.bib_level" multiple="true">
+ <option value=''>[% l('All Bib Levels') %]</option>
+ <option ng-repeat="bib_level in context.ccvm_lists('bib_level')"
+ value="{{bib_level.code()}}">{{bib_level.value()}}</option>
+ </select>
+ </div>
+ <div class="col-md-2">
+ <select class="form-control" ng-model="context.search_args.lit_form" multiple="true">
+ <option value=''>[% l('All Literary Forms') %]</option>
+ <option ng-repeat="lit_form in context.ccvm_lists('lit_form')"
+ value="{{lit_form.code()}}">{{lit_form.value()}}</option>
+ </select>
+ </div>
+ </div>
+</div>
+
--- /dev/null
+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;
+ }
+ );
+}])
+
+