1 import {OrgService} from '@eg/core/org.service';
2 import {IdlObject} from '@eg/core/idl.service';
3 import {Pager} from '@eg/share/util/pager';
5 // CCVM's we care about in a catalog context
6 // Don't fetch them all because there are a lot.
7 export const CATALOG_CCVM_FILTERS = [
20 export enum CatalogSearchState {
26 export class FacetFilter {
31 constructor(cls: string, name: string, value: string) {
32 this.facetClass = cls;
33 this.facetName = name;
34 this.facetValue = value;
37 equals(filter: FacetFilter): boolean {
39 this.facetClass === filter.facetClass &&
40 this.facetName === filter.facetName &&
41 this.facetValue === filter.facetValue
46 export class CatalogSearchResults {
57 export class CatalogBrowseContext {
65 this.fieldClass = 'title';
68 isSearchable(): boolean {
71 this.fieldClass !== ''
76 export class CatalogMarcContext {
84 this.subfields = [''];
89 this.tags[0] !== '' &&
96 export class CatalogIdentContext {
108 && this.queryType !== ''
114 export class CatalogCnBrowseContext {
116 // offset in pages from base browse term
117 // e.g. -2 means 2 pages back (alphabetically) from the original search.
126 return this.value !== '';
130 export class CatalogTermContext {
131 fieldClass: string[];
137 ccvmFilters: {[ccvmCode: string]: string[]};
138 facetFilters: FacetFilter[];
139 copyLocations: string[]; // ID's, but treated as strings in the UI.
141 // True when searching for metarecords
142 groupByMetarecord: boolean;
144 // Filter results by records which link to this metarecord ID.
145 fromMetarecord: number;
147 hasBrowseEntry: string; // "entryId,fieldId"
148 browseEntry: IdlObject;
151 dateOp: string; // before, after, between, is
155 this.fieldClass = ['keyword'];
156 this.matchOp = ['contains'];
158 this.facetFilters = [];
159 this.copyLocations = [''];
161 this.hasBrowseEntry = '';
165 this.fromMetarecord = null;
167 // Apply empty string values for each ccvm filter
168 this.ccvmFilters = {};
169 CATALOG_CCVM_FILTERS.forEach(code => this.ccvmFilters[code] = ['']);
172 // True when grouping by metarecord but not when displaying the
173 // contents of a metarecord.
174 isMetarecordSearch(): boolean {
176 this.isSearchable() &&
177 this.groupByMetarecord &&
178 this.fromMetarecord === null
182 isSearchable(): boolean {
185 || this.hasBrowseEntry !== ''
186 || this.fromMetarecord !== null
190 hasFacet(facet: FacetFilter): boolean {
192 this.facetFilters.filter(f => f.equals(facet))[0]
196 removeFacet(facet: FacetFilter): void {
197 this.facetFilters = this.facetFilters.filter(f => !f.equals(facet));
200 addFacet(facet: FacetFilter): void {
201 if (!this.hasFacet(facet)) {
202 this.facetFilters.push(facet);
206 toggleFacet(facet: FacetFilter): void {
207 if (this.hasFacet(facet)) {
208 this.removeFacet(facet);
210 this.facetFilters.push(facet);
217 // Not an angular service.
218 // It's conceviable there could be multiple contexts.
219 export class CatalogSearchContext {
221 // Attributes that are used across different contexts.
225 searchOrg: IdlObject;
228 termSearch: CatalogTermContext;
229 marcSearch: CatalogMarcContext;
230 identSearch: CatalogIdentContext;
231 browseSearch: CatalogBrowseContext;
232 cnBrowseSearch: CatalogCnBrowseContext;
234 // Result from most recent search.
235 result: CatalogSearchResults;
236 searchState: CatalogSearchState = CatalogSearchState.PENDING;
238 // List of IDs in page/offset context.
246 this.pager = new Pager();
247 this.termSearch = new CatalogTermContext();
248 this.marcSearch = new CatalogMarcContext();
249 this.identSearch = new CatalogIdentContext();
250 this.browseSearch = new CatalogBrowseContext();
251 this.cnBrowseSearch = new CatalogCnBrowseContext();
256 * Return search context to its default state, resetting search
257 * parameters and clearing any cached result data.
260 this.pager.offset = 0;
262 this.showBasket = false;
263 this.result = new CatalogSearchResults();
265 this.searchState = CatalogSearchState.PENDING;
266 this.termSearch.reset();
267 this.marcSearch.reset();
268 this.identSearch.reset();
269 this.browseSearch.reset();
272 isSearchable(): boolean {
275 this.termSearch.isSearchable() ||
276 this.marcSearch.isSearchable() ||
277 this.identSearch.isSearchable() ||
278 this.browseSearch.isSearchable()
282 // List of result IDs for the current page of data.
283 currentResultIds(): number[] {
285 const max = Math.min(
286 this.pager.offset + this.pager.limit,
287 this.pager.resultCount
289 for (let idx = this.pager.offset; idx < max; idx++) {
290 ids.push(this.resultIds[idx]);
295 addResultId(id: number, resultIdx: number ): void {
296 this.resultIds[resultIdx + this.pager.offset] = Number(id);
299 // Return the record at the requested index.
300 resultIdAt(index: number): number {
301 return this.resultIds[index] || null;
304 // Return the index of the requested record
305 indexForResult(id: number): number {
306 for (let i = 0; i < this.resultIds.length; i++) {
307 if (this.resultIds[i] === id) {
314 compileMarcSearchArgs(): any {
315 const searches: any = [];
316 const ms = this.marcSearch;
318 ms.values.forEach((val, idx) => {
322 // "_" is the wildcard subfield for the API.
323 subfield: ms.subfields[idx] ? ms.subfields[idx] : '_',
333 limit : this.pager.limit,
334 offset : this.pager.offset,
335 org_unit: this.searchOrg.id()
339 const parts = this.sort.split(/\./);
340 args.sort = parts[0]; // title, author, etc.
341 if (parts[1]) { args.sort_dir = 'descending'; }
347 compileIdentSearchQuery(): string {
348 const str = ' site(' + this.searchOrg.shortname() + ')';
350 this.identSearch.queryType + ':' + this.identSearch.value;
354 compileBoolQuerySet(idx: number): string {
355 const ts = this.termSearch;
356 let query = ts.query[idx];
357 const joinOp = ts.joinOp[idx];
358 const matchOp = ts.matchOp[idx];
359 const fieldClass = ts.fieldClass[idx];
362 if (!query) { return str; }
364 if (idx > 0) { str += ' ' + joinOp + ' '; }
367 if (fieldClass) { str += fieldClass + ':'; }
371 query = this.addQuotes(this.stripQuotes(query));
374 query = '-' + this.addQuotes(this.stripQuotes(query));
377 query = '^' + this.stripAnchors(query) + '$';
380 query = this.addQuotes('^' +
381 this.stripAnchors(this.stripQuotes(query)));
385 return str + query + ')';
388 stripQuotes(query: string): string {
389 return query.replace(/"/g, '');
392 stripAnchors(query: string): string {
393 return query.replace(/[\^\$]/g, '');
396 addQuotes(query: string): string {
397 if (query.match(/ /)) {
398 return '"' + query + '"';
403 compileTermSearchQuery(): string {
404 const ts = this.termSearch;
412 // e.g. title, title.descending
413 const parts = this.sort.split(/\./);
414 if (parts[1]) { str += ' #descending'; }
415 str += ' sort(' + parts[0] + ')';
418 if (ts.date1 && ts.dateOp) {
421 str += ` date1(${ts.date1})`;
424 str += ` before(${ts.date1})`;
427 str += ` after(${ts.date1})`;
431 str += ` between(${ts.date1},${ts.date2})`;
437 // Compile boolean sub-query components
438 if (str.length) { str += ' '; }
439 const qcount = ts.query.length;
441 // if we multiple boolean query components, wrap them in parens.
442 if (qcount > 1) { str += '('; }
443 ts.query.forEach((q, idx) => {
444 str += this.compileBoolQuerySet(idx);
446 if (qcount > 1) { str += ')'; }
449 if (ts.hasBrowseEntry) {
450 // stored as a comma-separated string of "entryId,fieldId"
451 str += ` has_browse_entry(${ts.hasBrowseEntry})`;
454 if (ts.fromMetarecord) {
455 str += ` from_metarecord(${ts.fromMetarecord})`;
459 str += ' format(' + ts.format + ')';
464 this.org.root().ou_type().depth() + ')';
467 if (ts.copyLocations[0] !== '') {
468 str += ' locations(' + ts.copyLocations + ')';
471 str += ' site(' + this.searchOrg.shortname() + ')';
473 Object.keys(ts.ccvmFilters).forEach(field => {
474 if (ts.ccvmFilters[field][0] !== '') {
475 str += ' ' + field + '(' + ts.ccvmFilters[field] + ')';
479 ts.facetFilters.forEach(f => {
480 str += ' ' + f.facetClass + '|'
481 + f.facetName + '[' + f.facetValue + ']';