diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index 053bf0a7f6..24359c624b 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -313,6 +313,38 @@ describe('dashboard (authenticated)', () => { }) }) }) + describe('search filters', () => { + describe('allRecords search filter', () => { + beforeEach(() => { + cy.visit('/catalog/search') + }) + 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() + cy.get('gn-ui-interactive-table') + .find('[data-cy="table-row"]') + .should('have.length', '5') + cy.get('gn-ui-results-table') + .find('[data-cy="ownerInfo"]') + .each(($ownerInfo) => { + cy.wrap($ownerInfo).invoke('text').should('eq', 'Barbara Roberts') + }) + }) + }) + describe('myRecords search filters', () => { + beforeEach(() => { + cy.visit('/my-space/my-records') + }) + it('should contain filter component with no search filter for now', () => { + cy.get('md-editor-search-filters') + .find('gn-ui-button') + .should('not.exist') + }) + }) + }) }) describe('when the user is not logged in', () => { diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.css b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.css new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..f95bc2d95b --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html @@ -0,0 +1,13 @@ +
+ filter_list + +
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 new file mode 100644 index 0000000000..487bdb8615 --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { SearchFiltersComponent } from './search-filters.component' +import { MockBuilder } from 'ng-mocks' +import { TranslateModule } from '@ngx-translate/core' + +describe('SearchFiltersComponent', () => { + let component: SearchFiltersComponent + let fixture: ComponentFixture + + beforeEach(() => { + return MockBuilder(SearchFiltersComponent) + }) + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SearchFiltersComponent, TranslateModule.forRoot()], + }).compileComponents() + fixture = TestBed.createComponent(SearchFiltersComponent) + component = fixture.componentInstance + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('searchFields', () => { + it('should correctly read searchFields and create searchConfig', () => { + const searchFields = ['user', 'publisherOrg', 'format', 'isSpatial'] + component.searchFields = searchFields + fixture.detectChanges() + expect(component.searchConfig).toEqual([ + { fieldName: 'user', title: 'search.filters.user' }, + { fieldName: 'publisherOrg', title: 'search.filters.publisherOrg' }, + { fieldName: 'format', title: 'search.filters.format' }, + { fieldName: 'isSpatial', title: 'search.filters.isSpatial' }, + ]) + }) + it('should read empty searchFields and create empty searchConfig', () => { + component.searchFields = [] + fixture.detectChanges() + expect(component.searchConfig).toEqual([]) + }) + }) +}) 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 new file mode 100644 index 0000000000..23e05c159e --- /dev/null +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts @@ -0,0 +1,24 @@ +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 { MatIconModule } from '@angular/material/icon' + +@Component({ + selector: 'md-editor-search-filters', + standalone: true, + imports: [CommonModule, TranslateModule, FeatureSearchModule, MatIconModule], + templateUrl: './search-filters.component.html', + styleUrls: ['./search-filters.component.css'], +}) +export class SearchFiltersComponent implements OnInit { + @Input() searchFields: string[] = [] + searchConfig: { fieldName: string; title: string }[] + + ngOnInit(): void { + this.searchConfig = this.searchFields.map((filter) => ({ + fieldName: filter, + title: `search.filters.${filter}`, + })) + } +} diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.html b/apps/metadata-editor/src/app/records/all-records/all-records.component.html index b63f271f04..f306af4abb 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.html +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.html @@ -77,6 +77,8 @@

