1 import {OrgService} from '@eg/core/org.service';
2 import {IdlObject} from '@eg/core/idl.service';
3 import {Pager} from '@eg/share/util/pager';
4 import {Params} from '@angular/router';
6 // CCVM's we care about in a catalog context
7 // Don't fetch them all because there are a lot.
8 export const CATALOG_CCVM_FILTERS = [
21 export enum CatalogSearchState {
27 export class FacetFilter {
32 constructor(cls: string, name: string, value: string) {
33 this.facetClass = cls;
34 this.facetName = name;
35 this.facetValue = value;
38 equals(filter: FacetFilter): boolean {
40 this.facetClass === filter.facetClass &&
41 this.facetName === filter.facetName &&
42 this.facetValue === filter.facetValue
47 export class CatalogSearchResults {
58 export class CatalogBrowseContext {
66 this.fieldClass = 'title';
69 isSearchable(): boolean {
72 this.fieldClass !== ''
77 export class CatalogMarcContext {
85 this.subfields = [''];
90 this.tags[0] !== '' &&
97 export class CatalogIdentContext {
109 && this.queryType !== ''
115 export class CatalogTermContext {
116 fieldClass: string[];
122 ccvmFilters: {[ccvmCode: string]: string[]};
123 facetFilters: FacetFilter[];
124 copyLocations: string[]; // ID's, but treated as strings in the UI.
126 // True when searching for metarecords
127 groupByMetarecord: boolean;
129 // Filter results by records which link to this metarecord ID.
130 fromMetarecord: number;
132 hasBrowseEntry: string; // "entryId,fieldId"
133 browseEntry: IdlObject;
136 dateOp: string; // before, after, between, is
140 this.fieldClass = ['keyword'];
141 this.matchOp = ['contains'];
143 this.facetFilters = [];
144 this.copyLocations = [''];
146 this.hasBrowseEntry = '';
150 this.fromMetarecord = null;
152 // Apply empty string values for each ccvm filter
153 this.ccvmFilters = {};
154 CATALOG_CCVM_FILTERS.forEach(code => this.ccvmFilters[code] = ['']);
157 // True when grouping by metarecord but not when displaying the
158 // contents of a metarecord.
159 isMetarecordSearch(): boolean {
161 this.isSearchable() &&
162 this.groupByMetarecord &&
163 this.fromMetarecord === null
167 isSearchable(): boolean {
170 || this.hasBrowseEntry !== ''
171 || this.fromMetarecord !== null
175 hasFacet(facet: FacetFilter): boolean {
177 this.facetFilters.filter(f => f.equals(facet))[0]
181 removeFacet(facet: FacetFilter): void {
182 this.facetFilters = this.facetFilters.filter(f => !f.equals(facet));
185 addFacet(facet: FacetFilter): void {
186 if (!this.hasFacet(facet)) {
187 this.facetFilters.push(facet);
191 toggleFacet(facet: FacetFilter): void {
192 if (this.hasFacet(facet)) {
193 this.removeFacet(facet);
195 this.facetFilters.push(facet);
202 // Not an angular service.
203 // It's conceviable there could be multiple contexts.
204 export class CatalogSearchContext {
206 // Attributes that are used across different contexts.
210 searchOrg: IdlObject;
213 termSearch: CatalogTermContext;
214 marcSearch: CatalogMarcContext;
215 identSearch: CatalogIdentContext;
216 browseSearch: CatalogBrowseContext;
218 // Result from most recent search.
219 result: CatalogSearchResults;
220 searchState: CatalogSearchState = CatalogSearchState.PENDING;
222 // List of IDs in page/offset context.
230 this.pager = new Pager();
231 this.termSearch = new CatalogTermContext();
232 this.marcSearch = new CatalogMarcContext();
233 this.identSearch = new CatalogIdentContext();
234 this.browseSearch = new CatalogBrowseContext();
239 * Return search context to its default state, resetting search
240 * parameters and clearing any cached result data.
243 this.pager.offset = 0;
245 this.showBasket = false;
246 this.result = new CatalogSearchResults();
248 this.searchState = CatalogSearchState.PENDING;
249 this.termSearch.reset();
250 this.marcSearch.reset();
251 this.identSearch.reset();
252 this.browseSearch.reset();
255 isSearchable(): boolean {
258 this.termSearch.isSearchable() ||
259 this.marcSearch.isSearchable() ||
260 this.identSearch.isSearchable() ||
261 this.browseSearch.isSearchable()
265 // List of result IDs for the current page of data.
266 currentResultIds(): number[] {
268 const max = Math.min(
269 this.pager.offset + this.pager.limit,
270 this.pager.resultCount
272 for (let idx = this.pager.offset; idx < max; idx++) {
273 ids.push(this.resultIds[idx]);
278 addResultId(id: number, resultIdx: number ): void {
279 this.resultIds[resultIdx + this.pager.offset] = Number(id);
282 // Return the record at the requested index.
283 resultIdAt(index: number): number {
284 return this.resultIds[index] || null;
287 // Return the index of the requested record
288 indexForResult(id: number): number {
289 for (let i = 0; i < this.resultIds.length; i++) {
290 if (this.resultIds[i] === id) {
297 compileMarcSearchArgs(): any {
298 const searches: any = [];
299 const ms = this.marcSearch;
301 ms.values.forEach((val, idx) => {
305 // "_" is the wildcard subfield for the API.
306 subfield: ms.subfields[idx] ? ms.subfields[idx] : '_',
316 limit : this.pager.limit,
317 offset : this.pager.offset,
318 org_unit: this.searchOrg.id()
322 const parts = this.sort.split(/\./);
323 args.sort = parts[0]; // title, author, etc.
324 if (parts[1]) { args.sort_dir = 'descending'; }
330 compileIdentSearchQuery(): string {
331 const str = ' site(' + this.searchOrg.shortname() + ')';
333 this.identSearch.queryType + ':' + this.identSearch.value;
337 compileBoolQuerySet(idx: number): string {
338 const ts = this.termSearch;
339 let query = ts.query[idx];
340 const joinOp = ts.joinOp[idx];
341 const matchOp = ts.matchOp[idx];
342 const fieldClass = ts.fieldClass[idx];
345 if (!query) { return str; }
347 if (idx > 0) { str += ' ' + joinOp + ' '; }
350 if (fieldClass) { str += fieldClass + ':'; }
354 query = this.addQuotes(this.stripQuotes(query));
357 query = '-' + this.addQuotes(this.stripQuotes(query));
360 query = '^' + this.stripAnchors(query) + '$';
363 query = this.addQuotes('^' +
364 this.stripAnchors(this.stripQuotes(query)));
368 return str + query + ')';
371 stripQuotes(query: string): string {
372 return query.replace(/"/g, '');
375 stripAnchors(query: string): string {
376 return query.replace(/[\^\$]/g, '');
379 addQuotes(query: string): string {
380 if (query.match(/ /)) {
381 return '"' + query + '"';
386 compileTermSearchQuery(): string {
387 const ts = this.termSearch;
395 // e.g. title, title.descending
396 const parts = this.sort.split(/\./);
397 if (parts[1]) { str += ' #descending'; }
398 str += ' sort(' + parts[0] + ')';
401 if (ts.date1 && ts.dateOp) {
404 str += ` date1(${ts.date1})`;
407 str += ` before(${ts.date1})`;
410 str += ` after(${ts.date1})`;
414 str += ` between(${ts.date1},${ts.date2})`;
420 // Compile boolean sub-query components
421 if (str.length) { str += ' '; }
422 const qcount = ts.query.length;
424 // if we multiple boolean query components, wrap them in parens.
425 if (qcount > 1) { str += '('; }
426 ts.query.forEach((q, idx) => {
427 str += this.compileBoolQuerySet(idx);
429 if (qcount > 1) { str += ')'; }
432 if (ts.hasBrowseEntry) {
433 // stored as a comma-separated string of "entryId,fieldId"
434 str += ` has_browse_entry(${ts.hasBrowseEntry})`;
437 if (ts.fromMetarecord) {
438 str += ` from_metarecord(${ts.fromMetarecord})`;
442 str += ' format(' + ts.format + ')';
447 this.org.root().ou_type().depth() + ')';
450 if (ts.copyLocations[0] !== '') {
451 str += ' locations(' + ts.copyLocations + ')';
454 str += ' site(' + this.searchOrg.shortname() + ')';
456 Object.keys(ts.ccvmFilters).forEach(field => {
457 if (ts.ccvmFilters[field][0] !== '') {
458 str += ' ' + field + '(' + ts.ccvmFilters[field] + ')';
462 ts.facetFilters.forEach(f => {
463 str += ' ' + f.facetClass + '|'
464 + f.facetName + '[' + f.facetValue + ']';