]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
LP1889128 Holds reset repairs and making space
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / catalog / search-form.component.ts
1 import {Component, OnInit, AfterViewInit, Renderer2} from '@angular/core';
2 import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
3 import {IdlObject} from '@eg/core/idl.service';
4 import {OrgService} from '@eg/core/org.service';
5 import {ServerStoreService} from '@eg/core/server-store.service';
6 import {CatalogService} from '@eg/share/catalog/catalog.service';
7 import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
8 import {StaffCatalogService} from './catalog.service';
9 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
10
11 // Maps opac-style default tab names to local tab names.
12 const LEGACY_TAB_NAME_MAP = {
13     expert: 'marc',
14     numeric: 'ident',
15     advanced: 'term'
16 };
17
18 // Automatically collapse the search form on these pages
19 const COLLAPSE_ON_PAGES = [
20     new RegExp(/catalog\/record\//),
21     new RegExp(/catalog\/hold\//)
22 ];
23
24 @Component({
25   selector: 'eg-catalog-search-form',
26   styleUrls: ['search-form.component.css'],
27   templateUrl: 'search-form.component.html'
28 })
29 export class SearchFormComponent implements OnInit, AfterViewInit {
30
31     context: CatalogSearchContext;
32     ccvmMap: {[ccvm: string]: IdlObject[]} = {};
33     cmfMap: {[cmf: string]: IdlObject} = {};
34     showSearchFilters = false;
35     copyLocations: IdlObject[];
36     searchTab: string;
37
38     // Display the full form if true, otherwise display the expandy.
39     showThyself = true;
40
41     constructor(
42         private renderer: Renderer2,
43         private router: Router,
44         private route: ActivatedRoute,
45         private org: OrgService,
46         private cat: CatalogService,
47         private staffCat: StaffCatalogService
48     ) {
49         this.copyLocations = [];
50
51         // Some search scenarios, like rendering a search template,
52         // will not be searchable and thus not resovle to a specific
53         // search tab.  Check to see if a specific tab is requested
54         // via the URL.
55         this.route.queryParams.subscribe(params => {
56             if (params.searchTab) {
57                 this.searchTab = params.searchTab;
58             }
59         });
60
61         this.router.events.subscribe(routeEvent => {
62             if (routeEvent instanceof NavigationEnd) {
63                 this.showThyself = true;
64                 COLLAPSE_ON_PAGES.forEach(pageRegex => {
65                     if (routeEvent.url.match(pageRegex)) {
66                         this.showThyself = false;
67                     }
68                 });
69             }
70         });
71     }
72
73     ngOnInit() {
74         this.ccvmMap = this.cat.ccvmMap;
75         this.cmfMap = this.cat.cmfMap;
76         this.context = this.staffCat.searchContext;
77
78         // Start with advanced search options open
79         // if any filters are active.
80         this.showSearchFilters = this.filtersActive();
81     }
82
83     ngAfterViewInit() {
84         // Query inputs are generated from search context data,
85         // so they are not available until after the first render.
86         // Search context data is extracted synchronously from the URL.
87
88         // Avoid changing the tab in the lifecycle hook thread.
89         setTimeout(() => {
90
91             if (this.context.identSearch.queryType === '') {
92                 this.context.identSearch.queryType = 'identifier|isbn';
93             }
94
95             // Apply a tab if none was already specified
96             if (!this.searchTab) {
97                 // Assumes that only one type of search will be searchable
98                 // at any given time.
99                 if (this.context.marcSearch.isSearchable()) {
100                     this.searchTab = 'marc';
101                 } else if (this.context.identSearch.isSearchable()) {
102                     this.searchTab = 'ident';
103                 } else if (this.context.browseSearch.isSearchable()) {
104                     this.searchTab = 'browse';
105                 } else if (this.context.termSearch.isSearchable()) {
106                     this.searchTab = 'term';
107
108                 } else {
109
110                     this.searchTab =
111                         LEGACY_TAB_NAME_MAP[this.staffCat.defaultTab]
112                         || this.staffCat.defaultTab || 'term';
113
114                 }
115
116                 if (this.searchTab === 'term') {
117                     this.refreshCopyLocations();
118                 }
119             }
120
121             this.focusTabInput();
122         });
123     }
124
125     onTabChange(evt: NgbTabChangeEvent) {
126         this.searchTab = evt.nextId;
127
128         // Focus after tab-change event has a chance to complete
129         // or the tab body and its input won't exist yet and no
130         // elements will be focus-able.
131         setTimeout(() => this.focusTabInput());
132     }
133
134     focusTabInput() {
135         // Select a DOM node to focus when the tab changes.
136         let selector: string;
137         switch (this.searchTab) {
138             case 'ident':
139                 selector = '#ident-query-input';
140                 break;
141             case 'marc':
142                 selector = '#first-marc-tag';
143                 break;
144             case 'browse':
145                 selector = '#browse-term-input';
146                 break;
147             case 'cnbrowse':
148                 selector = '#cnbrowse-term-input';
149                 break;
150             default:
151                 this.refreshCopyLocations();
152                 selector = '#first-query-input';
153         }
154
155         try {
156             // TODO: sometime the selector is not available in the DOM
157             // until even later (even with setTimeouts).  Need to fix this.
158             // Note the error is thrown from selectRootElement(), not the
159             // call to .focus() on a null reference.
160             this.renderer.selectRootElement(selector).focus();
161         } catch (E) {}
162     }
163
164     /**
165      * Display the advanced/extended search options when asked to
166      * or if any advanced options are selected.
167      */
168     showFilters(): boolean {
169         // Note that filters may become active due to external
170         // actions on the search context.  Always show the filters
171         // if filter values are applied.
172         return this.showSearchFilters || this.filtersActive();
173     }
174
175     toggleFilters() {
176         this.showSearchFilters = !this.showSearchFilters;
177         this.refreshCopyLocations();
178     }
179
180     filtersActive(): boolean {
181
182         if (this.context.termSearch.copyLocations[0] !== '') { return true; }
183
184         // ccvm filters may be present without any filters applied.
185         // e.g. if filters were applied then removed.
186         let show = false;
187         Object.keys(this.context.termSearch.ccvmFilters).forEach(ccvm => {
188             if (this.context.termSearch.ccvmFilters[ccvm][0] !== '') {
189                 show = true;
190             }
191         });
192
193         return show;
194     }
195
196     orgOnChange = (org: IdlObject): void => {
197         this.context.searchOrg = org;
198         this.refreshCopyLocations();
199     }
200
201     refreshCopyLocations() {
202         if (!this.showFilters()) { return; }
203
204         // TODO: is this how we avoid displaying too many locations?
205         const org = this.context.searchOrg;
206         if (org.id() === this.org.root().id()) {
207             this.copyLocations = [];
208             return;
209         }
210
211         this.cat.fetchCopyLocations(org).then(() =>
212             this.copyLocations = this.cat.copyLocations
213         );
214     }
215
216     orgName(orgId: number): string {
217         return this.org.get(orgId).shortname();
218     }
219
220     addSearchRow(index: number): void {
221         this.context.termSearch.query.splice(index, 0, '');
222         this.context.termSearch.fieldClass.splice(index, 0, 'keyword');
223         this.context.termSearch.joinOp.splice(index, 0, '&&');
224         this.context.termSearch.matchOp.splice(index, 0, 'contains');
225     }
226
227     delSearchRow(index: number): void {
228         this.context.termSearch.query.splice(index, 1);
229         this.context.termSearch.fieldClass.splice(index, 1);
230         this.context.termSearch.joinOp.splice(index, 1);
231         this.context.termSearch.matchOp.splice(index, 1);
232     }
233
234     addMarcSearchRow(index: number): void {
235         this.context.marcSearch.tags.splice(index, 0, '');
236         this.context.marcSearch.subfields.splice(index, 0, '');
237         this.context.marcSearch.values.splice(index, 0, '');
238     }
239
240     delMarcSearchRow(index: number): void {
241         this.context.marcSearch.tags.splice(index, 1);
242         this.context.marcSearch.subfields.splice(index, 1);
243         this.context.marcSearch.values.splice(index, 1);
244     }
245
246     searchByForm(): void {
247         this.context.pager.offset = 0; // New search
248
249         // Form search overrides basket display
250         this.context.showBasket = false;
251
252         this.context.scrub(this.searchTab);
253
254         switch (this.searchTab) {
255
256             case 'term':
257             case 'ident':
258             case 'marc':
259                 this.staffCat.search();
260                 break;
261
262             case 'browse':
263                 this.staffCat.browse();
264                 break;
265
266             case 'cnbrowse':
267                 this.staffCat.cnBrowse();
268                 break;
269         }
270     }
271
272     // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes
273     trackByIdx(index: any, item: any) {
274        return index;
275     }
276
277     searchIsActive(): boolean {
278         return this.context.searchState === CatalogSearchState.SEARCHING;
279     }
280
281     // It's possible to chose invalid combos depending on the order of selection
282     preventBogusCombos(idx: number) {
283         if (this.context.termSearch.fieldClass[idx] === 'keyword') {
284             const op = this.context.termSearch.matchOp[idx];
285             if (op === 'exact' || op === 'starts') {
286                 this.context.termSearch.matchOp[idx] = 'contains';
287             }
288         }
289     }
290
291     showBookplate(): boolean {
292         return this.staffCat.enableBookplates;
293     }
294     showExcludeElectronic(): boolean {
295         return this.staffCat.showExcludeElectronic;
296     }
297 }
298
299