dashboard.createRecord - + 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 46bfbfed2e..7cae55358c 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 @@ -30,6 +30,7 @@ import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' import { RecordsListComponent } from '../records-list.component' import { map } from 'rxjs/operators' import { SearchHeaderComponent } from '../../dashboard/search-header/search-header.component' +import { SearchFiltersComponent } from '../../dashboard/search-filters/search-filters.component' @Component({ selector: 'md-editor-all-records', @@ -49,6 +50,7 @@ import { SearchHeaderComponent } from '../../dashboard/search-header/search-head CdkConnectedOverlay, RecordsListComponent, SearchHeaderComponent, + SearchFiltersComponent, ], }) export class AllRecordsComponent { @@ -56,7 +58,7 @@ export class AllRecordsComponent { importRecordButton!: ElementRef @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef - + searchFields = ['user'] 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.html b/apps/metadata-editor/src/app/records/my-records/my-records.component.html index 95da1b8ed8..d496efb6fa 100644 --- a/apps/metadata-editor/src/app/records/my-records/my-records.component.html +++ b/apps/metadata-editor/src/app/records/my-records/my-records.component.html @@ -77,6 +77,8 @@

dashboard.createRecord - + 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 72bc39f756..2a4c544175 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 @@ -27,6 +27,7 @@ import { MatIconModule } from '@angular/material/icon' import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' import { SearchHeaderComponent } from '../../dashboard/search-header/search-header.component' import { map, Observable } from 'rxjs' +import { SearchFiltersComponent } from '../../dashboard/search-filters/search-filters.component' @Component({ selector: 'md-editor-my-records', @@ -45,6 +46,7 @@ import { map, Observable } from 'rxjs' ImportRecordComponent, FeatureSearchModule, SearchHeaderComponent, + SearchFiltersComponent, ], }) export class MyRecordsComponent implements OnInit { @@ -52,7 +54,7 @@ export class MyRecordsComponent implements OnInit { private importRecordButton!: ElementRef @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef - + searchFields = [] searchText$: Observable isImportMenuOpen = false 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 eaf8b09deb..a973d029a8 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 @@ -101,6 +101,7 @@ describe('FieldsService', () => { 'owner', 'producerOrg', 'publisherOrg', + 'user', ]) }) }) @@ -184,6 +185,7 @@ describe('FieldsService', () => { owner: [], producerOrg: [], publisherOrg: [], + user: [], }) }) }) 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 76a1850932..016b3162f4 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -10,6 +10,7 @@ import { OwnerSearchField, SimpleSearchField, TranslatedSearchField, + UserSearchField, } from './fields' import { forkJoin, Observable, of } from 'rxjs' import { map } from 'rxjs/operators' @@ -33,6 +34,7 @@ marker('search.filters.topic') marker('search.filters.contact') marker('search.filters.producerOrg') marker('search.filters.publisherOrg') +marker('search.filters.user') @Injectable({ providedIn: 'root', @@ -84,6 +86,7 @@ export class FieldsService { 'asc', 'key' ), + user: new UserSearchField(this.injector), } as Record get supportedFields() { diff --git a/libs/feature/search/src/lib/utils/service/fields.spec.ts b/libs/feature/search/src/lib/utils/service/fields.spec.ts index 16ec9b9501..39facadac2 100644 --- a/libs/feature/search/src/lib/utils/service/fields.spec.ts +++ b/libs/feature/search/src/lib/utils/service/fields.spec.ts @@ -8,6 +8,7 @@ import { OrganizationSearchField, SimpleSearchField, MultilingualSearchField, + UserSearchField, } from './fields' import { TestBed } from '@angular/core/testing' import { Injector } from '@angular/core' @@ -98,6 +99,25 @@ class RecordsRepositoryMock { ], }, }) + if (aggName === 'userinfo.keyword') + return of({ + 'userinfo.keyword': { + buckets: [ + { + term: 'admin|admin|admin|Administrator', + count: 10, + }, + { + term: 'barbie|Roberts|Barbara|UserAdmin', + count: 5, + }, + { + term: 'johndoe|Doe|John|Editor', + count: 1, + }, + ], + }, + }) const buckets = [ { term: 'First value', @@ -666,4 +686,41 @@ describe('search fields implementations', () => { }) }) }) + describe('UserSearchField', () => { + beforeEach(() => { + searchField = new UserSearchField(injector) + }) + describe('#getAvailableValues', () => { + let values + beforeEach(async () => { + values = await lastValueFrom(searchField.getAvailableValues()) + }) + it('calls aggregate with expected payload', () => { + expect(repository.aggregate).toHaveBeenCalledWith({ + 'userinfo.keyword': { + type: 'terms', + limit: 1000, + field: 'userinfo.keyword', + sort: ['asc', 'key'], + }, + }) + }) + it('returns the available users, in expected format', () => { + expect(values).toEqual([ + { + label: 'admin admin (10)', + value: 'admin|admin|admin|Administrator', + }, + { + label: 'Barbara Roberts (5)', + value: 'barbie|Roberts|Barbara|UserAdmin', + }, + { + label: 'John Doe (1)', + value: 'johndoe|Doe|John|Editor', + }, + ]) + }) + }) + }) }) diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 13f0bf2819..4a377ec969 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -346,3 +346,29 @@ export class OwnerSearchField extends SimpleSearchField { return of([]) } } + +export class UserSearchField extends SimpleSearchField { + constructor(injector: Injector) { + super('userinfo.keyword', injector, 'asc') + } + + getAvailableValues(): Observable { + return super.getAvailableValues().pipe( + map((values) => + values.map((v) => ({ + ...v, + label: this.formatUserInfo(v.label), + })) + ) + ) + } + + 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 + } +} diff --git a/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html b/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html index b94dad1bce..c0bfd3625b 100644 --- a/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +++ b/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html @@ -14,7 +14,7 @@
{{ selected.length }}
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 b4a6c4b926..fe2c86a8a5 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 @@ -95,7 +95,9 @@ person - {{ formatUserInfo(item.extras?.ownerInfo) }} + {{ + formatUserInfo(item.extras?.ownerInfo) + }} diff --git a/tailwind.base.css b/tailwind.base.css index 210e076e71..5be20d7644 100644 --- a/tailwind.base.css +++ b/tailwind.base.css @@ -129,6 +129,16 @@ border border-white focus:ring-4 focus:ring-gray-300; } + /* DROPDOWN MULTISELECT CLASS */ + .gn-ui-multiselect-counter { + --text-color: var(--gn-ui-multiselect-counter-text-color, white); + --background-color: var( + --gn-ui-multiselect-counter-background-color, + var(--color-primary-lightest) + ); + @apply bg-[color:--background-color] text-[color:--text-color]; + } + /* BADGE CLASS */ .gn-ui-badge { --rounded: var(--gn-ui-badge-rounded, 0.25em); diff --git a/translations/de.json b/translations/de.json index cfba8a822e..5e538f0551 100644 --- a/translations/de.json +++ b/translations/de.json @@ -517,6 +517,7 @@ "search.filters.topic": "Themen", "search.filters.useSpatialFilter": "Zuerst Datensätze im Interessenbereich anzeigen", "search.filters.useSpatialFilterHelp": "Wenn diese Option aktiviert ist, werden Datensätze im Bereich des Katalogs zuerst angezeigt. Datensätze außerhalb dieses Bereichs werden nicht angezeigt.", + "search.filters.user": "Editor", "share.tab.permalink": "Teilen", "share.tab.webComponent": "Integrieren", "table.loading.data": "Daten werden geladen...", diff --git a/translations/en.json b/translations/en.json index d5e48b717a..8f812703d2 100644 --- a/translations/en.json +++ b/translations/en.json @@ -517,6 +517,7 @@ "search.filters.topic": "Topics", "search.filters.useSpatialFilter": "Show records in the area of interest first", "search.filters.useSpatialFilterHelp": "When this is enabled, records within the catalog's area of interest are shown first; records outside of this area will not appear.", + "search.filters.user": "Editor", "share.tab.permalink": "Share", "share.tab.webComponent": "Integrate", "table.loading.data": "Loading data...", diff --git a/translations/es.json b/translations/es.json index 41796cba82..b640929582 100644 --- a/translations/es.json +++ b/translations/es.json @@ -517,6 +517,7 @@ "search.filters.topic": "", "search.filters.useSpatialFilter": "", "search.filters.useSpatialFilterHelp": "", + "search.filters.user": "", "share.tab.permalink": "", "share.tab.webComponent": "", "table.loading.data": "", diff --git a/translations/fr.json b/translations/fr.json index 11f3ae812f..d47fcc6573 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -517,6 +517,7 @@ "search.filters.topic": "Thèmes", "search.filters.useSpatialFilter": "Mettre en avant les résultats sur la zone d'intérêt", "search.filters.useSpatialFilterHelp": "Si cette option est activée, les fiches portant sur la zone d'intérêt du catalogue seront montrées en premier; les fiches en dehors de cette zone n'apparaîtront pas dans les résultats.", + "search.filters.user": "Éditeur", "share.tab.permalink": "Partager", "share.tab.webComponent": "Intégrer", "table.loading.data": "Chargement des données...", diff --git a/translations/it.json b/translations/it.json index 3e76378bf4..4a2436d0fd 100644 --- a/translations/it.json +++ b/translations/it.json @@ -517,6 +517,7 @@ "search.filters.topic": "Argomenti", "search.filters.useSpatialFilter": "Evidenzia i risultati nell'area di interesse", "search.filters.useSpatialFilterHelp": "Se attivata, le schede relative all'area di interesse del catalogo saranno mostrate per prime; le schede al di fuori di questa area non appariranno nei risultati.", + "search.filters.user": "", "share.tab.permalink": "Condividere", "share.tab.webComponent": "Incorporare", "table.loading.data": "Caricamento dei dati...", diff --git a/translations/nl.json b/translations/nl.json index 295e085b2d..ed11f83f1e 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -517,6 +517,7 @@ "search.filters.topic": "", "search.filters.useSpatialFilter": "", "search.filters.useSpatialFilterHelp": "", + "search.filters.user": "", "share.tab.permalink": "", "share.tab.webComponent": "", "table.loading.data": "", diff --git a/translations/pt.json b/translations/pt.json index 379707d432..253b9c5478 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -517,6 +517,7 @@ "search.filters.topic": "", "search.filters.useSpatialFilter": "", "search.filters.useSpatialFilterHelp": "", + "search.filters.user": "", "share.tab.permalink": "", "share.tab.webComponent": "", "table.loading.data": "", diff --git a/translations/sk.json b/translations/sk.json index bed0575d39..54a8f41069 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -517,6 +517,7 @@ "search.filters.topic": "Témy", "search.filters.useSpatialFilter": "Najskôr zobraziť záznamy v oblasti záujmu", "search.filters.useSpatialFilterHelp": "Keď je táto možnosť zapnutá, záznamy nachádzajúce sa v oblasti záujmu katalógu sa zobrazia ako prvé; záznamy mimo tejto oblasti sa nezobrazia.", + "search.filters.user": "", "share.tab.permalink": "Zdieľať", "share.tab.webComponent": "Integrovať", "table.loading.data": "Načítanie údajov...",