From: Bill Erickson Date: Mon, 10 Dec 2018 16:53:44 +0000 (-0500) Subject: LP#1806087 Move browse to tab; context segregation X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=8ac3616f59b47c1363b775fbada1f83fb9883a04;p=working%2FEvergreen.git LP#1806087 Move browse to tab; context segregation Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts index 831c5d3ecc..42432ee2a0 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts @@ -1,7 +1,8 @@ import {Injectable} from '@angular/core'; import {ParamMap} from '@angular/router'; import {OrgService} from '@eg/core/org.service'; -import {CatalogSearchContext, FacetFilter} from './search-context'; +import {CatalogSearchContext, CatalogBrowseContext, CatalogMarcContext, + CatalogTermContext, FacetFilter} from './search-context'; import {CATALOG_CCVM_FILTERS} from './catalog.service'; @Injectable() @@ -19,35 +20,22 @@ export class CatalogUrlService { toUrlParams(context: CatalogSearchContext): {[key: string]: string | string[]} { - const params = { - query: [], - fieldClass: [], - joinOp: [], - matchOp: [], - facets: [], - identQuery: null, - identQueryType: null, - org: null, - limit: null, - offset: null, - copyLocations: null, - browsePivot: null, - hasBrowseEntry: null, - marcTag: [''], - marcSubfield: [''], - marcValue: [''] - }; - - params.org = context.searchOrg.id(); - - params.limit = context.pager.limit; + const params: any = {}; + + if (context.searchOrg) { + params.org = context.searchOrg.id(); + } + + if (context.pager.limit) { + params.limit = context.pager.limit; + } + if (context.pager.offset) { params.offset = context.pager.offset; } // These fields can be copied directly into place - ['format', 'sort', 'available', 'global', 'identQuery', - 'identQueryType', 'basket', 'browsePivot', 'hasBrowseEntry'] + ['limit', 'offset', 'sort', 'global', 'showBasket', 'sort'] .forEach(field => { if (context[field]) { // Only propagate applied values to the URL. @@ -55,48 +43,87 @@ export class CatalogUrlService { } }); - if (params.identQuery) { - // Ident queries (e.g. tcn search) discards all remaining filters - return params; - } else { - // Avoid propagating the type when it's not used. - delete params.identQueryType; + if (context.marcSearch.isSearchable()) { + const ms = context.marcSearch; + params.marcTag = []; + params.marcSubfield = []; + params.marcValue = []; + + ms.values.forEach((val, idx) => { + if (val !== '') { + params.marcTag.push(ms.tags[idx]); + params.marcSubfield.push(ms.subfields[idx]); + params.marcValue.push(ms.values[idx]); + } + }); } - context.query.filter(q => q !== '').forEach((q, idx) => { - ['query', 'fieldClass', 'joinOp', 'matchOp'].forEach(field => { - // Propagate all array-based fields regardless of - // whether a value is applied to ensure correct - // correlation between values - params[field][idx] = context[field][idx]; - }); - }); + if (context.identSearch.isSearchable()) { + params.identQuery = context.identSearch.value; + params.identQueryType = context.identSearch.queryType; + } - context.marcValue.filter(v => v !== '').forEach((val, idx) => { - ['marcValue', 'marcTag', 'marcSubfield'].forEach(field => { - params[field][idx] = context[field][idx]; - }); - }); + if (context.browseSearch.isSearchable()) { + params.browseTerm = context.browseSearch.value; + params.browseClass = context.browseSearch.fieldClass; + if (context.browseSearch.pivot) { + params.browsePivot = context.browseSearch.pivot; + } + } + + if (context.termSearch.isSearchable()) { + + const ts = context.termSearch; - // CCVM filters are encoded as comma-separated lists - Object.keys(context.ccvmFilters).forEach(code => { - if (context.ccvmFilters[code] && - context.ccvmFilters[code][0] !== '') { - params[code] = context.ccvmFilters[code].join(','); + params.query = []; + params.fieldClass = []; + params.joinOp = []; + params.matchOp = []; + + if (ts.format) { + params.format = ts.format; } - }); - // Each facet is a JSON encoded blob of class, name, and value - context.facetFilters.forEach(facet => { - params.facets.push(JSON.stringify({ - c : facet.facetClass, - n : facet.facetName, - v : facet.facetValue - })); - }); + if (ts.available) { + params.available = ts.available; + } + + if (ts.hasBrowseEntry) { + params.hasBrowseEntry = ts.hasBrowseEntry; + } + + ts.query.forEach((val, idx) => { + if (val !== '') { + params.query.push(ts.query[idx]); + params.fieldClass.push(ts.fieldClass[idx]); + params.joinOp.push(ts.joinOp[idx]); + params.matchOp.push(ts.matchOp[idx]); + } + }); - if (context.copyLocations.length && context.copyLocations[0] !== '') { - params.copyLocations = context.copyLocations.join(','); + // CCVM filters are encoded as comma-separated lists + Object.keys(ts.ccvmFilters).forEach(code => { + if (ts.ccvmFilters[code] && + ts.ccvmFilters[code][0] !== '') { + params[code] = ts.ccvmFilters[code].join(','); + } + }); + + // Each facet is a JSON encoded blob of class, name, and value + if (ts.facetFilters.length) { + params.facets = []; + ts.facetFilters.forEach(facet => { + params.facets.push(JSON.stringify({ + c : facet.facetClass, + n : facet.facetName, + v : facet.facetValue + })); + }); + } + + if (ts.copyLocations.length && ts.copyLocations[0] !== '') { + params.copyLocations = ts.copyLocations.join(','); + } } return params; @@ -117,59 +144,92 @@ export class CatalogUrlService { // Reset query/filter args. The will be reconstructed below. context.reset(); + let val; - // These fields can be copied directly into place - ['format', 'sort', 'available', 'global', 'identQuery', - 'identQueryType', 'basket', 'browsePivot', 'hasBrowseEntry'] - .forEach(field => { - const val = params.get(field); - if (val !== null) { - context[field] = val; - } - }); + if (params.get('org')) { + context.searchOrg = this.org.get(+params.get('org')); + } + + if (val = params.get('limit')) { + context.pager.limit = +val; + } + + if (val = params.get('offset')) { + context.pager.offset = +val; + } + + if (val = params.get('sort')) { + context.sort = val; + } + + if (val = params.get('global')) { + context.global = val; + } + + if (val = params.get('showBasket')) { + context.showBasket = val; + } - if (params.get('limit')) { - context.pager.limit = +params.get('limit'); + if (params.get('marcValue')) { + context.marcSearch.tags = params.getAll('marcTag'); + context.marcSearch.subfields = params.getAll('marcSubfield'); + context.marcSearch.values = params.getAll('marcValue'); } - if (params.get('offset')) { - context.pager.offset = +params.get('offset'); + if (params.get('identQuery')) { + context.identSearch.value = params.get('identQuery'); + context.identSearch.queryType = params.get('identQueryType'); } - ['query', 'fieldClass', 'joinOp', 'matchOp'].forEach(field => { - const arr = params.getAll(field); - if (arr && arr.length) { - context[field] = arr; + if (params.get('browseTerm')) { + context.browseSearch.value = params.get('browseTerm'); + context.browseSearch.fieldClass = params.get('browseClass'); + if (params.has('browsePivot')) { + context.browseSearch.pivot = +params.get('browsePivot'); } - }); + } + + const ts = context.termSearch; - ['marcValue', 'marcTag', 'marcSubfield'].forEach(field => { - const arr = params.getAll(field); - if (arr && arr.length) { - context[field] = arr; + if (params.has('hasBrowseEntry')) { + ts.hasBrowseEntry = params.get('hasBrowseEntry'); + + } else if (params.has('query')) { + + if (params.has('format')) { + ts.format = params.get('format'); } - }); - CATALOG_CCVM_FILTERS.forEach(code => { - const val = params.get(code); - if (val) { - context.ccvmFilters[code] = val.split(/,/); - } else { - context.ccvmFilters[code] = ['']; + if (params.get('available')) { + ts.available = Boolean(params.get('available')); } - }); - params.getAll('facets').forEach(blob => { - const facet = JSON.parse(blob); - context.addFacet(new FacetFilter(facet.c, facet.n, facet.v)); - }); + ['query', 'fieldClass', 'joinOp', 'matchOp'].forEach(field => { + const arr = params.getAll(field); + if (params.has(field)) { + ts[field] = params.getAll(field); // array + } + }); - if (params.get('org')) { - context.searchOrg = this.org.get(+params.get('org')); - } + CATALOG_CCVM_FILTERS.forEach(code => { + const val = params.get(code); + if (val) { + ts.ccvmFilters[code] = val.split(/,/); + } else { + ts.ccvmFilters[code] = ['']; + } + }); + + params.getAll('facets').forEach(blob => { + const facet = JSON.parse(blob); + ts.addFacet(new FacetFilter(facet.c, facet.n, facet.v)); + }); - if (params.get('copyLocations')) { - context.copyLocations = params.get('copyLocations').split(/,/); + if (params.get('copyLocations')) { + ts.copyLocations = params.get('copyLocations').split(/,/); + } } } } + + diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts index ccf928441f..22a6744276 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts @@ -59,11 +59,12 @@ export class CatalogService { search(ctx: CatalogSearchContext): Promise { ctx.searchState = CatalogSearchState.SEARCHING; - if (ctx.basket) { + if (ctx.showBasket) { return this.basketSearch(ctx); - } else if (ctx.marcValue[0] !== '') { + } else if (ctx.marcSearch.isSearchable()) { return this.marcSearch(ctx); - } else if (ctx.identQueryType === 'item_barcode') { + } else if (ctx.identSearch.isSearchable() && + ctx.identSearch.queryType === 'item_barcode') { return this.barcodeSearch(ctx); } else { return this.querySearch(ctx); @@ -74,7 +75,7 @@ export class CatalogService { return this.net.request( 'open-ils.search', 'open-ils.search.multi_home.bib_ids.by_barcode', - ctx.identQuery + ctx.identSearch.value ).toPromise().then(ids => { const result = { count: ids.length, @@ -110,7 +111,7 @@ export class CatalogService { let method = 'open-ils.search.biblio.marc'; if (ctx.isStaff) { method += '.staff'; } - const queryStruct = ctx.compileMarcSearch(); + const queryStruct = ctx.compileMarcSearchArgs(); return this.net.request('open-ils.search', method, queryStruct) .toPromise().then(result => { @@ -124,7 +125,14 @@ export class CatalogService { } querySearch(ctx: CatalogSearchContext): Promise { - const fullQuery = ctx.compileSearch(); + let fullQuery; + + if (ctx.identSearch.isSearchable()) { + console.log('IDENT IS SEARCHABLE'); + fullQuery = ctx.compileIdentSearchQuery(); + } else { + fullQuery = ctx.compileTermSearchQuery(); + } console.debug(`search query: ${fullQuery}`); @@ -190,6 +198,10 @@ export class CatalogService { return Promise.reject('Cannot fetch facets without results'); } + if (!ctx.result.facet_key) { + return Promise.resolve(); + } + if (this.lastFacetKey === ctx.result.facet_key) { ctx.result.facetData = this.lastFacetData; return Promise.resolve(); @@ -307,6 +319,7 @@ export class CatalogService { browse(ctx: CatalogSearchContext): Observable { ctx.searchState = CatalogSearchState.SEARCHING; + const bs = ctx.browseSearch; let method = 'open-ils.search.browse'; if (ctx.isStaff) { @@ -316,10 +329,10 @@ export class CatalogService { return this.net.request( 'open-ils.search', 'open-ils.search.browse.staff', { - browse_class: ctx.fieldClass[0], - term: ctx.query[0], + browse_class: bs.fieldClass, + term: bs.value, limit : ctx.pager.limit, - pivot: ctx.browsePivot, + pivot: bs.pivot, org_unit: ctx.searchOrg.id() } ).pipe(tap(result => { diff --git a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts index b1b7e7a6a9..51aa72d56c 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts @@ -29,40 +29,156 @@ export class FacetFilter { } } -// Not an angular service. -// It's conceviable there could be multiple contexts. -export class CatalogSearchContext { +export class CatalogSearchResults { + ids: number[]; + count: number; + [misc: string]: any; - // Search options and filters - available = false; - global = false; - sort: string; + constructor() { + this.ids = []; + this.count = 0; + } +} + +export class CatalogBrowseContext { + value: string; + pivot: number; + fieldClass: string; + + reset() { + this.value = ''; + this.pivot = null; + this.fieldClass = 'title'; + } + + isSearchable(): boolean { + return ( + this.value !== '' && + this.fieldClass !== '' + ); + } +} + +export class CatalogMarcContext { + tags: string[]; + subfields: string[]; + values: string[]; + + reset() { + this.tags = ['']; + this.values = ['']; + this.subfields = ['']; + } + + isSearchable() { + return ( + this.tags[0] !== '' && + this.values[0] !== '' + ); + } + +} + +export class CatalogIdentContext { + value: string; + queryType: string; + + reset() { + this.value = ''; + this.queryType = ''; + } + + isSearchable() { + return ( + this.value !== '' + && this.queryType !== '' + ); + } + +} + +export class CatalogTermContext { fieldClass: string[]; query: string[]; - identQuery: string; - identQueryType: string; // isbn, issn, etc. joinOp: string[]; matchOp: string[]; format: string; - searchOrg: IdlObject; + available = false; ccvmFilters: {[ccvmCode: string]: string[]}; facetFilters: FacetFilter[]; - isStaff: boolean; - basket = false; copyLocations: string[]; // ID's, but treated as strings in the UI. - browsePivot: number; - hasBrowseEntry: string; // "entryId,fieldId" - marcTag: string[]; - marcSubfield: string[]; - marcValue: string[]; isMetarecord: boolean; // TODO + hasBrowseEntry: string; // "entryId,fieldId" + + reset() { + this.query = ['']; + this.fieldClass = ['keyword']; + this.matchOp = ['contains']; + this.joinOp = ['']; + this.ccvmFilters = {}; + this.facetFilters = []; + this.copyLocations = ['']; + this.format = ''; + this.hasBrowseEntry = ''; + } + + isSearchable(): boolean { + return ( + this.query[0] !== '' + || this.hasBrowseEntry !== '' + ); + } + + hasFacet(facet: FacetFilter): boolean { + return Boolean( + this.facetFilters.filter(f => f.equals(facet))[0] + ); + } + + removeFacet(facet: FacetFilter): void { + this.facetFilters = this.facetFilters.filter(f => !f.equals(facet)); + } + + addFacet(facet: FacetFilter): void { + if (!this.hasFacet(facet)) { + this.facetFilters.push(facet); + } + } + + toggleFacet(facet: FacetFilter): void { + if (this.hasFacet(facet)) { + this.removeFacet(facet); + } else { + this.facetFilters.push(facet); + } + } + +} + + + +// Not an angular service. +// It's conceviable there could be multiple contexts. +export class CatalogSearchContext { + + // Attributes that are used across different contexts. + sort: string; + isStaff: boolean; + showBasket: boolean; + searchOrg: IdlObject; + global: boolean; + + termSearch: CatalogTermContext; + marcSearch: CatalogMarcContext; + identSearch: CatalogIdentContext; + browseSearch: CatalogBrowseContext; // Result from most recent search. - result: any = {}; + result: CatalogSearchResults; searchState: CatalogSearchState = CatalogSearchState.PENDING; // List of IDs in page/offset context. - resultIds: number[] = []; + resultIds: number[]; // Utility stuff pager: Pager; @@ -70,9 +186,40 @@ export class CatalogSearchContext { constructor() { this.pager = new Pager(); + this.termSearch = new CatalogTermContext(); + this.marcSearch = new CatalogMarcContext(); + this.identSearch = new CatalogIdentContext(); + this.browseSearch = new CatalogBrowseContext(); this.reset(); } + /** + * Return search context to its default state, resetting search + * parameters and clearing any cached result data. + */ + reset(): void { + this.pager.offset = 0; + this.sort = ''; + this.showBasket = false; + this.result = new CatalogSearchResults(); + this.resultIds = []; + this.searchState = CatalogSearchState.PENDING; + this.termSearch.reset(); + this.marcSearch.reset(); + this.identSearch.reset(); + this.browseSearch.reset(); + } + + isSearchable(): boolean { + return ( + this.showBasket || + this.termSearch.isSearchable() || + this.marcSearch.isSearchable() || + this.identSearch.isSearchable() || + this.browseSearch.isSearchable() + ); + } + // List of result IDs for the current page of data. currentResultIds(): number[] { const ids = []; @@ -105,95 +252,20 @@ export class CatalogSearchContext { return null; } - /** - * Return search context to its default state, resetting search - * parameters and clearing any cached result data. - * This does not reset global filters like limit-to-available - * search-global, or search-org. - */ - reset(): void { - this.pager.offset = 0; - this.format = ''; - this.sort = ''; - this.query = ['']; - this.identQuery = null; - this.identQueryType = 'identifier|isbn'; - this.fieldClass = ['keyword']; - this.matchOp = ['contains']; - this.joinOp = ['']; - this.ccvmFilters = {}; - this.facetFilters = []; - this.result = {}; - this.resultIds = []; - this.searchState = CatalogSearchState.PENDING; - this.basket = false; - this.copyLocations = ['']; - this.marcTag = ['']; - this.marcSubfield = ['']; - this.marcValue = ['']; - } - - // Returns true if we have enough information to perform a search. - isSearchable(): boolean { - return this.searchType() !== null; - } - - // Returns the type of search that would be performed from this - // context object if a search were run now. - // Returns NULL if no search is possible. - searchType(): string { - - if (this.basket) { - return 'basket'; - } - - if (this.identQuery && this.identQueryType) { - return 'ident'; - } - - if (this.marcTag[0] !== '' && this.marcValue[0] !== '') { - // MARC field search - return 'marc'; - } - - // searchOrg required for all following search scenarios - if (this.searchOrg === null) { - return null; - } - - if (this.hasBrowseEntry) { - // Limit results by records that link to browse entry - return 'browse'; - } - - // Query search - if (this.query.length && this.query[0] !== '') { - return 'query'; - } - - return null; - } - - // Returns true if we have enough information to perform a browse. - isBrowsable(): boolean { - return this.fieldClass.length - && this.fieldClass[0] !== '' - && this.query.length - && this.query[0] !== '' - && this.searchOrg !== null; - } - - compileMarcSearch(): any { + compileMarcSearchArgs(): any { const searches: any = []; - - this.marcValue.filter(v => v !== '').forEach((val, idx) => { - searches.push({ - restrict: [{ - subfield: this.marcSubfield[idx], - tag: this.marcTag[idx] - }], - term: this.marcValue[idx] - }); + const ms = this.marcSearch; + + ms.values.forEach((val, idx) => { + if (val !== '') { + searches.push({ + restrict: [{ + subfield: ms.subfields[idx], + tag: ms.tags[idx] + }], + term: ms.values[idx] + }); + } }); const args: any = { @@ -212,94 +284,20 @@ export class CatalogSearchContext { return args; } - compileSearch(): string { - let str = ''; - - if (this.available) { - str += '#available'; - } - - if (this.sort) { - // e.g. title, title.descending - const parts = this.sort.split(/\./); - if (parts[1]) { str += ' #descending'; } - str += ' sort(' + parts[0] + ')'; - } - - if (this.identQuery && this.identQueryType) { - if (str) { str += ' '; } - str += this.identQueryType + ':' + this.identQuery; - - } else { - - // ------- - // Compile boolean sub-query components - if (str.length) { str += ' '; } - const qcount = this.query.length; - - // if we multiple boolean query components, wrap them in parens. - if (qcount > 1) { str += '('; } - this.query.forEach((q, idx) => { - str += this.compileBoolQuerySet(idx); - }); - if (qcount > 1) { str += ')'; } - // ------- - } - - if (this.hasBrowseEntry) { - // stored as a comma-separated string of "entryId,fieldId" - str += ` has_browse_entry(${this.hasBrowseEntry})`; - } - - if (this.format) { - str += ' format(' + this.format + ')'; - } - - if (this.global) { - str += ' depth(' + - this.org.root().ou_type().depth() + ')'; - } - - if (this.copyLocations[0] !== '') { - str += ' locations(' + this.copyLocations + ')'; - } - - str += ' site(' + this.searchOrg.shortname() + ')'; + compileIdentSearchQuery(): string { - Object.keys(this.ccvmFilters).forEach(field => { - if (this.ccvmFilters[field][0] !== '') { - str += ' ' + field + '(' + this.ccvmFilters[field] + ')'; - } - }); - - this.facetFilters.forEach(f => { - str += ' ' + f.facetClass + '|' - + f.facetName + '[' + f.facetValue + ']'; - }); - - return str; + let str = ' site(' + this.searchOrg.shortname() + ')'; + return str + ' ' + + this.identSearch.queryType + ':' + this.identSearch.value; } - stripQuotes(query: string): string { - return query.replace(/"/g, ''); - } - - stripAnchors(query: string): string { - return query.replace(/[\^\$]/g, ''); - } - - addQuotes(query: string): string { - if (query.match(/ /)) { - return '"' + query + '"'; - } - return query; - } compileBoolQuerySet(idx: number): string { - let query = this.query[idx]; - const joinOp = this.joinOp[idx]; - const matchOp = this.matchOp[idx]; - const fieldClass = this.fieldClass[idx]; + const ts = this.termSearch; + let query = ts.query[idx]; + const joinOp = ts.joinOp[idx]; + const matchOp = ts.matchOp[idx]; + const fieldClass = ts.fieldClass[idx]; let str = ''; if (!query) { return str; } @@ -328,29 +326,81 @@ export class CatalogSearchContext { return str + query + ')'; } - hasFacet(facet: FacetFilter): boolean { - return Boolean( - this.facetFilters.filter(f => f.equals(facet))[0] - ); + stripQuotes(query: string): string { + return query.replace(/"/g, ''); } - removeFacet(facet: FacetFilter): void { - this.facetFilters = this.facetFilters.filter(f => !f.equals(facet)); + stripAnchors(query: string): string { + return query.replace(/[\^\$]/g, ''); } - addFacet(facet: FacetFilter): void { - if (!this.hasFacet(facet)) { - this.facetFilters.push(facet); + addQuotes(query: string): string { + if (query.match(/ /)) { + return '"' + query + '"'; } + return query; } - toggleFacet(facet: FacetFilter): void { - if (this.hasFacet(facet)) { - this.removeFacet(facet); - } else { - this.facetFilters.push(facet); + compileTermSearchQuery(): string { + const ts = this.termSearch; + let str = ''; + + if (ts.available) { + str += '#available'; } + + if (this.sort) { + // e.g. title, title.descending + const parts = this.sort.split(/\./); + if (parts[1]) { str += ' #descending'; } + str += ' sort(' + parts[0] + ')'; + } + + // ------- + // Compile boolean sub-query components + if (str.length) { str += ' '; } + const qcount = ts.query.length; + + // if we multiple boolean query components, wrap them in parens. + if (qcount > 1) { str += '('; } + ts.query.forEach((q, idx) => { + str += this.compileBoolQuerySet(idx); + }); + if (qcount > 1) { str += ')'; } + // ------- + + if (ts.hasBrowseEntry) { + // stored as a comma-separated string of "entryId,fieldId" + str += ` has_browse_entry(${ts.hasBrowseEntry})`; + } + + if (ts.format) { + str += ' format(' + ts.format + ')'; + } + + if (this.global) { + str += ' depth(' + + this.org.root().ou_type().depth() + ')'; + } + + if (ts.copyLocations[0] !== '') { + str += ' locations(' + ts.copyLocations + ')'; + } + + str += ' site(' + this.searchOrg.shortname() + ')'; + + Object.keys(ts.ccvmFilters).forEach(field => { + if (ts.ccvmFilters[field][0] !== '') { + str += ' ' + field + '(' + ts.ccvmFilters[field] + ')'; + } + }); + + ts.facetFilters.forEach(f => { + str += ' ' + f.facetClass + '|' + + f.facetName + '[' + f.facetValue + ']'; + }); + + return str; } } - diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/basket-actions.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/basket-actions.component.html index 86d8099bb1..5837acee84 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/basket-actions.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/basket-actions.component.html @@ -5,7 +5,7 @@
- shopping_basket ({{basketCount()}}) diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/browse.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/browse.component.html index 8412143d2e..97742f7da0 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/browse.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/browse.component.html @@ -1,5 +1,5 @@ - + diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/browse/form.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/browse/form.component.html deleted file mode 100644 index 6dba2508cd..0000000000 --- a/Open-ILS/src/eg2/src/app/staff/catalog/browse/form.component.html +++ /dev/null @@ -1,36 +0,0 @@ -
-
- - - - - - - - -
-
-
- -
-
-
- diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/browse/form.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/browse/form.component.ts deleted file mode 100644 index b9c4c8ef4a..0000000000 --- a/Open-ILS/src/eg2/src/app/staff/catalog/browse/form.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {Component, OnInit, AfterViewInit, Renderer2} from '@angular/core'; -import {Router} from '@angular/router'; -import {IdlObject} from '@eg/core/idl.service'; -import {OrgService} from '@eg/core/org.service'; -import {CatalogService} from '@eg/share/catalog/catalog.service'; -import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context'; -import {StaffCatalogService} from '../catalog.service'; - -@Component({ - selector: 'eg-catalog-browse-form', - templateUrl: 'form.component.html' -}) -export class BrowseFormComponent implements OnInit, AfterViewInit { - - searchContext: CatalogSearchContext; - ccvmMap: {[ccvm: string]: IdlObject[]} = {}; - cmfMap: {[cmf: string]: IdlObject} = {}; - - constructor( - private renderer: Renderer2, - private router: Router, - private org: OrgService, - private cat: CatalogService, - private staffCat: StaffCatalogService - ) { - } - - ngOnInit() { - this.ccvmMap = this.cat.ccvmMap; - this.cmfMap = this.cat.cmfMap; - this.searchContext = this.staffCat.searchContext; - } - - ngAfterViewInit() { - this.renderer.selectRootElement('#browse-term-input').focus(); - } - - orgName(orgId: number): string { - return this.org.get(orgId).shortname(); - } - - formEnter(source) { - this.searchContext.pager.offset = 0; - this.browseByForm(); - } - - browseByForm(): void { - this.staffCat.browse(); - } - - searchIsActive(): boolean { - return this.searchContext.searchState === CatalogSearchState.SEARCHING; - } - - goToSearch() { - this.router.navigate(['/staff/catalog/search']); - } - - orgOnChange = (org: IdlObject): void => { - this.searchContext.searchOrg = org; - } -} - - diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts index f706cd50c8..8fcbce1e65 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/browse/results.component.ts @@ -38,14 +38,15 @@ export class BrowseResultsComponent implements OnInit { browseByUrl(params: ParamMap): void { this.catUrl.applyUrlParams(this.searchContext, params); + const bs = this.searchContext.browseSearch; // SearchContext applies a default fieldClass value of 'keyword'. // Replace with 'title', since there is no 'keyword' browse. - if (this.searchContext.fieldClass[0] === 'keyword') { - this.searchContext.fieldClass = ['title']; + if (bs.fieldClass === 'keyword') { + bs.fieldClass = 'title'; } - if (this.searchContext.isBrowsable()) { + if (bs.isSearchable()) { this.results = []; this.cat.browse(this.searchContext) .subscribe(result => this.addResult(result)) @@ -105,7 +106,7 @@ export class BrowseResultsComponent implements OnInit { prevPage() { const firstResult = this.results[0]; if (firstResult) { - this.searchContext.browsePivot = firstResult.pivot_point; + this.searchContext.browseSearch.pivot = firstResult.pivot_point; this.staffCat.browse(); } } @@ -113,17 +114,17 @@ export class BrowseResultsComponent implements OnInit { nextPage() { const lastResult = this.results[this.results.length - 1]; if (lastResult) { - this.searchContext.browsePivot = lastResult.pivot_point; + this.searchContext.browseSearch.pivot = lastResult.pivot_point; this.staffCat.browse(); } } searchByBrowseEntry(result) { - // avoid propagating the browse query to the search form - this.searchContext.query[0] = ''; + // Avoid propagating browse values to term search. + this.searchContext.browseSearch.reset(); - this.searchContext.hasBrowseEntry = + this.searchContext.termSearch.hasBrowseEntry = result.browse_entry + ',' + result.fields; this.staffCat.search(); } @@ -131,7 +132,7 @@ export class BrowseResultsComponent implements OnInit { // NOTE: to test unauthorized heading display in concerto // browse for author = kab newBrowseFromHeading(heading) { - this.searchContext.query[0] = heading.heading; + this.searchContext.browseSearch.value = heading.heading; this.staffCat.browse(); } } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts index b083f67350..2d30199441 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts @@ -20,7 +20,6 @@ import {HoldService} from '@eg/staff/share/hold.service'; import {PartsComponent} from './record/parts.component'; import {PartMergeDialogComponent} from './record/part-merge-dialog.component'; import {BrowseComponent} from './browse.component'; -import {BrowseFormComponent} from './browse/form.component'; import {BrowseResultsComponent} from './browse/results.component'; @NgModule({ @@ -40,7 +39,6 @@ import {BrowseResultsComponent} from './browse/results.component'; PartsComponent, PartMergeDialogComponent, BrowseComponent, - BrowseFormComponent, BrowseResultsComponent ], imports: [ diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts index 681e159e81..cf0a36c97f 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts @@ -88,7 +88,7 @@ export class StaffCatalogService { * execute the actual browse. */ browse(): void { - if (!this.searchContext.isBrowsable()) { return; } + if (!this.searchContext.browseSearch.isSearchable()) { return; } const params = this.catUrl.toUrlParams(this.searchContext); diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts index 44583b8780..f16215a65f 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts @@ -35,11 +35,11 @@ export class ResultFacetsComponent implements OnInit { } facetIsApplied(cls: string, name: string, value: string): boolean { - return this.searchContext.hasFacet(new FacetFilter(cls, name, value)); + return this.searchContext.termSearch.hasFacet(new FacetFilter(cls, name, value)); } applyFacet(cls: string, name: string, value: string): void { - this.searchContext.toggleFacet(new FacetFilter(cls, name, value)); + this.searchContext.termSearch.toggleFacet(new FacetFilter(cls, name, value)); this.searchContext.pager.offset = 0; this.staffCat.search(); } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts index b05d6b436e..5224c389d0 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts @@ -68,8 +68,8 @@ export class ResultRecordComponent implements OnInit, OnDestroy { searchAuthor(summary: any) { this.searchContext.reset(); - this.searchContext.fieldClass = ['author']; - this.searchContext.query = [summary.display.author]; + this.searchContext.termSearch.fieldClass = ['author']; + this.searchContext.termSearch.query = [summary.display.author]; this.staffCat.search(); } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html index 01bb3bdbbe..374df5b1dc 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html @@ -4,14 +4,14 @@ TODO focus search input
- +
+ *ngFor="let q of context.termSearch.query; let idx = index; trackBy:trackByIdx">
- @@ -19,7 +19,7 @@ TODO focus search input
@@ -27,7 +27,7 @@ TODO focus search input
+ [(ngModel)]="context.termSearch.matchOp[idx]"> @@ -51,13 +51,13 @@ TODO focus search input
@@ -70,7 +70,7 @@ TODO focus search input add_circle_outline
- @@ -106,21 +106,21 @@ TODO focus search input
@@ -128,7 +128,7 @@ TODO focus search input
+ [(ngModel)]="context.termSearch.ccvmFilters.item_form"> @@ -144,7 +144,7 @@ TODO focus search input
+ [(ngModel)]="context.termSearch.ccvmFilters.audience" multiple="true"> @@ -162,7 +162,7 @@ TODO focus search input
+ [(ngModel)]="context.termSearch.ccvmFilters.bib_level" multiple="true"> @@ -178,7 +178,7 @@ TODO focus search input
+ [(ngModel)]="context.termSearch.copyLocations" multiple="true">
@@ -230,60 +227,82 @@ TODO focus search input
+ *ngFor="let q of context.marcSearch.values; let idx = index; trackBy:trackByIdx"> -
+ + +
+
+ + + + +
+
+
+
-
- - - - - +
+
+
+ + + + +
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts index fbe92b8301..1944a3c3e9 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts @@ -14,13 +14,12 @@ import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap'; }) export class SearchFormComponent implements OnInit, AfterViewInit { - searchContext: CatalogSearchContext; + context: CatalogSearchContext; ccvmMap: {[ccvm: string]: IdlObject[]} = {}; cmfMap: {[cmf: string]: IdlObject} = {}; showSearchFilters = false; copyLocations: IdlObject[]; searchTab: string; - //@ViewChild('searchTabs') searchTabs: NgbTabset; constructor( private renderer: Renderer2, @@ -36,7 +35,7 @@ export class SearchFormComponent implements OnInit, AfterViewInit { ngOnInit() { this.ccvmMap = this.cat.ccvmMap; this.cmfMap = this.cat.cmfMap; - this.searchContext = this.staffCat.searchContext; + this.context = this.staffCat.searchContext; // Start with advanced search options open // if any filters are active. @@ -50,15 +49,20 @@ export class SearchFormComponent implements OnInit, AfterViewInit { // Avoid changing the tab in the lifecycle hook thread. setTimeout(() => { - const st = this.searchContext.searchType(); - if (st === 'marc' || st === 'ident') { - this.searchTab = st; + // Assumes that only one type of search will be searchable + // at any given time. + if (this.context.marcSearch.isSearchable()) { + this.searchTab = 'marc'; + } else if (this.context.identSearch.isSearchable()) { + this.searchTab = 'ident'; + } else if (this.context.browseSearch.isSearchable()) { + this.searchTab = 'browse'; } else { + // Default tab this.searchTab = 'term'; + this.refreshCopyLocations(); } }); - - this.refreshCopyLocations(); } onTabChange(evt: NgbTabChangeEvent) { @@ -73,7 +77,11 @@ export class SearchFormComponent implements OnInit, AfterViewInit { case 'marc': selector = '#first-marc-tag'; break; + case 'browse': + selector = '#browse-term-input'; + break; default: + this.refreshCopyLocations(); selector = '#first-query-input'; } @@ -98,13 +106,13 @@ export class SearchFormComponent implements OnInit, AfterViewInit { filtersActive(): boolean { - if (this.searchContext.copyLocations[0] !== '') { return true; } + if (this.context.termSearch.copyLocations[0] !== '') { return true; } // ccvm filters may be present without any filters applied. // e.g. if filters were applied then removed. let show = false; - Object.keys(this.searchContext.ccvmFilters).forEach(ccvm => { - if (this.searchContext.ccvmFilters[ccvm][0] !== '') { + Object.keys(this.context.termSearch.ccvmFilters).forEach(ccvm => { + if (this.context.termSearch.ccvmFilters[ccvm][0] !== '') { show = true; } }); @@ -113,7 +121,7 @@ export class SearchFormComponent implements OnInit, AfterViewInit { } orgOnChange = (org: IdlObject): void => { - this.searchContext.searchOrg = org; + this.context.searchOrg = org; this.refreshCopyLocations(); } @@ -121,7 +129,7 @@ export class SearchFormComponent implements OnInit, AfterViewInit { if (!this.showFilters()) { return; } // TODO: is this how we avoid displaying too many locations? - const org = this.searchContext.searchOrg; + const org = this.context.searchOrg; if (org.id() === this.org.root().id()) { this.copyLocations = []; return; @@ -137,71 +145,69 @@ export class SearchFormComponent implements OnInit, AfterViewInit { } addSearchRow(index: number): void { - this.searchContext.query.splice(index, 0, ''); - this.searchContext.fieldClass.splice(index, 0, 'keyword'); - this.searchContext.joinOp.splice(index, 0, '&&'); - this.searchContext.matchOp.splice(index, 0, 'contains'); + this.context.termSearch.query.splice(index, 0, ''); + this.context.termSearch.fieldClass.splice(index, 0, 'keyword'); + this.context.termSearch.joinOp.splice(index, 0, '&&'); + this.context.termSearch.matchOp.splice(index, 0, 'contains'); } delSearchRow(index: number): void { - this.searchContext.query.splice(index, 1); - this.searchContext.fieldClass.splice(index, 1); - this.searchContext.joinOp.splice(index, 1); - this.searchContext.matchOp.splice(index, 1); + this.context.termSearch.query.splice(index, 1); + this.context.termSearch.fieldClass.splice(index, 1); + this.context.termSearch.joinOp.splice(index, 1); + this.context.termSearch.matchOp.splice(index, 1); } addMarcSearchRow(index: number): void { - this.searchContext.marcTag.splice(index, 0, ''); - this.searchContext.marcSubfield.splice(index, 0, ''); - this.searchContext.marcValue.splice(index, 0, ''); + this.context.marcSearch.tags.splice(index, 0, ''); + this.context.marcSearch.subfields.splice(index, 0, ''); + this.context.marcSearch.values.splice(index, 0, ''); } delMarcSearchRow(index: number): void { - this.searchContext.marcTag.splice(index, 1); - this.searchContext.marcSubfield.splice(index, 1); - this.searchContext.marcValue.splice(index, 1); + this.context.marcSearch.tags.splice(index, 1); + this.context.marcSearch.subfields.splice(index, 1); + this.context.marcSearch.values.splice(index, 1); } searchByForm(): void { - // Starting a new search - this.searchContext.pager.offset = 0; + this.context.pager.offset = 0; // New search - // Trim the search context to necessary data only depending - // on which tab the user is on, i.e. which type of search - // the user is requesting. - - // A form search overrides an existing basket display request - this.searchContext.basket = null; + // Form search overrides basket display + this.context.showBasket = false; switch (this.searchTab) { - case 'term': // main search form query input - - // Be sure a previous ident search does not take precedence - // over the new term query submitted via Enter within - // the search term/query box. - this.searchContext.marcValue[0] = ''; - this.searchContext.identQuery = null; + case 'term': // AKA keyword search + this.context.marcSearch.reset(); + this.context.browseSearch.reset(); + this.context.identSearch.reset(); + this.context.termSearch.hasBrowseEntry = ''; + this.staffCat.search(); break; - case 'ident': // identifier query input - const iq = this.searchContext.identQuery; - const qt = this.searchContext.identQueryType; - if (iq) { - // Ident queries ignore search-specific filters. - this.searchContext.reset(); - this.searchContext.identQuery = iq; - this.searchContext.identQueryType = qt; - } + case 'ident': + this.context.marcSearch.reset(); + this.context.browseSearch.reset(); + this.context.termSearch.reset(); + this.staffCat.search(); break; case 'marc': - this.searchContext.identQuery = null; - this.searchContext.query[0] = ''; // prevent term queries + this.context.browseSearch.reset(); + this.context.termSearch.reset(); + this.context.identSearch.reset(); + this.staffCat.search(); break; - } - this.staffCat.search(); + case 'browse': + this.context.marcSearch.reset(); + this.context.termSearch.reset(); + this.context.identSearch.reset(); + this.context.browseSearch.pivot = null; + this.staffCat.browse(); + break; + } } // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes @@ -209,9 +215,8 @@ export class SearchFormComponent implements OnInit, AfterViewInit { return index; } - searchIsActive(): boolean { - return this.searchContext.searchState === CatalogSearchState.SEARCHING; + return this.context.searchState === CatalogSearchState.SEARCHING; } goToBrowse() {