From 9b4b506ce209986de7fd62390904d47000ec3893 Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Wed, 4 Oct 2023 17:19:31 +0200 Subject: [PATCH 1/5] feat: implement my-record in datahub and metadata-editor --- .../search-filters.component.html | 26 +++++++++- .../search-filters.component.spec.ts | 32 ++++++++---- .../search-filters.component.ts | 25 ++++++++- .../my-records/my-records.component.html | 5 +- .../my-records/my-records.component.spec.ts | 52 +++++++++++++++++-- .../my-records/my-records.component.ts | 44 ++++++++++++++-- .../app/records/records-list.component.html | 9 ++++ .../src/app/records/records-list.component.ts | 13 ++++- .../metadata-editor/src/app/router.service.ts | 5 ++ conf/default.toml | 1 + .../src/lib/search/aggregation.model.ts | 4 +- .../search/src/lib/state/search.facade.ts | 2 +- .../lib/utils/service/fields.service.spec.ts | 2 + .../src/lib/utils/service/fields.service.ts | 2 + .../search/src/lib/utils/service/fields.ts | 9 ++++ .../src/lib/utils/service/search.service.ts | 4 ++ libs/util/app-config/src/lib/app-config.ts | 2 + libs/util/app-config/src/lib/model.ts | 1 + translations/de.json | 4 +- translations/en.json | 4 +- translations/es.json | 4 +- translations/fr.json | 4 +- translations/it.json | 4 +- translations/nl.json | 4 +- translations/pt.json | 4 +- 25 files changed, 234 insertions(+), 32 deletions(-) diff --git a/apps/datahub/src/app/home/search/search-filters/search-filters.component.html b/apps/datahub/src/app/home/search/search-filters/search-filters.component.html index e4f7b3eed9..1becddb161 100644 --- a/apps/datahub/src/app/home/search/search-filters/search-filters.component.html +++ b/apps/datahub/src/app/home/search/search-filters/search-filters.component.html @@ -53,7 +53,7 @@

