1 import {Injectable, EventEmitter, NgZone} from '@angular/core';
2 import {Router, ActivatedRoute} from '@angular/router';
3 import {IdlObject} from '@eg/core/idl.service';
4 import {OrgService} from '@eg/core/org.service';
5 import {CatalogService} from '@eg/share/catalog/catalog.service';
6 import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
7 import {CatalogSearchContext} from '@eg/share/catalog/search-context';
8 import {BibRecordSummary} from '@eg/share/catalog/bib-record.service';
9 import {PatronService} from '@eg/staff/share/patron/patron.service';
10 import {StoreService} from '@eg/core/store.service';
11 import {BroadcastService} from '@eg/share/util/broadcast.service';
12 import {Observable} from 'rxjs';
13 import {tap} from 'rxjs/operators';
15 const HOLD_FOR_PATRON_KEY = 'eg.circ.patron_hold_target';
18 * Shared bits needed by the staff version of the catalog.
22 export class StaffCatalogService {
24 searchContext: CatalogSearchContext;
26 defaultSearchOrg: IdlObject;
27 defaultSearchLimit: number;
28 // Track the current template through route changes.
29 selectedTemplate: string;
31 // Display the Exclude Electronic checkbox
32 showExcludeElectronic = false;
34 // Advanced search filters to display
35 searchFilters: string[];
37 // TODO: does unapi support pref-lib for result-page copy counts?
43 // Patron barcode we hope to place a hold for.
44 holdForBarcode: string;
45 // User object for above barcode.
46 holdForUser: IdlObject;
48 // Emit that the value has changed so components can detect
49 // the change even when the component is not itself digesting
51 holdForChange: EventEmitter<void> = new EventEmitter<void>();
53 // Cache the currently selected detail record (i.g. catalog/record/123)
54 // summary so the record detail component can avoid duplicate fetches
55 // during record tab navigation.
56 currentDetailRecordSummary: any;
58 // Add digital bookplate to search options.
59 enableBookplates = false;
61 // Cache of browse results so the browse pager is not forced to
62 // re-run the browse search on each navigation.
63 browsePagerData: any[];
65 // whether to redirect to record page upon a single search
67 jumpOnSingleHit = false;
69 // discovery layer URL to display an item in "patron view"
73 private router: Router,
74 private route: ActivatedRoute,
75 private store: StoreService,
76 private org: OrgService,
77 private cat: CatalogService,
78 private patron: PatronService,
79 private catUrl: CatalogUrlService,
80 private broadcaster: BroadcastService,
84 createContext(): void {
85 // Initialize the search context from the load-time URL params.
86 // Do this here so the search form and other context data are
87 // applied on every page, not just the search results page. The
88 // search results pages will handle running the actual search.
90 this.catUrl.fromUrlParams(this.route.snapshot.queryParamMap);
92 this.holdForBarcode = this.store.getLoginSessionItem(HOLD_FOR_PATRON_KEY);
94 if (this.holdForBarcode) {
95 this.patron.getByBarcode(this.holdForBarcode)
97 this.holdForUser = user;
98 this.holdForChange.emit();
101 // In case the session item was cleared from another component.
102 this.clearHoldPatron();
105 this.searchContext.org = this.org; // service, not searchOrg
106 this.searchContext.isStaff = true;
107 this.applySearchDefaults();
110 clearHoldPatron(broadcast = true) {
111 const removedTarget = this.holdForBarcode;
113 this.holdForUser = null;
114 this.holdForBarcode = null;
115 this.store.removeLoginSessionItem(HOLD_FOR_PATRON_KEY);
116 this.holdForChange.emit();
117 if (!broadcast) {return;}
119 // clear hold patron on other tabs
120 this.broadcaster.broadcast(
121 HOLD_FOR_PATRON_KEY, { removedTarget }
125 onBeforeUnload(): void {
126 const closedTarget = this.holdForBarcode;
128 this.clearHoldPatron(false);
129 this.broadcaster.broadcast(HOLD_FOR_PATRON_KEY,
135 onChangeHoldPatron(): Observable<any> {
136 return this.broadcaster.listen(HOLD_FOR_PATRON_KEY).pipe(
137 tap(({ removedTarget, closedTarget }) => {
138 if (removedTarget && this.holdForBarcode) {
139 // broadcaster doesn't trigger change detection,
140 // so trigger it manually
141 this.zone.run(() => this.clearHoldPatron(false));
143 } else if (closedTarget) {
144 // if hold target was unset by another tab,
145 // restore the hold target
146 if (closedTarget === this.holdForBarcode) {
147 this.store.setLoginSessionItem(
148 HOLD_FOR_PATRON_KEY, closedTarget
156 cloneContext(context: CatalogSearchContext): CatalogSearchContext {
157 const params: any = this.catUrl.toUrlParams(context);
158 const ctx = this.catUrl.fromUrlHash(params);
159 ctx.isStaff = true; // not carried in the URL
163 applySearchDefaults(): void {
164 if (!this.searchContext.searchOrg) {
165 this.searchContext.searchOrg =
166 this.defaultSearchOrg || this.org.root();
169 if (!this.searchContext.pager.limit) {
170 this.searchContext.pager.limit = this.defaultSearchLimit || 10;
175 * Redirect to the search results page while propagating the current
176 * search paramters into the URL. Let the search results component
177 * execute the actual search.
180 if (!this.searchContext.isSearchable()) { return; }
182 // Clear cached detail summary for new searches.
183 this.currentDetailRecordSummary = null;
185 const params = this.catUrl.toUrlParams(this.searchContext);
187 // Force a new search every time this method is called, even if
188 // it's the same as the active search. Since router navigation
189 // exits early when the route + params is identical, add a
190 // random token to the route params to force a full navigation.
191 // This also resolves a problem where only removing secondary+
192 // versions of a query param fail to cause a route navigation.
193 // (E.g. going from two query= params to one). Investigation
195 params.ridx = '' + this.routeIndex++;
197 this.router.navigate(
198 ['/staff/catalog/search'], {queryParams: params});
202 * Redirect to the browse results page while propagating the current
203 * browse paramters into the URL. Let the browse results component
204 * execute the actual browse.
207 if (!this.searchContext.browseSearch.isSearchable()) { return; }
208 const params = this.catUrl.toUrlParams(this.searchContext);
210 // Force a new browse every time this method is called, even if
211 // it's the same as the active browse. Since router navigation
212 // exits early when the route + params is identical, add a
213 // random token to the route params to force a full navigation.
214 // This also resolves a problem where only removing secondary+
215 // versions of a query param fail to cause a route navigation.
216 // (E.g. going from two query= params to one).
217 params.ridx = '' + this.routeIndex++;
219 this.router.navigate(
220 ['/staff/catalog/browse'], {queryParams: params});
223 // Call number browse.
224 // Redirect to cn browse page and let its component perform the search
226 if (!this.searchContext.cnBrowseSearch.isSearchable()) { return; }
227 const params = this.catUrl.toUrlParams(this.searchContext);
228 params.ridx = '' + this.routeIndex++; // see comments above
229 this.router.navigate(['/staff/catalog/cnbrowse'], {queryParams: params});
232 // Params to genreate a new author search based on a reset
233 // clone of the current page params.
234 getAuthorSearchParams(summary: BibRecordSummary): any {
235 const tmpContext = this.cloneContext(this.searchContext);
237 tmpContext.termSearch.fieldClass = ['author'];
238 tmpContext.termSearch.query = [summary.display.author];
239 return this.catUrl.toUrlParams(tmpContext);