From 0b5ba02901c16c0eb48a4396c496662dc22e9b50 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 25 Oct 2024 19:17:22 +0200 Subject: [PATCH 01/42] feat(filters): first draft to filter results by date range --- .../all-records/all-records.component.ts | 2 +- .../my-records/my-records.component.ts | 2 +- .../elasticsearch.service.spec.ts | 24 ++++++ .../elasticsearch/elasticsearch.service.ts | 82 ++++++++++++++---- .../src/lib/model/search/filter.model.ts | 10 ++- .../router/src/lib/default/constants.ts | 6 +- .../filter-dropdown.component.html | 29 ++++--- .../filter-dropdown.component.ts | 27 +++++- .../src/lib/utils/service/fields.service.ts | 32 +++++-- .../search/src/lib/utils/service/fields.ts | 86 ++++++++++++++++--- 10 files changed, 249 insertions(+), 51 deletions(-) diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts index d0a76bdf85..d9516441eb 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts @@ -77,7 +77,7 @@ export class AllRecordsComponent { importRecordButton!: ElementRef @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef - searchFields = ['user'] + searchFields = ['user', 'publicationYear', 'changeDate'] searchText$: Observable = this.searchFacade.searchFilters$.pipe( map((filters) => ('any' in filters ? (filters['any'] as string) : null)) diff --git a/apps/metadata-editor/src/app/records/my-records/my-records.component.ts b/apps/metadata-editor/src/app/records/my-records/my-records.component.ts index f389fb265f..2068fd36f6 100644 --- a/apps/metadata-editor/src/app/records/my-records/my-records.component.ts +++ b/apps/metadata-editor/src/app/records/my-records/my-records.component.ts @@ -73,7 +73,7 @@ export class MyRecordsComponent implements OnInit { private importRecordButton!: ElementRef @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef - searchFields = [] + searchFields = ['changeDate'] searchText$: Observable isImportMenuOpen = false 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 4519e45b1b..f29c267295 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 @@ -169,6 +169,13 @@ describe('ElasticsearchService', () => { Org: { world: true, }, + someDate: { + start: new Date('2021-03-03'), + }, + otherDate: { + start: new Date('2020-01-01'), + end: new Date('2020-12-31'), + }, any: 'hello', }, {}, @@ -187,6 +194,23 @@ describe('ElasticsearchService', () => { query: 'Org:("world")', }, }, + { + range: { + someDate: { + gte: '2021-03-03', + format: 'yyyy-MM-dd', + }, + }, + }, + { + range: { + otherDate: { + gte: '2020-01-01', + lte: '2020-12-31', + format: 'yyyy-MM-dd', + }, + }, + }, { ids: { values: ['record-1', 'record-2', 'record-3'], 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 c888d50870..616bf19ae9 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -12,6 +12,7 @@ import { FieldFilter, FieldFilters, FilterAggregationParams, + FiltersAggregationParams, SortByField, } from '@geonetwork-ui/common/domain/model/search' import { METADATA_LANGUAGE } from '../../metadata-language' @@ -27,6 +28,9 @@ import { } from '@geonetwork-ui/api/metadata-converter' import { LangService } from '@geonetwork-ui/util/i18n' +export type DateRange = { start?: Date; end?: Date } +export type TimestampRange = { start?: number; end?: number } + @Injectable({ providedIn: 'root', }) @@ -213,7 +217,10 @@ export class ElasticsearchService { return this.metadataLang === 'current' } - private filtersToQueryString(filters: FieldFilters): string { + // TODO: type this + private filtersToQuery( + filters: FieldFilters | FiltersAggregationParams | string + ): any[] { const makeQuery = (filter: FieldFilter): string => { if (typeof filter === 'string') { return filter @@ -227,13 +234,67 @@ export class ElasticsearchService { }) .join(' OR ') } - return Object.keys(filters) + const queryString = Object.keys(filters) + // .filter((fieldname) => !this.isDateRange(JSON.parse(filters[fieldname]))) + .filter((fieldname) => fieldname !== 'changeDate') // TODO: make this generic .filter( (fieldname) => filters[fieldname] && JSON.stringify(filters[fieldname]) !== '{}' ) .map((fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`) .join(' AND ') + const queryRange = Object.entries(filters) + // .filter(([key, value]) => this.isDateRange(JSON.parse(value))) + .filter(([key, value]) => key === 'changeDate') // TODO: make this generic + .map(([searchField, dateRange]) => { + console.log('dateRange', dateRange) + return { + searchField, + dateRange: this.parseUrlObject(dateRange[0]), // TODO: reading values on app load not working yet + } as unknown as { + searchField: string + dateRange: TimestampRange + } + })[0] + const queryParts = [ + queryString && { + query_string: { + query: queryString, + }, + }, + queryRange && + queryRange.dateRange && { + range: { + [queryRange.searchField]: { + gte: this.formatDate(queryRange.dateRange.start), + lte: this.formatDate(queryRange.dateRange.end), + format: 'yyyy-MM-dd', + }, + }, + }, + ].filter(Boolean) + return queryParts.length > 0 ? queryParts : undefined + } + + // TODO: move utility functions to right place + private isDateRange(filter: FieldFilter): boolean { + return typeof filter === 'object' && ('start' in filter || 'end' in filter) + } + + private formatDate(timestamp: number): string { + const date = new Date(timestamp) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + } + + private parseUrlObject(str: string): Record { + try { + return JSON.parse(str) + } catch (e) { + return null + } } private buildPayloadQuery( @@ -266,13 +327,9 @@ export class ElasticsearchService { }, }) } - const queryFilters = this.filtersToQueryString(fieldSearchFilters) + const queryFilters = this.filtersToQuery(fieldSearchFilters) if (queryFilters) { - filter.push({ - query_string: { - query: queryFilters, - }, - }) + filter.push(...queryFilters) } if (uuids) { filter.push({ @@ -480,14 +537,7 @@ export class ElasticsearchService { const filter = aggregation.filters[curr] return { ...prev, - [curr]: { - query_string: { - query: - typeof filter === 'string' - ? filter - : this.filtersToQueryString(filter), - }, - }, + [curr]: this.filtersToQuery(filter), } }, {}), } diff --git a/libs/common/domain/src/lib/model/search/filter.model.ts b/libs/common/domain/src/lib/model/search/filter.model.ts index ddb4b683c5..106d5e9317 100644 --- a/libs/common/domain/src/lib/model/search/filter.model.ts +++ b/libs/common/domain/src/lib/model/search/filter.model.ts @@ -2,5 +2,13 @@ import { FieldName } from './field.model' export type FieldFilterByValues = Record export type FieldFilterByExpression = string | number -export type FieldFilter = FieldFilterByExpression | FieldFilterByValues +export type FieldFilterByRange = { + start?: Date + end?: Date +} + +export type FieldFilter = + | FieldFilterByExpression + | FieldFilterByValues + | FieldFilterByRange export type FieldFilters = Record diff --git a/libs/feature/router/src/lib/default/constants.ts b/libs/feature/router/src/lib/default/constants.ts index 3365b3eca5..720f285b22 100644 --- a/libs/feature/router/src/lib/default/constants.ts +++ b/libs/feature/router/src/lib/default/constants.ts @@ -9,4 +9,8 @@ export enum ROUTE_PARAMS { PUBLISHER = 'publisher', // FIXME: this shouldn't be here as it is a search field PAGE = '_page', } -export type SearchRouteParams = Record +export type TimestampRange = { start?: number; end?: number } +export type SearchRouteParams = Record< + string, + string | string[] | number | TimestampRange +> diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html index 9ff8ff4c1b..710f1c9a7c 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html @@ -1,11 +1,18 @@ - - + + + + + diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts index 8e36fb08b5..c0ccb5d648 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts @@ -10,7 +10,12 @@ import { catchError, filter, map, startWith } from 'rxjs/operators' import { SearchFacade } from '../state/search.facade' import { SearchService } from '../utils/service/search.service' import { FieldsService } from '../utils/service/fields.service' -import { FieldAvailableValue, FieldValue } from '../utils/service/fields' +import { + DateRange, + FieldAvailableValue, + FieldType, + FieldValue, +} from '../utils/service/fields' @Component({ selector: 'gn-ui-filter-dropdown', @@ -22,6 +27,8 @@ export class FilterDropdownComponent implements OnInit { @Input() fieldName: string @Input() title: string + fieldType: FieldType + dateRange: DateRange choices$: Observable selected$ = this.searchFacade.searchFilters$.pipe( switchMap((filters) => @@ -46,6 +53,7 @@ export class FilterDropdownComponent implements OnInit { ) {} ngOnInit() { + this.fieldType = this.fieldsService.getFieldType(this.fieldName) this.choices$ = this.fieldsService.getAvailableValues(this.fieldName).pipe( startWith([] as FieldAvailableValue[]), map((values) => @@ -57,4 +65,21 @@ export class FilterDropdownComponent implements OnInit { catchError(() => of([])) ) } + + onStartDateChange(start: Date) { + this.dateRange = { ...this.dateRange, start } + } + + onEndDateChange(end: Date) { + this.dateRange = { ...this.dateRange, end } + if (this.dateRange.start && this.dateRange.end) { + this.fieldsService + .buildFiltersFromFieldValues({ + [this.fieldName]: this.dateRange, + }) + .subscribe((filters) => { + return this.searchService.updateFilters(filters) + }) + } + } } 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 016b3162f4..0d5bcb5436 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,8 @@ import { Injectable, Injector } from '@angular/core' import { AbstractSearchField, + DateRange, + DateRangeSearchField, FieldValue, FullTextSearchField, IsSpatialSearchField, @@ -9,6 +11,7 @@ import { OrganizationSearchField, OwnerSearchField, SimpleSearchField, + TimestampRange, TranslatedSearchField, UserSearchField, } from './fields' @@ -87,6 +90,7 @@ export class FieldsService { 'key' ), user: new UserSearchField(this.injector), + changeDate: new DateRangeSearchField('changeDate', this.injector, 'desc'), } as Record get supportedFields() { @@ -101,25 +105,37 @@ export class FieldsService { return this.fields[fieldName].getAvailableValues() } - private getFiltersForValues(fieldName: string, values: FieldValue[]) { + private getFiltersForValues( + fieldName: string, + values: FieldValue[] | DateRange + ) { return this.fields[fieldName].getFiltersForValues(values) } private getValuesForFilters(fieldName: string, filters: FieldFilters) { return this.fields[fieldName].getValuesForFilter(filters) } + getFieldType(fieldName: string) { + return this.fields[fieldName].getType() + } + buildFiltersFromFieldValues( - fieldValues: FieldValues + fieldValues: FieldValues | DateRange ): Observable { const fieldNames = Object.keys(fieldValues).filter((fieldName) => this.supportedFields.includes(fieldName) ) if (!fieldNames.length) return of({}) const filtersByField$ = fieldNames.map((fieldName) => { - const values = Array.isArray(fieldValues[fieldName]) - ? fieldValues[fieldName] - : [fieldValues[fieldName]] - return this.getFiltersForValues(fieldName, values as FieldValue[]) + const values = + Array.isArray(fieldValues[fieldName]) || + Object.keys(fieldValues[fieldName]).length > 1 + ? fieldValues[fieldName] + : [fieldValues[fieldName]] + return this.getFiltersForValues( + fieldName, + values as FieldValue[] | DateRange + ) }) return forkJoin(filtersByField$).pipe( map((filters) => @@ -128,7 +144,9 @@ export class FieldsService { ) } - readFieldValuesFromFilters(filters: FieldFilters): Observable { + readFieldValuesFromFilters( + filters: FieldFilters + ): Observable { const fieldValues$ = this.supportedFields.map((fieldName) => this.getValuesForFilters(fieldName, filters).pipe( map((values) => ({ [fieldName]: values })) diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 4a377ec969..2792630363 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -19,6 +19,10 @@ import { } from '@geonetwork-ui/api/repository' import { LangService } from '@geonetwork-ui/util/i18n' +export type DateRange = { start?: Date; end?: Date } +export type TimestampRange = { start?: number; end?: number } +export type FieldType = 'values' | 'dateRange' + export type FieldValue = string | number export interface FieldAvailableValue { value: FieldValue @@ -26,9 +30,14 @@ export interface FieldAvailableValue { } export abstract class AbstractSearchField { - abstract getAvailableValues(): Observable - abstract getFiltersForValues(values: FieldValue[]): Observable - abstract getValuesForFilter(filters: FieldFilters): Observable + abstract getAvailableValues(): Observable + abstract getFiltersForValues( + values: FieldValue[] | DateRange + ): Observable + abstract getValuesForFilter( + filters: FieldFilters + ): Observable + abstract getType(): FieldType } export class SimpleSearchField implements AbstractSearchField { @@ -74,22 +83,48 @@ export class SimpleSearchField implements AbstractSearchField { }) ) } - getFiltersForValues(values: FieldValue[]): Observable { + getFiltersForValues(values: FieldValue[] /*| DateRange*/): Observable { + // FieldValue[] + //TODO: check this + // if (Array.isArray(values)) { + if (Array.isArray(values) && this.getType() === 'values') { + return of({ + [this.esFieldName]: values.reduce((acc, val) => { + return { ...acc, [val.toString()]: true } + }, {}), + }) + } + // DateRange return of({ - [this.esFieldName]: values.reduce((acc, val) => { - return { ...acc, [val.toString()]: true } - }, {}), + [this.esFieldName]: values, }) } - getValuesForFilter(filters: FieldFilters): Observable { + getValuesForFilter( + filters: FieldFilters + ): Observable { const filter = filters[this.esFieldName] if (!filter) return of([]) - const values = - typeof filter === 'string' - ? [filter] - : Object.keys(filter).filter((v) => filter[v]) + // filter by expression + if (typeof filter === 'string') { + return of([filter]) + } + // filter by date range + if (typeof filter === 'object' && ('start' in filter || 'end' in filter)) { + const range = filter as DateRange + const timeStampFilter = { + start: range.start.getTime(), + end: range.end.getTime(), + } + return of([JSON.stringify(timeStampFilter as TimestampRange)]) //TODO: check this + } + // filter by values + const values = Object.keys(filter).filter((v) => filter[v]) return of(values) } + + getType(): FieldType { + return 'values' + } } export class TranslatedSearchField extends SimpleSearchField { @@ -163,6 +198,9 @@ export class FullTextSearchField implements AbstractSearchField { getValuesForFilter(filters: FieldFilters): Observable { return of(filters.any ? [filters.any as FieldFilterByExpression] : []) } + getType(): FieldType { + return 'values' + } } marker('search.filters.isSpatial.yes') @@ -336,6 +374,10 @@ export class OrganizationSearchField implements AbstractSearchField { ) ) } + + getType(): FieldType { + return 'values' + } } export class OwnerSearchField extends SimpleSearchField { constructor(injector: Injector) { @@ -372,3 +414,23 @@ export class UserSearchField extends SimpleSearchField { return undefined } } + +export class DateRangeSearchField extends SimpleSearchField { + constructor( + protected esFieldName: string, + protected injector: Injector, + protected order: 'asc' | 'desc' = 'asc', + protected orderType: 'key' | 'count' = 'key' + ) { + super(esFieldName, injector, order, orderType) + } + + getAvailableValues(): Observable { + // TODO: return an array of dates to show which one are available in the date picker + return of([]) + } + + getType(): FieldType { + return 'dateRange' + } +} From d4722dbc76a31b66b8e1d1a2cd4d8cfaf866e93f Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 29 Oct 2024 13:57:21 +0100 Subject: [PATCH 02/42] fix(fields): return one value instead of array for date range filter --- .../src/lib/gn4/elasticsearch/elasticsearch.service.ts | 2 +- libs/feature/search/src/lib/utils/service/fields.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 616bf19ae9..6a6c25c920 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -250,7 +250,7 @@ export class ElasticsearchService { console.log('dateRange', dateRange) return { searchField, - dateRange: this.parseUrlObject(dateRange[0]), // TODO: reading values on app load not working yet + dateRange: this.parseUrlObject(dateRange), } as unknown as { searchField: string dateRange: TimestampRange diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 2792630363..8ffd001175 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -36,7 +36,7 @@ export abstract class AbstractSearchField { ): Observable abstract getValuesForFilter( filters: FieldFilters - ): Observable + ): Observable abstract getType(): FieldType } @@ -101,7 +101,7 @@ export class SimpleSearchField implements AbstractSearchField { } getValuesForFilter( filters: FieldFilters - ): Observable { + ): Observable { const filter = filters[this.esFieldName] if (!filter) return of([]) // filter by expression @@ -115,7 +115,7 @@ export class SimpleSearchField implements AbstractSearchField { start: range.start.getTime(), end: range.end.getTime(), } - return of([JSON.stringify(timeStampFilter as TimestampRange)]) //TODO: check this + return of(JSON.stringify(timeStampFilter as TimestampRange)) } // filter by values const values = Object.keys(filter).filter((v) => filter[v]) From c4c898f19dfb4029024ee88e0c53bfc2c0ab0624 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 29 Oct 2024 15:24:13 +0100 Subject: [PATCH 03/42] fix(fields): handle stringified range filters to make them work with other filters --- libs/feature/search/src/lib/utils/service/fields.service.ts | 2 +- libs/feature/search/src/lib/utils/service/fields.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 0d5bcb5436..6bb6e6949f 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -131,7 +131,7 @@ export class FieldsService { Array.isArray(fieldValues[fieldName]) || Object.keys(fieldValues[fieldName]).length > 1 ? fieldValues[fieldName] - : [fieldValues[fieldName]] + : [fieldValues[fieldName]] //TODO: handle stringified ranges which are not an object properly return this.getFiltersForValues( fieldName, values as FieldValue[] | DateRange diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 8ffd001175..1df9760ff8 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -96,7 +96,7 @@ export class SimpleSearchField implements AbstractSearchField { } // DateRange return of({ - [this.esFieldName]: values, + [this.esFieldName]: Array.isArray(values) ? values[0] : values, }) } getValuesForFilter( From 7cb12dcd0fad9e6a91be09cb3a826493f616c02b Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 29 Oct 2024 15:52:30 +0100 Subject: [PATCH 04/42] refactor(es): check on filter type dateRange instead of name changeDate --- .../elasticsearch/elasticsearch.service.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) 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 6a6c25c920..f374b48f77 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -235,8 +235,10 @@ export class ElasticsearchService { .join(' OR ') } const queryString = Object.keys(filters) - // .filter((fieldname) => !this.isDateRange(JSON.parse(filters[fieldname]))) - .filter((fieldname) => fieldname !== 'changeDate') // TODO: make this generic + .filter( + (fieldname) => + !this.isDateRange(this.parseUrlObject(filters[fieldname])) + ) .filter( (fieldname) => filters[fieldname] && JSON.stringify(filters[fieldname]) !== '{}' @@ -244,8 +246,7 @@ export class ElasticsearchService { .map((fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`) .join(' AND ') const queryRange = Object.entries(filters) - // .filter(([key, value]) => this.isDateRange(JSON.parse(value))) - .filter(([key, value]) => key === 'changeDate') // TODO: make this generic + .filter(([, value]) => this.isDateRange(this.parseUrlObject(value))) .map(([searchField, dateRange]) => { console.log('dateRange', dateRange) return { @@ -278,6 +279,7 @@ export class ElasticsearchService { // TODO: move utility functions to right place private isDateRange(filter: FieldFilter): boolean { + if (!filter) return false return typeof filter === 'object' && ('start' in filter || 'end' in filter) } @@ -290,11 +292,14 @@ export class ElasticsearchService { } private parseUrlObject(str: string): Record { - try { - return JSON.parse(str) - } catch (e) { - return null + if (typeof str === 'string') { + try { + return JSON.parse(str) + } catch (e) { + return null + } } + return null } private buildPayloadQuery( From 0cef9b7c25c3993c4c1797b8842e4d8014c57134 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 29 Oct 2024 16:02:36 +0100 Subject: [PATCH 05/42] refactor(es): add types for FilterQuery --- .../src/lib/gn4/elasticsearch/elasticsearch.service.ts | 6 +++--- libs/common/domain/src/lib/model/search/filter.model.ts | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) 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 f374b48f77..389d6ca0c7 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -12,6 +12,7 @@ import { FieldFilter, FieldFilters, FilterAggregationParams, + FilterQuery, FiltersAggregationParams, SortByField, } from '@geonetwork-ui/common/domain/model/search' @@ -217,10 +218,9 @@ export class ElasticsearchService { return this.metadataLang === 'current' } - // TODO: type this private filtersToQuery( filters: FieldFilters | FiltersAggregationParams | string - ): any[] { + ): FilterQuery { const makeQuery = (filter: FieldFilter): string => { if (typeof filter === 'string') { return filter @@ -274,7 +274,7 @@ export class ElasticsearchService { }, }, ].filter(Boolean) - return queryParts.length > 0 ? queryParts : undefined + return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined } // TODO: move utility functions to right place diff --git a/libs/common/domain/src/lib/model/search/filter.model.ts b/libs/common/domain/src/lib/model/search/filter.model.ts index 106d5e9317..b0043bbbb5 100644 --- a/libs/common/domain/src/lib/model/search/filter.model.ts +++ b/libs/common/domain/src/lib/model/search/filter.model.ts @@ -12,3 +12,11 @@ export type FieldFilter = | FieldFilterByValues | FieldFilterByRange export type FieldFilters = Record + +export type QueryString = { + query_string: string +} +export type QueryRange = { + range: Record +} +export type FilterQuery = Array From bd48cb1bd1fa8912266c0bfadd85bc3edaf428c9 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 29 Oct 2024 16:27:24 +0100 Subject: [PATCH 06/42] refactor(es): move utility functions --- .../lib/gn4/elasticsearch/date-range.utils.ts | 25 +++++++++++ .../elasticsearch/elasticsearch.service.ts | 41 +++---------------- .../src/lib/gn4/elasticsearch/index.ts | 1 + .../search/src/lib/utils/service/fields.ts | 3 +- 4 files changed, 33 insertions(+), 37 deletions(-) create mode 100644 libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts new file mode 100644 index 0000000000..78b3c21877 --- /dev/null +++ b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts @@ -0,0 +1,25 @@ +import { FieldFilter } from '@geonetwork-ui/common/domain/model/search' + +export function isDateRange(filter: FieldFilter): boolean { + if (!filter) return false + return typeof filter === 'object' && ('start' in filter || 'end' in filter) +} + +export function formatDate(timestamp: number): string { + const date = new Date(timestamp) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +export function parseUrlObject(str: string): Record { + if (typeof str === 'string') { + try { + return JSON.parse(str) + } catch (e) { + return null + } + } + return null +} 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 389d6ca0c7..db6452feae 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -11,7 +11,6 @@ import { AggregationsParams, FieldFilter, FieldFilters, - FilterAggregationParams, FilterQuery, FiltersAggregationParams, SortByField, @@ -28,6 +27,7 @@ import { TermsAggregationResult, } from '@geonetwork-ui/api/metadata-converter' import { LangService } from '@geonetwork-ui/util/i18n' +import { formatDate, isDateRange, parseUrlObject } from './date-range.utils' export type DateRange = { start?: Date; end?: Date } export type TimestampRange = { start?: number; end?: number } @@ -235,10 +235,7 @@ export class ElasticsearchService { .join(' OR ') } const queryString = Object.keys(filters) - .filter( - (fieldname) => - !this.isDateRange(this.parseUrlObject(filters[fieldname])) - ) + .filter((fieldname) => !isDateRange(parseUrlObject(filters[fieldname]))) .filter( (fieldname) => filters[fieldname] && JSON.stringify(filters[fieldname]) !== '{}' @@ -246,12 +243,11 @@ export class ElasticsearchService { .map((fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`) .join(' AND ') const queryRange = Object.entries(filters) - .filter(([, value]) => this.isDateRange(this.parseUrlObject(value))) + .filter(([, value]) => isDateRange(parseUrlObject(value))) .map(([searchField, dateRange]) => { - console.log('dateRange', dateRange) return { searchField, - dateRange: this.parseUrlObject(dateRange), + dateRange: parseUrlObject(dateRange), } as unknown as { searchField: string dateRange: TimestampRange @@ -267,8 +263,8 @@ export class ElasticsearchService { queryRange.dateRange && { range: { [queryRange.searchField]: { - gte: this.formatDate(queryRange.dateRange.start), - lte: this.formatDate(queryRange.dateRange.end), + gte: formatDate(queryRange.dateRange.start), + lte: formatDate(queryRange.dateRange.end), format: 'yyyy-MM-dd', }, }, @@ -277,31 +273,6 @@ export class ElasticsearchService { return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined } - // TODO: move utility functions to right place - private isDateRange(filter: FieldFilter): boolean { - if (!filter) return false - return typeof filter === 'object' && ('start' in filter || 'end' in filter) - } - - private formatDate(timestamp: number): string { - const date = new Date(timestamp) - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - } - - private parseUrlObject(str: string): Record { - if (typeof str === 'string') { - try { - return JSON.parse(str) - } catch (e) { - return null - } - } - return null - } - private buildPayloadQuery( { any, ...fieldSearchFilters }: SearchFilters, configFilters: SearchFilters, diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/index.ts b/libs/api/repository/src/lib/gn4/elasticsearch/index.ts index 34bbef1fcd..87b40eae5f 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/index.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/index.ts @@ -1,2 +1,3 @@ export * from './elasticsearch.service' export * from './constant' +export * from './date-range.utils' diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 1df9760ff8..0d01f39c80 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -85,8 +85,6 @@ export class SimpleSearchField implements AbstractSearchField { } getFiltersForValues(values: FieldValue[] /*| DateRange*/): Observable { // FieldValue[] - //TODO: check this - // if (Array.isArray(values)) { if (Array.isArray(values) && this.getType() === 'values') { return of({ [this.esFieldName]: values.reduce((acc, val) => { @@ -94,6 +92,7 @@ export class SimpleSearchField implements AbstractSearchField { }, {}), }) } + //TODO: find proper solution to handle date ranges as objects and strings // DateRange return of({ [this.esFieldName]: Array.isArray(values) ? values[0] : values, From fc9d05b2b591c419f2ff19b1732a66f1fba423c6 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 29 Oct 2024 16:49:44 +0100 Subject: [PATCH 07/42] refactor(fields): clarify curently used types for date range todo: methods should only accept DateRange or string, not both --- libs/feature/router/src/lib/default/constants.ts | 6 +----- .../search/src/lib/utils/service/fields.service.ts | 11 ++++------- libs/feature/search/src/lib/utils/service/fields.ts | 6 ++++-- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/libs/feature/router/src/lib/default/constants.ts b/libs/feature/router/src/lib/default/constants.ts index 720f285b22..3365b3eca5 100644 --- a/libs/feature/router/src/lib/default/constants.ts +++ b/libs/feature/router/src/lib/default/constants.ts @@ -9,8 +9,4 @@ export enum ROUTE_PARAMS { PUBLISHER = 'publisher', // FIXME: this shouldn't be here as it is a search field PAGE = '_page', } -export type TimestampRange = { start?: number; end?: number } -export type SearchRouteParams = Record< - string, - string | string[] | number | TimestampRange -> +export type SearchRouteParams = Record 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 6bb6e6949f..c7a5b9b5e7 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -11,7 +11,6 @@ import { OrganizationSearchField, OwnerSearchField, SimpleSearchField, - TimestampRange, TranslatedSearchField, UserSearchField, } from './fields' @@ -107,7 +106,7 @@ export class FieldsService { private getFiltersForValues( fieldName: string, - values: FieldValue[] | DateRange + values: FieldValue[] | DateRange | string ) { return this.fields[fieldName].getFiltersForValues(values) } @@ -120,7 +119,7 @@ export class FieldsService { } buildFiltersFromFieldValues( - fieldValues: FieldValues | DateRange + fieldValues: FieldValues | DateRange | string ): Observable { const fieldNames = Object.keys(fieldValues).filter((fieldName) => this.supportedFields.includes(fieldName) @@ -134,7 +133,7 @@ export class FieldsService { : [fieldValues[fieldName]] //TODO: handle stringified ranges which are not an object properly return this.getFiltersForValues( fieldName, - values as FieldValue[] | DateRange + values as FieldValue[] | DateRange | string ) }) return forkJoin(filtersByField$).pipe( @@ -144,9 +143,7 @@ export class FieldsService { ) } - readFieldValuesFromFilters( - filters: FieldFilters - ): Observable { + readFieldValuesFromFilters(filters: FieldFilters): Observable { const fieldValues$ = this.supportedFields.map((fieldName) => this.getValuesForFilters(fieldName, filters).pipe( map((values) => ({ [fieldName]: values })) diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 0d01f39c80..b5db353665 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -32,7 +32,7 @@ export interface FieldAvailableValue { export abstract class AbstractSearchField { abstract getAvailableValues(): Observable abstract getFiltersForValues( - values: FieldValue[] | DateRange + values: FieldValue[] | DateRange | string ): Observable abstract getValuesForFilter( filters: FieldFilters @@ -83,7 +83,9 @@ export class SimpleSearchField implements AbstractSearchField { }) ) } - getFiltersForValues(values: FieldValue[] /*| DateRange*/): Observable { + getFiltersForValues( + values: FieldValue[] | DateRange | string + ): Observable { // FieldValue[] if (Array.isArray(values) && this.getType() === 'values') { return of({ From a36690c38d7833ba123e001c6bcf56667927a375 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 30 Oct 2024 12:59:03 +0100 Subject: [PATCH 08/42] feat(router): add utilitis to serialize DateRange objects in URL Co-authored-by: Olivia Guyot --- .../router/src/lib/default/constants.ts | 17 +++++- .../default/state/query-params.utils.spec.ts | 47 ++++++++++++++++ .../lib/default/state/query-params.utils.ts | 53 +++++++++++++++++++ .../src/lib/default/state/router.facade.ts | 8 +-- 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 libs/feature/router/src/lib/default/state/query-params.utils.spec.ts create mode 100644 libs/feature/router/src/lib/default/state/query-params.utils.ts diff --git a/libs/feature/router/src/lib/default/constants.ts b/libs/feature/router/src/lib/default/constants.ts index 3365b3eca5..2a77b229e1 100644 --- a/libs/feature/router/src/lib/default/constants.ts +++ b/libs/feature/router/src/lib/default/constants.ts @@ -1,3 +1,9 @@ +import { + DateRange, + FieldsService, + FieldValue, +} from '@geonetwork-ui/feature/search' + export const ROUTER_STATE_KEY = 'router' export const ROUTER_ROUTE_SEARCH = 'search' @@ -9,4 +15,13 @@ export enum ROUTE_PARAMS { PUBLISHER = 'publisher', // FIXME: this shouldn't be here as it is a search field PAGE = '_page', } -export type SearchRouteParams = Record +export type SearchRouteParams = Record< + string, + string | string[] | number | DateRange +> +//TODO: type RouteParamKey correctly +// type RouteParamKey = ROUTE_PARAMS[string] | FieldsService['fields'][string] +// export type SearchRouteParams = Record< +// RouteParamKey, +// FieldValue[] | FieldValue | DateRange +// > diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts b/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts new file mode 100644 index 0000000000..872af9ea91 --- /dev/null +++ b/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts @@ -0,0 +1,47 @@ +import { ROUTE_PARAMS } from '../constants' +import { expandQueryParams, flattenQueryParams } from './query-params.utils' + +describe('query params utilities', () => { + describe('flattenQueryParams', () => { + it('produces serialized query params from various route parameters', () => { + const params = flattenQueryParams({ + [ROUTE_PARAMS.SORT]: 'createDate', + publisher: ['john', 'barbie'], + updateDate: { + start: new Date('2010-03-10T14:50:12'), + end: new Date('2014-01-01'), + }, + changeDate: { + end: new Date('2008-08-14T14:50:12'), + }, + }) + expect(params).toEqual({ + _sort: 'createDate', + publisher: 'john,barbie', + updateDate: '2010-03-10..2014-01-01', + changeDate: '..2008-08-14', + }) + }) + }) + describe('expandQueryParams', () => { + it('restores full route parameters from serialized query params', () => { + const params = expandQueryParams({ + _sort: 'createDate', + publisher: 'john,barbie', + updateDate: '2010-03-10..2014-01-01', + changeDate: '..2008-08-14', + }) + expect(params).toEqual({ + [ROUTE_PARAMS.SORT]: 'createDate', + publisher: ['john', 'barbie'], + updateDate: { + start: new Date('2010-03-10'), + end: new Date('2014-01-01'), + }, + changeDate: { + end: new Date('2008-08-14'), + }, + }) + }) + }) +}) diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.ts b/libs/feature/router/src/lib/default/state/query-params.utils.ts new file mode 100644 index 0000000000..09151f236a --- /dev/null +++ b/libs/feature/router/src/lib/default/state/query-params.utils.ts @@ -0,0 +1,53 @@ +import { ROUTE_PARAMS, SearchRouteParams } from '../constants' +import { DateRange } from '@geonetwork-ui/feature/search' + +export function flattenQueryParams( + params: SearchRouteParams +): Record { + const flattened = { ...params } + for (const key in params) { + if (Array.isArray(flattened[key])) { + flattened[key] = (flattened[key] as string[]).join(',') + } else if (isDateRange(flattened[key] as DateRange)) { + flattened[key] = `${ + (flattened[key] as DateRange).start?.toISOString().split('T')[0] || '' + }..${ + (flattened[key] as DateRange).end?.toISOString().split('T')[0] || '' + }` + } + } + + return flattened +} + +export function expandQueryParams( + params: Record +): SearchRouteParams { + // this only matches if the separator ".." is present only once and not dots are present elsewhere + const ifDate = function (value: string) { + return value.match(/^[^.]*(\.\.)[^.]*$/) + } + const expanded = { ...params } + for (const key in params) { + if ( + Object.values(ROUTE_PARAMS).includes(key as ROUTE_PARAMS) && + key !== 'publisher' //FIXME: temporary workaround as publisher shouldn't be in ROUTE_PARAMS as it is a search field + ) { + //do nothing + } else if (ifDate(expanded[key] as string)) { + const [start, end] = (expanded[key] as string).split('..') + expanded[key] = { + ...(start && { start: new Date(start) }), + ...(end && { end: new Date(end) }), + } + } else { + expanded[key] = (expanded[key] as string).split(',') + } + } + return expanded +} + +function isDateRange(filter: DateRange): boolean { + if (!filter) return false + return typeof filter === 'object' && ('start' in filter || 'end' in filter) +} diff --git a/libs/feature/router/src/lib/default/state/router.facade.ts b/libs/feature/router/src/lib/default/state/router.facade.ts index 1186f2fa18..751edbec8a 100644 --- a/libs/feature/router/src/lib/default/state/router.facade.ts +++ b/libs/feature/router/src/lib/default/state/router.facade.ts @@ -17,6 +17,7 @@ import { } from './router.actions' import { selectCurrentRoute, selectRouteParams } from './router.selectors' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { expandQueryParams, flattenQueryParams } from './query-params.utils' @Injectable() export class RouterFacade { @@ -27,7 +28,8 @@ export class RouterFacade { filter((route) => !!route), filter((route) => route.url[0]?.path.startsWith(ROUTER_ROUTE_SEARCH)), map((route) => route.queryParams), - distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)) + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), + map(expandQueryParams) ) constructor( @@ -62,7 +64,7 @@ export class RouterFacade { updateSearch(query?: SearchRouteParams) { this.go({ path: this.routerService.getSearchRoute(), - ...(query && { query }), + ...(query && flattenQueryParams({ query })), queryParamsHandling: 'merge', }) } @@ -70,7 +72,7 @@ export class RouterFacade { setSearch(query?: SearchRouteParams) { this.go({ path: this.routerService.getSearchRoute(), - ...(query && { query }), + ...(query && flattenQueryParams({ query })), }) } From eb1e4594dd8bd28ac2ebe7168449d99e8487e52d Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 31 Oct 2024 14:10:46 +0100 Subject: [PATCH 09/42] feat(router): use and adapt query-params.utils for serialization --- .../lib/gn4/elasticsearch/date-range.utils.ts | 14 +---- .../elasticsearch/elasticsearch.service.ts | 13 ++--- .../default/state/query-params.utils.spec.ts | 26 ++++++--- .../lib/default/state/query-params.utils.ts | 57 +++++++++++-------- .../src/lib/default/state/router.facade.ts | 4 +- .../src/lib/utils/service/fields.service.ts | 8 +-- .../search/src/lib/utils/service/fields.ts | 14 ++--- 7 files changed, 70 insertions(+), 66 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts index 78b3c21877..3e77e8c92f 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts @@ -5,21 +5,9 @@ export function isDateRange(filter: FieldFilter): boolean { return typeof filter === 'object' && ('start' in filter || 'end' in filter) } -export function formatDate(timestamp: number): string { - const date = new Date(timestamp) +export function formatDate(date: Date): string { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } - -export function parseUrlObject(str: string): Record { - if (typeof str === 'string') { - try { - return JSON.parse(str) - } catch (e) { - return null - } - } - return null -} 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 db6452feae..1c64b7a808 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -27,10 +27,9 @@ import { TermsAggregationResult, } from '@geonetwork-ui/api/metadata-converter' import { LangService } from '@geonetwork-ui/util/i18n' -import { formatDate, isDateRange, parseUrlObject } from './date-range.utils' +import { formatDate, isDateRange } from './date-range.utils' export type DateRange = { start?: Date; end?: Date } -export type TimestampRange = { start?: number; end?: number } @Injectable({ providedIn: 'root', @@ -235,7 +234,7 @@ export class ElasticsearchService { .join(' OR ') } const queryString = Object.keys(filters) - .filter((fieldname) => !isDateRange(parseUrlObject(filters[fieldname]))) + .filter((fieldname) => !isDateRange(filters[fieldname])) .filter( (fieldname) => filters[fieldname] && JSON.stringify(filters[fieldname]) !== '{}' @@ -243,14 +242,14 @@ export class ElasticsearchService { .map((fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`) .join(' AND ') const queryRange = Object.entries(filters) - .filter(([, value]) => isDateRange(parseUrlObject(value))) + .filter(([, value]) => isDateRange(value)) .map(([searchField, dateRange]) => { return { searchField, - dateRange: parseUrlObject(dateRange), - } as unknown as { + dateRange, + } as { searchField: string - dateRange: TimestampRange + dateRange: DateRange } })[0] const queryParts = [ diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts b/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts index 872af9ea91..08393b9ec3 100644 --- a/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts +++ b/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts @@ -14,22 +14,24 @@ describe('query params utilities', () => { changeDate: { end: new Date('2008-08-14T14:50:12'), }, + emptyParam: [], }) expect(params).toEqual({ _sort: 'createDate', - publisher: 'john,barbie', - updateDate: '2010-03-10..2014-01-01', - changeDate: '..2008-08-14', + publisher: ['john,barbie'], + updateDate: ['2010-03-10..2014-01-01'], + changeDate: ['..2008-08-14'], + emptyParam: [], }) }) }) describe('expandQueryParams', () => { - it('restores full route parameters from serialized query params', () => { + it('restores full route parameters from serialized query params in arrays', () => { const params = expandQueryParams({ _sort: 'createDate', - publisher: 'john,barbie', - updateDate: '2010-03-10..2014-01-01', - changeDate: '..2008-08-14', + publisher: ['john,barbie'], + updateDate: ['2010-03-10..2014-01-01'], + changeDate: ['..2008-08-14'], }) expect(params).toEqual({ [ROUTE_PARAMS.SORT]: 'createDate', @@ -43,5 +45,15 @@ describe('query params utilities', () => { }, }) }) + it('restores full route parameter from a SINGLE serialized string query param', () => { + const params = expandQueryParams({ + changeDate: '..2008-08-14', + }) + expect(params).toEqual({ + changeDate: { + end: new Date('2008-08-14'), + }, + }) + }) }) }) diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.ts b/libs/feature/router/src/lib/default/state/query-params.utils.ts index 09151f236a..21e3d7f1d9 100644 --- a/libs/feature/router/src/lib/default/state/query-params.utils.ts +++ b/libs/feature/router/src/lib/default/state/query-params.utils.ts @@ -6,14 +6,19 @@ export function flattenQueryParams( ): Record { const flattened = { ...params } for (const key in params) { - if (Array.isArray(flattened[key])) { - flattened[key] = (flattened[key] as string[]).join(',') + if ( + Array.isArray(flattened[key]) && + (flattened[key] as string[]).length > 0 + ) { + flattened[key] = [(flattened[key] as string[]).join(',')] } else if (isDateRange(flattened[key] as DateRange)) { - flattened[key] = `${ - (flattened[key] as DateRange).start?.toISOString().split('T')[0] || '' - }..${ - (flattened[key] as DateRange).end?.toISOString().split('T')[0] || '' - }` + flattened[key] = [ + `${ + (flattened[key] as DateRange).start?.toISOString().split('T')[0] || '' + }..${ + (flattened[key] as DateRange).end?.toISOString().split('T')[0] || '' + }`, + ] } } @@ -23,25 +28,26 @@ export function flattenQueryParams( export function expandQueryParams( params: Record ): SearchRouteParams { - // this only matches if the separator ".." is present only once and not dots are present elsewhere - const ifDate = function (value: string) { - return value.match(/^[^.]*(\.\.)[^.]*$/) - } const expanded = { ...params } for (const key in params) { - if ( - Object.values(ROUTE_PARAMS).includes(key as ROUTE_PARAMS) && - key !== 'publisher' //FIXME: temporary workaround as publisher shouldn't be in ROUTE_PARAMS as it is a search field - ) { - //do nothing - } else if (ifDate(expanded[key] as string)) { - const [start, end] = (expanded[key] as string).split('..') - expanded[key] = { - ...(start && { start: new Date(start) }), - ...(end && { end: new Date(end) }), + const value: string = Array.isArray(expanded[key]) + ? expanded[key][0] + : (expanded[key] as string) + if (typeof value === 'string') { + if ( + Object.values(ROUTE_PARAMS).includes(key as ROUTE_PARAMS) && + key !== 'publisher' //FIXME: temporary workaround as publisher shouldn't be in ROUTE_PARAMS as it is a search field + ) { + //do nothing + } else if (isDateUrl(value)) { + const [start, end] = value.split('..') + expanded[key] = { + ...(start && { start: new Date(start) }), + ...(end && { end: new Date(end) }), + } + } else { + expanded[key] = value.split(',') } - } else { - expanded[key] = (expanded[key] as string).split(',') } } return expanded @@ -51,3 +57,8 @@ function isDateRange(filter: DateRange): boolean { if (!filter) return false return typeof filter === 'object' && ('start' in filter || 'end' in filter) } + +// this only matches if the separator ".." is present only once and not dots are present elsewhere +function isDateUrl(value: string) { + return value.match(/^[^.]*(\.\.)[^.]*$/) +} diff --git a/libs/feature/router/src/lib/default/state/router.facade.ts b/libs/feature/router/src/lib/default/state/router.facade.ts index 751edbec8a..a76e23114e 100644 --- a/libs/feature/router/src/lib/default/state/router.facade.ts +++ b/libs/feature/router/src/lib/default/state/router.facade.ts @@ -64,7 +64,7 @@ export class RouterFacade { updateSearch(query?: SearchRouteParams) { this.go({ path: this.routerService.getSearchRoute(), - ...(query && flattenQueryParams({ query })), + ...(query && { query: flattenQueryParams(query) }), queryParamsHandling: 'merge', }) } @@ -72,7 +72,7 @@ export class RouterFacade { setSearch(query?: SearchRouteParams) { this.go({ path: this.routerService.getSearchRoute(), - ...(query && flattenQueryParams({ query })), + ...(query && { query: flattenQueryParams(query) }), }) } 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 c7a5b9b5e7..bae8e30e1a 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -20,7 +20,7 @@ import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' import { marker } from '@biesbjerg/ngx-translate-extract-marker' // key is the field name -export type FieldValues = Record +export type FieldValues = Record marker('search.filters.format') marker('search.filters.inspireKeyword') @@ -106,7 +106,7 @@ export class FieldsService { private getFiltersForValues( fieldName: string, - values: FieldValue[] | DateRange | string + values: FieldValue[] | DateRange ) { return this.fields[fieldName].getFiltersForValues(values) } @@ -119,7 +119,7 @@ export class FieldsService { } buildFiltersFromFieldValues( - fieldValues: FieldValues | DateRange | string + fieldValues: FieldValues ): Observable { const fieldNames = Object.keys(fieldValues).filter((fieldName) => this.supportedFields.includes(fieldName) @@ -133,7 +133,7 @@ export class FieldsService { : [fieldValues[fieldName]] //TODO: handle stringified ranges which are not an object properly return this.getFiltersForValues( fieldName, - values as FieldValue[] | DateRange | string + values as FieldValue[] | DateRange ) }) return forkJoin(filtersByField$).pipe( diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index b5db353665..e9c831869d 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -20,7 +20,6 @@ import { import { LangService } from '@geonetwork-ui/util/i18n' export type DateRange = { start?: Date; end?: Date } -export type TimestampRange = { start?: number; end?: number } export type FieldType = 'values' | 'dateRange' export type FieldValue = string | number @@ -32,11 +31,11 @@ export interface FieldAvailableValue { export abstract class AbstractSearchField { abstract getAvailableValues(): Observable abstract getFiltersForValues( - values: FieldValue[] | DateRange | string + values: FieldValue[] | DateRange ): Observable abstract getValuesForFilter( filters: FieldFilters - ): Observable + ): Observable abstract getType(): FieldType } @@ -102,7 +101,7 @@ export class SimpleSearchField implements AbstractSearchField { } getValuesForFilter( filters: FieldFilters - ): Observable { + ): Observable { const filter = filters[this.esFieldName] if (!filter) return of([]) // filter by expression @@ -111,12 +110,7 @@ export class SimpleSearchField implements AbstractSearchField { } // filter by date range if (typeof filter === 'object' && ('start' in filter || 'end' in filter)) { - const range = filter as DateRange - const timeStampFilter = { - start: range.start.getTime(), - end: range.end.getTime(), - } - return of(JSON.stringify(timeStampFilter as TimestampRange)) + return of(filter) } // filter by values const values = Object.keys(filter).filter((v) => filter[v]) From adce683e742dbf74e150e31f7c86c08ad82393bf Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 31 Oct 2024 14:24:01 +0100 Subject: [PATCH 10/42] refactor(date-range): keep one isDateRange() function --- .../router/src/lib/default/state/query-params.utils.ts | 6 +----- libs/feature/search/src/lib/utils/service/fields.ts | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.ts b/libs/feature/router/src/lib/default/state/query-params.utils.ts index 21e3d7f1d9..0cc29c0f06 100644 --- a/libs/feature/router/src/lib/default/state/query-params.utils.ts +++ b/libs/feature/router/src/lib/default/state/query-params.utils.ts @@ -1,3 +1,4 @@ +import { isDateRange } from '@geonetwork-ui/api/repository' import { ROUTE_PARAMS, SearchRouteParams } from '../constants' import { DateRange } from '@geonetwork-ui/feature/search' @@ -53,11 +54,6 @@ export function expandQueryParams( return expanded } -function isDateRange(filter: DateRange): boolean { - if (!filter) return false - return typeof filter === 'object' && ('start' in filter || 'end' in filter) -} - // this only matches if the separator ".." is present only once and not dots are present elsewhere function isDateUrl(value: string) { return value.match(/^[^.]*(\.\.)[^.]*$/) diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index e9c831869d..c17de83e80 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -15,6 +15,7 @@ import { } from '@geonetwork-ui/common/domain/model/search' import { ElasticsearchService, + isDateRange, METADATA_LANGUAGE, } from '@geonetwork-ui/api/repository' import { LangService } from '@geonetwork-ui/util/i18n' @@ -109,7 +110,7 @@ export class SimpleSearchField implements AbstractSearchField { return of([filter]) } // filter by date range - if (typeof filter === 'object' && ('start' in filter || 'end' in filter)) { + if (isDateRange(filter)) { return of(filter) } // filter by values From 1dfeaed8c687ff0d2cacf7baacea02a7b5e80900 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 31 Oct 2024 15:16:03 +0100 Subject: [PATCH 11/42] refactor(fields): clean getFiltersForValues() and pass DateRange as array --- .../search/src/lib/utils/service/fields.service.ts | 12 +++++------- libs/feature/search/src/lib/utils/service/fields.ts | 13 +++++-------- 2 files changed, 10 insertions(+), 15 deletions(-) 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 bae8e30e1a..497f1ddfb8 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -106,7 +106,7 @@ export class FieldsService { private getFiltersForValues( fieldName: string, - values: FieldValue[] | DateRange + values: FieldValue[] | DateRange[] ) { return this.fields[fieldName].getFiltersForValues(values) } @@ -126,14 +126,12 @@ export class FieldsService { ) if (!fieldNames.length) return of({}) const filtersByField$ = fieldNames.map((fieldName) => { - const values = - Array.isArray(fieldValues[fieldName]) || - Object.keys(fieldValues[fieldName]).length > 1 - ? fieldValues[fieldName] - : [fieldValues[fieldName]] //TODO: handle stringified ranges which are not an object properly + const values = Array.isArray(fieldValues[fieldName]) + ? fieldValues[fieldName] + : [fieldValues[fieldName]] return this.getFiltersForValues( fieldName, - values as FieldValue[] | DateRange + values as FieldValue[] | DateRange[] ) }) return forkJoin(filtersByField$).pipe( diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index c17de83e80..e498430cf7 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -32,7 +32,7 @@ export interface FieldAvailableValue { export abstract class AbstractSearchField { abstract getAvailableValues(): Observable abstract getFiltersForValues( - values: FieldValue[] | DateRange + values: FieldValue[] | DateRange[] ): Observable abstract getValuesForFilter( filters: FieldFilters @@ -83,21 +83,18 @@ export class SimpleSearchField implements AbstractSearchField { }) ) } - getFiltersForValues( - values: FieldValue[] | DateRange | string - ): Observable { + getFiltersForValues(values: FieldValue[] | DateRange[]): Observable { // FieldValue[] - if (Array.isArray(values) && this.getType() === 'values') { + if (this.getType() === 'values') { return of({ - [this.esFieldName]: values.reduce((acc, val) => { + [this.esFieldName]: (values as FieldValue[]).reduce((acc, val) => { return { ...acc, [val.toString()]: true } }, {}), }) } - //TODO: find proper solution to handle date ranges as objects and strings // DateRange return of({ - [this.esFieldName]: Array.isArray(values) ? values[0] : values, + [this.esFieldName]: values[0], }) } getValuesForFilter( From 18494106f5e1c635835566b0bf9be90a784f9983 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 31 Oct 2024 16:51:09 +0100 Subject: [PATCH 12/42] feat(filter-dropdown): display date in date picker input --- .../lib/filter-dropdown/filter-dropdown.component.html | 4 +++- .../lib/filter-dropdown/filter-dropdown.component.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html index 710f1c9a7c..6865ca35a3 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html @@ -1,5 +1,7 @@ @@ -8,7 +10,7 @@ class="w-full" [title]="title" [maxRows]="6" - [choices]="choices$ | async" + [choices]="(choices$ | async) || []" [selected]="selected$ | async" [allowSearch]="true" (selectValues)="onSelectedValues($event)" diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts index c0ccb5d648..fbfdf39765 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts @@ -40,6 +40,16 @@ export class FilterDropdownComponent implements OnInit { catchError(() => of([])) ) as Observable + displayDate$ = this.searchFacade.searchFilters$.pipe( + switchMap((filters) => + this.fieldsService.readFieldValuesFromFilters(filters) + ), + map((fieldValues) => fieldValues[this.fieldName]), + filter((selected) => !!selected), + startWith([]), + catchError(() => of([])) + ) as Observable + onSelectedValues(values: unknown[]) { this.fieldsService .buildFiltersFromFieldValues({ [this.fieldName]: values as FieldValue[] }) From ac463c089db53b2f1bec002f338d11b6da844777 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 31 Oct 2024 17:19:25 +0100 Subject: [PATCH 13/42] fix(filter-dropdown): ignore timezones to set exact dates --- .../search/src/lib/filter-dropdown/filter-dropdown.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts index fbfdf39765..d54db3dd16 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts @@ -77,10 +77,12 @@ export class FilterDropdownComponent implements OnInit { } onStartDateChange(start: Date) { + start.setUTCHours(24, 0, 0, 0) this.dateRange = { ...this.dateRange, start } } onEndDateChange(end: Date) { + end.setUTCHours(24, 0, 0, 0) this.dateRange = { ...this.dateRange, end } if (this.dateRange.start && this.dateRange.end) { this.fieldsService From 8cc8b619258928eb958e23873b25ae34975c66b6 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 31 Oct 2024 17:26:26 +0100 Subject: [PATCH 14/42] feat(all-records): remove filter publicationYear --- .../src/app/records/all-records/all-records.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts index d9516441eb..4b68e87f52 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts @@ -77,7 +77,7 @@ export class AllRecordsComponent { importRecordButton!: ElementRef @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef - searchFields = ['user', 'publicationYear', 'changeDate'] + searchFields = ['user', 'changeDate'] searchText$: Observable = this.searchFacade.searchFilters$.pipe( map((filters) => ('any' in filters ? (filters['any'] as string) : null)) From 006f6395e2cd0f38d43f4c3f2caf26f7b350347c Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 5 Nov 2024 16:13:57 +0100 Subject: [PATCH 15/42] feat(date-range): add a date-range-dropdown.component to follow mockups --- .../search/src/lib/feature-search.module.ts | 7 ++- .../filter-dropdown.component.html | 7 +-- .../filter-dropdown.component.ts | 2 + .../src/lib/utils/service/fields.service.ts | 1 + libs/ui/inputs/src/index.ts | 1 + .../date-range-dropdown.component.css | 5 ++ .../date-range-dropdown.component.html | 21 +++++++ .../date-range-dropdown.component.spec.ts | 21 +++++++ .../date-range-dropdown.component.ts | 56 +++++++++++++++++++ translations/de.json | 1 + translations/en.json | 1 + translations/es.json | 1 + translations/fr.json | 1 + translations/it.json | 1 + translations/nl.json | 1 + translations/pt.json | 1 + translations/sk.json | 1 + 17 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css create mode 100644 libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html create mode 100644 libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.spec.ts create mode 100644 libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts diff --git a/libs/feature/search/src/lib/feature-search.module.ts b/libs/feature/search/src/lib/feature-search.module.ts index 32934d68af..ef0a063a3e 100644 --- a/libs/feature/search/src/lib/feature-search.module.ts +++ b/libs/feature/search/src/lib/feature-search.module.ts @@ -14,7 +14,11 @@ import { SearchEffects } from './state/effects' import { initialState, reducer, SEARCH_FEATURE_KEY } from './state/reducer' import { ResultsHitsContainerComponent } from './results-hits-number/results-hits.container.component' import { SearchStateContainerDirective } from './state/container/search-state.container.directive' -import { AutocompleteComponent, UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { + AutocompleteComponent, + DateRangeDropdownComponent, + UiInputsModule, +} from '@geonetwork-ui/ui/inputs' import { NgModule } from '@angular/core' import { ErrorComponent, UiElementsModule } from '@geonetwork-ui/ui/elements' import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' @@ -55,6 +59,7 @@ import { FavoriteStarComponent } from './favorites/favorite-star/favorite-star.c SpinningLoaderComponent, ErrorComponent, FavoriteStarComponent, + DateRangeDropdownComponent, ], exports: [ SortByComponent, diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html index 6865ca35a3..634903ce4e 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html @@ -1,10 +1,9 @@ - +> +
+
+ {{ title }} +
+
+ + expand_less + expand_more + + + + + + + + diff --git a/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.spec.ts b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.spec.ts new file mode 100644 index 0000000000..04a65656c9 --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { DateRangeDropdownComponent } from './date-range-dropdown.component' + +describe('DateRangeDropdownComponent', () => { + let component: DateRangeDropdownComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [DateRangeDropdownComponent], + }) + fixture = TestBed.createComponent(DateRangeDropdownComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts new file mode 100644 index 0000000000..97d36d4ce3 --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts @@ -0,0 +1,56 @@ +import { + AfterViewChecked, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core' +import { CommonModule } from '@angular/common' +import { DateRangePickerComponent } from '../date-range-picker/date-range-picker.component' +import { MatIconModule } from '@angular/material/icon' +import { MatNativeDateModule } from '@angular/material/core' +import { MatDatepickerModule } from '@angular/material/datepicker' +import { ButtonComponent } from '../button/button.component' +import { OverlayContainer } from '@angular/cdk/overlay' + +@Component({ + selector: 'gn-ui-date-range-dropdown', + standalone: true, + imports: [ + CommonModule, + DateRangePickerComponent, + MatIconModule, + MatNativeDateModule, + MatDatepickerModule, + ButtonComponent, + ], + templateUrl: './date-range-dropdown.component.html', + styleUrls: ['./date-range-dropdown.component.css'], +}) +export class DateRangeDropdownComponent implements AfterViewChecked { + @Input() title: string + @Output() startDateChange = new EventEmitter() + @Output() endDateChange = new EventEmitter() + + @ViewChild('picker') picker: ElementRef + isPickerDisplayed = false + + constructor( + private overlayContainer: OverlayContainer, + private cdr: ChangeDetectorRef + ) {} + + ngAfterViewChecked() { + this.checkPickerOverlay() + } + + checkPickerOverlay() { + const overlayContainerElement = this.overlayContainer.getContainerElement() + this.isPickerDisplayed = + overlayContainerElement.querySelector('.mat-datepicker-content') !== null + this.cdr.detectChanges() + } +} diff --git a/translations/de.json b/translations/de.json index 28414b901d..d1d2bc7b08 100644 --- a/translations/de.json +++ b/translations/de.json @@ -496,6 +496,7 @@ "search.error.recordNotFound": "Der Datensatz mit der Kennung \"{ id }\" konnte nicht gefunden werden.", "search.field.any.placeholder": "Suche Datensätze ...", "search.field.sortBy": "Sortieren nach:", + "search.filters.changeDate": "Letzte Aktualisierung", "search.filters.clear": "Zurücksetzen", "search.filters.contact": "Kontakte", "search.filters.format": "Formate", diff --git a/translations/en.json b/translations/en.json index 62ea740aaa..a297336616 100644 --- a/translations/en.json +++ b/translations/en.json @@ -496,6 +496,7 @@ "search.error.recordNotFound": "The record with identifier \"{ id }\" could not be found.", "search.field.any.placeholder": "Search datasets ...", "search.field.sortBy": "Sort by:", + "search.filters.changeDate": "Last updated", "search.filters.clear": "Reset", "search.filters.contact": "Contacts", "search.filters.format": "Formats", diff --git a/translations/es.json b/translations/es.json index 5f21daf824..185b8bf022 100644 --- a/translations/es.json +++ b/translations/es.json @@ -496,6 +496,7 @@ "search.error.recordNotFound": "", "search.field.any.placeholder": "", "search.field.sortBy": "", + "search.filters.changeDate": "Última actualización", "search.filters.clear": "", "search.filters.contact": "", "search.filters.format": "", diff --git a/translations/fr.json b/translations/fr.json index 612edba300..8a6b021add 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -496,6 +496,7 @@ "search.error.recordNotFound": "Cette donnée n'a pu être trouvée.", "search.field.any.placeholder": "Rechercher une donnée...", "search.field.sortBy": "Trier par :", + "search.filters.changeDate": "Dernière mise à jour", "search.filters.clear": "Réinitialiser", "search.filters.contact": "Contacts", "search.filters.format": "Formats", diff --git a/translations/it.json b/translations/it.json index faaa3ae872..445ca702df 100644 --- a/translations/it.json +++ b/translations/it.json @@ -496,6 +496,7 @@ "search.error.recordNotFound": "Impossibile trovare questo dato", "search.field.any.placeholder": "Cerca un dato...", "search.field.sortBy": "Ordina per:", + "search.filters.changeDate": "", "search.filters.clear": "Ripristina", "search.filters.contact": "Contatti", "search.filters.format": "Formati", diff --git a/translations/nl.json b/translations/nl.json index ef84e7e547..b620c75c81 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -496,6 +496,7 @@ "search.error.recordNotFound": "", "search.field.any.placeholder": "", "search.field.sortBy": "", + "search.filters.changeDate": "", "search.filters.clear": "", "search.filters.contact": "", "search.filters.format": "", diff --git a/translations/pt.json b/translations/pt.json index 1144fb5dc4..a532a8a843 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -496,6 +496,7 @@ "search.error.recordNotFound": "", "search.field.any.placeholder": "", "search.field.sortBy": "", + "search.filters.changeDate": "", "search.filters.clear": "", "search.filters.contact": "", "search.filters.format": "", diff --git a/translations/sk.json b/translations/sk.json index 3cfbb37587..088c2d708f 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -496,6 +496,7 @@ "search.error.recordNotFound": "Záznam s identifikátorom \"{ id }\" sa nepodarilo nájsť.", "search.field.any.placeholder": "Hľadať datasety ...", "search.field.sortBy": "Zoradiť podľa:", + "search.filters.changeDate": "", "search.filters.clear": "Obnoviť", "search.filters.contact": "Kontakty", "search.filters.format": "Formáty", From b22225e981136a97a95d5ce0e843c3fbc646c856 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 5 Nov 2024 16:52:40 +0100 Subject: [PATCH 16/42] test(filters): add e2e tests for date range search filter --- .../src/e2e/dashboard.cy.ts | 45 ++++++++++++++++++- .../results-table.component.html | 4 +- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index 6d2b978e28..cb5d4224df 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -296,10 +296,32 @@ describe('dashboard (authenticated)', () => { }) }) describe('search filters', () => { + function checkFilterByChangeDate() { + cy.get('mat-calendar-header').find('button').first().click() + cy.get('mat-multi-year-view').contains('button', '2024').click() + cy.get('mat-year-view').contains('button', 'AUG').click() + cy.get('mat-month-view').contains('button', '1').click() + cy.get('mat-month-view').contains('button', '30').click() + cy.get('gn-ui-interactive-table') + .find('[data-cy="table-row"]') + .should('have.length', '1') + cy.get('gn-ui-results-table') + .find('[data-cy="resultItemTitle"]') + .each(($resultItemTitle) => { + cy.wrap($resultItemTitle) + .invoke('text') + .should('eq', 'Accroches vélos MEL') + }) + } describe('allRecords search filter', () => { beforeEach(() => { cy.visit('/catalog/search') }) + it('should contain filter component with one search filter', () => { + cy.get('md-editor-search-filters') + .find('gn-ui-button') + .should('have.length', 2) + }) it('should filter the record list by editor (Barbara Roberts)', () => { cy.get('md-editor-search-filters').find('gn-ui-button').first().click() cy.get('.cdk-overlay-container') @@ -315,15 +337,34 @@ describe('dashboard (authenticated)', () => { cy.wrap($ownerInfo).invoke('text').should('eq', 'Barbara Roberts') }) }) + it('should filter the record list by last update (changeDate)', () => { + cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + checkFilterByChangeDate() + }) + it('should display the expand icon for the date range dropdown correctly', () => { + cy.get('md-editor-search-filters') + .find('gn-ui-date-range-dropdown') + .find('mat-icon') + .should('contain.text', 'expand_more') + cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + cy.get('md-editor-search-filters') + .find('gn-ui-date-range-dropdown') + .find('mat-icon') + .should('contain.text', 'expand_less') + }) }) describe('myRecords search filters', () => { beforeEach(() => { cy.visit('/my-space/my-records') }) - it('should contain filter component with no search filter for now', () => { + it('should contain filter component with one search filter', () => { cy.get('md-editor-search-filters') .find('gn-ui-button') - .should('not.exist') + .should('have.length', 1) + }) + it('should filter the record list by last update (changeDate)', () => { + cy.get('md-editor-search-filters').find('gn-ui-button').first().click() + checkFilterByChangeDate() }) }) }) diff --git a/libs/ui/search/src/lib/results-table/results-table.component.html b/libs/ui/search/src/lib/results-table/results-table.component.html index 38fcf8fbf7..f24f827cf3 100644 --- a/libs/ui/search/src/lib/results-table/results-table.component.html +++ b/libs/ui/search/src/lib/results-table/results-table.component.html @@ -26,7 +26,9 @@
- {{ item.title }} + {{ + item.title + }} Date: Wed, 6 Nov 2024 11:38:26 +0100 Subject: [PATCH 17/42] test(filters): add unit tests and fix code --- .../elasticsearch/date-range.utils.spec.ts | 46 ++++ .../elasticsearch.service.spec.ts | 69 ++++- .../elasticsearch/elasticsearch.service.ts | 32 ++- .../filter-dropdown.component.spec.ts | 249 ++++++++++++------ .../filter-dropdown.component.ts | 10 - .../lib/utils/service/fields.service.spec.ts | 10 + .../src/lib/utils/service/fields.spec.ts | 67 +++++ .../search/src/lib/utils/service/fields.ts | 2 +- 8 files changed, 370 insertions(+), 115 deletions(-) create mode 100644 libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.spec.ts diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.spec.ts b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.spec.ts new file mode 100644 index 0000000000..7108b689da --- /dev/null +++ b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.spec.ts @@ -0,0 +1,46 @@ +import { isDateRange, formatDate } from './date-range.utils' +import { FieldFilter } from '@geonetwork-ui/common/domain/model/search' + +describe('date-range.utils', () => { + describe('isDateRange', () => { + it('should return false if filter is null or undefined', () => { + expect(isDateRange(null)).toBe(false) + expect(isDateRange(undefined)).toBe(false) + }) + + it('should return false if filter is not an object', () => { + expect(isDateRange('string' as any)).toBe(false) + expect(isDateRange(123 as any)).toBe(false) + }) + + it('should return true if filter has start or end properties', () => { + const filterWithStart: FieldFilter = { start: '2023-01-01' } + const filterWithEnd: FieldFilter = { end: '2023-12-31' } + const filterWithBoth: FieldFilter = { + start: '2023-01-01', + end: '2023-12-31', + } + + expect(isDateRange(filterWithStart)).toBe(true) + expect(isDateRange(filterWithEnd)).toBe(true) + expect(isDateRange(filterWithBoth)).toBe(true) + }) + + it('should return false if filter does not have start or end properties', () => { + const filterWithoutDate: FieldFilter = { someOtherField: 'value' } + expect(isDateRange(filterWithoutDate)).toBe(false) + }) + }) + + describe('formatDate', () => { + it('should format date correctly', () => { + const date = new Date(2023, 11, 31) + expect(formatDate(date)).toBe('2023-12-31') + }) + + it('should handle single digit months and days', () => { + const date = new Date(2023, 3, 5) + expect(formatDate(date)).toBe('2023-04-05') + }) + }) +}) 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 f29c267295..7ad69a3fae 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 @@ -163,16 +163,13 @@ describe('ElasticsearchService', () => { }, }) }) - it('add any and other fields query_strings and limit search payload by ids', () => { + it('add any, other fields query_strings and date range and limit search payload by ids', () => { const query = service['buildPayloadQuery']( { Org: { world: true, }, someDate: { - start: new Date('2021-03-03'), - }, - otherDate: { start: new Date('2020-01-01'), end: new Date('2020-12-31'), }, @@ -197,16 +194,74 @@ describe('ElasticsearchService', () => { { range: { someDate: { - gte: '2021-03-03', + gte: '2020-01-01', + lte: '2020-12-31', format: 'yyyy-MM-dd', }, }, }, + { + ids: { + values: ['record-1', 'record-2', 'record-3'], + }, + }, + ], + should: [], + must: [ + { + query_string: { + default_operator: 'AND', + fields: [ + 'resourceTitleObject.langfre^5', + 'tag.langfre^4', + 'resourceAbstractObject.langfre^3', + 'lineageObject.langfre^2', + 'any.langfre', + 'uuid', + ], + query: 'hello', + }, + }, + ], + must_not: { + terms: { + resourceType: ['service', 'map', 'map/static', 'mapDigital'], + }, + }, + }, + }) + }) + it('handles date range object with start only, and limit search payload by ids', () => { + const query = service['buildPayloadQuery']( + { + Org: { + world: true, + }, + otherDate: { + start: new Date('2021-03-03'), + }, + any: 'hello', + }, + {}, + ['record-1', 'record-2', 'record-3'] + ) + expect(query).toEqual({ + bool: { + filter: [ + { + terms: { + isTemplate: ['n'], + }, + }, + { + query_string: { + query: 'Org:("world")', + }, + }, { range: { otherDate: { - gte: '2020-01-01', - lte: '2020-12-31', + gte: '2021-03-03', format: 'yyyy-MM-dd', }, }, 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 1c64b7a808..21e6c330cd 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -233,14 +233,20 @@ export class ElasticsearchService { }) .join(' OR ') } - const queryString = Object.keys(filters) - .filter((fieldname) => !isDateRange(filters[fieldname])) - .filter( - (fieldname) => - filters[fieldname] && JSON.stringify(filters[fieldname]) !== '{}' - ) - .map((fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`) - .join(' AND ') + const queryString = + typeof filters === 'string' + ? filters + : Object.keys(filters) + .filter((fieldname) => !isDateRange(filters[fieldname])) + .filter( + (fieldname) => + filters[fieldname] && + JSON.stringify(filters[fieldname]) !== '{}' + ) + .map( + (fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})` + ) + .join(' AND ') const queryRange = Object.entries(filters) .filter(([, value]) => isDateRange(value)) .map(([searchField, dateRange]) => { @@ -262,8 +268,12 @@ export class ElasticsearchService { queryRange.dateRange && { range: { [queryRange.searchField]: { - gte: formatDate(queryRange.dateRange.start), - lte: formatDate(queryRange.dateRange.end), + ...(queryRange.dateRange.start && { + gte: formatDate(queryRange.dateRange.start), + }), + ...(queryRange.dateRange.end && { + lte: formatDate(queryRange.dateRange.end), + }), format: 'yyyy-MM-dd', }, }, @@ -512,7 +522,7 @@ export class ElasticsearchService { const filter = aggregation.filters[curr] return { ...prev, - [curr]: this.filtersToQuery(filter), + [curr]: this.filtersToQuery(filter)[0], } }, {}), } diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.spec.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.spec.ts index 293918e081..1172802872 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.spec.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.spec.ts @@ -44,6 +44,7 @@ class FieldsServiceMock { ) ) ) + getFieldType = jest.fn(() => 'values') } @Component({ @@ -60,18 +61,32 @@ export class MockDropdownComponent { @Input() selected: unknown[] @Output() selectValues = new EventEmitter() } +@Component({ + selector: 'gn-ui-date-range-dropdown', + template: '
', +}) +export class MockDateRangeDropdownComponent { + @Input() title: string + @Output() startDateChange = new EventEmitter() + @Output() endDateChange = new EventEmitter() +} describe('FilterDropdownComponent', () => { let facade: SearchFacadeMock let component: FilterDropdownComponent let dropdown: MockDropdownComponent + let dateRangeDropdown: MockDateRangeDropdownComponent let searchService: SearchService let fieldsService: FieldsService let fixture: ComponentFixture beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [FilterDropdownComponent, MockDropdownComponent], + declarations: [ + FilterDropdownComponent, + MockDropdownComponent, + MockDateRangeDropdownComponent, + ], schemas: [NO_ERRORS_SCHEMA], providers: [ { @@ -102,6 +117,7 @@ describe('FilterDropdownComponent', () => { component = fixture.componentInstance component.fieldName = 'Org' + fixture.detectChanges() dropdown = fixture.debugElement.query( By.directive(MockDropdownComponent) ).componentInstance @@ -111,122 +127,183 @@ describe('FilterDropdownComponent', () => { expect(component).toBeTruthy() }) - it('provides selected values initially', () => { - fixture.detectChanges() - expect(dropdown.selected).toEqual([]) + describe('displays dropdown component based on fieldtype', () => { + it('displays dropdown-multiselect for fields of type values', () => { + expect( + fixture.debugElement.query(By.directive(MockDropdownComponent)) + ).toBeTruthy() + expect( + fixture.debugElement.query(By.directive(MockDateRangeDropdownComponent)) + ).toBeFalsy() + }) + it('displays daterange-dropdown for fields of type dateRange', () => { + component.fieldType = 'dateRange' + fixture.detectChanges() + expect( + fixture.debugElement.query(By.directive(MockDropdownComponent)) + ).toBeFalsy() + expect( + fixture.debugElement.query(By.directive(MockDateRangeDropdownComponent)) + ).toBeTruthy() + }) }) - describe('when selected values change', () => { - const values = ['org1', 'org2', 34] - beforeEach(fakeAsync(() => { - dropdown.selectValues.emit(values) - tick() - })) - it('converts values to filters', () => { - expect(fieldsService.buildFiltersFromFieldValues).toHaveBeenCalledWith({ - Org: values, - }) + describe('#dropdown-multiselect', () => { + it('provides selected values initially', () => { + fixture.detectChanges() + expect(dropdown.selected).toEqual([]) }) - it('calls updateSearch on the search service', () => { - expect(searchService.updateFilters).toHaveBeenCalledWith({ - 'converted from values': { + + describe('when selected values change', () => { + const values = ['org1', 'org2', 34] + beforeEach(fakeAsync(() => { + dropdown.selectValues.emit(values) + tick() + })) + it('converts values to filters', () => { + expect(fieldsService.buildFiltersFromFieldValues).toHaveBeenCalledWith({ Org: values, - }, + }) + }) + it('calls updateSearch on the search service', () => { + expect(searchService.updateFilters).toHaveBeenCalledWith({ + 'converted from values': { + Org: values, + }, + }) }) }) - }) - describe('available choices', () => { - describe('on init', () => { - beforeEach(() => { - component.ngOnInit() + describe('available choices', () => { + describe('on init', () => { + beforeEach(() => { + component.ngOnInit() + }) + it('reads available values', () => { + expect(fieldsService.getAvailableValues).toHaveBeenCalledWith('Org') + }) }) - it('reads available values', () => { - expect(fieldsService.getAvailableValues).toHaveBeenCalledWith('Org') + describe('when there are available values', () => { + const values = [ + { label: 'First Org (4)', value: 'First Org' }, + { label: 'Second Org (2)', value: 'Second Org' }, + { label: 'Third Org (1)', value: 'Third Org' }, + ] + beforeEach(() => { + fieldsService.getAvailableValues = () => of(values) + component.ngOnInit() + fixture.detectChanges() + }) + it('reads choices from the search response', () => { + expect(dropdown.choices).toEqual(values) + }) }) - }) - describe('when there are available values', () => { - const values = [ - { label: 'First Org (4)', value: 'First Org' }, - { label: 'Second Org (2)', value: 'Second Org' }, - { label: 'Third Org (1)', value: 'Third Org' }, - ] - beforeEach(() => { - fieldsService.getAvailableValues = () => of(values) - component.ngOnInit() - fixture.detectChanges() + describe('no available values', () => { + beforeEach(() => { + fieldsService.getAvailableValues = () => of([]) + component.ngOnInit() + fixture.detectChanges() + }) + it('uses an empty array', () => { + expect(dropdown.choices).toEqual([]) + }) }) - it('reads choices from the search response', () => { - expect(dropdown.choices).toEqual(values) + describe('available values are numerical', () => { + const values = [ + { label: '1 (4)', value: 1 }, + { label: '2 (2)', value: 2 }, + { label: '3 (1)', value: 3 }, + ] + beforeEach(() => { + fieldsService.getAvailableValues = () => of(values) + component.ngOnInit() + fixture.detectChanges() + }) + it('converts values to string', () => { + expect(dropdown.choices).toEqual([ + { label: '1 (4)', value: '1' }, + { label: '2 (2)', value: '2' }, + { label: '3 (1)', value: '3' }, + ]) + }) }) }) - describe('no available values', () => { + + describe('selected values', () => { + const filters = { + Org: 'bla', + } beforeEach(() => { - fieldsService.getAvailableValues = () => of([]) - component.ngOnInit() + facade.searchFilters$.next(filters) fixture.detectChanges() }) - it('uses an empty array', () => { - expect(dropdown.choices).toEqual([]) + it('converts filters to values', () => { + expect(fieldsService.readFieldValuesFromFilters).toHaveBeenCalledWith( + filters + ) + }) + it('shows selected values in the dropdown', () => { + expect(dropdown.selected).toEqual(['converted from filters', 'bla']) }) }) - describe('available values are numerical', () => { - const values = [ - { label: '1 (4)', value: 1 }, - { label: '2 (2)', value: 2 }, - { label: '3 (1)', value: 3 }, - ] + + describe('field is unsupported', () => { beforeEach(() => { - fieldsService.getAvailableValues = () => of(values) + fieldsService.getAvailableValues = () => + throwError(() => new Error('blah')) + fieldsService.readFieldValuesFromFilters = () => { + throw new Error('blah') + } + fieldsService.buildFiltersFromFieldValues = () => { + throw new Error('blah') + } component.ngOnInit() fixture.detectChanges() }) - it('converts values to string', () => { - expect(dropdown.choices).toEqual([ - { label: '1 (4)', value: '1' }, - { label: '2 (2)', value: '2' }, - { label: '3 (1)', value: '3' }, - ]) + it('still gives an array for choices', () => { + expect(dropdown.choices).toEqual([]) + }) + it('still gives an array for selected', () => { + expect(dropdown.selected).toEqual([]) }) }) }) - describe('selected values', () => { - const filters = { - Org: 'bla', - } + describe('#daterange-dropdown', () => { + const start = new Date('2021-01-01') + const end = new Date('2021-01-02') + beforeEach(() => { - facade.searchFilters$.next(filters) + component.fieldType = 'dateRange' + component.fieldName = 'someDateField' fixture.detectChanges() + dateRangeDropdown = fixture.debugElement.query( + By.directive(MockDateRangeDropdownComponent) + ).componentInstance }) - it('converts filters to values', () => { - expect(fieldsService.readFieldValuesFromFilters).toHaveBeenCalledWith( - filters - ) - }) - it('shows selected values in the dropdown', () => { - expect(dropdown.selected).toEqual(['converted from filters', 'bla']) + it('updates the start date', () => { + dateRangeDropdown.startDateChange.emit(start) + expect(component.dateRange).toEqual({ start }) }) - }) - - describe('field is unsupported', () => { - beforeEach(() => { - fieldsService.getAvailableValues = () => - throwError(() => new Error('blah')) - fieldsService.readFieldValuesFromFilters = () => { - throw new Error('blah') - } - fieldsService.buildFiltersFromFieldValues = () => { - throw new Error('blah') - } - component.ngOnInit() - fixture.detectChanges() + it('updates the end date', () => { + dateRangeDropdown.endDateChange.emit(end) + expect(component.dateRange).toEqual({ end }) }) - it('still gives an array for choices', () => { - expect(dropdown.choices).toEqual([]) + it('calls buildFiltersFromFieldValues with dates', () => { + dateRangeDropdown.startDateChange.emit(start) + dateRangeDropdown.endDateChange.emit(end) + expect(fieldsService.buildFiltersFromFieldValues).toHaveBeenCalledWith({ + someDateField: { start, end }, + }) }) - it('still gives an array for selected', () => { - expect(dropdown.selected).toEqual([]) + it('calls updateSearch on the search service', () => { + dateRangeDropdown.startDateChange.emit(start) + dateRangeDropdown.endDateChange.emit(end) + expect(searchService.updateFilters).toHaveBeenCalledWith({ + 'converted from values': { + someDateField: { start, end }, + }, + }) }) }) }) diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts index 8742e817a9..ea0aaf291b 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts @@ -40,16 +40,6 @@ export class FilterDropdownComponent implements OnInit { catchError(() => of([])) ) as Observable - displayDate$ = this.searchFacade.searchFilters$.pipe( - switchMap((filters) => - this.fieldsService.readFieldValuesFromFilters(filters) - ), - map((fieldValues) => fieldValues[this.fieldName]), - filter((selected) => !!selected), - startWith([]), - catchError(() => of([])) - ) as Observable - onSelectedValues(values: unknown[]) { this.fieldsService .buildFiltersFromFieldValues({ [this.fieldName]: values as FieldValue[] }) diff --git a/libs/feature/search/src/lib/utils/service/fields.service.spec.ts b/libs/feature/search/src/lib/utils/service/fields.service.spec.ts index a973d029a8..fc9934c05b 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.spec.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.spec.ts @@ -102,6 +102,7 @@ describe('FieldsService', () => { 'producerOrg', 'publisherOrg', 'user', + 'changeDate', ]) }) }) @@ -186,8 +187,17 @@ describe('FieldsService', () => { producerOrg: [], publisherOrg: [], user: [], + changeDate: [], }) }) }) + describe('#getFieldType', () => { + it('returns the field type', () => { + expect(service.getFieldType('organization')).toEqual('values') + expect(service.getFieldType('publicationYear')).toEqual('values') + expect(service.getFieldType('format')).toEqual('values') + expect(service.getFieldType('changeDate')).toEqual('dateRange') + }) + }) }) }) 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 39facadac2..95b2fce3ec 100644 --- a/libs/feature/search/src/lib/utils/service/fields.spec.ts +++ b/libs/feature/search/src/lib/utils/service/fields.spec.ts @@ -9,6 +9,7 @@ import { SimpleSearchField, MultilingualSearchField, UserSearchField, + DateRangeSearchField, } from './fields' import { TestBed } from '@angular/core/testing' import { Injector } from '@angular/core' @@ -330,6 +331,72 @@ describe('search fields implementations', () => { }) }) + describe('DateRangeSearchField (SimpleSearchField with date range)', () => { + beforeEach(() => { + searchField = new DateRangeSearchField('changeDate', injector, 'desc') + }) + describe('#getAvailableValues', () => { + let values + beforeEach(async () => { + values = await lastValueFrom(searchField.getAvailableValues()) + }) + it('returns an empty list of values for now', () => { + expect(values).toEqual([]) + }) + }) + describe('#getFiltersForValues', () => { + let filter + beforeEach(async () => { + filter = await lastValueFrom( + searchField.getFiltersForValues([ + { start: new Date('2020-01-01'), end: new Date('2020-12-31') }, + ]) + ) + }) + it('returns appropriate filters', () => { + expect(filter).toEqual({ + changeDate: { + start: new Date('2020-01-01'), + end: new Date('2020-12-31'), + }, + }) + }) + }) + describe('#getValuesForFilters', () => { + let values + describe('with several values', () => { + beforeEach(async () => { + values = await lastValueFrom( + searchField.getValuesForFilter({ + changeDate: { + start: new Date('2020-01-01'), + end: new Date('2020-12-31'), + }, + }) + ) + }) + it('returns filtered values', () => { + expect(values).toEqual({ + start: new Date('2020-01-01'), + end: new Date('2020-12-31'), + }) + }) + }) + describe('with a unique value', () => { + beforeEach(async () => { + values = await lastValueFrom( + searchField.getValuesForFilter({ + changeDate: { start: new Date('2020-01-01') }, + }) + ) + }) + it('returns the only value', () => { + expect(values).toEqual({ start: new Date('2020-01-01') }) + }) + }) + }) + }) + describe('TranslatedSearchField', () => { describe('sort by key', () => { beforeEach(() => { diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index e498430cf7..a6ea5121b2 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -418,7 +418,7 @@ export class DateRangeSearchField extends SimpleSearchField { super(esFieldName, injector, order, orderType) } - getAvailableValues(): Observable { + getAvailableValues(): Observable { // TODO: return an array of dates to show which one are available in the date picker return of([]) } From 32b7c5cf3f98d4459e6fe71db6e1931ef4a7ef0e Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 6 Nov 2024 11:47:03 +0100 Subject: [PATCH 18/42] fix(date-range): define and export type once --- libs/feature/router/src/lib/default/constants.ts | 7 ++----- .../router/src/lib/default/state/query-params.utils.ts | 3 +-- .../src/lib/filter-dropdown/filter-dropdown.component.ts | 2 +- .../feature/search/src/lib/utils/service/fields.service.ts | 2 +- libs/feature/search/src/lib/utils/service/fields.ts | 2 +- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/libs/feature/router/src/lib/default/constants.ts b/libs/feature/router/src/lib/default/constants.ts index 2a77b229e1..f09a71a860 100644 --- a/libs/feature/router/src/lib/default/constants.ts +++ b/libs/feature/router/src/lib/default/constants.ts @@ -1,8 +1,5 @@ -import { - DateRange, - FieldsService, - FieldValue, -} from '@geonetwork-ui/feature/search' +import { DateRange } from '@geonetwork-ui/api/repository' +// import { FieldsService, FieldValue } from '@geonetwork-ui/feature/search' export const ROUTER_STATE_KEY = 'router' diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.ts b/libs/feature/router/src/lib/default/state/query-params.utils.ts index 0cc29c0f06..a3c5e33778 100644 --- a/libs/feature/router/src/lib/default/state/query-params.utils.ts +++ b/libs/feature/router/src/lib/default/state/query-params.utils.ts @@ -1,6 +1,5 @@ -import { isDateRange } from '@geonetwork-ui/api/repository' +import { DateRange, isDateRange } from '@geonetwork-ui/api/repository' import { ROUTE_PARAMS, SearchRouteParams } from '../constants' -import { DateRange } from '@geonetwork-ui/feature/search' export function flattenQueryParams( params: SearchRouteParams diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts index ea0aaf291b..388ea27222 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts @@ -11,11 +11,11 @@ import { SearchFacade } from '../state/search.facade' import { SearchService } from '../utils/service/search.service' import { FieldsService } from '../utils/service/fields.service' import { - DateRange, FieldAvailableValue, FieldType, FieldValue, } from '../utils/service/fields' +import { DateRange } from '@geonetwork-ui/api/repository' @Component({ selector: 'gn-ui-filter-dropdown', 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 3c4df1ad92..d79ed22684 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -1,7 +1,6 @@ import { Injectable, Injector } from '@angular/core' import { AbstractSearchField, - DateRange, DateRangeSearchField, FieldValue, FullTextSearchField, @@ -18,6 +17,7 @@ import { forkJoin, Observable, of } from 'rxjs' import { map } from 'rxjs/operators' import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' import { marker } from '@biesbjerg/ngx-translate-extract-marker' +import { DateRange } from '@geonetwork-ui/api/repository' // key is the field name export type FieldValues = Record diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index a6ea5121b2..3e5620043e 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -14,13 +14,13 @@ import { TermBucket, } from '@geonetwork-ui/common/domain/model/search' import { + DateRange, ElasticsearchService, isDateRange, METADATA_LANGUAGE, } from '@geonetwork-ui/api/repository' import { LangService } from '@geonetwork-ui/util/i18n' -export type DateRange = { start?: Date; end?: Date } export type FieldType = 'values' | 'dateRange' export type FieldValue = string | number From 1a3f86afa2ff0473078a494ddb8ebc4c13a1fc13 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 13 Nov 2024 09:30:35 +0100 Subject: [PATCH 19/42] review(search filters): address comments --- apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts | 2 +- libs/feature/router/src/lib/default/state/query-params.utils.ts | 2 +- .../lib/date-range-dropdown/date-range-dropdown.component.css | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index cb5d4224df..13a9215b91 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -317,7 +317,7 @@ describe('dashboard (authenticated)', () => { beforeEach(() => { cy.visit('/catalog/search') }) - it('should contain filter component with one search filter', () => { + it('should contain filter component with two search filters', () => { cy.get('md-editor-search-filters') .find('gn-ui-button') .should('have.length', 2) diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.ts b/libs/feature/router/src/lib/default/state/query-params.utils.ts index a3c5e33778..73ea22fa84 100644 --- a/libs/feature/router/src/lib/default/state/query-params.utils.ts +++ b/libs/feature/router/src/lib/default/state/query-params.utils.ts @@ -53,7 +53,7 @@ export function expandQueryParams( return expanded } -// this only matches if the separator ".." is present only once and not dots are present elsewhere +// this only matches if the separator ".." is present only once and no dots are present elsewhere function isDateUrl(value: string) { return value.match(/^[^.]*(\.\.)[^.]*$/) } diff --git a/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css index b33dfb3cc3..f3c90ef140 100644 --- a/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +++ b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css @@ -1,5 +1,5 @@ :host { .mat-date-range-input-container { - display: none !important; + display: none; } } From bf9cae03652a678a5a1f069c7216df1e985fc07a Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 13 Nov 2024 12:49:49 +0100 Subject: [PATCH 20/42] feat(date-range): synchronize date-range-dropdown with selected date range even without the date input, this is used in the calendar display --- .../filter-dropdown/filter-dropdown.component.html | 2 ++ .../lib/filter-dropdown/filter-dropdown.component.ts | 4 ++++ .../date-range-dropdown.component.html | 12 ++++++++++-- .../date-range-dropdown.component.ts | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html index 634903ce4e..661f7fb47f 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html @@ -1,6 +1,8 @@ diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts index 388ea27222..d5bb87d075 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts @@ -40,6 +40,10 @@ export class FilterDropdownComponent implements OnInit { catchError(() => of([])) ) as Observable + selectedDateRange$ = this.selected$.pipe( + map((selectedDateRange) => selectedDateRange as DateRange) + ) as Observable + onSelectedValues(values: unknown[]) { this.fieldsService .buildFiltersFromFieldValues({ [this.fieldName]: values as FieldValue[] }) diff --git a/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html index 999830e5cf..1ae1354365 100644 --- a/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +++ b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html @@ -15,7 +15,15 @@ - - + + diff --git a/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts index 97d36d4ce3..14f6c88aec 100644 --- a/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +++ b/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts @@ -32,6 +32,8 @@ import { OverlayContainer } from '@angular/cdk/overlay' }) export class DateRangeDropdownComponent implements AfterViewChecked { @Input() title: string + @Input() startDate: Date + @Input() endDate: Date @Output() startDateChange = new EventEmitter() @Output() endDateChange = new EventEmitter() From e4db9eaa195a736a77311448683f1740e7270058 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 26 Nov 2024 09:25:12 +0100 Subject: [PATCH 21/42] chore(router): clean commented type --- libs/feature/router/src/lib/default/constants.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libs/feature/router/src/lib/default/constants.ts b/libs/feature/router/src/lib/default/constants.ts index f09a71a860..bbb46353c2 100644 --- a/libs/feature/router/src/lib/default/constants.ts +++ b/libs/feature/router/src/lib/default/constants.ts @@ -1,5 +1,4 @@ import { DateRange } from '@geonetwork-ui/api/repository' -// import { FieldsService, FieldValue } from '@geonetwork-ui/feature/search' export const ROUTER_STATE_KEY = 'router' @@ -16,9 +15,3 @@ export type SearchRouteParams = Record< string, string | string[] | number | DateRange > -//TODO: type RouteParamKey correctly -// type RouteParamKey = ROUTE_PARAMS[string] | FieldsService['fields'][string] -// export type SearchRouteParams = Record< -// RouteParamKey, -// FieldValue[] | FieldValue | DateRange -// > From b96614381f0a026a474fc0e6796f1768087af1a3 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 8 Nov 2024 12:13:34 +0100 Subject: [PATCH 22/42] feat(search-filters): add search-filters-summary --- .../search-filters-summary.component.css | 0 .../search-filters-summary.component.html | 13 ++++ .../search-filters-summary.component.spec.ts | 21 +++++++ .../search-filters-summary.component.ts | 61 +++++++++++++++++++ .../search-filters.component.html | 27 +++++--- .../search-filters.component.ts | 2 + .../src/lib/utils/service/fields.service.ts | 3 + translations/de.json | 2 + translations/en.json | 2 + translations/es.json | 2 + translations/fr.json | 2 + translations/it.json | 2 + translations/nl.json | 2 + translations/pt.json | 2 + translations/sk.json | 2 + 15 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html new file mode 100644 index 0000000000..3f060e4ff9 --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html @@ -0,0 +1,13 @@ +
+
+ + search.filters.summaryLabel.{{ item.key }}{{ itemValue }} + +
+
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts new file mode 100644 index 0000000000..041b9755eb --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { SearchFiltersSummaryComponent } from './search-filters-summary.component' + +describe('SearchFiltersSummaryComponent', () => { + let component: SearchFiltersSummaryComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SearchFiltersSummaryComponent], + }) + fixture = TestBed.createComponent(SearchFiltersSummaryComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts new file mode 100644 index 0000000000..0954ad8b04 --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core' +import { CommonModule } from '@angular/common' +import { + FieldsService, + FieldValue, + FieldValues, + SearchFacade, +} from '@geonetwork-ui/feature/search' +import { + catchError, + filter, + map, + Observable, + startWith, + switchMap, + tap, +} from 'rxjs' +import { BadgeComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' +import { isDateRange } from '@geonetwork-ui/api/repository' + +@Component({ + selector: 'md-editor-search-filters-summary', + standalone: true, + imports: [CommonModule, TranslateModule, BadgeComponent], + templateUrl: './search-filters-summary.component.html', + styleUrls: ['./search-filters-summary.component.css'], +}) +export class SearchFiltersSummaryComponent { + fieldValues$ = this.searchFacade.searchFilters$.pipe( + switchMap((filters) => + this.fieldsService.readFieldValuesFromFilters(filters) + ), + tap((fieldValues) => console.log(fieldValues)), + map((fieldValues) => this.filterEmptyValues(fieldValues)), + tap((fieldValues) => console.log(fieldValues)) + // map((fieldValues) => fieldValues[this.fieldName]), + // filter((selected) => !!selected), + // startWith([]), + // catchError(() => of([])) + ) as Observable> // TODO: transform date objects to arrays + + constructor( + private searchFacade: SearchFacade, + private fieldsService: FieldsService + ) {} + + filterEmptyValues(fieldValues: any): any { + return Object.fromEntries( + Object.entries(fieldValues).filter( + ([key, value]) => + (Array.isArray(value) && value.length > 0) || isDateRange(value) + ) + ) + } + + removeFilterValue(key: string, fieldValue: any) { + // TODO + console.log('removeFilterValue', key, fieldValue) + } +} diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html index 75710a0f5e..750d2e9dc9 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html @@ -1,13 +1,20 @@
- - +
+ + +
+
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts index 68cc32e55e..26fd248350 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts @@ -8,6 +8,7 @@ import { provideNgIconsConfig, } from '@ng-icons/core' import { iconoirFilterList } from '@ng-icons/iconoir' +import { SearchFiltersSummaryComponent } from '../search-filters-summary/search-filters-summary.component' @Component({ selector: 'md-editor-search-filters', @@ -17,6 +18,7 @@ import { iconoirFilterList } from '@ng-icons/iconoir' TranslateModule, FeatureSearchModule, NgIconComponent, + SearchFiltersSummaryComponent, ], providers: [ provideIcons({ 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..81364c5943 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -39,6 +39,9 @@ marker('search.filters.publisherOrg') marker('search.filters.user') marker('search.filters.changeDate') +marker('search.filters.summaryLabel.user') +marker('search.filters.summaryLabel.changeDate') + @Injectable({ providedIn: 'root', }) diff --git a/translations/de.json b/translations/de.json index d1d2bc7b08..e1f39e6ada 100644 --- a/translations/de.json +++ b/translations/de.json @@ -527,6 +527,8 @@ "search.filters.representationType": "Repräsentationstyp", "search.filters.resourceType": "Ressourcentyp", "search.filters.standard": "Standard", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "Ergebnisse filtern", "search.filters.topic": "Themen", "search.filters.useSpatialFilter": "Zuerst Datensätze im Interessenbereich anzeigen", diff --git a/translations/en.json b/translations/en.json index a297336616..857cd3a952 100644 --- a/translations/en.json +++ b/translations/en.json @@ -527,6 +527,8 @@ "search.filters.representationType": "Representation type", "search.filters.resourceType": "Resource type", "search.filters.standard": "Standard", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "Filter your results", "search.filters.topic": "Topics", "search.filters.useSpatialFilter": "Show records in the area of interest first", diff --git a/translations/es.json b/translations/es.json index 185b8bf022..2d179493da 100644 --- a/translations/es.json +++ b/translations/es.json @@ -527,6 +527,8 @@ "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "", "search.filters.topic": "", "search.filters.useSpatialFilter": "", diff --git a/translations/fr.json b/translations/fr.json index 8a6b021add..76054eaf30 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -527,6 +527,8 @@ "search.filters.representationType": "Type de représentation", "search.filters.resourceType": "Type de ressource", "search.filters.standard": "Standard", + "search.filters.summaryLabel.changeDate": "Modifiée le", + "search.filters.summaryLabel.user": "Modifiée par", "search.filters.title": "Affiner votre recherche", "search.filters.topic": "Thèmes", "search.filters.useSpatialFilter": "Mettre en avant les résultats sur la zone d'intérêt", diff --git a/translations/it.json b/translations/it.json index 445ca702df..9d63219f34 100644 --- a/translations/it.json +++ b/translations/it.json @@ -527,6 +527,8 @@ "search.filters.representationType": "Tipo di rappresentazione", "search.filters.resourceType": "Tipo di risorsa", "search.filters.standard": "Standard", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "Affina la sua ricerca", "search.filters.topic": "Argomenti", "search.filters.useSpatialFilter": "Evidenzia i risultati nell'area di interesse", diff --git a/translations/nl.json b/translations/nl.json index b620c75c81..e51a598a98 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -527,6 +527,8 @@ "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "", "search.filters.topic": "", "search.filters.useSpatialFilter": "", diff --git a/translations/pt.json b/translations/pt.json index a532a8a843..cfe9539f16 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -527,6 +527,8 @@ "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "", "search.filters.topic": "", "search.filters.useSpatialFilter": "", diff --git a/translations/sk.json b/translations/sk.json index 088c2d708f..b95f54ffd2 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -527,6 +527,8 @@ "search.filters.representationType": "Typ reprezentácie", "search.filters.resourceType": "Typ zdroja", "search.filters.standard": "Štandard", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "Filtrovanie výsledkov", "search.filters.topic": "Témy", "search.filters.useSpatialFilter": "Najskôr zobraziť záznamy v oblasti záujmu", From 57d9deaf66647b3d880916626e764c5f66aa2913 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 8 Nov 2024 14:21:26 +0100 Subject: [PATCH 23/42] refactor(search-filters): refactor summary component --- .../search-filters-summary.component.html | 31 +++++++------ .../search-filters-summary.component.ts | 43 ++++++++++++------- .../search-filters.component.html | 9 +++- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html index 3f060e4ff9..040f0144e0 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html @@ -1,13 +1,20 @@ -
-
- - search.filters.summaryLabel.{{ item.key }}{{ itemValue }} - -
+
+ search.filters.summaryLabel.{{ fieldName }} + {{ fieldValue['start'] }} - >{{ fieldValue['end'] }}
+ + search.filters.summaryLabel.{{ fieldName }} + {{ fieldValue }} + diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts index 0954ad8b04..d6124fce2e 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts @@ -1,7 +1,8 @@ -import { Component } from '@angular/core' +import { Component, Input, OnInit } from '@angular/core' import { CommonModule } from '@angular/common' import { FieldsService, + FieldType, FieldValue, FieldValues, SearchFacade, @@ -17,7 +18,7 @@ import { } from 'rxjs' import { BadgeComponent } from '@geonetwork-ui/ui/inputs' import { TranslateModule } from '@ngx-translate/core' -import { isDateRange } from '@geonetwork-ui/api/repository' +import { DateRange } from '@geonetwork-ui/api/repository' @Component({ selector: 'md-editor-search-filters-summary', @@ -26,36 +27,46 @@ import { isDateRange } from '@geonetwork-ui/api/repository' templateUrl: './search-filters-summary.component.html', styleUrls: ['./search-filters-summary.component.css'], }) -export class SearchFiltersSummaryComponent { +export class SearchFiltersSummaryComponent implements OnInit { + @Input() fieldName: string + fieldType: FieldType + fieldValues$ = this.searchFacade.searchFilters$.pipe( switchMap((filters) => this.fieldsService.readFieldValuesFromFilters(filters) ), tap((fieldValues) => console.log(fieldValues)), - map((fieldValues) => this.filterEmptyValues(fieldValues)), + map((fieldValues) => + Array.isArray(fieldValues[this.fieldName]) //TODO: handle date ranges as arrays averywhere? + ? fieldValues[this.fieldName] + : [fieldValues[this.fieldName]] + ), tap((fieldValues) => console.log(fieldValues)) - // map((fieldValues) => fieldValues[this.fieldName]), - // filter((selected) => !!selected), // startWith([]), // catchError(() => of([])) - ) as Observable> // TODO: transform date objects to arrays + ) as Observable + + dateRange$ = this.fieldValues$.pipe( + filter(() => this.fieldType === 'dateRange'), + map((fieldValues) => fieldValues as DateRange[]) + ) + + values$ = this.fieldValues$.pipe( + filter(() => this.fieldType === 'values'), + map((fieldValues) => fieldValues as FieldValue[]) + ) constructor( private searchFacade: SearchFacade, private fieldsService: FieldsService ) {} - filterEmptyValues(fieldValues: any): any { - return Object.fromEntries( - Object.entries(fieldValues).filter( - ([key, value]) => - (Array.isArray(value) && value.length > 0) || isDateRange(value) - ) - ) + ngOnInit() { + this.fieldType = this.fieldsService.getFieldType(this.fieldName) } - removeFilterValue(key: string, fieldValue: any) { + removeFilterValue(fieldValue: any) { // TODO - console.log('removeFilterValue', key, fieldValue) + console.log('removeFilterValue', this.fieldName, fieldValue) } } diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html index 750d2e9dc9..b3c9cfa7db 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html @@ -14,7 +14,12 @@ [style.--gn-ui-multiselect-counter-background-color]="'white'" >
- + > + +
From 33b27889574bf9498a8b59a44cebde948492b2cb Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 8 Nov 2024 14:27:50 +0100 Subject: [PATCH 24/42] refactor(search-filters-summary): rename component into search-filters-summary-item --- ...search-filters-summary-item.component.css} | 0 ...earch-filters-summary-item.component.html} | 0 ...rch-filters-summary-item.component.spec.ts | 21 +++++++++++++++++++ .../search-filters-summary-item.component.ts} | 8 +++---- .../search-filters-summary.component.spec.ts | 21 ------------------- .../search-filters.component.html | 4 ++-- .../search-filters.component.ts | 6 ++++-- 7 files changed, 31 insertions(+), 29 deletions(-) rename apps/metadata-editor/src/app/dashboard/{search-filters-summary/search-filters-summary.component.css => search-filters-summary-item/search-filters-summary-item.component.css} (100%) rename apps/metadata-editor/src/app/dashboard/{search-filters-summary/search-filters-summary.component.html => search-filters-summary-item/search-filters-summary-item.component.html} (100%) create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.spec.ts rename apps/metadata-editor/src/app/dashboard/{search-filters-summary/search-filters-summary.component.ts => search-filters-summary-item/search-filters-summary-item.component.ts} (88%) delete mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.css similarity index 100% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css rename to apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.css diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html similarity index 100% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html rename to apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.spec.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.spec.ts new file mode 100644 index 0000000000..19fcb7e286 --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { SearchFiltersSummaryItemComponent } from './search-filters-summary-item.component' + +describe('SearchFiltersSummaryComponent', () => { + let component: SearchFiltersSummaryItemComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SearchFiltersSummaryItemComponent], + }) + fixture = TestBed.createComponent(SearchFiltersSummaryItemComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts similarity index 88% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts rename to apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts index d6124fce2e..a1cb8584bd 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts @@ -21,13 +21,13 @@ import { TranslateModule } from '@ngx-translate/core' import { DateRange } from '@geonetwork-ui/api/repository' @Component({ - selector: 'md-editor-search-filters-summary', + selector: 'md-editor-search-filters-summary-item', standalone: true, imports: [CommonModule, TranslateModule, BadgeComponent], - templateUrl: './search-filters-summary.component.html', - styleUrls: ['./search-filters-summary.component.css'], + templateUrl: './search-filters-summary-item.component.html', + styleUrls: ['./search-filters-summary-item.component.css'], }) -export class SearchFiltersSummaryComponent implements OnInit { +export class SearchFiltersSummaryItemComponent implements OnInit { @Input() fieldName: string fieldType: FieldType diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts deleted file mode 100644 index 041b9755eb..0000000000 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' - -import { SearchFiltersSummaryComponent } from './search-filters-summary.component' - -describe('SearchFiltersSummaryComponent', () => { - let component: SearchFiltersSummaryComponent - let fixture: ComponentFixture - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [SearchFiltersSummaryComponent], - }) - fixture = TestBed.createComponent(SearchFiltersSummaryComponent) - component = fixture.componentInstance - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) -}) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html index b3c9cfa7db..507d3a2506 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html @@ -17,9 +17,9 @@
- + >
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts index 26fd248350..72796c3910 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts @@ -8,7 +8,8 @@ import { provideNgIconsConfig, } from '@ng-icons/core' import { iconoirFilterList } from '@ng-icons/iconoir' -import { SearchFiltersSummaryComponent } from '../search-filters-summary/search-filters-summary.component' +import { MatIconModule } from '@angular/material/icon' +import { SearchFiltersSummaryItemComponent } from '../search-filters-summary-item/search-filters-summary-item.component' @Component({ selector: 'md-editor-search-filters', @@ -18,7 +19,8 @@ import { SearchFiltersSummaryComponent } from '../search-filters-summary/search- TranslateModule, FeatureSearchModule, NgIconComponent, - SearchFiltersSummaryComponent, + MatIconModule, + SearchFiltersSummaryItemComponent, ], providers: [ provideIcons({ From a3757eae030b8955543b18be86ef0625cecb0286 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 13 Nov 2024 10:18:19 +0100 Subject: [PATCH 25/42] feat(search-filters): display summary and filter labels only if values present --- ...search-filters-summary-item.component.html | 40 ++++++++++--------- .../search-filters.component.html | 1 + .../search-filters.component.ts | 21 +++++++++- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html index 040f0144e0..9f7147d11e 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html @@ -1,20 +1,22 @@ -
- search.filters.summaryLabel.{{ fieldName }} - {{ fieldValue['start'] }} - >{{ fieldValue['end'] }} +
+
+ search.filters.summaryLabel.{{ fieldName }} + {{ fieldValue['start'] }} - >{{ fieldValue['end'] }} +
+ + search.filters.summaryLabel.{{ fieldName }} + {{ fieldValue }} +
- - search.filters.summaryLabel.{{ fieldName }} - {{ fieldValue }} - diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html index 507d3a2506..04a87166d8 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html @@ -16,6 +16,7 @@
this.hasNonEmptyValues(filters)) + ) + + constructor(private searchFacade: SearchFacade) {} + ngOnInit(): void { this.searchConfig = this.searchFields.map((filter) => ({ fieldName: filter, title: `search.filters.${filter}`, })) } + + hasNonEmptyValues(filters: any): boolean { + return Object.values(filters).some( + (value) => + value !== undefined && + (typeof value !== 'object' || + (typeof value === 'object' && Object.keys(value).length > 0)) + ) + } } From e80aa114b44d77ece14fd8b06e24e1746af117eb Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 13 Nov 2024 10:39:50 +0100 Subject: [PATCH 26/42] feat(search-filters): introduce intermediate search-filters-summary.component --- .../search-filters-summary.component.css | 0 .../search-filters-summary.component.html | 9 ++++++ .../search-filters-summary.component.spec.ts | 21 +++++++++++++ .../search-filters-summary.component.ts | 31 +++++++++++++++++++ .../search-filters.component.html | 12 ++----- .../search-filters.component.ts | 25 ++------------- 6 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts create mode 100644 apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html new file mode 100644 index 0000000000..e7633e298a --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html @@ -0,0 +1,9 @@ +
+ +
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts new file mode 100644 index 0000000000..d4defdbb60 --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { SearchFiltersSummaryComponent } from './search-filters-summary.component' + +describe('SearchFiltersSummaryComponent', () => { + let component: SearchFiltersSummaryComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SearchFiltersSummaryComponent], + }).compileComponents() + + fixture = TestBed.createComponent(SearchFiltersSummaryComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts new file mode 100644 index 0000000000..9999272c33 --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts @@ -0,0 +1,31 @@ +import { Component, Input } from '@angular/core' +import { CommonModule } from '@angular/common' +import { SearchFacade } from '@geonetwork-ui/feature/search' +import { map } from 'rxjs' +import { SearchFiltersSummaryItemComponent } from '../search-filters-summary-item/search-filters-summary-item.component' + +@Component({ + selector: 'md-editor-search-filters-summary', + standalone: true, + imports: [CommonModule, SearchFiltersSummaryItemComponent], + templateUrl: './search-filters-summary.component.html', + styleUrls: ['./search-filters-summary.component.css'], +}) +export class SearchFiltersSummaryComponent { + @Input() searchFields: string[] = [] + + searchFilterActive$ = this.searchFacade.searchFilters$.pipe( + map((filters) => this.hasNonEmptyValues(filters)) + ) + + constructor(private searchFacade: SearchFacade) {} + + hasNonEmptyValues(filters: any): boolean { + return Object.values(filters).some( + (value) => + value !== undefined && + (typeof value !== 'object' || + (typeof value === 'object' && Object.keys(value).length > 0)) + ) + } +} diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html index 04a87166d8..91cc57132e 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html @@ -14,13 +14,7 @@ [style.--gn-ui-multiselect-counter-background-color]="'white'" >
-
- -
+ diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts index c1acb7f04a..f38f562f22 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts @@ -7,13 +7,9 @@ import { provideNgIconsConfig, } from '@ng-icons/core' import { iconoirFilterList } from '@ng-icons/iconoir' -import { - FeatureSearchModule, - SearchFacade, -} from '@geonetwork-ui/feature/search' +import { FeatureSearchModule } from '@geonetwork-ui/feature/search' import { MatIconModule } from '@angular/material/icon' -import { SearchFiltersSummaryItemComponent } from '../search-filters-summary-item/search-filters-summary-item.component' -import { map } from 'rxjs' +import { SearchFiltersSummaryComponent } from '../search-filters-summary/search-filters-summary.component' @Component({ selector: 'md-editor-search-filters', @@ -24,7 +20,7 @@ import { map } from 'rxjs' FeatureSearchModule, NgIconComponent, MatIconModule, - SearchFiltersSummaryItemComponent, + SearchFiltersSummaryComponent, ], providers: [ provideIcons({ @@ -41,25 +37,10 @@ export class SearchFiltersComponent implements OnInit { @Input() searchFields: string[] = [] searchConfig: { fieldName: string; title: string }[] - searchFilterActive$ = this.searchFacade.searchFilters$.pipe( - map((filters) => this.hasNonEmptyValues(filters)) - ) - - constructor(private searchFacade: SearchFacade) {} - ngOnInit(): void { this.searchConfig = this.searchFields.map((filter) => ({ fieldName: filter, title: `search.filters.${filter}`, })) } - - hasNonEmptyValues(filters: any): boolean { - return Object.values(filters).some( - (value) => - value !== undefined && - (typeof value !== 'object' || - (typeof value === 'object' && Object.keys(value).length > 0)) - ) - } } From b8a909087237ac628a6b7b677ac0ba1f14b50a2f Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 13 Nov 2024 12:03:19 +0100 Subject: [PATCH 27/42] feat(search-filter-summary): allow to remove one filter value and clear all --- .../search-filters-summary-item.component.ts | 22 ++++++++++++++----- .../search-filters-summary.component.html | 3 +++ .../search-filters-summary.component.ts | 16 ++++++++++---- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts index a1cb8584bd..221636c2c5 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core' +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { CommonModule } from '@angular/common' import { FieldsService, @@ -6,10 +6,12 @@ import { FieldValue, FieldValues, SearchFacade, + SearchService, } from '@geonetwork-ui/feature/search' import { catchError, filter, + firstValueFrom, map, Observable, startWith, @@ -37,7 +39,7 @@ export class SearchFiltersSummaryItemComponent implements OnInit { ), tap((fieldValues) => console.log(fieldValues)), map((fieldValues) => - Array.isArray(fieldValues[this.fieldName]) //TODO: handle date ranges as arrays averywhere? + Array.isArray(fieldValues[this.fieldName]) //TODO: handle date ranges as arrays everywhere? ? fieldValues[this.fieldName] : [fieldValues[this.fieldName]] ), @@ -58,6 +60,7 @@ export class SearchFiltersSummaryItemComponent implements OnInit { constructor( private searchFacade: SearchFacade, + private searchService: SearchService, private fieldsService: FieldsService ) {} @@ -65,8 +68,17 @@ export class SearchFiltersSummaryItemComponent implements OnInit { this.fieldType = this.fieldsService.getFieldType(this.fieldName) } - removeFilterValue(fieldValue: any) { - // TODO - console.log('removeFilterValue', this.fieldName, fieldValue) + async removeFilterValue(fieldValue: FieldValue | DateRange) { + const currentFieldValues: (FieldValue | DateRange)[] = await firstValueFrom( + this.fieldValues$ + ) + const updatedFieldValues = currentFieldValues.filter( + (value: string | DateRange) => value !== fieldValue + ) + this.fieldsService + .buildFiltersFromFieldValues({ + [this.fieldName]: updatedFieldValues as FieldValue[], + }) + .subscribe((filters) => this.searchService.updateFilters(filters)) } } diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html index e7633e298a..0ac1b069f4 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html @@ -6,4 +6,7 @@ *ngFor="let field of searchFields; let i = index" [fieldName]="field" > + diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts index 9999272c33..04ad02a1d9 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.ts @@ -1,15 +1,16 @@ import { Component, Input } from '@angular/core' import { CommonModule } from '@angular/common' -import { SearchFacade } from '@geonetwork-ui/feature/search' +import { SearchFacade, SearchService } from '@geonetwork-ui/feature/search' import { map } from 'rxjs' import { SearchFiltersSummaryItemComponent } from '../search-filters-summary-item/search-filters-summary-item.component' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'md-editor-search-filters-summary', - standalone: true, - imports: [CommonModule, SearchFiltersSummaryItemComponent], + imports: [CommonModule, SearchFiltersSummaryItemComponent, TranslateModule], templateUrl: './search-filters-summary.component.html', styleUrls: ['./search-filters-summary.component.css'], + standalone: true, }) export class SearchFiltersSummaryComponent { @Input() searchFields: string[] = [] @@ -18,7 +19,10 @@ export class SearchFiltersSummaryComponent { map((filters) => this.hasNonEmptyValues(filters)) ) - constructor(private searchFacade: SearchFacade) {} + constructor( + private searchFacade: SearchFacade, + private searchService: SearchService + ) {} hasNonEmptyValues(filters: any): boolean { return Object.values(filters).some( @@ -28,4 +32,8 @@ export class SearchFiltersSummaryComponent { (typeof value === 'object' && Object.keys(value).length > 0)) ) } + + clearFilters() { + this.searchService.setFilters({}) + } } From 4a4a20a31c5143bc0cf9046e390ea5ed19091e60 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 14 Nov 2024 13:47:48 +0100 Subject: [PATCH 28/42] feat(search-filters-summary): adapt UI --- ...search-filters-summary-item.component.html | 38 +++++++++++++------ .../search-filters-summary.component.html | 17 ++++++--- .../inputs/src/lib/badge/badge.component.html | 2 +- tailwind.base.css | 3 +- translations/de.json | 4 +- translations/en.json | 4 +- translations/fr.json | 4 +- 7 files changed, 46 insertions(+), 26 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html index 9f7147d11e..28e8982719 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html @@ -1,22 +1,36 @@
-
- search.filters.summaryLabel.{{ fieldName }} +
+ search.filters.summaryLabel.{{ fieldName }} {{ fieldValue['start'] }} - >{{ fieldValue['end'] }}{{ fieldValue['start'] | date: 'dd.MM.yyyy' }} - + {{ fieldValue['end'] | date: 'dd.MM.yyyy' }}
- search.filters.summaryLabel.{{ fieldName }} - {{ fieldValue }} +
+ search.filters.summaryLabel.{{ fieldName }} + {{ fieldValue }} +
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html index 0ac1b069f4..fe4d727ff4 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html @@ -2,11 +2,16 @@ class="flex flex-row py-3 px-4 gap-4 overflow-hidden grow border-[1px] border-gray-200" *ngIf="searchFilterActive$ | async" > - -
diff --git a/libs/ui/inputs/src/lib/badge/badge.component.html b/libs/ui/inputs/src/lib/badge/badge.component.html index b2e838d459..1eb95970e5 100644 --- a/libs/ui/inputs/src/lib/badge/badge.component.html +++ b/libs/ui/inputs/src/lib/badge/badge.component.html @@ -18,7 +18,7 @@ --gn-ui-button-width: 1.4em; --gn-ui-button-height: 1.4em; --gn-ui-button-rounded: 1.4em; - --gn-ui-button-background: white; + --gn-ui-button-background: var(--gn-ui-badge-background-color, white); " > diff --git a/tailwind.base.css b/tailwind.base.css index ad0ec12ad8..7638150011 100644 --- a/tailwind.base.css +++ b/tailwind.base.css @@ -143,12 +143,13 @@ .gn-ui-badge { --rounded: var(--gn-ui-badge-rounded, 0.25em); --padding: var(--gn-ui-badge-padding, 0.375em 0.75em); + --font-weight: var(--gn-ui-badge-font-weight, 500); --text-size: var(--gn-ui-badge-text-size, 0.875em); --text-color: var(--gn-ui-badge-text-color, var(--color-gray-50)); --background-color: var(--gn-ui-badge-background-color, black); --opacity: var(--gn-ui-badge-opacity, 0.7); @apply opacity-[--opacity] p-[--padding] rounded-[--rounded] - font-medium text-[length:--text-size] text-[color:--text-color] bg-[color:--background-color] flex justify-center items-center content-center; + font-[--font-weight] text-[length:--text-size] text-[color:--text-color] bg-[color:--background-color] flex justify-center items-center content-center; } /* makes sure icons will not make the badges grow vertically; also make size proportional */ .gn-ui-badge ng-icon { diff --git a/translations/de.json b/translations/de.json index e1f39e6ada..dae4a058c8 100644 --- a/translations/de.json +++ b/translations/de.json @@ -527,8 +527,8 @@ "search.filters.representationType": "Repräsentationstyp", "search.filters.resourceType": "Ressourcentyp", "search.filters.standard": "Standard", - "search.filters.summaryLabel.changeDate": "", - "search.filters.summaryLabel.user": "", + "search.filters.summaryLabel.changeDate": "Geändert am: ", + "search.filters.summaryLabel.user": "Geändert von: ", "search.filters.title": "Ergebnisse filtern", "search.filters.topic": "Themen", "search.filters.useSpatialFilter": "Zuerst Datensätze im Interessenbereich anzeigen", diff --git a/translations/en.json b/translations/en.json index 857cd3a952..4b8fda7ba4 100644 --- a/translations/en.json +++ b/translations/en.json @@ -527,8 +527,8 @@ "search.filters.representationType": "Representation type", "search.filters.resourceType": "Resource type", "search.filters.standard": "Standard", - "search.filters.summaryLabel.changeDate": "", - "search.filters.summaryLabel.user": "", + "search.filters.summaryLabel.changeDate": "Modified on: ", + "search.filters.summaryLabel.user": "Modified by: ", "search.filters.title": "Filter your results", "search.filters.topic": "Topics", "search.filters.useSpatialFilter": "Show records in the area of interest first", diff --git a/translations/fr.json b/translations/fr.json index 76054eaf30..a0283ff2dc 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -527,8 +527,8 @@ "search.filters.representationType": "Type de représentation", "search.filters.resourceType": "Type de ressource", "search.filters.standard": "Standard", - "search.filters.summaryLabel.changeDate": "Modifiée le", - "search.filters.summaryLabel.user": "Modifiée par", + "search.filters.summaryLabel.changeDate": "Modifiée le : ", + "search.filters.summaryLabel.user": "Modifiée par : ", "search.filters.title": "Affiner votre recherche", "search.filters.topic": "Thèmes", "search.filters.useSpatialFilter": "Mettre en avant les résultats sur la zone d'intérêt", From 527c253e8bec46ee1ce746beb8f2001caa17c6c4 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 19 Nov 2024 11:16:13 +0100 Subject: [PATCH 29/42] refactor(search-filters-summary-item): implement a getReadableValues method to simplify template and align badges --- ...search-filters-summary-item.component.html | 52 +++++++----------- .../search-filters-summary-item.component.ts | 54 ++++++++----------- .../search-filters-summary.component.html | 2 +- 3 files changed, 42 insertions(+), 66 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html index 28e8982719..b8a763cc84 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html @@ -1,36 +1,20 @@ -
-
+ search.filters.summaryLabel.{{ fieldName }} + {{ fieldValue }} - search.filters.summaryLabel.{{ fieldName }} - {{ fieldValue['start'] | date: 'dd.MM.yyyy' }} - - {{ fieldValue['end'] | date: 'dd.MM.yyyy' }} -
- -
- search.filters.summaryLabel.{{ fieldName }} - {{ fieldValue }} -
-
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts index 221636c2c5..1f96740efe 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts @@ -1,26 +1,22 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' -import { CommonModule } from '@angular/common' +import { Component, Input } from '@angular/core' +import { CommonModule, DatePipe } from '@angular/common' import { FieldsService, - FieldType, FieldValue, - FieldValues, SearchFacade, SearchService, } from '@geonetwork-ui/feature/search' import { catchError, - filter, firstValueFrom, map, Observable, - startWith, + of, switchMap, - tap, } from 'rxjs' import { BadgeComponent } from '@geonetwork-ui/ui/inputs' import { TranslateModule } from '@ngx-translate/core' -import { DateRange } from '@geonetwork-ui/api/repository' +import { DateRange, isDateRange } from '@geonetwork-ui/api/repository' @Component({ selector: 'md-editor-search-filters-summary-item', @@ -28,44 +24,40 @@ import { DateRange } from '@geonetwork-ui/api/repository' imports: [CommonModule, TranslateModule, BadgeComponent], templateUrl: './search-filters-summary-item.component.html', styleUrls: ['./search-filters-summary-item.component.css'], + providers: [DatePipe], }) -export class SearchFiltersSummaryItemComponent implements OnInit { +export class SearchFiltersSummaryItemComponent { @Input() fieldName: string - fieldType: FieldType fieldValues$ = this.searchFacade.searchFilters$.pipe( switchMap((filters) => this.fieldsService.readFieldValuesFromFilters(filters) ), - tap((fieldValues) => console.log(fieldValues)), map((fieldValues) => - Array.isArray(fieldValues[this.fieldName]) //TODO: handle date ranges as arrays everywhere? - ? fieldValues[this.fieldName] - : [fieldValues[this.fieldName]] + Array.isArray(fieldValues[this.fieldName]) + ? (fieldValues[this.fieldName] as FieldValue[]) + : ([fieldValues[this.fieldName]] as FieldValue[]) ), - tap((fieldValues) => console.log(fieldValues)) - // startWith([]), - // catchError(() => of([])) - ) as Observable - - dateRange$ = this.fieldValues$.pipe( - filter(() => this.fieldType === 'dateRange'), - map((fieldValues) => fieldValues as DateRange[]) - ) - - values$ = this.fieldValues$.pipe( - filter(() => this.fieldType === 'values'), - map((fieldValues) => fieldValues as FieldValue[]) - ) + map((fieldValues) => this.getReadableValues(fieldValues) as FieldValue[]), + catchError(() => of([])) + ) as Observable constructor( private searchFacade: SearchFacade, private searchService: SearchService, - private fieldsService: FieldsService + private fieldsService: FieldsService, + private datePipe: DatePipe ) {} - ngOnInit() { - this.fieldType = this.fieldsService.getFieldType(this.fieldName) + getReadableValues(fieldValues: FieldValue[] | DateRange[]): FieldValue[] { + return fieldValues.map((value) => + isDateRange(value) + ? `${this.datePipe.transform( + value.start, + 'dd.MM.yyyy' + )} - ${this.datePipe.transform(value.end, 'dd.MM.yyyy')}` + : value + ) } async removeFilterValue(fieldValue: FieldValue | DateRange) { diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html index fe4d727ff4..5c9e9ef8fa 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html @@ -2,7 +2,7 @@ class="flex flex-row py-3 px-4 gap-4 overflow-hidden grow border-[1px] border-gray-200" *ngIf="searchFilterActive$ | async" > -
+
Date: Tue, 19 Nov 2024 14:19:16 +0100 Subject: [PATCH 30/42] refactor(search-filter-summary): move components for feature-search lib --- .../search-filters/search-filters.component.html | 4 ++-- .../search-filters/search-filters.component.ts | 6 ++++-- libs/feature/search/src/index.ts | 2 ++ .../search-filters-summary-item.component.css | 0 .../search-filters-summary-item.component.html | 0 .../search-filters-summary-item.component.spec.ts | 0 .../search-filters-summary-item.component.ts | 12 +++++------- .../search-filters-summary.component.css | 0 .../search-filters-summary.component.html | 4 ++-- .../search-filters-summary.component.spec.ts | 0 .../search-filters-summary.component.ts | 8 +++++--- 11 files changed, 20 insertions(+), 16 deletions(-) rename {apps/metadata-editor/src/app/dashboard => libs/feature/search/src/lib}/search-filters-summary-item/search-filters-summary-item.component.css (100%) rename {apps/metadata-editor/src/app/dashboard => libs/feature/search/src/lib}/search-filters-summary-item/search-filters-summary-item.component.html (100%) rename {apps/metadata-editor/src/app/dashboard => libs/feature/search/src/lib}/search-filters-summary-item/search-filters-summary-item.component.spec.ts (100%) rename {apps/metadata-editor/src/app/dashboard => libs/feature/search/src/lib}/search-filters-summary-item/search-filters-summary-item.component.ts (88%) rename {apps/metadata-editor/src/app/dashboard => libs/feature/search/src/lib}/search-filters-summary/search-filters-summary.component.css (100%) rename {apps/metadata-editor/src/app/dashboard => libs/feature/search/src/lib}/search-filters-summary/search-filters-summary.component.html (83%) rename {apps/metadata-editor/src/app/dashboard => libs/feature/search/src/lib}/search-filters-summary/search-filters-summary.component.spec.ts (100%) rename {apps/metadata-editor/src/app/dashboard => libs/feature/search/src/lib}/search-filters-summary/search-filters-summary.component.ts (79%) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html index 91cc57132e..3d4728505f 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html @@ -14,7 +14,7 @@ [style.--gn-ui-multiselect-counter-background-color]="'white'" >
- + >
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts index f38f562f22..e8221a130c 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts @@ -7,9 +7,11 @@ import { provideNgIconsConfig, } from '@ng-icons/core' import { iconoirFilterList } from '@ng-icons/iconoir' -import { FeatureSearchModule } from '@geonetwork-ui/feature/search' +import { + FeatureSearchModule, + SearchFiltersSummaryComponent, +} from '@geonetwork-ui/feature/search' import { MatIconModule } from '@angular/material/icon' -import { SearchFiltersSummaryComponent } from '../search-filters-summary/search-filters-summary.component' @Component({ selector: 'md-editor-search-filters', diff --git a/libs/feature/search/src/index.ts b/libs/feature/search/src/index.ts index f6103754e3..63ae62fd76 100644 --- a/libs/feature/search/src/index.ts +++ b/libs/feature/search/src/index.ts @@ -22,3 +22,5 @@ export * from './lib/results-layout/results-layout.component' export * from './lib/sort-by/sort-by.component' export * from './lib/state/container/search-state.container.directive' export * from './lib/results-table/results-table-container.component' +export * from './lib/search-filters-summary/search-filters-summary.component' +export * from './lib/search-filters-summary-item/search-filters-summary-item.component' diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.css b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.css similarity index 100% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.css rename to libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.css diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.html similarity index 100% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.html rename to libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.html diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.spec.ts b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.spec.ts similarity index 100% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.spec.ts rename to libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.spec.ts diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts similarity index 88% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts rename to libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts index 1f96740efe..3e409f558f 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary-item/search-filters-summary-item.component.ts +++ b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts @@ -1,11 +1,5 @@ import { Component, Input } from '@angular/core' import { CommonModule, DatePipe } from '@angular/common' -import { - FieldsService, - FieldValue, - SearchFacade, - SearchService, -} from '@geonetwork-ui/feature/search' import { catchError, firstValueFrom, @@ -17,9 +11,13 @@ import { import { BadgeComponent } from '@geonetwork-ui/ui/inputs' import { TranslateModule } from '@ngx-translate/core' import { DateRange, isDateRange } from '@geonetwork-ui/api/repository' +import { FieldValue } from '../utils/service/fields' +import { SearchFacade } from '../state/search.facade' +import { SearchService } from '../utils/service/search.service' +import { FieldsService } from '../utils/service/fields.service' @Component({ - selector: 'md-editor-search-filters-summary-item', + selector: 'gn-ui-search-filters-summary-item', standalone: true, imports: [CommonModule, TranslateModule, BadgeComponent], templateUrl: './search-filters-summary-item.component.html', diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.css similarity index 100% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.css rename to libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.css diff --git a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html similarity index 83% rename from apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html rename to libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html index 5c9e9ef8fa..289a9d9946 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters-summary/search-filters-summary.component.html +++ b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html @@ -3,10 +3,10 @@ *ngIf="searchFilterActive$ | async" >
- + >