diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index 13a9215b9..5b76e1b93 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -296,12 +296,26 @@ describe('dashboard (authenticated)', () => { }) }) describe('search filters', () => { - function checkFilterByChangeDate() { + function selectUser(index = 0, openDropdown = true) { + if (openDropdown) { + cy.get('md-editor-search-filters').find('gn-ui-button').first().click() + } + cy.get('.cdk-overlay-container') + .find('input[type="checkbox"]') + .eq(index) + .check() + } + function selectDateRange() { 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() + } + function closeDropDown() { + cy.get('body').click(0, 0) + } + function checkFilterByChangeDate() { cy.get('gn-ui-interactive-table') .find('[data-cy="table-row"]') .should('have.length', '1') @@ -322,12 +336,8 @@ describe('dashboard (authenticated)', () => { .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') - .find('input[type="checkbox"]') - .eq(1) - .check() + it('should filter the record list by user (Barbara Roberts)', () => { + selectUser(1) cy.get('gn-ui-interactive-table') .find('[data-cy="table-row"]') .should('have.length', '5') @@ -339,18 +349,19 @@ describe('dashboard (authenticated)', () => { }) it('should filter the record list by last update (changeDate)', () => { cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + selectDateRange() 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') + .find('ng-icon') + .should('have.attr', 'ng-reflect-name', 'matExpandMore') 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') + .find('ng-icon') + .should('have.attr', 'ng-reflect-name', 'matExpandLess') }) }) describe('myRecords search filters', () => { @@ -364,9 +375,145 @@ describe('dashboard (authenticated)', () => { }) it('should filter the record list by last update (changeDate)', () => { cy.get('md-editor-search-filters').find('gn-ui-button').first().click() + selectDateRange() checkFilterByChangeDate() }) }) + describe('allRecord search filters summary', () => { + beforeEach(() => { + cy.visit('/catalog/search') + }) + it('should not display anything without selected filters', () => { + cy.get('gn-ui-search-filters-summary-item').should('not.exist') + }) + describe('selecting users', () => { + beforeEach(() => { + selectUser(1) + }) + it('should display a label for badges of selected users', () => { + cy.get('gn-ui-search-filters-summary') + .find('[data-cy="filterSummaryLabel"]') + .invoke('text') + .should('eq', 'Modified by: ') + }) + it('should display the badge for a selected user', () => { + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 1) + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .invoke('text') + .should('eq', 'Barbara Roberts') + }) + it('should display a second badge for a second selected user', () => { + selectUser(0, false) + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 2) + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .eq(1) + .invoke('text') + .should('eq', 'admin admin') + }) + it('should remove one of two badges when a badge cross is clicked', () => { + selectUser(0, false) + closeDropDown() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .eq(0) + .find('ng-icon') + .click() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 1) + }) + }) + describe('selecting date range', () => { + beforeEach(() => { + cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + selectDateRange() + }) + it('should display a label for the date range', () => { + cy.get('gn-ui-search-filters-summary') + .find('[data-cy="filterSummaryLabel"]') + .invoke('text') + .should('eq', 'Modified on: ') + }) + it('should display the badge for the selected date range', () => { + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .invoke('text') + .should('eq', '01.08.2024 - 30.08.2024') + }) + it('should remove the badge when the badge cross is clicked', () => { + closeDropDown() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .find('ng-icon') + .click() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('not.exist') + }) + }) + describe('selecting multiple filters (users and date range)', () => { + beforeEach(() => { + selectUser(0) + closeDropDown() + cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + selectDateRange() + }) + it('should display both badges', () => { + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 2) + }) + it('should clear all filters when the clear button is clicked', () => { + cy.get('gn-ui-search-filters-summary').find('button').last().click() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 0) + cy.get('gn-ui-search-filters-summary-item').should('not.exist') + }) + }) + }) + describe('myRecords search filters summary', () => { + beforeEach(() => { + cy.visit('/my-space/my-records') + }) + it('should not display anything without selected filters', () => { + cy.get('gn-ui-search-filters-summary-item').should('not.exist') + }) + describe('selecting date range', () => { + beforeEach(() => { + cy.get('md-editor-search-filters').find('gn-ui-button').eq(0).click() + selectDateRange() + }) + it('should display a label for the date range', () => { + cy.get('gn-ui-search-filters-summary') + .find('[data-cy="filterSummaryLabel"]') + .invoke('text') + .should('eq', 'Modified on: ') + }) + it('should display the badge for the selected date range', () => { + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .invoke('text') + .should('eq', '01.08.2024 - 30.08.2024') + }) + it('should remove the badge when the badge cross is clicked', () => { + closeDropDown() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .find('ng-icon') + .click() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('not.exist') + }) + }) + }) }) describe('Account settings access', () => { it('should navigate to the account settings page', () => { 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 75710a0f5..3d4728505 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.spec.ts b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.spec.ts index 487bdb861..2b2d9a2fb 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.spec.ts @@ -2,13 +2,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { SearchFiltersComponent } from './search-filters.component' import { MockBuilder } from 'ng-mocks' import { TranslateModule } from '@ngx-translate/core' +import { By } from '@angular/platform-browser' +import { SearchFiltersSummaryComponent } from '@geonetwork-ui/feature/search' describe('SearchFiltersComponent', () => { let component: SearchFiltersComponent let fixture: ComponentFixture beforeEach(() => { - return MockBuilder(SearchFiltersComponent) + return MockBuilder(SearchFiltersComponent).mock( + SearchFiltersSummaryComponent + ) }) beforeEach(async () => { @@ -40,5 +44,15 @@ describe('SearchFiltersComponent', () => { fixture.detectChanges() expect(component.searchConfig).toEqual([]) }) + + it('should pass searchFields to SearchFiltersSummaryComponent', () => { + const searchFields = ['user', 'publisherOrg', 'format', 'isSpatial'] + component.searchFields = searchFields + fixture.detectChanges() + const summaryComponent = fixture.debugElement.query( + By.directive(SearchFiltersSummaryComponent) + ).componentInstance + expect(summaryComponent.searchFields).toEqual(searchFields) + }) }) }) 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 68cc32e55..3b64b8a64 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 @@ -1,13 +1,16 @@ import { Component, Input, OnInit } from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' -import { FeatureSearchModule } from '@geonetwork-ui/feature/search' import { NgIconComponent, provideIcons, provideNgIconsConfig, } from '@ng-icons/core' import { iconoirFilterList } from '@ng-icons/iconoir' +import { + FeatureSearchModule, + SearchFiltersSummaryComponent, +} from '@geonetwork-ui/feature/search' @Component({ selector: 'md-editor-search-filters', @@ -17,6 +20,7 @@ import { iconoirFilterList } from '@ng-icons/iconoir' TranslateModule, FeatureSearchModule, NgIconComponent, + SearchFiltersSummaryComponent, ], providers: [ provideIcons({ 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 2068fd36f..de10855ff 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 @@ -13,7 +13,7 @@ import { RecordsListComponent } from '../records-list.component' import { FeatureSearchModule, FieldsService, - ResultsTableContainerComponent, + FILTER_SUMMARY_IGNORE_LIST, SearchFacade, } from '@geonetwork-ui/feature/search' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' @@ -38,6 +38,8 @@ import { iconoirPagePlus, } from '@ng-icons/iconoir' +const FILTER_OWNER = 'owner' + @Component({ selector: 'md-editor-my-records', templateUrl: './my-records.component.html', @@ -47,7 +49,6 @@ import { CommonModule, TranslateModule, RecordsListComponent, - ResultsTableContainerComponent, UiElementsModule, RecordsCountComponent, ButtonComponent, @@ -66,6 +67,7 @@ import { provideNgIconsConfig({ size: '1.5rem', }), + { provide: FILTER_SUMMARY_IGNORE_LIST, useValue: [FILTER_OWNER] }, ], }) export class MyRecordsComponent implements OnInit { @@ -93,7 +95,7 @@ export class MyRecordsComponent implements OnInit { this.platformService.getMe().subscribe((user) => { this.fieldsService - .buildFiltersFromFieldValues({ owner: user.id }) + .buildFiltersFromFieldValues({ [FILTER_OWNER]: user.id }) .subscribe((filters) => { this.searchFacade.updateFilters(filters) }) 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 08393b9ec..ec3c3ab1b 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 @@ -37,11 +37,11 @@ describe('query params utilities', () => { [ROUTE_PARAMS.SORT]: 'createDate', publisher: ['john', 'barbie'], updateDate: { - start: new Date('2010-03-10'), - end: new Date('2014-01-01'), + start: new Date('2010-03-10T00:00:00'), + end: new Date('2014-01-01T00:00:00'), }, changeDate: { - end: new Date('2008-08-14'), + end: new Date('2008-08-14T00:00:00'), }, }) }) @@ -51,7 +51,7 @@ describe('query params utilities', () => { }) expect(params).toEqual({ changeDate: { - end: new Date('2008-08-14'), + end: new Date('2008-08-14T00:00:00'), }, }) }) 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 73ea22fa8..133d5dbc7 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,4 +1,8 @@ -import { DateRange, isDateRange } from '@geonetwork-ui/api/repository' +import { + DateRange, + formatDate, + isDateRange, +} from '@geonetwork-ui/api/repository' import { ROUTE_PARAMS, SearchRouteParams } from '../constants' export function flattenQueryParams( @@ -12,12 +16,10 @@ export function flattenQueryParams( ) { flattened[key] = [(flattened[key] as string[]).join(',')] } else if (isDateRange(flattened[key] as DateRange)) { + const start = (flattened[key] as DateRange).start + const end = (flattened[key] as DateRange).end flattened[key] = [ - `${ - (flattened[key] as DateRange).start?.toISOString().split('T')[0] || '' - }..${ - (flattened[key] as DateRange).end?.toISOString().split('T')[0] || '' - }`, + `${start ? formatDate(start) : ''}..${formatDate(end) || ''}`, ] } } @@ -42,8 +44,8 @@ export function expandQueryParams( } else if (isDateUrl(value)) { const [start, end] = value.split('..') expanded[key] = { - ...(start && { start: new Date(start) }), - ...(end && { end: new Date(end) }), + ...(start && { start: new Date(`${start}T00:00:00`) }), + ...(end && { end: new Date(`${end}T00:00:00`) }), } } else { expanded[key] = value.split(',') diff --git a/libs/feature/search/src/index.ts b/libs/feature/search/src/index.ts index f6103754e..63ae62fd7 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/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts index d5bb87d07..33f7d30ad 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 @@ -72,13 +72,11 @@ export class FilterDropdownComponent implements OnInit { onStartDateChange(start: Date) { if (!start) return - start.setUTCHours(24, 0, 0, 0) this.dateRange = { ...this.dateRange, start } } onEndDateChange(end: Date) { if (!end) return - end.setUTCHours(24, 0, 0, 0) this.dateRange = { ...this.dateRange, end } if (this.dateRange.start && this.dateRange.end) { this.fieldsService diff --git a/libs/feature/search/src/lib/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 new file mode 100644 index 000000000..e69de29bb diff --git a/libs/feature/search/src/lib/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 new file mode 100644 index 000000000..558703608 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.html @@ -0,0 +1,20 @@ +
+ {{ + translatedLabel + }} + {{ fieldValue.label }} +
diff --git a/libs/feature/search/src/lib/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 new file mode 100644 index 000000000..45bf0fdb3 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.spec.ts @@ -0,0 +1,155 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { SearchFiltersSummaryItemComponent } from './search-filters-summary-item.component' +import { BehaviorSubject, firstValueFrom, of } from 'rxjs' +import { MockBuilder, MockComponent, MockProvider } from 'ng-mocks' +import { SearchFacade } from '../state/search.facade' +import { SearchService } from '../utils/service/search.service' +import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' +import { BadgeComponent } from '@geonetwork-ui/ui/inputs' +import { CommonModule, DatePipe } from '@angular/common' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { FieldsService } from '../utils/service/fields.service' +import { FieldType } from '../utils/service/fields' + +const FIELD_VALUES_FROM_FILTERS_MOCK = { + organization: [], + format: [], + resourceType: [], + representationType: [], + publicationYear: [], + topic: [], + inspireKeyword: [], + keyword: [], + documentStandard: [], + isSpatial: [], + q: [], + license: [], + owner: [], + producerOrg: [], + publisherOrg: [], + user: ['admin|admin|admin|Administrator', 'barbie|Roberts|Barbara|UserAdmin'], + changeDate: { + start: new Date('2024-11-01T00:00:00.000Z'), + end: new Date('2024-11-29T00:00:00.000Z'), + }, +} +/* searchFilters$ is only used to trigger change detection. + ** its value is replaced by FIELD_VALUES_FROM_FILTERS_MOCK in stream + */ +class SearchFacadeMock { + searchFilters$ = new BehaviorSubject({}) +} +class SearchServiceMock { + setFilters = jest.fn() +} + +class TranslateServiceMock { + get = jest.fn(() => of('')) +} + +describe('SearchFiltersSummaryItemComponent', () => { + let component: SearchFiltersSummaryItemComponent + let fixture: ComponentFixture + let searchFacade: SearchFacade + let translateService: TranslateService + let fieldsService: FieldsService + + beforeEach(() => { + return MockBuilder(SearchFiltersSummaryItemComponent) + }) + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CommonModule, TranslateModule.forRoot()], + declarations: [ + SearchFiltersSummaryItemComponent, + MockComponent(BadgeComponent), + ], + providers: [ + MockProvider(SearchFacade, SearchFacadeMock, 'useClass'), + MockProvider(SearchService, SearchServiceMock, 'useClass'), + MockProvider(FieldsService), + MockProvider(DatePipe), + MockProvider(TranslateService, TranslateServiceMock, 'useClass'), + ], + }).compileComponents() + fixture = TestBed.createComponent(SearchFiltersSummaryItemComponent) + component = fixture.componentInstance + searchFacade = TestBed.inject(SearchFacade) + fieldsService = TestBed.inject(FieldsService) + translateService = TestBed.inject(TranslateService) + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should set fieldValues$ observable for empty filters', async () => { + const fieldValues = await firstValueFrom(component.fieldValues$) + expect(fieldValues).toEqual([]) + }) + + describe('fieldValues$', () => { + beforeEach(() => { + fieldsService.getFieldType = jest.fn( + (field: 'changeDate' | 'user') => + (field === 'changeDate' ? 'dateRange' : 'values') as FieldType + ) + fieldsService.readFieldValuesFromFilters = jest.fn(() => + of(FIELD_VALUES_FROM_FILTERS_MOCK) + ) + ;(searchFacade.searchFilters$ as BehaviorSubject).next({}) + }) + it('should set fieldValues$ observable for user values filters', async () => { + component.fieldName = 'user' + const fieldValues = await firstValueFrom(component.fieldValues$) + expect(fieldValues).toEqual([ + { + value: 'admin|admin|admin|Administrator', + label: 'admin admin', + }, + { + value: 'barbie|Roberts|Barbara|UserAdmin', + label: 'Barbara Roberts', + }, + ]) + }) + it('should set fieldValues$ observable for changeDate dateRange filters', async () => { + component.fieldName = 'changeDate' + fixture.detectChanges() + const fieldValues = await firstValueFrom(component.fieldValues$) + expect(fieldValues).toEqual([ + { + value: { + start: new Date('2024-11-01T00:00:00.000Z'), + end: new Date('2024-11-29T00:00:00.000Z'), + }, + label: '01.11.2024 - 29.11.2024', + }, + ]) + }) + }) + + describe('translateLabel', () => { + const fieldName = 'user' + const labelKey = `search.filters.summaryLabel.${fieldName}` + const fallbackKey = `search.filters.${fieldName}` + beforeEach(() => { + component.fieldName = fieldName + fixture.detectChanges() + translateService.get = jest.fn((key) => { + if (key === labelKey) { + return of(labelKey) // Simulate missing translation + } else if (key === fallbackKey) { + return of('Fallback Label') + } + return of('') + }) + }) + it('should translate label with fallback if necessary', () => { + component.translateLabel() + expect(component.translatedLabel).toBe('Fallback Label') + }) + }) +}) diff --git a/libs/feature/search/src/lib/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 new file mode 100644 index 000000000..024a0217c --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts @@ -0,0 +1,118 @@ +import { Component, Input, OnInit } from '@angular/core' +import { CommonModule, DatePipe } from '@angular/common' +import { + catchError, + firstValueFrom, + map, + Observable, + of, + switchMap, +} from 'rxjs' +import { BadgeComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { DateRange } from '@geonetwork-ui/api/repository' +import { FieldType, 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' +import { formatUserInfo } from '@geonetwork-ui/util/shared' +import { marker } from '@biesbjerg/ngx-translate-extract-marker' + +marker('search.filters.summaryLabel.user') +marker('search.filters.summaryLabel.changeDate') + +interface DisplayedValue { + label: string + value: FieldValue | DateRange +} + +@Component({ + selector: 'gn-ui-search-filters-summary-item', + standalone: true, + 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 { + @Input() fieldName: string + fieldType: FieldType + translatedLabel: string + + fieldValues$ = this.searchFacade.searchFilters$.pipe( + switchMap((filters) => + this.fieldsService.readFieldValuesFromFilters(filters) + ), + map((fieldValues) => + Array.isArray(fieldValues[this.fieldName]) + ? (fieldValues[this.fieldName] as FieldValue[]) + : ([fieldValues[this.fieldName]] as FieldValue[]) + ), + map( + (fieldValues) => this.getReadableValues(fieldValues) as DisplayedValue[] + ), + catchError(() => of([])) + ) as Observable + + constructor( + private searchFacade: SearchFacade, + private searchService: SearchService, + private fieldsService: FieldsService, + private datePipe: DatePipe, + private translate: TranslateService + ) {} + + ngOnInit() { + this.fieldType = this.fieldsService.getFieldType(this.fieldName) + this.translateLabel() + } + + translateLabel() { + const labelKey = `search.filters.summaryLabel.${this.fieldName}` + const fallbackKey = `search.filters.${this.fieldName}` + this.translate.get(labelKey).subscribe((value: string) => { + if (value === labelKey) { + this.translate.get(fallbackKey).subscribe((fallbackValue: string) => { + this.translatedLabel = fallbackValue + }) + } else { + this.translatedLabel = value + } + }) + } + + getReadableValues(fieldValues: FieldValue[] | DateRange[]): DisplayedValue[] { + return fieldValues.map((value) => { + if (this.fieldType === 'dateRange') { + return { + value, + label: `${this.datePipe.transform( + value.start, + 'dd.MM.yyyy' + )} - ${this.datePipe.transform(value.end, 'dd.MM.yyyy')}`, + } + } else if (this.fieldName === 'user') { + return { value, label: formatUserInfo(value) } + } else { + return { value, label: value } + } + }) + } + + async removeFilterValue(fieldValue: FieldValue | DateRange) { + const currentFieldValues: DisplayedValue[] = await firstValueFrom( + this.fieldValues$ + ) + const updatedFieldValues = currentFieldValues + .filter( + (displayedValue: DisplayedValue) => displayedValue.value !== fieldValue + ) + .map((displayedValue: DisplayedValue) => displayedValue.value) + + this.fieldsService + .buildFiltersFromFieldValues({ + [this.fieldName]: updatedFieldValues as FieldValue[], + }) + .subscribe((filters) => this.searchService.updateFilters(filters)) + } +} diff --git a/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.css b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html new file mode 100644 index 000000000..289a9d994 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html @@ -0,0 +1,17 @@ +
+
+ +
+ +
diff --git a/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.spec.ts b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.spec.ts new file mode 100644 index 000000000..8056ac459 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.spec.ts @@ -0,0 +1,156 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { + FILTER_SUMMARY_IGNORE_LIST, + SearchFiltersSummaryComponent, +} from './search-filters-summary.component' +import { MockComponent, MockProvider } from 'ng-mocks' +import { SearchService } from '../utils/service/search.service' +import { SearchFacade } from '../state/search.facade' +import { BehaviorSubject, firstValueFrom } from 'rxjs' +import { TranslateModule } from '@ngx-translate/core' +import { SearchFiltersSummaryItemComponent } from '../search-filters-summary-item/search-filters-summary-item.component' +import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' + +class SearchFacadeMock { + searchFilters$ = new BehaviorSubject({ + format: {}, + isSpatial: {}, + license: {}, + 'userInfo.keyword': {}, + }) +} +class SearchServiceMock { + setFilters = jest.fn() +} + +describe('SearchFiltersSummaryComponent', () => { + let component: SearchFiltersSummaryComponent + let fixture: ComponentFixture + let searchFacade: SearchFacade + let searchService: SearchService + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ + SearchFiltersSummaryComponent, + MockComponent(SearchFiltersSummaryItemComponent), + ], + providers: [ + MockProvider(SearchFacade, SearchFacadeMock, 'useClass'), + MockProvider(SearchService, SearchServiceMock, 'useClass'), + ], + }) + }) + + describe('no injection token provided', () => { + beforeEach(async () => { + await TestBed.compileComponents() + fixture = TestBed.createComponent(SearchFiltersSummaryComponent) + searchFacade = TestBed.inject(SearchFacade) + searchService = TestBed.inject(SearchService) + component = fixture.componentInstance + fixture.detectChanges() + }) + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should set searchFilterActive$ observable to false for empty filters', async () => { + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeFalsy() + }) + + it('should set searchFilterActive$ observable to true for NON empty value filters', async () => { + const filters = { + format: {}, + isSpatial: {}, + license: {}, + 'userInfo.keyword': { 'admin|admin|admin|Administrator': true }, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeTruthy() + }) + + it('should set searchFilterActive$ observable to true for NON empty dateRange filters', async () => { + const filters = { + format: {}, + isSpatial: {}, + license: {}, + changeDate: { + start: new Date('2021-01-01'), + end: new Date('2021-01-02'), + }, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeTruthy() + }) + + it('should clear filters', () => { + component.clearFilters() + expect(searchService.setFilters).toHaveBeenCalledWith({}) + }) + }) + + describe('FILTER_SUMMARY_IGNORE_LIST injection token provided', () => { + beforeEach(async () => { + TestBed.overrideProvider(FILTER_SUMMARY_IGNORE_LIST, { + useValue: ['owner'], + }) + await TestBed.compileComponents() + fixture = TestBed.createComponent(SearchFiltersSummaryComponent) + searchFacade = TestBed.inject(SearchFacade) + searchService = TestBed.inject(SearchService) + component = fixture.componentInstance + fixture.detectChanges() + }) + it('should ignore filters from FILTER_SUMMARY_IGNORE_LIST', async () => { + const filters = { + owner: { 1: true }, + isSpatial: {}, + license: {}, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeFalsy() + }) + it('should set searchFilterActive$ observable to true for NON empty value filters', async () => { + const filters = { + owner: { 1: true }, + format: {}, + isSpatial: {}, + license: {}, + 'userInfo.keyword': { 'admin|admin|admin|Administrator': true }, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeTruthy() + }) + it('should clear filters except with keys from FILTER_SUMMARY_IGNORE_LIST', () => { + const filters = { + owner: { 1: true }, + format: {}, + isSpatial: {}, + license: {}, + 'userInfo.keyword': { 'admin|admin|admin|Administrator': true }, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + component.clearFilters() + expect(searchService.setFilters).toHaveBeenCalledWith({ + owner: { 1: true }, + }) + }) + }) +}) diff --git a/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.ts b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.ts new file mode 100644 index 000000000..c4318f8f7 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.ts @@ -0,0 +1,74 @@ +import { Component, Inject, Input, OnInit, Optional } from '@angular/core' +import { CommonModule } from '@angular/common' +import { first, map, Observable } from 'rxjs' +import { SearchFiltersSummaryItemComponent } from '../search-filters-summary-item/search-filters-summary-item.component' +import { TranslateModule } from '@ngx-translate/core' +import { SearchFacade } from '../state/search.facade' +import { SearchService } from '../utils/service/search.service' +import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' +import { InjectionToken } from '@angular/core' + +export const FILTER_SUMMARY_IGNORE_LIST = new InjectionToken( + 'FILTER_SUMMARY_IGNORE_LIST' +) +@Component({ + selector: 'gn-ui-search-filters-summary', + imports: [CommonModule, SearchFiltersSummaryItemComponent, TranslateModule], + templateUrl: './search-filters-summary.component.html', + styleUrls: ['./search-filters-summary.component.css'], + standalone: true, +}) +export class SearchFiltersSummaryComponent implements OnInit { + @Input() searchFields: string[] = [] + filterSummaryIgnoreList: string[] + + searchFilterActive$: Observable + + constructor( + private searchFacade: SearchFacade, + private searchService: SearchService, + @Optional() + @Inject(FILTER_SUMMARY_IGNORE_LIST) + filterSummaryIgnoreList: string[] + ) { + this.filterSummaryIgnoreList = filterSummaryIgnoreList || [] + } + + ngOnInit(): void { + this.searchFilterActive$ = this.searchFacade.searchFilters$.pipe( + map((filters) => this.hasNonEmptyValues(filters)) + ) + } + + hasNonEmptyValues(filters: FieldFilters): boolean { + const filteredFilters = {} + for (const [key, value] of Object.entries(filters)) { + if (!this.filterSummaryIgnoreList.includes(key)) { + filteredFilters[key] = value + } + } + return Object.values(filteredFilters).some( + (value) => + value !== undefined && + (typeof value !== 'object' || + (typeof value === 'object' && Object.keys(value).length > 0)) + ) + } + + clearFilters() { + this.searchFacade.searchFilters$ + .pipe( + first(), + map((filters) => { + const newFilters = { ...filters } + Object.keys(newFilters).forEach((key) => { + if (!this.filterSummaryIgnoreList.includes(key)) { + delete newFilters[key] + } + }) + return newFilters + }) + ) + .subscribe((filters) => this.searchService.setFilters(filters)) + } +} 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 95b2fce3e..4d8ba4138 100644 --- a/libs/feature/search/src/lib/utils/service/fields.spec.ts +++ b/libs/feature/search/src/lib/utils/service/fields.spec.ts @@ -275,10 +275,10 @@ describe('search fields implementations', () => { let filter beforeEach(async () => { filter = await lastValueFrom( - searchField.getFiltersForValues(['First value', 'Second value']) + searchField.getFiltersForValues(['First value', 'Second value', '']) ) }) - it('returns appropriate filters', () => { + it('returns appropriate filters (ignoring empty strings)', () => { expect(filter).toEqual({ myField: { 'First value': true, @@ -287,6 +287,17 @@ describe('search fields implementations', () => { }) }) }) + describe('#getFiltersForValues with empty value', () => { + let filter + beforeEach(async () => { + filter = await lastValueFrom(searchField.getFiltersForValues([''])) + }) + it('returns empty filter', () => { + expect(filter).toEqual({ + myField: {}, + }) + }) + }) describe('#getValuesForFilters', () => { let values describe('with several values', () => { @@ -362,6 +373,17 @@ describe('search fields implementations', () => { }) }) }) + describe('#getFiltersForValues with empty value', () => { + let filter + beforeEach(async () => { + filter = await lastValueFrom(searchField.getFiltersForValues([''])) + }) + it('returns empty filter', () => { + expect(filter).toEqual({ + changeDate: {}, + }) + }) + }) describe('#getValuesForFilters', () => { let values describe('with several values', () => { diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 3e5620043..1d8422ba0 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -20,6 +20,7 @@ import { METADATA_LANGUAGE, } from '@geonetwork-ui/api/repository' import { LangService } from '@geonetwork-ui/util/i18n' +import { formatUserInfo } from '@geonetwork-ui/util/shared' export type FieldType = 'values' | 'dateRange' @@ -83,18 +84,24 @@ export class SimpleSearchField implements AbstractSearchField { }) ) } - getFiltersForValues(values: FieldValue[] | DateRange[]): Observable { + getFiltersForValues( + values: FieldValue[] | DateRange[] + ): Observable { // FieldValue[] if (this.getType() === 'values') { return of({ [this.esFieldName]: (values as FieldValue[]).reduce((acc, val) => { - return { ...acc, [val.toString()]: true } + const value = val.toString() + if (value !== '') { + return { ...acc, [value]: true } + } + return acc }, {}), }) } // DateRange return of({ - [this.esFieldName]: values[0], + [this.esFieldName]: values[0] !== '' ? values[0] : {}, }) } getValuesForFilter( @@ -392,20 +399,11 @@ export class UserSearchField extends SimpleSearchField { map((values) => values.map((v) => ({ ...v, - label: this.formatUserInfo(v.label), + label: formatUserInfo(v.label, true), })) ) ) } - - private formatUserInfo(userInfo: string | unknown): string { - const infos = (typeof userInfo === 'string' ? userInfo : '').split('|') - const count = infos[3].split(' ')[1] - if (infos && infos.length === 4) { - return `${infos[2]} ${infos[1]} ${count}` - } - return undefined - } } export class DateRangeSearchField extends SimpleSearchField { diff --git a/libs/ui/inputs/src/lib/badge/badge.component.html b/libs/ui/inputs/src/lib/badge/badge.component.html index b2e838d45..1eb95970e 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/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 1ae135436..e8d138ea6 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 @@ -8,10 +8,11 @@ {{ title }} - - expand_less - expand_more - + + 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 14f6c88ae..82447a1ba 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 @@ -9,24 +9,29 @@ import { 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' +import { NgIconComponent, provideIcons } from '@ng-icons/core' +import { matExpandLess, matExpandMore } from '@ng-icons/material-icons/baseline' @Component({ selector: 'gn-ui-date-range-dropdown', standalone: true, imports: [ CommonModule, - DateRangePickerComponent, - MatIconModule, + NgIconComponent, MatNativeDateModule, MatDatepickerModule, ButtonComponent, ], + providers: [ + provideIcons({ + matExpandMore, + matExpandLess, + }), + ], templateUrl: './date-range-dropdown.component.html', styleUrls: ['./date-range-dropdown.component.css'], }) diff --git a/libs/ui/search/src/lib/results-table/results-table.component.ts b/libs/ui/search/src/lib/results-table/results-table.component.ts index 78a5aa689..fc2ed7755 100644 --- a/libs/ui/search/src/lib/results-table/results-table.component.ts +++ b/libs/ui/search/src/lib/results-table/results-table.component.ts @@ -22,6 +22,7 @@ import { } from '@geonetwork-ui/ui/layout' import { FileFormat, + formatUserInfo, getBadgeColor, getFileFormat, getFormatPriority, @@ -162,11 +163,7 @@ export class ResultsTableComponent { } formatUserInfo(userInfo: string | unknown): string { - const infos = (typeof userInfo === 'string' ? userInfo : '').split('|') - if (infos && infos.length === 4) { - return `${infos[2]} ${infos[1]}` - } - return undefined + return formatUserInfo(userInfo) } getBadgeColor(format: FileFormat): string { diff --git a/libs/util/shared/src/lib/utils/format-fields.spec.ts b/libs/util/shared/src/lib/utils/format-fields.spec.ts new file mode 100644 index 000000000..6420ccd4a --- /dev/null +++ b/libs/util/shared/src/lib/utils/format-fields.spec.ts @@ -0,0 +1,23 @@ +import { formatUserInfo } from './format-fields' + +describe('formatUserInfo', () => { + it('should format user info correctly', () => { + expect(formatUserInfo('barbie|Roberts|Barbara|UserAdmin (5)')).toEqual( + 'Barbara Roberts' + ) + }) + + it('should format user info correctly with count', () => { + expect( + formatUserInfo('barbie|Roberts|Barbara|UserAdmin (5)', true) + ).toEqual('Barbara Roberts (5)') + }) + + it('should return undefined if user info is empty', () => { + expect(formatUserInfo('')).toBeUndefined() + }) + + it('should return undefined if user info is not a string', () => { + expect(formatUserInfo(undefined)).toBeUndefined() + }) +}) diff --git a/libs/util/shared/src/lib/utils/format-fields.ts b/libs/util/shared/src/lib/utils/format-fields.ts new file mode 100644 index 000000000..f3604251d --- /dev/null +++ b/libs/util/shared/src/lib/utils/format-fields.ts @@ -0,0 +1,11 @@ +export function formatUserInfo( + userInfo: string | unknown, + displayCount = false +): string { + const infos = (typeof userInfo === 'string' ? userInfo : '').split('|') + const count = displayCount ? ` ${infos[3].split(' ')[1]}` : '' + if (infos && infos.length === 4) { + return `${infos[2]} ${infos[1]}${count}` + } + return undefined +} diff --git a/libs/util/shared/src/lib/utils/index.ts b/libs/util/shared/src/lib/utils/index.ts index 301dce35e..6ca3d5c95 100644 --- a/libs/util/shared/src/lib/utils/index.ts +++ b/libs/util/shared/src/lib/utils/index.ts @@ -1,5 +1,6 @@ export * from './bytes-convert' export * from './event' +export * from './format-fields' export * from './fuzzy-filter' export * from './geojson' export * from './image-resize' diff --git a/tailwind.base.css b/tailwind.base.css index ad0ec12ad..763815001 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 d1d2bc7b0..dae4a058c 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": "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 a29733661..4b8fda7ba 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": "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/es.json b/translations/es.json index 185b8bf02..2d179493d 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 8a6b021ad..a0283ff2d 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 445ca702d..9d63219f3 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 b620c75c8..e51a598a9 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 a532a8a84..cfe9539f1 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 088c2d708..b95f54ffd 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",