From 1a9a6d3b40c1757f59c1262dea7ad2aba016adc6 Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Fri, 6 Jan 2023 14:51:43 +0100 Subject: [PATCH 01/12] refactor(organisation): put organisationCount$ as a class propery no need to have a method here, more consistent with organisations$ --- .../news-page/key-figures/key-figures.component.spec.ts | 2 +- .../home/news-page/key-figures/key-figures.component.ts | 2 +- .../src/lib/organisations/organisations.service.ts | 9 +++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.spec.ts b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.spec.ts index d9f7ae3482..8ce6ca590b 100644 --- a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.spec.ts +++ b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.spec.ts @@ -15,7 +15,7 @@ class RecordsServiceMock { } class OrganisationsServiceMock { - countOrganisations = () => of(456) + organisationsCount$ = of(456) } describe('KeyFiguresComponent', () => { diff --git a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts index 8b10714c65..38e8b70651 100644 --- a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts +++ b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts @@ -18,7 +18,7 @@ import { }) export class KeyFiguresComponent { recordsCount$ = this.catalogRecords.recordsCount$.pipe(startWith('-')) - orgsCount$ = this.catalogOrgs.countOrganisations().pipe(startWith('-')) + orgsCount$ = this.catalogOrgs.organisationsCount$.pipe(startWith('-')) ROUTE_SEARCH = `/${ROUTER_ROUTE_HOME}/${ROUTER_ROUTE_SEARCH}` ROUTE_ORGANISATIONS = `/${ROUTER_ROUTE_HOME}/${ROUTER_ROUTE_ORGANISATIONS}` diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.ts index 85f2160deb..28613af8fc 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.ts @@ -22,6 +22,9 @@ export class OrganisationsService { map((response) => response.buckets), shareReplay() ) + organisationsCount$ = this.organisations$.pipe( + map((organisations) => organisations.length) + ) constructor( private esService: ElasticsearchService, @@ -29,12 +32,6 @@ export class OrganisationsService { private aggregationsService: AggregationsService ) {} - countOrganisations(): Observable { - return this.organisations$.pipe( - map((organisations) => organisations.length) - ) - } - getOrganisationsWithGroups(): Observable { return combineLatest([this.organisations$, this.groups$]).pipe( map(([organisations, groups]) => From 2290b3a1bb5d7363bdfdd3359cb52545ac1f3fda Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Fri, 6 Jan 2023 14:52:34 +0100 Subject: [PATCH 02/12] feat(groups): create a group service in catalog library to share the groups --- .../src/lib/group/group.service.spec.ts | 44 +++++++++++++++++++ .../catalog/src/lib/group/group.service.ts | 15 +++++++ 2 files changed, 59 insertions(+) create mode 100644 libs/feature/catalog/src/lib/group/group.service.spec.ts create mode 100644 libs/feature/catalog/src/lib/group/group.service.ts diff --git a/libs/feature/catalog/src/lib/group/group.service.spec.ts b/libs/feature/catalog/src/lib/group/group.service.spec.ts new file mode 100644 index 0000000000..d0ffdc673c --- /dev/null +++ b/libs/feature/catalog/src/lib/group/group.service.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing' +import { GroupsApiService } from '@geonetwork-ui/data-access/gn4' +import { of } from 'rxjs' + +import { GroupService } from './group.service' + +const groupsApiMock = [ + { + name: 'agence', + label: { eng: 'AGENCE-DE-TEST' }, + description: 'une agence', + logo: 'logo-ag.png', + }, + { + name: 'association', + label: { eng: 'Association National du testing' }, + description: 'une association', + logo: 'logo-asso.png', + }, +] + +const groupsApiServiceMock = { + getGroups: jest.fn(() => of(groupsApiMock)), +} + +describe('GroupsService', () => { + let service: GroupService + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: GroupsApiService, + useValue: groupsApiServiceMock, + }, + ], + }) + service = TestBed.inject(GroupService) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) +}) diff --git a/libs/feature/catalog/src/lib/group/group.service.ts b/libs/feature/catalog/src/lib/group/group.service.ts new file mode 100644 index 0000000000..52235422a0 --- /dev/null +++ b/libs/feature/catalog/src/lib/group/group.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core' +import { GroupApiModel, GroupsApiService } from '@geonetwork-ui/data-access/gn4' +import { Observable } from 'rxjs' +import { shareReplay } from 'rxjs/operators' + +@Injectable({ + providedIn: 'root', +}) +export class GroupService { + groups$: Observable = this.groupsApiService + .getGroups() + .pipe(shareReplay()) + + constructor(private groupsApiService: GroupsApiService) {} +} From 6f16eb344ceb67e90f43c6157cb94fee2a107bd3 Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Fri, 6 Jan 2023 14:53:13 +0100 Subject: [PATCH 03/12] feat(users): create a user service in catalog library to share the users --- .../src/lib/users/users.service.spec.ts | 31 +++++++++++++++++++ .../catalog/src/lib/users/users.service.ts | 12 +++++++ .../shared/src/lib/fixtures/user.fixtures.ts | 24 ++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 libs/feature/catalog/src/lib/users/users.service.spec.ts create mode 100644 libs/feature/catalog/src/lib/users/users.service.ts diff --git a/libs/feature/catalog/src/lib/users/users.service.spec.ts b/libs/feature/catalog/src/lib/users/users.service.spec.ts new file mode 100644 index 0000000000..8925619f8a --- /dev/null +++ b/libs/feature/catalog/src/lib/users/users.service.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from '@angular/core/testing' +import { UsersApiService } from '@geonetwork-ui/data-access/gn4' +import { USERS_FIXTURE } from '@geonetwork-ui/util/shared/fixtures' +import { of } from 'rxjs' + +import { UsersService } from './users.service' + +class UsersApiServiceMock { + getUsers = jest.fn(() => of(USERS_FIXTURE())) +} + +describe('UsersService', () => { + let service: UsersService + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: UsersApiService, + useClass: UsersApiServiceMock, + }, + ], + }) + TestBed.inject(UsersApiService) + service = TestBed.inject(UsersService) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) +}) diff --git a/libs/feature/catalog/src/lib/users/users.service.ts b/libs/feature/catalog/src/lib/users/users.service.ts new file mode 100644 index 0000000000..77f218647e --- /dev/null +++ b/libs/feature/catalog/src/lib/users/users.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@angular/core' +import { UsersApiService } from '@geonetwork-ui/data-access/gn4' +import { shareReplay } from 'rxjs/operators' + +@Injectable({ + providedIn: 'root', +}) +export class UsersService { + users$ = this.usersService.getUsers().pipe(shareReplay()) + + constructor(private usersService: UsersApiService) {} +} diff --git a/libs/util/shared/src/lib/fixtures/user.fixtures.ts b/libs/util/shared/src/lib/fixtures/user.fixtures.ts index 170ee1ba69..56534f5ce6 100644 --- a/libs/util/shared/src/lib/fixtures/user.fixtures.ts +++ b/libs/util/shared/src/lib/fixtures/user.fixtures.ts @@ -10,3 +10,27 @@ export const USER_FIXTURE = (): UserModel => ({ organisation: 'RĂ©gion Hauts-de-France', admin: true, }) + +export const USERS_FIXTURE = (): UserModel[] => [ + USER_FIXTURE(), + { + id: '1', + profile: 'Editor', + username: 'neo', + name: 'Tomas', + surname: 'Anderson', + email: 't.anderson@matrix.com', + organisation: 'The matrix', + admin: true, + }, + { + id: '2', + profile: 'Editor', + username: 'trinity', + name: 'Tyfany', + surname: 'Trinity', + email: 't.trinity@matrix.com', + organisation: 'The matrix', + admin: true, + }, +] From b45c44073a524b5f47abd22d91accfc54ca2cf56 Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Fri, 6 Jan 2023 14:53:56 +0100 Subject: [PATCH 04/12] refactor(organisation): rely on group service instead of having its own group state --- .../catalog/src/lib/organisations/organisations.service.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.ts index 28613af8fc..1db76c275f 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core' -import { GroupApiModel, GroupsApiService } from '@geonetwork-ui/data-access/gn4' import { ElasticsearchService, Organisation } from '@geonetwork-ui/util/shared' import { AggregationsService } from '@geonetwork-ui/feature/search' import { combineLatest, Observable } from 'rxjs' import { map, shareReplay } from 'rxjs/operators' +import { GroupService } from '../group/group.service' const IMAGE_URL = '/geonetwork/images/harvesting/' @@ -15,7 +15,6 @@ interface OrganisationApiModel { providedIn: 'root', }) export class OrganisationsService { - groups$: Observable = this.groupsApiService.getGroups() organisations$: Observable = this.aggregationsService .getFullSearchTermAggregation('OrgForResource') .pipe( @@ -28,12 +27,12 @@ export class OrganisationsService { constructor( private esService: ElasticsearchService, - private groupsApiService: GroupsApiService, + private groupService: GroupService, private aggregationsService: AggregationsService ) {} getOrganisationsWithGroups(): Observable { - return combineLatest([this.organisations$, this.groups$]).pipe( + return combineLatest([this.organisations$, this.groupService.groups$]).pipe( map(([organisations, groups]) => organisations.map((organisation) => { const group = groups.find( From c9229f8d3363d4260fc5d26f590c6bedbc67af3e Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Fri, 6 Jan 2023 15:13:57 +0100 Subject: [PATCH 05/12] refactor(org): improve typing for aggregation bucket distinguish 2 observables, one from the bucket, and one for organisation models --- .../organisations/organisations.service.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.ts index 1db76c275f..039b512fb8 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@angular/core' import { ElasticsearchService, Organisation } from '@geonetwork-ui/util/shared' import { AggregationsService } from '@geonetwork-ui/feature/search' import { combineLatest, Observable } from 'rxjs' -import { map, shareReplay } from 'rxjs/operators' +import { delay, map, shareReplay } from 'rxjs/operators' import { GroupService } from '../group/group.service' const IMAGE_URL = '/geonetwork/images/harvesting/' -interface OrganisationApiModel { +interface OrganisationAggsBucket { key: string doc_count: number } @@ -15,13 +15,22 @@ interface OrganisationApiModel { providedIn: 'root', }) export class OrganisationsService { - organisations$: Observable = this.aggregationsService - .getFullSearchTermAggregation('OrgForResource') - .pipe( - map((response) => response.buckets), - shareReplay() + organisationsAggs$: Observable = + this.aggregationsService + .getFullSearchTermAggregation('OrgForResource') + .pipe( + map((response) => response.buckets), + shareReplay() + ) + organisations$: Observable = this.organisationsAggs$.pipe( + map((buckets) => + buckets.map((bucket) => ({ + name: bucket.key, + recordCount: bucket.doc_count, + })) ) - organisationsCount$ = this.organisations$.pipe( + ) + organisationsCount$ = this.organisationsAggs$.pipe( map((organisations) => organisations.length) ) From 511292380840f518c62987282ad53245fb735ec1 Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Fri, 6 Jan 2023 15:19:36 +0100 Subject: [PATCH 06/12] style(feed): card title should have title font --- .../lib/record-preview-feed/record-preview-feed.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.html b/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.html index 19d53d1b0f..6b1ade37bf 100644 --- a/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.html +++ b/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.html @@ -58,7 +58,7 @@ [ngTemplateOutletContext]="{ $implicit: record }" > -

+

{{ record.title }}

{{ record.abstract }}

From 2543bdafcba000c29fb1efaf4ad20831690ac8ca Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Fri, 6 Jan 2023 17:28:02 +0100 Subject: [PATCH 07/12] refactor(org): add ghosting with async loading display static ghost until orgs are binded with the groups, then display the hydrated orgs binding with users will come afterwards --- .../organisations.component.html | 13 +++- .../organisations.component.spec.ts | 11 ++- .../organisations/organisations.component.ts | 6 +- .../organisations.service.spec.ts | 75 +++++++++++++------ .../organisations/organisations.service.ts | 57 ++++++++------ libs/ui/elements/src/index.ts | 1 + 6 files changed, 103 insertions(+), 60 deletions(-) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.html b/libs/feature/catalog/src/lib/organisations/organisations.component.html index 2e05e295cb..670daa1950 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.html +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.html @@ -4,11 +4,16 @@
- + [showContent]="organisation.description !== null" + > + +
of(ORGANISATIONS_FIXTURE)) + hydratedOrganisations$ = of(ORGANISATIONS_FIXTURE) } class SearchServiceMock { @@ -73,6 +74,7 @@ describe('OrganisationsComponent', () => { OrganisationsSortMockComponent, OrganisationPreviewMockComponent, PaginationMockComponent, + ContentGhostComponent, ], providers: [ { @@ -111,9 +113,6 @@ describe('OrganisationsComponent', () => { beforeEach(() => { paginationComponentDE = de.query(By.directive(PaginationMockComponent)) }) - it('should call getOrganisationsWithGroups', () => { - expect(organisationsService.getOrganisationsWithGroups).toHaveBeenCalled() - }) describe('pass organisations to ui preview components', () => { beforeEach(() => { orgPreviewComponents = de diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.ts b/libs/feature/catalog/src/lib/organisations/organisations.component.ts index ee27bd8d5b..294d9d9d93 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { SearchService } from '@geonetwork-ui/feature/search' import { Organisation } from '@geonetwork-ui/util/shared' import { BehaviorSubject, combineLatest, Observable } from 'rxjs' -import { map, tap } from 'rxjs/operators' +import { map, startWith, tap } from 'rxjs/operators' import { OrganisationsService } from './organisations.service' @Component({ @@ -23,7 +23,9 @@ export class OrganisationsComponent { sortBy$ = new BehaviorSubject('name-asc') organisationsSorted$: Observable = combineLatest([ - this.organisationsService.getOrganisationsWithGroups(), + this.organisationsService.hydratedOrganisations$.pipe( + startWith(Array(this.itemsOnPage).fill({})) + ), this.sortBy$, ]).pipe( map(([organisations, sortBy]) => diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts index aea1e6da77..c1c729886c 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts @@ -2,6 +2,8 @@ import { TestBed } from '@angular/core/testing' import { GroupsApiService } from '@geonetwork-ui/data-access/gn4' import { AggregationsService } from '@geonetwork-ui/feature/search' import { of } from 'rxjs' +import { take } from 'rxjs/operators' +import { GroupService } from '../group/group.service' import { OrganisationsService } from './organisations.service' @@ -31,8 +33,8 @@ const groupsApiMock = [ }, ] -const groupsApiServiceMock = { - getGroups: jest.fn(() => of(groupsApiMock)), +const groupServiceMock = { + groups$: of(groupsApiMock), } describe('OrganisationsService', () => { @@ -46,8 +48,8 @@ describe('OrganisationsService', () => { useValue: aggregationsServiceMock, }, { - provide: GroupsApiService, - useValue: groupsApiServiceMock, + provide: GroupService, + useValue: groupServiceMock, }, ], }) @@ -57,28 +59,53 @@ describe('OrganisationsService', () => { it('should be created', () => { expect(service).toBeTruthy() }) - describe('#getOrganisationsWithGroups', () => { + describe('hydratedOrganisations$', () => { let organisations - beforeEach(() => { - service - .getOrganisationsWithGroups() - .subscribe((orgs) => (organisations = orgs)) + describe('initially', () => { + beforeEach(() => { + service.hydratedOrganisations$ + .pipe(take(1)) + .subscribe((orgs) => (organisations = orgs)) + }) + it('get rough organisations', () => { + expect(organisations).toEqual([ + { + name: 'Agence de test', + description: null, + logoUrl: null, + recordCount: 5, + }, + { + name: 'Association pour le testing', + description: null, + logoUrl: null, + recordCount: 3, + }, + ]) + }) }) - it('should get organisations, enriching first one with description, logoUrl from groups', () => { - expect(organisations).toEqual([ - { - name: 'Agence de test', - description: 'une agence', - logoUrl: '/geonetwork/images/harvesting/logo-ag.png', - recordCount: 5, - }, - { - name: 'Association pour le testing', - description: undefined, - logoUrl: undefined, - recordCount: 3, - }, - ]) + describe('when users tick', () => { + beforeEach(() => { + service.hydratedOrganisations$ + .pipe(take(2)) + .subscribe((orgs) => (organisations = orgs)) + }) + it('get organisations hydrated from groups ', () => { + expect(organisations).toEqual([ + { + name: 'Agence de test', + description: 'une agence', + logoUrl: '/geonetwork/images/harvesting/logo-ag.png', + recordCount: 5, + }, + { + name: 'Association pour le testing', + description: undefined, + logoUrl: undefined, + recordCount: 3, + }, + ]) + }) }) }) describe('#normalizeName', () => { diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.ts index 039b512fb8..dce85b4391 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core' -import { ElasticsearchService, Organisation } from '@geonetwork-ui/util/shared' +import { GroupApiModel } from '@geonetwork-ui/data-access/gn4' import { AggregationsService } from '@geonetwork-ui/feature/search' +import { ElasticsearchService, Organisation } from '@geonetwork-ui/util/shared' import { combineLatest, Observable } from 'rxjs' -import { delay, map, shareReplay } from 'rxjs/operators' +import { map, shareReplay, startWith } from 'rxjs/operators' import { GroupService } from '../group/group.service' const IMAGE_URL = '/geonetwork/images/harvesting/' @@ -27,12 +28,22 @@ export class OrganisationsService { buckets.map((bucket) => ({ name: bucket.key, recordCount: bucket.doc_count, + description: null, + logoUrl: null, })) ) ) organisationsCount$ = this.organisationsAggs$.pipe( map((organisations) => organisations.length) ) + hydratedOrganisations$ = combineLatest([ + this.organisations$, + this.groupService.groups$.pipe(startWith(null)), + ]).pipe( + map(([organisations, groups]) => { + return !groups ? organisations : this.mapWithGroups(organisations, groups) + }) + ) constructor( private esService: ElasticsearchService, @@ -40,28 +51,6 @@ export class OrganisationsService { private aggregationsService: AggregationsService ) {} - getOrganisationsWithGroups(): Observable { - return combineLatest([this.organisations$, this.groupService.groups$]).pipe( - map(([organisations, groups]) => - organisations.map((organisation) => { - const group = groups.find( - (group) => - (group.label.eng - ? this.normalizeName(group.label.eng) - : this.normalizeName(group.name)) === - this.normalizeName(organisation.key) - ) - return { - name: organisation.key, - description: group?.description, - logoUrl: group?.logo ? `${IMAGE_URL}${group.logo}` : undefined, - recordCount: organisation.doc_count, - } as Organisation - }) - ) - ) - } - normalizeName(name: string): string { return name .normalize('NFD') // decompose graphemes to remove accents from letters @@ -69,4 +58,24 @@ export class OrganisationsService { .toLowerCase() .replace(/[^a-z0-9]/g, '') // replace all except letters & numbers } + + private mapWithGroups( + organisations: Organisation[], + groups: GroupApiModel[] + ) { + return organisations.map((organisation) => { + const group = groups.find( + (group) => + (group.label.eng + ? this.normalizeName(group.label.eng) + : this.normalizeName(group.name)) === + this.normalizeName(organisation.name) + ) + return { + ...organisation, + description: group?.description || undefined, + logoUrl: group?.logo ? `${IMAGE_URL}${group.logo}` : undefined, + } as Organisation + }) + } } diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index 5003185719..289d360ce2 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -4,3 +4,4 @@ export * from './lib/metadata-contact/metadata-contact.component' export * from './lib/metadata-catalog/metadata-catalog.component' export * from './lib/search-results-error/search-results-error.component' export * from './lib/thumbnail/thumbnail.component' +export * from './lib/content-ghost/content-ghost.component' From f60430676fb522b7d7d5f68e5de8fa5c26a21e01 Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Sun, 8 Jan 2023 13:17:23 +0100 Subject: [PATCH 08/12] feat(org): use multi_terms to catch org emails used for the mapping with gn users --- .../organisations.service.spec.ts | 32 ++++++--- .../organisations/organisations.service.ts | 67 ++++++++++++++++--- .../shared/src/lib/models/search.model.ts | 1 + 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts index c1c729886c..a89831e354 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts @@ -1,6 +1,5 @@ import { TestBed } from '@angular/core/testing' -import { GroupsApiService } from '@geonetwork-ui/data-access/gn4' -import { AggregationsService } from '@geonetwork-ui/feature/search' +import { SearchApiService } from '@geonetwork-ui/data-access/gn4' import { of } from 'rxjs' import { take } from 'rxjs/operators' import { GroupService } from '../group/group.service' @@ -8,14 +7,23 @@ import { GroupService } from '../group/group.service' import { OrganisationsService } from './organisations.service' const organisationsAggregationMock = { - buckets: [ - { key: 'Agence de test', doc_count: 5 }, - { key: 'Association pour le testing', doc_count: 3 }, - ], + aggregations: { + contact: { + org: { + buckets: [ + { key: ['Agence de test', 'test@agence.com'], doc_count: 5 }, + { + key: ['Association pour le testing', 'testing@assoc.net'], + doc_count: 3, + }, + ], + }, + }, + }, } -const aggregationsServiceMock = { - getFullSearchTermAggregation: jest.fn(() => of(organisationsAggregationMock)), +const searchApiServiceMock = { + search: jest.fn(() => of(organisationsAggregationMock)), } const groupsApiMock = [ @@ -44,8 +52,8 @@ describe('OrganisationsService', () => { TestBed.configureTestingModule({ providers: [ { - provide: AggregationsService, - useValue: aggregationsServiceMock, + provide: SearchApiService, + useValue: searchApiServiceMock, }, { provide: GroupService, @@ -71,12 +79,14 @@ describe('OrganisationsService', () => { expect(organisations).toEqual([ { name: 'Agence de test', + email: 'test@agence.com', description: null, logoUrl: null, recordCount: 5, }, { name: 'Association pour le testing', + email: 'testing@assoc.net', description: null, logoUrl: null, recordCount: 3, @@ -95,11 +105,13 @@ describe('OrganisationsService', () => { { name: 'Agence de test', description: 'une agence', + email: 'test@agence.com', logoUrl: '/geonetwork/images/harvesting/logo-ag.png', recordCount: 5, }, { name: 'Association pour le testing', + email: 'testing@assoc.net', description: undefined, logoUrl: undefined, recordCount: 3, diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.ts index dce85b4391..b1ed61f371 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core' -import { GroupApiModel } from '@geonetwork-ui/data-access/gn4' +import { GroupApiModel, SearchApiService } from '@geonetwork-ui/data-access/gn4' import { AggregationsService } from '@geonetwork-ui/feature/search' import { ElasticsearchService, Organisation } from '@geonetwork-ui/util/shared' import { combineLatest, Observable } from 'rxjs' -import { map, shareReplay, startWith } from 'rxjs/operators' +import { filter, map, shareReplay, startWith, tap } from 'rxjs/operators' import { GroupService } from '../group/group.service' const IMAGE_URL = '/geonetwork/images/harvesting/' @@ -17,16 +17,15 @@ interface OrganisationAggsBucket { }) export class OrganisationsService { organisationsAggs$: Observable = - this.aggregationsService - .getFullSearchTermAggregation('OrgForResource') - .pipe( - map((response) => response.buckets), - shareReplay() - ) + this.fetchOrgForResourceAggs().pipe( + map((response) => response.buckets), + shareReplay() + ) organisations$: Observable = this.organisationsAggs$.pipe( map((buckets) => buckets.map((bucket) => ({ - name: bucket.key, + name: bucket.key[0], + email: bucket.key[1], recordCount: bucket.doc_count, description: null, logoUrl: null, @@ -47,8 +46,8 @@ export class OrganisationsService { constructor( private esService: ElasticsearchService, - private groupService: GroupService, - private aggregationsService: AggregationsService + private searchApiService: SearchApiService, + private groupService: GroupService ) {} normalizeName(name: string): string { @@ -59,6 +58,52 @@ export class OrganisationsService { .replace(/[^a-z0-9]/g, '') // replace all except letters & numbers } + private fetchOrgForResourceAggs() { + return this.searchApiService + .search( + 'bucket', + JSON.stringify( + this.esService.getSearchRequestBody({ + contact: { + nested: { + path: 'contactForResource', + }, + aggs: { + org: { + multi_terms: { + size: 1000, + order: { _key: 'asc' }, + terms: [ + { + field: 'contactForResource.organisation', + }, + { + field: 'contactForResource.email.keyword', + }, + ], + }, + }, + }, + }, + }) + ) + ) + .pipe( + filter((response) => response.aggregations.contact.org), + map((response) => response.aggregations.contact.org), + tap(({ buckets }) => { + console.log( + buckets.reduce((output, bucket) => { + const org = bucket.key[0] + output[org] = output[org] ?? 0 + output[org]++ + return output + }, {}) + ) + }) + ) + } + private mapWithGroups( organisations: Organisation[], groups: GroupApiModel[] diff --git a/libs/util/shared/src/lib/models/search.model.ts b/libs/util/shared/src/lib/models/search.model.ts index a7f3c76181..85aff0462c 100644 --- a/libs/util/shared/src/lib/models/search.model.ts +++ b/libs/util/shared/src/lib/models/search.model.ts @@ -18,6 +18,7 @@ export interface Organisation { description?: string logoUrl?: string recordCount?: number + email?: string } export interface MetadataContact { From 5431157b3e9ef5a61219c5e1f2740894b325bef0 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 11 Jan 2023 17:59:41 +0100 Subject: [PATCH 09/12] feat(org): add org-group mapping via email --- .../organisations.service.spec.ts | 52 ++++++++++++---- .../organisations/organisations.service.ts | 59 +++++++++++-------- 2 files changed, 76 insertions(+), 35 deletions(-) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts index a89831e354..14d674d14d 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts @@ -38,6 +38,7 @@ const groupsApiMock = [ label: { eng: 'Association National du testing' }, description: 'une association', logo: 'logo-asso.png', + email: 'testing@assoc.net', }, ] @@ -94,13 +95,14 @@ describe('OrganisationsService', () => { ]) }) }) - describe('when users tick', () => { + describe('when groups tick', () => { beforeEach(() => { + organisations = null service.hydratedOrganisations$ .pipe(take(2)) .subscribe((orgs) => (organisations = orgs)) }) - it('get organisations hydrated from groups ', () => { + it('get organisations hydrated from groups via name or email mapping', () => { expect(organisations).toEqual([ { name: 'Agence de test', @@ -112,29 +114,57 @@ describe('OrganisationsService', () => { { name: 'Association pour le testing', email: 'testing@assoc.net', - description: undefined, - logoUrl: undefined, + description: 'une association', + logoUrl: '/geonetwork/images/harvesting/logo-asso.png', recordCount: 3, }, ]) }) }) }) - describe('#normalizeName', () => { + describe('#normalizeString', () => { it('should match "ATMO Haut de France" and "ATMO Haut-de-France"', () => { - expect(service.normalizeName('ATMO Haut de France')).toEqual( - service.normalizeName('ATMO Haut-de-France') + expect(service.normalizeString('ATMO Haut de France')).toEqual( + service.normalizeString('ATMO Haut-de-France') ) }) it('should match "ATMO Haut de France" and "ATMOHautdeFrance"', () => { - expect(service.normalizeName('ATMO Haut de France')).toEqual( - service.normalizeName('ATMOHautdeFrance') + expect(service.normalizeString('ATMO Haut de France')).toEqual( + service.normalizeString('ATMOHautdeFrance') ) }) it('should NOT match "ATMO Haut de France" and "ATMO HDF"', () => { - expect(service.normalizeName('ATMO Haut de France')).not.toEqual( - service.normalizeName('ATMO HDF') + expect(service.normalizeString('ATMO Haut de France')).not.toEqual( + service.normalizeString('ATMO HDF') ) }) }) + describe('#compareNormalizedString', () => { + it('should match "ATMO Haut de France" and "ATMO Haut-de-France"', () => { + expect( + service.compareNormalizedStrings( + 'ATMO Haut de France', + 'ATMO Haut-de-France' + ) + ).toBeTruthy() + }) + it('should NOT match "ATMO Haut de France" and "ATMO Haut-de-France" (not replacing special chars)', () => { + expect( + service.compareNormalizedStrings( + 'ATMO Haut de France', + 'ATMO Haut-de-France', + false + ) + ).toBeFalsy() + }) + it('should match email adresses (not replacing special chars)', () => { + expect( + service.compareNormalizedStrings( + 'Some.user@C2C.com', + 'some.user@c2c.com', + false + ) + ).toBeTruthy() + }) + }) }) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.ts index b1ed61f371..b1f1a16157 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core' import { GroupApiModel, SearchApiService } from '@geonetwork-ui/data-access/gn4' -import { AggregationsService } from '@geonetwork-ui/feature/search' import { ElasticsearchService, Organisation } from '@geonetwork-ui/util/shared' import { combineLatest, Observable } from 'rxjs' import { filter, map, shareReplay, startWith, tap } from 'rxjs/operators' @@ -50,12 +49,30 @@ export class OrganisationsService { private groupService: GroupService ) {} - normalizeName(name: string): string { - return name - .normalize('NFD') // decompose graphemes to remove accents from letters - .replace(/[\u0300-\u036f]/g, '') // remove accent characters - .toLowerCase() - .replace(/[^a-z0-9]/g, '') // replace all except letters & numbers + compareNormalizedStrings( + str1: string, + str2: string, + replaceSpecialChars = true + ): boolean { + if (!str1 || !str2) return false + return ( + this.normalizeString(str1, replaceSpecialChars) === + this.normalizeString(str2, replaceSpecialChars) + ) + } + + normalizeString(str: string, replaceSpecialChars = true): string { + function normalize(str: string) { + return str + .normalize('NFD') // decompose graphemes to remove accents from letters + .replace(/[\u0300-\u036f]/g, '') // remove accent characters + .toLowerCase() + } + if (replaceSpecialChars) { + return normalize(str).replace(/[^a-z0-9]/g, '') // replace all except letters & numbers + } else { + return normalize(str) + } } private fetchOrgForResourceAggs() { @@ -90,17 +107,7 @@ export class OrganisationsService { ) .pipe( filter((response) => response.aggregations.contact.org), - map((response) => response.aggregations.contact.org), - tap(({ buckets }) => { - console.log( - buckets.reduce((output, bucket) => { - const org = bucket.key[0] - output[org] = output[org] ?? 0 - output[org]++ - return output - }, {}) - ) - }) + map((response) => response.aggregations.contact.org) ) } @@ -109,13 +116,17 @@ export class OrganisationsService { groups: GroupApiModel[] ) { return organisations.map((organisation) => { - const group = groups.find( - (group) => - (group.label.eng - ? this.normalizeName(group.label.eng) - : this.normalizeName(group.name)) === - this.normalizeName(organisation.name) + let group = groups.find((group) => + this.compareNormalizedStrings( + group.label.eng ? group.label.eng : group.name, + organisation.name + ) ) + if (!group) { + group = groups.find((group) => + this.compareNormalizedStrings(group.email, organisation.email, false) + ) + } return { ...organisation, description: group?.description || undefined, From e7bcd4597070c993f5abb547b2913815398b3624 Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Thu, 12 Jan 2023 12:32:53 +0100 Subject: [PATCH 10/12] refactor(org): improve naming and syntax --- .../organisations.service.spec.ts | 6 +++--- .../organisations/organisations.service.ts | 21 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts index 14d674d14d..e63282eab8 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts @@ -142,7 +142,7 @@ describe('OrganisationsService', () => { describe('#compareNormalizedString', () => { it('should match "ATMO Haut de France" and "ATMO Haut-de-France"', () => { expect( - service.compareNormalizedStrings( + service.equalsNormalizedStrings( 'ATMO Haut de France', 'ATMO Haut-de-France' ) @@ -150,7 +150,7 @@ describe('OrganisationsService', () => { }) it('should NOT match "ATMO Haut de France" and "ATMO Haut-de-France" (not replacing special chars)', () => { expect( - service.compareNormalizedStrings( + service.equalsNormalizedStrings( 'ATMO Haut de France', 'ATMO Haut-de-France', false @@ -159,7 +159,7 @@ describe('OrganisationsService', () => { }) it('should match email adresses (not replacing special chars)', () => { expect( - service.compareNormalizedStrings( + service.equalsNormalizedStrings( 'Some.user@C2C.com', 'some.user@c2c.com', false diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.ts index b1f1a16157..242eca9043 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.ts @@ -49,7 +49,7 @@ export class OrganisationsService { private groupService: GroupService ) {} - compareNormalizedStrings( + equalsNormalizedStrings( str1: string, str2: string, replaceSpecialChars = true @@ -116,17 +116,16 @@ export class OrganisationsService { groups: GroupApiModel[] ) { return organisations.map((organisation) => { - let group = groups.find((group) => - this.compareNormalizedStrings( - group.label.eng ? group.label.eng : group.name, - organisation.name + const group = + groups.find((group) => + this.equalsNormalizedStrings( + group.label.eng ? group.label.eng : group.name, + organisation.name + ) + ) ?? + groups.find((group) => + this.equalsNormalizedStrings(group.email, organisation.email, false) ) - ) - if (!group) { - group = groups.find((group) => - this.compareNormalizedStrings(group.email, organisation.email, false) - ) - } return { ...organisation, description: group?.description || undefined, From e2b115ee25e918ead54a47b53d802ed5937ece7b Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Thu, 12 Jan 2023 12:33:31 +0100 Subject: [PATCH 11/12] style(cards): change card title on hover for organisations and datasets pages --- .../organisation-preview.component.html | 11 ++++------- .../record-preview-row.component.html | 8 +++++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.html b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.html index 37aeb1f354..e9e78ecfff 100644 --- a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.html +++ b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.html @@ -1,20 +1,17 @@ -
+
{{ organisation.name }} diff --git a/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html b/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html index 7f7e526755..77c8379fbc 100644 --- a/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +++ b/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html @@ -1,4 +1,7 @@ -
+
{{ record.title }}
From ae119a91728241bdcdd0caa8d34618e1421f6de6 Mon Sep 17 00:00:00 2001 From: Florent gravin Date: Thu, 12 Jan 2023 13:55:06 +0100 Subject: [PATCH 12/12] refactor(org): improve nested facets get distinct organisation name, then all emails for each organisation fix ref_count of organisation and improve mail matching --- .../organisations.service.spec.ts | 62 +++++++++++++++---- .../organisations/organisations.service.ts | 41 ++++++++---- .../shared/src/lib/models/search.model.ts | 1 + 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts index e63282eab8..d993c0f7b6 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.spec.ts @@ -11,10 +11,37 @@ const organisationsAggregationMock = { contact: { org: { buckets: [ - { key: ['Agence de test', 'test@agence.com'], doc_count: 5 }, { - key: ['Association pour le testing', 'testing@assoc.net'], - doc_count: 3, + key: 'Agence de test', + doc_count: 5, + mail: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'test@agence.com', + doc_count: 3, + }, + { + key: 'test2@agence.com', + doc_count: 1, + }, + ], + }, + }, + { + key: 'Association pour le testing', + doc_count: 1, + mail: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'testing@assoc.net', + doc_count: 1, + }, + ], + }, }, ], }, @@ -27,6 +54,13 @@ const searchApiServiceMock = { } const groupsApiMock = [ + { + name: 'agence de test', + label: { eng: 'AGENCE-DE-TEST' }, + description: 'une agence', + email: 'test@test.net', + logo: 'logo-ag.png', + }, { name: 'agence', label: { eng: 'AGENCE-DE-TEST' }, @@ -79,18 +113,18 @@ describe('OrganisationsService', () => { it('get rough organisations', () => { expect(organisations).toEqual([ { - name: 'Agence de test', - email: 'test@agence.com', description: null, + emails: ['test@agence.com', 'test2@agence.com'], logoUrl: null, + name: 'Agence de test', recordCount: 5, }, { - name: 'Association pour le testing', - email: 'testing@assoc.net', description: null, + emails: ['testing@assoc.net'], logoUrl: null, - recordCount: 3, + name: 'Association pour le testing', + recordCount: 1, }, ]) }) @@ -105,18 +139,20 @@ describe('OrganisationsService', () => { it('get organisations hydrated from groups via name or email mapping', () => { expect(organisations).toEqual([ { - name: 'Agence de test', description: 'une agence', - email: 'test@agence.com', + email: 'test@test.net', + emails: ['test@agence.com', 'test2@agence.com'], logoUrl: '/geonetwork/images/harvesting/logo-ag.png', + name: 'Agence de test', recordCount: 5, }, { - name: 'Association pour le testing', - email: 'testing@assoc.net', description: 'une association', + email: 'testing@assoc.net', + emails: ['testing@assoc.net'], logoUrl: '/geonetwork/images/harvesting/logo-asso.png', - recordCount: 3, + name: 'Association pour le testing', + recordCount: 1, }, ]) }) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.service.ts b/libs/feature/catalog/src/lib/organisations/organisations.service.ts index 242eca9043..b8fa40fd14 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.service.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.service.ts @@ -7,10 +7,15 @@ import { GroupService } from '../group/group.service' const IMAGE_URL = '/geonetwork/images/harvesting/' -interface OrganisationAggsBucket { +type ESBucket = { key: string doc_count: number } +interface OrganisationAggsBucket extends ESBucket { + mail: { + buckets: ESBucket[] + } +} @Injectable({ providedIn: 'root', }) @@ -23,8 +28,10 @@ export class OrganisationsService { organisations$: Observable = this.organisationsAggs$.pipe( map((buckets) => buckets.map((bucket) => ({ - name: bucket.key[0], - email: bucket.key[1], + name: bucket.key, + emails: bucket.mail.buckets + .map((bucket) => bucket.key) + .filter((mail) => !!mail), recordCount: bucket.doc_count, description: null, logoUrl: null, @@ -87,17 +94,20 @@ export class OrganisationsService { }, aggs: { org: { - multi_terms: { + terms: { + field: 'contactForResource.organisation', + exclude: '', size: 1000, order: { _key: 'asc' }, - terms: [ - { - field: 'contactForResource.organisation', - }, - { + }, + aggs: { + mail: { + terms: { + size: 1000, + exclude: '', field: 'contactForResource.email.keyword', }, - ], + }, }, }, }, @@ -123,11 +133,16 @@ export class OrganisationsService { organisation.name ) ) ?? - groups.find((group) => - this.equalsNormalizedStrings(group.email, organisation.email, false) - ) + groups + .filter((group) => !!group.email) + .find((group) => + organisation.emails + .map((mail) => this.normalizeString(mail, false)) + .includes(this.normalizeString(group.email, false)) + ) return { ...organisation, + email: group?.email || undefined, description: group?.description || undefined, logoUrl: group?.logo ? `${IMAGE_URL}${group.logo}` : undefined, } as Organisation diff --git a/libs/util/shared/src/lib/models/search.model.ts b/libs/util/shared/src/lib/models/search.model.ts index 85aff0462c..0ffb128edb 100644 --- a/libs/util/shared/src/lib/models/search.model.ts +++ b/libs/util/shared/src/lib/models/search.model.ts @@ -19,6 +19,7 @@ export interface Organisation { logoUrl?: string recordCount?: number email?: string + emails?: string[] } export interface MetadataContact {