diff --git a/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts b/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts new file mode 100644 index 0000000000..2362dbcc74 --- /dev/null +++ b/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts @@ -0,0 +1,44 @@ +describe('my-org', () => { + beforeEach(() => { + cy.loginGN('barbie', 'p4ssworD_', false) + cy.visit(`/records/my-org`) + cy.get('md-editor-dashboard-menu').find('a').first().click() + cy.get('main').children('div').first().children('div').eq(1).as('linkGroup') + }) + describe('my-org display', () => { + it('should show my-org name and logo', () => { + cy.get('h1').should('not.have.text', '') + cy.get('gn-ui-thumbnail') + }) + it('should show the user and record count', () => { + cy.get('@linkGroup') + .find('a') + .children('span') + .first() + .should('not.have.text', '') + cy.get('@linkGroup') + .find('gn-ui-button') + .children('span') + .first() + .should('not.have.text', '') + }) + it('should show my-org records', () => { + cy.get('.grid').should('have.length.above', 0) + }) + }) + describe('routing', () => { + it('should access the datahub with a filter', () => { + cy.get('@linkGroup').find('a').click() + cy.url().should('include', 'search/publisher=') + }) + it('should access the user list page and show my-org users', () => { + cy.visit(`/records/my-org`) + cy.get('md-editor-dashboard-menu').find('a').first().click() + cy.get('@linkGroup').find('gn-ui-button').click() + cy.url().should('include', '/users/my-org') + cy.get('.grid').should('have.length.above', 0) + cy.get('h1').should('not.have.text', '') + cy.get('gn-ui-thumbnail') + }) + }) +}) diff --git a/apps/metadata-editor/src/app/app.routes.ts b/apps/metadata-editor/src/app/app.routes.ts index 8f95f45a5d..e6fa2b5e1a 100644 --- a/apps/metadata-editor/src/app/app.routes.ts +++ b/apps/metadata-editor/src/app/app.routes.ts @@ -10,6 +10,7 @@ import { MyRecordsComponent } from './records/my-records/my-records.component' import { MyDraftComponent } from './records/my-draft/my-draft.component' import { MyLibraryComponent } from './records/my-library/my-library.component' import { SearchRecordsComponent } from './records/search-records/search-records-list.component' +import { MyOrgUsersComponent } from './my-org-users/my-org-users.component' export const appRoutes: Route[] = [ { path: '', component: DashboardPageComponent, pathMatch: 'prefix' }, @@ -61,6 +62,10 @@ export const appRoutes: Route[] = [ }, ], }, + { + path: 'users/my-org', + component: MyOrgUsersComponent, + }, { path: 'sign-in', component: SignInPageComponent }, { path: 'create', component: CreatePageComponent }, { diff --git a/apps/metadata-editor/src/app/my-org-users/my-org-users.component.css b/apps/metadata-editor/src/app/my-org-users/my-org-users.component.css new file mode 100644 index 0000000000..8318f27f3e --- /dev/null +++ b/apps/metadata-editor/src/app/my-org-users/my-org-users.component.css @@ -0,0 +1,7 @@ +.record-table-col { + @apply px-5 py-3 items-center truncate; +} + +.record-table-header { + @apply record-table-col capitalize; +} diff --git a/apps/metadata-editor/src/app/my-org-users/my-org-users.component.html b/apps/metadata-editor/src/app/my-org-users/my-org-users.component.html new file mode 100644 index 0000000000..1ecec479eb --- /dev/null +++ b/apps/metadata-editor/src/app/my-org-users/my-org-users.component.html @@ -0,0 +1,6 @@ + + diff --git a/apps/metadata-editor/src/app/my-org-users/my-org-users.component.spec.ts b/apps/metadata-editor/src/app/my-org-users/my-org-users.component.spec.ts new file mode 100644 index 0000000000..897fa36d43 --- /dev/null +++ b/apps/metadata-editor/src/app/my-org-users/my-org-users.component.spec.ts @@ -0,0 +1,131 @@ +import { MyOrgUsersComponent } from './my-org-users.component' +import { BehaviorSubject, of } from 'rxjs' +import { MyOrgService } from '@geonetwork-ui/feature/catalog' +import { + ORGANISATIONS_FIXTURE, + USER_FIXTURE_ANON, + USERS_FIXTURE, +} from '@geonetwork-ui/common/fixtures' +import { AuthService } from '@geonetwork-ui/api/repository/gn4' +import { SearchFacade } from '@geonetwork-ui/feature/search' + +describe('MyOrgUsersComponent', () => { + let component: MyOrgUsersComponent + let searchFacade: SearchFacade + let myOrgService: MyOrgService + let authService: AuthService + + beforeEach(() => { + const user = USER_FIXTURE_ANON() + const allUsers = USERS_FIXTURE() + + const myOrgServiceMock = { + myOrgData$: of({ + orgName: 'wizard-org', + logoUrl: 'https://my-geonetwork.org/logo11.png', + recordCount: 10, + userCount: 3, + userList: [ + { + id: '161', + profile: 'Administrator', + username: 'ghost16', + name: 'Ghost', + surname: 'Old', + email: 'old.ghost@wiz.fr', + organisation: 'wizard-org', + profileIcon: + 'https://www.gravatar.com/avatar/dbdffd183622800bcf8587328daf43a6?d=mp', + }, + { + id: '3', + profile: 'Editor', + username: 'voldy63', + name: 'Lord', + surname: 'Voldemort', + email: 'lord.voldy@wiz.com', + organisation: 'wizard-org', + }, + { + id: '4', + profile: 'Editor', + username: 'al.dumble98', + name: 'Albus', + surname: 'Dumbledore', + email: 'albus.dumble@wiz.com', + organisation: 'wizard-org', + }, + ], + }), + } + + const authServiceMock = { + user$: new BehaviorSubject(user), + allUsers$: new BehaviorSubject(allUsers), + } + + const organisationsServiceMock = { + organisations$: of(ORGANISATIONS_FIXTURE), + } + + const searchFacadeMock = { + resetSearch: jest.fn(), + } + + myOrgService = myOrgServiceMock as any + authService = authServiceMock as any + searchFacade = searchFacadeMock as any + + component = new MyOrgUsersComponent(myOrgService, searchFacade) + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('Get organization users info', () => { + it('should get the org name', () => { + expect(component.orgData.orgName).toEqual('wizard-org') + }) + + it('should get the org logo', () => { + expect(component.orgData.logoUrl).toEqual( + 'https://my-geonetwork.org/logo11.png' + ) + }) + + it('should get the list of users', () => { + expect(component.orgData.userList).toEqual([ + { + id: '161', + profile: 'Administrator', + username: 'ghost16', + name: 'Ghost', + surname: 'Old', + email: 'old.ghost@wiz.fr', + organisation: 'wizard-org', + profileIcon: + 'https://www.gravatar.com/avatar/dbdffd183622800bcf8587328daf43a6?d=mp', + }, + { + id: '3', + profile: 'Editor', + username: 'voldy63', + name: 'Lord', + surname: 'Voldemort', + email: 'lord.voldy@wiz.com', + organisation: 'wizard-org', + }, + { + id: '4', + profile: 'Editor', + username: 'al.dumble98', + name: 'Albus', + surname: 'Dumbledore', + email: 'albus.dumble@wiz.com', + organisation: 'wizard-org', + }, + ]) + }) + }) +}) diff --git a/apps/metadata-editor/src/app/my-org-users/my-org-users.component.ts b/apps/metadata-editor/src/app/my-org-users/my-org-users.component.ts new file mode 100644 index 0000000000..aae4f1a49f --- /dev/null +++ b/apps/metadata-editor/src/app/my-org-users/my-org-users.component.ts @@ -0,0 +1,48 @@ +import { Component, OnDestroy } from '@angular/core' +import { RecordsListComponent } from '../records/records-list.component' +import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' +import { CommonModule } from '@angular/common' +import { MyOrgService } from '@geonetwork-ui/feature/catalog' +import { SearchFacade } from '@geonetwork-ui/feature/search' +import { UserApiModel } from '@geonetwork-ui/data-access/gn4' + +@Component({ + selector: 'md-editor-my-org-users', + templateUrl: './my-org-users.component.html', + styleUrls: ['./my-org-users.component.css'], + standalone: true, + imports: [ + RecordsListComponent, + UiInputsModule, + TranslateModule, + CommonModule, + ], +}) +export class MyOrgUsersComponent implements OnDestroy { + orgData: { + orgName: string + logoUrl: string + recordCount: number + userCount: number + userList: UserApiModel[] + } + + private myOrgDataSubscription + + constructor( + private myOrgRecordsService: MyOrgService, + public searchFacade: SearchFacade + ) { + this.searchFacade.resetSearch() + this.myOrgDataSubscription = this.myOrgRecordsService.myOrgData$.subscribe( + (data) => { + this.orgData = data + } + ) + } + + ngOnDestroy() { + this.myOrgDataSubscription.unsubscribe() + } +} diff --git a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.html b/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.html index 200b0ba65e..f5365b2f10 100644 --- a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.html +++ b/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.html @@ -1,2 +1,8 @@ - + diff --git a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.spec.ts b/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.spec.ts index 7ca9a17948..63e0629753 100644 --- a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.spec.ts +++ b/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.spec.ts @@ -1,108 +1,164 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' import { MyOrgRecordsComponent } from './my-org-records.component' -import { SearchFacade, SearchService } from '@geonetwork-ui/feature/search' -import { Component, importProvidersFrom } from '@angular/core' -import { TranslateModule } from '@ngx-translate/core' -import { RecordsListComponent } from '../records-list.component' +import { BehaviorSubject, of } from 'rxjs' +import { MyOrgService } from '@geonetwork-ui/feature/catalog' import { - FILTERS_AGGREGATION, - USER_FIXTURE, + ORGANISATIONS_FIXTURE, + USER_FIXTURE_ANON, + USERS_FIXTURE, } from '@geonetwork-ui/common/fixtures' -import { BehaviorSubject, of } from 'rxjs' -import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { AuthService } from '@geonetwork-ui/api/repository/gn4' +import { SearchFacade } from '@geonetwork-ui/feature/search' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { EditorRouterService } from '../../router.service' -const user = USER_FIXTURE() -const filters = FILTERS_AGGREGATION +const orgDataMock = { + orgName: 'wizard-org', + logoUrl: 'https://my-geonetwork.org/logo11.png', + recordCount: 10, + userCount: 3, + userList: [ + { + id: '161', + profile: 'Administrator', + username: 'ghost16', + name: 'Ghost', + surname: 'Old', + email: 'old.ghost@wiz.fr', + organisation: 'wizard-org', + profileIcon: + 'https://www.gravatar.com/avatar/dbdffd183622800bcf8587328daf43a6?d=mp', + }, + { + id: '3', + profile: 'Editor', + username: 'voldy63', + name: 'Lord', + surname: 'Voldemort', + email: 'lord.voldy@wiz.com', + organisation: 'wizard-org', + }, + { + id: '4', + profile: 'Editor', + username: 'al.dumble98', + name: 'Albus', + surname: 'Dumbledore', + email: 'albus.dumble@wiz.com', + organisation: 'wizard-org', + }, + ], +} -class AuthServiceMock { - user$ = new BehaviorSubject(user) - authReady = jest.fn(() => this._authSubject$) - _authSubject$ = new BehaviorSubject({}) +const myOrgServiceMock = { + myOrgData$: of(orgDataMock), } -class OrganisationsServiceMock { - getFiltersForOrgs = jest.fn(() => new BehaviorSubject(filters)) - organisationsCount$ = of(456) + +const user = USER_FIXTURE_ANON() +const allUsers = USERS_FIXTURE() + +const authServiceMock = { + user$: new BehaviorSubject(user), + allUsers$: new BehaviorSubject(allUsers), } -class searchServiceMock { - updateSearchFilters = jest.fn() - setSearch = jest.fn() - setSortBy = jest.fn() - setSortAndFilters = jest.fn() +const organisationsServiceMock = { + organisations$: of(ORGANISATIONS_FIXTURE), } -class SearchFacadeMock { - resetSearch = jest.fn() - setFilters = jest.fn() +const searchFacadeMock = { + resetSearch: jest.fn(), } -@Component({ - // eslint-disable-next-line - selector: 'md-editor-records-list', - template: '', - standalone: true, -}) -export class MockRecordsListComponent {} +const routeServiceMock = { + getDatahubSearchRoute: jest.fn(), +} describe('MyOrgRecordsComponent', () => { let component: MyOrgRecordsComponent - let fixture: ComponentFixture let searchFacade: SearchFacade - let orgService: OrganizationsServiceInterface + let myOrgService: MyOrgService + let authService: AuthService + let orgServiceInterface: OrganizationsServiceInterface + let routerService: EditorRouterService beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - importProvidersFrom(TranslateModule.forRoot()), - { - provide: SearchFacade, - useClass: SearchFacadeMock, - }, - { provide: AuthService, useClass: AuthServiceMock }, - { - provide: OrganizationsServiceInterface, - useClass: OrganisationsServiceMock, - }, - { - provide: SearchFacade, - useClass: SearchFacadeMock, - }, - { - provide: SearchService, - useClass: searchServiceMock, - }, - ], - }).overrideComponent(MyOrgRecordsComponent, { - remove: { - imports: [RecordsListComponent], - }, - add: { - imports: [MockRecordsListComponent], - }, - }) - searchFacade = TestBed.inject(SearchFacade) - orgService = TestBed.inject(OrganizationsServiceInterface) - fixture = TestBed.createComponent(MyOrgRecordsComponent) - component = fixture.componentInstance - fixture.detectChanges() + orgServiceInterface = organisationsServiceMock as any + myOrgService = myOrgServiceMock as any + authService = authServiceMock as any + searchFacade = searchFacadeMock as any + routerService = routeServiceMock as any + + component = new MyOrgRecordsComponent( + myOrgService, + searchFacade, + orgServiceInterface, + routerService + ) }) it('should create', () => { expect(component).toBeTruthy() }) - describe('filters', () => { - it('clears filters on init', () => { - expect(searchFacade.resetSearch).toHaveBeenCalled() + describe('Get organization users info', () => { + it('should get the org name', () => { + expect(component.orgData.orgName).toEqual('wizard-org') }) - it('filters by user organisation on init', () => { - expect(orgService.getFiltersForOrgs).toHaveBeenCalledWith([ + + it('should get the org logo', () => { + expect(component.orgData.logoUrl).toEqual( + 'https://my-geonetwork.org/logo11.png' + ) + }) + + it('should get the list of users', () => { + expect(component.orgData.userList).toEqual([ { - name: user.organisation, + id: '161', + profile: 'Administrator', + username: 'ghost16', + name: 'Ghost', + surname: 'Old', + email: 'old.ghost@wiz.fr', + organisation: 'wizard-org', + profileIcon: + 'https://www.gravatar.com/avatar/dbdffd183622800bcf8587328daf43a6?d=mp', + }, + { + id: '3', + profile: 'Editor', + username: 'voldy63', + name: 'Lord', + surname: 'Voldemort', + email: 'lord.voldy@wiz.com', + organisation: 'wizard-org', + }, + { + id: '4', + profile: 'Editor', + username: 'al.dumble98', + name: 'Albus', + surname: 'Dumbledore', + email: 'albus.dumble@wiz.com', + organisation: 'wizard-org', }, ]) - expect(searchFacade.setFilters).toHaveBeenCalledWith(filters) }) }) + it('should generate the correct Datahub URL', () => { + // Mock the router method and set orgData + component.router.getDatahubSearchRoute = () => 'http://example.com' + component.orgData = { + orgName: 'TestOrg', + logoUrl: '', + recordCount: 5, + userCount: 3, + userList: [], + } + + const datahubUrl = component.getDatahubUrl() + + // Assert that the generated URL contains the orgName + expect(datahubUrl).toContain('publisher=TestOrg') + }) }) diff --git a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts b/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts index 7a2141398e..16fbbfebd0 100644 --- a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts +++ b/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts @@ -2,11 +2,12 @@ import { Component, OnDestroy } from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' import { RecordsListComponent } from '../records-list.component' +import { MyOrgService } from '@geonetwork-ui/feature/catalog' import { SearchFacade } from '@geonetwork-ui/feature/search' -import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { Organization } from '@geonetwork-ui/common/domain/record' -import { Subscription } from 'rxjs' -import { AuthService } from '@geonetwork-ui/api/repository/gn4' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { UserApiModel } from '@geonetwork-ui/data-access/gn4' +import { EditorRouterService } from '../../router.service' @Component({ selector: 'md-editor-my-org-records', @@ -16,29 +17,48 @@ import { AuthService } from '@geonetwork-ui/api/repository/gn4' imports: [CommonModule, TranslateModule, RecordsListComponent], }) export class MyOrgRecordsComponent implements OnDestroy { - subscriptionAuthService: Subscription - subscriptionOrgService: Subscription + orgData: { + orgName: string + logoUrl: string + recordCount: number + userCount: number + userList: UserApiModel[] + } + + public myOrgDataSubscription constructor( + private myOrgRecordsService: MyOrgService, public searchFacade: SearchFacade, - private authService: AuthService, - private orgService: OrganizationsServiceInterface + public orgService: OrganizationsServiceInterface, + public router: EditorRouterService ) { this.searchFacade.resetSearch() - - this.subscriptionAuthService = this.authService.user$.subscribe((user) => { - this.searchByOrganisation({ name: user?.organisation }) - }) + this.myOrgDataSubscription = this.myOrgRecordsService.myOrgData$.subscribe( + (data) => { + this.orgData = data + this.searchByOrganisation({ name: data.orgName }) + } + ) } searchByOrganisation(organisation: Organization) { - this.subscriptionOrgService = this.orgService + this.orgService .getFiltersForOrgs([organisation]) .subscribe((filters) => this.searchFacade.setFilters(filters)) } - ngOnDestroy(): void { - this.subscriptionAuthService.unsubscribe() - this.subscriptionOrgService.unsubscribe() + getDatahubUrl(): string { + const url = new URL( + this.router.getDatahubSearchRoute(), + window.location.toString() + ) + + url.searchParams.append('publisher', this.orgData?.orgName) + return url.toString() + } + + ngOnDestroy() { + this.myOrgDataSubscription.unsubscribe() } } diff --git a/apps/metadata-editor/src/app/records/records-list.component.html b/apps/metadata-editor/src/app/records/records-list.component.html index 06e70f9220..ef2f49181f 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.html +++ b/apps/metadata-editor/src/app/records/records-list.component.html @@ -1,23 +1,68 @@
-
-

{{ title }}

+
+
+
+ + +
+
+

{{ title }}

+ np +

+ dashboard.records.users +

+
+
{{ searchFacade.resultsHits$ | async }} {{ 'record.metadata.publications' | translate }} +
+ {{ recordCount }} + dashboard.records.publishedRecords + {{ userCount }} + dashboard.records.users +
+
+ dashboard.records.noUser + dashboard.records.noRecord +
{ useClass: AvatarServiceInterfaceMock, }, ], + imports: [HttpClientTestingModule], }) }) diff --git a/libs/api/repository/src/lib/gn4/auth/auth.service.ts b/libs/api/repository/src/lib/gn4/auth/auth.service.ts index 820c89107b..b901e4fdbb 100644 --- a/libs/api/repository/src/lib/gn4/auth/auth.service.ts +++ b/libs/api/repository/src/lib/gn4/auth/auth.service.ts @@ -2,6 +2,8 @@ import { Inject, Injectable, InjectionToken, Optional } from '@angular/core' import { MeApiService, MeResponseApiModel, + UserApiModel, + UsersApiService, } from '@geonetwork-ui/data-access/gn4' import { LANG_2_TO_3_MAPPER } from '@geonetwork-ui/util/i18n' import { UserModel } from '@geonetwork-ui/common/domain/user.model' @@ -19,6 +21,7 @@ export const LOGIN_URL = new InjectionToken('loginUrl') export class AuthService { authReady$: Observable user$: Observable + allUsers$: Observable isAnonymous$ = this.authReady().pipe(map((user) => !user || !('id' in user))) baseLoginUrl = this.baseLoginUrlToken || DEFAULT_GN4_LOGIN_URL @@ -42,6 +45,7 @@ export class AuthService { @Inject(LOGIN_URL) private baseLoginUrlToken: string, private meApi: MeApiService, + private usersApi: UsersApiService, private translateService: TranslateService, private avatarService: AvatarServiceInterface ) { @@ -49,6 +53,7 @@ export class AuthService { map((apiUser) => this.mapToUserModel(apiUser)), shareReplay({ bufferSize: 1, refCount: true }) ) + this.allUsers$ = this.usersApi.getUsers().pipe(shareReplay()) } // TODO: refactor authReady diff --git a/libs/common/fixtures/src/lib/organisations.fixture.ts b/libs/common/fixtures/src/lib/organisations.fixture.ts index f78bd303cb..833d53a5b7 100644 --- a/libs/common/fixtures/src/lib/organisations.fixture.ts +++ b/libs/common/fixtures/src/lib/organisations.fixture.ts @@ -68,4 +68,10 @@ export const ORGANISATIONS_FIXTURE: Organization[] = deepFreeze([ logoUrl: new URL('https://my-geonetwork.org/logo10.png'), recordCount: 2, }, + { + name: 'wizard-org', + description: 'another org for testing', + logoUrl: new URL('https://my-geonetwork.org/logo11.png'), + recordCount: 2, + }, ]) diff --git a/libs/common/fixtures/src/lib/user.fixtures.ts b/libs/common/fixtures/src/lib/user.fixtures.ts index a1e9cea331..654fc14d21 100644 --- a/libs/common/fixtures/src/lib/user.fixtures.ts +++ b/libs/common/fixtures/src/lib/user.fixtures.ts @@ -12,8 +12,21 @@ export const USER_FIXTURE = (): UserModel => ({ 'https://www.gravatar.com/avatar/dbdffd183622800bcf8587328daf43a6?d=mp', }) +export const USER_FIXTURE_ANON = (): UserModel => ({ + id: '161', + profile: 'Administrator', + username: 'ghost16', + name: 'Ghost', + surname: 'Old', + email: 'old.ghost@wiz.fr', + organisation: 'wizard-org', + profileIcon: + 'https://www.gravatar.com/avatar/dbdffd183622800bcf8587328daf43a6?d=mp', +}) + export const USERS_FIXTURE = (): UserModel[] => [ USER_FIXTURE(), + USER_FIXTURE_ANON(), { id: '1', profile: 'Editor', @@ -32,4 +45,22 @@ export const USERS_FIXTURE = (): UserModel[] => [ email: 't.trinity@matrix.com', organisation: 'The matrix', }, + { + id: '3', + profile: 'Editor', + username: 'voldy63', + name: 'Lord', + surname: 'Voldemort', + email: 'lord.voldy@wiz.com', + organisation: 'wizard-org', + }, + { + id: '4', + profile: 'Editor', + username: 'al.dumble98', + name: 'Albus', + surname: 'Dumblerdore', + email: 'albus.dumble@wiz.com', + organisation: 'wizard-org', + }, ] diff --git a/libs/feature/catalog/src/index.ts b/libs/feature/catalog/src/index.ts index 38ad1c9e7a..fee6dd1389 100644 --- a/libs/feature/catalog/src/index.ts +++ b/libs/feature/catalog/src/index.ts @@ -5,3 +5,4 @@ export * from './lib/feature-catalog.module' export * from './lib/sources/sources.service' export * from './lib/sources/sources.model' export * from './lib/records/records.service' +export * from './lib/my-org/my-org.service' diff --git a/libs/feature/catalog/src/lib/my-org/my-org.service.spec.ts b/libs/feature/catalog/src/lib/my-org/my-org.service.spec.ts new file mode 100644 index 0000000000..e4def91fa1 --- /dev/null +++ b/libs/feature/catalog/src/lib/my-org/my-org.service.spec.ts @@ -0,0 +1,113 @@ +import { TestBed } from '@angular/core/testing' +import { MyOrgService } from './my-org.service' +import { AuthService } from '@geonetwork-ui/api/repository/gn4' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { BehaviorSubject, Observable, of, Subject } from 'rxjs' +import { UserApiModel } from '@geonetwork-ui/data-access/gn4' +import { UserModel } from '@geonetwork-ui/common/domain/user.model' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { TranslateService } from '@ngx-translate/core' +import { AvatarServiceInterface } from '@geonetwork-ui/api/repository/gn4' + +const translateServiceMock = { + currentLang: 'fr', +} + +class AvatarServiceInterfaceMock { + placeholder = 'http://placeholder.com' + getProfileIcon = (hash: string) => `${hash}` +} + +const orgs = [ + { name: 'Géo2France', logoUrl: { href: 'logo-url' }, recordCount: 10 }, +] +const orgs$ = of(orgs) + +class orgServiceMock { + organisations$ = orgs$ +} + +describe('MyOrgService', () => { + let myOrgService: MyOrgService + let authService: AuthService + let orgService: OrganizationsServiceInterface + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + MyOrgService, + { + provide: TranslateService, + useValue: translateServiceMock, + }, + { + provide: AvatarServiceInterface, + useClass: AvatarServiceInterfaceMock, + }, + { provide: OrganizationsServiceInterface, useClass: orgServiceMock }, + AuthService, + ], + imports: [HttpClientTestingModule], + }) + myOrgService = TestBed.inject(MyOrgService) + authService = TestBed.inject(AuthService) + orgService = TestBed.inject(OrganizationsServiceInterface) + }) + + it('should be created', () => { + expect(myOrgService).toBeTruthy() + }) + + it('should update myOrgDataSubject when authService user$ emits a user', () => { + const userSubject = new BehaviorSubject(null) + const user: UserModel = { + organisation: 'Géo2France', + id: '2', + profile: 'profile', + username: 'username', + name: 'name', + surname: 'surname', + email: 'email@email', + profileIcon: 'icon.com', + } + authService.user$ = userSubject.asObservable() + + userSubject.next(user) + + myOrgService.myOrgData$.subscribe((data) => { + expect(data.orgName).toEqual('Géo2France') + }) + }) + + it('should update myOrgDataSubject when orgService organisations$ emits organizations', () => { + const orgsSubject = new BehaviorSubject([]) + const orgs = [ + { name: 'Géo2France', logoUrl: { href: 'logo-url' }, recordCount: 10 }, + ] + orgService.organisations$ = orgsSubject.asObservable() + + orgsSubject.next(orgs) + + myOrgService.myOrgData$.subscribe((data) => { + expect(data.orgName).toEqual('Géo2France') + expect(data.logoUrl).toEqual('logo-url') + expect(data.recordCount).toEqual(10) + }) + }) + + it('should update myOrgDataSubject when authService allUsers$ emits users', () => { + const allUsersSubject = new BehaviorSubject([]) + const users: UserApiModel[] = [ + { organisation: 'Géo2France' }, + { organisation: 'Géo2France' }, + ] + authService.allUsers$ = allUsersSubject.asObservable() + + allUsersSubject.next(users) + + myOrgService.myOrgData$.subscribe((data) => { + expect(data.orgName).toEqual('Géo2France') + expect(data.userList.length).toEqual(2) + }) + }) +}) diff --git a/libs/feature/catalog/src/lib/my-org/my-org.service.ts b/libs/feature/catalog/src/lib/my-org/my-org.service.ts new file mode 100644 index 0000000000..b2250696fb --- /dev/null +++ b/libs/feature/catalog/src/lib/my-org/my-org.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { AuthService } from '@geonetwork-ui/api/repository/gn4' +import { BehaviorSubject, combineLatest, map, Observable } from 'rxjs' +import { UserApiModel } from '@geonetwork-ui/data-access/gn4' + +@Injectable({ + providedIn: 'root', +}) +export class MyOrgService { + myOrgData$: Observable<{ + orgName: string + logoUrl: string + recordCount: number + userCount: number + userList: UserApiModel[] + }> + + private myOrgDataSubject = new BehaviorSubject<{ + orgName: string + logoUrl: string + recordCount: number + userCount: number + userList: UserApiModel[] + }>({ + orgName: '', + logoUrl: '', + recordCount: 0, + userCount: 0, + userList: [], + }) + + constructor( + private authService: AuthService, + private orgService: OrganizationsServiceInterface + ) { + this.myOrgData$ = combineLatest([ + this.authService.user$, + this.authService.allUsers$, + this.orgService.organisations$, + ]).pipe( + map(([user, allUsers, orgs]) => { + const orgName = user.organisation + const org = orgs.find((org) => org.name === orgName) + const logoUrl = org?.logoUrl?.href.toString() + const recordCount = org?.recordCount + const userList = allUsers.filter( + (user) => user.organisation === orgName + ) + const userCount = userList.length + return { + orgName, + logoUrl, + recordCount, + userList, + userCount, + } + }) + ) + } +} diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts b/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts index 7ff3a3fca1..d5ef56539e 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts @@ -148,7 +148,7 @@ describe('OrganisationsComponent', () => { expect( orgPreviewComponents[orgPreviewComponents.length - 1].organisation .name - ).toEqual('J Data Org') + ).toEqual('wizard-org') }) }) }) diff --git a/libs/ui/search/src/lib/record-table/record-table.component.html b/libs/ui/search/src/lib/record-table/record-table.component.html index e93f1977c4..6343d7a8a1 100644 --- a/libs/ui/search/src/lib/record-table/record-table.component.html +++ b/libs/ui/search/src/lib/record-table/record-table.component.html @@ -11,8 +11,41 @@ results.records.hits.displayedOn
+
+
+
+ dashboard.records.userDetail +
+
+ dashboard.records.username +
+
+ dashboard.records.userEmail +
+
+
+
+ {{ record.name }} +
+
+ {{ record.username }} +
+
+ {{ record.emailAddresses[0] }} +
+
+
+
@@ -130,7 +163,10 @@ > {{ formats[1] }} -
+
+{{ formats.slice(2).length }}
diff --git a/libs/ui/search/src/lib/record-table/record-table.component.ts b/libs/ui/search/src/lib/record-table/record-table.component.ts index e98926e42a..2be118f1fb 100644 --- a/libs/ui/search/src/lib/record-table/record-table.component.ts +++ b/libs/ui/search/src/lib/record-table/record-table.component.ts @@ -14,7 +14,7 @@ import { SortByField } from '@geonetwork-ui/common/domain/search' styleUrls: ['./record-table.component.css'], }) export class RecordTableComponent { - @Input() records: CatalogRecord[] = [] + @Input() records: any[] = [] @Input() totalHits?: number @Input() sortBy?: SortByField @Output() recordSelect = new EventEmitter() diff --git a/translations/de.json b/translations/de.json index 51fa939bdd..c6703b3d0b 100644 --- a/translations/de.json +++ b/translations/de.json @@ -22,7 +22,14 @@ "dashboard.records.myLibrary": "", "dashboard.records.myOrg": "", "dashboard.records.myRecords": "", + "dashboard.records.noRecord": "", + "dashboard.records.noUser": "", + "dashboard.records.publishedRecords": "", "dashboard.records.search": "", + "dashboard.records.users": "", + "dashboard.records.username": "", + "dashboard.records.userDetail": "", + "dashboard.records.userEmail": "", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "Dateiformat-Erkennung", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Sammeln von Datensatzinformationen", "datafeeder.analysisProgressBar.illustration.samplingData": "Datenauswahl", diff --git a/translations/en.json b/translations/en.json index 865a83bed5..a8f6d85feb 100644 --- a/translations/en.json +++ b/translations/en.json @@ -22,7 +22,14 @@ "dashboard.records.myLibrary": "My library", "dashboard.records.myOrg": "Organization", "dashboard.records.myRecords": "My Records", + "dashboard.records.noRecord": "No record for this organization", + "dashboard.records.noUser": "No users for this organization", + "dashboard.records.publishedRecords": "published records", "dashboard.records.search": "Search for \"{searchText}\"", + "dashboard.records.users": "users", + "dashboard.records.username": "Username", + "dashboard.records.userDetail": "Name", + "dashboard.records.userEmail": "Email", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "File format \n detection", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Gathering dataset \n information", "datafeeder.analysisProgressBar.illustration.samplingData": "Sampling \n data", diff --git a/translations/es.json b/translations/es.json index ee50a1f071..deb48dee7a 100644 --- a/translations/es.json +++ b/translations/es.json @@ -22,7 +22,14 @@ "dashboard.records.myLibrary": "", "dashboard.records.myOrg": "", "dashboard.records.myRecords": "", + "dashboard.records.noRecord": "", + "dashboard.records.noUser": "", + "dashboard.records.publishedRecords": "", "dashboard.records.search": "", + "dashboard.records.users": "", + "dashboard.records.username": "", + "dashboard.records.userDetail": "", + "dashboard.records.userEmail": "", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "", "datafeeder.analysisProgressBar.illustration.samplingData": "", diff --git a/translations/fr.json b/translations/fr.json index 8c8b9880bd..b433aaa0f9 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -22,7 +22,14 @@ "dashboard.records.myLibrary": "Ma bibliothèque", "dashboard.records.myOrg": "Mon organisation", "dashboard.records.myRecords": "Mes fiches publiées", + "dashboard.records.noRecord": "Aucun jeu de données pour cette organisation", + "dashboard.records.noUser": "Aucun utilisateur pour cette organisation", + "dashboard.records.publishedRecords": "données publiées", "dashboard.records.search": "Résultats pour \"{searchText}\"", + "dashboard.records.users": "utilisateurs", + "dashboard.records.username": "Nom d'utilisateur", + "dashboard.records.userDetail": "Nom", + "dashboard.records.userEmail": "Email", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "Détection du \n format de fichier", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Récupération des informations \n sur le jeu de données", "datafeeder.analysisProgressBar.illustration.samplingData": "Sampling \n des données", diff --git a/translations/it.json b/translations/it.json index 97f1920271..db14efc962 100644 --- a/translations/it.json +++ b/translations/it.json @@ -22,7 +22,14 @@ "dashboard.records.myLibrary": "", "dashboard.records.myOrg": "", "dashboard.records.myRecords": "", + "dashboard.records.noRecord": "", + "dashboard.records.noUser": "", + "dashboard.records.publishedRecords": "", "dashboard.records.search": "", + "dashboard.records.users": "", + "dashboard.records.username": "", + "dashboard.records.userDetail": "", + "dashboard.records.userEmail": "", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "", "datafeeder.analysisProgressBar.illustration.samplingData": "", diff --git a/translations/nl.json b/translations/nl.json index 665755a10b..4af1cd9267 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -22,7 +22,14 @@ "dashboard.records.myLibrary": "", "dashboard.records.myOrg": "", "dashboard.records.myRecords": "", + "dashboard.records.noRecord": "", + "dashboard.records.noUser": "", + "dashboard.records.publishedRecords": "", "dashboard.records.search": "", + "dashboard.records.users": "", + "dashboard.records.username": "", + "dashboard.records.userDetail": "", + "dashboard.records.userEmail": "", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "", "datafeeder.analysisProgressBar.illustration.samplingData": "", diff --git a/translations/pt.json b/translations/pt.json index dbcdc6ec2a..d4a4d66612 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -22,7 +22,14 @@ "dashboard.records.myLibrary": "", "dashboard.records.myOrg": "", "dashboard.records.myRecords": "", + "dashboard.records.noRecord": "", + "dashboard.records.noUser": "", + "dashboard.records.publishedRecords": "", "dashboard.records.search": "", + "dashboard.records.users": "", + "dashboard.records.username": "", + "dashboard.records.userDetail": "", + "dashboard.records.userEmail": "", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "", "datafeeder.analysisProgressBar.illustration.samplingData": "",