>
(toggled)="toggleSpatialFilter($event)" >
+
+ +
+
+ + {{ 'search.filters.otherRecords' | translate }} + close + +
({ getOptionalSearchConfig: () => ({ @@ -67,6 +66,7 @@ export class MockFilterDropdownComponent { @Input() title: string } const state = { OrgForResource: { mel: true } } as FieldFilters +const user = USER_FIXTURE() class SearchFacadeMock { searchFilters$ = new BehaviorSubject(state) hasSpatialFilter$ = new BehaviorSubject(false) @@ -103,6 +103,12 @@ class FieldsServiceMock { } } +class AuthServiceMock { + user$ = new BehaviorSubject(user) + authReady = jest.fn(() => this._authSubject$) + _authSubject$ = new BehaviorSubject({}) +} + describe('SearchFiltersComponent', () => { let component: SearchFiltersComponent let fixture: ComponentFixture @@ -131,6 +137,10 @@ describe('SearchFiltersComponent', () => { provide: FieldsService, useClass: FieldsServiceMock, }, + { + provide: AuthService, + useClass: AuthServiceMock, + }, ], }) .overrideComponent(SearchFiltersComponent, { @@ -155,7 +165,9 @@ describe('SearchFiltersComponent', () => { describe('spatial filter button', () => { function getCheckToggleDebugElement() { - return fixture.debugElement.query(By.directive(MockCheckToggleComponent)) + return fixture.debugElement.queryAll( + By.directive(MockCheckToggleComponent) + ) } describe('when panel is closed', () => { @@ -164,7 +176,7 @@ describe('SearchFiltersComponent', () => { fixture.detectChanges() }) it('does not show up', () => { - expect(getCheckToggleDebugElement()).toBeFalsy() + expect(getCheckToggleDebugElement().length).toBeFalsy() }) }) describe('when panel is opened & a spatial filter is unavailable', () => { @@ -174,7 +186,7 @@ describe('SearchFiltersComponent', () => { fixture.detectChanges() }) it('does not show up', () => { - expect(getCheckToggleDebugElement()).toBeFalsy() + expect(getCheckToggleDebugElement().length).toBe(1) }) }) describe('when panel is opened & a spatial filter is available', () => { @@ -185,11 +197,11 @@ describe('SearchFiltersComponent', () => { fixture.detectChanges() }) it('does show up', () => { - expect(getCheckToggleDebugElement()).toBeTruthy() + expect(getCheckToggleDebugElement().length).toBe(2) }) it('has the value set in the state', () => { expect( - getCheckToggleDebugElement().componentInstance.value + getCheckToggleDebugElement()[0].componentInstance.value ).toBeTruthy() }) }) @@ -201,7 +213,7 @@ describe('SearchFiltersComponent', () => { }) it('emits a SetSpatialFilterEnabled action', () => { const checkToggleComponent = - getCheckToggleDebugElement().componentInstance + getCheckToggleDebugElement()[0].componentInstance checkToggleComponent.toggled.emit(false) expect(searchFacade.setSpatialFilterEnabled).toHaveBeenCalledWith(false) }) diff --git a/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts b/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts index f28155e0db..77c87748f2 100644 --- a/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts +++ b/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts @@ -5,6 +5,7 @@ import { QueryList, ViewChildren, } from '@angular/core' +import { AuthService } from '@geonetwork-ui/api/repository/gn4' import { FieldsService, FilterDropdownComponent, @@ -12,6 +13,8 @@ import { SearchService, } from '@geonetwork-ui/feature/search' import { getOptionalSearchConfig } from '@geonetwork-ui/util/app-config' +import { Observable, switchMap } from 'rxjs' +import { map } from 'rxjs/operators' @Component({ selector: 'datahub-search-filters', @@ -24,14 +27,28 @@ export class SearchFiltersComponent implements OnInit { filters: QueryList searchConfig: { fieldName: string; title: string }[] isOpen = false + userId: string + myRecordsFilterEnabled$: Observable = + this.searchFacade.searchFilters$.pipe( + switchMap((filters) => { + return this.fieldsService.readFieldValuesFromFilters(filters) + }), + map((fieldValues) => + fieldValues['owner'] && Array.isArray(fieldValues['owner']) + ? fieldValues['owner'].length > 0 + : !!fieldValues['owner'] + ) + ) constructor( public searchFacade: SearchFacade, private searchService: SearchService, - private fieldsService: FieldsService + private fieldsService: FieldsService, + private authService: AuthService ) {} ngOnInit(): void { + this.authService.user$.subscribe((user) => (this.userId = user?.id)) this.searchConfig = ( getOptionalSearchConfig().ADVANCED_FILTERS || [ 'publisher', @@ -70,6 +87,12 @@ export class SearchFiltersComponent implements OnInit { this.searchFacade.setSpatialFilterEnabled(enabled) } + toggleMyRecordsFilter(enabled: boolean) { + this.fieldsService + .buildFiltersFromFieldValues({ owner: enabled ? this.userId : [] }) + .subscribe((filters) => this.searchService.updateFilters(filters)) + } + clearFilters() { const fieldNames = this.filters.map((component) => component.fieldName) const fieldValues = fieldNames.reduce( 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 8b3aa4d840..049f7af0c3 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 @@ -1,2 +1,5 @@ - + diff --git a/apps/metadata-editor/src/app/records/my-records/my-records.component.spec.ts b/apps/metadata-editor/src/app/records/my-records/my-records.component.spec.ts index f1bca7b7ba..f1fbc3d519 100644 --- a/apps/metadata-editor/src/app/records/my-records/my-records.component.spec.ts +++ b/apps/metadata-editor/src/app/records/my-records/my-records.component.spec.ts @@ -1,9 +1,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { MyRecordsComponent } from './my-records.component' -import { SearchFacade } from '@geonetwork-ui/feature/search' -import { Component, importProvidersFrom } from '@angular/core' +import { FieldsService, SearchFacade } from '@geonetwork-ui/feature/search' +import { Component, importProvidersFrom, Input } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' import { RecordsListComponent } from '../records-list.component' +import { BehaviorSubject, of } from 'rxjs' +import { USER_FIXTURE } from '@geonetwork-ui/common/fixtures' +import { AuthService } from '@geonetwork-ui/api/repository/gn4' +import { EditorRouterService } from '../../router.service' @Component({ // eslint-disable-next-line @@ -11,10 +15,27 @@ import { RecordsListComponent } from '../records-list.component' template: '', standalone: true, }) -export class MockRecordsListComponent {} +export class MockRecordsListComponent { + @Input() linkToDatahub: string +} +const user = USER_FIXTURE() class SearchFacadeMock { resetSearch = jest.fn() + updateFilters = jest.fn() +} +class EditorRouterServiceMock { + getDatahubSearchRoute = jest.fn(() => `/datahub/`) +} + +class AuthServiceMock { + user$ = new BehaviorSubject(user) + authReady = jest.fn(() => this._authSubject$) + _authSubject$ = new BehaviorSubject({}) +} + +class FieldsServiceMock { + buildFiltersFromFieldValues = jest.fn((val) => of(val)) } describe('MyRecordsComponent', () => { @@ -26,10 +47,22 @@ describe('MyRecordsComponent', () => { TestBed.configureTestingModule({ providers: [ importProvidersFrom(TranslateModule.forRoot()), + { + provide: FieldsService, + useClass: FieldsServiceMock, + }, { provide: SearchFacade, useClass: SearchFacadeMock, }, + { + provide: AuthService, + useClass: AuthServiceMock, + }, + { + provide: EditorRouterService, + useClass: EditorRouterServiceMock, + }, ], }).overrideComponent(MyRecordsComponent, { remove: { @@ -53,5 +86,18 @@ describe('MyRecordsComponent', () => { it('clears filters on init', () => { expect(searchFacade.resetSearch).toHaveBeenCalled() }) + it('Update filters on init', () => { + expect(searchFacade.updateFilters).toHaveBeenCalledWith({ + owner: user.id, + }) + }) + }) + + describe('datahub url', () => { + it('get correct url', () => { + expect(component.getDatahubUrl()).toEqual( + 'http://localhost/datahub/?owner=46798' + ) + }) }) }) 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 c6c1e0bce7..cd12f08f91 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 @@ -1,8 +1,11 @@ -import { Component } from '@angular/core' +import { Component, OnDestroy, OnInit } from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' import { RecordsListComponent } from '../records-list.component' -import { SearchFacade } from '@geonetwork-ui/feature/search' +import { FieldsService, SearchFacade } from '@geonetwork-ui/feature/search' +import { AuthService } from '@geonetwork-ui/api/repository/gn4' +import { EditorRouterService } from '../../router.service' +import { Subscription } from 'rxjs' @Component({ selector: 'md-editor-my-records', @@ -11,8 +14,41 @@ import { SearchFacade } from '@geonetwork-ui/feature/search' standalone: true, imports: [CommonModule, TranslateModule, RecordsListComponent], }) -export class MyRecordsComponent { - constructor(public searchFacade: SearchFacade) { +export class MyRecordsComponent implements OnInit, OnDestroy { + private sub: Subscription + private ownerId: string + + constructor( + public fieldsService: FieldsService, + public searchFacade: SearchFacade, + private authService: AuthService, + private router: EditorRouterService + ) {} + + ngOnInit() { this.searchFacade.resetSearch() + this.sub = this.authService.user$.subscribe((user) => { + this.ownerId = user.id + this.fieldsService + .buildFiltersFromFieldValues({ owner: user.id }) + .subscribe((filters) => { + this.searchFacade.updateFilters(filters) + }) + }) + } + + getDatahubUrl(): string { + const url = new URL( + `${this.router.getDatahubSearchRoute()}`, + this.router.getDatahubSearchRoute().startsWith('http') + ? this.router.getDatahubSearchRoute() + : window.location.toString() + ) + url.searchParams.append('owner', this.ownerId) + return url.toString() + } + + ngOnDestroy(): void { + this.sub.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 01198dccb5..06e70f9220 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.html +++ b/apps/metadata-editor/src/app/records/records-list.component.html @@ -1,6 +1,15 @@
| string +export type FilterAggregationParams = FieldFilters | string export interface FiltersAggregationParams { type: 'filters' filters: Record diff --git a/libs/feature/search/src/lib/state/search.facade.ts b/libs/feature/search/src/lib/state/search.facade.ts index 4154dd780a..9300c61985 100644 --- a/libs/feature/search/src/lib/state/search.facade.ts +++ b/libs/feature/search/src/lib/state/search.facade.ts @@ -27,6 +27,7 @@ import { currentPage, getError, getFavoritesOnly, + getPageSize, getSearchConfigAggregations, getSearchFilters, getSearchResults, @@ -35,7 +36,6 @@ import { getSearchResultsLayout, getSearchResultsLoading, getSearchSortBy, - getPageSize, getSpatialFilterEnabled, isEndOfResults, totalPages, 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 4f82adf377..d2b8d9b669 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 @@ -77,6 +77,7 @@ describe('FieldsService', () => { 'isSpatial', 'q', 'license', + 'owner', ]) }) }) @@ -156,6 +157,7 @@ describe('FieldsService', () => { representationType: [], resourceType: [], topic: [], + owner: [], }) }) }) 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 a6b3db1776..ed3f94b72b 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -7,6 +7,7 @@ import { IsSpatialSearchField, LicenseSearchField, OrganizationSearchField, + OwnerSearchField, SimpleSearchField, } from './fields' import { forkJoin, Observable, of } from 'rxjs' @@ -63,6 +64,7 @@ export class FieldsService { isSpatial: new IsSpatialSearchField(this.injector), q: new FullTextSearchField(), license: new LicenseSearchField(this.injector), + owner: new OwnerSearchField(this.injector), } as Record get supportedFields() { diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 415c5fcc53..272fb73362 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -313,3 +313,12 @@ export class OrganizationSearchField implements AbstractSearchField { ) } } +export class OwnerSearchField extends SimpleSearchField { + constructor(injector: Injector) { + super('owner', 'asc', injector) + } + + getAvailableValues(): Observable { + return of([]) + } +} diff --git a/libs/feature/search/src/lib/utils/service/search.service.ts b/libs/feature/search/src/lib/utils/service/search.service.ts index 92576e73ba..f168cb8894 100644 --- a/libs/feature/search/src/lib/utils/service/search.service.ts +++ b/libs/feature/search/src/lib/utils/service/search.service.ts @@ -40,4 +40,8 @@ export class SearchService implements SearchServiceI { setPage(page: number): void { this.facade.paginate(page) } + + resetSearch(): void { + this.facade.resetSearch() + } } diff --git a/libs/util/app-config/src/lib/app-config.ts b/libs/util/app-config/src/lib/app-config.ts index f84f694b80..d5a7b49a3c 100644 --- a/libs/util/app-config/src/lib/app-config.ts +++ b/libs/util/app-config/src/lib/app-config.ts @@ -83,6 +83,7 @@ export function loadAppConfig() { 'global', ['geonetwork4_api_url'], [ + 'datahub_url', 'proxy_path', 'metadata_language', 'login_url', @@ -103,6 +104,7 @@ export function loadAppConfig() { ? null : ({ GN4_API_URL: parsedGlobalSection.geonetwork4_api_url, + DATAHUB_URL: parsedGlobalSection.datahub_url, PROXY_PATH: parsedGlobalSection.proxy_path, METADATA_LANGUAGE: parsedGlobalSection.metadata_language ? ( diff --git a/libs/util/app-config/src/lib/model.ts b/libs/util/app-config/src/lib/model.ts index a259d16b99..5189e3984e 100644 --- a/libs/util/app-config/src/lib/model.ts +++ b/libs/util/app-config/src/lib/model.ts @@ -2,6 +2,7 @@ import { Geometry } from 'geojson' export interface GlobalConfig { GN4_API_URL: string + DATAHUB_URL?: string PROXY_PATH?: string METADATA_LANGUAGE?: string LOGIN_URL?: string diff --git a/translations/de.json b/translations/de.json index 4423fc29a2..47ff0a24d7 100644 --- a/translations/de.json +++ b/translations/de.json @@ -211,7 +211,6 @@ "search.error.receivedError": "Ein Fehler wurde empfangen", "search.error.recordNotFound": "Der Datensatz mit der Kennung \"{ id }\" konnte nicht gefunden werden.", "search.field.any.placeholder": "Suche nach Datensätzen, Diensten und Karten ...", - "search.field.location.placeholder": "", "search.field.sortBy": "Sortieren nach:", "search.filters.clear": "Zurücksetzen", "search.filters.format": "Formate", @@ -231,6 +230,9 @@ "search.filters.license.unknown": "Unbekannt oder nicht vorhanden", "search.filters.maximize": "Erweitern", "search.filters.minimize": "Minimieren", + "search.filters.myRecords": "", + "search.filters.myRecordsHelp": "", + "search.filters.otherRecords": "", "search.filters.publisher": "Organisationen", "search.filters.representationType": "", "search.filters.resourceType": "", diff --git a/translations/en.json b/translations/en.json index 8023cfe426..dfa5e66083 100644 --- a/translations/en.json +++ b/translations/en.json @@ -211,7 +211,6 @@ "search.error.receivedError": "An error was received", "search.error.recordNotFound": "The record with identifier \"{ id }\" could not be found.", "search.field.any.placeholder": "Search datasets, services and maps ...", - "search.field.location.placeholder": "", "search.field.sortBy": "Sort by:", "search.filters.clear": "Reset", "search.filters.format": "Formats", @@ -231,6 +230,9 @@ "search.filters.license.unknown": "Unknown or absent", "search.filters.maximize": "Expand", "search.filters.minimize": "Minimize", + "search.filters.myRecords": "Show only my records", + "search.filters.myRecordsHelp": "When this is enabled, records only created by myself are shown; records created by others will not show up.", + "search.filters.otherRecords": "Showing records from another person", "search.filters.publisher": "Organizations", "search.filters.representationType": "Representation type", "search.filters.resourceType": "Resource type", diff --git a/translations/es.json b/translations/es.json index 301fafb704..59f4184c51 100644 --- a/translations/es.json +++ b/translations/es.json @@ -211,7 +211,6 @@ "search.error.receivedError": "", "search.error.recordNotFound": "", "search.field.any.placeholder": "", - "search.field.location.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", "search.filters.format": "", @@ -231,6 +230,9 @@ "search.filters.license.unknown": "", "search.filters.maximize": "", "search.filters.minimize": "", + "search.filters.myRecords": "", + "search.filters.myRecordsHelp": "", + "search.filters.otherRecords": "", "search.filters.publisher": "", "search.filters.representationType": "", "search.filters.resourceType": "", diff --git a/translations/fr.json b/translations/fr.json index 1a54e26722..233760220a 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -211,7 +211,6 @@ "search.error.receivedError": "Erreur retournée", "search.error.recordNotFound": "Cette donnée n'a pu être trouvée.", "search.field.any.placeholder": "Rechercher une fiche de métadonnée...", - "search.field.location.placeholder": "", "search.field.sortBy": "Trier par :", "search.filters.clear": "Réinitialiser", "search.filters.format": "Formats", @@ -231,6 +230,9 @@ "search.filters.license.unknown": "Non-reconnue ou absente", "search.filters.maximize": "Agrandir", "search.filters.minimize": "Réduire", + "search.filters.myRecords": "Voir mes données", + "search.filters.myRecordsHelp": "Quand activé, n'affiche que les données créées avec mon utilisateur. Les données créées par les autres utilisateurs ne sont pas affichées.", + "search.filters.otherRecords": "Affichage des données d'un autre utilisateur", "search.filters.publisher": "Organisations", "search.filters.representationType": "Type de représentation", "search.filters.resourceType": "Type de ressource", diff --git a/translations/it.json b/translations/it.json index d03c406127..66918a0760 100644 --- a/translations/it.json +++ b/translations/it.json @@ -211,7 +211,6 @@ "search.error.receivedError": "", "search.error.recordNotFound": "", "search.field.any.placeholder": "", - "search.field.location.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", "search.filters.format": "", @@ -231,6 +230,9 @@ "search.filters.license.unknown": "", "search.filters.maximize": "", "search.filters.minimize": "", + "search.filters.myRecords": "", + "search.filters.myRecordsHelp": "", + "search.filters.otherRecords": "", "search.filters.publisher": "", "search.filters.representationType": "", "search.filters.resourceType": "", diff --git a/translations/nl.json b/translations/nl.json index e13cb73ef9..5dbf956249 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -211,7 +211,6 @@ "search.error.receivedError": "", "search.error.recordNotFound": "", "search.field.any.placeholder": "", - "search.field.location.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", "search.filters.format": "", @@ -231,6 +230,9 @@ "search.filters.license.unknown": "", "search.filters.maximize": "", "search.filters.minimize": "", + "search.filters.myRecords": "", + "search.filters.myRecordsHelp": "", + "search.filters.otherRecords": "", "search.filters.publisher": "", "search.filters.representationType": "", "search.filters.resourceType": "", diff --git a/translations/pt.json b/translations/pt.json index 8b3f2168a0..8ffa5c5143 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -211,7 +211,6 @@ "search.error.receivedError": "", "search.error.recordNotFound": "", "search.field.any.placeholder": "", - "search.field.location.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", "search.filters.format": "", @@ -231,6 +230,9 @@ "search.filters.license.unknown": "", "search.filters.maximize": "", "search.filters.minimize": "", + "search.filters.myRecords": "", + "search.filters.myRecordsHelp": "", + "search.filters.otherRecords": "", "search.filters.publisher": "", "search.filters.representationType": "", "search.filters.resourceType": "", From 5b321c1eb51b9d52cd7c731dc237c6b15b6c3e88 Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Thu, 5 Oct 2023 14:14:29 +0200 Subject: [PATCH 2/5] feat(es): use simpler logic for generating a query string from filters feat(es): move simple filters to the filter array to save performance see https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-bool-query.html#query-dsl-bool-query "The clause (query) must appear in matching documents. However unlike must the score of the query will be ignored. Filter clauses are executed in filter context, meaning that scoring is ignored and clauses are considered for caching." --- .../elasticsearch.service.spec.ts | 195 +++++++++++++----- .../elasticsearch/elasticsearch.service.ts | 66 ++++-- ...rganizations-from-metadata.service.spec.ts | 4 +- 3 files changed, 198 insertions(+), 67 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts index 4dbe744ef7..1851409542 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts @@ -1,6 +1,6 @@ import { ElasticsearchService } from './elasticsearch.service' import { ES_FIXTURE_AGGS_RESPONSE } from '@geonetwork-ui/common/fixtures' -import { EsSearchParams } from '../types/elasticsearch.model' +import { EsSearchParams } from '@geonetwork-ui/api/metadata-converter' describe('ElasticsearchService', () => { let service: ElasticsearchService @@ -96,26 +96,31 @@ describe('ElasticsearchService', () => { }) }) describe('#buildPayloadQuery', () => { - it('add any and other fields query_strings', () => { + it('should not add fields query_strings if fieldsSearchFilters Object is empty', () => { const query = service['buildPayloadQuery']( { - Org: { - world: true, - }, any: 'hello', }, - {} + {}, + ['record-1', 'record-2', 'record-3'] ) + expect(query).toEqual({ bool: { - filter: [], - should: [], - must: [ + filter: [ { terms: { isTemplate: ['n'], }, }, + { + ids: { + values: ['record-1', 'record-2', 'record-3'], + }, + }, + ], + should: [], + must: [ { query_string: { default_operator: 'AND', @@ -130,11 +135,6 @@ describe('ElasticsearchService', () => { query: 'hello', }, }, - { - query_string: { - query: '(Org:"world")', - }, - }, ], must_not: { terms: { @@ -157,14 +157,25 @@ describe('ElasticsearchService', () => { ) expect(query).toEqual({ bool: { - filter: [], - should: [], - must: [ + filter: [ { terms: { isTemplate: ['n'], }, }, + { + query_string: { + query: 'Org:("world")', + }, + }, + { + ids: { + values: ['record-1', 'record-2', 'record-3'], + }, + }, + ], + should: [], + must: [ { query_string: { default_operator: 'AND', @@ -179,16 +190,6 @@ describe('ElasticsearchService', () => { query: 'hello', }, }, - { - query_string: { - query: '(Org:"world")', - }, - }, - { - ids: { - values: ['record-1', 'record-2', 'record-3'], - }, - }, ], must_not: { terms: { @@ -203,6 +204,10 @@ describe('ElasticsearchService', () => { { Org: { world: true, + world2: true, + }, + name: { + john: true, }, any: 'hello', }, @@ -211,14 +216,25 @@ describe('ElasticsearchService', () => { ) expect(query).toEqual({ bool: { - filter: [], - should: [], - must: [ + filter: [ { terms: { isTemplate: ['n'], }, }, + { + query_string: { + query: 'Org:("world" OR "world2") AND name:("john")', + }, + }, + { + ids: { + values: [], + }, + }, + ], + should: [], + must: [ { query_string: { default_operator: 'AND', @@ -233,9 +249,38 @@ describe('ElasticsearchService', () => { query: 'hello', }, }, + ], + must_not: { + terms: { + resourceType: ['service', 'map', 'map/static', 'mapDigital'], + }, + }, + }, + }) + }) + it('handle negative and empty filters', () => { + const query = service['buildPayloadQuery']( + { + Org: { + world: false, + }, + name: {}, + message: '', + }, + {}, + [] + ) + expect(query).toEqual({ + bool: { + filter: [ + { + terms: { + isTemplate: ['n'], + }, + }, { query_string: { - query: '(Org:"world")', + query: 'Org:(-"world")', }, }, { @@ -244,6 +289,61 @@ describe('ElasticsearchService', () => { }, }, ], + should: [], + must: [], + must_not: { + terms: { + resourceType: ['service', 'map', 'map/static', 'mapDigital'], + }, + }, + }, + }) + }) + it('handle filters expressed as queries', () => { + const query = service['buildPayloadQuery']( + { + Org: 'world AND world2', + any: 'hello', + }, + {}, + [] + ) + expect(query).toEqual({ + bool: { + filter: [ + { + terms: { + isTemplate: ['n'], + }, + }, + { + query_string: { + query: 'Org:(world AND world2)', + }, + }, + { + ids: { + values: [], + }, + }, + ], + should: [], + must: [ + { + query_string: { + default_operator: 'AND', + fields: [ + 'resourceTitleObject.langfre^5', + 'tag.langfre^4', + 'resourceAbstractObject.langfre^3', + 'lineageObject.langfre^2', + 'any.langfre', + 'uuid', + ], + query: 'hello', + }, + }, + ], must_not: { terms: { resourceType: ['service', 'map', 'map/static', 'mapDigital'], @@ -264,7 +364,7 @@ describe('ElasticsearchService', () => { ) }) it('escapes special char', () => { - expect(query.bool.must[1].query_string.query).toEqual( + expect(query.bool.must[0].query_string.query).toEqual( `scot \\(\\)\\{\\?\\[ \\/ test` ) }) @@ -287,9 +387,7 @@ describe('ElasticsearchService', () => { it('adds boosting of 7 for intersecting with it and boosting of 10 on geoms within', () => { const query = service['buildPayloadQuery']( { - Org: { - world: true, - }, + Org: 'world', any: 'hello', }, {}, @@ -298,13 +396,19 @@ describe('ElasticsearchService', () => { ) expect(query).toEqual({ bool: { - filter: [], - must: [ + filter: [ { terms: { isTemplate: ['n'], }, }, + { + query_string: { + query: 'Org:(world)', + }, + }, + ], + must: [ { query_string: { default_operator: 'AND', @@ -319,11 +423,6 @@ describe('ElasticsearchService', () => { query: 'hello', }, }, - { - query_string: { - query: '(Org:"world")', - }, - }, ], must_not: { terms: { @@ -611,6 +710,7 @@ describe('ElasticsearchService', () => { filters: { filter1: { field1: '100' }, filter2: { field2: { value1: true, value3: true } }, + filter3: 'my own query', }, }, myHistogram: { @@ -623,14 +723,13 @@ describe('ElasticsearchService', () => { myFilters: { filters: { filter1: { - match: { - field1: '100', - }, + query_string: { query: 'field1:(100)' }, }, filter2: { - match: { - field2: { value1: true, value3: true }, - }, + query_string: { query: 'field2:("value1" OR "value3")' }, + }, + filter3: { + query_string: { query: 'my own query' }, }, }, }, diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts index 8a9b92af1d..4da929819e 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -5,6 +5,8 @@ import { Aggregation, AggregationParams, AggregationsParams, + FieldFilter, + FieldFilters, FilterAggregationParams, SortByField, } from '@geonetwork-ui/common/domain/search' @@ -177,17 +179,36 @@ export class ElasticsearchService { }) } + private filtersToQueryString(filters: FieldFilters): string { + const makeQuery = (filter: FieldFilter): string => { + if (typeof filter === 'string') { + return filter + } + return Object.keys(filter) + .map((key) => { + if (filter[key] === true) { + return `"${key}"` + } + return `-"${key}"` + }) + .join(' OR ') + } + return Object.keys(filters) + .filter( + (fieldname) => + filters[fieldname] && JSON.stringify(filters[fieldname]) !== '{}' + ) + .map((fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`) + .join(' AND ') + } + private buildPayloadQuery( { any, ...fieldSearchFilters }: SearchFilters, configFilters: SearchFilters, uuids?: string[], geometry?: Geometry ) { - const queryFilters = this.stateFiltersToQueryString(fieldSearchFilters) - const must = [this.queryFilterOnValues('isTemplate', 'n')] as Record< - string, - unknown - >[] + const must = [] as Record[] const must_not = { ...this.queryFilterOnValues('resourceType', [ 'service', @@ -197,6 +218,10 @@ export class ElasticsearchService { ]), } const should = [] as Record[] + const filter = [this.queryFilterOnValues('isTemplate', 'n')] as Record< + string, + unknown + >[] if (any) { must.push({ @@ -210,15 +235,16 @@ export class ElasticsearchService { }, }) } + const queryFilters = this.filtersToQueryString(fieldSearchFilters) if (queryFilters) { - must.push({ + filter.push({ query_string: { query: queryFilters, }, }) } if (uuids) { - must.push({ + filter.push({ ids: { values: uuids, }, @@ -252,7 +278,7 @@ export class ElasticsearchService { must, must_not, should, - filter: [], + filter, }, } } @@ -348,6 +374,7 @@ export class ElasticsearchService { * } * } */ + // FIXME: this is not used anymore stateFiltersToQueryString(facetsState) { const query = [] for (const indexKey in facetsState) { @@ -365,6 +392,7 @@ export class ElasticsearchService { return this.combineQueryGroups(query) } + // FIXME: this is not used anymore private parseStateNode(nodeName, node, indexKey) { let queryString = '' if (node && typeof node === 'object') { @@ -416,20 +444,24 @@ export class ElasticsearchService { } buildAggregationsPayload(aggregations: AggregationsParams): any { - const mapFilterAggregation = (filterAgg: FilterAggregationParams) => ({ - match: filterAgg, - }) const mapToESAggregation = (aggregation: AggregationParams) => { switch (aggregation.type) { case 'filters': return { - filters: Object.keys(aggregation.filters).reduce( - (prev, curr) => ({ + filters: Object.keys(aggregation.filters).reduce((prev, curr) => { + const filter = aggregation.filters[curr] + return { ...prev, - [curr]: mapFilterAggregation(aggregation.filters[curr]), - }), - {} - ), + [curr]: { + query_string: { + query: + typeof filter === 'string' + ? filter + : this.filtersToQueryString(filter), + }, + }, + } + }, {}), } case 'terms': return { diff --git a/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts b/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts index 09fd07fcb9..879763bec1 100644 --- a/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts @@ -270,7 +270,7 @@ describe.each(['4.2.2-00', '4.2.3-xx', '4.2.5-xx'])( size: 0, query: { bool: { - must: [{ terms: { isTemplate: ['n'] } }], + must: [], must_not: { terms: { resourceType: [ @@ -282,7 +282,7 @@ describe.each(['4.2.2-00', '4.2.3-xx', '4.2.5-xx'])( }, }, should: [], - filter: [], + filter: [{ terms: { isTemplate: ['n'] } }], }, }, _source: [], From d59cc33211118a2bcd399a1707121870599560c4 Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Tue, 10 Oct 2023 09:55:38 +0200 Subject: [PATCH 3/5] feat: add metadata-editor to CI --- .github/workflows/artifacts.yml | 3 ++- .github/workflows/cleanup.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index d3dd650b69..d2ed5219e3 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -7,6 +7,7 @@ on: push: branches: - main + - develop release: types: [published] issue_comment: @@ -20,7 +21,7 @@ concurrency: env: NODE_VERSION: 18.16.1 # a list of apps to build and publish on releases - APP_NAMES: datafeeder,datahub + APP_NAMES: datafeeder,datahub,metadata-editor jobs: checks: diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 0d9c50c9b3..c11079c823 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -3,7 +3,7 @@ run-name: 🧹 Cleanup operations for 🌱 ${{github.event.ref}} env: # a list of apps to build and publish on releases - APP_NAMES: datafeeder,datahub + APP_NAMES: datafeeder,datahub,metadata-editor on: delete: From 5926a9cd7359e964f6df18a7a9064c77329285b2 Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Tue, 10 Oct 2023 14:30:58 +0200 Subject: [PATCH 4/5] feat: adds docker-build target --- apps/metadata-editor/project.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/metadata-editor/project.json b/apps/metadata-editor/project.json index e341d7c513..e93f92e72b 100644 --- a/apps/metadata-editor/project.json +++ b/apps/metadata-editor/project.json @@ -103,6 +103,16 @@ "jestConfig": "apps/metadata-editor/jest.config.ts", "passWithNoTests": true } + }, + "docker-build": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "nx build metadata-editor --base-href='/metadata-editor/'", + "docker build --build-arg APP_NAME=metadata-editor -f ./tools/docker/Dockerfile.apps . -t $(tools/print-docker-tag.sh metadata-editor)" + ], + "parallel": false + } } }, "tags": ["type:app"] From 1d7afe43a64f2b47a5f36dc205f4d7cc1e1ca47f Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 13 Oct 2023 15:39:35 +0200 Subject: [PATCH 5/5] feat(me/dh): overview of my org records --- apps/metadata-editor-e2e/src/e2e/my-org.cy.ts | 44 ++++ apps/metadata-editor/src/app/app.routes.ts | 5 + .../my-org-users/my-org-users.component.css | 7 + .../my-org-users/my-org-users.component.html | 6 + .../my-org-users.component.spec.ts | 131 +++++++++++ .../my-org-users/my-org-users.component.ts | 48 ++++ .../my-org-records.component.html | 8 +- .../my-org-records.component.spec.ts | 208 +++++++++++------- .../my-org-records.component.ts | 50 +++-- .../app/records/records-list.component.html | 55 ++++- .../src/app/records/records-list.component.ts | 12 + conf/default.toml | 2 +- docs/apps/editor.md | 13 +- .../src/lib/gn4/auth/auth.service.spec.ts | 2 + .../src/lib/gn4/auth/auth.service.ts | 5 + .../fixtures/src/lib/organisations.fixture.ts | 6 + libs/common/fixtures/src/lib/user.fixtures.ts | 31 +++ libs/feature/catalog/src/index.ts | 1 + .../src/lib/my-org/my-org.service.spec.ts | 113 ++++++++++ .../catalog/src/lib/my-org/my-org.service.ts | 61 +++++ .../organisations.component.spec.ts | 2 +- .../record-table/record-table.component.html | 38 +++- .../record-table/record-table.component.ts | 2 +- translations/de.json | 7 + translations/en.json | 7 + translations/es.json | 7 + translations/fr.json | 7 + translations/it.json | 7 + translations/nl.json | 7 + translations/pt.json | 7 + 30 files changed, 797 insertions(+), 102 deletions(-) create mode 100644 apps/metadata-editor-e2e/src/e2e/my-org.cy.ts create mode 100644 apps/metadata-editor/src/app/my-org-users/my-org-users.component.css create mode 100644 apps/metadata-editor/src/app/my-org-users/my-org-users.component.html create mode 100644 apps/metadata-editor/src/app/my-org-users/my-org-users.component.spec.ts create mode 100644 apps/metadata-editor/src/app/my-org-users/my-org-users.component.ts create mode 100644 libs/feature/catalog/src/lib/my-org/my-org.service.spec.ts create mode 100644 libs/feature/catalog/src/lib/my-org/my-org.service.ts 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": "",