From 2b071513577f80004649b591d3c1511b828fde8d Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Fri, 29 Nov 2024 13:56:25 +0100 Subject: [PATCH 1/5] [upstream] fix webcomponent styling --- apps/webcomponents/src/styles.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webcomponents/src/styles.css b/apps/webcomponents/src/styles.css index 91f95522b1..ac00df17ac 100644 --- a/apps/webcomponents/src/styles.css +++ b/apps/webcomponents/src/styles.css @@ -1,3 +1,5 @@ +@import '../../../tailwind.base.css'; + @tailwind base; @tailwind components; @tailwind utilities; From 53fae9c79c998f9b2fa3cd9ddc700990c659fe72 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Fri, 6 Dec 2024 10:58:53 +0100 Subject: [PATCH 2/5] [upstream] feat(repository): include all fields if requestFields not specified --- .../elasticsearch.service.spec.ts | 31 +++++++++++++++++-- .../elasticsearch/elasticsearch.service.ts | 4 +-- ...rganizations-from-metadata.service.spec.ts | 1 - 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts index 7ad69a3fae..2f00683361 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts @@ -91,9 +91,32 @@ describe('ElasticsearchService', () => { }) describe('#getSearchRequestBody', () => { - describe('#track_total_hits', () => { + let payload + describe('request fields', () => { + it('includes the _source property if fields are specified', () => { + payload = service.getSearchRequestBody({}, 4, 0, null, ['uuid', 'tag']) + expect(payload).toEqual({ + _source: ['uuid', 'tag'], + from: 0, + size: 4, + query: expect.any(Object), + aggregations: expect.any(Object), + track_total_hits: true, + }) + }) + it('does not include the _source property if no field specified', () => { + payload = service.getSearchRequestBody({}, 4, 0, null, null) + expect(payload).toEqual({ + from: 0, + size: 4, + query: expect.any(Object), + aggregations: expect.any(Object), + track_total_hits: true, + }) + }) + }) + describe('track_total_hits', () => { let size = 0 - let payload describe('when size is 0', () => { beforeEach(() => { payload = service.getSearchRequestBody({}, size) @@ -554,7 +577,9 @@ describe('ElasticsearchService', () => { }) describe('#injectLangInQueryStringFields - Search language', () => { - let queryStringFields = { 'resourceTitleObject.${searchLang}': 1 } + let queryStringFields: Record = { + 'resourceTitleObject.${searchLang}': 1, + } describe('When no lang from config', () => { beforeEach(() => { service['metadataLang'] = undefined diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts index 21e6c330cd..e5861fba35 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -50,7 +50,7 @@ export class ElasticsearchService { size = 0, from = 0, sortBy: SortByField = null, - requestFields: RequestFields = [], + requestFields: RequestFields = null, searchFilters: SearchFilters = {}, configFilters: SearchFilters = {}, uuids?: string[], @@ -68,7 +68,7 @@ export class ElasticsearchService { geometry ), ...(size > 0 ? { track_total_hits: true } : {}), - _source: requestFields, + ...(requestFields && { _source: requestFields }), } this.processRuntimeFields(payload) return payload diff --git a/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts b/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts index ddb05655a1..16f5077b28 100644 --- a/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts @@ -285,7 +285,6 @@ describe.each(['4.2.2-00', '4.2.3-xx', '4.2.5-xx'])( filter: [{ terms: { isTemplate: ['n'] } }], }, }, - _source: [], }) ) }) From 162cf000576ecba68eb158240499feeef8b09fb5 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Fri, 6 Dec 2024 14:32:37 +0100 Subject: [PATCH 3/5] [upstream] feat(search): add availableServices field This needed some modifications in the ES code to generate queries --- .../elasticsearch.service.spec.ts | 49 ++++++++++-- .../elasticsearch/elasticsearch.service.ts | 21 ++--- .../src/lib/utils/service/fields.service.ts | 2 + .../src/lib/utils/service/fields.spec.ts | 79 ++++++++++++++++++- .../search/src/lib/utils/service/fields.ts | 55 +++++++++++++ 5 files changed, 186 insertions(+), 20 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts index 2f00683361..206c85d3b8 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts @@ -473,6 +473,37 @@ describe('ElasticsearchService', () => { }, }) }) + it('handle values expressed as reg exp', () => { + const query = service['buildPayloadQuery']( + { + Org: { + '/world.*/': true, + '/*country^[fr|en]/': false, + }, + }, + {}, + [] + ) + expect(query).toMatchObject({ + bool: { + filter: [ + { + terms: { + isTemplate: ['n'], + }, + }, + { + query_string: { + query: 'Org:(/world.*/ OR -/*country^[fr|en]/)', + }, + }, + { + ids: { values: [] }, + }, + ], + }, + }) + }) describe('any has special characters', () => { let query beforeEach(() => { @@ -920,14 +951,16 @@ describe('ElasticsearchService', () => { ).toStrictEqual({ myFilters: { filters: { - filter1: { - query_string: { query: 'field1:(100)' }, - }, - filter2: { - query_string: { query: 'field2:("value1" OR "value3")' }, - }, - filter3: { - query_string: { query: 'my own query' }, + filters: { + filter1: { + query_string: { query: 'field1:(100)' }, + }, + filter2: { + query_string: { query: 'field2:("value1" OR "value3")' }, + }, + filter3: { + query_string: { query: 'my own query' }, + }, }, }, }, diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts index e5861fba35..aa54c2f0c0 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -220,6 +220,7 @@ export class ElasticsearchService { private filtersToQuery( filters: FieldFilters | FiltersAggregationParams | string ): FilterQuery { + const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`) const makeQuery = (filter: FieldFilter): string => { if (typeof filter === 'string') { return filter @@ -227,9 +228,9 @@ export class ElasticsearchService { return Object.keys(filter) .map((key) => { if (filter[key] === true) { - return `"${key}"` + return addQuote(key) } - return `-"${key}"` + return `-${addQuote(key)}` }) .join(' OR ') } @@ -518,13 +519,15 @@ export class ElasticsearchService { switch (aggregation.type) { case 'filters': return { - filters: Object.keys(aggregation.filters).reduce((prev, curr) => { - const filter = aggregation.filters[curr] - return { - ...prev, - [curr]: this.filtersToQuery(filter)[0], - } - }, {}), + filters: { + filters: Object.keys(aggregation.filters).reduce((prev, curr) => { + const filter = aggregation.filters[curr] + return { + ...prev, + [curr]: this.filtersToQuery(filter)[0], + } + }, {}), + }, } case 'terms': return { diff --git a/libs/feature/search/src/lib/utils/service/fields.service.ts b/libs/feature/search/src/lib/utils/service/fields.service.ts index d79ed22684..84693022a7 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -1,6 +1,7 @@ import { Injectable, Injector } from '@angular/core' import { AbstractSearchField, + AvailableServicesField, DateRangeSearchField, FieldValue, FullTextSearchField, @@ -91,6 +92,7 @@ export class FieldsService { ), user: new UserSearchField(this.injector), changeDate: new DateRangeSearchField('changeDate', this.injector, 'desc'), + availableServices: new AvailableServicesField(this.injector), } as Record get supportedFields() { diff --git a/libs/feature/search/src/lib/utils/service/fields.spec.ts b/libs/feature/search/src/lib/utils/service/fields.spec.ts index 4d8ba4138d..d166c8d5a4 100644 --- a/libs/feature/search/src/lib/utils/service/fields.spec.ts +++ b/libs/feature/search/src/lib/utils/service/fields.spec.ts @@ -1,13 +1,14 @@ import { lastValueFrom, of } from 'rxjs' import { AbstractSearchField, + AvailableServicesField, FullTextSearchField, IsSpatialSearchField, - TranslatedSearchField, LicenseSearchField, + MultilingualSearchField, OrganizationSearchField, SimpleSearchField, - MultilingualSearchField, + TranslatedSearchField, UserSearchField, DateRangeSearchField, } from './fields' @@ -30,7 +31,6 @@ class ElasticsearchServiceMock { class RecordsRepositoryMock { aggregate = jest.fn((aggregations) => { const aggName = Object.keys(aggregations)[0] - const sortType = aggregations[aggName].sort[1] if (aggName.startsWith('is')) return of({ [aggName]: { @@ -119,6 +119,21 @@ class RecordsRepositoryMock { ], }, }) + if (aggName === 'availableServices') + return of({ + availableServices: { + buckets: [ + { + term: 'view', + count: 10, + }, + { + term: 'download', + count: 5, + }, + ], + }, + }) const buckets = [ { term: 'First value', @@ -137,6 +152,7 @@ class RecordsRepositoryMock { count: 1, }, ] + const sortType = aggregations[aggName].sort?.[1] if (sortType === 'count') { buckets.sort((a, b) => b.count - a.count) } @@ -775,6 +791,7 @@ describe('search fields implementations', () => { }) }) }) + describe('UserSearchField', () => { beforeEach(() => { searchField = new UserSearchField(injector) @@ -812,4 +829,60 @@ describe('search fields implementations', () => { }) }) }) + + describe('AvailableServicesField', () => { + beforeEach(() => { + searchField = new AvailableServicesField(injector) + }) + describe('#getAvailableValues', () => { + let values + beforeEach(async () => { + values = await lastValueFrom(searchField.getAvailableValues()) + }) + it('returns the available values', () => { + expect(values).toEqual([ + { + label: 'search.filters.availableServices.view (10)', + value: 'view', + }, + { + label: 'search.filters.availableServices.download (5)', + value: 'download', + }, + ]) + }) + }) + describe('#getFiltersForValues', () => { + let filter + beforeEach(async () => { + filter = await lastValueFrom( + searchField.getFiltersForValues(['view', 'download']) + ) + }) + it('returns filter for both values', () => { + expect(filter).toEqual({ + linkProtocol: { + '/OGC:WFS.*/': true, + '/OGC:WMT?S.*/': true, + }, + }) + }) + }) + describe('#getValuesForFilters', () => { + let values + beforeEach(async () => { + values = await lastValueFrom( + searchField.getValuesForFilter({ + linkProtocol: { + '/OGC:WFS.*/': false, + '/OGC:WMT?S.*/': true, + }, + }) + ) + }) + it('returns value with an enabled filter', () => { + expect(values).toEqual(['view']) + }) + }) + }) }) diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 1d8422ba03..e0e2797b87 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -9,6 +9,7 @@ import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform. import { AggregationBuckets, AggregationsParams, + FieldFilter, FieldFilterByExpression, FieldFilters, TermBucket, @@ -425,3 +426,57 @@ export class DateRangeSearchField extends SimpleSearchField { return 'dateRange' } } + +marker('search.filters.availableServices.view') +marker('search.filters.availableServices.download') + +export class AvailableServicesField extends SimpleSearchField { + private translateService = this.injector.get(TranslateService) + + constructor(injector: Injector) { + super('availableServices', injector, 'asc') + } + + linkProtocolViewFilter = '/OGC:WMT?S.*/' + linkProtocolDownloadFilter = '/OGC:WFS.*/' + + protected async getBucketLabel(bucket: TermBucket) { + return firstValueFrom( + this.translateService.get( + `search.filters.availableServices.${bucket.term}` + ) + ) + } + + protected getAggregations(): AggregationsParams { + return { + availableServices: { + type: 'filters', + filters: { + view: `+linkProtocol:${this.linkProtocolViewFilter}`, + download: `+linkProtocol:${this.linkProtocolDownloadFilter}`, + }, + }, + } + } + + getFiltersForValues(values: FieldValue[]): Observable { + const filters: FieldFilter = {} + if (values.includes('view')) filters[this.linkProtocolViewFilter] = true + if (values.includes('download')) + filters[this.linkProtocolDownloadFilter] = true + + return of({ + linkProtocol: filters, + }) + } + + getValuesForFilter(filters: FieldFilters): Observable { + const linkFilter = filters.linkProtocol + if (!linkFilter) return of([]) + const values = [] + if (linkFilter[this.linkProtocolViewFilter]) values.push('view') + if (linkFilter[this.linkProtocolDownloadFilter]) values.push('download') + return of(values) + } +} From 4170b12e6cb382041869d86654f1c94a185d1be9 Mon Sep 17 00:00:00 2001 From: cmoinier Date: Fri, 28 Feb 2025 10:28:58 +0100 Subject: [PATCH 4/5] feat: fix and improve search wc --- .../gn-search-input-and-results.sample.html | 3 +- .../gn-search-input.component.html | 8 ++++- .../gn-search-input.component.ts | 30 +++++++------------ .../gn-search-input.sample.html | 1 + .../src/app/webcomponents.module.ts | 1 - apps/webcomponents/src/styles.css | 16 +++++----- tools/webcomponent/prepare-wc-pages.sh | 4 +++ 7 files changed, 34 insertions(+), 29 deletions(-) diff --git a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input-and-results.sample.html b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input-and-results.sample.html index 8416707d8b..2c57a95688 100644 --- a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input-and-results.sample.html +++ b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input-and-results.sample.html @@ -16,6 +16,7 @@ body { margin: 0; overflow-y: scroll; + font-family: var(--font-family-main); } header { height: 200px; @@ -50,9 +51,9 @@ main-color="#555" background-color="#fdfbff" main-font="'Inter', sans-serif" - filter='{"OrgForResource": { "Géo2France": true } }' title-font="'DM Serif Display', serif" show-more="auto" + filter='{"OrgForResourceObject.default": { "Géo2France": true } }' > diff --git a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.component.html b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.component.html index 1b159ba380..78edf6a75a 100644 --- a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.component.html +++ b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.component.html @@ -1 +1,7 @@ - + diff --git a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.component.ts b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.component.ts index ee4176eeec..56bec5c8ad 100644 --- a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.component.ts +++ b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.component.ts @@ -19,33 +19,25 @@ import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' encapsulation: ViewEncapsulation.ShadowDom, providers: [SearchFacade, SearchService], }) -export class GnSearchInputComponent - extends BaseComponent - implements AfterViewChecked -{ +export class GnSearchInputComponent extends BaseComponent { @Input() openOnSearch: string @Input() openOnSelect: string @ViewChild('searchInput') searchInput: FuzzySearchComponent - ngAfterViewChecked() { + search(any: string) { if (this.openOnSearch) { - this.searchInput.inputSubmitted.subscribe(this.search.bind(this)) - } - if (this.openOnSelect) { - this.searchInput.itemSelected.subscribe(this.select.bind(this)) + const landingPage = this.openOnSearch.replace(/\$\{search}/, any) + window.open(landingPage, '_self').focus() } } - search(any: string) { - const landingPage = this.openOnSearch.replace(/\$\{search}/, any) - window.open(landingPage, '_self').focus() - } - select(record: CatalogRecord) { - const landingPage = this.openOnSelect.replace( - /\$\{uuid}/, - record.uniqueIdentifier - ) - window.open(landingPage, '_self').focus() + if (this.openOnSelect) { + const landingPage = this.openOnSelect.replace( + /\$\{uuid}/, + record.uniqueIdentifier + ) + window.open(landingPage, '_self').focus() + } } } diff --git a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.sample.html b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.sample.html index b910f229dc..a63bdda899 100644 --- a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.sample.html +++ b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.sample.html @@ -15,6 +15,7 @@