From f21a87dcb60f4e9a86f96873bf7fbc7a8ac08441 Mon Sep 17 00:00:00 2001 From: cmoinier Date: Wed, 18 Sep 2024 11:32:55 +0200 Subject: [PATCH 01/49] disable non implemented btns --- .../dashboard-menu.component.html | 25 +++++++++------- .../dashboard-menu.component.ts | 1 + .../search-header.component.html | 20 +++++++++---- .../search-header/search-header.component.ts | 3 ++ .../top-toolbar/top-toolbar.component.html | 30 +++++++++++++++---- apps/metadata-editor/src/styles.css | 3 ++ .../import-record.component.html | 4 +++ .../import-record/import-record.component.ts | 5 +++- 8 files changed, 69 insertions(+), 22 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html index b57c19c048..7a2a11d2ca 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html @@ -11,37 +11,41 @@ dashboard.catalog.allRecords chat_bubble dashboard.catalog.discussion calendar_today dashboard.catalog.calendar person dashboard.catalog.contacts label dashboard.catalog.thesaurus @@ -74,10 +78,11 @@ > lightbulb dashboard.records.templates diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts index e2bb95762b..e602f6d713 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts @@ -27,6 +27,7 @@ export class DashboardMenuComponent { switchMap(() => this.recordsRepository.getAllDrafts()), map((drafts) => drafts.length) ) + activeLink = false constructor(private recordsRepository: RecordsRepositoryInterface) {} } diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html index 9b0edbaf48..64cc3f9525 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html @@ -3,18 +3,26 @@
- - - - + side_navigation - + lightbulb - + download @@ -53,10 +65,18 @@ editor.record.saveStatus.draftWithChangesPending
- + help - + verified diff --git a/apps/metadata-editor/src/styles.css b/apps/metadata-editor/src/styles.css index 6352aa4946..9c860e0720 100644 --- a/apps/metadata-editor/src/styles.css +++ b/apps/metadata-editor/src/styles.css @@ -18,6 +18,9 @@ body { .btn-active { @apply bg-neutral-200 text-blue-600 font-bold; } +.btn-inactive { + @apply text-slate-400 cursor-default; +} .menu-title { @apply text-2xl px-9 py-3; } diff --git a/libs/feature/editor/src/lib/components/import-record/import-record.component.html b/libs/feature/editor/src/lib/components/import-record/import-record.component.html index 5b9492a767..129ccc1209 100644 --- a/libs/feature/editor/src/lib/components/import-record/import-record.component.html +++ b/libs/feature/editor/src/lib/components/import-record/import-record.component.html @@ -11,6 +11,10 @@ type="light" extraClass="flex flex-row items-center gap-2 w-full justify-start" (buttonClick)="menuItem.action()" + [disabled]="menuItem.disabled" + [title]=" + (menuItem.disabled ? 'editor.temporary.disabled' : '') | translate + " > {{ menuItem.icon }} {{ menuItem.label }} any dataTest: string + disabled?: boolean } type ImportMenuPage = 'mainMenu' | 'importExternalFile' @@ -35,6 +36,7 @@ type ImportMenuPage = 'mainMenu' | 'importExternalFile' ButtonComponent, ThumbnailComponent, UrlInputComponent, + TranslateModule, ], }) export class ImportRecordComponent { @@ -46,6 +48,7 @@ export class ImportRecordComponent { icon: 'highlight', action: () => null, dataTest: 'useAModelButton', + disabled: true, }, { label: this.translateService.instant( From c64f943be927c462b6bc1ed45e47b11e8c9c5096 Mon Sep 17 00:00:00 2001 From: cmoinier Date: Wed, 18 Sep 2024 11:33:03 +0200 Subject: [PATCH 02/49] add translation --- translations/de.json | 1 + translations/en.json | 1 + translations/es.json | 1 + translations/fr.json | 1 + translations/it.json | 1 + translations/nl.json | 1 + translations/pt.json | 1 + translations/sk.json | 1 + 8 files changed, 8 insertions(+) diff --git a/translations/de.json b/translations/de.json index 348cebb372..24b74e605d 100644 --- a/translations/de.json +++ b/translations/de.json @@ -286,6 +286,7 @@ "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "Dieser Datensatz ist auf dem neuesten Stand", "editor.sidebar.menu.editor": "", + "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "Datensatz aus dem Datahub", "facets.block.title.OrgForResource": "Organisation", "facets.block.title.availableInServices": "Verfügbar für", diff --git a/translations/en.json b/translations/en.json index 9659f2a0bd..ce7750795b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -286,6 +286,7 @@ "editor.record.undo.tooltip.enabled": "Clicking this button will cancel the pending changes on this record.", "editor.record.upToDate": "This record is up to date", "editor.sidebar.menu.editor": "Editor", + "editor.temporary.disabled": "Not implemented yet", "externalviewer.dataset.unnamed": "Datahub layer", "facets.block.title.OrgForResource": "Organisation", "facets.block.title.availableInServices": "Available for", diff --git a/translations/es.json b/translations/es.json index 1c68c344c1..e708c32ec0 100644 --- a/translations/es.json +++ b/translations/es.json @@ -286,6 +286,7 @@ "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", "editor.sidebar.menu.editor": "", + "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "", "facets.block.title.OrgForResource": "", "facets.block.title.availableInServices": "", diff --git a/translations/fr.json b/translations/fr.json index 94f8e4e9a2..42a4944787 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -286,6 +286,7 @@ "editor.record.undo.tooltip.enabled": "Cliquer sur ce bouton pour annuler les modifications apportées à cette fiche", "editor.record.upToDate": "", "editor.sidebar.menu.editor": "", + "editor.temporary.disabled": "Pas encore implémenté", "externalviewer.dataset.unnamed": "Couche du datahub", "facets.block.title.OrgForResource": "Organisation", "facets.block.title.availableInServices": "Disponible pour", diff --git a/translations/it.json b/translations/it.json index 0f3083db62..1f87f83ac5 100644 --- a/translations/it.json +++ b/translations/it.json @@ -286,6 +286,7 @@ "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", "editor.sidebar.menu.editor": "", + "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "Layer del datahub", "facets.block.title.OrgForResource": "Organizzazione", "facets.block.title.availableInServices": "Disponibile per", diff --git a/translations/nl.json b/translations/nl.json index cb16537ecc..c5895c20c4 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -286,6 +286,7 @@ "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", "editor.sidebar.menu.editor": "", + "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "", "facets.block.title.OrgForResource": "", "facets.block.title.availableInServices": "", diff --git a/translations/pt.json b/translations/pt.json index 58095f6e35..64c5b7975d 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -286,6 +286,7 @@ "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", "editor.sidebar.menu.editor": "", + "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "", "facets.block.title.OrgForResource": "", "facets.block.title.availableInServices": "", diff --git a/translations/sk.json b/translations/sk.json index a1efbf9e37..3911e28e0e 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -286,6 +286,7 @@ "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", "editor.sidebar.menu.editor": "", + "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "", "facets.block.title.OrgForResource": "Organizácia", "facets.block.title.availableInServices": "Dostupné pre", From 0512152bd2e302167681a188d70774e5420552bb Mon Sep 17 00:00:00 2001 From: cmoinier Date: Wed, 18 Sep 2024 13:47:48 +0200 Subject: [PATCH 03/49] fix module import in UT --- .../dashboard/search-header/search-header.component.spec.ts | 5 +++-- .../components/import-record/import-record.component.spec.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts index 8a6d5f133b..858e05b7be 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts @@ -31,7 +31,7 @@ describe('SearchHeaderComponent', () => { SearchHeaderComponent, EffectsModule.forRoot(), StoreModule.forRoot({}), - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), + TranslateModule.forRoot(), ], providers: [ { @@ -47,7 +47,7 @@ describe('SearchHeaderComponent', () => { .overrideComponent(SearchHeaderComponent, { set: { changeDetection: ChangeDetectionStrategy.Default, - imports: [], + imports: [TranslateModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], }, }) @@ -55,6 +55,7 @@ describe('SearchHeaderComponent', () => { fixture = TestBed.createComponent(SearchHeaderComponent) component = fixture.componentInstance + component.activeBtn = true fixture.detectChanges() }) diff --git a/libs/feature/editor/src/lib/components/import-record/import-record.component.spec.ts b/libs/feature/editor/src/lib/components/import-record/import-record.component.spec.ts index 1a85b10858..09430bde77 100644 --- a/libs/feature/editor/src/lib/components/import-record/import-record.component.spec.ts +++ b/libs/feature/editor/src/lib/components/import-record/import-record.component.spec.ts @@ -39,6 +39,7 @@ describe('ImportRecordComponent', () => { .overrideComponent(ImportRecordComponent, { set: { changeDetection: ChangeDetectionStrategy.Default, + imports: [TranslateModule], }, }) .compileComponents() From 0498c36c9f6b0bfdd4516ed27c7dfe250a26fa2e Mon Sep 17 00:00:00 2001 From: f-necas <39771412+f-necas@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:00:15 +0200 Subject: [PATCH 04/49] fix: element ref doesn't work on ng-for wizard.component.html --- .../src/lib/components/wizard/wizard.component.html | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/libs/feature/editor/src/lib/components/wizard/wizard.component.html b/libs/feature/editor/src/lib/components/wizard/wizard.component.html index 1d9f8d3ac1..213879a053 100644 --- a/libs/feature/editor/src/lib/components/wizard/wizard.component.html +++ b/libs/feature/editor/src/lib/components/wizard/wizard.component.html @@ -1,9 +1,5 @@ -
-
+
+
From 9736e3d144d949fb858688f082c66d5cf57d9d75 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Mon, 26 Aug 2024 14:36:45 +0200 Subject: [PATCH 05/49] feat(editor): Added field contacts for metadata --- .../editor/src/lib/+state/editor.reducer.ts | 2 +- .../form-field-contacts.component.css | 0 .../form-field-contacts.component.html | 71 ++++++ .../form-field-contacts.component.spec.ts | 224 ++++++++++++++++++ .../form-field-contacts.component.ts | 217 +++++++++++++++++ .../form-field/form-field.component.html | 5 + libs/feature/editor/src/lib/fields.config.ts | 9 +- 7 files changed, 526 insertions(+), 2 deletions(-) create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.css create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.ts b/libs/feature/editor/src/lib/+state/editor.reducer.ts index d2b4e96a5c..dfb4835ae5 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.ts @@ -38,7 +38,7 @@ export const initialEditorState: EditorState = { saveError: null, changedSinceSave: false, editorConfig: DEFAULT_CONFIGURATION, - currentPage: 0, + currentPage: 2, //todo: remove before merge } const reducer = createReducer( diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html new file mode 100644 index 0000000000..86db0afdf0 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html @@ -0,0 +1,71 @@ +
+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ editor.record.form.field.contacts.noContact +
+
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts new file mode 100644 index 0000000000..0f21913bb4 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts @@ -0,0 +1,224 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { FormFieldContactsComponent } from './form-field-contacts.component' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { BehaviorSubject } from 'rxjs' +import { + Individual, + Organization, + Role, +} from '@geonetwork-ui/common/domain/model/record' +import { ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core' +import { UserModel } from '@geonetwork-ui/common/domain/model/user' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' +import { ContactCardComponent } from '../../../contact-card/contact-card.component' +import { + AutocompleteComponent, + DropdownSelectorComponent, + UiInputsModule, +} from '@geonetwork-ui/ui/inputs' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { FormControl } from '@angular/forms' + +const organizationBarbie: Organization = { + name: 'Barbie Inc.', +} + +const organizationGoogle: Organization = { + name: 'Google', +} + +class MockPlatformServiceInterface { + getUsers = jest.fn(() => new BehaviorSubject([])) +} + +class MockOrganizationsServiceInterface { + organisations$ = new BehaviorSubject([organizationBarbie, organizationGoogle]) +} + +describe('FormFieldContactsForResourceComponent', () => { + let component: FormFieldContactsComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + FormFieldContactsComponent, + CommonModule, + TranslateModule.forRoot(), + UiInputsModule, + ContactCardComponent, + DropdownSelectorComponent, + ], + providers: [ + { + provide: PlatformServiceInterface, + useClass: MockPlatformServiceInterface, + }, + { + provide: OrganizationsServiceInterface, + useClass: MockOrganizationsServiceInterface, + }, + ChangeDetectorRef, + ], + }) + .overrideComponent(AutocompleteComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents() + + fixture = TestBed.createComponent(FormFieldContactsComponent) + component = fixture.componentInstance + component.control = new FormControl([]) + fixture.detectChanges() + }) + + it('should create the component', () => { + expect(component).toBeTruthy() + }) + + describe('ngOnInit', () => { + it('should initialize organizations', async () => { + await component.ngOnInit() + + expect(component.allOrganizations.size).toBe(2) + }) + }) + + describe('addRoleToDisplay', () => { + it('should add role to display and filter roles to pick', () => { + const initialRolesToPick = [...component.rolesToPick] + const roleToAdd = initialRolesToPick[0] + + component.addRoleToDisplay(roleToAdd) + + expect(component.roleSectionsToDisplay).toContain(roleToAdd) + expect(component.rolesToPick).not.toContain(roleToAdd) + }) + }) + + describe('filterRolesToPick', () => { + it('should filter roles already in roleSectionsToDisplay', () => { + component.rolesToPick = ['custodian', 'owner'] as Role[] + component.roleSectionsToDisplay = ['custodian'] as Role[] + + component.filterRolesToPick() + + expect(component.rolesToPick).toEqual(['owner']) + }) + }) + + describe('updateContactsForRessource', () => { + it('should update contactsForRessourceByRole and contactsAsDynElemByRole', () => { + const mockContact: Individual = { + role: 'owner', + organization: { name: 'Org1' } as Organization, + } as Individual + + component.allOrganizations.set('Org1', { name: 'Org1' } as Organization) + component.control.setValue([mockContact]) + + component.updateContacts() + + expect(component.contactsForRessourceByRole.get('owner')).toEqual([ + mockContact, + ]) + expect(component.contactsAsDynElemByRole.get('owner').length).toBe(1) + }) + }) + + describe('manageRoleSectionsToDisplay', () => { + it('should add new roles to roleSectionsToDisplay', () => { + const mockContact: Individual = { + role: 'owner', + organization: { name: 'Org1' } as Organization, + } as Individual + + component.manageRoleSectionsToDisplay([mockContact]) + + expect(component.roleSectionsToDisplay).toContain('owner') + }) + }) + + describe('removeContact', () => { + it('should remove contact at specified index', () => { + const mockContacts: Individual[] = [ + { + role: 'owner', + organization: { name: 'Org1' } as Organization, + } as Individual, + { + role: 'custodian', + organization: { name: 'Org2' } as Organization, + } as Individual, + ] + + component.control.setValue(mockContacts) + component.removeContact(0) + + expect(component.control.value.length).toBe(1) + expect(component.control.value[0]).toEqual(mockContacts[1]) + }) + }) + + describe('handleContactsChanged', () => { + it('should update contacts based on reordered dynamic elements', () => { + const mockContacts: Individual[] = [ + { + role: 'owner', + organization: { name: 'Org1' } as Organization, + } as Individual, + { + role: 'owner', + organization: { name: 'Org2' } as Organization, + } as Individual, + ] + + component.contactsForRessourceByRole.set('owner', [mockContacts[0]]) + component.contactsForRessourceByRole.set('owner', [mockContacts[1]]) + + const reorderedElements = [ + { inputs: { contact: mockContacts[1] } } as any, + { inputs: { contact: mockContacts[0] } } as any, + ] + + component.handleContactsChanged(reorderedElements) + + const newControlValue = component.control.value + expect(newControlValue[0]).toEqual(mockContacts[1]) + expect(newControlValue[1]).toEqual(mockContacts[0]) + }) + }) + + describe('addContact', () => { + it('should add a new contact to the control value', () => { + const mockUser: UserModel = { + username: 'user1', + name: 'John', + surname: 'Doe', + organisation: 'Org1', + } as UserModel + + component.allOrganizations.set('Org1', { name: 'Org1' } as Organization) + const initialContacts = component.control.value.length + + component.addContact(mockUser, 'owner') + + expect(component.control.value.length).toBe(initialContacts + 1) + expect(component.control.value[initialContacts].role).toBe('owner') + expect(component.control.value[initialContacts].organization.name).toBe( + 'Org1' + ) + }) + }) + + describe('ngOnDestroy', () => { + it('should unsubscribe from all subscriptions', () => { + const subscriptionSpy = jest.spyOn(component.subscription, 'unsubscribe') + + component.ngOnDestroy() + + expect(subscriptionSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts new file mode 100644 index 0000000000..371379b611 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts @@ -0,0 +1,217 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, +} from '@angular/core' +import { FormControl } from '@angular/forms' +import { + AutocompleteComponent, + DropdownSelectorComponent, + UiInputsModule, +} from '@geonetwork-ui/ui/inputs' +import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { + Individual, + Organization, + Role, +} from '@geonetwork-ui/common/domain/model/record' +import { TranslateModule } from '@ngx-translate/core' +import { + debounceTime, + distinctUntilChanged, + firstValueFrom, + Observable, + Subscription, + switchMap, +} from 'rxjs' +import { UserModel } from '@geonetwork-ui/common/domain/model/user' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { ContactCardComponent } from '../../../contact-card/contact-card.component' +import { + DynamicElement, + SortableListComponent, +} from '@geonetwork-ui/ui/elements' +import { createFuzzyFilter } from '@geonetwork-ui/util/shared' +import { map } from 'rxjs/operators' + +@Component({ + selector: 'gn-ui-form-field-contacts', + templateUrl: './form-field-contacts.component.html', + styleUrls: ['./form-field-contacts.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + DropdownSelectorComponent, + UiInputsModule, + CommonModule, + UiWidgetsModule, + AutocompleteComponent, + TranslateModule, + ContactCardComponent, + SortableListComponent, + ], +}) +export class FormFieldContactsComponent + implements OnInit, OnDestroy, OnChanges +{ + @Input() control: FormControl + + contacts: Individual[] = [] + contactsAsDynElem: DynamicElement[] = [] + + subscription: Subscription = new Subscription() + + allUsers$: Observable + + rolesToPick: Role[] = ['point_of_contact'] + + allOrganizations: Map = new Map() + + constructor( + private platformServiceInterface: PlatformServiceInterface, + private organizationsServiceInterface: OrganizationsServiceInterface, + private changeDetectorRef: ChangeDetectorRef + ) { + this.allUsers$ = this.platformServiceInterface.getUsers() + } + + ngOnChanges(changes: SimpleChanges): void { + console.log(changes['control']) + } + + async ngOnInit(): Promise { + this.allOrganizations = new Map( + ( + await firstValueFrom(this.organizationsServiceInterface.organisations$) + ).map((organization) => [organization.name, organization]) + ) + + this.updateContacts() + + this.changeDetectorRef.markForCheck() + + this.subscription.add( + this.control.valueChanges.subscribe((contacts) => { + console.log('new contacts (valueChange): ', contacts) + this.updateContacts() + this.changeDetectorRef.markForCheck() + }) + ) + } + + updateContacts() { + this.contacts = this.control.value.reduce((acc, contact) => { + const completeOrganization = this.allOrganizations.get( + contact.organization.name + ) + + const updatedContact = { + ...contact, + organization: + completeOrganization ?? + ({ name: contact.organization.name } as Organization), + } + + acc.push(updatedContact) + + return acc + }, [] as Individual[]) + + this.contactsAsDynElem = this.control.value.reduce((acc, contact) => { + const completeOrganization = this.allOrganizations.get( + contact.organization.name + ) + + const updatedContact = { + ...contact, + organization: + completeOrganization ?? + ({ name: contact.organization.name } as Organization), + } + + const contactAsDynElem = { + component: ContactCardComponent, + inputs: { + contact: updatedContact, + removable: false, + }, + } as DynamicElement + + acc.push(contactAsDynElem) + + return acc + }, [] as DynamicElement[]) + + this.changeDetectorRef.markForCheck() + } + + removeContact() { + this.control.setValue([]) + } + + handleContactsChanged(event: DynamicElement[]) { + const newContactsOrdered = event.map( + (contactAsDynElem) => contactAsDynElem.inputs['contact'] + ) as Individual[] + + console.log('newContactsOrdered :', newContactsOrdered) + + this.control.setValue(newContactsOrdered) + } + + /** + * gn-ui-autocomplete + */ + displayWithFn: (user: UserModel) => string = (user) => + `${user.name} ${user.surname} ${ + user.organisation ? `(${user.organisation})` : '' + }` + + /** + * gn-ui-autocomplete + */ + autoCompleteAction = (query: string) => { + const fuzzyFilter = createFuzzyFilter(query) + return this.allUsers$.pipe( + switchMap((users) => [ + users.filter((user) => fuzzyFilter(user.username)), + ]), + map((results) => results.slice(0, 10)), + debounceTime(300), + distinctUntilChanged() + ) + } + + /** + * gn-ui-autocomplete + */ + addContact(contact: UserModel) { + const newContacts = { + firstName: contact.name ?? '', + lastName: contact.surname ?? '', + organization: + this.allOrganizations.get(contact.organisation) ?? + ({ name: contact.organisation } as Organization), + email: contact.email ?? '', + role: 'point_of_contact', + address: '', + phone: '', + position: '', + } as Individual + + const newControlValue = [...this.control.value, newContacts] + + this.control.setValue(newControlValue) + } + + ngOnDestroy(): void { + this.subscription.unsubscribe() + } +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html index ea903d38fb..00e4f87667 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html @@ -123,4 +123,9 @@ > + + + diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index 0b94d73a46..5574c24ff0 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -93,6 +93,13 @@ export const CONTACTS_FOR_RESOURCE_FIELD: EditorField = { }, } +export const CONTACTS: EditorField = { + model: 'contacts', + formFieldConfig: { + labelKey: '', + }, +} + export const RECORD_GRAPHICAL_OVERVIEW_FIELD: EditorField = { model: 'overviews', formFieldConfig: { @@ -191,7 +198,7 @@ export const DATA_POINT_OF_CONTACT_SECTION: EditorSection = { 'editor.record.form.section.dataPointOfContact.description' ), hidden: false, - fields: [], + fields: [CONTACTS], } /************************************************************ From deed6da4c4fccf96d67c44e5da31ff83e24ea33e Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Thu, 29 Aug 2024 10:39:04 +0200 Subject: [PATCH 06/49] wip --- .../form-field-contacts.component.html | 38 +---------- .../form-field-contacts.component.ts | 64 ++++++++----------- .../form-field/form-field.component.html | 11 ++-- translations/de.json | 1 + translations/en.json | 1 + translations/es.json | 1 + translations/fr.json | 1 + translations/it.json | 1 + translations/nl.json | 1 + translations/pt.json | 1 + translations/sk.json | 1 + 11 files changed, 42 insertions(+), 79 deletions(-) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html index 86db0afdf0..5bf1b7c49a 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html @@ -1,40 +1,4 @@
-
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts index 371379b611..ed75b78c66 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts @@ -3,13 +3,13 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + EventEmitter, Input, OnChanges, OnDestroy, - OnInit, + Output, SimpleChanges, } from '@angular/core' -import { FormControl } from '@angular/forms' import { AutocompleteComponent, DropdownSelectorComponent, @@ -58,10 +58,9 @@ import { map } from 'rxjs/operators' SortableListComponent, ], }) -export class FormFieldContactsComponent - implements OnInit, OnDestroy, OnChanges -{ - @Input() control: FormControl +export class FormFieldContactsComponent implements OnDestroy, OnChanges { + @Input() value: Individual[] + @Output() valueChange: EventEmitter = new EventEmitter() contacts: Individual[] = [] contactsAsDynElem: DynamicElement[] = [] @@ -82,32 +81,31 @@ export class FormFieldContactsComponent this.allUsers$ = this.platformServiceInterface.getUsers() } - ngOnChanges(changes: SimpleChanges): void { - console.log(changes['control']) - } + async ngOnChanges(changes: SimpleChanges): Promise { + const contactsChanges = changes['value'] - async ngOnInit(): Promise { - this.allOrganizations = new Map( - ( - await firstValueFrom(this.organizationsServiceInterface.organisations$) - ).map((organization) => [organization.name, organization]) - ) + if (contactsChanges.firstChange) { + this.allOrganizations = new Map( + ( + await firstValueFrom( + this.organizationsServiceInterface.organisations$ + ) + ).map((organization) => [organization.name, organization]) + ) + } - this.updateContacts() + if (contactsChanges.currentValue !== contactsChanges.previousValue) { + const contacts = contactsChanges.currentValue + console.log(contacts) - this.changeDetectorRef.markForCheck() + this.updateContacts() - this.subscription.add( - this.control.valueChanges.subscribe((contacts) => { - console.log('new contacts (valueChange): ', contacts) - this.updateContacts() - this.changeDetectorRef.markForCheck() - }) - ) + this.changeDetectorRef.markForCheck() + } } updateContacts() { - this.contacts = this.control.value.reduce((acc, contact) => { + this.contacts = this.value.reduce((acc, contact) => { const completeOrganization = this.allOrganizations.get( contact.organization.name ) @@ -124,7 +122,7 @@ export class FormFieldContactsComponent return acc }, [] as Individual[]) - this.contactsAsDynElem = this.control.value.reduce((acc, contact) => { + this.contactsAsDynElem = this.value.reduce((acc, contact) => { const completeOrganization = this.allOrganizations.get( contact.organization.name ) @@ -152,18 +150,12 @@ export class FormFieldContactsComponent this.changeDetectorRef.markForCheck() } - removeContact() { - this.control.setValue([]) - } - handleContactsChanged(event: DynamicElement[]) { const newContactsOrdered = event.map( (contactAsDynElem) => contactAsDynElem.inputs['contact'] ) as Individual[] - console.log('newContactsOrdered :', newContactsOrdered) - - this.control.setValue(newContactsOrdered) + this.valueChange.emit(newContactsOrdered) } /** @@ -193,7 +185,7 @@ export class FormFieldContactsComponent * gn-ui-autocomplete */ addContact(contact: UserModel) { - const newContacts = { + const newContact = { firstName: contact.name ?? '', lastName: contact.surname ?? '', organization: @@ -206,9 +198,7 @@ export class FormFieldContactsComponent position: '', } as Individual - const newControlValue = [...this.control.value, newContacts] - - this.control.setValue(newControlValue) + this.valueChange.emit([...this.value, newContact]) } ngOnDestroy(): void { diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html index 00e4f87667..a303aa2c93 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html @@ -122,10 +122,11 @@ (valueChange)="valueChange.emit($event)" > - - - + + + diff --git a/translations/de.json b/translations/de.json index 24b74e605d..38b8ea9523 100644 --- a/translations/de.json +++ b/translations/de.json @@ -206,6 +206,7 @@ "editor.record.form.bottomButtons.previous": "", "editor.record.form.classification.opendata": "", "editor.record.form.field.abstract": "Kurzbeschreibung", + "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "Schlagwörter", "editor.record.form.field.license": "Lizenz", diff --git a/translations/en.json b/translations/en.json index ce7750795b..57643c9fd1 100644 --- a/translations/en.json +++ b/translations/en.json @@ -206,6 +206,7 @@ "editor.record.form.bottomButtons.previous": "Previous", "editor.record.form.classification.opendata": "Open Data", "editor.record.form.field.abstract": "Abstract", + "editor.record.form.field.contacts.noContact": "Please provide at least one point of contact.", "editor.record.form.field.contactsForResource.noContact": "Please provide at least one point of contact responsible for the data.", "editor.record.form.field.keywords": "Keywords", "editor.record.form.field.license": "License", diff --git a/translations/es.json b/translations/es.json index e708c32ec0..6f87aa7c9d 100644 --- a/translations/es.json +++ b/translations/es.json @@ -206,6 +206,7 @@ "editor.record.form.bottomButtons.previous": "", "editor.record.form.classification.opendata": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "", diff --git a/translations/fr.json b/translations/fr.json index 42a4944787..9062405511 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -206,6 +206,7 @@ "editor.record.form.bottomButtons.previous": "Précédent", "editor.record.form.classification.opendata": "Données ouvertes", "editor.record.form.field.abstract": "Résumé", + "editor.record.form.field.contacts.noContact": "Veuillez renseigner au moins un point de contact.", "editor.record.form.field.contactsForResource.noContact": "Veuillez renseigner au moins un point de contact responsable de la donnée.", "editor.record.form.field.keywords": "Mots-clés", "editor.record.form.field.license": "Licence", diff --git a/translations/it.json b/translations/it.json index 1f87f83ac5..6508983d02 100644 --- a/translations/it.json +++ b/translations/it.json @@ -206,6 +206,7 @@ "editor.record.form.bottomButtons.previous": "", "editor.record.form.classification.opendata": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "Licenza", diff --git a/translations/nl.json b/translations/nl.json index c5895c20c4..d6ab9c4d22 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -206,6 +206,7 @@ "editor.record.form.bottomButtons.previous": "", "editor.record.form.classification.opendata": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "", diff --git a/translations/pt.json b/translations/pt.json index 64c5b7975d..58f0dd7185 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -206,6 +206,7 @@ "editor.record.form.bottomButtons.previous": "", "editor.record.form.classification.opendata": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "", diff --git a/translations/sk.json b/translations/sk.json index 3911e28e0e..c71d528ab3 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -206,6 +206,7 @@ "editor.record.form.bottomButtons.previous": "", "editor.record.form.classification.opendata": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "Licencia", From 770f3cae04d44bbd993fe70d9530cdf2ff955607 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Mon, 23 Sep 2024 09:56:32 +0200 Subject: [PATCH 07/49] fix after rebase --- .../form-field-contacts.component.html | 5 +- .../form-field-contacts.component.ts | 46 ++++--------------- .../form-field/form-field.component.ts | 4 +- 3 files changed, 14 insertions(+), 41 deletions(-) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html index 5bf1b7c49a..689c201feb 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html @@ -13,14 +13,13 @@ diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts index ed75b78c66..7972363553 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts @@ -34,12 +34,9 @@ import { UserModel } from '@geonetwork-ui/common/domain/model/user' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { ContactCardComponent } from '../../../contact-card/contact-card.component' -import { - DynamicElement, - SortableListComponent, -} from '@geonetwork-ui/ui/elements' import { createFuzzyFilter } from '@geonetwork-ui/util/shared' import { map } from 'rxjs/operators' +import { SortableListComponent } from '@geonetwork-ui/ui/layout' @Component({ selector: 'gn-ui-form-field-contacts', @@ -63,7 +60,6 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges { @Output() valueChange: EventEmitter = new EventEmitter() contacts: Individual[] = [] - contactsAsDynElem: DynamicElement[] = [] subscription: Subscription = new Subscription() @@ -84,6 +80,8 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges { async ngOnChanges(changes: SimpleChanges): Promise { const contactsChanges = changes['value'] + console.log(contactsChanges) + if (contactsChanges.firstChange) { this.allOrganizations = new Map( ( @@ -121,41 +119,15 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges { return acc }, [] as Individual[]) - - this.contactsAsDynElem = this.value.reduce((acc, contact) => { - const completeOrganization = this.allOrganizations.get( - contact.organization.name - ) - - const updatedContact = { - ...contact, - organization: - completeOrganization ?? - ({ name: contact.organization.name } as Organization), - } - - const contactAsDynElem = { - component: ContactCardComponent, - inputs: { - contact: updatedContact, - removable: false, - }, - } as DynamicElement - - acc.push(contactAsDynElem) - - return acc - }, [] as DynamicElement[]) - - this.changeDetectorRef.markForCheck() } - handleContactsChanged(event: DynamicElement[]) { - const newContactsOrdered = event.map( - (contactAsDynElem) => contactAsDynElem.inputs['contact'] - ) as Individual[] + handleContactsChanged(items: unknown[]) { + console.log(items) + // const newContactsOrdered = items.map( + // (contactAsDynElem) => contactAsDynElem.inputs['contact'] + // ) as Individual[] - this.valueChange.emit(newContactsOrdered) + // this.valueChange.emit(newContactsOrdered) } /** diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts index 258b8c3502..9d27053087 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts @@ -24,8 +24,8 @@ import { EditableLabelDirective } from '@geonetwork-ui/ui/inputs' import { FormFieldWrapperComponent } from '@geonetwork-ui/ui/layout' import { TranslateModule } from '@ngx-translate/core' import { - FormFieldLicenseComponent, FormFieldDateUpdatedComponent, + FormFieldLicenseComponent, FormFieldTemporalExtentsComponent, } from '.' import { FieldModelSpecifier, FormFieldConfig } from '../../../models' @@ -41,6 +41,7 @@ import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/for import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency/form-field-update-frequency.component' import { FormFieldOpenDataComponent } from './form-field-open-data/form-field-open-data.component' import { FormFieldOnlineLinkResourcesComponent } from './form-field-online-link-resources/form-field-online-link-resources.component' +import { FormFieldContactsComponent } from './form-field-contacts/form-field-contacts.component' @Component({ selector: 'gn-ui-form-field', @@ -70,6 +71,7 @@ import { FormFieldOnlineLinkResourcesComponent } from './form-field-online-link- FormFieldContactsForResourceComponent, FormFieldOpenDataComponent, FormFieldOnlineLinkResourcesComponent, + FormFieldContactsComponent, ], }) export class FormFieldComponent { From 0b997c1677c37b74311040627ee6417c9bfc8d83 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Mon, 23 Sep 2024 13:18:01 +0200 Subject: [PATCH 08/49] fix after rebase 2 --- .../src/lib/iso19115-3/read-parts.ts | 1 + .../src/lib/iso19139/read-parts.ts | 2 ++ .../src/lib/iso19139/write-parts.ts | 2 ++ .../form-field-contacts.component.html | 6 ++++++ .../form-field-contacts.component.ts | 14 ++++---------- .../form-field/form-field.component.html | 12 ++++++------ 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts b/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts index e06bf7b20b..caca9ebb73 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts @@ -229,6 +229,7 @@ export function readOwnerOrganization(rootEl: XmlElement): Organization { } export function readContacts(rootEl: XmlElement): Individual[] { + console.log('ici') return pipe( findNestedElements('mdb:contact', 'cit:CI_Responsibility'), mapArray(extractIndividuals()), diff --git a/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts b/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts index 34dee9e287..ebcd72e6ba 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts @@ -600,6 +600,7 @@ export function readAbstract(rootEl: XmlElement): string { } export function readContacts(rootEl: XmlElement): Individual[] { + console.log('converter : readContacts') return pipe( findChildrenElement('gmd:contact', false), mapArray(findChildElement('gmd:CI_ResponsibleParty', false)), @@ -608,6 +609,7 @@ export function readContacts(rootEl: XmlElement): Individual[] { } export function readContactsForResource(rootEl: XmlElement): Individual[] { + console.log('converter : readContactsForResource') return pipe( findIdentification(), combine( diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts index 8471232292..38ec9f59b4 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts @@ -782,6 +782,7 @@ export function writeStatus(record: DatasetRecord, rootEl: XmlElement) { } export function writeContacts(record: CatalogRecord, rootEl: XmlElement) { + console.log('converter : writeContacts') pipe( removeChildrenByName('gmd:contact'), appendChildren( @@ -796,6 +797,7 @@ export function writeContactsForResource( record: CatalogRecord, rootEl: XmlElement ) { + console.log('converter : writeContactsForResource') pipe( findOrCreateIdentification(), removeChildrenByName('gmd:pointOfContact'), diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html index 689c201feb..822ed4b9fb 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html @@ -8,6 +8,7 @@ [clearOnSelection]="true" > + @@ -20,10 +21,15 @@ + + +
+
{ const contactsChanges = changes['value'] - console.log(contactsChanges) - if (contactsChanges.firstChange) { this.allOrganizations = new Map( ( @@ -93,9 +91,6 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges { } if (contactsChanges.currentValue !== contactsChanges.previousValue) { - const contacts = contactsChanges.currentValue - console.log(contacts) - this.updateContacts() this.changeDetectorRef.markForCheck() @@ -122,12 +117,11 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges { } handleContactsChanged(items: unknown[]) { - console.log(items) - // const newContactsOrdered = items.map( - // (contactAsDynElem) => contactAsDynElem.inputs['contact'] - // ) as Individual[] + const contacts = items as Individual[] + + this.contacts = contacts - // this.valueChange.emit(newContactsOrdered) + this.valueChange.emit(contacts) } /** diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html index a303aa2c93..d48c8df844 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html @@ -114,6 +114,12 @@ (valueChange)="valueChange.emit($event)" > + + + - - - From 91efa1a1cbfd69dc3926df65e9e452010f5a46db Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Mon, 16 Sep 2024 15:44:05 +0200 Subject: [PATCH 09/49] fix(editor): fix-misc-issues-with-dashboard-views --- .../src/e2e/dashboard.cy.ts | 8 +- apps/metadata-editor-e2e/src/e2e/edit.cy.ts | 4 +- apps/metadata-editor-e2e/src/e2e/my-org.cy.ts | 41 --- .../src/e2e/record-actions.cy.ts | 20 +- apps/metadata-editor/src/app/app.routes.ts | 22 +- .../dashboard-menu.component.html | 10 +- .../dashboard/dashboard-page.component.html | 2 +- .../search-header/search-header.component.css | 4 +- .../search-header.component.html | 4 +- .../all-records.component.css} | 0 .../all-records.component.html} | 20 +- .../all-records/all-records.component.spec.ts | 162 ++++++++++++ .../all-records.component.ts} | 78 ++++-- .../my-library/my-library.component.html | 2 - .../my-org-records.component.html | 7 - .../my-org-records.component.spec.ts | 147 ----------- .../my-org-records.component.ts | 54 ---- .../my-records/my-records.component.html | 78 +++++- .../my-records/my-records.component.spec.ts | 148 ++++++----- .../my-records/my-records.component.ts | 152 +++++++++-- .../app/records/records-list.component.html | 83 ++----- .../src/app/records/records-list.component.ts | 33 +-- .../search-records-list.component.spec.ts | 235 ------------------ .../templates.component.css} | 0 .../templates.component.html} | 0 .../templates.component.spec.ts} | 10 +- .../templates.component.ts} | 8 +- .../form-field/form-field.component.html | 1 + .../results-table-container.component.html | 2 + .../results-table-container.component.ts | 3 + .../inputs/src/lib/badge/badge.component.html | 3 +- .../interactive-table.component.css | 2 +- .../interactive-table.component.html | 4 +- .../results-table.component.html | 23 +- tailwind.base.css | 10 +- translations/de.json | 2 + translations/en.json | 2 + translations/es.json | 2 + translations/fr.json | 2 + translations/it.json | 2 + translations/nl.json | 2 + translations/pt.json | 2 + translations/sk.json | 2 + 43 files changed, 615 insertions(+), 781 deletions(-) delete mode 100644 apps/metadata-editor-e2e/src/e2e/my-org.cy.ts rename apps/metadata-editor/src/app/records/{my-library/my-library.component.css => all-records/all-records.component.css} (100%) rename apps/metadata-editor/src/app/records/{search-records/search-records-list.component.html => all-records/all-records.component.html} (74%) create mode 100644 apps/metadata-editor/src/app/records/all-records/all-records.component.spec.ts rename apps/metadata-editor/src/app/records/{search-records/search-records-list.component.ts => all-records/all-records.component.ts} (67%) delete mode 100644 apps/metadata-editor/src/app/records/my-library/my-library.component.html delete mode 100644 apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.html delete mode 100644 apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.spec.ts delete mode 100644 apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts delete mode 100644 apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts rename apps/metadata-editor/src/app/records/{my-org-records/my-org-records.component.css => templates/templates.component.css} (100%) rename apps/metadata-editor/src/app/records/{search-records/search-records-list.component.css => templates/templates.component.html} (100%) rename apps/metadata-editor/src/app/records/{my-library/my-library.component.spec.ts => templates/templates.component.spec.ts} (83%) rename apps/metadata-editor/src/app/records/{my-library/my-library.component.ts => templates/templates.component.ts} (74%) diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index 9394e2df93..654849977c 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -187,7 +187,7 @@ describe('dashboard', () => { cy.get('@draft') .children('div') .eq(5) - .should('contain', ' Not published ') + .should('include.text', 'Not published') cy.get('@draft').children('div').eq(6).should('contain', ' - ') cy.clearRecordDrafts() }) @@ -204,7 +204,11 @@ describe('dashboard', () => { .find('span') .invoke('text') .should('eq', 'admin admin') - cy.get('@record').children('div').eq(5).should('contain', ' Published ') + cy.get('@record') + .children('div') + .eq(5) + .find('span') + .should('include.text', 'Published') cy.get('@record').children('div').eq(6).should('not.contain', ' - ') cy.clearRecordDrafts() }) diff --git a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts index c6492c2616..70ec2fef0a 100644 --- a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts @@ -1,9 +1,10 @@ /* eslint-disable cypress/no-unnecessary-waiting */ describe('editor form', () => { - let recordUuid + let recordUuid: any before(() => { cy.login('admin', 'admin', false) + cy.viewport(1920, 2400) cy.clearRecordDrafts() @@ -53,6 +54,7 @@ describe('editor form', () => { }) beforeEach(() => { cy.login('admin', 'admin', false) + cy.visit('/catalog/search') cy.wrap(recordUuid).as('recordUuid') cy.get('@recordUuid').then((recordUuid) => { diff --git a/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts b/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts deleted file mode 100644 index 14ddf94cf9..0000000000 --- a/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts +++ /dev/null @@ -1,41 +0,0 @@ -describe('my-org', () => { - beforeEach(() => { - cy.login('barbie', 'p4ssworD_', false) - cy.intercept({ - method: 'GET', - url: '/geonetwork/srv/api/userselections/0/101', - }).as('dataGetFirst') - cy.visit(`/catalog/my-org`) - cy.wait('@dataGetFirst').its('response.statusCode').should('equal', 200) - }) - 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('[data-cy=link-to-datahub]').should('not.have.text', '') - cy.get('[data-cy=link-to-users]').should('not.have.text', '') - }) - it('should show my-org records', () => { - cy.get('gn-ui-interactive-table .contents').should('have.length.above', 1) - }) - }) - describe('routing', () => { - it('should access the datahub with a filter', () => { - cy.get('[data-cy=link-to-datahub]') - .should('have.attr', 'href') - .then((href) => { - expect(href).to.include('search?publisher=Barbie+Inc') - }) - }) - it('should access the user list page and show my-org users', () => { - cy.visit(`/catalog/my-org`) - cy.get('[data-cy=link-to-users]').click() - cy.url().should('include', '/users/my-org') - cy.get('gn-ui-interactive-table .contents').should('have.length.above', 1) - cy.get('h1').should('not.have.text', '') - cy.get('gn-ui-thumbnail') - }) - }) -}) diff --git a/apps/metadata-editor-e2e/src/e2e/record-actions.cy.ts b/apps/metadata-editor-e2e/src/e2e/record-actions.cy.ts index ba6d551c22..20afc25802 100644 --- a/apps/metadata-editor-e2e/src/e2e/record-actions.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/record-actions.cy.ts @@ -4,6 +4,7 @@ describe('record-actions', () => { cy.visit('/catalog/search') }) describe('delete', () => { + const recordId = `TEST_RECORD_${Date.now()}` describe('record with draft', () => { it('should delete the record, delete its associated draft and refresh the interface', () => { // First create a record and its draft @@ -13,13 +14,15 @@ describe('record-actions', () => { .as('abstractField') .focus() cy.get('@abstractField').type('record abstract') + cy.get('[data-test="recordTitleInput"]').click() + cy.get('[data-test="recordTitleInput"]').type('{selectAll}{backspace}') + cy.get('[data-test="recordTitleInput"]').type(recordId) cy.intercept({ method: 'PUT', pathname: '**/records', }).as('insertRecord') cy.get('md-editor-publish-button').click() cy.wait('@insertRecord') - cy.get('@abstractField').focus() cy.get('@abstractField').type('draft abstract') // Assert that the draft exists in the local storage cy.editor_readFormUniqueIdentifier().then((uniqueIdentifier) => @@ -31,7 +34,7 @@ describe('record-actions', () => { ) cy.visit('/my-space/my-records') cy.get('[data-cy="table-row"]') - .contains('My new record') + .contains(recordId) .should('have.length', 1) cy.get('[data-cy="dashboard-drafts-count"]').should('contain', '1') // Delete the record @@ -39,10 +42,9 @@ describe('record-actions', () => { cy.get('[data-test="record-menu-delete-button"]').click() cy.get('[data-cy="confirm-button"]').click() cy.get('[data-cy="table-row"]') - .contains('My new record') + .contains(recordId) .should('have.length', 0) cy.get('gn-ui-notification').should('contain', 'Delete success') - cy.get('[data-cy="dashboard-drafts-count"]').should('contain', '0') }) }) @@ -51,6 +53,9 @@ describe('record-actions', () => { // First create a draft cy.get('[data-cy="create-record"]').click() cy.url().should('include', '/create') + cy.get('[data-test="recordTitleInput"]').click() + cy.get('[data-test="recordTitleInput"]').type('{selectAll}{backspace}') + cy.get('[data-test="recordTitleInput"]').type(recordId) cy.get('gn-ui-form-field[ng-reflect-model=abstract] textarea') .as('abstractField') .focus() @@ -63,17 +68,14 @@ describe('record-actions', () => { }) cy.visit('/my-space/my-draft') cy.get('[data-cy="table-row"]') - .contains('My new record') + .contains(recordId) .should('have.length', 1) cy.get('[data-cy="dashboard-drafts-count"]').should('contain', '1') // Delete the draft cy.get('[data-test="record-menu-button"]').click() cy.get('[data-test="record-menu-delete-button"]').click() cy.get('[data-cy="confirm-button"]').click() - cy.get('[data-cy="table-row"]') - .contains('New record') - .should('have.length', 0) - cy.get('[data-cy="dashboard-drafts-count"]').should('contain', '0') + cy.get('[data-cy="table-row"]').should('not.exist') }) }) }) diff --git a/apps/metadata-editor/src/app/app.routes.ts b/apps/metadata-editor/src/app/app.routes.ts index d8c5da1256..0a9e7df065 100644 --- a/apps/metadata-editor/src/app/app.routes.ts +++ b/apps/metadata-editor/src/app/app.routes.ts @@ -5,12 +5,11 @@ import { EditPageComponent } from './edit/edit-page.component' import { EditRecordResolver } from './edit-record.resolver' 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 { TemplatesComponent } from './records/templates/templates.component' import { MyOrgUsersComponent } from './my-org-users/my-org-users.component' -import { MyOrgRecordsComponent } from './records/my-org-records/my-org-records.component' import { NewRecordResolver } from './new-record.resolver' import { DuplicateRecordResolver } from './duplicate-record.resolver' +import { AllRecordsComponent } from './records/all-records/all-records.component' export const appRoutes: Route[] = [ { path: '', redirectTo: 'catalog/search', pathMatch: 'prefix' }, @@ -26,35 +25,30 @@ export const appRoutes: Route[] = [ }, { path: 'discussion', - component: SearchRecordsComponent, + component: AllRecordsComponent, pathMatch: 'prefix', }, { path: 'calendar', - component: SearchRecordsComponent, + component: AllRecordsComponent, pathMatch: 'prefix', }, { path: 'contacts', - component: SearchRecordsComponent, + component: AllRecordsComponent, pathMatch: 'prefix', }, { path: 'thesaurus', - component: SearchRecordsComponent, + component: AllRecordsComponent, pathMatch: 'prefix', }, { path: 'search', title: 'Search Records', - component: SearchRecordsComponent, + component: AllRecordsComponent, pathMatch: 'prefix', }, - { - path: 'my-org', - title: 'My Organisation', - component: MyOrgRecordsComponent, - }, ], }, { @@ -78,7 +72,7 @@ export const appRoutes: Route[] = [ { path: 'templates', title: 'Templates', - component: MyLibraryComponent, + component: TemplatesComponent, pathMatch: 'prefix', }, ], diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html index 7a2a11d2ca..7ded10a5ee 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html @@ -71,11 +71,11 @@ > edit_note dashboard.records.myDraft - {{ draftsCount$ | async }} + + {{ + draftsCount + }} +
-
+
diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.css b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.css index 03bcc57c34..542d168034 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.css +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.css @@ -1,8 +1,8 @@ :host ::ng-deep gn-ui-autocomplete input { - @apply bg-blue-50 rounded-3xl shadow-none !py-2 !px-8 !pl-14 hover:shadow-none focus:outline-4 focus:outline-offset-1 focus:outline-blue-100 focus:outline-dotted; + @apply rounded-3xl shadow-none !py-2 !px-8 !pl-14 hover:shadow-none; } :host ::ng-deep gn-ui-autocomplete button { - @apply bg-blue-50 hover:bg-blue-50 border-none shadow-none hover:shadow-none text-gray-500 hover:text-gray-600 left-0 right-auto rounded-3xl; + @apply border-none shadow-none hover:shadow-none text-gray-500 hover:text-gray-600 left-0 right-auto rounded-3xl; } ::ng-deep .mdc-menu-surface.mat-mdc-autocomplete-panel { diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html index 64cc3f9525..fffb542060 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html @@ -1,6 +1,8 @@
- +
-
- - -
-
- -
-
-
+ diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.spec.ts b/apps/metadata-editor/src/app/records/all-records/all-records.component.spec.ts new file mode 100644 index 0000000000..a1bc98c57d --- /dev/null +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.spec.ts @@ -0,0 +1,162 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { + FieldsService, + SearchFacade, + SearchService, +} from '@geonetwork-ui/feature/search' +import { ChangeDetectionStrategy } from '@angular/core' +import { TranslateModule } from '@ngx-translate/core' +import { BehaviorSubject, of } from 'rxjs' +import { barbieUserFixture } from '@geonetwork-ui/common/fixtures' +import { ActivatedRoute, Router } from '@angular/router' +import { AllRecordsComponent } from './all-records.component' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { + MockBuilder, + MockInstance, + MockProviders, + NG_MOCKS_ROOT_PROVIDERS, +} from 'ng-mocks' +import { EditorRouterService } from '../../router.service' +import { Overlay } from '@angular/cdk/overlay' + +describe('AllRecordsComponent', () => { + MockInstance.scope() + + const searchFilters = new BehaviorSubject({ + any: 'hello world', + }) + + let component: AllRecordsComponent + let fixture: ComponentFixture + + let router: Router + let searchFacade: SearchFacade + let platformService: PlatformServiceInterface + let fieldsService: FieldsService + + beforeEach(() => { + return MockBuilder(AllRecordsComponent) + .keep(Overlay) + .keep(NG_MOCKS_ROOT_PROVIDERS) + }) + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + MockProviders( + FieldsService, + SearchFacade, + PlatformServiceInterface, + EditorRouterService, + ActivatedRoute, + SearchService + ), + ], + }).overrideComponent(AllRecordsComponent, { + set: { + changeDetection: ChangeDetectionStrategy.Default, + }, + }) + + MockInstance( + SearchFacade, + 'searchFilters$', + jest.fn(), + 'get' + ).mockReturnValue(searchFilters) + + MockInstance(ActivatedRoute, 'snapshot', jest.fn(), 'get').mockReturnValue({ + paramMap: new Map([['paramId', 'paramValue']]), + queryParams: new Map([['paramId', 'paramValue']]), + }) + + fixture = TestBed.createComponent(AllRecordsComponent) + + router = TestBed.inject(Router) + searchFacade = TestBed.inject(SearchFacade) + platformService = TestBed.inject(PlatformServiceInterface) + fieldsService = TestBed.inject(FieldsService) + + router.navigate = jest.fn().mockReturnValue(Promise.resolve(true)) + + platformService.getMe = jest.fn( + () => new BehaviorSubject(barbieUserFixture()) + ) + + fieldsService.buildFiltersFromFieldValues = jest.fn((fieldValues) => + of( + Object.keys(fieldValues).reduce( + (_, curr) => ({ + [curr]: fieldValues[curr], + }), + {} + ) + ) + ) + + searchFacade.resetSearch = jest.fn(() => this) + searchFacade.updateFilters = jest.fn(() => this) + searchFacade.setFilters = jest.fn(() => this) + searchFacade.setSortBy = jest.fn(() => this) + searchFacade.setPageSize = jest.fn(() => this) + searchFacade.setConfigRequestFields = jest.fn(() => this) + + component = fixture.componentInstance + + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should map search filters to searchText$', (done) => { + component.searchText$.subscribe((text) => { + expect(text).toBe('hello world') + done() + }) + }) + + describe('when clicking createRecord', () => { + beforeEach(() => { + component.createRecord() + }) + + it('navigates to the create record page', () => { + expect(router.navigate).toHaveBeenCalledWith(['/create']) + }) + }) + + describe('when importing a record', () => { + beforeEach(() => { + component.duplicateExternalRecord() + }) + + it('sets isImportMenuOpen to true', () => { + expect(component.isImportMenuOpen).toBe(true) + }) + }) + + describe('when closing the import menu', () => { + let overlaySpy: any + + beforeEach(() => { + overlaySpy = { + dispose: jest.fn(), + } + component['overlayRef'] = overlaySpy + + component.closeImportMenu() + }) + + it('sets isImportMenuOpen to false', () => { + expect(component.isImportMenuOpen).toBe(false) + }) + + it('disposes the overlay', () => { + expect(overlaySpy.dispose).toHaveBeenCalled() + }) + }) +}) diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts similarity index 67% rename from apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts rename to apps/metadata-editor/src/app/records/all-records/all-records.component.ts index be862df39e..cd1412b1e2 100644 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectorRef, Component, ElementRef, + OnInit, TemplateRef, ViewChild, ViewContainerRef, @@ -13,11 +14,9 @@ import { SearchService, } from '@geonetwork-ui/feature/search' import { TranslateModule } from '@ngx-translate/core' -import { map } from 'rxjs/operators' -import { Router } from '@angular/router' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { ActivatedRoute, Router } from '@angular/router' import { RecordsCountComponent } from '../records-count/records-count.component' -import { Observable } from 'rxjs' +import { Observable, of } from 'rxjs' import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { MatIconModule } from '@angular/material/icon' @@ -29,11 +28,25 @@ import { } from '@angular/cdk/overlay' import { TemplatePortal } from '@angular/cdk/portal' import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' +import { RecordsListComponent } from '../records-list.component' +import { map } from 'rxjs/operators' + +export const allSearchFields = [ + 'uuid', + 'resourceTitleObject', + 'createDate', + 'changeDate', + 'userinfo', + 'cl_status', + 'isPublishedToAll', + 'link', + 'owner', +] @Component({ - selector: 'md-editor-search-records-list', - templateUrl: './search-records-list.component.html', - styleUrls: ['./search-records-list.component.css'], + selector: 'md-editor-all-records', + templateUrl: './all-records.component.html', + styleUrls: ['./all-records.component.css'], standalone: true, imports: [ CommonModule, @@ -46,43 +59,58 @@ import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' ImportRecordComponent, CdkOverlayOrigin, CdkConnectedOverlay, + RecordsListComponent, ], }) -export class SearchRecordsComponent { +export class AllRecordsComponent implements OnInit { @ViewChild('importRecordButton', { read: ElementRef }) - private importRecordButton!: ElementRef + importRecordButton!: ElementRef @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef - searchText$: Observable = - this.searchFacade.searchFilters$.pipe( - map((filters) => ('any' in filters ? (filters['any'] as string) : null)) - ) + searchText$: Observable = of(null) isImportMenuOpen = false constructor( private router: Router, + private activedRoute: ActivatedRoute, public searchFacade: SearchFacade, public searchService: SearchService, private overlay: Overlay, private viewContainerRef: ViewContainerRef, private cdr: ChangeDetectorRef - ) { - this.searchFacade.setPageSize(15) + ) {} + + ngOnInit(): void { + this.searchText$ = this.searchFacade.searchFilters$.pipe( + map((filters) => ('any' in filters ? (filters['any'] as string) : null)) + ) + this.searchFacade.resetSearch() - } - editRecord(record: CatalogRecord) { - this.router - .navigate(['/edit', record.uniqueIdentifier]) - .catch((err) => console.error(err)) - } + const searchTerms = this.activedRoute.snapshot.queryParams['q'] ?? '' + + if (searchTerms) { + this.searchFacade.setFilters({ any: searchTerms }) + } + + let sort = (this.activedRoute.snapshot.queryParams['_sort'] as string) ?? '' - duplicateRecord(record: CatalogRecord) { - this.router - .navigate(['/duplicate', record.uniqueIdentifier]) - .catch((err) => console.error(err)) + if (sort) { + let ascDesc = '' + + if (sort?.charAt(0) === '-') { + ascDesc = 'desc' + sort = sort.slice(1, sort.length) + } else { + ascDesc = 'asc' + } + this.searchFacade.setSortBy([ascDesc as 'asc' | 'desc', sort]) + } + + this.searchFacade.setPageSize(15) + this.searchFacade.setConfigRequestFields(allSearchFields) } createRecord() { diff --git a/apps/metadata-editor/src/app/records/my-library/my-library.component.html b/apps/metadata-editor/src/app/records/my-library/my-library.component.html deleted file mode 100644 index 938169810b..0000000000 --- a/apps/metadata-editor/src/app/records/my-library/my-library.component.html +++ /dev/null @@ -1,2 +0,0 @@ - - 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 deleted file mode 100644 index 0d7ebda3a1..0000000000 --- a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.html +++ /dev/null @@ -1,7 +0,0 @@ - - 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 deleted file mode 100644 index 981f406c9a..0000000000 --- a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { MyOrgRecordsComponent } from './my-org-records.component' -import { of } from 'rxjs' -import { MyOrgService } from '@geonetwork-ui/feature/catalog' -import { someOrganizationsFixture } from '@geonetwork-ui/common/fixtures' -import { SearchFacade } from '@geonetwork-ui/feature/search' -import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' -import { EditorRouterService } from '../../router.service' - -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', - }, - ], -} - -const myOrgServiceMock = { - myOrgData$: of(orgDataMock), -} - -const organisationsServiceMock = { - organisations$: of(someOrganizationsFixture), -} - -const searchFacadeMock = { - resetSearch: jest.fn(), -} - -const routeServiceMock = { - getDatahubSearchRoute: jest.fn(), -} - -describe('MyOrgRecordsComponent', () => { - let component: MyOrgRecordsComponent - let searchFacade: SearchFacade - let myOrgService: MyOrgService - let orgServiceInterface: OrganizationsServiceInterface - let routerService: EditorRouterService - - beforeEach(() => { - orgServiceInterface = organisationsServiceMock as any - myOrgService = myOrgServiceMock as any - searchFacade = searchFacadeMock as any - routerService = routeServiceMock as any - - component = new MyOrgRecordsComponent( - myOrgService, - searchFacade, - orgServiceInterface, - routerService - ) - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - describe('Get organization users info', () => { - let orgData - - beforeEach(() => { - orgData = null - component.orgData$.subscribe((data) => (orgData = data)) - }) - - it('should get the org name', () => { - expect(orgData.orgName).toEqual('wizard-org') - }) - - it('should get the org logo', () => { - expect(orgData.logoUrl).toEqual('https://my-geonetwork.org/logo11.png') - }) - - it('should get the list of users', () => { - expect(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', - }, - ]) - }) - }) - it('should generate the correct Datahub URL', () => { - // Mock the router method and set orgData - component.router.getDatahubSearchRoute = () => 'http://example.com' - - const datahubUrl = component.getDatahubUrl() - - // Assert that the generated URL contains the orgName - expect(datahubUrl).toContain('publisher=wizard-org') - }) -}) 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 deleted file mode 100644 index eb6e675179..0000000000 --- a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Component } 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 { Organization } from '@geonetwork-ui/common/domain/model/record' -import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' -import { EditorRouterService } from '../../router.service' -import { take } from 'rxjs' - -@Component({ - selector: 'md-editor-my-org-records', - templateUrl: './my-org-records.component.html', - styleUrls: ['./my-org-records.component.css'], - standalone: true, - imports: [CommonModule, TranslateModule, RecordsListComponent], -}) -export class MyOrgRecordsComponent { - orgData$ = this.myOrgRecordsService.myOrgData$ - userCount: number - orgName: string - logoUrl: string - - constructor( - private myOrgRecordsService: MyOrgService, - public searchFacade: SearchFacade, - public orgService: OrganizationsServiceInterface, - public router: EditorRouterService - ) { - this.searchFacade.resetSearch() - this.orgData$.pipe(take(1)).subscribe((data) => { - this.userCount = data.userCount - this.orgName = data.orgName - this.logoUrl = data.logoUrl - this.searchByOrganisation({ name: data.orgName }) - }) - } - - searchByOrganisation(organisation: Organization) { - this.orgService - .getFiltersForOrgs([organisation]) - .subscribe((filters) => this.searchFacade.setFilters(filters)) - } - - getDatahubUrl(): string { - const url = new URL( - this.router.getDatahubSearchRoute(), - window.location.toString() - ) - url.searchParams.append('publisher', this.orgName) - return url.toString() - } -} 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 049f7af0c3..577f01a0ee 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,5 +1,73 @@ - - +
+
+ +

+ dashboard.records.search +

+
+ +
+
+ +

+ dashboard.records.myRecords +

+
+ +
+
+
+
+
+ dashboard.myRecords.publishedMetadatas +
+
+ dashboard.myRecords.currentlyEdited +
+
+ + dashboard.importRecord + keyboard_arrow_down + keyboard_arrow_up + + + + + + edit_document + dashboard.createRecord + +
+ + +
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 7a2c62c186..64ddf17f6c 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,84 +1,104 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { MyRecordsComponent } from './my-records.component' -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 { + FieldsService, + SearchFacade, + SearchService, +} from '@geonetwork-ui/feature/search' +import { ChangeDetectionStrategy } from '@angular/core' import { BehaviorSubject, of } from 'rxjs' import { barbieUserFixture } from '@geonetwork-ui/common/fixtures' import { EditorRouterService } from '../../router.service' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' - -@Component({ - selector: 'md-editor-records-list', - template: '', - standalone: true, -}) -export class MockRecordsListComponent { - @Input() linkToDatahub: string -} -const user = barbieUserFixture() - -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)) -} - -const me$ = new BehaviorSubject(barbieUserFixture()) -class PlatformServiceMock { - getMe = jest.fn(() => me$) -} +import { MockBuilder, MockInstance, MockProviders } from 'ng-mocks' +import { ActivatedRoute, Router } from '@angular/router' +import { TranslateModule } from '@ngx-translate/core' describe('MyRecordsComponent', () => { + MockInstance.scope() + + const searchFilters = new BehaviorSubject({ + any: 'hello world', + }) + let component: MyRecordsComponent let fixture: ComponentFixture + + let router: Router let searchFacade: SearchFacade + let platformService: PlatformServiceInterface + let fieldsService: FieldsService + + const user = barbieUserFixture() + + beforeEach(() => { + return MockBuilder(MyRecordsComponent) + }) beforeEach(() => { TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], providers: [ - importProvidersFrom(TranslateModule.forRoot()), - { - provide: FieldsService, - useClass: FieldsServiceMock, - }, - { - provide: SearchFacade, - useClass: SearchFacadeMock, - }, - { - provide: PlatformServiceInterface, - useClass: PlatformServiceMock, - }, - { - provide: EditorRouterService, - useClass: EditorRouterServiceMock, - }, + MockProviders( + FieldsService, + SearchFacade, + PlatformServiceInterface, + EditorRouterService, + ActivatedRoute, + SearchService + ), ], }).overrideComponent(MyRecordsComponent, { - remove: { - imports: [RecordsListComponent], - }, - add: { - imports: [MockRecordsListComponent], + set: { + changeDetection: ChangeDetectionStrategy.Default, }, }) - searchFacade = TestBed.inject(SearchFacade) + + MockInstance( + SearchFacade, + 'searchFilters$', + jest.fn(), + 'get' + ).mockReturnValue(searchFilters) + + MockInstance(ActivatedRoute, 'snapshot', jest.fn(), 'get').mockReturnValue({ + paramMap: new Map([['paramId', 'paramValue']]), + queryParams: new Map([['paramId', 'paramValue']]), + }) + fixture = TestBed.createComponent(MyRecordsComponent) + + searchFacade = TestBed.inject(SearchFacade) + router = TestBed.inject(Router) + platformService = TestBed.inject(PlatformServiceInterface) + fieldsService = TestBed.inject(FieldsService) + + router.navigate = jest.fn().mockReturnValue(Promise.resolve(true)) + + platformService.getMe = jest.fn( + () => new BehaviorSubject(barbieUserFixture()) + ) + + fieldsService.buildFiltersFromFieldValues = jest.fn((fieldValues) => + of( + Object.keys(fieldValues).reduce( + (_, curr) => ({ + [curr]: fieldValues[curr], + }), + {} + ) + ) + ) + + searchFacade.resetSearch = jest.fn(() => this) + searchFacade.updateFilters = jest.fn(() => this) + searchFacade.setFilters = jest.fn(() => this) + searchFacade.setSortBy = jest.fn(() => this) + searchFacade.setPageSize = jest.fn(() => this) + searchFacade.setConfigRequestFields = jest.fn(() => this) + component = fixture.componentInstance + fixture.detectChanges() }) @@ -96,12 +116,4 @@ describe('MyRecordsComponent', () => { }) }) }) - - 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 24246ba0f7..2ae25aea7e 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,33 +1,109 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' +import { + ChangeDetectorRef, + Component, + ElementRef, + OnDestroy, + OnInit, + TemplateRef, + ViewChild, + ViewContainerRef, +} from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' import { RecordsListComponent } from '../records-list.component' -import { FieldsService, SearchFacade } from '@geonetwork-ui/feature/search' -import { AuthService } from '@geonetwork-ui/api/repository' -import { EditorRouterService } from '../../router.service' -import { Subscription } from 'rxjs' +import { + FeatureSearchModule, + FieldsService, + ResultsTableContainerComponent, + SearchFacade, + SearchService, +} from '@geonetwork-ui/feature/search' +import { Observable, Subscription } from 'rxjs' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { UiElementsModule } from '@geonetwork-ui/ui/elements' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { ActivatedRoute, Router } from '@angular/router' +import { Overlay, OverlayRef } from '@angular/cdk/overlay' +import { TemplatePortal } from '@angular/cdk/portal' +import { allSearchFields } from '../all-records/all-records.component' +import { RecordsCountComponent } from '../records-count/records-count.component' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { MatIconModule } from '@angular/material/icon' +import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' +import { map } from 'rxjs/operators' @Component({ selector: 'md-editor-my-records', templateUrl: './my-records.component.html', styleUrls: ['./my-records.component.css'], standalone: true, - imports: [CommonModule, TranslateModule, RecordsListComponent], + imports: [ + CommonModule, + TranslateModule, + RecordsListComponent, + ResultsTableContainerComponent, + UiElementsModule, + RecordsCountComponent, + ButtonComponent, + MatIconModule, + ImportRecordComponent, + FeatureSearchModule, + ], }) export class MyRecordsComponent implements OnInit, OnDestroy { private sub: Subscription - private ownerId: string + ownerId: string + + @ViewChild('importRecordButton', { read: ElementRef }) + private importRecordButton!: ElementRef + @ViewChild('template') template!: TemplateRef + private overlayRef!: OverlayRef + + searchText$: Observable = + this.searchFacade.searchFilters$.pipe( + map((filters) => ('any' in filters ? (filters['any'] as string) : null)) + ) + + isImportMenuOpen = false constructor( - public fieldsService: FieldsService, - public searchFacade: SearchFacade, + private router: Router, + private activedRoute: ActivatedRoute, + private searchFacade: SearchFacade, + public searchService: SearchService, private platformService: PlatformServiceInterface, - private router: EditorRouterService + private fieldsService: FieldsService, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private cdr: ChangeDetectorRef ) {} ngOnInit() { this.searchFacade.resetSearch() + + const searchTerms = this.activedRoute.snapshot.queryParams['q'] ?? '' + + if (searchTerms) { + this.searchFacade.setFilters({ any: searchTerms }) + } + + let sort = (this.activedRoute.snapshot.queryParams['_sort'] as string) ?? '' + + if (sort) { + let ascDesc = '' + + if (sort?.charAt(0) === '-') { + ascDesc = 'desc' + sort = sort.slice(1, sort.length) + } else { + ascDesc = 'asc' + } + this.searchFacade.setSortBy([ascDesc as 'asc' | 'desc', sort]) + } + + this.searchFacade.setPageSize(15) + this.searchFacade.setConfigRequestFields(allSearchFields) + this.sub = this.platformService.getMe().subscribe((user) => { this.ownerId = user.id this.fieldsService @@ -38,15 +114,53 @@ export class MyRecordsComponent implements OnInit, OnDestroy { }) } - 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() + duplicateRecord(record: CatalogRecord) { + this.router + .navigate(['/duplicate', record.uniqueIdentifier]) + .catch((err) => console.error(err)) + } + + createRecord() { + this.router.navigate(['/create']).catch((err) => console.error(err)) + } + + duplicateExternalRecord() { + this.isImportMenuOpen = true + + const positionStrategy = this.overlay + .position() + .flexibleConnectedTo(this.importRecordButton) + .withPositions([ + { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top', + }, + ]) + + this.overlayRef = this.overlay.create({ + hasBackdrop: true, + backdropClass: 'cdk-overlay-transparent-backdrop', + positionStrategy: positionStrategy, + scrollStrategy: this.overlay.scrollStrategies.reposition(), + }) + + const portal = new TemplatePortal(this.template, this.viewContainerRef) + + this.overlayRef.attach(portal) + + this.overlayRef.backdropClick().subscribe(() => { + this.closeImportMenu() + }) + } + + closeImportMenu() { + if (this.overlayRef) { + this.isImportMenuOpen = false + this.overlayRef.dispose() + this.cdr.markForCheck() + } } ngOnDestroy(): void { 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 710f8cfa65..3a9a806e9b 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.html +++ b/apps/metadata-editor/src/app/records/records-list.component.html @@ -1,68 +1,17 @@ -
-
-
-
- - -
-
-

- {{ title }} -

-
- -
-
-
-
- {{ searchFacade.resultsHits$ | async }} - dashboard.records.publishedRecords - - {{ userCount }}  - - dashboard.records.users - - -
+
+ + +
+
-
- -
-
- -
-
-
-
+
diff --git a/apps/metadata-editor/src/app/records/records-list.component.ts b/apps/metadata-editor/src/app/records/records-list.component.ts index c930da2a02..96645eefda 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common' -import { Component, Input } from '@angular/core' +import { Component } from '@angular/core' import { MatIconModule } from '@angular/material/icon' import { Router } from '@angular/router' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' @@ -14,18 +14,6 @@ import { TranslateModule } from '@ngx-translate/core' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { RecordsCountComponent } from './records-count/records-count.component' -const includes = [ - 'uuid', - 'resourceTitleObject', - 'createDate', - 'changeDate', - 'userinfo', - 'cl_status', - 'isPublishedToAll', - 'link', - 'owner', -] - @Component({ selector: 'md-editor-records-list', templateUrl: './records-list.component.html', @@ -43,27 +31,16 @@ const includes = [ ], }) export class RecordsListComponent { - @Input() title: string - @Input() logo: string - @Input() linkToDatahub?: string - @Input() userCount = 0 - constructor( private router: Router, public searchFacade: SearchFacade, - public searchService: SearchService - ) { - this.searchFacade.setPageSize(15).setConfigRequestFields(includes) - } + private searchService: SearchService + ) {} paginate(page: number) { this.searchService.setPage(page) } - createRecord() { - this.router.navigate(['/create']) - } - editRecord(record: CatalogRecord) { this.router.navigate(['/edit', record.uniqueIdentifier]) } @@ -71,8 +48,4 @@ export class RecordsListComponent { duplicateRecord(record: CatalogRecord) { this.router.navigate(['/duplicate', record.uniqueIdentifier]) } - - showUsers() { - this.router.navigate(['/users/my-org']) - } } diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts deleted file mode 100644 index d39c0a6771..0000000000 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { SearchFacade, SearchService } from '@geonetwork-ui/feature/search' -import { SearchRecordsComponent } from './search-records-list.component' -import { - Component, - CUSTOM_ELEMENTS_SCHEMA, - EventEmitter, - importProvidersFrom, - Input, - Output, -} from '@angular/core' -import { TranslateModule } from '@ngx-translate/core' -import { BehaviorSubject } from 'rxjs' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' -import { CommonModule } from '@angular/common' -import { MatIconModule } from '@angular/material/icon' -import { UiInputsModule } from '@geonetwork-ui/ui/inputs' -import { By } from '@angular/platform-browser' -import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures' -import { Router } from '@angular/router' - -const results = [{ md: true }] -const currentPage = 5 -const totalPages = 25 - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-results-table-container', - template: '', - standalone: true, -}) -export class ResultsTableContainerComponent { - @Output() recordClick = new EventEmitter() - @Output() duplicateRecord = new EventEmitter() -} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-pagination-buttons', - template: '', - standalone: true, -}) -export class PaginationButtonsComponent { - @Input() currentPage = 1 - @Input() totalPages = 1 - @Input() hideButton = false - @Output() newCurrentPageEvent = new EventEmitter() -} - -@Component({ - selector: 'md-editor-records-count', - template: '', - standalone: true, -}) -export class RecordsCountComponent {} - -class SearchFacadeMock { - searchFilters$ = new BehaviorSubject({ - any: 'hello world', - }) - results$ = new BehaviorSubject(results) - currentPage$ = new BehaviorSubject(currentPage) - totalPages$ = new BehaviorSubject(totalPages) - resultsHits$ = new BehaviorSubject(1000) - setConfigRequestFields = jest.fn(() => this) - setPageSize = jest.fn(() => this) - setSortBy = jest.fn(() => this) - resetSearch = jest.fn() -} - -class SearchServiceMock { - setPage = jest.fn() - setSortBy = jest.fn() -} - -class RouterMock { - navigate = jest.fn(() => Promise.resolve(true)) -} - -describe('SearchRecordsComponent', () => { - let component: SearchRecordsComponent - let fixture: ComponentFixture - let router: Router - let searchService: SearchService - let searchFacade: SearchFacade - - beforeEach(() => { - TestBed.configureTestingModule({ - schemas: [CUSTOM_ELEMENTS_SCHEMA], - providers: [ - importProvidersFrom(TranslateModule.forRoot()), - { - provide: SearchFacade, - useClass: SearchFacadeMock, - }, - { - provide: Router, - useClass: RouterMock, - }, - { - provide: SearchService, - useClass: SearchServiceMock, - }, - ], - }).overrideComponent(SearchRecordsComponent, { - set: { - imports: [ - CommonModule, - TranslateModule, - MatIconModule, - ResultsTableContainerComponent, - PaginationButtonsComponent, - UiInputsModule, - RecordsCountComponent, - ], - }, - }) - fixture = TestBed.createComponent(SearchRecordsComponent) - router = TestBed.inject(Router) - searchService = TestBed.inject(SearchService) - searchFacade = TestBed.inject(SearchFacade) - - component = fixture.componentInstance - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - it('should map search filters to searchText$', (done) => { - component.searchText$.subscribe((text) => { - expect(text).toBe('hello world') - done() - }) - }) - - describe('when clicking createRecord', () => { - beforeEach(() => { - component.createRecord() - }) - - it('navigates to the create record page', () => { - expect(router.navigate).toHaveBeenCalledWith(['/create']) - }) - }) - - describe('when importing a record', () => { - beforeEach(() => { - component.duplicateExternalRecord() - }) - - it('sets isImportMenuOpen to true', () => { - expect(component.isImportMenuOpen).toBe(true) - }) - }) - - describe('when closing the import menu', () => { - let overlaySpy: any - - beforeEach(() => { - overlaySpy = { - dispose: jest.fn(), - } - component['overlayRef'] = overlaySpy - - component.closeImportMenu() - }) - - it('sets isImportMenuOpen to false', () => { - expect(component.isImportMenuOpen).toBe(false) - }) - - it('disposes the overlay', () => { - expect(overlaySpy.dispose).toHaveBeenCalled() - }) - }) - - describe('when search results', () => { - let table, pagination - beforeEach(() => { - table = fixture.debugElement.query( - By.directive(ResultsTableContainerComponent) - ).componentInstance - pagination = fixture.debugElement.query( - By.directive(PaginationButtonsComponent) - ).componentInstance - }) - it('displays record table', () => { - expect(table).toBeTruthy() - }) - it('displays pagination', () => { - expect(pagination).toBeTruthy() - expect(pagination.currentPage).toEqual(currentPage) - expect(pagination.totalPages).toEqual(totalPages) - }) - - describe('when click on a record', () => { - const uniqueIdentifier = 123 - const singleRecord = { - ...datasetRecordsFixture()[0], - uniqueIdentifier, - } - beforeEach(() => { - table.recordClick.emit(singleRecord) - }) - it('routes to record edition', () => { - expect(router.navigate).toHaveBeenCalledWith(['/edit', 123]) - }) - }) - - describe('when asking for record duplication', () => { - const uniqueIdentifier = 123 - const singleRecord = { - ...datasetRecordsFixture()[0], - uniqueIdentifier, - } - beforeEach(() => { - table.duplicateRecord.emit(singleRecord) - }) - it('routes to record duplication', () => { - expect(router.navigate).toHaveBeenCalledWith(['/duplicate', 123]) - }) - }) - - describe('when click on pagination', () => { - beforeEach(() => { - pagination.newCurrentPageEvent.emit(3) - }) - it('paginates', () => { - expect(searchService.setPage).toHaveBeenCalledWith(3) - }) - }) - }) -}) diff --git a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.css b/apps/metadata-editor/src/app/records/templates/templates.component.css similarity index 100% rename from apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.css rename to apps/metadata-editor/src/app/records/templates/templates.component.css diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.css b/apps/metadata-editor/src/app/records/templates/templates.component.html similarity index 100% rename from apps/metadata-editor/src/app/records/search-records/search-records-list.component.css rename to apps/metadata-editor/src/app/records/templates/templates.component.html diff --git a/apps/metadata-editor/src/app/records/my-library/my-library.component.spec.ts b/apps/metadata-editor/src/app/records/templates/templates.component.spec.ts similarity index 83% rename from apps/metadata-editor/src/app/records/my-library/my-library.component.spec.ts rename to apps/metadata-editor/src/app/records/templates/templates.component.spec.ts index f44522a61a..bf2c40dc05 100644 --- a/apps/metadata-editor/src/app/records/my-library/my-library.component.spec.ts +++ b/apps/metadata-editor/src/app/records/templates/templates.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MyLibraryComponent } from './my-library.component' +import { TemplatesComponent } from './templates.component' import { SearchFacade } from '@geonetwork-ui/feature/search' import { Component, importProvidersFrom } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' @@ -17,8 +17,8 @@ class SearchFacadeMock { } describe('MyLibraryComponent', () => { - let component: MyLibraryComponent - let fixture: ComponentFixture + let component: TemplatesComponent + let fixture: ComponentFixture let searchFacade: SearchFacade beforeEach(() => { @@ -30,7 +30,7 @@ describe('MyLibraryComponent', () => { useClass: SearchFacadeMock, }, ], - }).overrideComponent(MyLibraryComponent, { + }).overrideComponent(TemplatesComponent, { remove: { imports: [RecordsListComponent], }, @@ -39,7 +39,7 @@ describe('MyLibraryComponent', () => { }, }) searchFacade = TestBed.inject(SearchFacade) - fixture = TestBed.createComponent(MyLibraryComponent) + fixture = TestBed.createComponent(TemplatesComponent) component = fixture.componentInstance fixture.detectChanges() }) diff --git a/apps/metadata-editor/src/app/records/my-library/my-library.component.ts b/apps/metadata-editor/src/app/records/templates/templates.component.ts similarity index 74% rename from apps/metadata-editor/src/app/records/my-library/my-library.component.ts rename to apps/metadata-editor/src/app/records/templates/templates.component.ts index b3ad45edb6..56e02ba67b 100644 --- a/apps/metadata-editor/src/app/records/my-library/my-library.component.ts +++ b/apps/metadata-editor/src/app/records/templates/templates.component.ts @@ -5,13 +5,13 @@ import { RecordsListComponent } from '../records-list.component' import { SearchFacade } from '@geonetwork-ui/feature/search' @Component({ - selector: 'md-editor-my-library', - templateUrl: './my-library.component.html', - styleUrls: ['./my-library.component.css'], + selector: 'md-editor-templates', + templateUrl: './templates.component.html', + styleUrls: ['./templates.component.css'], standalone: true, imports: [CommonModule, TranslateModule, RecordsListComponent], }) -export class MyLibraryComponent { +export class TemplatesComponent { constructor(public searchFacade: SearchFacade) { this.searchFacade.resetSearch() } diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html index ea903d38fb..236d3c5c64 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html @@ -25,6 +25,7 @@
boolean = () => true + @Input() canDelete: (record: CatalogRecord) => boolean = () => true + @Output() recordClick = new EventEmitter() @Output() duplicateRecord = new EventEmitter() diff --git a/libs/ui/inputs/src/lib/badge/badge.component.html b/libs/ui/inputs/src/lib/badge/badge.component.html index cfa8d07b99..f567849682 100644 --- a/libs/ui/inputs/src/lib/badge/badge.component.html +++ b/libs/ui/inputs/src/lib/badge/badge.component.html @@ -11,8 +11,7 @@ type="light" *ngIf="removable" (buttonClick)="removeBadge()" - class="ml-1 -my-[0.4em] -mr-[0.45em]" - extraClass="border-0" + extraClass="text-xs border-0 px-0 py-0" style=" --gn-ui-button-padding: 0; --gn-ui-button-font-size: 0.8em; diff --git a/libs/ui/layout/src/lib/interactive-table/interactive-table.component.css b/libs/ui/layout/src/lib/interactive-table/interactive-table.component.css index df5d1f2711..c808088fc4 100644 --- a/libs/ui/layout/src/lib/interactive-table/interactive-table.component.css +++ b/libs/ui/layout/src/lib/interactive-table/interactive-table.component.css @@ -7,7 +7,7 @@ } .table-header-cell { - @apply text-gray-700 px-4 py-5 flex items-center truncate bg-white; + @apply text-gray-700 px-3 py-3 flex items-center truncate bg-white; } button.table-header-cell { diff --git a/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html b/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html index 143497cfbb..eb8d5b500f 100644 --- a/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html +++ b/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html @@ -1,5 +1,5 @@
-
+
- - - - - -
+
+ +
From 4abfaf1c925c2265845e0e969e455d327581ad63 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Mon, 23 Sep 2024 14:37:29 +0200 Subject: [PATCH 17/49] refactor(record-list): remove duplicate code --- .../search-header.component.spec.ts | 39 ++++++------------- .../all-records/all-records.component.ts | 15 +------ .../my-records/my-records.component.spec.ts | 9 ----- .../my-records/my-records.component.ts | 4 -- .../records/records-list.component.spec.ts | 15 ++++++- .../src/app/records/records-list.component.ts | 12 +++++- 6 files changed, 37 insertions(+), 57 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts index 1abf932325..4450682870 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts @@ -2,28 +2,15 @@ import { ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { SearchHeaderComponent } from './search-header.component' -import { BehaviorSubject, of } from 'rxjs' -import { barbieUserFixture } from '@geonetwork-ui/common/fixtures' +import { of } from 'rxjs' import { StoreModule } from '@ngrx/store' import { EffectsModule } from '@ngrx/effects' import { TranslateModule } from '@ngx-translate/core' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' import { SearchService } from '@geonetwork-ui/feature/search' - -class AvatarServiceInterfaceMock { - getPlaceholder = () => of('http://placeholder.com') - getProfileIcon = (hash: string) => of(`${hash}`) -} - -const me$ = new BehaviorSubject(barbieUserFixture()) -class PlatformServiceMock { - getMe = jest.fn(() => me$) -} - -class SearchServiceMock { - updateFilters = jest.fn() -} +import { MockProvider, MockProviders } from 'ng-mocks' +import { RouterFacade } from '@geonetwork-ui/feature/router' describe('SearchHeaderComponent', () => { let component: SearchHeaderComponent @@ -38,18 +25,14 @@ describe('SearchHeaderComponent', () => { TranslateModule.forRoot(), ], providers: [ - { - provide: AvatarServiceInterface, - useClass: AvatarServiceInterfaceMock, - }, - { - provide: PlatformServiceInterface, - useClass: PlatformServiceMock, - }, - { - provide: SearchService, - useClass: SearchServiceMock, - }, + MockProviders( + AvatarServiceInterface, + PlatformServiceInterface, + SearchService + ), + MockProvider(RouterFacade, { + currentRoute$: of(null), + }), ], }) .overrideComponent(SearchHeaderComponent, { diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts index e5d5804c1c..beb5980686 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts @@ -13,7 +13,7 @@ import { SearchService, } from '@geonetwork-ui/feature/search' import { TranslateModule } from '@ngx-translate/core' -import { ActivatedRoute, Router } from '@angular/router' +import { Router } from '@angular/router' import { RecordsCountComponent } from '../records-count/records-count.component' import { Observable } from 'rxjs' import { UiElementsModule } from '@geonetwork-ui/ui/elements' @@ -30,18 +30,6 @@ import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' import { RecordsListComponent } from '../records-list.component' import { map } from 'rxjs/operators' -export const allSearchFields = [ - 'uuid', - 'resourceTitleObject', - 'createDate', - 'changeDate', - 'userinfo', - 'cl_status', - 'isPublishedToAll', - 'link', - 'owner', -] - @Component({ selector: 'md-editor-all-records', templateUrl: './all-records.component.html', @@ -76,7 +64,6 @@ export class AllRecordsComponent { constructor( private router: Router, - private activedRoute: ActivatedRoute, public searchFacade: SearchFacade, public searchService: SearchService, private overlay: Overlay, 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 0a24ee9ad5..86a6db86fd 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 @@ -13,7 +13,6 @@ import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform. import { MockBuilder, MockInstance, MockProviders } from 'ng-mocks' import { ActivatedRoute, Router } from '@angular/router' import { TranslateModule } from '@ngx-translate/core' -import { allSearchFields } from '../all-records/all-records.component' describe('MyRecordsComponent', () => { MockInstance.scope() @@ -108,14 +107,6 @@ describe('MyRecordsComponent', () => { }) describe('filters on init', () => { - it('sets search fields', () => { - expect(searchFacade.setConfigRequestFields).toHaveBeenCalledWith( - allSearchFields - ) - }) - it('sets page size', () => { - expect(searchFacade.setPageSize).toHaveBeenCalledWith(15) - }) it('updates filters with owner', () => { expect(searchFacade.updateFilters).toHaveBeenCalledWith({ owner: user.id, 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 cbadf029df..4feccf9178 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 @@ -22,7 +22,6 @@ import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { Router } from '@angular/router' import { Overlay, OverlayRef } from '@angular/cdk/overlay' import { TemplatePortal } from '@angular/cdk/portal' -import { allSearchFields } from '../all-records/all-records.component' import { RecordsCountComponent } from '../records-count/records-count.component' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { MatIconModule } from '@angular/material/icon' @@ -65,9 +64,6 @@ export class MyRecordsComponent implements OnInit { ) {} ngOnInit() { - this.searchFacade.setConfigRequestFields(allSearchFields) - this.searchFacade.setPageSize(15) - this.platformService.getMe().subscribe((user) => { this.fieldsService .buildFiltersFromFieldValues({ owner: user.id }) diff --git a/apps/metadata-editor/src/app/records/records-list.component.spec.ts b/apps/metadata-editor/src/app/records/records-list.component.spec.ts index a48a0d8572..a6e006662e 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.spec.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { SearchFacade, SearchService } from '@geonetwork-ui/feature/search' -import { RecordsListComponent } from './records-list.component' +import { allSearchFields, RecordsListComponent } from './records-list.component' import { Component, EventEmitter, Input, Output } from '@angular/core' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { By } from '@angular/platform-browser' @@ -67,6 +67,7 @@ describe('RecordsListComponent', () => { let fixture: ComponentFixture let router: Router let searchService: SearchService + let searchFacade: SearchFacade beforeEach(() => { TestBed.configureTestingModule({ @@ -97,6 +98,7 @@ describe('RecordsListComponent', () => { }) router = TestBed.inject(Router) searchService = TestBed.inject(SearchService) + searchFacade = TestBed.inject(SearchFacade) fixture = TestBed.createComponent(RecordsListComponent) component = fixture.componentInstance fixture.detectChanges() @@ -106,6 +108,17 @@ describe('RecordsListComponent', () => { expect(component).toBeTruthy() }) + describe('on init', () => { + it('sets search fields', () => { + expect(searchFacade.setConfigRequestFields).toHaveBeenCalledWith( + allSearchFields + ) + }) + it('sets page size', () => { + expect(searchFacade.setPageSize).toHaveBeenCalledWith(15) + }) + }) + describe('when search results', () => { let table, pagination beforeEach(() => { diff --git a/apps/metadata-editor/src/app/records/records-list.component.ts b/apps/metadata-editor/src/app/records/records-list.component.ts index b92e7549f1..a286b499fe 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.ts @@ -13,8 +13,18 @@ import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { TranslateModule } from '@ngx-translate/core' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { RecordsCountComponent } from './records-count/records-count.component' -import { allSearchFields } from './all-records/all-records.component' +export const allSearchFields = [ + 'uuid', + 'resourceTitleObject', + 'createDate', + 'changeDate', + 'userinfo', + 'cl_status', + 'isPublishedToAll', + 'link', + 'owner', +] @Component({ selector: 'md-editor-records-list', templateUrl: './records-list.component.html', From 25f5ef6911a8d53db6dc2a70c8800fd63096cd97 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Mon, 23 Sep 2024 16:20:37 +0200 Subject: [PATCH 18/49] fix(dashboard): add y autoscroll for table and content --- .../src/app/dashboard/dashboard-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-page.component.html b/apps/metadata-editor/src/app/dashboard/dashboard-page.component.html index d491517c18..805e9a45c7 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-page.component.html +++ b/apps/metadata-editor/src/app/dashboard/dashboard-page.component.html @@ -7,7 +7,7 @@
-
+
From 7e683b45ef4069fe22938fd541c87decbf7ffb1b Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Mon, 23 Sep 2024 16:21:07 +0200 Subject: [PATCH 19/49] fix(search-header): move cross to right --- .../search-header/search-header.component.css | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.css b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.css index 542d168034..7a34507ebf 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.css +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.css @@ -1,8 +1,14 @@ :host ::ng-deep gn-ui-autocomplete input { @apply rounded-3xl shadow-none !py-2 !px-8 !pl-14 hover:shadow-none; } -:host ::ng-deep gn-ui-autocomplete button { - @apply border-none shadow-none hover:shadow-none text-gray-500 hover:text-gray-600 left-0 right-auto rounded-3xl; +:host ::ng-deep gn-ui-autocomplete gn-ui-button button { + @apply border-none shadow-none hover:shadow-none text-gray-500 hover:text-gray-600 rounded-3xl; +} +:host ::ng-deep gn-ui-autocomplete gn-ui-button:first-of-type button { + @apply right-0 left-auto; +} +:host ::ng-deep gn-ui-autocomplete gn-ui-button:last-of-type button { + @apply left-0 right-auto; } ::ng-deep .mdc-menu-surface.mat-mdc-autocomplete-panel { From f545bc44a04a14267509fca56365518e54b6b4f6 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Mon, 23 Sep 2024 16:34:30 +0200 Subject: [PATCH 20/49] fix unit tests --- .../src/lib/iso19115-3/read-parts.ts | 1 - .../src/lib/iso19139/read-parts.ts | 2 - .../src/lib/iso19139/write-parts.ts | 2 - libs/common/fixtures/src/index.ts | 1 + .../fixtures/src/lib/individual.fixtures.ts | 61 ++++ libs/common/fixtures/src/lib/user.fixtures.ts | 13 + .../editor/src/lib/+state/editor.reducer.ts | 2 +- .../form-field-contacts.component.spec.ts | 285 ++++++++---------- .../form-field-contacts.component.ts | 3 - 9 files changed, 203 insertions(+), 167 deletions(-) create mode 100644 libs/common/fixtures/src/lib/individual.fixtures.ts diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts b/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts index caca9ebb73..e06bf7b20b 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts @@ -229,7 +229,6 @@ export function readOwnerOrganization(rootEl: XmlElement): Organization { } export function readContacts(rootEl: XmlElement): Individual[] { - console.log('ici') return pipe( findNestedElements('mdb:contact', 'cit:CI_Responsibility'), mapArray(extractIndividuals()), diff --git a/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts b/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts index ebcd72e6ba..34dee9e287 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts @@ -600,7 +600,6 @@ export function readAbstract(rootEl: XmlElement): string { } export function readContacts(rootEl: XmlElement): Individual[] { - console.log('converter : readContacts') return pipe( findChildrenElement('gmd:contact', false), mapArray(findChildElement('gmd:CI_ResponsibleParty', false)), @@ -609,7 +608,6 @@ export function readContacts(rootEl: XmlElement): Individual[] { } export function readContactsForResource(rootEl: XmlElement): Individual[] { - console.log('converter : readContactsForResource') return pipe( findIdentification(), combine( diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts index 38ec9f59b4..8471232292 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts @@ -782,7 +782,6 @@ export function writeStatus(record: DatasetRecord, rootEl: XmlElement) { } export function writeContacts(record: CatalogRecord, rootEl: XmlElement) { - console.log('converter : writeContacts') pipe( removeChildrenByName('gmd:contact'), appendChildren( @@ -797,7 +796,6 @@ export function writeContactsForResource( record: CatalogRecord, rootEl: XmlElement ) { - console.log('converter : writeContactsForResource') pipe( findOrCreateIdentification(), removeChildrenByName('gmd:pointOfContact'), diff --git a/libs/common/fixtures/src/index.ts b/libs/common/fixtures/src/index.ts index 71987343f8..232576ae5d 100644 --- a/libs/common/fixtures/src/index.ts +++ b/libs/common/fixtures/src/index.ts @@ -10,6 +10,7 @@ export * from './lib/record-link.fixtures' export * from './lib/records.fixtures' export * from './lib/repository.fixtures' export * from './lib/user.fixtures' +export * from './lib/individual.fixtures' export * from './lib/user-feedbacks.fixtures' export * from './lib/editor' diff --git a/libs/common/fixtures/src/lib/individual.fixtures.ts b/libs/common/fixtures/src/lib/individual.fixtures.ts new file mode 100644 index 0000000000..149281d954 --- /dev/null +++ b/libs/common/fixtures/src/lib/individual.fixtures.ts @@ -0,0 +1,61 @@ +import { Individual } from '@geonetwork-ui/common/domain/model/record' +import { barbieIncOrganizationFixture } from './organisations.fixture' + +export const createIndividualFixture = ( + overrides: Partial = {} +): Individual => ({ + firstName: 'Arnaud', + lastName: 'Demaison', + email: 'a.demaison@geo2france.fr', + organization: barbieIncOrganizationFixture(), + role: 'point_of_contact', + ...overrides, +}) + +export const barbieIndividualFixture = (): Individual => + createIndividualFixture({ + firstName: 'Barbara', + lastName: 'Roberts', + email: 'barbie@email.org', + organization: barbieIncOrganizationFixture(), + role: 'point_of_contact', + }) + +export const someIndividualsFixture = (): Individual[] => [ + barbieIndividualFixture(), + { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@email.com', + organization: barbieIncOrganizationFixture(), + role: 'owner', + }, + { + firstName: 'Alice', + lastName: 'Smith', + email: 'alice.smith@workplace.com', + organization: barbieIncOrganizationFixture(), + role: 'author', + }, + { + firstName: 'Michael', + lastName: 'Johnson', + email: 'michael.j@company.org', + organization: barbieIncOrganizationFixture(), + role: 'contributor', + }, + { + firstName: 'Emma', + lastName: 'Williams', + email: 'emma.w@business.io', + organization: barbieIncOrganizationFixture(), + role: 'rights_holder', + }, + { + firstName: 'David', + lastName: 'Brown', + email: 'david.brown@enterprise.net', + organization: barbieIncOrganizationFixture(), + role: 'stakeholder', + }, +] diff --git a/libs/common/fixtures/src/lib/user.fixtures.ts b/libs/common/fixtures/src/lib/user.fixtures.ts index b6399d0476..5d110f9835 100644 --- a/libs/common/fixtures/src/lib/user.fixtures.ts +++ b/libs/common/fixtures/src/lib/user.fixtures.ts @@ -28,6 +28,19 @@ export const barbieUserFixture = (): UserModel => 'https://www.gravatar.com/avatar/dbdffd183622800bcf8587328daf43a6?d=mp', }) +export const johnDoeUserFixture = (): UserModel => + createUserFixture({ + id: '12345', + profile: 'User', + username: 'johndoe', + name: 'John', + surname: 'Doe', + email: 'johndoe@email.com', + organisation: 'Doe Enterprises', + profileIcon: + 'https://www.gravatar.com/avatar/5f6d74eabcb57186a12f7c8ba40b4c9f?d=mp', + }) + export const ghostUserFixture = (): UserModel => createUserFixture({ id: '161', diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.ts b/libs/feature/editor/src/lib/+state/editor.reducer.ts index dfb4835ae5..d2b4e96a5c 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.ts @@ -38,7 +38,7 @@ export const initialEditorState: EditorState = { saveError: null, changedSinceSave: false, editorConfig: DEFAULT_CONFIGURATION, - currentPage: 2, //todo: remove before merge + currentPage: 0, } const reducer = createReducer( diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts index 0f21913bb4..9bd44bc4e2 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts @@ -1,75 +1,71 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { FormFieldContactsComponent } from './form-field-contacts.component' +import { ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' -import { BehaviorSubject } from 'rxjs' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { BehaviorSubject, of } from 'rxjs' import { Individual, Organization, - Role, } from '@geonetwork-ui/common/domain/model/record' -import { ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core' -import { UserModel } from '@geonetwork-ui/common/domain/model/user' -import { CommonModule } from '@angular/common' +import { MockBuilder, MockInstance, MockProvider } from 'ng-mocks' import { TranslateModule } from '@ngx-translate/core' -import { ContactCardComponent } from '../../../contact-card/contact-card.component' import { - AutocompleteComponent, - DropdownSelectorComponent, - UiInputsModule, -} from '@geonetwork-ui/ui/inputs' -import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' -import { FormControl } from '@angular/forms' + barbieIncOrganizationFixture, + barbieUserFixture, + someIndividualsFixture, + someOrganizationsFixture, + someUsersFixture, +} from '@geonetwork-ui/common/fixtures' -const organizationBarbie: Organization = { - name: 'Barbie Inc.', -} +describe('FormFieldContactsComponent', () => { + MockInstance.scope() -const organizationGoogle: Organization = { - name: 'Google', -} - -class MockPlatformServiceInterface { - getUsers = jest.fn(() => new BehaviorSubject([])) -} - -class MockOrganizationsServiceInterface { - organisations$ = new BehaviorSubject([organizationBarbie, organizationGoogle]) -} - -describe('FormFieldContactsForResourceComponent', () => { let component: FormFieldContactsComponent let fixture: ComponentFixture + let platformServiceInterface: PlatformServiceInterface + let organizationsServiceInterface: OrganizationsServiceInterface + let changeDetectorRef: ChangeDetectorRef - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - FormFieldContactsComponent, - CommonModule, - TranslateModule.forRoot(), - UiInputsModule, - ContactCardComponent, - DropdownSelectorComponent, - ], + beforeEach(() => { + return MockBuilder(FormFieldContactsComponent) + }) + + const mockUsers = someUsersFixture() + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FormFieldContactsComponent, TranslateModule.forRoot()], providers: [ - { - provide: PlatformServiceInterface, - useClass: MockPlatformServiceInterface, - }, - { - provide: OrganizationsServiceInterface, - useClass: MockOrganizationsServiceInterface, - }, - ChangeDetectorRef, + MockProvider(OrganizationsServiceInterface), + MockProvider(PlatformServiceInterface, { + getUsers: jest.fn().mockReturnValue(of(mockUsers)), + }), + MockProvider(ChangeDetectorRef, { + markForCheck: jest.fn().mockReturnValue({}), + }), ], + }).overrideComponent(FormFieldContactsComponent, { + set: { + changeDetection: ChangeDetectionStrategy.Default, + }, }) - .overrideComponent(AutocompleteComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() fixture = TestBed.createComponent(FormFieldContactsComponent) component = fixture.componentInstance - component.control = new FormControl([]) + + changeDetectorRef = TestBed.inject(ChangeDetectorRef) + platformServiceInterface = TestBed.inject(PlatformServiceInterface) + organizationsServiceInterface = TestBed.inject( + OrganizationsServiceInterface + ) + + organizationsServiceInterface.organisations$ = new BehaviorSubject( + someOrganizationsFixture() + ) + + component.value = [] + fixture.detectChanges() }) @@ -77,148 +73,121 @@ describe('FormFieldContactsForResourceComponent', () => { expect(component).toBeTruthy() }) - describe('ngOnInit', () => { - it('should initialize organizations', async () => { - await component.ngOnInit() + describe('ngOnChanges', () => { + it('should initialize allOrganizations on first change', async () => { + const orgs: Organization[] = [{ name: 'Org1' }, { name: 'Org2' }] + organizationsServiceInterface.organisations$ = of(orgs) + + component.value = [] + await component.ngOnChanges({ + value: { + currentValue: [], + previousValue: undefined, + firstChange: true, + isFirstChange: () => true, + }, + }) expect(component.allOrganizations.size).toBe(2) + expect(component.allOrganizations.get('Org1')).toEqual({ name: 'Org1' }) + expect(component.allOrganizations.get('Org2')).toEqual({ name: 'Org2' }) }) - }) - - describe('addRoleToDisplay', () => { - it('should add role to display and filter roles to pick', () => { - const initialRolesToPick = [...component.rolesToPick] - const roleToAdd = initialRolesToPick[0] - - component.addRoleToDisplay(roleToAdd) - - expect(component.roleSectionsToDisplay).toContain(roleToAdd) - expect(component.rolesToPick).not.toContain(roleToAdd) - }) - }) - - describe('filterRolesToPick', () => { - it('should filter roles already in roleSectionsToDisplay', () => { - component.rolesToPick = ['custodian', 'owner'] as Role[] - component.roleSectionsToDisplay = ['custodian'] as Role[] - - component.filterRolesToPick() - - expect(component.rolesToPick).toEqual(['owner']) - }) - }) - - describe('updateContactsForRessource', () => { - it('should update contactsForRessourceByRole and contactsAsDynElemByRole', () => { - const mockContact: Individual = { - role: 'owner', - organization: { name: 'Org1' } as Organization, - } as Individual - component.allOrganizations.set('Org1', { name: 'Org1' } as Organization) - component.control.setValue([mockContact]) + it('should update contacts and mark for check when value changes', async () => { + jest.spyOn(component, 'updateContacts') - component.updateContacts() + component.value = [] + await component.ngOnChanges({ + value: { + currentValue: [], + previousValue: null, + firstChange: false, + isFirstChange: () => false, + }, + }) - expect(component.contactsForRessourceByRole.get('owner')).toEqual([ - mockContact, - ]) - expect(component.contactsAsDynElemByRole.get('owner').length).toBe(1) + expect(component.updateContacts).toHaveBeenCalled() }) }) - describe('manageRoleSectionsToDisplay', () => { - it('should add new roles to roleSectionsToDisplay', () => { - const mockContact: Individual = { - role: 'owner', - organization: { name: 'Org1' } as Organization, - } as Individual - - component.manageRoleSectionsToDisplay([mockContact]) - - expect(component.roleSectionsToDisplay).toContain('owner') + describe('updateContacts', () => { + beforeEach(async () => { + await component.ngOnChanges({ + value: { + currentValue: [], + previousValue: undefined, + firstChange: true, + isFirstChange: () => true, + }, + }) }) - }) - describe('removeContact', () => { - it('should remove contact at specified index', () => { - const mockContacts: Individual[] = [ + it('should update contacts with complete organization data', () => { + component.value = [ { - role: 'owner', - organization: { name: 'Org1' } as Organization, - } as Individual, + ...someIndividualsFixture()[0], + organization: { name: barbieIncOrganizationFixture().name }, + }, { - role: 'custodian', - organization: { name: 'Org2' } as Organization, - } as Individual, + ...someIndividualsFixture()[1], + organization: { name: barbieIncOrganizationFixture().name }, + }, ] - component.control.setValue(mockContacts) - component.removeContact(0) + component.updateContacts() - expect(component.control.value.length).toBe(1) - expect(component.control.value[0]).toEqual(mockContacts[1]) + expect(component.contacts.length).toBe(2) + expect(component.contacts[0].organization).toEqual( + barbieIncOrganizationFixture() + ) + expect(component.contacts[1].organization).toEqual( + barbieIncOrganizationFixture() + ) }) }) describe('handleContactsChanged', () => { - it('should update contacts based on reordered dynamic elements', () => { - const mockContacts: Individual[] = [ - { - role: 'owner', - organization: { name: 'Org1' } as Organization, - } as Individual, - { - role: 'owner', - organization: { name: 'Org2' } as Organization, - } as Individual, - ] + it('should update contacts and emit valueChange', () => { + const contacts: Individual[] = someIndividualsFixture() + jest.spyOn(component.valueChange, 'emit') - component.contactsForRessourceByRole.set('owner', [mockContacts[0]]) - component.contactsForRessourceByRole.set('owner', [mockContacts[1]]) + component.handleContactsChanged(contacts) - const reorderedElements = [ - { inputs: { contact: mockContacts[1] } } as any, - { inputs: { contact: mockContacts[0] } } as any, - ] - - component.handleContactsChanged(reorderedElements) - - const newControlValue = component.control.value - expect(newControlValue[0]).toEqual(mockContacts[1]) - expect(newControlValue[1]).toEqual(mockContacts[0]) + expect(component.contacts).toEqual(contacts) + expect(component.valueChange.emit).toHaveBeenCalledWith(contacts) }) }) describe('addContact', () => { - it('should add a new contact to the control value', () => { - const mockUser: UserModel = { - username: 'user1', - name: 'John', - surname: 'Doe', - organisation: 'Org1', - } as UserModel - + it('should add the contact and emit new contacts', () => { + const spy = jest.spyOn(component.valueChange, 'emit') + const mockUser = barbieUserFixture() component.allOrganizations.set('Org1', { name: 'Org1' } as Organization) - const initialContacts = component.control.value.length - component.addContact(mockUser, 'owner') + component.addContact(mockUser) - expect(component.control.value.length).toBe(initialContacts + 1) - expect(component.control.value[initialContacts].role).toBe('owner') - expect(component.control.value[initialContacts].organization.name).toBe( - 'Org1' - ) + expect(spy).toHaveBeenCalledWith([ + { + address: '', + email: 'barbie@email.org', + firstName: 'Barbara', + lastName: 'Roberts', + organization: { name: 'Barbie Inc.' } as Organization, + phone: '', + position: '', + role: 'point_of_contact', + }, + ]) }) }) describe('ngOnDestroy', () => { - it('should unsubscribe from all subscriptions', () => { - const subscriptionSpy = jest.spyOn(component.subscription, 'unsubscribe') + it('should unsubscribe from subscriptions', () => { + jest.spyOn(component.subscription, 'unsubscribe') component.ngOnDestroy() - expect(subscriptionSpy).toHaveBeenCalled() + expect(component.subscription.unsubscribe).toHaveBeenCalled() }) }) }) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts index 3261c014a5..909557f721 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts @@ -19,7 +19,6 @@ import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { Individual, Organization, - Role, } from '@geonetwork-ui/common/domain/model/record' import { TranslateModule } from '@ngx-translate/core' import { @@ -65,8 +64,6 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges { allUsers$: Observable - rolesToPick: Role[] = ['point_of_contact'] - allOrganizations: Map = new Map() constructor( From cddc36552354328d4e9c6c6a0d837886ec56050a Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Mon, 23 Sep 2024 21:46:45 +0200 Subject: [PATCH 21/49] refactor(my-records): remove unused method --- .../src/app/records/my-records/my-records.component.ts | 6 ------ 1 file changed, 6 deletions(-) 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 4feccf9178..50adce074b 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 @@ -73,12 +73,6 @@ export class MyRecordsComponent implements OnInit { }) } - duplicateRecord(record: CatalogRecord) { - this.router - .navigate(['/duplicate', record.uniqueIdentifier]) - .catch((err) => console.error(err)) - } - createRecord() { this.router.navigate(['/create']).catch((err) => console.error(err)) } From 25d27d412439d32b1fb95b0e00fe07389e6af1ea Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Tue, 24 Sep 2024 09:43:35 +0200 Subject: [PATCH 22/49] fix code review --- tailwind.base.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tailwind.base.css b/tailwind.base.css index a505177312..de0836deef 100644 --- a/tailwind.base.css +++ b/tailwind.base.css @@ -2,10 +2,6 @@ @tailwind components; @tailwind utilities; -:root { - --gn-ui-text-input-background-color: #f0f0f0; -} - /* PLEASE NOTE: all Tailwind components should be prefixed by `gn-ui` */ @layer components { .container-sm { From 14ec2ff1b40a84558e0b2fbefed98d9b029e0f72 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 4 Jul 2024 14:37:23 +0200 Subject: [PATCH 23/49] feat(converter): implement DCAT-AP converter with reading ability only Writing is mostly not implemented for now --- libs/api/metadata-converter/src/index.ts | 1 + .../src/lib/common/license.ts | 66 ++ .../metadata-converter/src/lib/common/url.ts | 15 + .../src/lib/dcat-ap/dcat-ap.converter.spec.ts | 104 +++ .../src/lib/dcat-ap/dcat-ap.converter.ts | 373 ++++++++ .../src/lib/dcat-ap/index.ts | 1 + .../src/lib/dcat-ap/namespaces.ts | 17 + .../src/lib/dcat-ap/read-parts.spec.ts | 106 +++ .../src/lib/dcat-ap/read-parts.ts | 370 ++++++++ .../src/lib/dcat-ap/utils/graph-utils.spec.ts | 104 +++ .../src/lib/dcat-ap/utils/graph-utils.ts | 80 ++ .../lib/dcat-ap/utils/individual-name.spec.ts | 23 + .../src/lib/dcat-ap/utils/individual-name.ts | 20 + .../src/lib/dcat-ap/utils/keyword.mapper.ts | 16 + .../src/lib/dcat-ap/utils/role.mapper.ts | 48 + .../src/lib/dcat-ap/utils/status.mapper.ts | 19 + .../dcat-ap/utils/update-frequency.mapper.ts | 67 ++ .../src/lib/dcat-ap/utils/uri.ts | 1 + .../src/lib/dcat-ap/write-parts.spec.ts | 5 + .../src/lib/dcat-ap/write-parts.ts | 62 ++ .../src/lib/find-converter.ts | 13 + .../src/lib/fixtures/eu.dcat-ap.records.ts | 272 ++++++ .../generic-dataset+eu.dcat-ap.survey.xml | 818 ++++++++++++++++++ .../src/lib/fixtures/opendataswiss.records.ts | 110 +++ .../src/lib/fixtures/sextant.records.ts | 85 ++ .../fixtures/vlaanderen.dcat-ap.dataset.xml | 112 +++ .../fixtures/vlaanderen.dcat-ap.records.ts | 73 ++ .../src/lib/model/record/contact.model.ts | 2 +- package.json | 1 + 29 files changed, 2983 insertions(+), 1 deletion(-) create mode 100644 libs/api/metadata-converter/src/lib/common/license.ts create mode 100644 libs/api/metadata-converter/src/lib/common/url.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.spec.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/index.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/namespaces.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/read-parts.spec.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/read-parts.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.spec.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/individual-name.spec.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/individual-name.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/keyword.mapper.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/role.mapper.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/status.mapper.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/update-frequency.mapper.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/uri.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/write-parts.spec.ts create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/write-parts.ts create mode 100644 libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts create mode 100644 libs/api/metadata-converter/src/lib/fixtures/generic-dataset+eu.dcat-ap.survey.xml create mode 100644 libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts create mode 100644 libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts create mode 100644 libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.dataset.xml create mode 100644 libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts diff --git a/libs/api/metadata-converter/src/index.ts b/libs/api/metadata-converter/src/index.ts index 3ce8f4d066..3bcc08bef1 100644 --- a/libs/api/metadata-converter/src/index.ts +++ b/libs/api/metadata-converter/src/index.ts @@ -2,4 +2,5 @@ export * from './lib/iso19139' export * from './lib/iso19115-3' export * from './lib/find-converter' export * from './lib/gn4' +export * from './lib/dcat-ap' export * from './lib/xml-utils' diff --git a/libs/api/metadata-converter/src/lib/common/license.ts b/libs/api/metadata-converter/src/lib/common/license.ts new file mode 100644 index 0000000000..04278d1887 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/common/license.ts @@ -0,0 +1,66 @@ +import { Constraint } from '@geonetwork-ui/common/domain/model/record' + +export function readLicenseFromString(text: string): Constraint { + const isPddl = /pddl|public domain dedication and licence/i.test(text) + const isOdbl = /odbl|open database license/i.test(text) + const isOdcBy = /odc-by|opendatacommons.org\/licenses\/by/i.test(text) + const isCcBySa = + /cc-by-sa|creative.*commons.*(by-sa|attribution.*share-alike)/i.test(text) + const isCcBy = /cc-by|cc by|creative.*commons.*(by|attribution)/i.test(text) + const isCc0 = /cc.?0|creative.*commons.*(zero|0)/i.test(text) + const isEtalabV2 = /etalab/i.test(text) && /v2|2\.0/i.test(text) + const isEtalab = /etalab|open.?licence|licence.?ouverte/i.test(text) + + if (isPddl) { + return { + text: 'Open Data Commons PDDL', + url: new URL('https://opendatacommons.org/licenses/pddl/'), + } + } else if (isOdbl) { + return { + text: 'Open Data Commons ODbL', + url: new URL('https://opendatacommons.org/licenses/odbl/'), + } + } else if (isOdcBy) { + return { + text: 'Open Data Commons ODC-By', + url: new URL('https://opendatacommons.org/licenses/by/'), + } + } else if (isCcBySa) { + return { + text: 'Creative Commons CC-BY-SA', + url: new URL('https://creativecommons.org/licenses/by-sa/4.0/legalcode'), + } + } else if (isCcBy) { + return { + text: 'Creative Commons CC-BY', + url: new URL('https://creativecommons.org/licenses/by/4.0/legalcode'), + } + } else if (isCc0) { + return { + text: 'Creative Commons CC-0', + url: new URL( + 'https://creativecommons.org/publicdomain/zero/1.0/legalcode' + ), + } + } else if (isEtalabV2) { + return { + text: 'Open Licence v2.0 (Etalab)', + url: new URL( + 'https://www.etalab.gouv.fr/wp-content/uploads/2017/04/ETALAB-Licence-Ouverte-v2.0.pdf' + ), + } + } else if (isEtalab) { + return { + text: 'Open Licence (Etalab)', + url: new URL( + 'https://www.etalab.gouv.fr/wp-content/uploads/2014/05/Licence_Ouverte.pdf' + ), + } + } + const url = /^https?:\/\//.test(text) ? new URL(text) : undefined + return { + text, + ...(url && { url }), + } +} diff --git a/libs/api/metadata-converter/src/lib/common/url.ts b/libs/api/metadata-converter/src/lib/common/url.ts new file mode 100644 index 0000000000..4d18d5c249 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/common/url.ts @@ -0,0 +1,15 @@ +/** + * Will return null if the URL is invalid, without throwing + * @param url + * @param location + */ +export function getAsValidUrl( + url: string, + location: string = window.location.toString() +): URL | null { + try { + return new URL(url, location) + } catch (e) { + return null + } +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.spec.ts b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.spec.ts new file mode 100644 index 0000000000..cfb449a273 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.spec.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { DcatApConverter } from './dcat-ap.converter' +import { EU_SURVEY_DATASET_RECORD } from '../fixtures/eu.dcat-ap.records' +// @ts-ignore +import EU_SURVEY_DATASET from '../fixtures/eu.dcat-ap.survey.xml' +// @ts-ignore +import SEXTANT_BATHYMETRY_DATASET from '../fixtures/sextant.dcat.bathymetry.xml' +// @ts-ignore +import OPENDATASWISS_DATASET from '../fixtures/opendataswiss.dcat-ap.dataset.xml' +// @ts-ignore +import VLAANDEREN_DATASET from '../fixtures/vlaanderen.dcat-ap.dataset.xml' +// @ts-ignore +import GENERIC_DATASET_PLUS_EU_SURVEY_DATASET from '../fixtures/generic-dataset+eu.dcat-ap.survey.xml' +import { GENERIC_DATASET_RECORD } from '../fixtures/generic.records' +import { graph, parse } from 'rdflib' +import { SEXTANT_BATHYMETRY_DATASET_RECORD } from '../fixtures/sextant.records' +import { OPENDATASWISS_DATASET_RECORD } from '../fixtures/opendataswiss.records' +import { VLAANDEREN_DATASET_RECORD } from '../fixtures/vlaanderen.dcat-ap.records' + +// this makes the xml go through the same formatting as the converter +async function formatRdf(rdfString: string) { + const dataStore = graph() + await new Promise((resolve) => + parse( + rdfString, + dataStore, + 'http://data.europa.eu/', + 'application/rdf+xml', + resolve + ) + ) + return dataStore.serialize('', 'application/rdf+xml', null) +} + +describe('DCAT-AP converter', () => { + let converter: DcatApConverter + + beforeEach(() => { + converter = new DcatApConverter() + }) + + describe('from RDF to model', () => { + it('produces the corresponding record (EU survey)', async () => { + const record = await converter.readRecord(EU_SURVEY_DATASET) + expect(record).toStrictEqual(EU_SURVEY_DATASET_RECORD) + }) + it('produces the corresponding record (Sextant)', async () => { + const record = await converter.readRecord(SEXTANT_BATHYMETRY_DATASET) + expect(record).toStrictEqual(SEXTANT_BATHYMETRY_DATASET_RECORD) + }) + it('produces the corresponding record (OpenDataSwiss)', async () => { + const record = await converter.readRecord(OPENDATASWISS_DATASET) + expect(record).toStrictEqual(OPENDATASWISS_DATASET_RECORD) + }) + it('produces the corresponding record (Vlaanderen)', async () => { + const record = await converter.readRecord(VLAANDEREN_DATASET) + expect(record).toStrictEqual(VLAANDEREN_DATASET_RECORD) + }) + }) + + describe('from model to RDF', () => { + // TODO: WRITE GENERIC DATASET FIXTURE + // it('produces a valid XML document based on a generic record', async () => { + // // parse and output xml to guarantee identical formatting + // const ref = xmlToString(parseXmlString(GENERIC_DATASET)) + // const xml = await converter.writeRecord(GENERIC_DATASET_RECORD) + // expect(xml).toStrictEqual(ref) + // }) + it('produces a valid XML document by combining a generic record and a third-party XML', async () => { + // parse and output xml to guarantee identical formatting + const ref = await formatRdf(GENERIC_DATASET_PLUS_EU_SURVEY_DATASET) + const output = await converter.writeRecord( + GENERIC_DATASET_RECORD, + EU_SURVEY_DATASET + ) + expect(output).toStrictEqual(ref) + }) + }) + + describe('idempotency', () => { + describe('with a third-party XML record', () => { + describe('when converting to a native record and back to XML', () => { + it('keeps the record unchanged (dataset)', async () => { + const backAndForth = await converter.writeRecord( + await converter.readRecord(EU_SURVEY_DATASET), + EU_SURVEY_DATASET + ) + expect(backAndForth).toStrictEqual(await formatRdf(EU_SURVEY_DATASET)) + }) + }) + }) + describe('with a native record', () => { + // FIXME: restore this test once we can write RDF XML as well! + describe.skip('when converting to XML and back', () => { + it('keeps the record unchanged', async () => { + const backAndForth = await converter.readRecord( + await converter.writeRecord(GENERIC_DATASET_RECORD) + ) + expect(backAndForth).toStrictEqual(GENERIC_DATASET_RECORD) + }) + }) + }) + }) +}) diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts new file mode 100644 index 0000000000..d48dc3f593 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts @@ -0,0 +1,373 @@ +import { + CatalogRecord, + CatalogRecordKeys, + DatasetRecord, + ServiceRecord, +} from '@geonetwork-ui/common/domain/model/record' +import { BaseConverter, MetadataMapperContext } from '../base.converter' +import { isEqual } from '../convert-utils' +import { + readAbstract, + readContacts, + readContactsForResource, + readKeywords, + readLandingPage, + readLicenses, + readOnlineResources, + readOwnerOrganization, + readRecordCreated, + readRecordUpdated, + readResourceCreated, + readResourceUpdated, + readSpatialExtents, + readTitle, + readTopics, + readUniqueIdentifier, +} from './read-parts' +import { writeAbstract, writeTitle, writeUniqueIdentifier } from './write-parts' +import { graph, NamedNode, Statement, Store, sym } from 'rdflib' +import { DCAT, RDF } from './namespaces' +import { BASE_URI } from './utils/uri' +import type { ContentType } from 'rdflib/lib/types' +import { loadGraph } from './utils/graph-utils' + +export class DcatApConverter extends BaseConverter { + protected readers: Record< + CatalogRecordKeys, + (dataStore: Store, catalogRecord: NamedNode) => unknown + > = { + uniqueIdentifier: readUniqueIdentifier, + title: readTitle, + abstract: readAbstract, + contacts: readContacts, + contactsForResource: readContactsForResource, + landingPage: readLandingPage, + onlineResources: readOnlineResources, + spatialExtents: readSpatialExtents, + keywords: readKeywords, + topics: readTopics, + recordUpdated: readRecordUpdated, + recordCreated: readRecordCreated, + resourceUpdated: readResourceUpdated, + resourceCreated: readResourceCreated, + ownerOrganization: readOwnerOrganization, + licenses: readLicenses, + // TODO + kind: () => 'dataset', + recordPublished: () => undefined, + resourcePublished: () => undefined, + legalConstraints: () => [], + securityConstraints: () => [], + otherConstraints: () => [], + status: () => undefined, + updateFrequency: () => 'unknown', + overviews: () => [], + lineage: () => undefined, + temporalExtents: () => [], + spatialRepresentation: () => undefined, + extras: () => undefined, + languages: () => [], + } + + protected writers: Record< + CatalogRecordKeys, + (record: CatalogRecord, dataStore: Store, catalogRecord: NamedNode) => void + > = { + uniqueIdentifier: writeUniqueIdentifier, + title: writeTitle, + abstract: writeAbstract, + // TODO + kind: () => undefined, + ownerOrganization: () => undefined, + recordUpdated: () => undefined, + recordCreated: () => undefined, + recordPublished: () => undefined, + resourceUpdated: () => undefined, + resourceCreated: () => undefined, + resourcePublished: () => undefined, + contacts: () => undefined, + contactsForResource: () => undefined, + keywords: () => undefined, + topics: () => undefined, + licenses: () => undefined, + legalConstraints: () => undefined, + securityConstraints: () => undefined, + otherConstraints: () => undefined, + status: () => undefined, + updateFrequency: () => undefined, + spatialRepresentation: () => undefined, + overviews: () => undefined, + lineage: () => undefined, + onlineResources: () => undefined, + temporalExtents: () => undefined, + spatialExtents: () => undefined, + extras: () => undefined, + landingPage: () => undefined, + languages: () => undefined, + } + + constructor( + private contentType: ContentType = 'application/rdf+xml', + ctx: MetadataMapperContext = new MetadataMapperContext() + ) { + super(ctx) + } + + async readRecord(document: string): Promise { + const dataStore = graph() + await loadGraph(dataStore, document, this.contentType) + + const catalogRecord = dataStore.the( + null, + null, + DCAT('CatalogRecord') + ) as NamedNode + + const uniqueIdentifier = this.readers['uniqueIdentifier']( + dataStore, + catalogRecord + ) + const kind = this.readers['kind'](dataStore, catalogRecord) + const ownerOrganization = this.readers['ownerOrganization']( + dataStore, + catalogRecord + ) + const title = this.readers['title'](dataStore, catalogRecord) + const abstract = this.readers['abstract'](dataStore, catalogRecord) + const contacts = this.readers['contacts'](dataStore, catalogRecord) + const contactsForResource = this.readers['contactsForResource']( + dataStore, + catalogRecord + ) + const recordUpdated = this.readers['recordUpdated']( + dataStore, + catalogRecord + ) + const recordCreated = this.readers['recordCreated']( + dataStore, + catalogRecord + ) + const recordPublished = this.readers['recordPublished']( + dataStore, + catalogRecord + ) + const resourceCreated = this.readers['resourceCreated']( + dataStore, + catalogRecord + ) + const resourceUpdated = this.readers['resourceUpdated']( + dataStore, + catalogRecord + ) + const resourcePublished = this.readers['resourcePublished']( + dataStore, + catalogRecord + ) + const keywords = this.readers['keywords'](dataStore, catalogRecord) + const topics = this.readers['topics'](dataStore, catalogRecord) + const legalConstraints = this.readers['legalConstraints']( + dataStore, + catalogRecord + ) + const otherConstraints = this.readers['otherConstraints']( + dataStore, + catalogRecord + ) + const securityConstraints = this.readers['securityConstraints']( + dataStore, + catalogRecord + ) + const licenses = this.readers['licenses'](dataStore, catalogRecord) + const overviews = this.readers['overviews'](dataStore, catalogRecord) + const landingPage = this.readers['landingPage'](dataStore, catalogRecord) + + if (kind === 'dataset') { + const status = this.readers['status'](dataStore, catalogRecord) + const spatialRepresentation = this.readers['spatialRepresentation']( + dataStore, + catalogRecord + ) + const spatialExtents = this.readers['spatialExtents']( + dataStore, + catalogRecord + ) + const temporalExtents = this.readers['temporalExtents']( + dataStore, + catalogRecord + ) + const lineage = this.readers['lineage'](dataStore, catalogRecord) + const onlineResources = this.readers['onlineResources']( + dataStore, + catalogRecord + ) + const updateFrequency = this.readers['updateFrequency']( + dataStore, + catalogRecord + ) + + return { + uniqueIdentifier, + kind, + languages: [], + ...(recordCreated && { recordCreated }), + ...(recordPublished && { recordPublished }), + recordUpdated, + ...(resourceCreated && { resourceCreated }), + ...(resourceUpdated && { resourceUpdated }), + ...(resourcePublished && { resourcePublished }), + status, + title, + abstract, + ownerOrganization, + contacts, + contactsForResource, + keywords, + topics, + licenses, + legalConstraints, + securityConstraints, + otherConstraints, + lineage, + ...(spatialRepresentation && { spatialRepresentation }), + overviews, + spatialExtents, + temporalExtents, + onlineResources, + updateFrequency, + ...(landingPage && { landingPage }), + } as DatasetRecord + } else { + const onlineResources = this.readers['onlineResources']( + dataStore, + catalogRecord + ) + return { + uniqueIdentifier, + kind, + languages: [], + ...(recordCreated && { recordCreated }), + ...(recordPublished && { recordPublished }), + recordUpdated, + ...(resourceCreated && { resourceCreated }), + ...(resourceUpdated && { resourceUpdated }), + ...(resourcePublished && { resourcePublished }), + title, + abstract, + ownerOrganization, + contacts, + contactsForResource, + keywords, + topics, + licenses, + legalConstraints, + securityConstraints, + otherConstraints, + overviews, + onlineResources, + ...(landingPage && { landingPage }), + } as ServiceRecord + } + } + + async writeRecord( + record: CatalogRecord, + reference?: string + ): Promise { + const dataStore = graph() + let fieldChanged: (name: string) => boolean + if (reference) { + const originalRecord = await this.readRecord(reference) + await loadGraph( + dataStore, + reference, + this.contentType, + DCAT('CatalogRecord').value + ) + + fieldChanged = (name: string) => { + return originalRecord !== null + ? !isEqual(record[name], originalRecord[name]) + : true + } + } else { + fieldChanged = () => true + } + + let recordNode = dataStore.the( + null, + RDF('type'), + DCAT('CatalogRecord') + ) as NamedNode + if (!recordNode) { + const statement = dataStore.add( + sym(`${BASE_URI}record/${record.uniqueIdentifier}`), + RDF('type'), + DCAT('CatalogRecord') + ) as Statement + recordNode = statement.subject as NamedNode + } + + fieldChanged('uniqueIdentifier') && + this.writers['uniqueIdentifier'](record, dataStore, recordNode) + fieldChanged('kind') && this.writers['kind'](record, dataStore, recordNode) + + fieldChanged('contacts') && + this.writers['contacts'](record, dataStore, recordNode) + fieldChanged('ownerOrganization') && + this.writers['ownerOrganization'](record, dataStore, recordNode) + + fieldChanged('recordUpdated') && + this.writers['recordUpdated'](record, dataStore, recordNode) + fieldChanged('recordCreated') && + this.writers['recordCreated'](record, dataStore, recordNode) + fieldChanged('recordPublished') && + this.writers['recordPublished'](record, dataStore, recordNode) + + fieldChanged('title') && + this.writers['title'](record, dataStore, recordNode) + fieldChanged('abstract') && + this.writers['abstract'](record, dataStore, recordNode) + + fieldChanged('resourceCreated') && + this.writers['resourceCreated'](record, dataStore, recordNode) + fieldChanged('resourcePublished') && + this.writers['resourcePublished'](record, dataStore, recordNode) + fieldChanged('resourceUpdated') && + this.writers['resourceUpdated'](record, dataStore, recordNode) + + fieldChanged('contactsForResource') && + this.writers['contactsForResource'](record, dataStore, recordNode) + + fieldChanged('keywords') && + this.writers['keywords'](record, dataStore, recordNode) + fieldChanged('topics') && + this.writers['topics'](record, dataStore, recordNode) + fieldChanged('legalConstraints') && + this.writers['legalConstraints'](record, dataStore, recordNode) + fieldChanged('securityConstraints') && + this.writers['securityConstraints'](record, dataStore, recordNode) + fieldChanged('licenses') && + this.writers['licenses'](record, dataStore, recordNode) + fieldChanged('otherConstraints') && + this.writers['otherConstraints'](record, dataStore, recordNode) + fieldChanged('onlineResources') && + this.writers['onlineResources'](record, dataStore, recordNode) + + if (record.kind === 'dataset') { + fieldChanged('status') && + this.writers['status'](record, dataStore, recordNode) + fieldChanged('updateFrequency') && + this.writers['updateFrequency'](record, dataStore, recordNode) + fieldChanged('spatialRepresentation') && + this.writers['spatialRepresentation'](record, dataStore, recordNode) + fieldChanged('overviews') && + this.writers['overviews'](record, dataStore, recordNode) + fieldChanged('temporalExtents') && + this.writers['temporalExtents'](record, dataStore, recordNode) + fieldChanged('lineage') && + this.writers['lineage'](record, dataStore, recordNode) + } + + return dataStore.serialize(undefined, this.contentType, null, {}) + } +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/index.ts b/libs/api/metadata-converter/src/lib/dcat-ap/index.ts new file mode 100644 index 0000000000..e9cf09191e --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/index.ts @@ -0,0 +1 @@ +export * from './dcat-ap.converter' diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/namespaces.ts b/libs/api/metadata-converter/src/lib/dcat-ap/namespaces.ts new file mode 100644 index 0000000000..df291945d1 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/namespaces.ts @@ -0,0 +1,17 @@ +import { Namespace } from 'rdflib' + +export const RDF = Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') +export const RDFS = Namespace('http://www.w3.org/2000/01/rdf-schema#') +export const FOAF = Namespace('http://xmlns.com/foaf/0.1/') +export const XSD = Namespace('http://www.w3.org/2001/XMLSchema#') +export const DCAT = Namespace('http://www.w3.org/ns/dcat#') +export const DCTERMS = Namespace('http://purl.org/dc/terms/') +export const SKOS = Namespace('http://www.w3.org/2004/02/skos/core#') +export const SCHEMA_ORG = Namespace('http://schema.org/') +export const SPDX = Namespace('https://spdx.org/rdf/terms/#') +export const ADMS = Namespace('http://www.w3.org/ns/adms#') +export const DQV = Namespace('http://www.w3.org/ns/dqv#') +export const OWL = Namespace('http://www.w3.org/2002/07/owl#') +export const VCARD = Namespace('http://www.w3.org/2006/vcard/ns#') +export const TIME = Namespace('http://www.w3.org/2006/time#') +export const LOCN = Namespace('http://www.w3.org/ns/locn#') diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.spec.ts b/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.spec.ts new file mode 100644 index 0000000000..fb5e7c9300 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.spec.ts @@ -0,0 +1,106 @@ +import { DcatApConverter } from './dcat-ap.converter' +import { loadGraph } from './utils/graph-utils' +import { graph, NamedNode } from 'rdflib' +import { DCAT, RDF } from './namespaces' +import { mapOnlineResource } from './read-parts' + +describe('read parts', () => { + describe('find Dataset if not part of a record', () => { + const doc = ` + + + 2022-04-28 + 2022-04-28 + aantal ton gelost op de waterwegen + hoeveelheid goederen gelost langs waterwegen beheerd door De Vlaamse Waterweg uitgedrukt in ton + +` + it('reads title and abstract but not unique identifier', async () => { + const record = await new DcatApConverter().readRecord(doc) + expect(record).toMatchObject({ + uniqueIdentifier: undefined, + title: 'aantal ton gelost op de waterwegen', + abstract: + 'hoeveelheid goederen gelost langs waterwegen beheerd door De Vlaamse Waterweg uitgedrukt in ton', + }) + }) + }) + + describe('mapOnlineResource', () => { + describe('service distribution', () => { + const doc = ` +@prefix adms: . +@prefix dcat: . +@prefix dct: . +@prefix rdf: . +@prefix rdfs: . + +[] a dcat:Distribution ; + dct:accessRights ; + dct:title "NoiseContours_air_lnight"@en ; + dct:description "Description of the distribution" ; + dct:format ; + dct:license [ a dct:LicenseDocument ; + rdfs:label "The full noise contour maps data set is in principle only available for EEA internal use. A public version of the data set may in the future be available, excluding those data sets for which there is any limitation or restriction in their use (beyond acknowledgement)."@en ] ; + adms:representationTechnique ; + dcat:accessService [ a dcat:DataService ; + dct:conformsTo ; + dcat:endpointDescription ; + dcat:endpointURL ] ; + dcat:accessURL .` + + it('reads service distribution', async () => { + const dataStore = graph() + await loadGraph(dataStore, doc, 'text/n3', DCAT('CatalogRecord').value) + const node = dataStore.the( + null, + RDF('type'), + DCAT('Distribution') + ) as NamedNode + expect(mapOnlineResource(dataStore, node)).toEqual({ + name: 'NoiseContours_air_lnight', + description: 'Description of the distribution', + type: 'service', + accessServiceProtocol: 'wms', + url: new URL( + 'https://noise.discomap.eea.europa.eu/arcgis/services/noiseStoryMap/NoiseContours_air_lnight/ImageServer/WMSServer?request=GetCapabilities&service=WMS' + ), + }) + }) + }) + describe('download distribution', () => { + const doc = ` +@prefix dcat: . +@prefix dct: . + +[] a dcat:Distribution ; + dcat:downloadURL ; + dct:title "CSV distribution of imaginary dataset 001"@en ; + dct:description "A more complete description" ; + dct:title "distribución en CSV del conjunto de datos imaginario 001"@es ; + dcat:mediaType ; + dcat:byteSize "5120"^^xsd:nonNegativeInteger ;` + + it('reads download distribution', async () => { + const dataStore = graph() + await loadGraph(dataStore, doc, 'text/n3', DCAT('CatalogRecord').value) + const node = dataStore.the( + null, + RDF('type'), + DCAT('Distribution') + ) as NamedNode + expect(mapOnlineResource(dataStore, node)).toEqual({ + name: 'CSV distribution of imaginary dataset 001', + description: 'A more complete description', + type: 'download', + url: new URL( + 'https://noise.discomap.eea.europa.eu/arcgis/services/noiseStoryMap/NoiseContours_air_lnight/ImageServer/WMSServer?request=GetCapabilities&service=WMS' + ), + }) + }) + }) + }) +}) diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.ts b/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.ts new file mode 100644 index 0000000000..1e496c84ec --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.ts @@ -0,0 +1,370 @@ +import { NamedNode, Statement, Store } from 'rdflib' +import { DCAT, DCTERMS, FOAF, LOCN, RDF, SKOS, VCARD } from './namespaces' +import { findNodeLocalized } from './utils/graph-utils' +import { + DatasetDownloadDistribution, + DatasetServiceDistribution, + DatasetSpatialExtent, + Individual, + Keyword, + OnlineLinkResource, + OnlineResource, + Organization, +} from '@geonetwork-ui/common/domain/model/record' +import { getAsValidUrl } from '../common/url' +import { fullNameToParts } from '../iso19139/utils/individual-name' +import { readLicenseFromString } from '../common/license' +import { matchProtocol } from '../common/distribution.mapper' + +function getDataset(dataStore: Store, recordNode: NamedNode): NamedNode { + return (dataStore.the(recordNode, FOAF('primaryTopic'), null) || + dataStore.the(null, RDF('type'), DCAT('Dataset'))) as NamedNode +} + +export function readUniqueIdentifier( + dataStore: Store, + recordNode: NamedNode +): string { + return dataStore.the(recordNode, DCTERMS('identifier'), null)?.value +} + +export function readTitle(dataStore: Store, recordNode: NamedNode): string { + const dataset = getDataset(dataStore, recordNode) + return findNodeLocalized(dataStore, dataset, DCTERMS('title'), null, 'en') + ?.value +} + +export function readAbstract(dataStore: Store, recordNode: NamedNode): string { + const dataset = getDataset(dataStore, recordNode) + return findNodeLocalized( + dataStore, + dataset, + DCTERMS('description'), + null, + 'en' + )?.value +} + +function mapContactFromStatement( + dataStore: Store, + statement: Statement +): Individual { + const contactNode = statement.object as NamedNode + const fullName = + dataStore.the(contactNode, VCARD('fn'), null) ?? + dataStore.the(contactNode, VCARD('title'), null) ?? + dataStore.the(contactNode, VCARD('organisation-name'), null) + let firstName, lastName + if (fullName) { + const parts = fullNameToParts(fullName.value) + firstName = parts[0] + lastName = parts[1] + } + const role = dataStore.the(contactNode, VCARD('role'), null) + const email = dataStore.the(contactNode, VCARD('hasEmail'), null) + const emailValue = email + ? email.value.replace(/^mailto:/, '') + : 'missing@missing.com' + return { + role: role?.value ?? 'pointOfContact', + email: emailValue, + ...(firstName && { firstName }), + ...(lastName && { lastName }), + } +} + +export function readContacts( + dataStore: Store, + recordNode: NamedNode +): Individual[] { + const statements = dataStore.statementsMatching( + recordNode, + DCAT('contactPoint'), + null + ) + return statements.map((s) => mapContactFromStatement(dataStore, s)) +} + +export function readContactsForResource( + dataStore: Store, + recordNode: NamedNode +): Individual[] { + const dataset = getDataset(dataStore, recordNode) + const statements = dataStore.statementsMatching( + dataset, + DCAT('contactPoint'), + null + ) + return statements.map((s) => mapContactFromStatement(dataStore, s)) +} + +export function readLandingPage(dataStore: Store, recordNode: NamedNode): URL { + const dataset = getDataset(dataStore, recordNode) + const landingPage = dataStore.the(dataset, DCAT('landingPage'), null) + return landingPage !== null ? getAsValidUrl(landingPage.value) : undefined +} + +function readOnlineLinkResource( + dataStore: Store, + distributionNode: NamedNode +): OnlineLinkResource { + const accessUrl = dataStore.the(distributionNode, DCAT('accessURL'), null) + const description = findNodeLocalized( + dataStore, + distributionNode, + DCTERMS('description'), + null, + 'en' + ) + const title = findNodeLocalized( + dataStore, + distributionNode, + DCTERMS('title'), + null, + 'en' + ) + return { + url: getAsValidUrl(accessUrl?.value), + type: 'link', + ...(title && { name: title?.value }), + ...(description && { description: description?.value }), + } +} + +function readDownloadDistribution( + dataStore: Store, + distributionNode: NamedNode +): DatasetDownloadDistribution { + const downloadUrl = dataStore.the(distributionNode, DCAT('downloadURL'), null) + const description = findNodeLocalized( + dataStore, + distributionNode, + DCTERMS('description'), + null, + 'en' + ) + const title = findNodeLocalized( + dataStore, + distributionNode, + DCTERMS('title'), + null, + 'en' + ) + // todo mime type + return { + url: getAsValidUrl(downloadUrl?.value), + type: 'download', + ...(title && { name: title?.value }), + ...(description && { description: description?.value }), + } +} + +function readServiceDistribution( + dataStore: Store, + distributionNode: NamedNode +): DatasetServiceDistribution { + const service = dataStore.the( + distributionNode, + DCAT('accessService'), + null + ) as NamedNode + const conformsTo = dataStore.the(service, DCTERMS('conformsTo'), null) + const accessUrl = dataStore.the(distributionNode, DCAT('accessURL'), null) + const description = findNodeLocalized( + dataStore, + distributionNode, + DCTERMS('description'), + null, + 'en' + ) + const title = findNodeLocalized( + dataStore, + distributionNode, + DCTERMS('title'), + null, + 'en' + ) + return { + url: getAsValidUrl(accessUrl?.value), + type: 'service', + accessServiceProtocol: matchProtocol(conformsTo?.value), + ...(title && { name: title?.value }), + ...(description && { description: description?.value }), + } +} + +export function mapOnlineResource( + dataStore: Store, + distributionNode: NamedNode +): OnlineResource { + if (dataStore.holds(distributionNode, DCAT('accessService'), null)) { + const service = dataStore.the( + distributionNode, + DCAT('accessService'), + null + ) as NamedNode + const conformsTo = dataStore.the(service, DCTERMS('conformsTo'), null) + if (conformsTo) { + return readServiceDistribution(dataStore, distributionNode) + } + } + if (dataStore.holds(distributionNode, DCAT('downloadURL'), null)) { + return readDownloadDistribution(dataStore, distributionNode) + } + return readOnlineLinkResource(dataStore, distributionNode) +} + +export function readOnlineResources( + dataStore: Store, + recordNode: NamedNode +): OnlineResource[] { + const dataset = getDataset(dataStore, recordNode) + const statements = dataStore.statementsMatching( + dataset, + DCAT('distribution'), + null + ) + return statements.map((statement) => + mapOnlineResource(dataStore, statement.object as NamedNode) + ) +} + +export function readSpatialExtents( + dataStore: Store, + recordNode: NamedNode +): DatasetSpatialExtent[] { + const dataset = getDataset(dataStore, recordNode) + const statements = dataStore.statementsMatching( + dataset, + DCTERMS('spatial'), + null + ) + return statements + .map((statement) => { + const geom = dataStore.the( + statement.object as NamedNode, + LOCN('geometry'), + null + ) + if (!geom) + return { + description: statement.object.value, + } + return { + geometry: JSON.parse(geom.value), + } + }) + .filter((statement) => !!statement) +} + +export function readKeywords( + dataStore: Store, + recordNode: NamedNode +): Keyword[] { + const dataset = getDataset(dataStore, recordNode) + const statements = dataStore.statementsMatching( + dataset, + DCAT('keyword'), + null + ) + return statements.map((statement) => { + return { + label: statement.object.value, + type: 'theme', + } + }) +} + +export function readTopics(dataStore: Store, recordNode: NamedNode): string[] { + const dataset = getDataset(dataStore, recordNode) + const statements = dataStore.statementsMatching(dataset, DCAT('theme'), null) + return statements.map((statement) => { + const prefLabel = dataStore.the( + statement.object as NamedNode, + SKOS('prefLabel'), + null + ) + return prefLabel?.value ?? statement.object.value + }) +} + +export function readRecordCreated( + dataStore: Store, + recordNode: NamedNode +): Date { + const dateString = dataStore.the(recordNode, DCTERMS('issued'), null)?.value + if (dateString) return new Date(dateString) + return null +} + +export function readRecordUpdated( + dataStore: Store, + recordNode: NamedNode +): Date { + const dateString = dataStore.the(recordNode, DCTERMS('modified'), null)?.value + if (dateString) return new Date(dateString) + return null +} + +export function readResourceCreated( + dataStore: Store, + recordNode: NamedNode +): Date { + const dataset = getDataset(dataStore, recordNode) + const dateString = dataStore.the(dataset, DCTERMS('issued'), null)?.value + if (dateString) return new Date(dateString) + return null +} + +export function readResourceUpdated( + dataStore: Store, + recordNode: NamedNode +): Date { + const dataset = getDataset(dataStore, recordNode) + const dateString = dataStore.the(dataset, DCTERMS('modified'), null)?.value + if (dateString) return new Date(dateString) + return null +} + +export function readOwnerOrganization(dataStore: Store): Organization { + const publisherStatements = dataStore.statementsMatching( + null, + DCTERMS('publisher'), + null + ) + if (!publisherStatements.length) return null + const publisher = publisherStatements[0].object as NamedNode + const nameNode = dataStore.the(publisher, FOAF('name'), null) + const name = nameNode ? nameNode.value : publisher.value + return { + name, + } +} + +export function readLicenses(dataStore: Store, recordNode: NamedNode) { + const dataset = getDataset(dataStore, recordNode) + const distributions = dataStore.statementsMatching( + dataset, + DCAT('distribution'), + null + ) + return distributions + .map( + (statement) => + dataStore.the( + statement.object as NamedNode, + DCTERMS('license'), + null + ) as NamedNode + ) + .filter((license) => !!license) + .map((s) => readLicenseFromString(s.value)) + .filter( + (license, index, array) => + array.findIndex( + (l) => + l.url?.toString() === license.url?.toString() && + l.text === license.text + ) === index + ) +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.spec.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.spec.ts new file mode 100644 index 0000000000..6d364e8a26 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.spec.ts @@ -0,0 +1,104 @@ +import { NamedNode, Store } from 'rdflib' +import { findNodeLocalized, loadGraph } from './graph-utils' +import { DCAT, DCTERMS, RDF } from '../namespaces' + +describe('graph utils', () => { + describe('findNodeLocalized', () => { + it('finds a node with a specific language if present', async () => { + const dataStore = new Store() + await loadGraph( + dataStore, + ` + + + aantal ton gelost op de waterwegen + guten morgen + bla bla + english title + +`, + 'application/rdf+xml' + ) + + const dataset = dataStore.the( + null, + RDF('type'), + DCAT('Dataset') + ) as NamedNode + const result = findNodeLocalized( + dataStore, + dataset, + DCTERMS('title'), + null, + 'en' + ) + expect(result?.value).toBe('english title') + }) + it('finds a node with no language if given language is absent', async () => { + const dataStore = new Store() + await loadGraph( + dataStore, + ` + + + aantal ton gelost op de waterwegen + guten morgen + bla bla + +`, + 'application/rdf+xml' + ) + + const dataset = dataStore.the( + null, + RDF('type'), + DCAT('Dataset') + ) as NamedNode + const result = findNodeLocalized( + dataStore, + dataset, + DCTERMS('title'), + null, + 'en' + ) + expect(result?.value).toBe('bla bla') + }) + it('finds a node with any other language if no node without language', async () => { + const dataStore = new Store() + await loadGraph( + dataStore, + ` + + + aantal ton gelost op de waterwegen + guten morgen + +`, + 'application/rdf+xml' + ) + + const dataset = dataStore.the( + null, + RDF('type'), + DCAT('Dataset') + ) as NamedNode + const result = findNodeLocalized( + dataStore, + dataset, + DCTERMS('title'), + null, + 'en' + ) + expect(result?.value).toBe('aantal ton gelost op de waterwegen') + }) + }) +}) diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.ts new file mode 100644 index 0000000000..82d97d042a --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.ts @@ -0,0 +1,80 @@ +import { lit, Literal, Node, parse, Statement, Store } from 'rdflib' +import { Quad_Object, Quad_Predicate, Quad_Subject } from 'rdflib/lib/tf-types' +import { ContentType, ObjectType } from 'rdflib/lib/types' +import { BASE_URI } from './uri' + +export function findNodeLocalized( + dataStore: Store, + subject: Quad_Subject, + predicate: Quad_Predicate, + object: Quad_Object, + language: string +): Node | null { + const literals = dataStore + .each(subject, predicate, object) + .filter((node): node is Literal => node instanceof Literal) + const matchingLang = literals.find((node) => + node.language.startsWith(language) + ) + const noLang = literals.find((node) => !node.language) + const anyLang = literals[0] + return matchingLang ?? noLang ?? anyLang +} + +export function getOrAddStatement( + dataStore: Store, + subject: Quad_Subject, + predicate: Quad_Predicate, + object: Quad_Object | string +): Statement { + let statement = dataStore.statementsMatching( + subject, + predicate, + typeof object === 'string' ? null : object + )[0] + if (!statement) { + statement = dataStore.add(subject, predicate, object) as Statement + } + return statement +} + +export function getOrAddLocalizedObject( + dataStore: Store, + subject: Quad_Subject, + predicate: Quad_Predicate, + object: Quad_Object | string, + language: string +) { + let statement = dataStore + .statementsMatching( + subject, + predicate, + typeof object === 'string' ? null : object + ) + .filter( + (statement) => + statement.object instanceof Literal && + statement.object.language.startsWith(language) + )[0] + if (!statement) { + statement = dataStore.add(subject, predicate, object) as Statement + ;(statement.object as Literal).language = language + } else { + statement.object = + typeof object === 'string' + ? lit(object, language) + : (object as ObjectType) + } + return statement +} + +export function loadGraph( + dataStore: Store, + document: string, + contentType: ContentType, + base = BASE_URI +) { + return new Promise((resolve) => + parse(document, dataStore, base, contentType, resolve) + ) +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/individual-name.spec.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/individual-name.spec.ts new file mode 100644 index 0000000000..18fd1b68d9 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/individual-name.spec.ts @@ -0,0 +1,23 @@ +import { fullNameToParts, namePartsToFull } from './individual-name' + +describe('individual name utils', () => { + it('fullNameToParts', () => { + expect(fullNameToParts('John Doe')).toEqual(['John', 'Doe']) + expect(fullNameToParts('John')).toEqual(['John', null]) + expect(fullNameToParts(' John Jim Doe Blah ')).toEqual([ + 'John', + 'Jim Doe Blah', + ]) + }) + it('namePartsToFull', () => { + expect(namePartsToFull('John', 'Doe')).toEqual('John Doe') + expect(namePartsToFull('John', null)).toEqual('John') + expect(namePartsToFull(null, 'Doe')).toEqual('Doe') + expect(namePartsToFull(null, null)).toEqual(null) + expect(namePartsToFull('', ' ')).toEqual(null) + expect(namePartsToFull('John', 'Doe Blah')).toEqual('John Doe Blah') + expect(namePartsToFull(' John Jim ', ' Doe Blah ')).toEqual( + 'John Jim Doe Blah' + ) + }) +}) diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/individual-name.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/individual-name.ts new file mode 100644 index 0000000000..ef3ddb7b1a --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/individual-name.ts @@ -0,0 +1,20 @@ +/** + * Parts are [firstName, lastName] + * Second part will be null if no separation could be done + * @param fullName + */ +export function fullNameToParts(fullName: string): [string, string | null] { + const parts = fullName.trim().split(/\s+/) + const first = parts.shift() + return [first, parts.join(' ').trim() || null] +} + +export function namePartsToFull( + firstName: string | null, + lastName: string | null +): string | null { + const first = firstName?.trim() + const last = lastName?.trim() + if (!first && !last) return null + return last && first ? `${first} ${last}` : last || first +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/keyword.mapper.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/keyword.mapper.ts new file mode 100644 index 0000000000..5cb0a5314a --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/keyword.mapper.ts @@ -0,0 +1,16 @@ +import { KeywordType } from '@geonetwork-ui/common/domain/model/thesaurus' + +export function getKeywordTypeFromKeywordTypeCode( + typeCode: string +): KeywordType { + if (!typeCode) return 'other' + switch (typeCode) { + case 'theme': + case 'place': + case 'temporal': + case 'other': + return typeCode + default: + return 'other' + } +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/role.mapper.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/role.mapper.ts new file mode 100644 index 0000000000..90bc9254fd --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/role.mapper.ts @@ -0,0 +1,48 @@ +import { Role } from '@geonetwork-ui/common/domain/model/record' + +export function getRoleFromRoleCode(roleCode: string): Role { + if (!roleCode) return 'unspecified' + switch (roleCode) { + case 'author': + case 'coAuthor': + return 'author' + case 'originator': + return 'originator' + case 'principalInvestigator': + return 'principal_investigator' + case 'resourceProvider': + return 'resource_provider' + case 'processor': + return 'processor' + case 'custodian': + return 'custodian' + case 'owner': + return 'owner' + case 'pointOfContact': + return 'point_of_contact' + case 'publisher': + return 'publisher' + case 'distributor': + return 'distributor' + case 'user': + return 'user' + case 'collaborator': + return 'collaborator' + case 'editor': + return 'editor' + case 'contributor': + return 'contributor' + case 'stakeholder': + return 'stakeholder' + case 'sponsor': + return 'sponsor' + case 'funder': + return 'funder' + case 'rightsHolder': + return 'rights_holder' + case 'mediator': + return 'mediator' + default: + return 'other' + } +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/status.mapper.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/status.mapper.ts new file mode 100644 index 0000000000..f10f508b95 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/status.mapper.ts @@ -0,0 +1,19 @@ +import { RecordStatus } from '@geonetwork-ui/common/domain/model/record' + +export function getStatusFromStatusCode(statusCode: string): RecordStatus { + switch (statusCode) { + case 'completed': + return 'completed' + case 'historicalArchive': + return 'removed' + case 'obsolete': + return 'deprecated' + case 'onGoing': + return 'ongoing' + case 'planned': + case 'required': + case 'underDevelopment': + default: + return 'under_development' + } +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/update-frequency.mapper.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/update-frequency.mapper.ts new file mode 100644 index 0000000000..fff97e9677 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/update-frequency.mapper.ts @@ -0,0 +1,67 @@ +import { UpdateFrequency } from '@geonetwork-ui/common/domain/model/record' + +export function getUpdateFrequencyFromFrequencyCode( + frequencyCode: string +): UpdateFrequency { + switch (frequencyCode) { + case 'asNeeded': + return 'asNeeded' + case 'unknown': + return 'unknown' + case 'irregular': + return 'irregular' + case 'notPlanned': + return 'notPlanned' + case 'continual': + return 'continual' + case 'periodic': + return 'periodic' + case 'daily': + return { + updatedTimes: 1, + per: 'day', + } + case 'weekly': + return { + updatedTimes: 1, + per: 'week', + } + case 'fortnightly': + return { + updatedTimes: 0.5, + per: 'week', + } + case 'semimonthly': + return { + updatedTimes: 2, + per: 'month', + } + case 'monthly': + return { + updatedTimes: 1, + per: 'month', + } + case 'quarterly': + return { + updatedTimes: 4, + per: 'year', + } + case 'biannually': + return { + updatedTimes: 2, + per: 'year', + } + case 'annually': + return { + updatedTimes: 1, + per: 'year', + } + case 'biennially': + return { + updatedTimes: 0.5, + per: 'year', + } + default: + return null + } +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/uri.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/uri.ts new file mode 100644 index 0000000000..1a089830ec --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/uri.ts @@ -0,0 +1 @@ +export const BASE_URI = 'http://geonetwork-ui/' diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.spec.ts b/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.spec.ts new file mode 100644 index 0000000000..9d117fc679 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.spec.ts @@ -0,0 +1,5 @@ +describe('write parts', () => { + it('placeholder', () => { + expect(true).toBeTruthy() + }) +}) diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.ts b/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.ts new file mode 100644 index 0000000000..a6f6b6c868 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.ts @@ -0,0 +1,62 @@ +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { NamedNode, Store, sym } from 'rdflib' +import { DCAT, DCTERMS, FOAF, RDF } from './namespaces' +import { getOrAddLocalizedObject, getOrAddStatement } from './utils/graph-utils' +import { BASE_URI } from './utils/uri' + +function getOrAddDatasetNode( + record: CatalogRecord, + dataStore: Store, + recordNode: NamedNode +): NamedNode { + if (dataStore.holds(null, RDF('type'), DCAT('Dataset'))) { + return dataStore.the(null, RDF('type'), DCAT('Dataset')) as NamedNode + } + const datasetNode = sym(`${BASE_URI}dataset/${record.uniqueIdentifier}`) + dataStore.add(datasetNode, RDF('type'), DCAT('Dataset')) + dataStore.add(recordNode, FOAF('primaryTopic'), datasetNode) + return datasetNode +} + +export function writeUniqueIdentifier( + record: CatalogRecord, + dataStore: Store, + recordNode: NamedNode +) { + getOrAddStatement( + dataStore, + recordNode, + DCTERMS('identifier'), + record.uniqueIdentifier + ) +} + +export function writeTitle( + record: CatalogRecord, + dataStore: Store, + recordNode: NamedNode +) { + const dataset = getOrAddDatasetNode(record, dataStore, recordNode) + getOrAddLocalizedObject( + dataStore, + dataset, + DCTERMS('title'), + record.title, + 'en' + ) +} + +export function writeAbstract( + record: CatalogRecord, + dataStore: Store, + recordNode: NamedNode +) { + const dataset = getOrAddDatasetNode(record, dataStore, recordNode) + getOrAddLocalizedObject( + dataStore, + dataset, + DCTERMS('description'), + record.abstract, + 'en' + ) +} diff --git a/libs/api/metadata-converter/src/lib/find-converter.ts b/libs/api/metadata-converter/src/lib/find-converter.ts index acffdcb37e..571739c080 100644 --- a/libs/api/metadata-converter/src/lib/find-converter.ts +++ b/libs/api/metadata-converter/src/lib/find-converter.ts @@ -1,6 +1,7 @@ import { Iso19139Converter } from './iso19139' import { BaseConverter } from './base.converter' import { Iso191153Converter } from './iso19115-3' +import { DcatApConverter } from './dcat-ap' export function findConverterForDocument( document: string @@ -9,6 +10,18 @@ export function findConverterForDocument( return new Iso191153Converter() } else if (document.indexOf('gmd:MD_Metadata') > 0) { return new Iso19139Converter() + } else if ( + /@prefix\s*[a-z]+\s*:\s*\s*\./.test( + document + ) + ) { + return new DcatApConverter('text/turtle') + } else if (/xmlns:[a-z]+="http:\/\/www\.w3\.org\/ns\/dcat#"/.test(document)) { + return new DcatApConverter('application/rdf+xml') + } else if ( + /"[a-zA-Z]+"\s*:\s*"http:\/\/www\.w3\.org\/ns\/dcat#/.test(document) + ) { + return new DcatApConverter('application/ld+json') } else { throw new Error(`No suitable converter found for the following document: ${document.substring(0, 400)}...`) diff --git a/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts b/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts new file mode 100644 index 0000000000..7df7ba3741 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts @@ -0,0 +1,272 @@ +import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' + +export const EU_SURVEY_DATASET_RECORD: DatasetRecord = { + uniqueIdentifier: + 'second-european-union-minorities-and-discrimination-survey', + title: 'Second European Union Minorities and Discrimination Survey', + abstract: + 'The second European Union Minorities and Discrimination Survey collected information from over 25,500 respondents with different ethnic minority and immigrant backgrounds across all 28 EU Member States.', + contacts: [], + contactsForResource: [ + { + firstName: 'Rossalina', + lastName: 'Latcheva (PhD)', + email: 'missing@missing.com', + role: 'pointOfContact', + }, + ], + landingPage: new URL( + 'https://fra.europa.eu/en/publications-and-resources/data-and-maps/survey-data-explorer-second-eu-minorities-discrimination-survey' + ), + onlineResources: [ + { + description: + 'Despite efforts by the European Union (EU) and its Member States to reduce gender inequalities among citizens of Roma origin, important gender differences persist. Drawing on FRA’s own survey research in nine EU Member States this report highlights the position of Roma women in education, employment and health, as well as the extent to which they experience hate-motivated discrimination, harassment and physical violence.', + name: 'Roma women in nine EU Member States', + type: 'download', + url: new URL( + 'https://fra.europa.eu/sites/default/files/fra_uploads/fra-2019-eu-minorities-survey-roma-women_en.pdf' + ), + }, + { + description: + 'The report is based on a survey that collected information on almost 34,000 persons living in Roma households in nine European Union (EU) Member States, derived from nearly 8,000 face-to-face interviews with Roma. It presents a selection of results from FRA’s Second European Union Minorities and Discrimination Survey (EU-MIDIS II), which surveyed around 26,000 people with immigrant or ethnic minority background living in the EU. The European Union Minorities and Discrimination Survey is a major part of the agency’s commitment to collecting and publishing data on groups not covered in general population surveys. It is the third survey of the agency to focus on Roma.', + name: 'Second European Union Minorities and Discrimination Survey (EU-MIDIS II) Roma – Selected findings', + type: 'download', + url: new URL( + 'http://fra.europa.eu/en/publication/2016/eumidis-ii-roma-selected-findings' + ), + }, + { + description: + "The report examines how characteristics – such as an individual's first and last name, skin colour and the wearing of visible religious symbols like a headscarf, for example – may trigger discriminatory treatment and harassment.", + name: 'Second European Union Minorities and Discrimination Survey (EU-MIDIS II) Muslims – Selected findings', + type: 'download', + url: new URL( + 'http://fra.europa.eu/en/publication/2017/eumidis-ii-muslims-selected-findings' + ), + }, + { + description: + 'FRA’s second EU Minorities and Discrimination survey (EU-MIDIS II) collected information from over 25,000 respondents with different ethnic minority and immigrant backgrounds across all 28 EU Member States. The main findings from the survey, published in 2017, pointed to a number of differences in the way women and men with immigrant backgrounds across the European Union (EU) experience how their rights are respected. This report summarises some of the most relevant survey findings in this regard, which show the need for targeted, gender-sensitive measures that promote the integration of – specifically – women who are immigrants or descendants of immigrants.', + name: 'Second European Union Minorities and Discrimination Survey - Migrant women - selected findings', + type: 'link', + url: new URL( + 'https://fra.europa.eu/sites/default/files/fra_uploads/fra-2019-eu-midis-ii-migrant-women_en.pdf' + ), + }, + { + description: + 'Across the EU, people of African descent face widespread and entrenched prejudice and exclusion. Racial discrimination and harassment are commonplace. Experiences with racist violence vary, but reach as high as 14 %. Discriminatory profiling by the police is a common reality. Hurdles to inclusion are multi-faceted, particularly when it comes to looking for jobs and housing.', + name: 'Being Black in the EU - Summary', + type: 'link', + url: new URL( + 'https://fra.europa.eu/en/publication/2018/eumidis-ii-being-black' + ), + }, + { + description: + 'The report follows up and expands on FRA’s first major EU-wide survey on minorities’ and migrants’ experiences, conducted in 2008. The survey focuses on discrimination in different settings, police stops, criminal victimisation, rights awareness and societal participation.', + name: 'Second European Union Minorities and Discrimination Survey - Main results', + type: 'link', + url: new URL( + 'http://fra.europa.eu/en/publication/2017/eumidis-ii-main-results' + ), + }, + { + description: + "Almost twenty years after adoption of EU laws forbidding discrimination, people of African descent in the EU face widespread and entrenched prejudice and exclusion. This report outlines selected results from FRA's second large-scale EU-wide survey on migrants and minorities (EU-MIDIS II). It examines the experiences of almost 6,000 people of African descent in 12 EU Member States.", + name: 'Being Black in the EU', + type: 'download', + url: new URL( + 'https://fra.europa.eu/sites/default/files/fra_uploads/fra-2018-being-black-in-the-eu_en.pdf' + ), + }, + { + description: + 'This interactive tool offers different ways to explore the data behind the survey results.', + name: 'Second European Union Minorities and Discrimination Survey', + type: 'download', + url: new URL( + 'https://fra.europa.eu/en/publications-and-resources/data-and-maps/survey-data-explorer-second-eu-minorities-discrimination-survey?mdq1=dataset' + ), + }, + ], + keywords: [], + kind: 'dataset', + languages: [], + legalConstraints: [], + licenses: [ + { + text: 'http://publications.europa.eu/resource/authority/licence/COM_REUSE', + url: new URL( + 'http://publications.europa.eu/resource/authority/licence/COM_REUSE' + ), + }, + ], + otherConstraints: [], + overviews: [], + securityConstraints: [], + spatialExtents: [ + { + description: + 'http://publications.europa.eu/resource/authority/country/MLT', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/FIN', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/ITA', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/POL', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/HRV', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/LUX', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/GBR', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/CZE', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/ESP', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/DEU', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/AUT', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/BEL', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/EST', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/SWE', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/GRC', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/BGR', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/SVK', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/NLD', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/PRT', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/DNK', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/LVA', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/HUN', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/IRL', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/LTU', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/ROU', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/CYP', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/FRA', + }, + { + description: + 'http://publications.europa.eu/resource/authority/country/SVN', + }, + ], + temporalExtents: [], + topics: [ + 'http://publications.europa.eu/resource/authority/data-theme/JUST', + 'http://publications.europa.eu/resource/authority/data-theme/HEAL', + 'http://publications.europa.eu/resource/authority/data-theme/SOCI', + 'http://publications.europa.eu/resource/authority/data-theme/EDUC', + ], + lineage: undefined, + ownerOrganization: { + name: 'http://publications.europa.eu/resource/authority/corporate-body/FRA', + }, + recordCreated: new Date('2021-12-03T11:44:46.000Z'), + recordUpdated: new Date('2022-04-22T12:08:18.000Z'), + resourceCreated: new Date('2017-12-19T00:00:00.000Z'), + resourceUpdated: new Date('2018-12-14T00:00:00.000Z'), + status: undefined, + updateFrequency: 'unknown', +} + +export const EU_WHOISWHO_DATASET_RECORD: DatasetRecord = { + uniqueIdentifier: 'eu-whoiswho-the-official-directory-of-the-european-union', + title: 'EU Whoiswho: the official directory of the European Union', + contacts: [], + contactsForResource: [], + onlineResources: [], + keywords: [], + kind: 'dataset', + languages: [], + legalConstraints: [], + licenses: [], + otherConstraints: [], + overviews: [], + securityConstraints: [], + spatialExtents: [], + temporalExtents: [], + topics: [], + abstract: `EU Whoiswho is an electronic directory which presents the organisational charts of the EU institutions, bodies and agencies in all official EU languages. The personal data in this directory are provided by the institutions, bodies and agencies of EU. The SPARQL endpoint allows to query the full current content visible in EU whoiswho website. For historical content, PDF are available from 1981.\r + \r + It is strictly forbidden to use these data for direct marketing purposes. \r + \r + If you detect any errors, please report them to: whoiswho@publications.europa.eu\r + \r + [Privacy statement of EU Whoiswho](https://op.europa.eu/en/web/about-us/legal-notices/op_whoiswho).`, + lineage: undefined, + ownerOrganization: undefined, + recordUpdated: undefined, + status: undefined, + updateFrequency: 'unknown', +} diff --git a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+eu.dcat-ap.survey.xml b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+eu.dcat-ap.survey.xml new file mode 100644 index 0000000000..291d32bab2 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+eu.dcat-ap.survey.xml @@ -0,0 +1,818 @@ + + + + + + Toinen Euroopan unionin vähemmistöjä ja syrjintää koskeva tutkimus + Toisessa Euroopan unionin vähemmistöjä ja syrjintää koskevassa kyselyssä kerättiin tietoja yli 25 500:lta eri etnistä vähemmistöä ja maahanmuuttajataustaa edustavalta vastaajalta kaikissa EU:n 28 jäsenvaltiossa. + + + + + O segundo Inquérito sobre Minorias e Discriminação na União Europeia recolheu informações de mais de 25 500 inquiridos de diferentes minorias étnicas e imigrantes em todos os 28 Estados-Membros da UE. + + + + + + 2017-12-19 + La seconda indagine sulle minoranze e le discriminazioni nell'Unione europea ha raccolto informazioni da oltre 25 500 rispondenti con diverse minoranze etniche e contesti migratori in tutti i 28 Stati membri dell'UE. + + + + U drugom istraživanju Europske unije o manjinama i diskriminaciji prikupljene su informacije od više od 25 500 ispitanika različitih etničkih manjina i imigranata u svih 28 država članica EU-a. + # Introduction +This dataset has been established for testing purposes. + +## Details +This is a section about details. Here is an HTML tag: <img src="http://google.com" />. And [a link](https://google.com). + +## Informations intéressantes +Cette section contient des *caractères internationaux* (ainsi que des "caractères spéciaux"). 'çàü^@/~^& + 2018-12-14 + Δεύτερη έρευνα της Ευρωπαϊκής Ένωσης για τις μειονότητες και τις διακρίσεις + + + + Den Europæiske Unions anden undersøgelse af mindretal og forskelsbehandling + Otrais Eiropas Savienības minoritāšu un diskriminācijas apsekojums + + + + + + + Otrajā Eiropas Savienības minoritāšu un diskriminācijas apsekojumā tika apkopota informācija no vairāk nekā 25 500 respondentiem no dažādām etniskajām minoritātēm un imigrantiem visās 28 ES dalībvalstīs. + Bhailigh an dara Suirbhé de chuid an Aontais Eorpaigh ar Mhionlaigh agus ar Idirdhealú faisnéis ó níos mó ná 25,500 freagróir a raibh cúlraí éagsúla mionlaigh eitnigh agus inimirceacha acu ar fud 28 mBallstát an AE. + Tweede enquête van de Europese Unie naar minderheden en discriminatie + Segunda encuesta de la Unión Europea sobre las minorías y la discriminación + EU's anden undersøgelse af mindretal og forskelsbehandling indsamlede oplysninger fra over 25 500 respondenter med forskellig etnisk minoritets- og indvandrerbaggrund i alle 28 EU-medlemsstater. + + + + + + + Druga raziskava Evropske unije o manjšinah in diskriminaciji + + + + In het tweede EU-onderzoek naar minderheden en discriminatie is informatie verzameld van meer dan 25 500 respondenten met verschillende etnische minderheden en immigranten in alle 28 EU-lidstaten. + Den andra EU-undersökningen om minoriteter och diskriminering samlade in information från över 25 500 svarande med olika etnisk minoritets- och invandrarbakgrund i alla 28 EU-medlemsstater. + + + + + Второто проучване на малцинствата и дискриминацията в Европейския съюз събра информация от над 25 500 респонденти с различен етнически произход и имигрантски произход във всички 28 държави — членки на ЕС. + + + + + + Ondanks de inspanningen van de Europese Unie (EU) en haar lidstaten om de genderongelijkheid tussen burgers van Roma te verminderen, blijven er nog steeds grote verschillen tussen mannen en vrouwen bestaan. Op basis van het eigen enquêteonderzoek van het FRA in negen EU-lidstaten wordt in dit verslag gewezen op de positie van Roma-vrouwen op het gebied van onderwijs, werkgelegenheid en gezondheid, en op de mate waarin zij te maken krijgen met haat gemotiveerde discriminatie, intimidatie en fysiek geweld. + + Trotz der Bemühungen der Europäischen Union (EU) und ihrer Mitgliedstaaten, die Ungleichheiten zwischen den Geschlechtern der Roma zu verringern, bestehen nach wie vor erhebliche Unterschiede zwischen den Geschlechtern. Auf der Grundlage der eigenen Umfragen der FRA in neun EU-Mitgliedstaaten wird in diesem Bericht auf die Position von Roma-Frauen in Bildung, Beschäftigung und Gesundheit sowie auf das Ausmaß hingewiesen, in dem sie hassmotivierte Diskriminierung, Belästigung und körperliche Gewalt erfahren. + Romu sievietes deviņās ES dalībvalstīs + Trots insatser från Europeiska unionen (EU) och dess medlemsstater för att minska ojämlikheten mellan könen bland medborgare av romskt ursprung kvarstår betydande skillnader mellan könen. Med utgångspunkt i FRA:s egen undersökning i nio EU-medlemsstater belyser denna rapport romska kvinnors ställning inom utbildning, sysselsättning och hälsa samt i vilken utsträckning de utsätts för hatmotiverad diskriminering, trakasserier och fysiskt våld. + Kljub prizadevanjem Evropske unije (EU) in njenih držav članic za zmanjšanje neenakosti med spoloma med državljani romskega izvora še vedno obstajajo pomembne razlike med spoloma. Na podlagi raziskave Agencije za temeljne pravice v devetih državah članicah EU je v tem poročilu poudarjen položaj romskih žensk v izobraževanju, zaposlovanju in zdravju ter v kolikšni meri se soočajo z diskriminacijo, nadlegovanjem in fizičnim nasiljem, ki jih motivira sovraštvo. + Romaninaiset yhdeksässä EU:n jäsenvaltiossa + Napriek úsiliu Európskej únie (EÚ) a jej členských štátov znížiť rodové nerovnosti medzi občanmi rómskeho pôvodu pretrvávajú významné rodové rozdiely. Na základe vlastného prieskumu agentúry FRA v deviatich členských štátoch EÚ sa v tejto správe zdôrazňuje postavenie rómskych žien v oblasti vzdelávania, zamestnanosti a zdravia, ako aj rozsah, v akom sa stretávajú s diskrimináciou motivovanou nenávisťou, obťažovaním a fyzickým násilím. + http://data.europa.eu/88u/distribution/86d86813-bfac-42a3-8a02-735cad82834c + Vaatamata Euroopa Liidu (EL) ja selle liikmesriikide jõupingutustele vähendada soolist ebavõrdsust roma päritolu kodanike seas, püsivad olulised soolised erinevused. Tuginedes FRA uuringule üheksas ELi liikmesriigis, rõhutatakse käesolevas aruandes roma naiste olukorda hariduses, tööhõives ja tervishoius ning seda, mil määral kogevad nad vihast ajendatud diskrimineerimist, ahistamist ja füüsilist vägivalda. + A pesar de los esfuerzos de la Unión Europea (UE) y sus Estados miembros para reducir las desigualdades de género entre los ciudadanos de origen romaní, persisten importantes diferencias de género. Sobre la base de la propia investigación realizada por la FRA en nueve Estados miembros de la UE, este informe pone de relieve la posición de las mujeres gitanas en la educación, el empleo y la salud, así como en la medida en que sufren discriminación, acoso y violencia física motivados por el odio. + Mulheres ciganas em nove Estados-Membros da UE + Femeile de etnie romă în nouă state membre ale UE + Roma-Frauen in neun EU-Mitgliedstaaten + + Romské ženy v devíti členských státech EU + Жени от ромски произход в девет държави — членки на ЕС + Mná Romacha i naoi mBallstát den AE + Mujeres gitanas en nueve Estados miembros de la UE + Παρά τις προσπάθειες της Ευρωπαϊκής Ένωσης (ΕΕ) και των κρατών μελών της για τη μείωση των ανισοτήτων μεταξύ των φύλων μεταξύ των πολιτών καταγωγής Ρομά, εξακολουθούν να υπάρχουν σημαντικές διαφορές μεταξύ των φύλων. Με βάση την έρευνα του FRA σε εννέα κράτη μέλη της ΕΕ, η παρούσα έκθεση υπογραμμίζει τη θέση των γυναικών Ρομά στην εκπαίδευση, την απασχόληση και την υγεία, καθώς και τον βαθμό στον οποίο βιώνουν διακρίσεις, παρενόχληση και σωματική βία λόγω μίσους. + + Romakvinder i ni EU-medlemsstater + + Romų tautybės moterys devyniose ES valstybėse narėse + Γυναίκες Ρομά σε εννέα κράτη μέλη της ΕΕ + Neraugoties uz Eiropas Savienības (ES) un tās dalībvalstu centieniem samazināt dzimumu nevienlīdzību romu izcelsmes pilsoņu vidū, joprojām pastāv būtiskas dzimumu atšķirības. Pamatojoties uz FRA pētījumu deviņās ES dalībvalstīs, šis ziņojums uzsver romu sieviešu stāvokli izglītībā, nodarbinātībā un veselības aprūpē, kā arī to, cik lielā mērā viņas saskaras ar naida motivētu diskrimināciju, uzmākšanos un fizisku vardarbību. + Az Európai Unió (EU) és tagállamai által a roma származású polgárok között a nemek közötti egyenlőtlenségek csökkentése érdekében tett erőfeszítések ellenére továbbra is jelentős különbségek vannak a nemek között. Az FRA kilenc uniós tagállamban végzett saját felmérésére támaszkodva ez a jelentés kiemeli a roma nők helyzetét az oktatásban, a foglalkoztatásban és az egészségügyben, valamint azt, hogy milyen mértékben tapasztalnak gyűlölet-indító megkülönböztetést, zaklatást és fizikai erőszakot. + 2019-04-05 + + + + Nisa Roma f’disa’ Stati Membri tal-UE + Romkinje u devet država članica EU-a + Roma nők kilenc uniós tagállamban + + Kobiety romskie w dziewięciu państwach członkowskich UE + Navzdory úsilí Evropské unie (EU) a jejích členských států o snížení nerovností mezi ženami a muži mezi občany romského původu přetrvávají významné rozdíly mezi ženami a muži. Tato zpráva vychází z vlastního průzkumu provedeného agenturou FRA v devíti členských státech EU a zdůrazňuje postavení romských žen ve vzdělávání, zaměstnání a zdravotnictví, jakož i míru, v jaké se setkávají s diskriminací motivovanou z nenávisti, obtěžováním a fyzickým násilím. + Femmes roms dans neuf États membres de l’UE + Donne rom in nove Stati membri dell'UE + På trods af Den Europæiske Unions (EU's) og dens medlemsstaters bestræbelser på at mindske ulighederne mellem kønnene blandt borgere af romaoprindelse er der stadig store kønsforskelle. Med udgangspunkt i FRA's egen undersøgelse i ni EU-medlemsstater fremhæver denne rapport romakvinders stilling inden for uddannelse, beskæftigelse og sundhed samt i hvilket omfang de oplever hadmotiveret forskelsbehandling, chikane og fysisk vold. + În ciuda eforturilor depuse de Uniunea Europeană (UE) și de statele sale membre de a reduce inegalitățile de gen în rândul cetățenilor de origine romă, persistă diferențe importante de gen. Pe baza propriilor studii realizate de FRA în nouă state membre ale UE, prezentul raport evidențiază poziția femeilor rome în educație, ocuparea forței de muncă și sănătate, precum și măsura în care acestea se confruntă cu discriminarea, hărțuirea și violența fizică motivate de ură. + Romavrouwen in negen EU-lidstaten + Unatoč naporima Europske unije (EU) i njezinih država članica da smanje rodne nejednakosti među građanima romskog podrijetla, i dalje postoje važne rodne razlike. Oslanjajući se na istraživanje koje je Agencija za temeljna prava provela u devet država članica EU-a, u ovom se izvješću ističe položaj Romkinja u obrazovanju, zapošljavanju i zdravlju, kao i mjera u kojoj se suočavaju s diskriminacijom, uznemiravanjem i fizičkim nasiljem motiviranima mržnjom. + Pomimo wysiłków podejmowanych przez Unię Europejską (UE) i jej państwa członkowskie na rzecz zmniejszenia nierówności płci wśród obywateli pochodzenia romskiego utrzymują się istotne różnice w traktowaniu kobiet i mężczyzn. Opierając się na badaniach przeprowadzonych przez FRA w dziewięciu państwach członkowskich UE, w niniejszym sprawozdaniu podkreślono pozycję kobiet romskich w edukacji, zatrudnieniu i zdrowiu, a także stopień, w jakim kobiety te doświadczają dyskryminacji motywowanej nienawiścią, nękania i przemocy fizycznej. + In ainneoin iarrachtaí an Aontais Eorpaigh (AE) agus na mBallstát an neamhionannas inscne i measc saoránach de bhunadh Romach a laghdú, tá difríochtaí tábhachtacha inscne fós ann. Agus leas á bhaint as taighde suirbhé FRA féin i naoi mBallstát AE, leagtar béim sa tuarascáil seo ar sheasamh na mban Romach san oideachas, san fhostaíocht agus sa tsláinte, chomh maith lena mhéid a fhulaingíonn siad idirdhealú fuathspreagtha, ciapadh agus foréigean fisiciúil. + Nepaisant Europos Sąjungos (ES) ir jos valstybių narių pastangų mažinti romų kilmės piliečių lyčių nelygybę, tebėra didelių lyčių skirtumų. Remiantis FRA tyrimu devyniose ES valstybėse narėse, šiame pranešime pabrėžiama romų moterų padėtis švietimo, užimtumo ir sveikatos srityse, taip pat tai, kokiu mastu jos patiria neapykantą kurstančią diskriminaciją, priekabiavimą ir fizinį smurtą. + Romske ženske v devetih državah članicah EU + Roma naised üheksas ELi liikmesriigis + Apesar dos esforços envidados pela União Europeia (UE) e pelos seus Estados-Membros para reduzir as desigualdades de género entre os cidadãos de origem cigana, persistem importantes diferenças de género. Com base na investigação realizada pela FRA em nove Estados-Membros da UE, este relatório destaca a posição das mulheres ciganas na educação, no emprego e na saúde, bem como a medida em que são vítimas de discriminação motivada pelo ódio, assédio e violência física. + Въпреки усилията на Европейския съюз (ЕС) и неговите държави членки за намаляване на неравенството между половете сред гражданите от ромски произход продължават да съществуват значителни различия между половете. Въз основа на проучването на Агенцията на Европейския съюз за основните права (FRA) в девет държави — членки на ЕС, настоящият доклад подчертава позицията на жените от ромски произход в образованието, заетостта и здравето, както и степента, в която те са подложени на дискриминация, тормоз и физическо насилие, мотивирани от омраза. + Romska kvinnor i nio EU-medlemsstater + Rómske ženy v deviatich členských štátoch EÚ + Nonostante gli sforzi compiuti dall'Unione europea (UE) e dai suoi Stati membri per ridurre le disuguaglianze di genere tra i cittadini di origine rom, persistono notevoli differenze di genere. Sulla base delle ricerche condotte dalla FRA in nove Stati membri dell'UE, la relazione evidenzia la posizione delle donne Rom nell'istruzione, nell'occupazione e nella salute, nonché la misura in cui subiscono discriminazioni, molestie e violenze fisiche motivate dall'odio. + Malgré les efforts déployés par l’Union européenne (UE) et ses États membres pour réduire les inégalités entre les hommes et les femmes parmi les citoyens d’origine rom, d’importantes différences entre les sexes persistent. S’appuyant sur les recherches menées par la FRA dans neuf États membres de l’UE, le présent rapport met en lumière la position des femmes roms dans les domaines de l’éducation, de l’emploi et de la santé, ainsi que la mesure dans laquelle elles subissent des discriminations motivées par la haine, du harcèlement et des violences physiques. + Minkejja l-isforzi tal-Unjoni Ewropea (UE) u l-Istati Membri tagħha biex inaqqsu l-inugwaljanzi bejn is-sessi fost iċ-ċittadini ta’ oriġini Rom, għad hemm differenzi importanti bejn is-sessi. Abbażi tar-riċerka ta’ stħarriġ tal-FRA stess f’disa’ Stati Membri tal-UE, dan ir-rapport jenfasizza l-pożizzjoni tan-nisa Rom fl-edukazzjoni, l-impjiegi u s-saħħa, kif ukoll il-punt sa fejn jesperjenzaw diskriminazzjoni, fastidju u vjolenza fiżika mmotivati mill-mibegħda. + + + + + + + Roma women in nine EU Member States + Huolimatta Euroopan unionin (EU) ja sen jäsenvaltioiden pyrkimyksistä vähentää sukupuolten välistä epätasa-arvoa romaneista peräisin olevien kansalaisten välillä on edelleen merkittäviä sukupuolten välisiä eroja. Tässä raportissa, joka perustuu perusoikeusviraston yhdeksässä EU:n jäsenvaltiossa tekemään tutkimukseen, korostetaan romaninaisten asemaa koulutuksessa, työelämässä ja terveydenhuollossa sekä sitä, missä määrin he kokevat vihaan perustuvaa syrjintää, häirintää ja fyysistä väkivaltaa. + Despite efforts by the European Union (EU) and its Member States to reduce gender inequalities among citizens of Roma origin, important gender differences persist. Drawing on FRA’s own survey research in nine EU Member States this report highlights the position of Roma women in education, employment and health, as well as the extent to which they experience hate-motivated discrimination, harassment and physical violence. + + + + + + Cel de-al doilea Sondaj privind minoritățile și discriminarea în Uniunea Europeană a colectat informații de la peste 25 500 de respondenți provenind din diferite minorități etnice și imigranți din toate cele 28 de state membre ale UE. + + + + + + Η δεύτερη έρευνα της Ευρωπαϊκής Ένωσης για τις μειονότητες και τις διακρίσεις συγκέντρωσε πληροφορίες από περισσότερους από 25.500 ερωτηθέντες με διαφορετικές εθνοτικές μειονότητες και μεταναστευτικές καταβολές και στα 28 κράτη μέλη της ΕΕ. + + + + Dara Suirbhé an Aontais Eorpaigh ar Mhionlaigh agus ar Idirdhealú + Without multiple response questions + + + + + + + + + + + + Second European Union Minorities and Discrimination Survey - Technical report + + + + http://fra.europa.eu/en/publication/2017/eumidis-ii-technical-report + This technical report presents a detailed overview of the research methods used by FRA when collecting the survey data. + + + + + https://fra.europa.eu/en/publications-and-resources/data-and-maps/survey-data-explorer-second-eu-minorities-discrimination-survey + + + + + + + + + Aruanne põhineb uuringul, mille käigus koguti teavet peaaegu 34 000 roma leibkondades elava inimese kohta üheksas Euroopa Liidu (EL) liikmesriigis, mis saadi ligi 8000 romadega peetud küsitlusest. Selles tutvustatakse FRA teise Euroopa Liidu vähemuste ja diskrimineerimise uuringu (EU-MIDIS II) tulemusi, milles küsitleti ligikaudu 26 000 ELis elavat sisserändaja või etnilise vähemuse taustaga inimest. Euroopa Liidu vähemuste ja diskrimineerimise uuring on oluline osa ameti kohustusest koguda ja avaldada andmeid rühmade kohta, keda üldised rahvastiku-uuringud ei hõlma. See on ameti kolmas uuring, mis keskendub romadele. + Izvješće se temelji na anketi u kojoj su prikupljene informacije o gotovo 34 000 osoba koje žive u romskim kućanstvima u devet država članica Europske unije (EU), a koje su izvedene iz gotovo 8 000 izravnih razgovora s Romima. U njemu su predstavljeni rezultati drugog istraživanja Europske unije o manjinama i diskriminaciji (EU-MIDIS II) koje je provela Agencija Europske unije za temeljna prava (EU-MIDIS II), u kojem je provedeno istraživanje oko 26 000 osoba imigranata ili etničkih manjina koje žive u EU-u. Istraživanje Europske unije o manjinama i diskriminaciji velik je dio obveze agencije da prikuplja i objavljuje podatke o skupinama koje nisu obuhvaćene anketama o općoj populaciji. To je treće istraživanje agencije koje se usredotočuje na Rome. + Druga raziskava Evropske unije o manjšinah in diskriminaciji (EU-MIDIS II) Romi – izbrane ugotovitve + El informe se basa en una encuesta que recogió información sobre casi 34.000 personas que viven en hogares romaníes en nueve Estados miembros de la Unión Europea (UE), derivada de casi 8.000 entrevistas presenciales con romaníes. Presenta una selección de los resultados de la segunda encuesta de la FRA sobre minorías y discriminación de la Unión Europea (EU-MIDIS II), en la que participaron unas 26.000 personas de origen inmigrante o de minorías étnicas que viven en la UE. La encuesta de la Unión Europea sobre las minorías y la discriminación es una parte importante del compromiso del organismo de reunir y publicar datos sobre grupos no incluidos en las encuestas generales de población. Se trata de la tercera encuesta realizada por el organismo para centrarse en los romaníes. + Tá an tuarascáil bunaithe ar shuirbhé inar bailíodh faisnéis faoi bheagnach 34,000 duine atá ina gcónaí i dteaghlaigh Romacha i naoi mBallstát den Aontas Eorpach (AE), a fuarthas ó bheagnach 8,000 agallamh duine le duine leis na Romaigh. Cuireann sé rogha torthaí i láthair ó Dara Suirbhé an Aontais Eorpaigh ar Mhionlaigh agus ar Idirdhealú (EU-MIDIS II), a rinne suirbhé ar thart ar 26,000 duine de chúlra inimirceach nó mionlaigh eitnigh atá ina gcónaí san AE. Tá Suirbhé an Aontais Eorpaigh ar Mhionlaigh agus ar Idirdhealú ina chuid mhór de thiomantas na gníomhaireachta sonraí a bhailiú agus a fhoilsiú maidir le grúpaí nach gcumhdaítear i suirbhéanna ginearálta daonra. Is é an tríú suirbhé ar an ngníomhaireacht é díriú ar na Romaigh. + Raportul se bazează pe un sondaj care a colectat informații privind aproape 34 000 de persoane care locuiesc în gospodării rome în nouă state membre ale Uniunii Europene (UE), provenite din aproape 8 000 de interviuri față în față cu romii. Raportul prezintă o selecție de rezultate ale celui de-al doilea sondaj al FRA privind minoritățile și discriminarea (EU-MIDIS II), care a intervievat aproximativ 26 000 de persoane care provin din familii de imigranți sau minorități etnice care trăiesc în UE. Sondajul Uniunii Europene privind minoritățile și discriminarea este o parte importantă a angajamentului agenției de a colecta și publica date privind grupurile care nu sunt incluse în anchetele privind populația generală. Este al treilea sondaj al agenției care se concentrează asupra romilor. + Al doilea sondaj al Uniunii Europene privind minoritățile și discriminarea (EU-MIDIS II) romi – constatări selectate + A jelentés egy olyan felmérésen alapul, amely az Európai Unió (EU) kilenc tagállamában közel 34 000 roma háztartásban élő személyről gyűjtött információkat, amelyek a romákkal folytatott közel 8000 személyes interjúból származnak. A jelentés az FRA második európai uniós kisebbségi és hátrányos megkülönböztetés-felmérésének (EU-MIDIS II) eredményeit mutatja be, amelyek mintegy 26 000, az EU-ban élő bevándorló vagy etnikai kisebbségi háttérrel rendelkező személyt vizsgáltak meg. Az Európai Unió Minorities and Discrimination Survey (Európai Uniós Kisebbségi és Megkülönböztetési Felmérés) jelentős része az ügynökség azon kötelezettségvállalásának, hogy adatokat gyűjt és tesz közzé az általános népességi felmérésekben nem szereplő csoportokra vonatkozóan. Ez az ügynökség harmadik felmérése, amely a romákra összpontosít. + The report is based on a survey that collected information on almost 34,000 persons living in Roma households in nine European Union (EU) Member States, derived from nearly 8,000 face-to-face interviews with Roma. It presents a selection of results from FRA’s Second European Union Minorities and Discrimination Survey (EU-MIDIS II), which surveyed around 26,000 people with immigrant or ethnic minority background living in the EU. The European Union Minorities and Discrimination Survey is a major part of the agency’s commitment to collecting and publishing data on groups not covered in general population surveys. It is the third survey of the agency to focus on Roma. + + Der Bericht stützt sich auf eine Umfrage, bei der Informationen über fast 34.000 Personen, die in Roma-Haushalten in neun Mitgliedstaaten der Europäischen Union (EU) leben, gesammelt wurden, die aus fast 8.000 persönlichen Interviews mit Roma stammen. Es stellt eine Auswahl von Ergebnissen der zweiten Erhebung der FRA für Minderheiten und Diskriminierung (EU-MIDIS II) der FRA vor, in der rund 26.000 Personen mit Migrationshintergrund oder ethnischer Minderheit in der EU befragt wurden. Die Erhebung über Minderheiten und Diskriminierung der Europäischen Union ist ein wichtiger Teil der Verpflichtung der Agentur, Daten zu Gruppen zu erheben und zu veröffentlichen, die nicht in allgemeinen Bevölkerungsumfragen erfasst sind. Es ist die dritte Umfrage der Agentur, die sich auf die Roma konzentriert. + Η έκθεση βασίζεται σε έρευνα που συγκέντρωσε πληροφορίες για σχεδόν 34.000 άτομα που ζουν σε νοικοκυριά Ρομά σε εννέα κράτη μέλη της Ευρωπαϊκής Ένωσης (ΕΕ), τα οποία προέκυψαν από σχεδόν 8.000 προσωπικές συνεντεύξεις με Ρομά. Παρουσιάζει μια επιλογή αποτελεσμάτων από τη δεύτερη έρευνα του FRA για τις μειονότητες και τις διακρίσεις (EU-MIDIS II), στην οποία συμμετείχαν περίπου 26.000 άτομα με καταγωγή μεταναστών ή εθνοτικών μειονοτήτων που ζουν στην ΕΕ. Η έρευνα της Ευρωπαϊκής Ένωσης για τις μειονότητες και τις διακρίσεις αποτελεί σημαντικό μέρος της δέσμευσης του οργανισμού για τη συλλογή και τη δημοσίευση στοιχείων σχετικά με ομάδες που δεν καλύπτονται από γενικές δημογραφικές έρευνες. Είναι η τρίτη έρευνα του οργανισμού που επικεντρώνεται στους Ρομά. + Второ проучване на малцинствата и дискриминацията в Европейския съюз (EU-MIDIS II) роми — избрани констатации + Drugie badanie Unii Europejskiej na temat mniejszości i dyskryminacji (EU-MIDIS II) Romowie – wybrane ustalenia + Toinen Euroopan unionin vähemmistöjä ja syrjintää koskeva tutkimus (EU-MIDIS II) Roma – Valitut tulokset + Otrais Eiropas Savienības minoritāšu un diskriminācijas apsekojums (EU-MIDIS II) Roma — atlasīti konstatējumi + Europeiska unionens andra undersökning av minoriteter och diskriminering (EU-MIDIS II) Roma – utvalda resultat + La relazione si basa su un'indagine che ha raccolto informazioni su quasi 34 000 persone che vivono in famiglie rom in nove Stati membri dell'Unione europea (UE), ricavate da quasi 8 000 interviste faccia a faccia con i Rom. Presenta una selezione di risultati della seconda indagine della FRA sulle minoranze e le discriminazioni nell'Unione europea (EU-MIDIS II), che ha esaminato circa 26 000 persone provenienti da immigrati o minoranze etniche residenti nell'UE. L'indagine sulle minoranze e le discriminazioni nell'Unione europea è una parte importante dell'impegno dell'agenzia a raccogliere e pubblicare dati su gruppi non contemplati dalle indagini generali sulla popolazione. È la terza indagine dell'agenzia a concentrarsi sui Rom. + Second European Union Minorities and Discrimination Survey (EU-MIDIS II) Roma – Selected findings + + Druhý průzkum menšin a diskriminace v Evropské unii (EU-MIDIS II) Romové – Vybraná zjištění + Sprawozdanie opiera się na badaniu, w którym zgromadzono informacje na temat prawie 34 000 osób mieszkających w gospodarstwach domowych romskich w dziewięciu państwach członkowskich Unii Europejskiej (UE), pochodzące z prawie 8 000 bezpośrednich wywiadów z Romami. Przedstawiono w nim wybrane wyniki drugiego badania FRA na temat mniejszości i dyskryminacji w Unii Europejskiej (EU-MIDIS II), w którym wzięło udział około 26 000 osób ze środowisk imigrantów lub mniejszości etnicznych mieszkających w UE. Badanie Unii Europejskiej na temat mniejszości i dyskryminacji stanowi istotną część zobowiązania agencji do gromadzenia i publikowania danych dotyczących grup nieobjętych ogólnymi badaniami populacji. Jest to trzecie badanie agencji, które koncentruje się na Romów. + Drugo istraživanje Europske unije o manjinama i diskriminaciji (EU-MIDIS II) Romi – odabrani nalazi + Rapporten bygger på en undersökning som samlade in information om nästan 34 000 personer som bor i romska hushåll i nio EU-medlemsstater, från nästan 8 000 personliga intervjuer med romer. Här presenteras ett urval av resultat från FRA:s andra undersökning om minoriteter och diskriminering i Europeiska unionen (EU-MIDIS II), som omfattade omkring 26 000 personer med invandrarbakgrund eller etnisk minoritetsbakgrund som bor i EU. Europeiska unionens undersökning om minoriteter och diskriminering är en stor del av byråns åtagande att samla in och offentliggöra uppgifter om grupper som inte omfattas av allmänna befolkningsundersökningar. Det är den tredje undersökningen av byrån som fokuserar på romer. + Raportti perustuu kyselyyn, jossa kerättiin tietoa lähes 34 000 romanitalouksissa asuvasta henkilöstä yhdeksässä Euroopan unionin (EU) jäsenvaltiossa. Tiedot saatiin lähes 8 000:sta romanien henkilökohtaisesta haastattelusta. Siinä esitellään FRA:n toisen Euroopan unionin vähemmistöjä ja syrjintää koskevan tutkimuksen (EU-MIDIS II) tulokset. Tutkimuksessa tutkittiin noin 26 000 maahanmuuttaja- tai etniseen vähemmistöön kuuluvaa henkilöä, jotka asuvat eu:ssa. Euroopan unionin vähemmistöjä ja syrjintää koskeva tutkimus on merkittävä osa viraston sitoumusta kerätä ja julkaista tietoja ryhmistä, jotka eivät kuulu yleisten väestötutkimusten piiriin. Se on viraston kolmas tutkimus, jossa keskitytään romaneihin. + Den Europæiske Unions anden undersøgelse af mindretal og forskelsbehandling (EU-MIDIS II) romaer — udvalgte resultater + Poročilo temelji na raziskavi, v kateri so bile zbrane informacije o skoraj 34 000 osebah, ki živijo v romskih gospodinjstvih v devetih državah članicah Evropske unije (EU), pridobljene pa so bile na podlagi skoraj 8 000 osebnih razgovorov z Romi. V njem so predstavljeni rezultati druge raziskave Evropske agencije za temeljne pravice o manjšinah in diskriminaciji (EU-MIDIS II), v kateri je bilo vključenih približno 26 000 ljudi s priseljenskim ali etničnim ozadjem, ki živijo v EU. Raziskava o manjšinah in diskriminaciji Evropske unije je pomemben del zavezanosti agencije k zbiranju in objavljanju podatkov o skupinah, ki niso vključene v splošne raziskave o prebivalstvu. To je tretja raziskava agencije, ki se osredotoča na Rome. + + + + + Deuxième enquête de l’Union européenne sur les minorités et la discrimination (EU-MIDIS II) Roms — Résultats sélectionnés + + Ir-rapport huwa bbażat fuq stħarriġ li ġabar informazzjoni dwar kważi 34,000 persuna li jgħixu f’unitajiet domestiċi Rom f’disa’ Stati Membri tal-Unjoni Ewropea (UE), miksuba minn kważi 8,000 intervista wiċċ imb’wiċċ mar-Rom. Huwa jippreżenta għażla ta’ riżultati mit-Tieni Stħarriġ tal-FRA dwar il-Minoranzi u d-Diskriminazzjoni fl-Unjoni Ewropea (EU-MIDIS II), li stħarreġ madwar 26,000 persuna bi sfond ta’ immigrazzjoni jew minorità etnika li jgħixu fl-UE. L-Istħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni huwa parti kbira mill-impenn tal-aġenzija li tiġbor u tippubblika data dwar gruppi mhux koperti fi stħarriġ ġenerali dwar il-popolazzjoni. Huwa t-tielet stħarriġ tal-aġenzija li jiffoka fuq ir-Rom. + Dara Suirbhé an Aontais Eorpaigh ar Mhionlaigh agus ar Idirdhealú (EU-MIDIS II) — Torthaí roghnaithe + Le rapport s’appuie sur une enquête qui a recueilli des informations sur près de 34 000 personnes vivant dans des ménages roms dans neuf États membres de l’Union européenne (UE), tirées de près de 8 000 entretiens en personne avec des Roms. Il présente une sélection des résultats de la deuxième enquête de l’Union européenne sur les minorités et la discrimination (EU-MIDIS II), qui a porté sur quelque 26 000 personnes issues de l’immigration ou d’une minorité ethnique vivant dans l’UE. L’enquête de l’Union européenne sur les minorités et la discrimination constitue une part importante de l’engagement pris par l’Agence de collecter et de publier des données sur les groupes non couverts par les enquêtes démographiques générales. Il s’agit de la troisième enquête menée par l’agence pour mettre l’accent sur les Roms. + 2017-12-19T11:41:41.537218+02:00 + http://data.europa.eu/88u/distribution/05b2d8c6-8386-4a8a-8bd3-0cc0a066f775 + Segundo Inquérito sobre Minorias e Discriminação na União Europeia (EU-MIDIS II) Roma — Conclusões selecionadas + Rapporten er baseret på en undersøgelse, der indsamlede oplysninger om næsten 34 000 personer, der bor i romahusstande i ni EU-medlemsstater, og som stammer fra næsten 8 000 personlige interviews med romaer. Den præsenterer et udvalg af resultater fra FRA's anden undersøgelse af mindretal og forskelsbehandling i Den Europæiske Union (EU-MIDIS II), som undersøgte ca. 26 000 personer med indvandrerbaggrund eller etnisk minoritetsbaggrund, der bor i EU. EU's undersøgelse af mindretal og forskelsbehandling er en stor del af agenturets forpligtelse til at indsamle og offentliggøre data om grupper, der ikke er omfattet af generelle befolkningsundersøgelser. Det er den tredje undersøgelse foretaget af agenturet, der fokuserer på romaer. + Antrasis Europos Sąjungos mažumų ir diskriminacijos tyrimas (EU-MIDIS II) romai. Atrinktos išvados + Segunda encuesta de la Unión Europea sobre las minorías y la discriminación (EU-MIDIS II) sobre los romaníes — Conclusiones seleccionadas + + Het verslag is gebaseerd op een enquête waarin informatie is verzameld over bijna 34 000 personen in Romahuishoudens in negen lidstaten van de Europese Unie (EU), afkomstig van bijna 8 000 persoonlijke interviews met Roma. Het bevat een selectie van de resultaten van de tweede enquête van het FRA naar minderheden en discriminatie van de Europese Unie (EU-MIDIS II), waarin ongeveer 26 000 mensen met een immigranten- of etnische minderheidsachtergrond in de EU werden ondervraagd. De enquête over minderheden en discriminatie van de Europese Unie is een belangrijk onderdeel van de toezegging van het agentschap om gegevens te verzamelen en te publiceren over groepen die niet onder algemene bevolkingsenquêtes vallen. Het is het derde onderzoek van het agentschap dat zich richt op de Roma. + Ataskaita grindžiama apklausa, kurioje surinkta informacija apie beveik 34 000 romų namų ūkiuose gyvenančių asmenų devyniose Europos Sąjungos (ES) valstybėse narėse, gauta iš beveik 8 000 tiesioginių pokalbių su romais. Joje pateikiami FRA Antrojo Europos Sąjungos mažumų ir diskriminacijos tyrimo (EU-MIDIS II) rezultatai, kuriuose dalyvavo apie 26 000 imigrantų ar etninių mažumų kilmės asmenų, gyvenančių ES. Europos Sąjungos mažumų ir diskriminacijos tyrimas yra pagrindinė agentūros įsipareigojimo rinkti ir skelbti duomenis apie grupes, neįtrauktas į gyventojų apklausas, dalis. Tai trečiasis agentūros tyrimas, kuriame daugiausia dėmesio skiriama romams. + Druhý prieskum Európskej únie týkajúci sa menšín a diskriminácie (EU-MIDIS II) – vybrané zistenia + 2016-11-29T00:00:00+02:00 + Második európai uniós felmérés a kisebbségekről és a hátrányos megkülönböztetésről (EU-MIDIS II) romákról – Válogatott megállapítások + Zweite Erhebung über Minderheiten und Diskriminierung der Europäischen Union (EU-MIDIS II) Roma – Ausgewählte Ergebnisse + Správa je založená na prieskume, v ktorom sa zhromaždili informácie o takmer 34 000 osobách žijúcich v rómskych domácnostiach v deviatich členských štátoch Európskej únie (EÚ), ktoré boli odvodené z takmer 8 000 osobných rozhovorov s Rómami. Predstavuje výber výsledkov druhého prieskumu Európskej únie o menšinách a diskriminácii (EU-MIDIS II), v ktorom sa zúčastnilo približne 26 000 ľudí s prisťahovaleckým alebo etnickým menšinám žijúcich v EÚ. Prieskum Európskej únie týkajúci sa menšín a diskriminácie je hlavnou súčasťou záväzku agentúry zhromažďovať a zverejňovať údaje o skupinách, ktoré nie sú zahrnuté v prieskumoch všeobecnej populácie. Je to tretí prieskum agentúry, ktorý sa zameriava na Rómov. + Zpráva vychází z průzkumu, který shromáždil informace o téměř 34 000 osob žijících v romských domácnostech v devíti členských státech Evropské unie (EU), které byly získány z téměř 8 000 osobních rozhovorů s Romy. Představuje výběr výsledků druhého průzkumu o menšinách a diskriminaci v Evropské unii (EU-MIDIS II), který provedl agentura FRA a který zkoumal přibližně 26 000 osob z prostředí přistěhovalců nebo etnických menšin žijících v EU. Průzkum Evropské unie týkající se menšin a diskriminace je významnou součástí závazku agentury shromažďovat a zveřejňovat údaje o skupinách, které nejsou zahrnuty v průzkumech obecné populace. Je to třetí průzkum agentury, který se zaměřuje na Romy. + Δεύτερη έρευνα της Ευρωπαϊκής Ένωσης για τις μειονότητες και τις διακρίσεις (EU-MIDIS II) Ρομά — Επιλεγμένα πορίσματα + O relatório baseia-se num inquérito que recolheu informações sobre quase 34 000 pessoas que vivem em agregados familiares ciganos em nove Estados-Membros da União Europeia (UE), provenientes de cerca de 8 000 entrevistas presenciais com ciganos. Apresenta uma seleção de resultados do Segundo Inquérito da FRA sobre Minorias e Discriminação na União Europeia (EU-MIDIS II), que inquiriu cerca de 26 000 pessoas oriundas de imigrantes ou minorias étnicas residentes na UE. O Inquérito sobre Minorias e Discriminação na União Europeia constitui uma parte importante do compromisso da agência de recolher e publicar dados sobre grupos não abrangidos por inquéritos à população em geral. É o terceiro inquérito da agência a centrar-se nos ciganos. + Tweede enquête van de Europese Unie naar minderheden en discriminatie (EU-MIDIS II) Roma — Geselecteerde bevindingen + + Teine Euroopa Liidu vähemuste ja diskrimineerimise uuring (EU-MIDIS II) Roma – Valitud järeldused + Ziņojuma pamatā ir apsekojums, kurā apkopota informācija par gandrīz 34,000 personām, kas dzīvo romu mājsaimniecībās deviņās Eiropas Savienības (ES) dalībvalstīs, un kas iegūta no gandrīz 8000 klātienes intervijām ar romiem. Tajā apkopoti FRA otrā Eiropas Savienības minoritāšu un diskriminācijas apsekojuma (EU-MIDIS II) rezultāti, kas apsekoja aptuveni 26 000 cilvēku ar imigrantu vai etnisko minoritāšu izcelsmi, kuri dzīvo ES. Eiropas Savienības minoritāšu un diskriminācijas apsekojums ir būtiska daļa no aģentūras apņemšanās vākt un publicēt datus par grupām, kas nav iekļautas iedzīvotāju vispārējos apsekojumos. Tas ir trešais aģentūras pētījums, kurā galvenā uzmanība pievērsta romiem. + + It-Tieni Stħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni (EU-MIDIS II) Roma — Sejbiet magħżula + Докладът се основава на проучване, което събра информация за почти 34 000 души, живеещи в ромски домакинства в девет държави — членки на Европейския съюз (ЕС), получена от близо 8000 преки интервюта с роми. В него са представени подбрани резултати от второто проучване на FRA относно малцинствата и дискриминацията в Европейския съюз (EU-MIDIS II), което изследва около 26 000 души с имигрантски произход или етнически малцинства, живеещи в ЕС. Проучването на малцинствата и дискриминацията в Европейския съюз е основна част от ангажимента на агенцията да събира и публикува данни за групи, които не са включени в общите проучвания на населението. Това е третото проучване на агенцията, което се фокусира върху ромите. + Seconda indagine sulle minoranze e le discriminazioni nell'Unione europea (EU-MIDIS II) Roma — Risultati selezionati + + + In der zweiten Erhebung über Minderheiten und Diskriminierungen der Europäischen Union wurden Informationen von mehr als 25 500 Befragten mit unterschiedlichem ethnischen Minderheiten- und Migrationshintergrund in allen 28 EU-Mitgliedstaaten erfasst. + + Seconda indagine sulle minoranze e le discriminazioni nell'Unione europea + Per antrąjį Europos Sąjungos mažumų ir diskriminacijos tyrimą surinkta daugiau kaip 25 500 respondentų, turinčių skirtingą etninę mažumą ir imigrantų kilmę visose 28 ES valstybėse narėse. + 1.0 + + + + + + Druga raziskava o manjšinah in diskriminaciji Evropske unije (EU-MIDIS II) muslimani – izbrane ugotovitve + В доклада се разглежда как характеристиките — като например собственото и фамилното име, цвета на кожата и носенето на видими религиозни символи като забрадка — могат да доведат до дискриминационно отношение и тормоз. + In dem Bericht wird untersucht, wie Merkmale wie Vor- und Nachname eines Individuums, Hautfarbe und das Tragen sichtbarer religiöser Symbole wie beispielsweise Kopftuch diskriminierende Behandlungen und Belästigungen auslösen können. + Dara Suirbhé an Aontais Eorpaigh ar Mhionlaigh agus ar Idirdhealú (EU-MIDIS II) — Torthaí roghnaithe + + + Aruandes uuritakse, kuidas omadused, nagu näiteks isiku ees- ja perekonnanimi, nahavärv ja nähtavate ususümbolite, näiteks pearäti kandmine, võivad põhjustada diskrimineerivat kohtlemist ja ahistamist. + + In het verslag wordt onderzocht hoe kenmerken — zoals de voor- en achternaam van een individu, huidskleur en het dragen van zichtbare religieuze symbolen zoals een hoofddoek — tot discriminatie en intimidatie kunnen leiden. + It-Tieni Stħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni (EU-MIDIS II) Musulmani — Sejbiet magħżula + Η έκθεση εξετάζει τον τρόπο με τον οποίο χαρακτηριστικά — όπως το όνομα και το επώνυμο ενός ατόμου, το χρώμα του δέρματος και η χρήση ορατών θρησκευτικών συμβόλων όπως η μαντίλα, για παράδειγμα — μπορούν να προκαλέσουν διακριτική μεταχείριση και παρενόχληση. + + Deuxième enquête de l’Union européenne sur les minorités et la discrimination (EU-MIDIS II) Musulmans — Résultats sélectionnés + 2017-12-19T11:41:41.537188+02:00 + Segundo Inquérito sobre Minorias e Discriminação na União Europeia (EU-MIDIS II) Muçulmanos — Conclusões selecionadas + Drugo istraživanje o manjinama i diskriminaciji Europske unije (EU-MIDIS II) muslimani – odabrani nalazi + Zpráva zkoumá, jak mohou vlastnosti, jako je například jméno a příjmení jednotlivce, barva kůže a nošení viditelných náboženských symbolů, jako je šátek, vést k diskriminačnímu zacházení a obtěžování. + Ir-rapport jeżamina kif il-karatteristiċi — bħall-isem u l-kunjom ta’ individwu, il-kulur tal-ġilda u l-ilbies ta’ simboli reliġjużi viżibbli bħal velu tar-ras, pereżempju — jistgħu jwasslu għal trattament diskriminatorju u fastidju. + Teine Euroopa Liidu vähemuste ja diskrimineerimise uuring (EU-MIDIS II) moslemid – valitud järeldused + Europeiska unionens andra undersökning av minoriteter och diskriminering (EU-MIDIS II) muslimer – utvalda resultat + Le rapport examine comment des caractéristiques — comme le prénom et le nom de famille d’une personne, la couleur de la peau et le port de symboles religieux visibles comme le foulard, par exemple — peuvent déclencher un traitement discriminatoire et du harcèlement. + The report examines how characteristics – such as an individual's first and last name, skin colour and the wearing of visible religious symbols like a headscarf, for example – may trigger discriminatory treatment and harassment. + Raportissa tarkastellaan, miten esimerkiksi henkilön etu- ja sukunimi, ihon väri ja näkyvien uskonnollisten symbolien, kuten huivin, käyttö voivat johtaa syrjivään kohteluun ja häirintään. + Al doilea sondaj al Uniunii Europene privind minoritățile și discriminarea (EU-MIDIS II) Musulmanii – constatări selectate + Seconda indagine sulle minoranze e le discriminazioni nell'Unione europea (EU-MIDIS II) Musulmani — Risultati selezionati + Druhý prieskum Európskej únie týkajúci sa menšín a diskriminácie (EU-MIDIS II) moslimovia – vybrané zistenia + Второ проучване на малцинствата и дискриминацията в Европейския съюз (EU-MIDIS II) мюсюлмани — избрани констатации + + Második európai uniós felmérés a kisebbségekről és a hátrányos megkülönböztetésről (EU-MIDIS II) muzulmánok – Válogatott megállapítások + A jelentés azt vizsgálja, hogy a jellemzők – például az egyén kereszt- és utóneve, bőrszíne és látható vallási szimbólumok, például fejkendő viselése – milyen módon idézhetnek elő megkülönböztető bánásmódot és zaklatást. + + O relatório analisa como características — como o nome e o apelido de um indivíduo, a cor da pele e o uso de símbolos religiosos visíveis como um lenço de cabeça, por exemplo — podem desencadear tratamento discriminatório e assédio. + Zweite Umfrage der Europäischen Union für Minderheiten und Diskriminierung (EU-MIDIS II) Muslime – Ausgewählte Ergebnisse + + http://data.europa.eu/88u/distribution/fc386e52-8f29-4f15-9ec0-1f08d4499fa1 + Ataskaitoje nagrinėjama, kaip charakteristikos, pvz., asmens vardas ir pavardė, odos spalva ir matomų religinių simbolių, pvz., skarelės, dėvėjimas, gali sukelti diskriminacinį elgesį ir priekabiavimą. + W sprawozdaniu przeanalizowano, w jaki sposób cechy charakterystyczne – takie jak imię i nazwisko danej osoby, kolor skóry i noszenie widocznych symboli religijnych, takich jak chusta na głowy – mogą spowodować dyskryminujące traktowanie i nękanie. + Δεύτερη έρευνα της Ευρωπαϊκής Ένωσης για τις μειονότητες και τις διακρίσεις (EU-MIDIS II) Μουσουλμάνοι — Επιλεγμένα πορίσματα + Ziņojumā aplūkots, kā īpašības, piemēram, personas vārds un uzvārds, ādas krāsa un redzamu reliģisku simbolu, piemēram, galvas lakata, valkāšana, var izraisīt diskriminējošu attieksmi un uzmākšanos. + Antrasis Europos Sąjungos mažumų ir diskriminacijos tyrimas (EU-MIDIS II) Musulmonai. Atrinktos išvados + Rapporten undersøger, hvordan karakteristika — som f.eks. en persons for- og efternavn, hudfarve og brug af synlige religiøse symboler som f.eks. et hovedtørklæde — kan udløse forskelsbehandling og chikane. + La relazione esamina come caratteristiche — come ad esempio il nome e il cognome di un individuo, il colore della pelle e l'uso di simboli religiosi visibili come un velo, ad esempio — possano innescare trattamenti discriminatori e molestie. + Toinen Euroopan unionin vähemmistöjä ja syrjintää koskeva tutkimus (EU-MIDIS II) Muslimit – Valitut havainnot + 2017-09-21T00:00:00+02:00 + U izvješću se ispituje kako obilježja, kao što su ime i prezime pojedinca, boja kože i nošenje vidljivih vjerskih simbola poput marame za glavu, mogu izazvati diskriminatorno postupanje i uznemiravanje. + Den Europæiske Unions anden undersøgelse af mindretal og forskelsbehandling (EU-MIDIS II) muslimer — udvalgte resultater + Scrúdaítear sa tuarascáil conas a d’fhéadfadh tréithe — amhail céadainm agus sloinne an duine aonair, dath craicinn agus siombailí reiligiúnacha infheicthe amhail carf cinn, mar shampla — a bheith ina gcúis le cóireáil agus ciapadh idirdhealaitheach. + Second European Union Minorities and Discrimination Survey (EU-MIDIS II) Muslims – Selected findings + Segundo Estudio sobre las minorías y la discriminación de la Unión Europea (EU-MIDIS II) Musulmanes — Conclusiones seleccionadas + V správe sa skúma, ako môžu charakteristiky, ako napríklad meno a priezvisko jednotlivca, farba kože a nosenie viditeľných náboženských symbolov, ako je napríklad šatka, vyvolať diskriminačné zaobchádzanie a obťažovanie. + + I rapporten undersöks hur egenskaper – t.ex. en individs för- och efternamn, hudfärg och synliga religiösa symboler som t.ex. huvudduk – kan utlösa diskriminerande behandling och trakasserier. + Poročilo preučuje, kako lahko značilnosti – na primer ime in priimek posameznika, barva kože in nošenje vidnih verskih simbolov, kot je na primer naglavna ruta – sprožijo diskriminatorno obravnavo in nadlegovanje. + Drugie badanie Unii Europejskiej na temat mniejszości i dyskryminacji (EU-MIDIS II) muzułmanie – wybrane ustalenia + Tweede enquête over minderheden en discriminatie van de Europese Unie (EU-MIDIS II) moslims — Geselecteerde bevindingen + Otrais Eiropas Savienības minoritāšu un diskriminācijas apsekojums (EU-MIDIS II) musulmaņi — atlasīti konstatējumi + Raportul examinează modul în care caracteristicile – cum ar fi numele și prenumele unei persoane, culoarea pielii și purtarea de simboluri religioase vizibile, cum ar fi un văl, de exemplu – pot declanșa tratament discriminatoriu și hărțuire. + Druhý průzkum menšin a diskriminace v Evropské unii (EU-MIDIS II) muslimové – Vybraná zjištění + En el informe se examina cómo las características -por ejemplo, el nombre y apellido de una persona, el color de la piel y el uso de símbolos religiosos visibles como el pañuelo en la cabeza- pueden desencadenar un trato discriminatorio y acoso. + + + + + + Drugo istraživanje Europske unije o manjinama i diskriminaciji + + + + + + + Druhý průzkum Evropské unie týkající se menšin a diskriminace shromáždil informace od více než 25 500 respondentů z různých etnických menšin a přistěhovalců ve všech 28 členských státech EU. + + + + Teine Euroopa Liidu vähemuste ja diskrimineerimise uuring + Drugie badanie Unii Europejskiej na temat mniejszości i dyskryminacji zgromadziło informacje od ponad 25 500 respondentów z różnych mniejszości etnicznych i imigrantów we wszystkich 28 państwach członkowskich UE. + + + Rossalina Latcheva (PhD) + + + http://fra.europa.eu/en/project/2015/eu-midis-ii-european-union-minorities-and-discrimination-survey/contact + + + + + + + + + + Zweite Erhebung über Minderheiten und Diskriminierung in der Europäischen Union + EUODP Legacy->DEU xslt, version:20220321 + Teise Euroopa Liidu vähemuste ja diskrimineerimise uuringu käigus koguti teavet enam kui 25 500 erineva etnilise vähemuse ja sisserändaja taustaga vastajalt kõigis 28 ELi liikmesriigis. + + + Dara Suirbhé ar Mhionlaigh agus ar Idirdhealú an Aontais Eorpaigh — Mná Imirceacha — torthaí roghnaithe + + Η δεύτερη έρευνα του FRA για τις μειονότητες και τις διακρίσεις (EU-MIDIS II) συγκέντρωσε πληροφορίες από περισσότερους από 25.000 ερωτηθέντες με διαφορετικές εθνοτικές μειονότητες και μεταναστευτικές καταβολές και στα 28 κράτη μέλη της ΕΕ. Τα κύρια πορίσματα της έρευνας, που δημοσιεύθηκε το 2017, κατέδειξαν ορισμένες διαφορές στον τρόπο με τον οποίο οι γυναίκες και οι άνδρες με μεταναστευτικό υπόβαθρο σε ολόκληρη την Ευρωπαϊκή Ένωση (ΕΕ) βιώνουν τον τρόπο με τον οποίο γίνονται σεβαστά τα δικαιώματά τους. Η παρούσα έκθεση συνοψίζει ορισμένα από τα πλέον συναφή πορίσματα της έρευνας, τα οποία καταδεικνύουν την ανάγκη για στοχευμένα και ευαισθητοποιημένα ως προς το φύλο μέτρα που προωθούν την ένταξη — ειδικά- των γυναικών που είναι μετανάστριες ή κατιόντες μεταναστών. + Zweite Erhebung über Minderheiten und Diskriminierung in der Europäischen Union – Migrantinnen – ausgewählte Ergebnisse + Al doilea sondaj al FRA privind minoritățile și discriminarea în UE (EU-MIDIS II) a colectat informații de la peste 25 000 de respondenți care provin din minorități etnice și imigranți din toate cele 28 de state membre ale UE. Principalele constatări ale sondajului, publicat în 2017, au evidențiat o serie de diferențe în ceea ce privește modul în care femeile și bărbații care provin din familii de imigranți din Uniunea Europeană (UE) se confruntă cu modul în care drepturile lor sunt respectate. Prezentul raport sintetizează unele dintre cele mai relevante constatări ale sondajului în această privință, care arată necesitatea unor măsuri specifice, care să țină seama de dimensiunea de gen, care să promoveze integrarea – în special – a femeilor care sunt imigranți sau descendenți ai imigranților. + Deuxième enquête de l’Union européenne sur les minorités et la discrimination — Femmes migrantes — résultats sélectionnés + Druhý průzkum agentury FRA týkající se menšin a diskriminace v EU (EU-MIDIS II) shromáždil informace od více než 25 000 respondentů z různých etnických menšin a přistěhovalců ve všech 28 členských státech EU. Hlavní zjištění průzkumu, který byl zveřejněn v roce 2017, poukázala na řadu rozdílů ve způsobu, jakým ženy a muži z přistěhovaleckého prostředí v celé Evropské unii (EU) pociťují dodržování jejich práv. Tato zpráva shrnuje některá z nejdůležitějších zjištění průzkumu v tomto ohledu, která ukazují potřebu cílených opatření zohledňujících rovnost žen a mužů, která podporují integraci – konkrétně – žen, které jsou přistěhovalci nebo potomky přistěhovalců. + Druhý průzkum Evropské unie týkající se menšin a diskriminace – migrantky – vybraná zjištění + Δεύτερη έρευνα της Ευρωπαϊκής Ένωσης για τις μειονότητες και τις διακρίσεις — Διακινούμενες γυναίκες — επιλεγμένα πορίσματα + FRA:s andra EU-undersökning om minoriteter och diskriminering (EU-Midis II) samlade in information från över 25 000 svarande med olika etnisk minoritets- och invandrarbakgrund i alla 28 EU-medlemsstater. De viktigaste resultaten från undersökningen, som offentliggjordes 2017, pekade på ett antal skillnader i hur kvinnor och män med invandrarbakgrund i Europeiska unionen (EU) upplever hur deras rättigheter respekteras. I denna rapport sammanfattas några av de mest relevanta undersökningsresultaten i detta avseende, som visar att det behövs riktade, jämställdhetsinriktade åtgärder som främjar integrationen av – särskilt – kvinnor som är invandrare eller avkomlingar till invandrare. + La seconda indagine della FRA sulle minoranze e le discriminazioni nell'UE (EU-MIDIS II) ha raccolto informazioni da oltre 25 000 rispondenti con diverse minoranze etniche e contesti migratori in tutti i 28 Stati membri dell'UE. I principali risultati dell'indagine, pubblicata nel 2017, hanno evidenziato una serie di differenze nel modo in cui le donne e gli uomini provenienti da un contesto migratorio nell'Unione europea (UE) sperimentano il modo in cui i loro diritti sono rispettati. La presente relazione riassume alcuni dei risultati dell'indagine più rilevanti al riguardo, che evidenziano la necessità di misure mirate e sensibili alla dimensione di genere che promuovano l'integrazione — in particolare — delle donne immigrate o discendenti di immigrati. + Drugo istraživanje Europske unije o manjinama i diskriminaciji – Žene migranti – odabrani nalazi + Segundo Inquérito sobre Minorias e Discriminação na União Europeia — Mulheres migrantes — resultados selecionados + Tweede enquête van de Europese Unie naar minderheden en discriminatie — migrantenvrouwen — geselecteerde bevindingen + Antrasis Europos Sąjungos mažumų ir diskriminacijos tyrimas „Moterys migrantės. Atrinktos išvados“ + Otrais Eiropas Savienības minoritāšu un diskriminācijas apsekojums — migrējošas sievietes — atlasīti konstatējumi + FRA antrajame ES mažumų ir diskriminacijos tyrime (EU-MIDIS II) surinkta informacija iš daugiau kaip 25 000 respondentų, turinčių skirtingą etninių mažumų ir imigrantų kilmę visose 28 ES valstybėse narėse. Pagrindinės 2017 m. paskelbtos apklausos išvados parodė, kad imigrantų kilmės moterys ir vyrai visoje Europos Sąjungoje (ES) patiria, kaip gerbiamos jų teisės. Šioje ataskaitoje apibendrinami kai kurie svarbiausi apklausos rezultatai šiuo klausimu, iš kurių matyti, kad reikalingos tikslinės, lyčių aspektu grindžiamos priemonės, kuriomis būtų skatinama visų pirma moterų, kurios yra imigrantės arba imigrantų palikuonys, integracija. + + Europeiska unionens andra undersökning av minoriteter och diskriminering – Migrantkvinnor – utvalda resultat + FRA otrais ES minoritāšu un diskriminācijas apsekojums (EU-MIDIS II) apkopoja informāciju no vairāk nekā 25 000 respondentiem no dažādām etniskajām minoritātēm un imigrantiem visās 28 ES dalībvalstīs. + 2017. gadā publicētā apsekojuma galvenie konstatējumi norādīja uz vairākām atšķirībām Eiropas Savienības (ES) imigrantu izcelsmes sieviešu un vīriešu pieredzē, kā tiek ievērotas viņu tiesības. + Šajā ziņojumā apkopoti daži visnozīmīgākie apsekojuma rezultāti šajā sakarā, kas liecina, ka ir vajadzīgi mērķtiecīgi, dzimumsensitīvi pasākumi, kas īpaši veicina to sieviešu integrāciju, kuras ir imigrantes vai imigrantu pēcnācēji. + La segunda encuesta de la FRA sobre las minorías y la discriminación de la UE (EU-MIDIS II) recogió información de más de 25 000 encuestados con diferentes orígenes étnicos e inmigrantes en los 28 Estados miembros de la UE. Los principales resultados de la encuesta, publicada en 2017, apuntaron a una serie de diferencias en la forma en que las mujeres y los hombres con origen inmigrante en toda la Unión Europea (UE) experimentan el respeto de sus derechos. En el presente informe se resumen algunas de las conclusiones más relevantes de la encuesta a este respecto, que muestran la necesidad de adoptar medidas específicas y que tengan en cuenta las cuestiones de género que promuevan la integración de las mujeres inmigrantes o descendientes de inmigrantes, en particular. + It-tieni stħarriġ tal-FRA dwar il-Minoranzi u d-Diskriminazzjoni tal-UE (EU-MIDIS II) ġabar informazzjoni minn aktar minn 25,000 persuna li wieġbu b’minoranza etnika u sfondi ta’ immigrazzjoni differenti fit-28 Stat Membru kollha tal-UE. Is-sejbiet ewlenin mill-istħarriġ, ippubblikat fl-2017, indikaw għadd ta’ differenzi fil-mod kif in-nisa u l-irġiel bi sfond ta’ immigrazzjoni madwar l-Unjoni Ewropea (UE) jesperjenzaw kif id-drittijiet tagħhom jiġu rispettati. Dan ir-rapport jiġbor fil-qosor uħud mis-sejbiet tal-istħarriġ l-aktar rilevanti f’dan ir-rigward, li juru l-ħtieġa għal miżuri mmirati u sensittivi għall-ġeneru li jippromwovu l-integrazzjoni ta’ — speċifikament — nisa li huma immigranti jew dixxendenti ta’ immigranti. + Druhý prieskum agentúry FRA týkajúci sa menšín a diskriminácie (EU-MIDIS II) zhromaždil informácie od viac ako 25 000 respondentov z rôznych etnických menšín a prisťahovaleckého pôvodu vo všetkých 28 členských štátoch EÚ. Hlavné zistenia prieskumu uverejneného v roku 2017 poukázali na množstvo rozdielov v tom, ako ženy a muži s prisťahovaleckým pôvodom v celej Európskej únii (EÚ) využívajú svoje práva. Táto správa sumarizuje niektoré z najdôležitejších zistení prieskumu v tejto súvislosti, ktoré poukazujú na potrebu cielených, rodovo citlivých opatrení, ktoré podporujú integráciu – konkrétne – žien, ktoré sú prisťahovalcami alebo potomkami prisťahovalcov. + + Drugie badanie Unii Europejskiej na temat mniejszości i dyskryminacji – kobiety migrujące – wybrane wyniki + Perusoikeusviraston toisessa EU:n vähemmistöjä ja syrjintää koskevassa kyselyssä (EU-MIDIS II) kerättiin tietoja yli 25 000 vastaajalta, joilla oli eri etninen vähemmistö- ja maahanmuuttajatausta eri EU:n 28 jäsenvaltiossa. Vuonna 2017 julkaistun tutkimuksen tärkeimmät tulokset osoittivat, että maahanmuuttajataustaiset naiset ja miehet kokevat eri puolilla Euroopan unionia, miten heidän oikeuksiaan kunnioitetaan. Tässä kertomuksessa esitetään yhteenveto tärkeimmistä tutkimuksen tuloksista, jotka osoittavat, että tarvitaan kohdennettuja, sukupuolisensitiivisiä toimenpiteitä, joilla edistetään erityisesti maahanmuuttajanaisten tai maahanmuuttajien jälkeläisten kotoutumista. + Второ проучване на малцинствата и дискриминацията в Европейския съюз — жени мигранти — избрани констатации + + + Den Europæiske Unions anden undersøgelse af mindretal og forskelsbehandling — migrantkvinder — udvalgte resultater + Bhailigh an dara suirbhé de chuid FRA ar Mhionlaigh agus ar Idirdhealú (EU-MIDIS II) faisnéis ó bhreis agus 25,000 freagróir a bhfuil cúlraí mionlaigh eitneacha agus inimirceach éagsúla acu ar fud 28 mBallstát an AE. Léirigh príomhthorthaí an tsuirbhé, a foilsíodh in 2017, go raibh roinnt difríochtaí ann maidir leis an gcaoi a bhfaigheann mná agus fir a bhfuil cúlra inimirceach acu ar fud an Aontais Eorpaigh (AE) taithí ar an gcaoi a n-urramaítear a gcearta. Déanann an tuarascáil seo achoimre ar chuid de na torthaí suirbhé is ábhartha maidir leis seo, a léiríonn an gá atá le bearta spriocdhírithe, atá íogair ó thaobh inscne de, a chuireann lánpháirtiú na mban atá ina n-inimircigh nó sliocht na n-inimirceach chun cinn — go sonrach. + La deuxième enquête de la FRA sur les minorités et la discrimination dans l’UE (EU-MIDIS II) a recueilli des informations auprès de plus de 25 000 personnes ayant des origines ethniques et immigrées différentes dans l’ensemble des 28 États membres de l’UE. Les principales conclusions de l’enquête, publiée en 2017, ont mis en évidence un certain nombre de différences dans la manière dont les femmes et les hommes issus de l’immigration dans l’Union européenne (UE) connaissent le respect de leurs droits. Le présent rapport résume certaines des conclusions les plus pertinentes de l’enquête à cet égard, qui montrent qu’il est nécessaire de prendre des mesures ciblées et tenant compte des sexospécificités qui favorisent l’intégration des femmes immigrées ou des descendants d’immigrants, en particulier. + Druhý prieskum Európskej únie týkajúci sa menšín a diskriminácie – migrantky – vybrané zistenia + http://data.europa.eu/88u/distribution/933bb31c-9531-40ed-b64d-b2d63c7264fc + Az FRA második uniós kisebbségi és hátrányos megkülönböztetés-felmérése (EU-MIDIS II) több mint 25 000 különböző etnikai kisebbséggel és bevándorló háttérrel rendelkező válaszadótól gyűjtött információkat mind a 28 uniós tagállamban. A 2017-ben közzétett felmérés fő megállapításai rámutattak arra, hogy az Európai Unióban a bevándorló hátterű nők és férfiak hogyan tapasztalják meg jogaik tiszteletben tartását. + Ez a jelentés összefoglalja az ezzel kapcsolatos legrelevánsabb felmérés megállapításait, amelyek azt mutatják, hogy célzott, a nemek közötti egyenlőséget figyelembe vevő intézkedésekre van szükség, amelyek előmozdítják különösen a bevándorló vagy bevándorló leszármazott nők integrációját. + O segundo inquérito da FRA sobre as minorias e a discriminação na UE (EU-MIDIS II) recolheu informações de mais de 25 000 inquiridos de diferentes minorias étnicas e imigrantes em todos os 28 Estados-Membros da UE. As principais conclusões do inquérito, publicado em 2017, apontaram para uma série de diferenças na forma como as mulheres e os homens oriundos de imigrantes em toda a União Europeia (UE) experimentam a forma como os seus direitos são respeitados. O presente relatório resume alguns dos resultados mais relevantes do inquérito a este respeito, que mostram a necessidade de medidas específicas e sensíveis às questões de género que promovam a integração — especificamente — das mulheres imigrantes ou descendentes de imigrantes. + Seconda indagine sulle minoranze e le discriminazioni nell'Unione europea — Donne migranti — risultati selezionati + + In het tweede EU-onderzoek naar minderheden en discriminatie (EU-MIDIS II) van het FRA is informatie verzameld van meer dan 25 000 respondenten met verschillende etnische minderheden en immigranten in alle 28 EU-lidstaten. De belangrijkste bevindingen van de enquête, die in 2017 werd gepubliceerd, wezen op een aantal verschillen in de manier waarop vrouwen en mannen met een immigrantenachtergrond in de Europese Unie (EU) ervaren hoe hun rechten worden geëerbiedigd. In dit verslag worden enkele van de meest relevante onderzoeksresultaten in dit verband samengevat, waaruit blijkt dat er behoefte is aan gerichte, gendergevoelige maatregelen die de integratie bevorderen van — met name — vrouwen die immigranten of nakomelingen van immigranten zijn. + FRA’s second EU Minorities and Discrimination survey (EU-MIDIS II) collected information from over 25,000 respondents with different ethnic minority and immigrant backgrounds across all 28 EU Member States. The main findings from the survey, published in 2017, pointed to a number of differences in the way women and men with immigrant backgrounds across the European Union (EU) experience how their rights are respected. This report summarises some of the most relevant survey findings in this regard, which show the need for targeted, gender-sensitive measures that promote the integration of – specifically – women who are immigrants or descendants of immigrants. + FRA teine ELi vähemuste ja diskrimineerimise uuring (EU-MIDIS II) kogus teavet enam kui 25 000 erineva etnilise vähemuse ja sisserändaja taustaga vastajalt kõigis 28 ELi liikmesriigis. 2017. aastal avaldatud uuringu peamised tulemused osutasid mitmetele erinevustele sisserändaja taustaga naiste ja meeste vahel kogu Euroopa Liidus, kuidas nende õigusi austatakse. Käesolevas aruandes võetakse kokku mõned kõige olulisemad uuringutulemused, mis näitavad vajadust sihipäraste sootundlike meetmete järele, mis edendaksid sisserändajatest või sisserändajate järeltulijatest naiste integreerimist. + Al doilea sondaj al Uniunii Europene privind minoritățile și discriminarea – Femeile migrante – constatări selectate + Die zweite EU-Erhebung über Minderheiten und Diskriminierung (EU-MIDIS II) der FRA sammelte Informationen von über 25 000 Befragten mit unterschiedlichem ethnischen Minderheiten- und Migrationshintergrund in allen 28 EU-Mitgliedstaaten. Die wichtigsten Ergebnisse der 2017 veröffentlichten Umfrage zeigten eine Reihe von Unterschieden in der Art und Weise, wie Frauen und Männer mit Migrationshintergrund in der Europäischen Union (EU) erfahren, wie ihre Rechte geachtet werden. Dieser Bericht fasst einige der wichtigsten Umfrageergebnisse in dieser Hinsicht zusammen, die zeigen, dass gezielte, geschlechtsspezifische Maßnahmen erforderlich sind, die die Integration von Frauen, die Einwanderer oder Nachkommen von Einwanderern sind, fördern. + Teine Euroopa Liidu vähemuste ja diskrimineerimise uuring – sisserändajatest naised – valitud järeldused + Second European Union Minorities and Discrimination Survey - Migrant women - selected findings + Drugie badanie Agencji Praw Podstawowych UE na temat mniejszości i dyskryminacji (EU-MIDIS II) zgromadziło informacje od ponad 25 000 respondentów z różnych mniejszości etnicznych i środowisk imigrantów we wszystkich 28 państwach członkowskich UE. Główne wyniki badania opublikowanego w 2017 r. wskazują na szereg różnic w sposobie, w jaki kobiety i mężczyźni ze środowisk imigrantów w całej Unii Europejskiej (UE) doświadczają poszanowania ich praw. W niniejszym sprawozdaniu podsumowano niektóre z najistotniejszych wyników badań w tym zakresie, które wskazują na potrzebę ukierunkowanych, uwzględniających aspekt płci środków wspierających integrację – w szczególności – kobiet będących imigrantkami lub potomkami imigrantów. + Fra's anden EU-undersøgelse om mindretal og forskelsbehandling (EU-MIDIS II) indsamlede oplysninger fra over 25 000 respondenter med forskellig etnisk minoritets- og indvandrerbaggrund i alle 28 EU-medlemsstater. De vigtigste resultater af undersøgelsen, der blev offentliggjort i 2017, pegede på en række forskelle i den måde, hvorpå kvinder og mænd med indvandrerbaggrund i Den Europæiske Union (EU) oplever, hvordan deres rettigheder respekteres. Denne rapport opsummerer nogle af de mest relevante undersøgelsesresultater i denne henseende, som viser behovet for målrettede, kønssensitive foranstaltninger, der fremmer integrationen af — navnlig — kvinder, der er indvandrere eller efterkommere af indvandrere. + Második európai uniós felmérés a kisebbségekről és a hátrányos megkülönböztetésről – Migráns nők – kiválasztott megállapítások + Drugo istraživanje Agencije Europske unije za temeljna prava o manjinama i diskriminaciji (EU-MIDIS II) prikupilo je informacije od više od 25 000 ispitanika različitog etničkog i imigrantskog podrijetla u svih 28 država članica EU-a. Glavni rezultati istraživanja, objavljeni 2017. godine, ukazali su na brojne razlike u načinu na koji žene i muškarci imigrantskog podrijetla diljem Europske unije (EU) doživljavaju svoja prava. U ovom se izvješću sažimaju neki od najvažnijih rezultata istraživanja u tom pogledu, koji pokazuju potrebu za ciljanim, rodno osjetljivim mjerama kojima se promiče integracija žena koje su imigrantice ili potomci imigranata. + Второто проучване на FRA относно малцинствата и дискриминацията в ЕС (EU-MIDIS II) събра информация от над 25 000 респонденти с различен етнически произход и имигрантски произход във всички 28 държави — членки на ЕС. Основните констатации от проучването, публикувано през 2017 г., показват редица различия в начина, по който жените и мъжете с имигрантски произход в Европейския съюз (ЕС) изпитват как се зачитат техните права. В настоящия доклад се обобщават някои от най-важните констатации от проучването в това отношение, които показват необходимостта от целенасочени, съобразени с пола мерки, които насърчават интеграцията по-специално на жените имигранти или потомци на имигранти. + Toinen Euroopan unionin vähemmistöjä ja syrjintää koskeva tutkimus – maahanmuuttajanaiset – valitut tulokset + Segunda encuesta de la Unión Europea sobre las minorías y la discriminación — Mujeres migrantes: conclusiones seleccionadas + It-Tieni Stħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni — Nisa migranti — sejbiet magħżula + Agencija FRA je v drugi raziskavi EU o manjšinah in diskriminaciji (EU-MIDIS II) zbrala informacije od več kot 25 000 anketirancev iz različnih etničnih manjšin in priseljencev iz vseh 28 držav članic EU. Glavne ugotovitve raziskave, objavljene leta 2017, so opozorile na številne razlike v tem, kako ženske in moški s priseljenskim ozadjem v Evropski uniji (EU) doživljajo, kako se spoštujejo njihove pravice. To poročilo povzema nekatere najpomembnejše ugotovitve raziskave v zvezi s tem, ki kažejo potrebo po ciljno usmerjenih ukrepih, ki upoštevajo vidik spola in spodbujajo vključevanje – zlasti – žensk, ki so priseljenke ali potomke priseljencev. + Druga raziskava Evropske unije o manjšinah in diskriminaciji – ženske migrantke – izbrane ugotovitve + + + + + + Segundo Inquérito sobre Minorias e Discriminação na União Europeia + + + + + + + + + 2016-07-31T00:00:00 + 2015-10-01T00:00:00 + + + + + + + + + + Europeiska unionens andra undersökning av minoriteter och diskriminering + + + + Druhý průzkum o menšinách a diskriminaci v Evropské unii + + + + + + + It-Tieni Stħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni + + It-tieni Stħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni ġabar informazzjoni minn aktar minn 25,500 rispondent b’minoranza etnika u sfondi ta’ immigrazzjoni differenti fit-28 Stat Membru kollha tal-UE. + + + + Denne rapport beskriver udvalgte resultater fra FRA’s anden store EU-dækkende undersøgelse + af migranter og mindretal (EU-MIDIS II). Den undersøger erfaringerne fra næsten 6 000 personer af afrikansk herkomst i 12 EU-medlemsstater. Resultaterne viser, at snart tyve år efter vedtagelsen af EU’s lovgivning om forbud mod forskelsbehandling møder personer af afrikansk herkomst i EU udbredte og rodfæstede fordomme og udstødelse. + Ser negro en la UE — Resumen + Dieser Bericht präsentiert ausgewählte Ergebnisse aus der zweiten groß angelegten EU-weiten + Erhebung der FRA zu Migranten und Minderheiten (EU-MIDIS II). Darin werden die Erfahrungen + von fast 6 000 Menschen afrikanischer Abstammung in zwölf EU-Mitgliedstaaten untersucht. + Die Ergebnisse machen deutlich, dass Menschen afrikanischer Abstammung in der EU auch fast + 20 Jahre nach dem Erlass von EU-Rechtsvorschriften zum Verbot von Diskriminierung noch auf + ausgeprägte und tief sitzende Vorurteile und Ausgrenzung stoßen. + En toda la UE, los afrodescendientes se enfrentan a prejuicios y exclusiones generalizados y arraigados. La discriminación racial y el acoso son comunes. Las experiencias con violencia racista varían, pero alcanzan hasta el 14 %. La elaboración de perfiles discriminatorios por parte de la policía es una realidad común. Los obstáculos a la inclusión son polifacéticos, especialmente cuando se trata de buscar empleo y vivienda. + Mustanahaline olemine ELis – kokkuvõte + Le présent rapport met en lumière les résultats de la deuxième enquête de grande ampleur + de l’Union européenne sur les minorités et la discrimination (EU-MIDIS II) menée par la FRA sur l’ensemble du territoire de l’UE. Il analyse les expériences de près de 6 000 personnes d’ascendance africaine dans 12 États membres de l’UE. Les résultats montrent que, près de vingt ans après l’adoption de la législation de l’UE interdisant la discrimination, les personnes d’ascendance africaine dans l’UE doivent faire face à des préjugés largement répandus et fermement ancrés ainsi qu’à l’exclusion. + Fiind negru în UE – Rezumat + Būvimas juodaodžiu ES. Santrauka + I rapporten beskrivs utvalda resultat från FRA:s andra stora EU-undersökning av migranter och minoriteter (EU-Midis II). I rapporten undersöks erfarenheterna hos nästan 6 000 personer av afrikansk härkomst i tolv EU-medlemsstater. Resultaten visar att personer av afrikansk härkomst utsätts för allmänt utbredda och inrotade fördomar och utestängning i EU, nästan 20 år efter antagandet av EU-lagstiftning som förbjuder diskriminering. + Kogu ELis seisavad Aafrika päritolu inimesed silmitsi laialt levinud ja juurdunud eelarvamuste ja tõrjutusega. Rassiline diskrimineerimine ja ahistamine on tavalised. Rassistliku vägivallaga seotud kogemused on erinevad, kuid ulatuvad 14 %-ni. Diskrimineeriv profileerimine politsei poolt on tavaline reaalsus. Kaasamisele seatud takistused on mitmetahulised, eriti mis puudutab töökohtade ja eluaseme otsimist. + Tässä raportissa esitellään yhteenveto Euroopan unionin perusoikeusviraston (FRA) toisesta laajamittaisesta EU:n laajuisesta maahanmuuttajia ja vähemmistöjä koskevasta kyselytutkimuksesta (EU-MIDIS II). Siinä tarkastellaan lähes 6 000 afrikkalaistaustaisen henkilön kokemuksia EU:n 12 jäsenvaltiossa. Tulokset osoittavat, että EU:ssa asuvat afrikkalaistaustaiset henkilöt kärsivät laajamittaisista ja sitkeistä ennakkoluuloista ja syrjäytymisestä vielä lähes 20 vuotta EU:n syrjinnänvastaisen lainsäädännön hyväksymisen jälkeen. + http://data.europa.eu/88u/distribution/21ea3195-9a32-4b4b-a71a-fd53e125122c + Biti črna v EU – povzetek + Bycie czarnym w UE – podsumowanie + Als Schwarzer in der EU leben Zweite Erhebung der Europäischen Union zu Minderheiten und Diskriminierung - Zusammenfassung + Tummaihoisena EU:ssa Toinen Euroopan unionin vähemmistöjä ja syrjintää koskeva tutkimus - Tiivistelmä + La presente relazione illustra i risultati selezionati della seconda indagine su larga scala dell’Agenzia su migranti e minoranze (EU-MIDIS II), esaminando le esperienze di quasi 6 000 persone di origine africana in 12 Stati membri dell’UE. I risultati mostrano che, quasi vent’anni dopo l’adozione di leggi dell’UE che vietano la discriminazione, le persone di origine africana nell’UE si trovano ad affrontare pregiudizi ed esclusione diffusi e radicati. + Όντας μαύρος στην ΕΕ — Περίληψη + Across the EU, people of African descent face widespread and entrenched prejudice and exclusion. Racial discrimination and harassment are commonplace. Experiences with racist violence vary, but reach as high as 14 %. Discriminatory profiling by the police is a common reality. Hurdles to inclusion are multi-faceted, particularly when it comes to looking for jobs and housing. + Essere di colore nell’UE Seconda indagine su minoranze e discriminazioni nell’Unione europea - Sommario + W całej UE osoby pochodzenia afrykańskiego borykają się z powszechnymi i zakorzenionymi uprzedzeniami i wykluczeniem. Dyskryminacja rasowa i molestowanie są powszechne. Doświadczenia z przemocą rasistowską różnią się, ale sięgają nawet 14 %. Dyskryminujące profilowanie przez policję jest wspólną rzeczywistością. Przeszkody w integracji są wieloaspektowe, zwłaszcza jeśli chodzi o poszukiwanie pracy i mieszkalnictwo. + + In de hele EU worden mensen van Afrikaanse afkomst geconfronteerd met wijdverspreide en verankerde vooroordelen en uitsluiting. Rassendiscriminatie en intimidatie zijn gemeengoed. De ervaringen met racistisch geweld lopen uiteen, maar bereiken maar liefst 14 %. Discriminerende profilering door de politie is een gemeenschappelijke realiteit. Belemmeringen voor inclusie zijn veelzijdig, met name als het gaat om het zoeken naar werk en huisvesting. + + В целия ЕС хората от африкански произход са изправени пред широко разпространени и утвърдени предразсъдъци и изолация. Расовата дискриминация и тормозът са често срещани. Опитът с расисткото насилие варира, но достига 14 %. Дискриминационното профилиране от страна на полицията е често срещана реалност. Пречките пред приобщаването са многостранни, особено когато става въпрос за търсене на работа и жилищно настаняване. + Bheith Dubh san Aontas Eorpach — Achoimre + Visā ES afrikāņu izcelsmes cilvēki saskaras ar plaši izplatītiem un iesakņojušiem aizspriedumiem un atstumtību. Rasu diskriminācija un uzmākšanās ir ikdienišķa parādība. Pieredze ar rasistisku vardarbību atšķiras, bet sasniedz pat 14 %. Diskriminējoša profilēšana, ko veic policija, ir izplatīta realitāte. + Šķēršļi iekļaušanai ir daudzšķautņaini, jo īpaši attiecībā uz darba un mājokļu meklējumiem. + Dan ir-rapport jiddeskrivi fil-qosor xi riżultati magħżula mit-tieni stħarriġ tal-FRA fuq skala kbira madwar l-UE rigward il-migranti u l-minoranzi (EU-MIDIS II). Huwa jeżamina l-esperjenzi ta’ kważi 6,000 persuna ta’ dixxendenza Afrikana fi 12-il Stat Membru tal-UE. Ir-riżultati juru li, kważi għoxrin sena wara l-adozzjoni ta’ liġijiet tal-UE li jipprojbixxu d-diskriminazzjoni, persuni ta’ dixxendenza Afrikana fl-UE għadhom jiffaċċjaw preġudizzju u esklużjoni mifruxa u stabbiliti. + Ar fud an Aontais, tá réamhchlaonadh agus eisiamh forleathan agus buanseasmhach os comhair daoine de bhunadh na hAfraice. Tá idirdhealú ciníoch agus ciapadh coitianta. Tá éagsúlacht ann maidir le taithí ar fhoréigean ciníoch, ach bíonn siad chomh hard le 14 %. Is gnáthphróifíliú ag na póilíní é próifíliú idirdhealaitheach a dhéanamh. Tá constaicí ilghnéitheacha ar chuimsiú, go háirithe maidir le poist agus tithíocht a lorg. + + + + Feketeség az EU-ban – Összefoglaló + Byť čiernou v EÚ – zhrnutie + Biti crnac u EU-u – Sažetak + At være sort i EU Den Europæiske Unions anden undersøgelse af mindretal og forskelsbehandling - Resumé + Li Tkun Iswed fl-UE It-Tieni Stħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni - Sommarju + Att vara svart i EU Europeiska unionens andra undersökning av minoriteter och diskriminering - Sammanfattning + Ser negro na União Europeia Segundo Inquérito sobre Minorias e Discriminação na União Europeia - Resumo + + Being Black in the EU - Summary + Être noir dans l’UE Deuxième enquête de l’Union européenne sur les minorités et la discrimination - Résumé + Být černoch v EU – shrnutí + Да бъдеш черен в ЕС — Резюме + În întreaga UE, persoanele de origine africană se confruntă cu prejudecăți și excluziune larg răspândite și înrădăcinate. Discriminarea rasială și hărțuirea sunt obișnuite. Experiențele cu violența rasistă variază, dar ajung până la 14 %. Crearea de profiluri discriminatorii de către poliție este o realitate comună. Obstacolele în calea incluziunii sunt multiple, în special în ceea ce privește căutarea de locuri de muncă și de locuințe. + Visoje ES Afrikos kilmės žmonės susiduria su plačiai paplitusiu ir įtvirtintu išankstiniu nusistatymu ir atskirtimi. Rasinė diskriminacija ir priekabiavimas yra įprastas dalykas. Patirtis su rasistiniu smurtu skiriasi, bet pasiekia net 14 %. Policijos vykdomas diskriminacinis profiliavimas yra įprasta realybė. Įtraukties kliūtys yra daugialypės, ypač kalbant apie darbo vietų ir būsto paiešką. + Σε ολόκληρη την ΕΕ, τα άτομα αφρικανικής καταγωγής αντιμετωπίζουν εκτεταμένες και παγιωμένες προκαταλήψεις και αποκλεισμό. Οι φυλετικές διακρίσεις και η παρενόχληση είναι συνηθισμένες. Οι εμπειρίες με ρατσιστική βία ποικίλλουν, αλλά φτάνουν μέχρι και το 14 %. Η μεροληπτική κατάρτιση προφίλ από την αστυνομία αποτελεί κοινή πραγματικότητα. Τα εμπόδια για την ένταξη είναι πολύπλευρα, ιδίως όσον αφορά την αναζήτηση θέσεων εργασίας και στέγασης. + V celé EU čelí lidé afrického původu rozšířeným a zakořeněným předsudkům a vyloučením. Rasová diskriminace a obtěžování jsou samozřejmostí. Zkušenosti s rasistickým násilím se liší, ale dosahují až 14 %. Diskriminační profilování ze strany policie je běžnou realitou. Překážky bránící začlenění jsou mnohostranné, zejména pokud jde o hledání pracovních míst a bydlení. + + + + O presente relatório apresenta resultados selecionados do segundo inquérito da FRA realizado em grande escala na UE sobre migrantes e minorias (EU-MIDIS II). Analisa as experiências de quase 6 000 afrodescendentes em 12 Estados-Membros da UE. Os resultados demonstram que, quase 20 anos após a adoção de leis da UE que proíbem a discriminação, os afrodescendentes na UE ainda são alvo de preconceito e exclusão generalizados e enraizados. + + Zwart zijn in de EU — Samenvatting + Būt melnam ES — kopsavilkums + Az afrikai származású emberek Unió-szerte széles körű és állandó előítéletekkel és kirekesztéssel szembesülnek. A faji megkülönböztetés és a zaklatás gyakori. A rasszista erőszakkal kapcsolatos tapasztalatok eltérőek, de elérik az 14%-ot. A rendőrség diszkriminatív profilalkotása közös valóság. A befogadás előtt álló akadályok sokrétűek, különösen a munkahelyteremtés és a lakhatás terén. + Ľudia afrického pôvodu v celej EÚ čelia rozsiahlym a zakoreneným predsudkom a vylúčeniu. Rasová diskriminácia a obťažovanie sú bežné. Skúsenosti s rasistickým násilím sa líšia, ale dosahujú až 14 %. Diskriminačné profilovanie zo strany polície je bežnou realitou. + Prekážky začlenenia sú mnohostranné, najmä pokiaľ ide o hľadanie pracovných miest a bývania. + Diljem EU-a osobe afričkog podrijetla suočavaju se s raširenim i ukorijenjenim predrasudama i isključenošću. Rasna diskriminacija i uznemiravanje uobičajeni su. Iskustva s rasističkim nasiljem razlikuju se, ali dosežu čak 14 %. Diskriminatorno profiliranje od strane policije uobičajena je stvarnost. Prepreke uključivanju višedimenzionalne su, osobito kada je riječ o traženju radnih mjesta i stanovanju. + Prebivalci afriškega porekla se po vsej EU soočajo z razširjenimi in zakoreninjenimi predsodki in izključenostjo. Rasna diskriminacija in nadlegovanje sta običajna. Izkušnje z rasističnim nasiljem se razlikujejo, vendar dosegajo celo 14 %. Diskriminatorno oblikovanje profilov s strani policije je skupna realnost. Ovire za vključevanje so večplastne, zlasti pri iskanju delovnih mest in stanovanj. + + + Druga raziskava Evropske unije o manjšinah in diskriminaciji je zbrala informacije več kot 25 500 anketirancev iz različnih etničnih manjšin in priseljencev iz vseh 28 držav članic EU. + Antrasis Europos Sąjungos mažumų ir diskriminacijos tyrimas + A very interesting dataset (un jeu de données très intéressant) + La segunda encuesta de la Unión Europea sobre las minorías y la discriminación recogió información de más de 25 500 encuestados con diferentes orígenes étnicos e inmigrantes en los 28 Estados miembros de la UE. + V rámci druhého prieskumu Európskej únie týkajúceho sa menšín a diskriminácie sa zozbierali informácie od viac ako 25 500 respondentov z rôznych etnických menšín a prisťahovalcov vo všetkých 28 členských štátoch EÚ. + Второ проучване на малцинствата и дискриминацията в Европейския съюз + + + Antrasis Europos Sąjungos mažumų ir diskriminacijos tyrimas. Pagrindiniai rezultatai + Segunda encuesta de la Unión Europea sobre las minorías y la discriminación — Principales resultados + Segundo Inquérito sobre Minorias e Discriminação na União Europeia — Principais resultados + The report follows up and expands on FRA’s first major EU-wide survey on minorities’ and migrants’ experiences, conducted in 2008. The survey focuses on discrimination in different settings, police stops, criminal victimisation, rights awareness and societal participation. + Sprawozdanie stanowi kontynuację pierwszego ważnego ogólnounijnego badania FRA dotyczącego doświadczeń mniejszości i migrantów, przeprowadzonego w 2008 r., i jego rozszerzenie. Badanie koncentruje się na dyskryminacji w różnych środowiskach, postojach policji, wiktymizacji przestępczej, świadomości praw i udziale społeczeństwa. + Déanann an tuarascáil obair leantach agus fairsingiú ar an gcéad mhórshuirbhé a rinne FRA ar fud an AE maidir le heispéiris mhionlach agus imirceach, a rinneadh in 2008. Díríonn an suirbhé ar idirdhealú i suíomhanna éagsúla, stadanna na bpóilíní, íospairt choiriúil, feasacht ar chearta agus rannpháirtíocht na sochaí. + http://data.europa.eu/88u/distribution/f2ec416d-ad8a-4389-af26-69cc857218df + Al doilea sondaj al Uniunii Europene privind minoritățile și discriminarea – principalele rezultate + Den Europæiske Unions anden undersøgelse af mindretal og forskelsbehandling — vigtigste resultater + Druhý prieskum Európskej únie týkajúci sa menšín a diskriminácie – hlavné výsledky + Aruandes käsitletakse ja täiendatakse FRA 2008. aastal läbi viidud esimest olulist kogu ELi hõlmavat uuringut vähemuste ja rändajate kogemuste kohta. Uuringus keskendutakse diskrimineerimisele erinevates keskkondades, politsei peatustele, kuriteo ohvriks langemisele, teadlikkusele õigustest ja ühiskondlikule osalusele. + Η έκθεση παρακολουθεί και διευρύνει την πρώτη σημαντική πανευρωπαϊκή έρευνα του FRA σχετικά με τις εμπειρίες των μειονοτήτων και των μεταναστών, η οποία πραγματοποιήθηκε το 2008. Η έρευνα επικεντρώνεται στις διακρίσεις σε διαφορετικά περιβάλλοντα, στις αστυνομικές στάσεις, στην ποινική θυματοποίηση, στην ευαισθητοποίηση σχετικά με τα δικαιώματα και στη συμμετοχή της κοινωνίας. + 2017-12-19T11:36:46.159274+02:00 + Teine Euroopa Liidu vähemuste ja diskrimineerimise uuring – peamised tulemused + El informe da seguimiento y amplía la primera gran encuesta de la FRA sobre las experiencias de las minorías y los migrantes, realizada en 2008. La encuesta se centra en la discriminación en diferentes entornos, las detenciones policiales, la victimización de delitos, la sensibilización sobre los derechos y la participación social. + Toinen Euroopan unionin vähemmistöjä ja syrjintää koskeva tutkimus – Tärkeimmät tulokset + It-Tieni Stħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni — Riżultati ewlenin + Europeiska unionens andra undersökning av minoriteter och diskriminering – de viktigaste resultaten + Druga raziskava Evropske unije o manjšinah in diskriminaciji – glavni rezultati + Dara Suirbhé an Aontais Eorpaigh ar Mhionlaigh agus ar Idirdhealú — Na príomhthorthaí + Rapporten følger op på og uddyber FRA's første store EU-dækkende undersøgelse af mindretals og migranters erfaringer, der blev gennemført i 2008. Undersøgelsen fokuserer på forskelsbehandling i forskellige sammenhænge, politistop, kriminel viktimisering, bevidsthed om rettigheder og samfundsdeltagelse. + Ziņojums papildina un paplašina FRA 2008. gadā veikto pirmo ES mēroga apsekojumu par minoritāšu un migrantu pieredzi. Apsekojumā galvenā uzmanība pievērsta diskriminācijai dažādās vidēs, policijas apstājas, noziedzīgai viktimizācijai, informētībai par tiesībām un sabiedrības līdzdalībai. + La relazione fa seguito e amplia la prima importante indagine a livello dell'UE condotta dalla FRA sulle esperienze delle minoranze e dei migranti, condotta nel 2008. L'indagine si concentra sulla discriminazione in diversi contesti, le fermate della polizia, la vittimizzazione criminale, la consapevolezza dei diritti e la partecipazione della società. + + + + The report follows up and expands on FRA’s first major EU-wide survey on minorities’ and migrants’ experiences, conducted in 2008. The survey focuses on discrimination in different settings, police stops, criminal victimisation, rights awareness and societal participation. + 2017-12-19T11:36:46.159274+02:00 + + + Second European Union Minorities and Discrimination Survey - Main results + + + Izvješće se nastavlja i proširuje na prvo veliko istraživanje Agencije Europske unije za temeljna prava o iskustvima manjina i migranata provedeno 2008. na razini EU-a. Istraživanje je usmjereno na diskriminaciju u različitim okruženjima, zaustavljanje policije, kriminalnu viktimizaciju, osviještenost o pravima i sudjelovanje u društvu. + Ir-rapport isegwi u jespandi fuq l-ewwel stħarriġ ewlieni tal-FRA madwar l-UE kollha dwar l-esperjenzi tal-minoranzi u l-migranti, li sar fl-2008. L-istħarriġ jiffoka fuq id-diskriminazzjoni f’ambjenti differenti, il-waqfiet tal-pulizija, il-vittimizzazzjoni kriminali, is-sensibilizzazzjoni dwar id-drittijiet u l-parteċipazzjoni fis-soċjetà. + Второ проучване на малцинствата и дискриминацията в Европейския съюз — основни резултати + A jelentés az Európai Unió Alapjogi Ügynökségének a kisebbségek és migránsok tapasztalatairól 2008-ban végzett első nagyszabású felmérését követi nyomon és terjeszti ki. A felmérés a különböző helyszíneken történő megkülönböztetésre, a rendőrség leállítására, a bűncselekmények viktimizációjára, a jogokkal kapcsolatos tudatosságra és a társadalmi részvételre összpontosít. + O relatório dá seguimento e alarga o primeiro grande inquérito da FRA à escala da UE sobre as experiências das minorias e dos migrantes, realizado em 2008. O inquérito centra-se na discriminação em diferentes contextos, nas paragens policiais, na vitimização criminal, na sensibilização para os direitos e na participação social. + Seconda indagine sulle minoranze e le discriminazioni nell'Unione europea — Principali risultati + + Le rapport fait suite à la première grande enquête européenne sur l’expérience des minorités et des migrants menée par la FRA en 2008. L’enquête porte sur la discrimination dans différents contextes, les arrêts de la police, la victimisation criminelle, la sensibilisation aux droits et la participation de la société. + Drugie badanie Unii Europejskiej na temat mniejszości i dyskryminacji – główne wyniki + In dem Bericht wird die erste umfassende EU-weite Umfrage der FRA über die Erfahrungen von Minderheiten und Migranten im Jahr 2008 weiterverfolgt und erweitert. Die Umfrage konzentriert sich auf Diskriminierung in verschiedenen Bereichen, polizeiliche Beendigungen, kriminelle Viktimisierung, Rechtebewusstsein und gesellschaftliche Teilhabe. + Ataskaitoje apžvelgiama ir išplečiama FRA 2008 m. atlikta pirmoji pagrindinė ES masto apklausa dėl mažumų ir migrantų patirties. Apklausoje daugiausia dėmesio skiriama diskriminacijai įvairiose aplinkose, policijos sustojimui, nusikalstamai viktimizacijai, informuotumui apie teises ir visuomenės dalyvavimui. + Zweite Erhebung über Minderheiten und Diskriminierung in der Europäischen Union – Hauptergebnisse + Otrais Eiropas Savienības minoritāšu un diskriminācijas apsekojums — galvenie rezultāti + + Zpráva navazuje na první významný celounijní průzkum o zkušenostech menšin a migrantů, který byl proveden v roce 2008, a rozšiřuje jej. Průzkum se zaměřuje na diskriminaci v různých prostředích, policejní zastávky, viktimizaci trestné činnosti, povědomí o právech a zapojení společnosti. + Rapporten följer upp och vidareutvecklar FRA:s första stora EU-omfattande undersökning om minoriteters och migranters erfarenheter, som genomfördes 2008. Undersökningen fokuserar på diskriminering i olika miljöer, polisstopp, kriminell viktimisering, medvetenhet om rättigheter och samhällsdeltagande. + Second European Union Minorities and Discrimination Survey - Main results + Raportul urmărește și extinde primul sondaj major realizat de FRA la nivelul UE privind experiențele minorităților și ale migranților, realizat în 2008. Sondajul se concentrează asupra discriminării în diferite contexte, a opririlor poliției, a victimizării infracționale, a sensibilizării cu privire la drepturi și a participării societății. + Druhý průzkum Evropské unie týkající se menšin a diskriminace – hlavní výsledky + Poročilo spremlja in razširja prvo pomembno vseevropsko raziskavo agencije FRA o izkušnjah manjšin in migrantov, ki je bila izvedena leta 2008. Raziskava se osredotoča na diskriminacijo v različnih okoljih, policijske postaje, kriminalno viktimizacijo, ozaveščanje o pravicah in družbeno udeležbo. + Drugo istraživanje Europske unije o manjinama i diskriminaciji – glavni rezultati + Raportissa seurataan ja laajennetaan perusoikeusviraston vuonna 2008 tekemää ensimmäistä merkittävää EU:n laajuista kyselytutkimusta vähemmistöjen ja maahanmuuttajien kokemuksista. Tutkimuksessa keskitytään syrjintään eri ympäristöissä, poliisin pysähdyksiin, rikoksen uhriksi joutumiseen, tietoisuutta oikeuksista ja yhteiskunnallista osallistumista. + In het verslag wordt een vervolg gegeven aan en wordt ingegaan op de eerste grote EU-brede enquête van het FRA over de ervaringen van minderheden en migranten, die in 2008 is uitgevoerd. De enquête richt zich op discriminatie in verschillende omgevingen, politiestops, criminele victimisatie, rechtenbewustzijn en maatschappelijke participatie. + Tweede enquête over minderheden en discriminatie van de Europese Unie — Belangrijkste resultaten + + Deuxième enquête de l’Union européenne sur les minorités et la discrimination — Principaux résultats + Správa nadväzuje na prvý významný celoeurópsky prieskum o skúsenostiach menšín a migrantov, ktorý sa uskutočnil v roku 2008, a rozširuje ho. Prieskum sa zameriava na diskrimináciu v rôznych prostrediach, policajné zastávky, trestnú viktimizáciu, informovanosť o právach a spoločenskú účasť. + Második európai uniós felmérés a kisebbségekről és a hátrányos megkülönböztetésről – főbb eredmények + Δεύτερη έρευνα της Ευρωπαϊκής Ένωσης για τις μειονότητες και τις διακρίσεις — Βασικά αποτελέσματα + В доклада се проследява и доразвива първото мащабно проучване на Агенцията на Европейския съюз за основните права (FRA) относно опита на малцинствата и мигрантите, проведено през 2008 г. Проучването е съсредоточено върху дискриминацията в различни среди, полицейските спирания, криминалната виктимизация, осведомеността за правата и участието на обществото. + + + A kisebbségekről és a hátrányos megkülönböztetésről szóló második európai uniós felmérés több mint 25 500 különböző etnikai kisebbséggel és bevándorló háttérrel rendelkező válaszadótól gyűjtött információkat mind a 28 uniós tagállamban. + + + + + + + + Második európai uniós felmérés a kisebbségekről és a hátrányos megkülönböztetésről + Drugie badanie Unii Europejskiej na temat mniejszości i dyskryminacji + + + + La deuxième enquête de l’Union européenne sur les minorités et la discrimination a recueilli des informations auprès de plus de 25 500 personnes ayant des origines ethniques et immigrées différentes dans l’ensemble des 28 États membres de l’UE. + Druhý prieskum Európskej únie týkajúci sa menšín a diskriminácie + + + + + Deuxième enquête de l’Union européenne sur les minorités et la discrimination + + + Essere neri nell'UE + + Li tkun Iswed fl-UE + Être Noir dans l’UE + Být černoch v EU + La aproape douăzeci de ani de la adoptarea legislației UE care interzice discriminarea, persoanele de origine africană din UE se confruntă cu prejudecăți și excluziune larg răspândite și înrădăcinate. Prezentul raport prezintă rezultatele alese din cel de-al doilea sondaj la scară largă la nivelul UE privind migranții și minoritățile (EU-MIDIS II). + Acesta examinează experiențele a aproape 6 000 de persoane de origine africană în 12 state membre ale UE. + Casi veinte años después de la adopción de la legislación de la UE que prohíbe la discriminación, los afrodescendientes en la UE se enfrentan a prejuicios y exclusiones generalizados y arraigados. El presente informe resume los resultados seleccionados de la segunda encuesta a gran escala de la FRA sobre los migrantes y las minorías (EU-MIDIS II). + Examina las experiencias de casi 6.000 afrodescendientes en doce Estados miembros de la UE. + Почти двадесет години след приемането на законите на ЕС, забраняващи дискриминацията, хората от африкански произход в ЕС са изправени пред широко разпространени и утвърдени предразсъдъци и изключване. В настоящия доклад са представени избрани резултати от второто широкомащабно проучване на FRA относно мигрантите и малцинствата (EU-MIDIS II), проведено в целия ЕС. В него се разглежда опитът на почти 6000 души от африкански произход в 12 държави — членки на ЕС. + Téměř dvacet let po přijetí právních předpisů EU zakazujících diskriminaci čelí lidé afrického původu v EU rozšířeným a zakořeněným předsudkům a vyloučením. Tato zpráva nastiňuje vybrané výsledky druhého rozsáhlého celoevropského průzkumu agentury FRA o migrantech a menšinách (EU-MIDIS II). Zkoumá zkušenosti téměř 6 000 obyvatel afrického původu ve 12 členských státech EU. + Næsten tyve år efter vedtagelsen af EU-love, der forbyder forskelsbehandling, står personer af afrikansk afstamning i EU over for udbredte og forankrede fordomme og udstødelse. Denne rapport skitserer udvalgte resultater fra FRA's anden omfattende EU-dækkende undersøgelse af migranter og mindretal (EU-MIDIS II). Den undersøger erfaringerne fra næsten 6 000 personer af afrikansk afstamning i 12 EU-medlemsstater. + Almost twenty years after adoption of EU laws forbidding discrimination, people of African descent in the EU face widespread and entrenched prejudice and exclusion. This report outlines selected results from FRA's second large-scale EU-wide survey on migrants and minorities (EU-MIDIS II). It examines the experiences of almost 6,000 people of African descent in 12 EU Member States. + Being Black in the EU + Bheith Dubh san Aontas Eorpach + Quasi vent'anni dopo l'adozione delle leggi dell'UE che vietano la discriminazione, le persone di origine africana nell'UE devono affrontare pregiudizi ed esclusioni diffusi e radicati. La presente relazione illustra i risultati selezionati della seconda indagine su vasta scala della FRA su migranti e minoranze (EU-MIDIS II). + Esamina le esperienze di quasi 6 000 persone di origine africana in 12 Stati membri dell'UE. + Lähes kaksikymmentä vuotta syrjinnän kieltävän EU:n lainsäädännön antamisen jälkeen afrikkalaistaustaiset ihmiset kohtaavat eu:ssa laajaa ja syvälle juurtunutta ennakkoluuloa ja syrjäytymistä. Tässä raportissa esitetään valitut tulokset perusoikeusviraston toisesta laajasta EU:n laajuisesta tutkimuksesta maahanmuuttajista ja vähemmistöistä (EU-MIDIS II). + Siinä tarkastellaan lähes 6 000 afrikkalaistaustaisen ihmisen kokemuksia 12:sta EU:n jäsenvaltiosta. + Musta eu:ssa + Όντας μαύρος στην ΕΕ + http://data.europa.eu/88u/distribution/5796a0a9-7b7c-437d-92a1-3a9df520a117 + Biti črna v EU + Peaaegu kakskümmend aastat pärast diskrimineerimist keelavate ELi õigusaktide vastuvõtmist seisavad Aafrika päritolu inimesed ELis silmitsi laialt levinud ja juurdunud eelarvamuste ja tõrjutusega. Käesolevas aruandes antakse ülevaade valitud tulemustest, mis saadi FRA teise ulatusliku ELi-ülese rändajate ja vähemuste uuringu (EU-MIDIS II) põhjal. + Selles uuritakse peaaegu 6000 Aafrika päritolu inimese kogemusi 12 ELi liikmesriigis. + Skoraj dvajset let po sprejetju zakonov EU, ki prepovedujejo diskriminacijo, se ljudje afriškega porekla v EU soočajo z razširjenimi in zakoreninjenimi predsodki in izključenostjo. V tem poročilu so predstavljeni izbrani rezultati druge obsežne vseevropske raziskave agencije FRA o migrantih in manjšinah (EU-MIDIS II). + V njem so preučene izkušnje skoraj 6 000 ljudi afriškega porekla v 12 državah članicah EU. + + Σχεδόν είκοσι χρόνια μετά την έγκριση της νομοθεσίας της ΕΕ που απαγορεύει τις διακρίσεις, τα άτομα αφρικανικής καταγωγής στην ΕΕ αντιμετωπίζουν εκτεταμένες και παγιωμένες προκαταλήψεις και αποκλεισμό. Η παρούσα έκθεση περιγράφει τα επιλεγμένα αποτελέσματα της δεύτερης μεγάλης κλίμακας έρευνας του FRA για τους μετανάστες και τις μειονότητες (EU-MIDIS II). + Εξετάζει τις εμπειρίες σχεδόν 6.000 ατόμων αφρικανικής καταγωγής σε 12 κράτη μέλη της ΕΕ. + At være sort i EU + + Bijna twintig jaar na de aanneming van EU-wetgeving die discriminatie verbiedt, worden mensen van Afrikaanse afkomst in de EU geconfronteerd met wijdverbreide en hardnekkige vooroordelen en uitsluiting. Dit verslag schetst de geselecteerde resultaten van de tweede grootschalige EU-brede enquête over migranten en minderheden (EU-MIDIS II) van het FRA. Zij onderzoekt de ervaringen van bijna 6000 mensen van Afrikaanse afkomst in 12 EU-lidstaten. + Bycie czarnym w UE + Beagnach fiche bliain tar éis dlíthe an Aontais a ghlacadh lena gcuirtear cosc ar an idirdhealú, tá réamhchlaonadh agus eisiamh forleathan agus seanbhunaithe roimh dhaoine de bhunadh Afracach san Aontas. Leagtar amach sa tuarascáil seo torthaí roghnaithe ón dara suirbhé ar mhórscála a rinne FRA ar fud an Aontais maidir le himircigh agus mionlaigh (EU-MIDIS II). Scrúdaítear ann an taithí a bhí ag beagnach 6,000 duine de bhunadh Afracach in 12 Bhallstát de chuid an Aontais. + + Csaknem húsz évvel a diszkriminációt tiltó uniós jogszabályok elfogadása után az afrikai származású emberek az EU-ban széles körű és állandó előítéletekkel és kirekesztéssel szembesülnek. Ez a jelentés felvázolja az FRA második nagyszabású, a migránsokról és a kisebbségekről szóló, EU-szerte végzett felmérésének (EU-MIDIS II) kiválasztott eredményeit. A jelentés 12 uniós tagállamban közel 6000 afrikai származású ember tapasztalatait vizsgálja. + Près de vingt ans après l’adoption de la législation de l’UE interdisant la discrimination, les personnes d’ascendance africaine dans l’UE sont confrontées à des préjugés et à l’exclusion généralisés et bien ancrés. Le présent rapport présente les résultats sélectionnés de la deuxième enquête à grande échelle menée par la FRA à l’échelle de l’UE sur les migrants et les minorités (EU-MIDIS II). Il examine l’expérience de près de 6 000 personnes d’ascendance africaine dans 12 États membres de l’UE. + + Ser negro na UE + Zwart zijn in de EU + Nästan tjugo år efter antagandet av EU-lagar som förbjuder diskriminering står människor av afrikansk härkomst i EU inför utbredda och förankrade fördomar och utestängning. I denna rapport beskrivs utvalda resultat från FRA:s andra storskaliga EU-omfattande undersökning om migranter och minoriteter (EU-Midis II). + I rapporten undersöks erfarenheterna från nästan 6 000 personer av afrikansk härkomst i tolv EU-medlemsstater. + Praėjus beveik dvidešimčiai metų nuo ES teisės aktų, kuriais draudžiama diskriminacija, priėmimo Afrikos kilmės asmenys ES susiduria su plačiai paplitusiu ir įtvirtintu išankstiniu nusistatymu ir atskirtimi. Šioje ataskaitoje apžvelgiami FRA antrojo didelio masto ES masto migrantų ir mažumų tyrimo (EU-MIDIS II) rezultatai. Joje nagrinėjama beveik 6 000 Afrikos kilmės asmenų 12 ES valstybių narių patirtis. + Schwarz in der EU + Да бъдеш чернокож в ЕС + Gotovo dvadeset godina nakon donošenja zakona EU-a kojima se zabranjuje diskriminacija osobe afričkog podrijetla u EU-u suočavaju se s raširenim i ukorijenjenim predrasudama i isključenošću. U ovom se izvješću navode odabrani rezultati drugog opsežnog istraživanja Agencije za temeljna prava o migrantima i manjinama na razini EU-a (EU-MIDIS II). + U njemu se razmatraju iskustva gotovo 6 000 osoba afričkog podrijetla u 12 država članica EU-a. + Takmer dvadsať rokov po prijatí právnych predpisov EÚ zakazujúcich diskrimináciu čelia ľudia afrického pôvodu v EÚ rozsiahlym a zakoreneným predsudkom a vylúčeniu. V tejto správe sú načrtnuté vybrané výsledky druhého rozsiahleho celoeurópskeho prieskumu agentúry FRA o migrantoch a menšinách (EU-MIDIS II). + Skúma skúsenosti takmer 6 000 ľudí afrického pôvodu v 12 členských štátoch EÚ. + Fast zwanzig Jahre nach der Verabschiedung von EU-Rechtsvorschriften, die Diskriminierung verbieten, sehen sich Menschen afrikanischer Abstammung in der EU mit weit verbreiteten und verwurzelten Vorurteilen und Ausgrenzung konfrontiert. In diesem Bericht werden ausgewählte Ergebnisse der zweiten groß angelegten EU-weiten Umfrage der FRA zu Migranten und Minderheiten (EU-MIDIS II) skizziert. Er untersucht die Erfahrungen von fast 6.000 Menschen afrikanischer Abstammung in 12 EU-Mitgliedstaaten. + Ser negro en la UE + Byť čiernym v EÚ + Feketeség az EU-ban + Mustanahaline olemine ELis + + Kważi għoxrin sena wara l-adozzjoni tal-liġijiet tal-UE li jipprojbixxu d-diskriminazzjoni, il-persuni ta’ nisel Afrikan fl-UE jiffaċċjaw preġudizzju u esklużjoni mifruxa u stabbiliti. Dan ir-rapport jiddeskrivi r-riżultati magħżula mit-tieni stħarriġ fuq skala kbira tal-FRA dwar il-migranti u l-minoranzi (EU-MIDIS II). Jeżamina l-esperjenzi ta’ kważi 6,000 persuna ta’ dixxendenza Afrikana fi 12-il Stat Membru tal-UE. + Gandrīz divdesmit gadus pēc tam, kad pieņemti ES tiesību akti, kas aizliedz diskrimināciju, afrikāņu izcelsmes cilvēki ES saskaras ar plaši izplatītiem un iesakņojušiem aizspriedumiem un atstumtību. Šajā ziņojumā ir izklāstīti atlasītie rezultāti, kas iegūti FRA otrajā plašajā ES mēroga apsekojumā par migrantiem un minoritātēm (EU-MIDIS II). + Tajā aplūkota gandrīz 6000 afrikāņu izcelsmes cilvēku pieredze 12 ES dalībvalstīs. + Būti juodaodžiu ES + Biti crnac u EU-u + Būt melnam ES + Fiind negru în UE + Prawie dwadzieścia lat po przyjęciu unijnych przepisów zakazujących dyskryminacji osoby pochodzenia afrykańskiego w UE napotykają powszechne i utrwalone uprzedzenia i wykluczenie. W niniejszym sprawozdaniu przedstawiono wybrane wyniki drugiego szeroko zakrojonego ogólnounijnego badania dotyczącego migrantów i mniejszości (EU-MIDIS II) przeprowadzonego przez FRA. + Przeanalizowano w nim doświadczenia prawie 6000 osób pochodzenia afrykańskiego w 12 państwach członkowskich UE. + + + Att vara svart i EU + Quase vinte anos após a adoção da legislação da UE que proíbe a discriminação, as pessoas de ascendência africana na UE enfrentam preconceitos e exclusões generalizados e enraizados. O presente relatório descreve os resultados selecionados do segundo inquérito em grande escala da FRA a nível da UE sobre migrantes e minorias (EU-MIDIS II). Examina as experiências de quase 6 000 pessoas de ascendência africana em 12 Estados-Membros da UE. + 2018-11-28 + + + Al doilea sondaj al Uniunii Europene privind minoritățile și discriminarea + + + + Second European Union Minorities and Discrimination Survey - Questionnaire 2016 + The survey questionnaire was implemented + by means of face-to-face interviewing + using computer-assisted in-person interviewing + (CAPI) in all the survey countries. + + + + http://fra.europa.eu/sites/default/files/fra_uploads/fra-2017-eu-midis-ii-questionnaire_en.pdf + + + + + Ovaj interaktivni alat nudi različite načine za istraživanje podataka na kojima se temelje rezultati ankete. + Drugo istraživanje Europske unije o manjinama i diskriminaciji + Deze interactieve tool biedt verschillende manieren om de gegevens achter de enquêteresultaten te verkennen. + Esta herramienta interactiva ofrece diferentes maneras de explorar los datos detrás de los resultados de la encuesta. + See interaktiivne vahend pakub erinevaid võimalusi uuringutulemuste aluseks olevate andmete uurimiseks. + http://data.europa.eu/88u/distribution/2722649f-78e9-4354-87a1-8e0e7ea28bd8 + To interaktivno orodje ponuja različne načine za raziskovanje podatkov, na katerih temeljijo rezultati raziskave. + Tento interaktivní nástroj nabízí různé způsoby, jak prozkoumat údaje, které jsou základem výsledků průzkumu. + To interaktywne narzędzie oferuje różne sposoby odkrywania danych leżących u podstaw wyników ankiety. + Segundo Inquérito sobre Minorias e Discriminação na União Europeia + + Toinen Euroopan unionin vähemmistöjä ja syrjintää koskeva tutkimus + Drugie badanie Unii Europejskiej na temat mniejszości i dyskryminacji + Druhý prieskum Európskej únie týkajúci sa menšín a diskriminácie + Δεύτερη έρευνα της Ευρωπαϊκής Ένωσης για τις μειονότητες και τις διακρίσεις + 2017-12-19T11:46:40.796965+02:00 + Esta ferramenta interativa oferece diferentes formas de explorar os dados por trás dos resultados do inquérito. + + + + Den Europæiske Unions anden undersøgelse af mindretal og forskelsbehandling + Deuxième enquête de l’Union européenne sur les minorités et la discrimination + Segunda encuesta de la Unión Europea sobre las minorías y la discriminación + + Otrais Eiropas Savienības minoritāšu un diskriminācijas apsekojums + This interactive tool offers different ways to explore the data behind the survey results. + Ši interaktyvi priemonė siūlo įvairius būdus, kaip ištirti apklausos rezultatus pagrindžiančius duomenis. + Seconda indagine sulle minoranze e le discriminazioni nell'Unione europea + Dette interaktive værktøj tilbyder forskellige måder at udforske dataene bag undersøgelsens resultater på. + Tento interaktívny nástroj ponúka rôzne spôsoby, ako preskúmať údaje, ktoré sú základom výsledkov prieskumu. + Detta interaktiva verktyg erbjuder olika sätt att utforska de data som ligger bakom enkätresultaten. + Този интерактивен инструмент предлага различни начини за проучване на данните в основата на резултатите от проучването. + Cuireann an uirlis idirghníomhach seo bealaí éagsúla ar fáil chun na sonraí atá taobh thiar de thorthaí an tsuirbhé a fhiosrú. + + Dara Suirbhé an Aontais Eorpaigh ar Mhionlaigh agus ar Idirdhealú + Teine Euroopa Liidu vähemuste ja diskrimineerimise uuring + + Ez az interaktív eszköz különböző lehetőségeket kínál a felmérési eredmények mögötti adatok feltárására. + Druga raziskava Evropske unije o manjšinah in diskriminaciji + Din l-għodda interattiva toffri modi differenti biex tiġi esplorata d-data wara r-riżultati tal-istħarriġ. + Šis interaktīvais rīks piedāvā dažādus veidus, kā izpētīt aptaujas rezultātus. + Al doilea sondaj al Uniunii Europene privind minoritățile și discriminarea + Tämä interaktiivinen työkalu tarjoaa erilaisia tapoja tutkia kyselyn tulosten taustalla olevia tietoja. + 2017-12-06T00:00:00+02:00 + Druhý průzkum o menšinách a diskriminaci v Evropské unii + + Europeiska unionens andra undersökning av minoriteter och diskriminering + Második európai uniós felmérés a kisebbségekről és a hátrányos megkülönböztetésről + Acest instrument interactiv oferă diferite modalități de a explora datele din spatele rezultatelor sondajului. + It-Tieni Stħarriġ tal-Unjoni Ewropea dwar il-Minoranzi u d-Diskriminazzjoni + Zweite Erhebung über Minderheiten und Diskriminierung in der Europäischen Union + + Questo strumento interattivo offre diversi modi per esplorare i dati alla base dei risultati dell'indagine. + Second European Union Minorities and Discrimination Survey + Tweede enquête van de Europese Unie naar minderheden en discriminatie + Antrasis Europos Sąjungos mažumų ir diskriminacijos tyrimas + Dieses interaktive Tool bietet verschiedene Möglichkeiten, die Daten hinter den Umfrageergebnissen zu erforschen. + Второ проучване на малцинствата и дискриминацията в Европейския съюз + Cet outil interactif offre différentes façons d’explorer les données qui sous-tendent les résultats de l’enquête. + Αυτό το διαδραστικό εργαλείο προσφέρει διάφορους τρόπους διερεύνησης των δεδομένων πίσω από τα αποτελέσματα της έρευνας. + + + + + + + 9d839391284f5b263074b7dfa3c7e5d0 + + + + en + second-european-union-minorities-and-discrimination-survey + 2022-04-22T12:08:18Z + 2022-02-01T10:08:11Z + + + 2022-02-04T01:19:57Z + 2021-12-03T11:44:46Z + + + diff --git a/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts b/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts new file mode 100644 index 0000000000..359e95667d --- /dev/null +++ b/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts @@ -0,0 +1,110 @@ +import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' + +export const OPENDATASWISS_DATASET_RECORD: DatasetRecord = { + uniqueIdentifier: + '393940cd-6a67-4190-8b91-378669cdea1d@bundesamt-fur-energie-bfe', + title: 'Energy cities', + contacts: [], + contactsForResource: [ + { + email: 'geoinformation@bfe.admin.ch', + role: 'pointOfContact', + firstName: 'geoinformation@bfe.admin.ch', + }, + ], + landingPage: new URL('https://www.bfe.admin.ch/energiestaedte'), + onlineResources: [ + { + description: 'GeoPackage', + name: 'GeoPackage', + type: 'download', + url: new URL( + 'https://data.geo.admin.ch/ch.bfe.energiestaedte/gpkg/2056/ch.bfe.energiestaedte.zip' + ), + }, + { + description: 'swisstopo REST API', + name: 'swisstopo REST API', + type: 'link', + url: new URL( + 'https://api3.geo.admin.ch/rest/services/api/MapServer/ch.bfe.energiestaedte' + ), + }, + { + description: 'Thematic geoportal - map.geo.admin.ch', + name: 'Map (Preview) Thematic geoportal - map.geo.admin.ch', + type: 'link', + url: new URL( + 'https://map.geo.admin.ch/?topic=energie&lang=de&layers=ch.bfe.energiestaedte' + ), + }, + { + description: 'INTERLIS', + name: 'INTERLIS', + type: 'download', + url: new URL( + 'https://data.geo.admin.ch/ch.bfe.energiestaedte/xtf/2056/ch.bfe.energiestaedte.zip' + ), + }, + { + description: 'Web Map Services WMS', + name: 'ch.bfe.energiestaedte', + type: 'link', + url: new URL( + 'http://wms.geo.admin.ch/?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities&lang=de' + ), + }, + ], + keywords: [ + { + label: 'ifdg-linfrastructure-federale-de-donnees-geographiques', + type: 'theme', + }, + { + label: 'ifdg-infrastruttura-federale-dei-dati-geografici', + type: 'theme', + }, + { + label: 'bgdi-bundesgeodaten-infrastruktur', + type: 'theme', + }, + { + label: 'fsdi-federal-spatial-data-infrastructure', + type: 'theme', + }, + ], + kind: 'dataset', + languages: [], + legalConstraints: [], + licenses: [], + otherConstraints: [], + overviews: [], + securityConstraints: [], + spatialExtents: [ + { + description: 'Schweiz', + }, + ], + temporalExtents: [], + topics: [ + 'http://publications.europa.eu/resource/authority/data-theme/ENVI', + 'http://dcat-ap.ch/vocabulary/themes/energy', + 'http://publications.europa.eu/resource/authority/data-theme/ENER', + 'http://dcat-ap.ch/vocabulary/themes/territory', + 'http://publications.europa.eu/resource/authority/data-theme/REGI', + 'http://dcat-ap.ch/vocabulary/themes/culture', + 'http://publications.europa.eu/resource/authority/data-theme/EDUC', + 'http://dcat-ap.ch/vocabulary/themes/geography', + ], + abstract: `The Energy City label is used for certifying municipalities that develop and implement a sustainable energy policy. Municipalities that have been awarded this label promote renewable energy and ecological mobility, and focus on the efficient use of resources. In order to qualify for the label, a municipality must have realised or adopted at least 50 percent of its scope for action in the area of energy policy. Here the calculation is based on the Energy Cities catalogue. The European Energy Award®GOLD is the highest level of certification. This label is awarded to municipalities that have implemented at least 75 percent of the measures listed in the catalogue at the time of certification. Municipalities that qualify for this award demonstrate the highest level of commitment towards a sustainable energy future. The label is based on an assessment of municipal energy policy in the areas of development and spatial planning, municipal buildings and installations, supply and disposal, mobility, internal organisation, communication and cooperation.`, + lineage: undefined, + ownerOrganization: { + name: 'Bundesamt für Energie', + }, + recordCreated: new Date('2022-06-03T08:24:30.000Z'), + recordUpdated: new Date('2022-06-16T20:06:22.000Z'), + resourceCreated: new Date('2014-06-30T00:00:00.000'), + resourceUpdated: new Date('2022-06-15T00:00:00.000'), + status: undefined, + updateFrequency: 'unknown', +} diff --git a/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts b/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts new file mode 100644 index 0000000000..ef5ca0eeae --- /dev/null +++ b/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts @@ -0,0 +1,85 @@ +import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' + +export const SEXTANT_BATHYMETRY_DATASET_RECORD: DatasetRecord = { + uniqueIdentifier: '370ab490-e8b4-11df-b733-005056987263', + title: + 'MNT de Bathymétrie de la zone du Golo - Campagne Sigolo (réalisé en 2008, résolution 25m)', + contacts: [], + contactsForResource: [ + { + role: 'custodian', + email: 'gmcarto@ifremer.fr', + firstName: 'Ifremer', + lastName: '- Géosciences Marines', + }, + { + email: 'gmcarto@ifremer.fr', + firstName: 'Ifremer', + lastName: '- Géosciences Marines', + role: 'processor', + }, + { + email: 'gmcarto@ifremer.fr', + firstName: 'Ifremer', + lastName: '- Géosciences Marines', + role: 'processor', + }, + ], + landingPage: new URL( + 'https://sextant.ifremer.fr/geonetwork/api/collections/main/items/370ab490-e8b4-11df-b733-005056987263' + ), + onlineResources: [ + { + description: 'Cartographie - Géosciences Marines Ifremer', + name: '', + type: 'link', + url: new URL('http://wwz.ifremer.fr/drogm/Cartographie'), + }, + ], + keywords: [], + kind: 'dataset', + languages: [], + legalConstraints: [], + licenses: [], + otherConstraints: [], + overviews: [], + securityConstraints: [], + spatialExtents: [ + { + geometry: { + coordinates: [ + [ + [9.449999, 42.283333], + [10.033471, 42.283333], + [10.033471, 42.86684], + [9.449999, 42.86684], + [9.449999, 42.283333], + ], + ], + type: 'Polygon', + }, + }, + ], + temporalExtents: [], + topics: [ + '/Milieu physique/Bathymétrie/MNT', + 'Altitude', + 'fond marin', + 'morphologie sous-marine', + 'Bathymétrie', + 'Modèle Numérique de Terrain', + 'Corse', + 'Méditerranée', + 'France', + 'Géophysique', + ], + abstract: `Modèle bathymétrique (MNT) d'une partie de l'édifice sédimentaire sous-marin du Golo, à l'Est du Cap Corse. Les données ont été acquises lors de la campagne océanographique Sigolo en 2008, avec les sondeurs multifaisceaux EM300 et EM1000. + Le pas de la grille est de 25m. + Produit interne Ifremer.`, + lineage: undefined, + ownerOrganization: null, + recordUpdated: new Date('2020-06-03T22:34:05.000Z'), + resourceUpdated: new Date('2020-06-03T22:34:05.000Z'), + status: undefined, + updateFrequency: 'unknown', +} diff --git a/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.dataset.xml b/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.dataset.xml new file mode 100644 index 0000000000..34caaa59c0 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.dataset.xml @@ -0,0 +1,112 @@ + + + + INBO IPT + The INBO IPT is hosted at the Research Institute for Nature and Forest (INBO) in Brussels, Belgium. + + + Research Institute for Nature and Forest (INBO) + + + + + + + + + + + + + + 2012-09-17T17:01+02:00 + 2024-09-18T18:24+02:00 + + + { "type": "Point", "coordinates": [ 4.35018,50.86666 ] } + + + + + f3bd3e9b-cec1-38de-bb16-d063edace486 + + 2024-09-19T01:15:09.732Z + + + Dcat-ap-vl + Dit applicatieprofiel beschrijft Open Data Catalogi in Vlaanderen. DCAT-AP Vlaanderen (DCAT-AP VL) is een verdere specialisatie van DCAT-AP. De applicatie waarop dit profiel betrekking heeft is een Open Data Portaal in Vlaanderen. Open Data portalen zijn catalogussen van Open Data datasets. Ze hebben als belangrijkste doelstelling het vindbaar maken van data en hierdoor het hergebruik ervan te stimuleren. Open Data portalen vervullen een centrale rol in de overheidsopdracht om de toegankelijkheid tot overheidsinformatie te realiseren. Met dit applicatieprofiel bevorderen we de uniformiteit van de beschikbare informatie over datasets. Tevens vereenvoudigen we het aggregatie proces van meerdere Open Data Catalogi. Dit document bevat de verplichte elementen en bijkomende elementen waarover DCAT-AP Vlaanderen een uitspraak doet. Aanbevolen en optionele informatie waarvoor geen bijkomende afspraken in de context van DCAT-AP Vlaanderen zijn, zijn niet opgenomen in dit document. Hiervoor verwijzen we naar de DCAT-AP specificatie zelf. + 2.0 + + + + + https://ipt.inbo.be/resource?r=visschade-occurrences#Dataset-record + + + + + + + https://www.gbif.org/dataset/5d06d34c-f74d-461e-8d8b-c3351beb0db8 + Fish damage at pump stations + Fish damage at pump stations is a sampling event dataset published by the Research Institute for Nature and Forest (INBO). It contains 7319 occurrences, recorded during 120 events as well as lengths and weights of the fish that were recorded. Issues with the dataset can be reported at https://github.com/inbo/data-publication/issues + We have released this dataset to the public domain under a Creative Commons Zero waiver. We would appreciate it if you follow the INBO norms for data use (https://www.inbo.be/en/norms-data-use) when using the data. If you have any questions regarding this dataset, don't hesitate to contact us via the contact information provided in the metadata or via opendata@inbo.be. + + + David Buysse + + + + 2021-04-14T11:15+02:00 + + + Research Institute for Nature and Forest (INBO) + + + fish + pumping station + migration + Samplingevent + mortality + + + Vlaamse Open data + Vlaamse Open data + Vlaamse Open data + Vlaamse Open data + + + + + + biodiversity + + + + + + + + + + + + { "type": "Polygon", "coordinates": [ [ [3.768,51.072], [3.768,51.31], [4.202,51.31], [4.202,51.072], [3.768,51.072] ] ] } + + + + + Darwin Core Archive of Fish damage at pump stations + Darwin Core Archive + + + + + + + + + + + + diff --git a/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts b/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts new file mode 100644 index 0000000000..a2de7d2e38 --- /dev/null +++ b/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts @@ -0,0 +1,73 @@ +import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' + +export const VLAANDEREN_DATASET_RECORD: DatasetRecord = { + uniqueIdentifier: 'f3bd3e9b-cec1-38de-bb16-d063edace486', + title: 'Fish damage at pump stations', + abstract: `Fish damage at pump stations is a sampling event dataset published by the Research Institute for Nature and Forest (INBO). It contains 7319 occurrences, recorded during 120 events as well as lengths and weights of the fish that were recorded. Issues with the dataset can be reported at https://github.com/inbo/data-publication/issues + We have released this dataset to the public domain under a Creative Commons Zero waiver. We would appreciate it if you follow the INBO norms for data use (https://www.inbo.be/en/norms-data-use) when using the data. If you have any questions regarding this dataset, don't hesitate to contact us via the contact information provided in the metadata or via opendata@inbo.be.`, + contacts: [], + contactsForResource: [ + { + firstName: 'David', + lastName: 'Buysse', + email: 'david.buysse@inbo.be', + role: 'pointOfContact', + }, + ], + ownerOrganization: { + name: 'Research Institute for Nature and Forest (INBO)', + }, + landingPage: new URL('https://ipt.inbo.be/resource?r=visschade-occurrences'), + onlineResources: [ + { + description: 'Darwin Core Archive', + name: 'Darwin Core Archive of Fish damage at pump stations', + type: 'download', + url: new URL('https://ipt.inbo.be/archive.do?r=visschade-occurrences'), + }, + ], + keywords: [ + { label: 'fish', type: 'theme' }, + { label: 'pumping station', type: 'theme' }, + { label: 'migration', type: 'theme' }, + { label: 'Samplingevent', type: 'theme' }, + { label: 'mortality', type: 'theme' }, + ], + kind: 'dataset', + languages: [], + legalConstraints: [], + licenses: [ + { + text: 'Creative Commons CC-0', + url: new URL( + 'https://creativecommons.org/publicdomain/zero/1.0/legalcode' + ), + }, + ], + otherConstraints: [], + overviews: [], + securityConstraints: [], + spatialExtents: [ + { + geometry: { + type: 'Polygon', + coordinates: [ + [ + [3.768, 51.072], + [3.768, 51.31], + [4.202, 51.31], + [4.202, 51.072], + [3.768, 51.072], + ], + ], + }, + }, + ], + temporalExtents: [], + topics: ['biodiversity'], + lineage: undefined, + recordUpdated: new Date('2024-09-19T01:15:09.732Z'), + resourceUpdated: new Date('2021-04-14T11:15+02:00'), + status: undefined, + updateFrequency: 'unknown', +} diff --git a/libs/common/domain/src/lib/model/record/contact.model.ts b/libs/common/domain/src/lib/model/record/contact.model.ts index 0be5481e4d..2954e52019 100644 --- a/libs/common/domain/src/lib/model/record/contact.model.ts +++ b/libs/common/domain/src/lib/model/record/contact.model.ts @@ -60,7 +60,7 @@ export interface Individual { email: string role: Role position?: string - organization: Organization + organization?: Organization address?: string phone?: string } diff --git a/package.json b/package.json index 6af955bf37..65fa4c146f 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "papaparse": "^5.3.1", "pg": "^8.9.0", "proj4": "^2.9.2", + "rdflib": "^2.2.35", "reflect-metadata": "^0.1.13", "rxjs": "^7.0.0", "semver": "^7.5.4", From 9a83f8701f899a428e840b3a8679feaa74010340 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 4 Jul 2024 14:37:41 +0200 Subject: [PATCH 24/49] feat(md-converter): handle DCAT-AP, transform between formats --- .../src/app/app.component.html | 25 ++++- .../src/app/app.component.ts | 14 +++ .../record-form/record-form.component.html | 2 +- .../app/components/status/status.component.ts | 94 +++++++++++++------ apps/metadata-converter/src/app/md-formats.ts | 18 ++++ libs/api/metadata-converter/src/index.ts | 1 + 6 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 apps/metadata-converter/src/app/md-formats.ts diff --git a/apps/metadata-converter/src/app/app.component.html b/apps/metadata-converter/src/app/app.component.html index 0a13090598..94c186c01e 100644 --- a/apps/metadata-converter/src/app/app.component.html +++ b/apps/metadata-converter/src/app/app.component.html @@ -1,8 +1,8 @@
-
+

- Simple metadata editor
+
Simple metadata editor
>

-
+
/> +
+ +
this.statusComponent.errorLoadingFile(e.message)) } + onFormatChange(format: string) { + this.statusComponent.changeFormat(format) + } } diff --git a/apps/metadata-converter/src/app/components/record-form/record-form.component.html b/apps/metadata-converter/src/app/components/record-form/record-form.component.html index 8eb3cecfbd..2e757cd2db 100644 --- a/apps/metadata-converter/src/app/components/record-form/record-form.component.html +++ b/apps/metadata-converter/src/app/components/record-form/record-form.component.html @@ -119,7 +119,7 @@ (fieldValueChange)="set($event, 'position')" (confirm)="emitChangedRecord()" > - + { - this.newMetadata.emit(output) - const time = Math.round(performance.now() - start) - this.status = `Converting to ISO9139... Done (${time} ms).` - }) - .catch((e) => { - this.status = `Converting to ISO9139... Failed: ${ - e instanceof Error ? e.message : e - }` - console.error(e) - }) + this._currentRecord = value + this.convertRecordToXml(value) } @Input() set currentMetadata(value: string) { const start = performance.now() - this.status = 'Converting to native format...' + this.status = 'Converting to CatalogRecord...' this.xmlToRecord(value) .then((output) => { + this._currentRecord = output this.newRecordNative.emit(output) this.newMetadata.emit(value) const time = Math.round(performance.now() - start) - this.status = `Converting to native format... Done (${time} ms).` + this.status = `Converting to CatalogRecord... Done (${time} ms).` }) .catch((e) => { - this.status = `Converting to native format... Failed: ${ + this.status = `Converting to CatalogRecord... Failed: ${ e instanceof Error ? e.message : e }` console.error(e) @@ -48,6 +41,8 @@ export class StatusComponent { @Output() newRecordNative = new EventEmitter() @Output() newMetadata = new EventEmitter() + currentConverter: BaseConverter = null + status = 'Standing by.' startLoadingFile() { @@ -64,15 +59,60 @@ export class StatusComponent { this.status = `Reading file... Failed` } - private recordToXml(record: CatalogRecord) { - const converter = this.referenceMetadata - ? findConverterForDocument(this.referenceMetadata) - : new Iso191153Converter() - return converter.writeRecord(record, this.referenceMetadata) + public changeFormat(format: string) { + const converterClass = FORMATS[format] + if (!converterClass) { + throw new Error(`Metadata format ${format} not supported`) + } + this.referenceMetadata = '' + this.convertRecordToXml(this._currentRecord, new converterClass()) + } + + private convertRecordToXml( + record: CatalogRecord, + currentConverter?: BaseConverter + ) { + const start = performance.now() + const converter = currentConverter ?? this.currentConverter + const converterName = getFormatName(converter) + this.status = `Converting to ${converterName}...` + this.newMetadata.emit('') + this.recordToXml(record, currentConverter) + .then((output) => { + this.newMetadata.emit(output) + const time = Math.round(performance.now() - start) + this.status = `Converting to ${converterName}... Done (${time} ms).` + }) + .catch((e) => { + this.status = `Converting to ${converterName}... Failed: ${ + e instanceof Error ? e.message : e + }` + console.error(e) + }) + } + + private recordToXml( + record: CatalogRecord, + currentConverter?: BaseConverter + ) { + try { + this.currentConverter = currentConverter + ? currentConverter + : this.referenceMetadata + ? findConverterForDocument(this.referenceMetadata) + : new Iso191153Converter() + return this.currentConverter.writeRecord(record, this.referenceMetadata) + } catch (e) { + return Promise.reject(e) + } } private xmlToRecord(metadata: string) { - const converter = findConverterForDocument(metadata) - return converter.readRecord(metadata) + try { + this.currentConverter = findConverterForDocument(metadata) + return this.currentConverter.readRecord(metadata) + } catch (e) { + return Promise.reject(e) + } } } diff --git a/apps/metadata-converter/src/app/md-formats.ts b/apps/metadata-converter/src/app/md-formats.ts new file mode 100644 index 0000000000..39a6cb8052 --- /dev/null +++ b/apps/metadata-converter/src/app/md-formats.ts @@ -0,0 +1,18 @@ +import { + BaseConverter, + DcatApConverter, + Iso191153Converter, + Iso19139Converter, +} from '@geonetwork-ui/api/metadata-converter' + +export const FORMATS = { + 'ISO 19139': Iso19139Converter, + 'ISO 19115-3': Iso191153Converter, + 'DCAT-AP': DcatApConverter, +} + +export function getFormatName(converter: BaseConverter): string { + return Object.keys(FORMATS).reduce((prev, key) => + converter instanceof FORMATS[key] ? key : prev + ) +} diff --git a/libs/api/metadata-converter/src/index.ts b/libs/api/metadata-converter/src/index.ts index 3bcc08bef1..6b5e3035b2 100644 --- a/libs/api/metadata-converter/src/index.ts +++ b/libs/api/metadata-converter/src/index.ts @@ -4,3 +4,4 @@ export * from './lib/find-converter' export * from './lib/gn4' export * from './lib/dcat-ap' export * from './lib/xml-utils' +export * from './lib/base.converter' From 70565e0813bbe98a8ff1a650b79c9594941af6b6 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 2 Jul 2024 23:57:24 +0200 Subject: [PATCH 25/49] ci: deploy md-converter app on gh-pages --- .github/workflows/deploy.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7320553d00..cd999d6c68 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -105,3 +105,33 @@ jobs: comment_tag: github-links pr_number: ${{ github.event.issue.number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + apps: + needs: checks + if: github.event_name != 'issue_comment' + name: Deploy Apps to GitHub Pages + runs-on: ubuntu-latest + env: + BRANCH_NAME: main + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install + run: npm ci + + - name: Build metadata-converter + run: npx nx build metadata-converter --prod + + - name: Deploy to directory ${{ env.BRANCH_NAME }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + npx gh-pages --dist dist/apps/ --dest ${{env.BRANCH_NAME}} --remove "${{env.BRANCH_NAME}}/**" --no-history --repo "https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" From 66fb168b191ff5ccaaafa3889e0d1ab869d0a741 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 19 Sep 2024 11:20:12 +0200 Subject: [PATCH 26/49] chore: package-lock --- package-lock.json | 2255 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 2195 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3656a227f..73a679c4ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "papaparse": "^5.3.1", "pg": "^8.9.0", "proj4": "^2.9.2", + "rdflib": "^2.2.35", "reflect-metadata": "^0.1.13", "rxjs": "^7.0.0", "semver": "^7.5.4", @@ -3900,6 +3901,19 @@ "ms": "^2.1.1" } }, + "node_modules/@digitalbazaar/http-client": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", + "integrity": "sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==", + "dependencies": { + "ky": "^0.33.3", + "ky-universal": "^0.11.0", + "undici": "^5.21.2" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -4329,7 +4343,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -4412,12 +4425,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@fal-works/esbuild-plugin-global-externals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", "dev": true }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@floating-ui/core": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", @@ -4456,6 +4485,14 @@ "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==", "dev": true }, + "node_modules/@frogcat/ttl2jsonld": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@frogcat/ttl2jsonld/-/ttl2jsonld-0.0.9.tgz", + "integrity": "sha512-oT3Abc9sEnwcCx9cTgRCTbz+Y/9fvbqfW22A5V4ChoQ8/P++2eAvlWgUghFoNm2V9U3/CCDSP9HTGJ51D+n1Uw==", + "bin": { + "ttl2jsonld": "bin/cli.js" + } + }, "node_modules/@geospatial-sdk/core": { "version": "0.0.5-dev.21", "resolved": "https://registry.npmjs.org/@geospatial-sdk/core/-/core-0.0.5-dev.21.tgz", @@ -4497,6 +4534,18 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -9295,6 +9344,11 @@ "win32" ] }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" + }, "node_modules/@schematics/angular": { "version": "16.2.14", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.14.tgz", @@ -12368,6 +12422,11 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -12823,6 +12882,11 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", @@ -13640,6 +13704,14 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -13762,6 +13834,17 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -14265,11 +14348,45 @@ "dequal": "^2.0.3" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-timsort": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", @@ -14293,6 +14410,114 @@ "node": ">=0.10.0" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -14402,10 +14627,12 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -15229,7 +15456,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, "dependencies": { "semver": "^7.0.0" } @@ -15423,6 +15649,11 @@ } ] }, + "node_modules/canonicalize": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.8.tgz", + "integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==" + }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", @@ -17180,6 +17411,54 @@ "node": ">=14" } }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -17304,11 +17583,11 @@ } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -17883,6 +18162,65 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -17902,11 +18240,83 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==" }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es6-object-assign": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", @@ -18096,6 +18506,115 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-config-standard-jsx": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz", + "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peerDependencies": { + "eslint": "^8.8.0", + "eslint-plugin-react": "^7.28.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz", + "integrity": "sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/eslint-plugin-cypress": { "version": "2.13.3", "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.13.3.tgz", @@ -18108,6 +18627,180 @@ "eslint": ">= 3.2.1" } }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", + "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.9.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-no-only-tests": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz", @@ -18117,6 +18810,97 @@ "node": ">=5.0.0" } }, + "node_modules/eslint-plugin-promise": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", + "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.36.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz", + "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-storybook": { "version": "0.6.13", "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.6.13.tgz", @@ -18185,9 +18969,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", - "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -18401,6 +19185,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter-asyncresource": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", @@ -19010,7 +19802,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -19088,7 +19879,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -19405,11 +20195,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==" }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gauge": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", @@ -19552,6 +20367,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -19563,6 +20389,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/getos": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", @@ -19801,6 +20643,21 @@ "node": ">=4" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -19839,8 +20696,7 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/guess-parser": { "version": "0.4.22", @@ -19910,15 +20766,12 @@ "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { @@ -19950,9 +20803,9 @@ } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -19972,12 +20825,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -20784,6 +21636,19 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -20832,11 +21697,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -20848,6 +21753,21 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -20867,7 +21787,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -20888,11 +21807,42 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -20926,6 +21876,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -20946,7 +21907,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -21007,6 +21967,17 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -21029,6 +22000,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -21037,6 +22019,20 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -21050,7 +22046,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "devOptional": true, "engines": { "node": ">=8" } @@ -21082,6 +22077,46 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -21093,19 +22128,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-subset": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", "dev": true }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -21131,6 +22193,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-what": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", @@ -21147,6 +22246,11 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -21284,6 +22388,18 @@ "node": ">=6" } }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -23107,6 +24223,11 @@ "node": ">=4" } }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -23184,6 +24305,36 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonld": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-8.3.2.tgz", + "integrity": "sha512-MwBbq95szLwt8eVQ1Bcfwmgju/Y5P2GdtlHE2ncyfuYjIdEhluUVyj1eudacf1mOkWIoS9GpDBTECqhmq7EOaA==", + "dependencies": { + "@digitalbazaar/http-client": "^3.4.1", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jsonld/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsonld/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -23208,6 +24359,20 @@ "verror": "1.10.0" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -23241,6 +24406,58 @@ "node": ">= 8" } }, + "node_modules/ky": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", + "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/ky-universal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.11.0.tgz", + "integrity": "sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^3.2.10" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky-universal?sponsor=1" + }, + "peerDependencies": { + "ky": ">=0.31.4", + "web-streams-polyfill": ">=3.2.1" + }, + "peerDependenciesMeta": { + "web-streams-polyfill": { + "optional": true + } + } + }, + "node_modules/ky-universal/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/launch-editor": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", @@ -23505,6 +24722,57 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dependencies": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -23525,7 +24793,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -23735,7 +25002,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -24391,6 +25657,57 @@ "thenify-all": "^1.0.0" } }, + "node_modules/n3": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.22.0.tgz", + "integrity": "sha512-0SmSJCUviTbQtbHGFuTYVumu06juM1iDnXBAD0TNdwlGt2w+w85wZeCanA0/tc2M3ucc0mS2jiIrpw0KfkG/dw==", + "dependencies": { + "buffer": "^6.0.3", + "queue-microtask": "^1.1.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/n3/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/n3/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -25887,7 +27204,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -25900,6 +27216,82 @@ "node": ">= 10.12.0" } }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -26118,7 +27510,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -26633,6 +28024,74 @@ "nice-napi": "^1.0.2" } }, + "node_modules/pkg-conf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", + "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", + "dependencies": { + "find-up": "^3.0.0", + "load-json-file": "^5.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-conf/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/pkg-dir": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", @@ -26698,6 +28157,14 @@ "ms": "^2.1.1" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -27535,7 +29002,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -27545,8 +29011,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", @@ -27853,6 +29318,47 @@ "quickselect": "^2.0.0" } }, + "node_modules/rdf-canonize": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", + "integrity": "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rdflib": { + "version": "2.2.35", + "resolved": "https://registry.npmjs.org/rdflib/-/rdflib-2.2.35.tgz", + "integrity": "sha512-PudSzYz0cVy5iuKmxaNfl0WPuPq4h9LAWbyJSn36gwTMtWbBgcqUDobdcDK3P6mxeOui/cosDXu0A9oVicVfyA==", + "dependencies": { + "@babel/runtime": "^7.24.4", + "@frogcat/ttl2jsonld": "^0.0.9", + "@xmldom/xmldom": "^0.8.10", + "cross-fetch": "^3.1.8", + "jsonld": "^8.3.2", + "n3": "^1.17.3", + "solid-namespace": "^0.5.3" + } + }, + "node_modules/rdflib/node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/rdflib/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -28255,6 +29761,26 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -28289,6 +29815,23 @@ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -28725,6 +30268,23 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -28735,6 +30295,22 @@ "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==" }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -29090,6 +30666,25 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -29307,6 +30902,14 @@ "node": ">= 10" } }, + "node_modules/solid-namespace": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/solid-namespace/-/solid-namespace-0.5.3.tgz", + "integrity": "sha512-b2u2qkrRa0yrcc/jh6Nv0/mkwMyL4fMSNZtKG4dv3IxQtZOEUB8O6Xe7GrkoQaRoGrbUxRzbve9GHJD0w7p+KA==", + "dependencies": { + "standard": "^17.0.0" + } + }, "node_modules/sort-asc": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz", @@ -29570,6 +31173,292 @@ "node": ">=8" } }, + "node_modules/standard": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/standard/-/standard-17.1.2.tgz", + "integrity": "sha512-WLm12WoXveKkvnPnPnaFUUHuOB2cUdAsJ4AiGHL2G0UNMrcRAWY2WriQaV8IQ3oRmYr0AWUbLNr94ekYFAHOrA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "eslint": "^8.41.0", + "eslint-config-standard": "17.1.0", + "eslint-config-standard-jsx": "^11.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.36.1", + "standard-engine": "^15.1.0", + "version-guard": "^1.1.1" + }, + "bin": { + "standard": "bin/cmd.cjs" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/standard-engine": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.1.0.tgz", + "integrity": "sha512-VHysfoyxFu/ukT+9v49d4BRXIokFRZuH3z1VRxzFArZdjSCFpro6rEIU3ji7e4AoAtuSfKBkiOmsrDqKW5ZSRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "get-stdin": "^8.0.0", + "minimist": "^1.2.6", + "pkg-conf": "^3.1.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/standard/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/standard/node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/standard/node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead" + }, + "node_modules/standard/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/standard/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/standard/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/standard/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/standard/node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/standard/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/standard/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/standard/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/standard/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/standard/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/standard/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/standard/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -29699,6 +31588,86 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -31042,6 +33011,75 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-assert": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", @@ -31326,6 +33364,31 @@ "node": ">=8" } }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -31723,6 +33786,14 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "devOptional": true }, + "node_modules/version-guard": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/version-guard/-/version-guard-1.1.3.tgz", + "integrity": "sha512-JwPr6erhX53EWH/HCSzfy1tTFrtPXUe927wdM1jqBBeYp1OM+qPHjWbsvv6pIBduqdgxxS+ScfG7S28pzyr2DQ==", + "engines": { + "node": ">=0.10.48" + } + }, "node_modules/vite": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", @@ -33183,17 +35254,73 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dependencies": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", - "dev": true, + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -33419,6 +35546,14 @@ } } }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/xhr2": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", From dba2f7c57c19e231933f5b1fac5e185fc9f719bd Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 19 Sep 2024 13:24:09 +0200 Subject: [PATCH 27/49] fix(me): give original record source when saving it Otherwise the operation will be largely destructive & not keep the initial schema --- .../src/lib/+state/editor.effects.spec.ts | 5 +++- .../editor/src/lib/+state/editor.effects.ts | 15 +++++++--- .../form-field-update-frequency.component.ts | 2 +- .../src/lib/services/editor.service.spec.ts | 30 +++++++++++++++---- .../editor/src/lib/services/editor.service.ts | 10 +++++-- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/libs/feature/editor/src/lib/+state/editor.effects.spec.ts b/libs/feature/editor/src/lib/+state/editor.effects.spec.ts index 7e3515f409..c32c91af4a 100644 --- a/libs/feature/editor/src/lib/+state/editor.effects.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.effects.spec.ts @@ -78,6 +78,7 @@ describe('EditorEffects', () => { expect(effects.saveRecord$).toBeObservable(expected) expect(service.saveRecord).toHaveBeenCalledWith( datasetRecordsFixture()[0], + 'blabla', [], false ) @@ -94,6 +95,7 @@ describe('EditorEffects', () => { await firstValueFrom(effects.saveRecord$) expect(service.saveRecord).toHaveBeenCalledWith( datasetRecordsFixture()[0], + 'blabla', [], true ) @@ -158,7 +160,8 @@ describe('EditorEffects', () => { }) ) expect(service.saveRecordAsDraft).toHaveBeenCalledWith( - datasetRecordsFixture()[0] + datasetRecordsFixture()[0], + 'blabla' ) }) }) diff --git a/libs/feature/editor/src/lib/+state/editor.effects.ts b/libs/feature/editor/src/lib/+state/editor.effects.ts index 7d14e6dc06..bde47e4b4f 100644 --- a/libs/feature/editor/src/lib/+state/editor.effects.ts +++ b/libs/feature/editor/src/lib/+state/editor.effects.ts @@ -9,6 +9,7 @@ import { selectEditorConfig, selectRecord, selectRecordAlreadySavedOnce, + selectRecordSource, } from './editor.selectors' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @@ -24,12 +25,13 @@ export class EditorEffects { ofType(EditorActions.saveRecord), withLatestFrom( this.store.select(selectRecord), + this.store.select(selectRecordSource), this.store.select(selectEditorConfig), this.store.select(selectRecordAlreadySavedOnce) ), - switchMap(([, record, fieldsConfig, alreadySavedOnce]) => + switchMap(([, record, recordSource, fieldsConfig, alreadySavedOnce]) => this.editorService - .saveRecord(record, fieldsConfig, !alreadySavedOnce) + .saveRecord(record, recordSource, fieldsConfig, !alreadySavedOnce) .pipe( switchMap(([record, recordSource]) => of( @@ -64,8 +66,13 @@ export class EditorEffects { this.actions$.pipe( ofType(EditorActions.updateRecordField), debounceTime(1000), - withLatestFrom(this.store.select(selectRecord)), - switchMap(([, record]) => this.editorService.saveRecordAsDraft(record)), + withLatestFrom( + this.store.select(selectRecord), + this.store.select(selectRecordSource) + ), + switchMap(([, record, recordSource]) => + this.editorService.saveRecordAsDraft(record, recordSource) + ), map(() => EditorActions.draftSaveSuccess()) ) ) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts index 526303134e..100e056ea8 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts @@ -70,7 +70,7 @@ export class FormFieldUpdateFrequencyComponent implements OnInit { } get selectedFrequency(): string { - if (typeof this.value === 'string') return null + if (!this.value || typeof this.value === 'string') return null const { updatedTimes, per } = this.value return `${per}.${updatedTimes}` } diff --git a/libs/feature/editor/src/lib/services/editor.service.spec.ts b/libs/feature/editor/src/lib/services/editor.service.spec.ts index 25dfc02981..f5cb0121bf 100644 --- a/libs/feature/editor/src/lib/services/editor.service.spec.ts +++ b/libs/feature/editor/src/lib/services/editor.service.spec.ts @@ -57,7 +57,11 @@ describe('EditorService', () => { let savedRecord: [CatalogRecord, string] beforeEach(async () => { savedRecord = await firstValueFrom( - service.saveRecord(SAMPLE_RECORD, DEFAULT_CONFIGURATION) + service.saveRecord( + SAMPLE_RECORD, + 'blabla', + DEFAULT_CONFIGURATION + ) ) }) it('calls repository.saveRecord and repository.clearRecordDraft', () => { @@ -65,7 +69,10 @@ describe('EditorService', () => { ...SAMPLE_RECORD, recordUpdated: expect.any(Date), } - expect(repository.saveRecord).toHaveBeenCalledWith(expected) + expect(repository.saveRecord).toHaveBeenCalledWith( + expected, + 'blabla' + ) expect(repository.clearRecordDraft).toHaveBeenCalledWith( SAMPLE_RECORD.uniqueIdentifier ) @@ -78,7 +85,12 @@ describe('EditorService', () => { describe('if a new one has to be generated', () => { beforeEach(() => { service - .saveRecord(SAMPLE_RECORD, DEFAULT_CONFIGURATION, true) + .saveRecord( + SAMPLE_RECORD, + 'blabla', + DEFAULT_CONFIGURATION, + true + ) .subscribe() }) it('clears the unique identifier of the record', () => { @@ -87,17 +99,23 @@ describe('EditorService', () => { recordUpdated: expect.any(Date), uniqueIdentifier: null, } - expect(repository.saveRecord).toHaveBeenCalledWith(expected) + expect(repository.saveRecord).toHaveBeenCalledWith( + expected, + 'blabla' + ) }) }) }) describe('saveRecordAsDraft', () => { beforeEach(() => { - service.saveRecordAsDraft(SAMPLE_RECORD).subscribe() + service.saveRecordAsDraft(SAMPLE_RECORD, 'blabla').subscribe() }) it('calls saveRecordAsDraft', () => { - expect(repository.saveRecordAsDraft).toHaveBeenCalledWith(SAMPLE_RECORD) + expect(repository.saveRecordAsDraft).toHaveBeenCalledWith( + SAMPLE_RECORD, + 'blabla' + ) }) }) }) diff --git a/libs/feature/editor/src/lib/services/editor.service.ts b/libs/feature/editor/src/lib/services/editor.service.ts index b76c47bcd1..64d58af9bf 100644 --- a/libs/feature/editor/src/lib/services/editor.service.ts +++ b/libs/feature/editor/src/lib/services/editor.service.ts @@ -15,6 +15,7 @@ export class EditorService { // returns the record as it was when saved, alongside its source saveRecord( record: CatalogRecord, + recordSource: string, fieldsConfig: EditorConfig, generateNewUniqueIdentifier = false ): Observable<[CatalogRecord, string]> { @@ -40,7 +41,7 @@ export class EditorService { savedRecord.uniqueIdentifier = null } - return this.recordsRepository.saveRecord(savedRecord).pipe( + return this.recordsRepository.saveRecord(savedRecord, recordSource).pipe( switchMap((uniqueIdentifier) => this.recordsRepository.openRecordForEdition(uniqueIdentifier) ), @@ -54,9 +55,12 @@ export class EditorService { // emits and completes once saving is done // note: onSave processes are not run for drafts - saveRecordAsDraft(record: CatalogRecord): Observable { + saveRecordAsDraft( + record: CatalogRecord, + recordSource: string + ): Observable { return this.recordsRepository - .saveRecordAsDraft(record) + .saveRecordAsDraft(record, recordSource) .pipe(map(() => undefined)) } From 17b35a1c248bc0a4b5aaf070c10277653ff3e253 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Fri, 20 Sep 2024 11:04:27 +0200 Subject: [PATCH 28/49] feat(me): handle contacts without an org in the form --- .../contact-card/contact-card.component.html | 2 +- .../form-field-contacts-for-resource.component.ts | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.html b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.html index 1254c513cc..66edd01964 100644 --- a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.html +++ b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.html @@ -1,7 +1,7 @@
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts index 6ea8c9c009..f0e4c9e67e 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts @@ -109,15 +109,14 @@ export class FormFieldContactsForResourceComponent updateContactsForRessource() { this.contactsForRessourceByRole = this.value.reduce((acc, contact) => { - const completeOrganization = this.allOrganizations.get( - contact.organization.name - ) + const completeOrganization = contact.organization + ? this.allOrganizations.get(contact.organization.name) + : null + const organization = completeOrganization ?? contact.organization const updatedContact = { ...contact, - organization: - completeOrganization ?? - ({ name: contact.organization.name } as Organization), + ...(organization && { organization }), } if (!acc.has(contact.role)) { From 9333a8572aa29b7d52affcc0d369d375f30e6191 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 23 Sep 2024 13:23:03 +0200 Subject: [PATCH 29/49] feat(converter): implement a custom XML output for dcat for GN compatibility The output should work for the DCAT-AP plugin --- .../src/lib/dcat-ap/dcat-ap.converter.spec.ts | 7 +- .../src/lib/dcat-ap/dcat-ap.converter.ts | 5 + .../src/lib/dcat-ap/utils/graph-utils.spec.ts | 102 +++++++++++++++++- .../src/lib/dcat-ap/utils/graph-utils.ts | 53 +++++---- .../src/lib/dcat-ap/utils/serialize-to-xml.ts | 97 +++++++++++++++++ .../src/lib/dcat-ap/write-parts.ts | 12 ++- .../metadata-converter/src/lib/xml-utils.ts | 18 +++- 7 files changed, 267 insertions(+), 27 deletions(-) create mode 100644 libs/api/metadata-converter/src/lib/dcat-ap/utils/serialize-to-xml.ts diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.spec.ts b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.spec.ts index cfb449a273..381f846c98 100644 --- a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.spec.ts +++ b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.spec.ts @@ -16,6 +16,7 @@ import { graph, parse } from 'rdflib' import { SEXTANT_BATHYMETRY_DATASET_RECORD } from '../fixtures/sextant.records' import { OPENDATASWISS_DATASET_RECORD } from '../fixtures/opendataswiss.records' import { VLAANDEREN_DATASET_RECORD } from '../fixtures/vlaanderen.dcat-ap.records' +import { exportGraphToXml } from './utils/serialize-to-xml' // this makes the xml go through the same formatting as the converter async function formatRdf(rdfString: string) { @@ -29,7 +30,7 @@ async function formatRdf(rdfString: string) { resolve ) ) - return dataStore.serialize('', 'application/rdf+xml', null) + return exportGraphToXml(dataStore) } describe('DCAT-AP converter', () => { @@ -91,8 +92,8 @@ describe('DCAT-AP converter', () => { }) describe('with a native record', () => { // FIXME: restore this test once we can write RDF XML as well! - describe.skip('when converting to XML and back', () => { - it('keeps the record unchanged', async () => { + describe('when converting to XML and back', () => { + it.skip('keeps the record unchanged', async () => { const backAndForth = await converter.readRecord( await converter.writeRecord(GENERIC_DATASET_RECORD) ) diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts index d48dc3f593..cbe09742dd 100644 --- a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +++ b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts @@ -30,6 +30,7 @@ import { DCAT, RDF } from './namespaces' import { BASE_URI } from './utils/uri' import type { ContentType } from 'rdflib/lib/types' import { loadGraph } from './utils/graph-utils' +import { exportGraphToXml } from './utils/serialize-to-xml' export class DcatApConverter extends BaseConverter { protected readers: Record< @@ -368,6 +369,10 @@ export class DcatApConverter extends BaseConverter { this.writers['lineage'](record, dataStore, recordNode) } + if (this.contentType.includes('xml')) { + return exportGraphToXml(dataStore) + } + return dataStore.serialize(undefined, this.contentType, null, {}) } } diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.spec.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.spec.ts index 6d364e8a26..4df45ba961 100644 --- a/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.spec.ts +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.spec.ts @@ -1,7 +1,17 @@ import { NamedNode, Store } from 'rdflib' -import { findNodeLocalized, loadGraph } from './graph-utils' +import { + findNodeLocalized, + getOrAddLocalizedLiteral, + loadGraph, +} from './graph-utils' import { DCAT, DCTERMS, RDF } from '../namespaces' +function graphToXml(dataStore: Store) { + return dataStore + .serialize(undefined, 'application/rdf+xml', null, {}) + .replace(/ rdf:nodeID=".+"/g, '') +} + describe('graph utils', () => { describe('findNodeLocalized', () => { it('finds a node with a specific language if present', async () => { @@ -101,4 +111,94 @@ describe('graph utils', () => { expect(result?.value).toBe('aantal ton gelost op de waterwegen') }) }) + + describe('getOrAddLocalizedObject', () => { + it('finds a node with a specific language if present', async () => { + const dataStore = new Store() + await loadGraph( + dataStore, + ` + + + aantal ton gelost op de waterwegen + guten morgen + bla bla + english title + +`, + 'application/rdf+xml' + ) + + const dataset = dataStore.the( + null, + RDF('type'), + DCAT('Dataset') + ) as NamedNode + getOrAddLocalizedLiteral( + dataStore, + dataset, + DCTERMS('title'), + 'new title in english', + 'en' + ) + expect(graphToXml(dataStore)).toBe(` + + aantal ton gelost op de waterwegen + guten morgen + bla bla + new title in english + + +`) + }) + it('modifies the first node without language if no language matches', async () => { + const dataStore = new Store() + await loadGraph( + dataStore, + ` + + + aantal ton gelost op de waterwegen + guten morgen + bla bla + + +`, + 'application/rdf+xml' + ) + + const dataset = dataStore.the( + null, + RDF('type'), + DCAT('Dataset') + ) as NamedNode + getOrAddLocalizedLiteral( + dataStore, + dataset, + DCTERMS('title'), + 'new title in english', + 'en' + ) + expect(graphToXml(dataStore)).toBe(` + + aantal ton gelost op de waterwegen + guten morgen + new title in english + + +`) + }) + }) }) diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.ts index 82d97d042a..fbd33c0537 100644 --- a/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.ts +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/graph-utils.ts @@ -1,6 +1,6 @@ import { lit, Literal, Node, parse, Statement, Store } from 'rdflib' import { Quad_Object, Quad_Predicate, Quad_Subject } from 'rdflib/lib/tf-types' -import { ContentType, ObjectType } from 'rdflib/lib/types' +import { ContentType } from 'rdflib/lib/types' import { BASE_URI } from './uri' export function findNodeLocalized( @@ -38,32 +38,47 @@ export function getOrAddStatement( return statement } -export function getOrAddLocalizedObject( +export function getOrAddLiteral( dataStore: Store, subject: Quad_Subject, predicate: Quad_Predicate, - object: Quad_Object | string, + objectValue: string +) { + let statement = dataStore.statementsMatching(subject, predicate, null)[0] + if (!statement) { + statement = dataStore.add(subject, predicate, lit(objectValue)) as Statement + } else { + statement.object = lit(objectValue) + } + return statement +} + +export function getOrAddLocalizedLiteral( + dataStore: Store, + subject: Quad_Subject, + predicate: Quad_Predicate, + objectValue: string, language: string ) { - let statement = dataStore - .statementsMatching( + const statements = dataStore.statementsMatching(subject, predicate, null) + const withMatchingLanguage = statements.filter( + (statement) => + statement.object instanceof Literal && + statement.object.language.startsWith(language) + ) + const withNoLanguage = statements.filter( + (statement) => + statement.object instanceof Literal && !statement.object.language + ) + let statement = withMatchingLanguage[0] || withNoLanguage[0] + if (!statement) { + statement = dataStore.add( subject, predicate, - typeof object === 'string' ? null : object - ) - .filter( - (statement) => - statement.object instanceof Literal && - statement.object.language.startsWith(language) - )[0] - if (!statement) { - statement = dataStore.add(subject, predicate, object) as Statement - ;(statement.object as Literal).language = language + lit(objectValue, language) + ) as Statement } else { - statement.object = - typeof object === 'string' - ? lit(object, language) - : (object as ObjectType) + statement.object = lit(objectValue, language) } return statement } diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/utils/serialize-to-xml.ts b/libs/api/metadata-converter/src/lib/dcat-ap/utils/serialize-to-xml.ts new file mode 100644 index 0000000000..53d73defaf --- /dev/null +++ b/libs/api/metadata-converter/src/lib/dcat-ap/utils/serialize-to-xml.ts @@ -0,0 +1,97 @@ +import { BlankNode, Literal, NamedNode, Statement, Store, sym } from 'rdflib' +import { XmlElement, XmlText } from '@rgrove/parse-xml' +import { DCAT, FOAF, RDF } from '../namespaces' +import { createDocument, NAMESPACES, xmlToString } from '../../xml-utils' +import { BASE_URI } from './uri' + +function fullNameToXml(fullName: string): [string, string] { + for (const key in NAMESPACES) { + if (fullName.startsWith(NAMESPACES[key])) { + return [key, fullName.replace(NAMESPACES[key], '')] + } + } + return [null, fullName] +} + +function createXmlElementFromNode( + dataStore: Store, + node: NamedNode | Literal +): XmlElement | XmlText { + if (node instanceof Literal) { + return new XmlText(node.value) + } + const type = dataStore.the(node, RDF('type')) as NamedNode + if (!type) return null + + const statements = dataStore.statementsMatching(node, null, null) + const children = statements + // filtering out the rdf:type statements because they are already used above + .filter((statement) => statement.predicate.value !== RDF('type').value) + .map((statement) => { + if (statement.object.toString() === statement.subject.toString()) { + return null + } + + // special cases: foaf:primaryTopic in dcat:CatalogRecord + if (statement.predicate.value === FOAF('primaryTopic').value) { + return new XmlElement(`foaf:primaryTopic`, { + 'rdf:resource': statement.object.value, + }) + } + + const [namespace, name] = fullNameToXml(statement.predicate.value) + const objectNode = createXmlElementFromNode( + dataStore, + statement.object as NamedNode + ) + if (!objectNode) { + // no object node: simply use the object value + return new XmlElement( + `${namespace}:${name}`, + { + 'rdf:resource': statement.object.value, + }, + [] + ) + } + // wrap the object node in the predicate one + return new XmlElement(`${namespace}:${name}`, {}, [objectNode]) + }) + .filter((child) => !!child) + const [namespace, name] = fullNameToXml(type.value) + const attributes = + node instanceof BlankNode + ? {} + : { + 'rdf:about': node.value, + } + return new XmlElement(`${namespace}:${name}`, attributes, children) +} + +/** + * This will output an XML document serialized as string, tailored in a way that it + * can be understood and indexed by GN4 dcat-ap plugin + * @param dataStore + */ +export function exportGraphToXml(dataStore: Store): string { + // we start with the catalog node (which is created if missing) + let catalogNode = dataStore.statementsMatching( + null, + RDF('type'), + DCAT('Catalog') + )[0]?.subject as NamedNode + if (!catalogNode) { + const recordNode = dataStore.the(null, RDF('type'), DCAT('CatalogRecord')) + const statement = dataStore.add( + sym(`${BASE_URI}catalog`), + RDF('type'), + DCAT('Catalog') + ) as Statement + catalogNode = statement.subject as NamedNode + dataStore.add(catalogNode, DCAT('record'), recordNode) + } + const rootEl = new XmlElement('rdf:RDF', {}, [ + createXmlElementFromNode(dataStore, catalogNode), + ]) + return xmlToString(createDocument(rootEl)) +} diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.ts b/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.ts index a6f6b6c868..03bd5a4881 100644 --- a/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/dcat-ap/write-parts.ts @@ -1,7 +1,11 @@ import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { NamedNode, Store, sym } from 'rdflib' import { DCAT, DCTERMS, FOAF, RDF } from './namespaces' -import { getOrAddLocalizedObject, getOrAddStatement } from './utils/graph-utils' +import { + getOrAddLiteral, + getOrAddLocalizedLiteral, + getOrAddStatement, +} from './utils/graph-utils' import { BASE_URI } from './utils/uri' function getOrAddDatasetNode( @@ -37,13 +41,14 @@ export function writeTitle( recordNode: NamedNode ) { const dataset = getOrAddDatasetNode(record, dataStore, recordNode) - getOrAddLocalizedObject( + getOrAddLocalizedLiteral( dataStore, dataset, DCTERMS('title'), record.title, 'en' ) + getOrAddLiteral(dataStore, dataset, DCTERMS('title'), record.title) } export function writeAbstract( @@ -52,11 +57,12 @@ export function writeAbstract( recordNode: NamedNode ) { const dataset = getOrAddDatasetNode(record, dataStore, recordNode) - getOrAddLocalizedObject( + getOrAddLocalizedLiteral( dataStore, dataset, DCTERMS('description'), record.abstract, 'en' ) + getOrAddLiteral(dataStore, dataset, DCTERMS('description'), record.abstract) } diff --git a/libs/api/metadata-converter/src/lib/xml-utils.ts b/libs/api/metadata-converter/src/lib/xml-utils.ts index 1e2b70f563..3b6146a90a 100644 --- a/libs/api/metadata-converter/src/lib/xml-utils.ts +++ b/libs/api/metadata-converter/src/lib/xml-utils.ts @@ -249,7 +249,7 @@ function extractNamespace(name: string): string | null { return colon > -1 ? name.substring(0, colon) : null } -const NAMESPACES = { +export const NAMESPACES = { gmd: 'http://www.isotc211.org/2005/gmd', gco: 'http://www.isotc211.org/2005/gco', gfc: 'http://www.isotc211.org/2005/gfc', @@ -284,6 +284,22 @@ const NAMESPACES = { cat: 'http://standards.iso.org/iso/19115/-3/cat/1.0', lan: 'http://standards.iso.org/iso/19115/-3/lan/1.0', mrc: 'http://standards.iso.org/iso/19115/-3/mrc/2.0', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + rdfs: 'http://www.w3.org/2000/01/rdf-schema#', + foaf: 'http://xmlns.com/foaf/0.1/', + xsd: 'http://www.w3.org/2001/XMLSchema#', + dcat: 'http://www.w3.org/ns/dcat#', + dct: 'http://purl.org/dc/terms/', + skos: 'http://www.w3.org/2004/02/skos/core#', + schema_org: 'http://schema.org/', + spdx: 'https://spdx.org/rdf/terms/#', + adms: 'http://www.w3.org/ns/adms#', + dqv: 'http://www.w3.org/ns/dqv#', + owl: 'http://www.w3.org/2002/07/owl#', + vcard: 'http://www.w3.org/2006/vcard/ns#', + time: 'http://www.w3.org/2006/time#', + locn: 'http://www.w3.org/ns/locn#', + mdcat: 'https://data.vlaanderen.be/ns/metadata-dcat#', } /** From acc3eb980f703464b0fa3d1179583d588a3f9c06 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 24 Sep 2024 13:47:04 +0200 Subject: [PATCH 30/49] feat(md-converter): allow ttl, jsonld and n3 files, handle missing owner org --- .../src/app/app.component.html | 2 +- .../record-form/record-form.component.html | 56 +++++++++++-------- .../record-form/record-form.component.ts | 10 ++++ 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/apps/metadata-converter/src/app/app.component.html b/apps/metadata-converter/src/app/app.component.html index 94c186c01e..c706338321 100644 --- a/apps/metadata-converter/src/app/app.component.html +++ b/apps/metadata-converter/src/app/app.component.html @@ -14,7 +14,7 @@

- - - - - + + + Add a owner organization to this record + + + + + + +

diff --git a/apps/metadata-converter/src/app/components/record-form/record-form.component.ts b/apps/metadata-converter/src/app/components/record-form/record-form.component.ts index 4b6ef8e950..ca7bf21862 100644 --- a/apps/metadata-converter/src/app/components/record-form/record-form.component.ts +++ b/apps/metadata-converter/src/app/components/record-form/record-form.component.ts @@ -120,4 +120,14 @@ export class RecordFormComponent implements AfterViewInit { } this.recordChanged.emit(this.record) } + + addOwnerOrg() { + this.record = { + ...this.record, + ownerOrganization: { + name: 'My Organization', + }, + } + this.recordChanged.emit(this.record) + } } From 5eb5f9f0fdbb7cf557a3d755384934390d074570 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 24 Sep 2024 16:34:43 +0200 Subject: [PATCH 31/49] feat(search): separate search inputs for allRecords and myRecords hide search for drafts for now --- .../dashboard-menu.component.html | 1 - .../dashboard-menu.component.ts | 10 +- .../dashboard/dashboard-page.component.html | 10 +- .../search-header/search-header.component.ts | 8 +- .../all-records/all-records.component.html | 136 ++++++++++-------- .../all-records/all-records.component.ts | 4 + .../records/my-draft/my-draft.component.html | 41 +++--- .../records/my-draft/my-draft.component.ts | 2 + .../my-records/my-records.component.html | 120 +++++++++------- .../my-records/my-records.component.ts | 7 +- 10 files changed, 176 insertions(+), 163 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html index 547abf9c4c..7ded10a5ee 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html @@ -3,7 +3,6 @@

-
- -
-
-
- -
- -
+

diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.ts b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.ts index 04a219aa90..9b547ab0a0 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.ts @@ -4,7 +4,6 @@ import { MatIconModule } from '@angular/material/icon' import { LetDirective } from '@ngrx/component' import { FeatureSearchModule, - FuzzySearchComponent, SearchService, } from '@geonetwork-ui/feature/search' import { UiElementsModule } from '@geonetwork-ui/ui/elements' @@ -32,18 +31,13 @@ import { RouterFacade } from '@geonetwork-ui/feature/router' export class SearchHeaderComponent { public placeholder$ = this.avatarService.getPlaceholder() activeBtn = false - @ViewChild('fuzzySearch') fuzzySearch: FuzzySearchComponent constructor( public platformService: PlatformServiceInterface, private avatarService: AvatarServiceInterface, private searchService: SearchService, private routerFacade: RouterFacade - ) { - this.routerFacade.currentRoute$.subscribe(() => { - this.fuzzySearch?.autocomplete?.clear() - }) - } + ) {} handleItemSelection(item: CatalogRecord) { this.searchService.updateFilters({ any: item.title }) diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.html b/apps/metadata-editor/src/app/records/all-records/all-records.component.html index 29848acc54..03601657c8 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.html +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.html @@ -1,69 +1,79 @@ -
-
- -

- dashboard.records.search -

-
- -
-
- -

- dashboard.records.all -

-
- -
-
+
+ +
+
+
+
-
-
- dashboard.results.listMetadata +
+
+ +

+ dashboard.records.search +

+
+ +
+
+ +

+ dashboard.records.all +

+
+ +
+
-
- dashboard.results.listResources -
-
- - dashboard.importRecord - keyboard_arrow_down + dashboard.results.listMetadata +
+
+ dashboard.results.listResources +
+
+ - keyboard_arrow_updashboard.importRecord + keyboard_arrow_down + keyboard_arrow_up + + + + + - - - - - - edit_document - dashboard.createRecord - -
+ edit_document + dashboard.createRecord + +
- -
+ + +
diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts index beb5980686..8ff48433ad 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts @@ -29,6 +29,8 @@ import { TemplatePortal } from '@angular/cdk/portal' import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' import { RecordsListComponent } from '../records-list.component' import { map } from 'rxjs/operators' +import { SearchHeaderComponent } from '../../dashboard/search-header/search-header.component' +import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifications' @Component({ selector: 'md-editor-all-records', @@ -47,6 +49,8 @@ import { map } from 'rxjs/operators' CdkOverlayOrigin, CdkConnectedOverlay, RecordsListComponent, + SearchHeaderComponent, + NotificationsContainerComponent, ], }) export class AllRecordsComponent { diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html index 24ef15fa5a..21f0adc770 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html @@ -1,20 +1,25 @@ -
-
-

- dashboard.records.myDraft -

+
+
+
+
+
+

+ dashboard.records.myDraft +

+
-
- -
-
+
+ +
+
+
diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts index 88dfa0005e..0c56c0f624 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts @@ -12,6 +12,7 @@ import { TranslateModule } from '@ngx-translate/core' import { startWith, switchMap } from 'rxjs' import { RecordsCountComponent } from '../records-count/records-count.component' import { RecordsListComponent } from '../records-list.component' +import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifications' @Component({ selector: 'md-editor-my-my-draft', templateUrl: './my-draft.component.html', @@ -27,6 +28,7 @@ import { RecordsListComponent } from '../records-list.component' ResultsTableContainerComponent, UiElementsModule, ResultsTableComponent, + NotificationsContainerComponent, ], }) export class MyDraftComponent { 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 a8456fafd4..74cb62ec02 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,61 +1,71 @@ -
-
- -

- dashboard.records.myRecords -

-
- -
-
+
+ +
+
+
+
-
-
- dashboard.myRecords.publishedMetadatas +
+
+ +

+ dashboard.records.myRecords +

+
+ +
+
-
- dashboard.myRecords.currentlyEdited -
-
- - dashboard.importRecord - keyboard_arrow_down + dashboard.myRecords.publishedMetadatas +
+
+ dashboard.myRecords.currentlyEdited +
+
+ - keyboard_arrow_updashboard.importRecord + keyboard_arrow_down + keyboard_arrow_up + + + + + - - - - - - edit_document - dashboard.createRecord - -
+ edit_document + dashboard.createRecord + +
- -
+ + +
diff --git a/apps/metadata-editor/src/app/records/my-records/my-records.component.ts b/apps/metadata-editor/src/app/records/my-records/my-records.component.ts index 50adce074b..8f6be35987 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 @@ -18,7 +18,6 @@ import { } from '@geonetwork-ui/feature/search' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { UiElementsModule } from '@geonetwork-ui/ui/elements' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { Router } from '@angular/router' import { Overlay, OverlayRef } from '@angular/cdk/overlay' import { TemplatePortal } from '@angular/cdk/portal' @@ -26,6 +25,8 @@ import { RecordsCountComponent } from '../records-count/records-count.component' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { MatIconModule } from '@angular/material/icon' import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' +import { SearchHeaderComponent } from '../../dashboard/search-header/search-header.component' +import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifications' @Component({ selector: 'md-editor-my-records', @@ -43,6 +44,8 @@ import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' MatIconModule, ImportRecordComponent, FeatureSearchModule, + SearchHeaderComponent, + NotificationsContainerComponent, ], }) export class MyRecordsComponent implements OnInit { @@ -64,6 +67,8 @@ export class MyRecordsComponent implements OnInit { ) {} ngOnInit() { + this.searchFacade.resetSearch() + this.platformService.getMe().subscribe((user) => { this.fieldsService .buildFiltersFromFieldValues({ owner: user.id }) From 197304cb05f92fdaf96a3fe565915ca6ac1860f8 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 24 Sep 2024 16:47:49 +0200 Subject: [PATCH 32/49] feat(search): navigate to record on autocomplete select --- .../search-header/search-header.component.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.ts b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.ts index 9b547ab0a0..1025a8590c 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.ts @@ -2,16 +2,12 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core' import { MatIconModule } from '@angular/material/icon' import { LetDirective } from '@ngrx/component' -import { - FeatureSearchModule, - SearchService, -} from '@geonetwork-ui/feature/search' +import { FeatureSearchModule } from '@geonetwork-ui/feature/search' import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' -import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { TranslateModule } from '@ngx-translate/core' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' -import { RouterFacade } from '@geonetwork-ui/feature/router' +import { Router } from '@angular/router' @Component({ selector: 'md-editor-search-header', @@ -33,13 +29,11 @@ export class SearchHeaderComponent { activeBtn = false constructor( - public platformService: PlatformServiceInterface, private avatarService: AvatarServiceInterface, - private searchService: SearchService, - private routerFacade: RouterFacade + private router: Router ) {} handleItemSelection(item: CatalogRecord) { - this.searchService.updateFilters({ any: item.title }) + this.router.navigate(['edit', item.uniqueIdentifier]) } } From a6f78250c492def2a506d9e9ddb3c471bc1b9a8a Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 24 Sep 2024 14:29:56 +0200 Subject: [PATCH 33/49] feat(converter): allow clearing dates on iso schemas, better logic & tests --- ...eneric-dataset+geo2france-plu.iso19139.xml | 37 ++-- .../src/lib/iso19115-3/write-parts.spec.ts | 98 ++++++++++- .../src/lib/iso19115-3/write-parts.ts | 14 +- .../src/lib/iso19139/write-parts.spec.ts | 78 +++++++++ .../src/lib/iso19139/write-parts.ts | 161 ++++++++---------- 5 files changed, 267 insertions(+), 121 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+geo2france-plu.iso19139.xml b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+geo2france-plu.iso19139.xml index fd5058ace3..0d77d758ce 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+geo2france-plu.iso19139.xml +++ b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+geo2france-plu.iso19139.xml @@ -45,29 +45,6 @@ A very interesting dataset (un jeu de données très intéressant) - - - - 2022-12-04T15:12:00 - - - - - - - - - - 2022-05-01 - - - - - - @@ -81,9 +58,17 @@ 2022-09-01T14:18:19 - + + + + + + + + 2022-12-04T15:12:00 + + + diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts index f8e3f29ab8..4056e3bb10 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts @@ -1,5 +1,12 @@ import { GENERIC_DATASET_RECORD } from '../fixtures/generic.records' -import { writeContactsForResource, writeOnlineResources } from './write-parts' +import { + writeContactsForResource, + writeOnlineResources, + writeRecordCreated, + writeResourceCreated, + writeResourcePublished, + writeResourceUpdated, +} from './write-parts' import { createElement, getRootElement, @@ -22,6 +29,95 @@ describe('write parts', () => { datasetRecord = { ...GENERIC_DATASET_RECORD } }) + describe('write dates', () => { + it('writes the record dates', () => { + const modified = { + ...datasetRecord, + resourcePublished: new Date('2024-01-01T00:00:00Z'), + } + writeRecordCreated(modified, rootEl) + writeResourceCreated(modified, rootEl) + writeResourceUpdated(modified, rootEl) + writeResourcePublished(modified, rootEl) + expect(rootAsString()).toEqual(` + + + + 2021-11-15T09:00:00 + + + creation + + + + + + + + + + + 2022-09-01T14:18:19 + + + creation + + + + + + + 2022-12-04T15:12:00 + + + revision + + + + + + + 2024-01-01T01:00:00 + + + publication + + + + + + + +`) + }) + it('delete date if the date is not present in the record', () => { + // first write dates + writeRecordCreated(datasetRecord, rootEl) + writeResourceCreated(datasetRecord, rootEl) + writeResourceUpdated(datasetRecord, rootEl) + const modified = { + ...datasetRecord, + recordCreated: null, + resourceUpdated: null, + resourceCreated: null, + resourcePublished: null, + } + writeRecordCreated(modified, rootEl) + writeResourceCreated(modified, rootEl) + writeResourceUpdated(modified, rootEl) + writeResourcePublished(modified, rootEl) + expect(rootAsString()).toEqual(` + + + + + + + +`) + }) + }) + describe('writeOnlineResources', () => { const distributionShp = GENERIC_DATASET_RECORD.onlineResources[0] const distributionLink = GENERIC_DATASET_RECORD.onlineResources[2] diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts index 24aacc8948..78a1d26d9e 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts @@ -125,7 +125,7 @@ export function writeRecordUpdated(record: CatalogRecord, rootEl: XmlElement) { export function writeRecordCreated(record: CatalogRecord, rootEl: XmlElement) { removeRecordDate('creation')(rootEl) - if (!('recordCreated' in record)) return + if (!record.recordCreated) return appendRecordDate(record.recordCreated, 'creation')(rootEl) } @@ -134,14 +134,14 @@ export function writeRecordPublished( rootEl: XmlElement ) { removeRecordDate('publication')(rootEl) - if (!('recordPublished' in record)) return + if (!record.recordPublished) return appendRecordDate(record.recordPublished, 'publication')(rootEl) } function removeResourceDate(type: 'revision' | 'creation' | 'publication') { return pipe( - findIdentification(), - findNestedElement('mri:citation', 'cit:CI_Citation'), + findOrCreateIdentification(), + findNestedChildOrCreate('mri:citation', 'cit:CI_Citation'), removeChildren( pipe( findChildrenElement('cit:date', false), @@ -191,7 +191,7 @@ export function writeResourceUpdated( rootEl: XmlElement ) { removeResourceDate('revision')(rootEl) - if (!('resourceUpdated' in record)) return + if (!record.resourceUpdated) return appendResourceDate(record.resourceUpdated, 'revision')(rootEl) } @@ -200,7 +200,7 @@ export function writeResourceCreated( rootEl: XmlElement ) { removeResourceDate('creation')(rootEl) - if (!('resourceCreated' in record)) return + if (!record.resourceCreated) return appendResourceDate(record.resourceCreated, 'creation')(rootEl) } @@ -209,7 +209,7 @@ export function writeResourcePublished( rootEl: XmlElement ) { removeResourceDate('publication')(rootEl) - if (!('resourcePublished' in record)) return + if (!record.resourcePublished) return appendResourceDate(record.resourcePublished, 'publication')(rootEl) } diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts index 6cc13027ff..ca6d3aeb92 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts @@ -11,6 +11,9 @@ import { getISODuration, writeKeywords, writeOnlineResources, + writeResourceCreated, + writeResourcePublished, + writeResourceUpdated, writeSpatialExtents, writeTemporalExtents, } from './write-parts' @@ -28,6 +31,81 @@ describe('write parts', () => { datasetRecord = { ...GENERIC_DATASET_RECORD } }) + describe('write dates', () => { + it('writes the record dates', () => { + const modified = { + ...datasetRecord, + resourcePublished: new Date('2024-01-01T00:00:00Z'), + } + writeResourceCreated(modified, rootEl) + writeResourceUpdated(modified, rootEl) + writeResourcePublished(modified, rootEl) + expect(rootAsString()).toEqual(` + + + + + + + + 2022-09-01T14:18:19 + + + + + + + + + + 2022-12-04T15:12:00 + + + + + + + + + + 2024-01-01T01:00:00 + + + + + + + + + + +`) + }) + it('delete date if the date is not present in the record', () => { + // first write dates + writeResourceCreated(datasetRecord, rootEl) + writeResourceUpdated(datasetRecord, rootEl) + const modified = { + ...datasetRecord, + resourceUpdated: null, + resourceCreated: null, + resourcePublished: null, + } + writeResourceCreated(modified, rootEl) + writeResourceUpdated(modified, rootEl) + writeResourcePublished(modified, rootEl) + expect(rootAsString()).toEqual(` + + + + + + + +`) + }) + }) + describe('writeOnlineResources', () => { const distributionShp = GENERIC_DATASET_RECORD.onlineResources[0] const distributionLink = GENERIC_DATASET_RECORD.onlineResources[2] diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts index 8471232292..96f9d14049 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts @@ -19,9 +19,7 @@ import format from 'date-fns/format' import { Geometry } from 'geojson' import { ChainableFunction, - fallback, filterArray, - getAtIndex, map, mapArray, noop, @@ -33,10 +31,10 @@ import { appendChildren, createChild, createElement, - findChildElement, findChildOrCreate, findChildrenElement, findNestedChildOrCreate, + findNestedElement, findNestedElements, readAttribute, removeAllChildren, @@ -84,11 +82,9 @@ export function writeAnchor( export function writeDateTime( date: Date ): ChainableFunction { - return tap( - pipe( - findChildOrCreate('gco:DateTime'), - setTextContent(format(date, "yyyy-MM-dd'T'HH:mm:ss")) - ) + return pipe( + findChildOrCreate('gco:DateTime'), + setTextContent(format(date, "yyyy-MM-dd'T'HH:mm:ss")) ) } @@ -328,50 +324,6 @@ export function appendResponsibleParty(contact: Individual) { ) } -export function updateCitationDate( - date: Date, - type: 'revision' | 'creation' | 'publication' -) { - return pipe( - findNestedElements('gmd:date', 'gmd:CI_Date'), - filterArray( - pipe( - findChildElement('gmd:CI_DateTypeCode'), - readAttribute('codeListValue'), - map((value) => value === type) - ) - ), - getAtIndex(0), - findChildElement('gmd:date'), - removeAllChildren(), - writeDateTime(date) - ) -} - -export function appendCitationDate( - date: Date, - type: 'revision' | 'creation' | 'publication' -) { - return appendChildren( - pipe( - createElement('gmd:date'), - createChild('gmd:CI_Date'), - appendChildren( - pipe(createElement('gmd:date'), writeDateTime(date)), - pipe( - createElement('gmd:dateType'), - createChild('gmd:CI_DateTypeCode'), - addAttribute( - 'codeList', - 'http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#CI_DateTypeCode' - ), - addAttribute('codeListValue', type) - ) - ) - ) - ) -} - export function removeKeywords() { return removeChildren(pipe(findNestedElements('gmd:descriptiveKeywords'))) } @@ -745,14 +697,6 @@ export function writeKind(record: CatalogRecord, rootEl: XmlElement) { )(rootEl) } -export function writeRecordUpdated(record: CatalogRecord, rootEl: XmlElement) { - pipe( - findChildOrCreate('gmd:dateStamp'), - removeAllChildren(), - writeDateTime(record.recordUpdated) - )(rootEl) -} - export function writeTitle(record: CatalogRecord, rootEl: XmlElement) { pipe( findOrCreateIdentification(), @@ -912,49 +856,92 @@ export function writeUpdateFrequency( )(rootEl) } -export function writeResourceCreated( - record: DatasetRecord, - rootEl: XmlElement -) { - if (!('resourceCreated' in record)) return +export function writeRecordUpdated(record: CatalogRecord, rootEl: XmlElement) { pipe( + findChildOrCreate('gmd:dateStamp'), + removeAllChildren(), + writeDateTime(record.recordUpdated) + )(rootEl) +} + +export function removeResourceDate( + type: 'revision' | 'creation' | 'publication' +) { + return pipe( findOrCreateIdentification(), findNestedChildOrCreate('gmd:citation', 'gmd:CI_Citation'), - fallback( - updateCitationDate(record.resourceCreated, 'creation'), - appendCitationDate(record.resourceCreated, 'creation') + removeChildren( + pipe( + findNestedElements('gmd:date'), + filterArray( + pipe( + findNestedElement( + 'gmd:CI_Date', + 'gmd:dateType', + 'gmd:CI_DateTypeCode' + ), + readAttribute('codeListValue'), + map((value) => value === type) + ) + ) + ) ) - )(rootEl) + ) } -export function writeResourceUpdated( - record: DatasetRecord, - rootEl: XmlElement +export function appendResourceDate( + date: Date, + type: 'revision' | 'creation' | 'publication' ) { - if (!('resourceUpdated' in record)) return - pipe( + return pipe( findOrCreateIdentification(), findNestedChildOrCreate('gmd:citation', 'gmd:CI_Citation'), - fallback( - updateCitationDate(record.resourceUpdated, 'revision'), - appendCitationDate(record.resourceUpdated, 'revision') + appendChildren( + pipe( + createElement('gmd:date'), + createChild('gmd:CI_Date'), + appendChildren( + pipe(createElement('gmd:date'), writeDateTime(date)), + pipe( + createElement('gmd:dateType'), + createChild('gmd:CI_DateTypeCode'), + addAttribute( + 'codeList', + 'http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#CI_DateTypeCode' + ), + addAttribute('codeListValue', type) + ) + ) + ) ) - )(rootEl) + ) +} + +export function writeResourceCreated( + record: DatasetRecord, + rootEl: XmlElement +) { + removeResourceDate('creation')(rootEl) + if (!record.resourceCreated) return + appendResourceDate(record.resourceCreated, 'creation')(rootEl) +} + +export function writeResourceUpdated( + record: DatasetRecord, + rootEl: XmlElement +) { + removeResourceDate('revision')(rootEl) + if (!record.resourceUpdated) return + appendResourceDate(record.resourceUpdated, 'revision')(rootEl) } export function writeResourcePublished( record: DatasetRecord, rootEl: XmlElement ) { - if (!('resourcePublished' in record)) return - pipe( - findOrCreateIdentification(), - findNestedChildOrCreate('gmd:citation', 'gmd:CI_Citation'), - fallback( - updateCitationDate(record.resourcePublished, 'publication'), - appendCitationDate(record.resourcePublished, 'publication') - ) - )(rootEl) + removeResourceDate('publication')(rootEl) + if (!record.resourcePublished) return + appendResourceDate(record.resourcePublished, 'publication')(rootEl) } export function writeSpatialRepresentation( From ce0c15158aafeb0676b904b128a23f5946d1a6f3 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 24 Sep 2024 14:33:22 +0200 Subject: [PATCH 34/49] feat(converter): make sure mandatory fields are filled in DCAT records Also fix a contact role --- .../src/lib/dcat-ap/dcat-ap.converter.ts | 4 ++-- .../metadata-converter/src/lib/dcat-ap/read-parts.ts | 4 ++-- .../src/lib/fixtures/eu.dcat-ap.records.ts | 10 +++++----- .../src/lib/fixtures/opendataswiss.records.ts | 6 +++--- .../src/lib/fixtures/sextant.records.ts | 4 ++-- .../src/lib/fixtures/vlaanderen.dcat-ap.records.ts | 6 +++--- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts index cbe09742dd..cda2c800e1 100644 --- a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +++ b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts @@ -60,10 +60,10 @@ export class DcatApConverter extends BaseConverter { legalConstraints: () => [], securityConstraints: () => [], otherConstraints: () => [], - status: () => undefined, + status: () => 'completed', updateFrequency: () => 'unknown', overviews: () => [], - lineage: () => undefined, + lineage: () => '', temporalExtents: () => [], spatialRepresentation: () => undefined, extras: () => undefined, diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.ts b/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.ts index 1e496c84ec..db0c9d91ec 100644 --- a/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.ts +++ b/libs/api/metadata-converter/src/lib/dcat-ap/read-parts.ts @@ -66,7 +66,7 @@ function mapContactFromStatement( ? email.value.replace(/^mailto:/, '') : 'missing@missing.com' return { - role: role?.value ?? 'pointOfContact', + role: role?.value ?? 'point_of_contact', email: emailValue, ...(firstName && { firstName }), ...(lastName && { lastName }), @@ -303,7 +303,7 @@ export function readRecordUpdated( ): Date { const dateString = dataStore.the(recordNode, DCTERMS('modified'), null)?.value if (dateString) return new Date(dateString) - return null + return new Date() // record updated is a mandatory field } export function readResourceCreated( diff --git a/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts b/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts index 7df7ba3741..601d16fd18 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts @@ -12,7 +12,7 @@ export const EU_SURVEY_DATASET_RECORD: DatasetRecord = { firstName: 'Rossalina', lastName: 'Latcheva (PhD)', email: 'missing@missing.com', - role: 'pointOfContact', + role: 'point_of_contact', }, ], landingPage: new URL( @@ -228,7 +228,7 @@ export const EU_SURVEY_DATASET_RECORD: DatasetRecord = { 'http://publications.europa.eu/resource/authority/data-theme/SOCI', 'http://publications.europa.eu/resource/authority/data-theme/EDUC', ], - lineage: undefined, + lineage: '', ownerOrganization: { name: 'http://publications.europa.eu/resource/authority/corporate-body/FRA', }, @@ -236,7 +236,7 @@ export const EU_SURVEY_DATASET_RECORD: DatasetRecord = { recordUpdated: new Date('2022-04-22T12:08:18.000Z'), resourceCreated: new Date('2017-12-19T00:00:00.000Z'), resourceUpdated: new Date('2018-12-14T00:00:00.000Z'), - status: undefined, + status: 'completed', updateFrequency: 'unknown', } @@ -264,9 +264,9 @@ export const EU_WHOISWHO_DATASET_RECORD: DatasetRecord = { If you detect any errors, please report them to: whoiswho@publications.europa.eu\r \r [Privacy statement of EU Whoiswho](https://op.europa.eu/en/web/about-us/legal-notices/op_whoiswho).`, - lineage: undefined, + lineage: '', ownerOrganization: undefined, recordUpdated: undefined, - status: undefined, + status: 'completed', updateFrequency: 'unknown', } diff --git a/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts b/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts index 359e95667d..f5cb4fdaf8 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts @@ -8,7 +8,7 @@ export const OPENDATASWISS_DATASET_RECORD: DatasetRecord = { contactsForResource: [ { email: 'geoinformation@bfe.admin.ch', - role: 'pointOfContact', + role: 'point_of_contact', firstName: 'geoinformation@bfe.admin.ch', }, ], @@ -97,7 +97,7 @@ export const OPENDATASWISS_DATASET_RECORD: DatasetRecord = { 'http://dcat-ap.ch/vocabulary/themes/geography', ], abstract: `The Energy City label is used for certifying municipalities that develop and implement a sustainable energy policy. Municipalities that have been awarded this label promote renewable energy and ecological mobility, and focus on the efficient use of resources. In order to qualify for the label, a municipality must have realised or adopted at least 50 percent of its scope for action in the area of energy policy. Here the calculation is based on the Energy Cities catalogue. The European Energy Award®GOLD is the highest level of certification. This label is awarded to municipalities that have implemented at least 75 percent of the measures listed in the catalogue at the time of certification. Municipalities that qualify for this award demonstrate the highest level of commitment towards a sustainable energy future. The label is based on an assessment of municipal energy policy in the areas of development and spatial planning, municipal buildings and installations, supply and disposal, mobility, internal organisation, communication and cooperation.`, - lineage: undefined, + lineage: '', ownerOrganization: { name: 'Bundesamt für Energie', }, @@ -105,6 +105,6 @@ export const OPENDATASWISS_DATASET_RECORD: DatasetRecord = { recordUpdated: new Date('2022-06-16T20:06:22.000Z'), resourceCreated: new Date('2014-06-30T00:00:00.000'), resourceUpdated: new Date('2022-06-15T00:00:00.000'), - status: undefined, + status: 'completed', updateFrequency: 'unknown', } diff --git a/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts b/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts index ef5ca0eeae..676180cf70 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts @@ -76,10 +76,10 @@ export const SEXTANT_BATHYMETRY_DATASET_RECORD: DatasetRecord = { abstract: `Modèle bathymétrique (MNT) d'une partie de l'édifice sédimentaire sous-marin du Golo, à l'Est du Cap Corse. Les données ont été acquises lors de la campagne océanographique Sigolo en 2008, avec les sondeurs multifaisceaux EM300 et EM1000. Le pas de la grille est de 25m. Produit interne Ifremer.`, - lineage: undefined, + lineage: '', ownerOrganization: null, recordUpdated: new Date('2020-06-03T22:34:05.000Z'), resourceUpdated: new Date('2020-06-03T22:34:05.000Z'), - status: undefined, + status: 'completed', updateFrequency: 'unknown', } diff --git a/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts b/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts index a2de7d2e38..c85408167d 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts @@ -11,7 +11,7 @@ export const VLAANDEREN_DATASET_RECORD: DatasetRecord = { firstName: 'David', lastName: 'Buysse', email: 'david.buysse@inbo.be', - role: 'pointOfContact', + role: 'point_of_contact', }, ], ownerOrganization: { @@ -65,9 +65,9 @@ export const VLAANDEREN_DATASET_RECORD: DatasetRecord = { ], temporalExtents: [], topics: ['biodiversity'], - lineage: undefined, + lineage: '', recordUpdated: new Date('2024-09-19T01:15:09.732Z'), resourceUpdated: new Date('2021-04-14T11:15+02:00'), - status: undefined, + status: 'completed', updateFrequency: 'unknown', } From d8df3f009a64e5f134b331a52203a025f1d34c9f Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 24 Sep 2024 23:01:48 +0200 Subject: [PATCH 35/49] feat(converter): improve contacts writing in ISO --- .../lib/iso19115-3/iso19115-3.converter.ts | 3 +- .../src/lib/iso19115-3/write-parts.spec.ts | 156 ++++++++++++++++++ .../src/lib/iso19115-3/write-parts.ts | 28 ---- .../src/lib/iso19139/write-parts.spec.ts | 125 ++++++++++++++ .../src/lib/iso19139/write-parts.ts | 17 +- 5 files changed, 293 insertions(+), 36 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts b/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts index 968f1dcedf..371d253197 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts @@ -22,7 +22,6 @@ import { writeLandingPage, writeLineage, writeOnlineResources, - writeOwnerOrganization, writeRecordCreated, writeRecordPublished, writeRecordUpdated, @@ -60,7 +59,7 @@ export class Iso191153Converter extends Iso19139Converter { this.writers['resourcePublished'] = writeResourcePublished this.writers['contacts'] = writeContacts this.writers['contactsForResource'] = writeContactsForResource - this.writers['ownerOrganization'] = writeOwnerOrganization + this.writers['ownerOrganization'] = () => undefined // fixme: find a way to store this value properly this.writers['landingPage'] = writeLandingPage this.writers['lineage'] = writeLineage this.writers['onlineResources'] = writeOnlineResources diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts index 4056e3bb10..7bd1e6633e 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts @@ -1,5 +1,6 @@ import { GENERIC_DATASET_RECORD } from '../fixtures/generic.records' import { + writeContacts, writeContactsForResource, writeOnlineResources, writeRecordCreated, @@ -449,6 +450,161 @@ describe('write parts', () => { +`) + }) + }) + + describe('writeContacts', () => { + it('works with incomplete contacts', () => { + const contacts = [ + { + firstName: 'John', + role: 'point_of_contact', + email: 'aaa@bbb.ccc', + }, + { + lastName: 'Doe', + role: 'contributor', + email: 'abc@def.ghi', + organization: { + name: 'ACME', + }, + }, + ] + const modified: DatasetRecord = { + ...datasetRecord, + contacts, + contactsForResource: contacts, + } + writeContacts(modified, rootEl) + writeContactsForResource(modified, rootEl) + expect(rootAsString()).toEqual(` + + + + pointOfContact + + + + + + + + + aaa@bbb.ccc + + + + + + + + + John + + + + + + + + + + + contributor + + + + + ACME + + + + + + + abc@def.ghi + + + + + + + + + Doe + + + + + + + + + + + + + pointOfContact + + + + + + + + + aaa@bbb.ccc + + + + + + + + + John + + + + + + + + + + + contributor + + + + + ACME + + + + + + + abc@def.ghi + + + + + + + + + Doe + + + + + + + + + `) }) }) diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts index 78a1d26d9e..feb3443acd 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts @@ -17,7 +17,6 @@ import { findNestedElement, findNestedElements, readAttribute, - removeAllChildren, removeChildren, removeChildrenByName, setTextContent, @@ -213,33 +212,6 @@ export function writeResourcePublished( appendResourceDate(record.resourcePublished, 'publication')(rootEl) } -export function writeOwnerOrganization( - record: CatalogRecord, - rootEl: XmlElement -) { - // if no contact matches the owner org, create an empty one - const ownerContact: Individual = record.contacts.find( - (contact) => contact.organization.name === record.ownerOrganization.name - ) - pipe( - findChildOrCreate('mdb:contact'), - removeAllChildren(), - appendResponsibleParty( - ownerContact - ? { - ...ownerContact, - // owner responsible party is always point of contact - role: 'point_of_contact', - } - : { - organization: record.ownerOrganization, - email: 'missing@missing.com', - role: 'point_of_contact', - } - ) - )(rootEl) -} - export function appendResponsibleParty(contact: Individual) { const fullName = namePartsToFull(contact.firstName, contact.lastName) diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts index ca6d3aeb92..7c038e5df4 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts @@ -9,6 +9,8 @@ import { } from '../xml-utils' import { getISODuration, + writeContacts, + writeContactsForResource, writeKeywords, writeOnlineResources, writeResourceCreated, @@ -785,6 +787,129 @@ describe('write parts', () => { }) }) + describe('writeContacts', () => { + it('works with incomplete contacts', () => { + const contacts = [ + { + firstName: 'John', + role: 'point_of_contact', + email: 'aaa@bbb.ccc', + }, + { + lastName: 'Doe', + role: 'contributor', + email: 'abc@def.ghi', + organization: { + name: 'ACME', + }, + }, + ] + const modified: DatasetRecord = { + ...datasetRecord, + contacts, + contactsForResource: contacts, + } + writeContacts(modified, rootEl) + writeContactsForResource(modified, rootEl) + expect(rootAsString()).toEqual(` + + + + John + + + + + + + aaa@bbb.ccc + + + + + + + + + + + + + + Doe + + + ACME + + + + + + + abc@def.ghi + + + + + + + + + + + + + + + + John + + + + + + + aaa@bbb.ccc + + + + + + + + + + + + + + Doe + + + ACME + + + + + + + abc@def.ghi + + + + + + + + + + + + +`) + }) + }) + describe('getISODuration', () => { it('keeps a partial weekly period', () => { expect( diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts index 96f9d14049..34a80e1b80 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts @@ -238,7 +238,7 @@ export function getISODuration(updateFrequency: UpdateFrequencyCustom): string { return `P${duration.years}Y${duration.months}M${duration.days}D${hours}` } -export function appendResponsibleParty(contact: Individual) { +function appendResponsibleParty(contact: Individual) { const fullName = namePartsToFull(contact.firstName, contact.lastName) const createAddress = pipe( @@ -274,7 +274,7 @@ export function appendResponsibleParty(contact: Individual) { ) : noop, appendChildren(createAddress), - 'website' in contact.organization + contact.organization?.website ? appendChildren( pipe( createElement('gmd:onlineResource'), @@ -304,11 +304,16 @@ export function appendResponsibleParty(contact: Individual) { ) ) : noop, + + contact.organization?.name + ? appendChildren( + pipe( + createElement('gmd:organisationName'), + writeCharacterString(contact.organization.name) + ) + ) + : noop, appendChildren( - pipe( - createElement('gmd:organisationName'), - writeCharacterString(contact.organization.name) - ), createContact, pipe( createElement('gmd:role'), From 4219ecc3d3b2faf72d9d1c3fa4962e8cd718b0b5 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 24 Sep 2024 23:53:03 +0200 Subject: [PATCH 36/49] chore: fix tests --- .../metadata-converter/src/lib/iso19115-3/write-parts.spec.ts | 4 ++-- .../metadata-converter/src/lib/iso19139/write-parts.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts index 7bd1e6633e..0a92d0800c 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.spec.ts @@ -34,7 +34,7 @@ describe('write parts', () => { it('writes the record dates', () => { const modified = { ...datasetRecord, - resourcePublished: new Date('2024-01-01T00:00:00Z'), + resourcePublished: new Date('2024-01-01T00:00:00'), } writeRecordCreated(modified, rootEl) writeResourceCreated(modified, rootEl) @@ -78,7 +78,7 @@ describe('write parts', () => { - 2024-01-01T01:00:00 + 2024-01-01T00:00:00 publication diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts index 7c038e5df4..820c0edb5a 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts @@ -37,7 +37,7 @@ describe('write parts', () => { it('writes the record dates', () => { const modified = { ...datasetRecord, - resourcePublished: new Date('2024-01-01T00:00:00Z'), + resourcePublished: new Date('2024-01-01T00:00:00'), } writeResourceCreated(modified, rootEl) writeResourceUpdated(modified, rootEl) @@ -70,7 +70,7 @@ describe('write parts', () => { - 2024-01-01T01:00:00 + 2024-01-01T00:00:00 From 54cc28795d40de317486150dc6dc924be928200f Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 25 Sep 2024 10:33:29 +0200 Subject: [PATCH 37/49] test(search): add and adapt tests --- .../src/e2e/dashboard.cy.ts | 82 +++++++++++++------ .../dashboard-menu.component.spec.ts | 13 +-- .../search-header.component.html | 2 - 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index f6f6c161c5..ad270663d4 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -213,35 +213,17 @@ describe('dashboard', () => { cy.clearRecordDrafts() }) }) + describe('navigation', () => { - function checkDashboardFiltered() { - cy.get('gn-ui-autocomplete').type('Mat') - cy.get('mat-option').first().click() - cy.get('gn-ui-interactive-table') - .find('[data-cy="table-row"]') - .should('have.length', '1') - } beforeEach(() => { cy.login('admin', 'admin', false) cy.visit('/catalog/search') }) - describe('search input', () => { - it('should filter the dashboard based on the search input', () => { - checkDashboardFiltered() - }) - it('should navigate to list of all records and filter the dashboard based on the search input when on different page', () => { - cy.visit('/my-space/my-records') - checkDashboardFiltered() - }) - it('should clear the search input when navigating to my records', () => { - cy.get('gn-ui-autocomplete').type('Mat') - cy.get('md-editor-dashboard-menu').find('a').eq(5).click() - cy.get('gn-ui-autocomplete').should('have.value', '') - }) - it('should clear the search input when navigating to my drafts', () => { - cy.get('gn-ui-autocomplete').type('Mat') - cy.get('md-editor-dashboard-menu').find('a').eq(6).click() - cy.get('gn-ui-autocomplete').should('have.value', '') + describe('all records', () => { + it('should display the correct amount of records', () => { + cy.get('gn-ui-results-table') + .find('[data-cy="table-row"]') + .should('have.length', '15') }) }) describe('my records', () => { @@ -280,4 +262,56 @@ describe('dashboard', () => { }) }) }) + + describe('search', () => { + function checkDashboardFiltered() { + cy.get('gn-ui-autocomplete').type('velo{enter}') + cy.get('gn-ui-interactive-table') + .find('[data-cy="table-row"]') + .should('have.length', '1') + } + function checkAutocompleteSelected() { + cy.get('gn-ui-autocomplete').type('velo') + cy.get('mat-option').first().click() + cy.url().should('include', '/edit/accroche_velos') + } + describe('allRecords search input', () => { + beforeEach(() => { + cy.login('admin', 'admin', false) + cy.visit('/catalog/search') + }) + it('should filter the dashboard based on the search input', () => { + checkDashboardFiltered() + }) + it('should navigate to the record selected in the autocomplete', () => { + checkAutocompleteSelected() + }) + it('should clear the search input when navigating to my records', () => { + cy.get('gn-ui-autocomplete').type('velo') + cy.get('md-editor-dashboard-menu').find('a').eq(5).click() + cy.get('gn-ui-autocomplete').should('have.value', '') + }) + it('should hide the search input when navigating to my drafts', () => { + cy.get('md-editor-dashboard-menu').find('a').eq(6).click() + cy.get('gn-ui-autocomplete').should('not.exist') + }) + }) + describe('myRecords search input', () => { + beforeEach(() => { + cy.login('admin', 'admin', false) + cy.visit('/my-space/my-records') + }) + it('should filter the dashboard based on the search input', () => { + checkDashboardFiltered() + }) + it('should navigate to the record selected in the autocomplete', () => { + checkAutocompleteSelected() + }) + it('should clear the search input when navigating to all records', () => { + cy.get('gn-ui-autocomplete').type('velo') + cy.get('md-editor-dashboard-menu').find('a').first().click() + cy.get('gn-ui-autocomplete').should('have.value', '') + }) + }) + }) }) diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.spec.ts b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.spec.ts index 78f4e757ec..f6160ffb1a 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.spec.ts @@ -5,13 +5,11 @@ import { TranslateModule } from '@ngx-translate/core' import { cold, hot } from 'jasmine-marbles' import { MockBuilder, MockProviders } from 'ng-mocks' import { DashboardMenuComponent } from './dashboard-menu.component' -import { SearchFacade } from '@geonetwork-ui/feature/search' describe('DashboardMenuComponent', () => { let component: DashboardMenuComponent let fixture: ComponentFixture let recordsRepository: RecordsRepositoryInterface - let searchFacade: SearchFacade beforeEach(() => { return MockBuilder(DashboardMenuComponent) @@ -20,12 +18,9 @@ describe('DashboardMenuComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DashboardMenuComponent, TranslateModule.forRoot()], - providers: [ - MockProviders(ActivatedRoute, RecordsRepositoryInterface, SearchFacade), - ], + providers: [MockProviders(ActivatedRoute, RecordsRepositoryInterface)], }).compileComponents() recordsRepository = TestBed.inject(RecordsRepositoryInterface) - searchFacade = TestBed.inject(SearchFacade) fixture = TestBed.createComponent(DashboardMenuComponent) component = fixture.componentInstance fixture.detectChanges() @@ -50,10 +45,4 @@ describe('DashboardMenuComponent', () => { // Assert that draftsCount$ behaves as expected expect(component.draftsCount$).toBeObservable(expected) }) - - it('should reset filters in main search', () => { - searchFacade.setFilters = jest.fn() - component.resetMainSearch() - expect(searchFacade.setFilters).toHaveBeenCalledWith({}) - }) }) diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html index 451767f83c..e4627b2381 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.html @@ -1,7 +1,5 @@
- From a363ffac777701f6d41abb8f10e585b1d1511e81 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 25 Sep 2024 15:57:08 +0200 Subject: [PATCH 38/49] fix(search): address review comments --- .../dashboard/dashboard-page.component.html | 5 +- .../all-records/all-records.component.css | 4 + .../all-records/all-records.component.html | 133 +++++++++--------- .../all-records/all-records.component.ts | 2 - .../records/my-draft/my-draft.component.css | 4 + .../records/my-draft/my-draft.component.html | 42 +++--- .../records/my-draft/my-draft.component.ts | 2 - .../my-records/my-records.component.css | 4 + .../my-records/my-records.component.html | 129 +++++++++-------- .../my-records/my-records.component.spec.ts | 7 + .../my-records/my-records.component.ts | 9 +- .../app/records/records-list.component.html | 3 +- 12 files changed, 180 insertions(+), 164 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-page.component.html b/apps/metadata-editor/src/app/dashboard/dashboard-page.component.html index d38dc02b86..b4d92652e5 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-page.component.html +++ b/apps/metadata-editor/src/app/dashboard/dashboard-page.component.html @@ -3,7 +3,10 @@ -
+
+
+ +
diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.css b/apps/metadata-editor/src/app/records/all-records/all-records.component.css index e69de29bb2..157df3d655 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.css +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.css @@ -0,0 +1,4 @@ +/* set max-height for overflow-y-auto */ +main { + max-height: calc(100vh - 60px); +} diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.html b/apps/metadata-editor/src/app/records/all-records/all-records.component.html index 03601657c8..303d0280f8 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.html +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.html @@ -1,79 +1,72 @@
-
-
- +
+
+ +

+ dashboard.records.search +

+
+ +
+
+ +

+ dashboard.records.all +

+
+ +
+
-
-
- -

- dashboard.records.search -

-
- -
-
- -

- dashboard.records.all -

-
- -
-
+
+
+ dashboard.results.listMetadata
-
+ dashboard.results.listResources +
+
+ -
- dashboard.results.listMetadata -
-
- dashboard.results.listResources -
-
- dashboard.importRecord + keyboard_arrow_down - dashboard.importRecord - keyboard_arrow_down - keyboard_arrow_up - - - - - keyboard_arrow_up - edit_document - dashboard.createRecord - -
+ + + + + + edit_document + dashboard.createRecord + +
- -
-
+ + diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts index 8ff48433ad..46bfbfed2e 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts @@ -30,7 +30,6 @@ import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' import { RecordsListComponent } from '../records-list.component' import { map } from 'rxjs/operators' import { SearchHeaderComponent } from '../../dashboard/search-header/search-header.component' -import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifications' @Component({ selector: 'md-editor-all-records', @@ -50,7 +49,6 @@ import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifica CdkConnectedOverlay, RecordsListComponent, SearchHeaderComponent, - NotificationsContainerComponent, ], }) export class AllRecordsComponent { diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.css b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.css index e69de29bb2..157df3d655 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.css +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.css @@ -0,0 +1,4 @@ +/* set max-height for overflow-y-auto */ +main { + max-height: calc(100vh - 60px); +} diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html index 21f0adc770..f65d41a994 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html @@ -1,25 +1,21 @@ -
-
- +
+
+
+

+ dashboard.records.myDraft +

-
-
-

- dashboard.records.myDraft -

-
-
- -
-
-
+
+ +
+ diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts index 0c56c0f624..88dfa0005e 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts @@ -12,7 +12,6 @@ import { TranslateModule } from '@ngx-translate/core' import { startWith, switchMap } from 'rxjs' import { RecordsCountComponent } from '../records-count/records-count.component' import { RecordsListComponent } from '../records-list.component' -import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifications' @Component({ selector: 'md-editor-my-my-draft', templateUrl: './my-draft.component.html', @@ -28,7 +27,6 @@ import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifica ResultsTableContainerComponent, UiElementsModule, ResultsTableComponent, - NotificationsContainerComponent, ], }) export class MyDraftComponent { diff --git a/apps/metadata-editor/src/app/records/my-records/my-records.component.css b/apps/metadata-editor/src/app/records/my-records/my-records.component.css index e69de29bb2..157df3d655 100644 --- a/apps/metadata-editor/src/app/records/my-records/my-records.component.css +++ b/apps/metadata-editor/src/app/records/my-records/my-records.component.css @@ -0,0 +1,4 @@ +/* set max-height for overflow-y-auto */ +main { + max-height: calc(100vh - 60px); +} 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 74cb62ec02..8b883d9a0d 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,71 +1,76 @@
-
-
- -
-
-
- -

- dashboard.records.myRecords -

-
- -
-
-
-
-
- dashboard.myRecords.publishedMetadatas +
+
+ +

+ dashboard.records.search +

+
+
-
- dashboard.myRecords.currentlyEdited + + +

+ dashboard.records.myRecords +

+
+
-
- +
+
+
+ dashboard.myRecords.publishedMetadatas - dashboard.importRecord - keyboard_arrow_down - keyboard_arrow_up - - - - - +
+ dashboard.myRecords.currentlyEdited - edit_document - dashboard.createRecord -
+
+ + dashboard.importRecord + keyboard_arrow_down + keyboard_arrow_up + + + + + + edit_document + dashboard.createRecord + +
- -
-
+ +
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 86a6db86fd..8bb1b4deef 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 @@ -112,5 +112,12 @@ describe('MyRecordsComponent', () => { owner: user.id, }) }) + + it('should map search filters to searchText$', (done) => { + component.searchText$.subscribe((text) => { + expect(text).toBe('hello world') + done() + }) + }) }) }) 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 8f6be35987..72bc39f756 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 @@ -26,7 +26,7 @@ import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { MatIconModule } from '@angular/material/icon' import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' import { SearchHeaderComponent } from '../../dashboard/search-header/search-header.component' -import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifications' +import { map, Observable } from 'rxjs' @Component({ selector: 'md-editor-my-records', @@ -45,7 +45,6 @@ import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifica ImportRecordComponent, FeatureSearchModule, SearchHeaderComponent, - NotificationsContainerComponent, ], }) export class MyRecordsComponent implements OnInit { @@ -54,6 +53,8 @@ export class MyRecordsComponent implements OnInit { @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef + searchText$: Observable + isImportMenuOpen = false constructor( @@ -76,6 +77,10 @@ export class MyRecordsComponent implements OnInit { this.searchFacade.updateFilters(filters) }) }) + + this.searchText$ = this.searchFacade.searchFilters$.pipe( + map((filters) => ('any' in filters ? (filters['any'] as string) : null)) + ) } createRecord() { 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 3a9a806e9b..215e2e950f 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.html +++ b/apps/metadata-editor/src/app/records/records-list.component.html @@ -1,8 +1,7 @@
From df28ed3d7c03df74ea480b93180a58bc37ce7d09 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 25 Sep 2024 17:14:36 +0200 Subject: [PATCH 39/49] fix(me): correct typos and grammatical errors in the French translation file --- translations/fr.json | 134 +++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/translations/fr.json b/translations/fr.json index 9062405511..9cd20a7b16 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -18,7 +18,7 @@ "chart.type.line": "ligne", "chart.type.lineSmooth": "ligne lisse", "chart.type.pie": "camembert", - "dashboard.catalog.allRecords": "Fiches de métadonnée", + "dashboard.catalog.allRecords": "Fiches de métadonnées", "dashboard.catalog.calendar": "Calendrier", "dashboard.catalog.contacts": "Annuaire", "dashboard.catalog.discussion": "Discussions", @@ -27,7 +27,7 @@ "dashboard.importRecord": "Importer", "dashboard.importRecord.importExternal": "Importer une fiche externe", "dashboard.importRecord.importExternalLabel": "URL de la fiche externe", - "dashboard.importRecord.useModel": "Utiliser un modele", + "dashboard.importRecord.useModel": "Utiliser un modèle", "dashboard.labels.catalog": "Catalogue", "dashboard.labels.mySpace": "Mon espace", "dashboard.records.all": "Catalogue", @@ -46,22 +46,22 @@ "dashboard.results.listResources": "Afficher les ressources", "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", - "datafeeder.analysisProgressBar.subtitle": "L'analyse peut prendre plusieurs minutes, merci d'attendre.", + "datafeeder.analysisProgressBar.illustration.samplingData": "Échantillonnage \n des données", + "datafeeder.analysisProgressBar.subtitle": "L'analyse peut prendre plusieurs minutes, merci de patienter.", "datafeeder.analysisProgressBar.title": "Analyse en cours", "datafeeder.datasetValidation.datasetInformation": "Le jeu de données fourni contient {number} entités", "datafeeder.datasetValidation.submitButton": "OK, mes données sont correctes", "datafeeder.datasetValidation.title": "Vérifiez que vos données sont correctes", "datafeeder.datasetValidation.unknown": " - ", - "datafeeder.datasetValidationCsv.explicitLineNumbers": "*Le tableau doit afficher les 5 premières lignes (hors en-tête)
Si ce n'est pas le cas, vérifier que le fichier est bien formatté", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "*Le tableau doit afficher les 5 premières lignes (hors en-tête)
Si ce n'est pas le cas, vérifiez que le fichier est bien formaté", "datafeeder.datasetValidationCsv.lineNumbers": "Résumé des 5 premières lignes* du CSV :", "datafeeder.form.abstract": "Comment décrire votre jeu de données ?", "datafeeder.form.datepicker": "Savez-vous quand la donnée a été créée ?", "datafeeder.form.description": "Enfin, décrivez le processus utilisé pour créer la donnée", "datafeeder.form.dropdown": "Et pour quelle échelle ?", - "datafeeder.form.tags": "Choisissez un ou plusieurs mot-clés correspondant à vos données", + "datafeeder.form.tags": "Choisissez un ou plusieurs mots-clés correspondant à vos données", "datafeeder.form.title": "Donnez un titre à votre jeu de données", - "datafeeder.formsPage.title": "Dites nous en plus sur ces données", + "datafeeder.formsPage.title": "Dites-nous en plus sur ces données", "datafeeder.month.april": "Avril", "datafeeder.month.august": "Août", "datafeeder.month.december": "Décembre", @@ -74,19 +74,19 @@ "datafeeder.month.november": "Novembre", "datafeeder.month.october": "Octobre", "datafeeder.month.september": "Septembre", - "datafeeder.publish.hint": "Vous pouvez quitter cette page en sécurité, vous serez prévenus quand le processus sera terminé", + "datafeeder.publish.hint": "Vous pouvez quitter cette page en toute sécurité, vous serez prévenus quand le processus sera terminé", "datafeeder.publish.illustration.title": "Une autre donnée \n à publier ?", "datafeeder.publish.subtitle": "La publication peut prendre plusieurs minutes.", "datafeeder.publish.title": "Merci! \n Vos données sont en cours de publication", - "datafeeder.publish.upload": "Upload maintenant", - "datafeeder.publishSuccess.geonetworkRecord": "Fiche de métadonnée", + "datafeeder.publish.upload": "Télécharger maintenant", + "datafeeder.publishSuccess.geonetworkRecord": "Fiche de métadonnées", "datafeeder.publishSuccess.illustration.title": "Terminé, tout s'est bien passé !", "datafeeder.publishSuccess.mapViewer": "Visualiseur", "datafeeder.publishSuccess.ogcFeature": "OGC API", "datafeeder.publishSuccess.subtitle": "Visualisez vos données :", "datafeeder.publishSuccess.title": "Félicitations! \n Vos données ont été publiées", "datafeeder.publishSuccess.uploadAnotherData": "Importer une autre donnée", - "datafeeder.summarizePage.illustration": "pas d'erreur ? c'est parti !", + "datafeeder.summarizePage.illustration": "Pas d'erreur ? C'est parti !", "datafeeder.summarizePage.previous": "Précédent", "datafeeder.summarizePage.submit": "Publier", "datafeeder.summarizePage.title": "On y est presque...", @@ -120,10 +120,10 @@ "datafeeder.validation.csv.quote.none": "Aucun", "datafeeder.validation.csv.quote.simple": "Simple guillemet", "datafeeder.validation.csv.quoteChar": "Séparateur de texte", - "datafeeder.validation.encoding": "encodage", + "datafeeder.validation.encoding": "Encodage", "datafeeder.validation.extent.title": "Voici l'emprise du jeu de données", "datafeeder.validation.extent.title.unknown": "Le système de projection est inconnu", - "datafeeder.validation.projection": "Projection:", + "datafeeder.validation.projection": "Projection :", "datafeeder.validation.projection.unknown": "Choisissez une projection", "datafeeder.validation.sample.title": "Et un exemple d'objet", "datafeeder.wizard.emptyRequiredValuesMessage": "Veuillez remplir les champs obligatoires", @@ -190,11 +190,11 @@ "downloads.format.unknown": "inconnu", "downloads.wfs.featuretype.not.found": "La couche n'a pas été retrouvée", "dropFile": "Faites glisser votre fichier", - "editor.form.keywords.placeholder": "Sélectionner un mot-clé", + "editor.form.keywords.placeholder": "Sélectionnez un mot-clé", "editor.form.placeKeywordWithoutExtent": "Ce mot-clé n'a pas de localisation géographique associée", "editor.record.delete.confirmation.cancelText": "Annuler", "editor.record.delete.confirmation.confirmText": "Supprimer", - "editor.record.delete.confirmation.message": "Etes-vous sûr de vouloir supprimer cette fiche ?", + "editor.record.delete.confirmation.message": "Êtes-vous sûr de vouloir supprimer cette fiche ?", "editor.record.delete.confirmation.title": "Supprimer la fiche ?", "editor.record.deleteError.body": "La fiche n'a pas pu être supprimée :", "editor.record.deleteError.closeMessage": "Compris", @@ -234,36 +234,36 @@ "editor.record.form.license.odbl": "", "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", - "editor.record.form.license.unknown": "Non-reconnue ou absente", - "editor.record.form.page.accessAndContact": "Acces et contact", + "editor.record.form.license.unknown": "Non reconnue ou absente", + "editor.record.form.page.accessAndContact": "Accès et contact", "editor.record.form.page.description": "Description de la ressource", "editor.record.form.page.ressources": "Ressources", "editor.record.form.section.about.description": "Ces informations concernent la donnée.", - "editor.record.form.section.about.label": "A propos de la ressource", + "editor.record.form.section.about.label": "À propos de la ressource", "editor.record.form.section.annexes.label": "Annexes", - "editor.record.form.section.associatedResources.description": "Déposez les jeux de données associées à cette fiche de métadonnée.", - "editor.record.form.section.associatedResources.label": "Ressources associees", - "editor.record.form.section.classification.description": "La classification a un impact sur la recherche du jeux de données.", + "editor.record.form.section.associatedResources.description": "Déposez les jeux de données associées à cette fiche de métadonnées.", + "editor.record.form.section.associatedResources.label": "Ressources associées", + "editor.record.form.section.classification.description": "La classification a un impact sur la recherche du jeu de données.", "editor.record.form.section.classification.label": "Classification", "editor.record.form.section.dataManagers.description": "Cette information concerne la donnée.", - "editor.record.form.section.dataManagers.label": "Responsables de la donnee", - "editor.record.form.section.dataPointOfContact.description": "Cette information concerne la fiche de métadonnée.", - "editor.record.form.section.dataPointOfContact.label": "Point de contact de la metadonee", - "editor.record.form.section.geographicalCoverage.label": "Couverture geographique", - "editor.record.form.section.useAndAccessConditions.label": "Conditions d'acces et usage", + "editor.record.form.section.dataManagers.label": "Responsables de la donnée", + "editor.record.form.section.dataPointOfContact.description": "Cette information concerne la fiche de métadonnées.", + "editor.record.form.section.dataPointOfContact.label": "Point de contact de la métadonnée", + "editor.record.form.section.geographicalCoverage.label": "Couverture géographique", + "editor.record.form.section.useAndAccessConditions.label": "Conditions d'accès et usage", "editor.record.form.temporalExtents.addDate": "Date déterminée", "editor.record.form.temporalExtents.addRange": "Période de temps", "editor.record.form.temporalExtents.date": "Date concernée", "editor.record.form.temporalExtents.range": "Période concernée", - "editor.record.form.updateFrequency.planned": "Ces données doivent être mise à jour régulièrement.", - "editor.record.importFromExternalFile.failure.body": "Une erreur est survenue pendant l'import de la fiche: ", + "editor.record.form.updateFrequency.planned": "Ces données doivent être mises à jour régulièrement.", + "editor.record.importFromExternalFile.failure.body": "Une erreur est survenue pendant l'import de la fiche : ", "editor.record.importFromExternalFile.failure.title": "Erreur", - "editor.record.importFromExternalFile.success.body": "L'import de la fiche de metadonnée à été realisée avec succès.", - "editor.record.importFromExternalFile.success.title": "Import reussi", + "editor.record.importFromExternalFile.success.body": "L'import de la fiche de métadonnées a été réalisé avec succès.", + "editor.record.importFromExternalFile.success.title": "Import réussi", "editor.record.loadError.body": "La fiche n'a pas pu être chargée :", "editor.record.loadError.closeMessage": "Compris", "editor.record.loadError.title": "Erreur lors du chargement", - "editor.record.onlineResourceError.body": "Une erreur est survenue lors de l'ajout de la resource:", + "editor.record.onlineResourceError.body": "Une erreur est survenue lors de l'ajout de la ressource :", "editor.record.onlineResourceError.closeMessage": "Compris", "editor.record.onlineResourceError.title": "Erreur lors de l'ajout d'une ressource", "editor.record.placeKeywordWithoutLabel": "Localisation sans nom", @@ -281,10 +281,10 @@ "editor.record.saveStatus.recordUpToDate": "La fiche publiée est à jour", "editor.record.undo.confirmation.cancelText": "Garder les modifications", "editor.record.undo.confirmation.confirmText": "Retirer les modifications", - "editor.record.undo.confirmation.message": "Etes-vous sûr de vouloir annuler les modifications apportées à cette fiche ?", + "editor.record.undo.confirmation.message": "Êtes-vous sûr de vouloir annuler les modifications apportées à cette fiche ?", "editor.record.undo.confirmation.title": "Annuler les modifications ?", "editor.record.undo.tooltip.disabled": "Il n'y a pas de modifications en cours sur cette fiche", - "editor.record.undo.tooltip.enabled": "Cliquer sur ce bouton pour annuler les modifications apportées à cette fiche", + "editor.record.undo.tooltip.enabled": "Cliquez sur ce bouton pour annuler les modifications apportées à cette fiche", "editor.record.upToDate": "", "editor.sidebar.menu.editor": "", "editor.temporary.disabled": "Pas encore implémenté", @@ -296,8 +296,8 @@ "facets.block.title.cl_spatialRepresentationType.key": "Représentation spatiale", "facets.block.title.cl_status.key": "Statut", "facets.block.title.creationYearForResource": "Année de création", - "facets.block.title.resolutionScaleDenominator": "Echelle", - "facets.block.title.tag": "Mots clés", + "facets.block.title.resolutionScaleDenominator": "Échelle", + "facets.block.title.tag": "Mots-clés", "facets.block.title.tag.default": "Tag", "facets.block.title.th_regions_tree.default": "Régions", "favorite.not.authenticated.tooltip": "
Connectez-vous pour avoir accès à cette fonctionnalité
", @@ -352,11 +352,11 @@ "map.wms.urlInput.hint": "Entrez l'URL du service WMS", "multiselect.filter.placeholder": "Rechercher", "nav.back": "Retour", - "next": "suivant", + "next": "Suivant", "ogc.unreachable.unknown": "Le service n'est pas accessible", "organisation.filter.placeholder": "Filtrer les résultats", "organisation.sort.sortBy": "Trier par :", - "organisations.hits.found": "{hits, plural, =0{Aucune organisation trouvé} one{1 organisation sur {total} affichée} other{{hits} organisations sur {total} affichées}}", + "organisations.hits.found": "{hits, plural, =0{Aucune organisation trouvée} one{1 organisation sur {total} affichée} other{{hits} organisations sur {total} affichées}}", "organisations.sortBy.nameAsc": "Nom A → Z", "organisations.sortBy.nameDesc": "Nom Z → A", "organisations.sortBy.recordCountAsc": "Données 0 → 9", @@ -366,9 +366,9 @@ "organization.details.mailContact": "Contacter par mail", "organization.header.recordCount": "{count, plural, =0{donnée} one{donnée} other{données}}", "pagination.nextPage": "Page suivante", - "pagination.page": "page", + "pagination.page": "Page", "pagination.pageOf": "sur", - "previous": "précédent", + "previous": "Précédent", "record.action.delete": "Supprimer", "record.action.download": "Télécharger", "record.action.duplicate": "Dupliquer", @@ -391,10 +391,10 @@ "record.metadata.catalog": "Catalogue", "record.metadata.contact": "Contact", "record.metadata.creation": "Date de création", - "record.metadata.details": "A propos de la donnée", + "record.metadata.details": "À propos de la donnée", "record.metadata.download": "Téléchargements", "record.metadata.formats": "Formats", - "record.metadata.keywords": "Mots clés", + "record.metadata.keywords": "Mots-clés", "record.metadata.languages": "Langues", "record.metadata.lastUpdate": "Mis à jour le {date}", "record.metadata.links": "Liens", @@ -406,30 +406,30 @@ "record.metadata.publication": "Date de publication", "record.metadata.publications": "{count, plural, =0{donnée} one{donnée} other{données}}", "record.metadata.quality": "Qualité des métadonnées", - "record.metadata.quality.contact.failed": "Contact n'est pas renseigné", - "record.metadata.quality.contact.success": "Contact est renseigné", - "record.metadata.quality.description.failed": "Description n'est pas renseignée", - "record.metadata.quality.description.success": "Description est renseignée", + "record.metadata.quality.contact.failed": "Le contact n'est pas renseigné", + "record.metadata.quality.contact.success": "Le contact est renseigné", + "record.metadata.quality.description.failed": "La description n'est pas renseignée", + "record.metadata.quality.description.success": "La description est renseignée", "record.metadata.quality.details": "Détails", - "record.metadata.quality.keywords.failed": "Mots clés ne sont pas renseignés", - "record.metadata.quality.keywords.success": "Mots clés sont renseignés", - "record.metadata.quality.legalConstraints.failed": "Contraintes légales ne sont pas renseignées", - "record.metadata.quality.legalConstraints.success": "Contraintes légales sont renseignées", - "record.metadata.quality.organisation.failed": "Producteur n'est pas renseigné", - "record.metadata.quality.organisation.success": "Producteur est renseigné", - "record.metadata.quality.title.failed": "Titre n'est pas renseigné", - "record.metadata.quality.title.success": "Titre est renseigné", - "record.metadata.quality.topic.failed": "Thème n'est pas renseigné", - "record.metadata.quality.topic.success": "Thème est renseigné", - "record.metadata.quality.updateFrequency.failed": "Fréquence de mise à jour n'est pas renseignée", - "record.metadata.quality.updateFrequency.success": "Fréquence de mise à jour est renseignée", + "record.metadata.quality.keywords.failed": "Les mots-clés ne sont pas renseignés", + "record.metadata.quality.keywords.success": "Les mots-clés sont renseignés", + "record.metadata.quality.legalConstraints.failed": "Les contraintes légales ne sont pas renseignées", + "record.metadata.quality.legalConstraints.success": "Les contraintes légales sont renseignées", + "record.metadata.quality.organisation.failed": "Le producteur n'est pas renseigné", + "record.metadata.quality.organisation.success": "Le producteur est renseigné", + "record.metadata.quality.title.failed": "Le titre n'est pas renseigné", + "record.metadata.quality.title.success": "Le titre est renseigné", + "record.metadata.quality.topic.failed": "Le thème n'est pas renseigné", + "record.metadata.quality.topic.success": "Le thème est renseigné", + "record.metadata.quality.updateFrequency.failed": "La fréquence de mise à jour n'est pas renseignée", + "record.metadata.quality.updateFrequency.success": "La fréquence de mise à jour est renseignée", "record.metadata.related": "Voir aussi", "record.metadata.sheet": "Fiche de métadonnées d'origine", "record.metadata.status": "Statut", "record.metadata.status.notPublished": "Non publié", "record.metadata.status.published": "Publié", "record.metadata.technical": "Informations techniques", - "record.metadata.temporalExtent": "Etendue temporelle", + "record.metadata.temporalExtent": "Étendue temporelle", "record.metadata.temporalExtent.fromDateToDate": "Du { start } au { end }", "record.metadata.temporalExtent.sinceDate": "Depuis le { start }", "record.metadata.temporalExtent.untilDate": "Jusqu'au { end }", @@ -444,7 +444,7 @@ "record.metadata.userFeedbacks.anonymousUser": "Pour rédiger un commentaire, veuillez vous identifier.", "record.metadata.userFeedbacks.newAnswer.buttonTitle": "Publier", "record.metadata.userFeedbacks.newAnswer.placeholder": "Répondre...", - "record.metadata.userFeedbacks.newComment.placeholder": "Rédiger votre commentaire ici...", + "record.metadata.userFeedbacks.newComment.placeholder": "Rédigez votre commentaire ici...", "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "Les plus récents en premier", "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "Les plus anciens en premier", "record.metadata.userFeedbacks.sortSelector.label": "Trier par ...", @@ -453,12 +453,12 @@ "record.tab.data": "Tableau", "record.tab.map": "Carte", "record.was.created.time": "a créé ce jeu de données {time}", - "records": "enregistrements", + "records": "Enregistrements", "results.layout.selectOne": "Affichage des résultats", "results.records.hits.displayedOn": "{displayed, plural, =0{Aucun enregistrement} one{1 enregistrement affiché} other{{displayed} enregistrements affichés}} {hits, plural, other{sur {hits} au total.}}", - "results.records.hits.empty.help.html": "Suggestions :
  • Essayez d'autres mots clés
  • Cherchez moins de mots
", + "results.records.hits.empty.help.html": "Suggestions :
  • Essayez d'autres mots-clés
  • Cherchez moins de mots
", "results.records.hits.found": "{hits, plural, =0{Aucune correspondance.} one{1 enregistrement trouvé.} other{{hits} résultats.}}", - "results.records.hits.selected": "{amount, plural, one{1 selectionnée} other{{ amount } sélectionnées}}", + "results.records.hits.selected": "{amount, plural, one{1 sélectionnée} other{{ amount } sélectionnées}}", "results.showMore": "Plus de résultats...", "results.sortBy.dateStamp": "Plus récent", "results.sortBy.popularity": "Popularité", @@ -469,7 +469,7 @@ "search.error.organizationHasNoDataset": "Cette organisation n'a pas encore de données.", "search.error.organizationNotFound": "L'organisation n'a pas pu être trouvée.", "search.error.receivedError": "Erreur retournée", - "search.error.recordHasnolink": "Ce dataset n'a pas encore de lien, réessayez plus tard s'il vous plaît.", + "search.error.recordHasnolink": "Ce jeu de données n'a pas encore de lien, réessayez plus tard s'il vous plaît.", "search.error.recordNotFound": "Cette donnée n'a pu être trouvée.", "search.field.any.placeholder": "Rechercher une donnée...", "search.field.sortBy": "Trier par :", @@ -478,8 +478,8 @@ "search.filters.format": "Formats", "search.filters.inspireKeyword": "Mot-clé INSPIRE", "search.filters.isSpatial": "Données spatiales", - "search.filters.isSpatial.no": "non-géolocalisées", - "search.filters.isSpatial.yes": "géolocalisées", + "search.filters.isSpatial.no": "Non-géolocalisées", + "search.filters.isSpatial.yes": "Géolocalisées", "search.filters.keyword": "Mot-clé", "search.filters.license": "Licence", "search.filters.license.cc-by": "cc-by", @@ -490,7 +490,7 @@ "search.filters.license.odbl": "odbl", "search.filters.license.odc-by": "odc-by", "search.filters.license.pddl": "pddl", - "search.filters.license.unknown": "Non-reconnue ou absente", + "search.filters.license.unknown": "Non reconnue ou absente", "search.filters.maximize": "Agrandir", "search.filters.minimize": "Réduire", "search.filters.myRecords": "Voir mes données", @@ -518,7 +518,7 @@ "tooltip.url.open": "Ouvrir l'URL", "ui.readLess": "Réduire", "ui.readMore": "Lire la suite", - "wfs.featuretype.notfound": "La classe d'objet n'a pas été trouvée dans le service", + "wfs.featuretype.notfound": "La classe d'objets n'a pas été trouvée dans le service", "wfs.geojsongml.notsupported": "Le service ne supporte pas le format GeoJSON ou GML", "wfs.unreachable.cors": "Le service n'est pas accessible en raison de limitations CORS", "wfs.unreachable.http": "Le service a retourné une erreur HTTP", From ac0f1dd4abe4f5ba8832d239494d2b669f802b34 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 25 Sep 2024 17:21:35 +0200 Subject: [PATCH 40/49] fix(badge): adapt dashboard badge font sizes and padding and revert generic badge font size and padding --- .../dashboard-menu/dashboard-menu.component.html | 10 +++++++--- .../src/lib/results-table/results-table.component.html | 9 +++++---- tailwind.base.css | 5 +++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html index 7ded10a5ee..425d9d2591 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html @@ -72,9 +72,13 @@ edit_note dashboard.records.myDraft - {{ - draftsCount - }} + {{ draftsCount }}
{{ item.title }} {{ formats[0] }} {{ formats[1] }} -
+
+{{ formats.slice(2).length }}
diff --git a/tailwind.base.css b/tailwind.base.css index de0836deef..210e076e71 100644 --- a/tailwind.base.css +++ b/tailwind.base.css @@ -132,12 +132,13 @@ /* BADGE CLASS */ .gn-ui-badge { --rounded: var(--gn-ui-badge-rounded, 0.25em); - --padding: var(--gn-ui-badge-padding, 0.375em 0.875em); + --padding: var(--gn-ui-badge-padding, 0.375em 0.75em); + --text-size: var(--gn-ui-badge-text-size, 0.875em); --text-color: var(--gn-ui-badge-text-color, var(--color-gray-50)); --background-color: var(--gn-ui-badge-background-color, black); --opacity: var(--gn-ui-badge-opacity, 0.7); @apply opacity-[--opacity] p-[--padding] rounded-[--rounded] - font-medium text-[length:0.875em] text-[color:--text-color] text-xs bg-[color:--background-color] flex justify-center items-center content-center; + font-medium text-[length:--text-size] text-[color:--text-color] bg-[color:--background-color] flex justify-center items-center content-center; } /* makes sure icons will not make the badges grow vertically; also make size proportional */ .gn-ui-badge mat-icon.mat-icon { From 90201e28fcb7bf38dc4885ab49070000fe5196a0 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 25 Sep 2024 17:36:22 +0200 Subject: [PATCH 41/49] fix: correct typos and grammatical errors in the English translation file --- translations/en.json | 68 ++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/translations/en.json b/translations/en.json index 57643c9fd1..3ec0245834 100644 --- a/translations/en.json +++ b/translations/en.json @@ -48,17 +48,17 @@ "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Gathering dataset \n information", "datafeeder.analysisProgressBar.illustration.samplingData": "Sampling \n data", "datafeeder.analysisProgressBar.subtitle": "The analysis may take several minutes, please wait.", - "datafeeder.analysisProgressBar.title": "Analyze in progress", + "datafeeder.analysisProgressBar.title": "Analysis in progress", "datafeeder.datasetValidation.datasetInformation": "The provided dataset contains {number} entities", - "datafeeder.datasetValidation.submitButton": "OK, my data are correct", - "datafeeder.datasetValidation.title": "Make sure your data are correct", + "datafeeder.datasetValidation.submitButton": "OK, my data is correct", + "datafeeder.datasetValidation.title": "Make sure your data is correct", "datafeeder.datasetValidation.unknown": " - ", "datafeeder.datasetValidationCsv.explicitLineNumbers": "*The table must display the first 5 lines (excluding the header)
If this is not the case, check that the file is correctly formatted", "datafeeder.datasetValidationCsv.lineNumbers": "Sample of the first 5 lines* of the dataset:", "datafeeder.form.abstract": "How would you describe your dataset?", "datafeeder.form.datepicker": "Do you know when the dataset was created?", - "datafeeder.form.description": "Finally, please describe the process that was used to create the dataset", - "datafeeder.form.dropdown": "For which scale was it created ?", + "datafeeder.form.description": "Finally, please describe the process used to create the dataset", + "datafeeder.form.dropdown": "For which scale was it created?", "datafeeder.form.tags": "Select one or more tags that fit your dataset", "datafeeder.form.title": "Give your dataset the best title", "datafeeder.formsPage.title": "Tell us more about your dataset", @@ -75,7 +75,7 @@ "datafeeder.month.october": "October", "datafeeder.month.september": "September", "datafeeder.publish.hint": "You can safely exit this page, you will be notified when the process is over", - "datafeeder.publish.illustration.title": "Another dataset \n to publish ?", + "datafeeder.publish.illustration.title": "Another dataset \n to publish?", "datafeeder.publish.subtitle": "Publishing may take several minutes.", "datafeeder.publish.title": "Thank you! \n Your dataset is being published", "datafeeder.publish.upload": "Upload it now", @@ -84,9 +84,9 @@ "datafeeder.publishSuccess.mapViewer": "Map viewer", "datafeeder.publishSuccess.ogcFeature": "OGC API", "datafeeder.publishSuccess.subtitle": "View your data in:", - "datafeeder.publishSuccess.title": "Congratulation! \n Your dataset has been published", + "datafeeder.publishSuccess.title": "Congratulations! \n Your dataset has been published", "datafeeder.publishSuccess.uploadAnotherData": "Upload another dataset", - "datafeeder.summarizePage.illustration": "no mistake? let's go!", + "datafeeder.summarizePage.illustration": "No mistake? Let's go!", "datafeeder.summarizePage.previous": "Previous", "datafeeder.summarizePage.submit": "Submit", "datafeeder.summarizePage.title": "You're almost there...", @@ -100,7 +100,7 @@ "datafeeder.upload.error.title.cantOpenFile": "Error while opening the file", "datafeeder.upload.error.title.fileFormat": "The selected file format is not supported", "datafeeder.upload.error.title.fileHasntSelected": "No file selected", - "datafeeder.upload.error.title.fileSize": "The file size is too big", + "datafeeder.upload.error.title.fileSize": "The file size is too large", "datafeeder.upload.error.title.noRightsToSendData": "You are not allowed to publish this dataset", "datafeeder.upload.illustration.enrichment": "Enrichment", "datafeeder.upload.illustration.import": "Import", @@ -118,15 +118,15 @@ "datafeeder.validation.csv.lng.field": "Longitude column", "datafeeder.validation.csv.quote.double": "Double quote", "datafeeder.validation.csv.quote.none": "None", - "datafeeder.validation.csv.quote.simple": "Simple quote", + "datafeeder.validation.csv.quote.simple": "Single quote", "datafeeder.validation.csv.quoteChar": "Quote separator", - "datafeeder.validation.encoding": "encoding", + "datafeeder.validation.encoding": "Encoding", "datafeeder.validation.extent.title": "Here is the dataset extent", "datafeeder.validation.extent.title.unknown": "The projection system is unknown", - "datafeeder.validation.projection": "spatial reference system:", + "datafeeder.validation.projection": "Spatial reference system:", "datafeeder.validation.projection.unknown": "Choose a spatial reference system", "datafeeder.validation.sample.title": "And a sample entity from the dataset", - "datafeeder.wizard.emptyRequiredValuesMessage": "Please fill mandatory fields", + "datafeeder.wizard.emptyRequiredValuesMessage": "Please fill in mandatory fields", "datafeeder.wizardSummarize.createdAt": "Created at", "datafeeder.wizardSummarize.scale": "Scale", "datahub.header.datasets": "Datasets", @@ -144,7 +144,7 @@ "datahub.record.addToFavorites": "Add to favorites", "datahub.search.back": "Back", "datahub.search.filter.all": "All", - "datahub.search.filter.generatedByWfs": "generated by an API", + "datahub.search.filter.generatedByWfs": "Generated by an API", "datahub.search.filter.others": "Others", "dataset.error.http": "The data could not be loaded because of an HTTP error: \"{ info }\"", "dataset.error.network": "The data could not be loaded because of a network error or CORS limitations: \"{ info }\"", @@ -174,7 +174,7 @@ "domain.contact.role.user": "User", "domain.record.status.completed": "Completed", "domain.record.status.deprecated": "Deprecated", - "domain.record.status.ongoing": "On going", + "domain.record.status.ongoing": "Ongoing", "domain.record.status.removed": "Removed", "domain.record.status.under_development": "Under development", "domain.record.updateFrequency.asNeeded": "As needed", @@ -187,9 +187,9 @@ "domain.record.updateFrequency.unknown": "Unknown", "domain.record.updateFrequency.week": "{count, plural, =0{0 times} one{Once} other{{count} times}} per week", "domain.record.updateFrequency.year": "{count, plural, =0{0 times} one{Once} other{{count} times}} per year", - "downloads.format.unknown": "unknown", + "downloads.format.unknown": "Unknown", "downloads.wfs.featuretype.not.found": "The layer was not found", - "dropFile": "drop file", + "dropFile": "Drop file", "editor.form.keywords.placeholder": "Select a keyword", "editor.form.placeKeywordWithoutExtent": "This keyword is not associated with a geographical extent", "editor.record.delete.confirmation.cancelText": "Cancel", @@ -199,7 +199,7 @@ "editor.record.deleteError.body": "The record could not be deleted:", "editor.record.deleteError.closeMessage": "Understood", "editor.record.deleteError.title": "Error deleting record", - "editor.record.deleteSuccess.body": "The record was successfully deleted !", + "editor.record.deleteSuccess.body": "The record was successfully deleted!", "editor.record.deleteSuccess.title": "Delete success", "editor.record.form.bottomButtons.comeBackLater": "Come back later", "editor.record.form.bottomButtons.next": "Next", @@ -258,12 +258,12 @@ "editor.record.form.updateFrequency.planned": "The data should be updated regularly.", "editor.record.importFromExternalFile.failure.body": "Failure", "editor.record.importFromExternalFile.failure.title": "The import of the record has failed: ", - "editor.record.importFromExternalFile.success.body": "Import succesful", - "editor.record.importFromExternalFile.success.title": "The record has been succefuly imported.", + "editor.record.importFromExternalFile.success.body": "Import successful", + "editor.record.importFromExternalFile.success.title": "The record has been successfully imported.", "editor.record.loadError.body": "The record could not be loaded:", "editor.record.loadError.closeMessage": "Understood", "editor.record.loadError.title": "Error loading record", - "editor.record.onlineResourceError.body": "An error happened while adding the resource:", + "editor.record.onlineResourceError.body": "An error occurred while adding the resource:", "editor.record.onlineResourceError.closeMessage": "Understood", "editor.record.onlineResourceError.title": "Error adding resource", "editor.record.placeKeywordWithoutLabel": "Unnamed location", @@ -283,7 +283,7 @@ "editor.record.undo.confirmation.confirmText": "Discard the changes", "editor.record.undo.confirmation.message": "Are you sure you want to cancel the pending changes on this record?", "editor.record.undo.confirmation.title": "Cancel changes?", - "editor.record.undo.tooltip.disabled": "There is no pending changes on this record", + "editor.record.undo.tooltip.disabled": "There are no pending changes on this record", "editor.record.undo.tooltip.enabled": "Clicking this button will cancel the pending changes on this record.", "editor.record.upToDate": "This record is up to date", "editor.sidebar.menu.editor": "Editor", @@ -352,7 +352,7 @@ "map.wms.urlInput.hint": "Enter WMS service URL", "multiselect.filter.placeholder": "Search", "nav.back": "Back", - "next": "next", + "next": "Next", "ogc.unreachable.unknown": "The service could not be reached", "organisation.filter.placeholder": "Filter results", "organisation.sort.sortBy": "Sort by:", @@ -366,9 +366,9 @@ "organization.details.mailContact": "Contact by email", "organization.header.recordCount": "{count, plural, =0{data} one{data} other{datas}}", "pagination.nextPage": "Next page", - "pagination.page": "page", + "pagination.page": "Page", "pagination.pageOf": "of", - "previous": "previous", + "previous": "Previous", "record.action.delete": "Delete", "record.action.download": "Download", "record.action.duplicate": "Duplicate", @@ -380,9 +380,9 @@ "record.metadata.api.form.closeForm": "Close the form", "record.metadata.api.form.create": "Create your request", "record.metadata.api.form.customUrl": "Custom URL", - "record.metadata.api.form.limit": "Count of records", + "record.metadata.api.form.limit": "Number of records", "record.metadata.api.form.limit.all": "All", - "record.metadata.api.form.offset": "Count of first record", + "record.metadata.api.form.offset": "Number of first record", "record.metadata.api.form.openForm": "Open the form", "record.metadata.api.form.reset": "Reset", "record.metadata.api.form.title": "Generate a custom URL", @@ -467,7 +467,7 @@ "search.autocomplete.error": "Suggestions could not be fetched:", "search.error.couldNotReachApi": "The API could not be reached", "search.error.organizationHasNoDataset": "This organization has no dataset yet.", - "search.error.organizationNotFound": "This organization could not be found.", + "search.error.organizationNotFound": "This organization could not be found.", "search.error.receivedError": "An error was received", "search.error.recordHasnolink": "This record currently has no link yet, please come back later.", "search.error.recordNotFound": "The record with identifier \"{ id }\" could not be found.", @@ -478,8 +478,8 @@ "search.filters.format": "Formats", "search.filters.inspireKeyword": "INSPIRE keyword", "search.filters.isSpatial": "Is spatial data", - "search.filters.isSpatial.no": "non spatial", - "search.filters.isSpatial.yes": "spatial", + "search.filters.isSpatial.no": "Non-spatial", + "search.filters.isSpatial.yes": "Spatial", "search.filters.keyword": "Keyword", "search.filters.license": "License", "search.filters.license.cc-by": "Creative Commons CC-BY", @@ -494,7 +494,7 @@ "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.myRecordsHelp": "When this is enabled, only records created by me are shown; records created by others will not appear.", "search.filters.organization": "Organization", "search.filters.otherRecords": "Showing records from another person", "search.filters.producerOrg": "Producer", @@ -506,11 +506,11 @@ "search.filters.title": "Filter your results", "search.filters.topic": "Topics", "search.filters.useSpatialFilter": "Show records in the area of interest first", - "search.filters.useSpatialFilterHelp": "When this is enabled, records situated in the catalog's area of interest are shown first; records outside of this area will not show up.", + "search.filters.useSpatialFilterHelp": "When this is enabled, records within the catalog's area of interest are shown first; records outside of this area will not appear.", "share.tab.permalink": "Share", "share.tab.webComponent": "Integrate", "table.loading.data": "Loading data...", - "table.object.count": "objects in this dataset", + "table.object.count": "Objects in this dataset", "table.select.data": "Data source", "tooltip.html.copy": "Copy HTML", "tooltip.id.copy": "Copy unique identifier", @@ -520,7 +520,7 @@ "ui.readMore": "Read more", "wfs.featuretype.notfound": "No matching feature type was found in the service", "wfs.geojsongml.notsupported": "This service does not support the GeoJSON or GML format", - "wfs.unreachable.cors": "The service could not be reached because of CORS limitations", + "wfs.unreachable.cors": "The service could not be reached due to CORS limitations", "wfs.unreachable.http": "The service returned an HTTP error", "wfs.unreachable.unknown": "The service could not be reached" } From 0f4306dbf088b203445252eb8e0e6cd0e1241d81 Mon Sep 17 00:00:00 2001 From: Jody Garnett Date: Wed, 25 Sep 2024 23:20:48 -0700 Subject: [PATCH 42/49] Update communication options for geonetwork-ui --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bafecb52d..101c4c0fe8 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,14 @@ # GeoNetwork UI -GeoNetwork UI is a suite of Applications made to provide a modern facade to your GeoNetwork 4 catalog. +GeoNetwork UI suite of applications provide a modern look for your GeoNetwork catalog. It also provides Web Components to embed various parts of your data catalog in third party websites. +End users are [invited](https://discourse.osgeo.org/t/about-the-geonetwork-ui-category/59280) to join us on the [geonetwork-ui](https://discourse.osgeo.org/c/geonetwork/ui/59) forum. + +Developers should check out the [discussions](https://github.com/geonetwork/geonetwork-ui/discussions). + ## Documentation To check out docs, visit [geonetwork-ui website](https://geonetwork.github.io/geonetwork-ui/main/docs/) From 31aad0f0341e52010a12ea1e343d79ff7fff68a7 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 26 Sep 2024 09:03:20 +0200 Subject: [PATCH 43/49] chore(all/myrecords): grey out unimplemented buttons --- .../records/all-records/all-records.component.html | 14 ++++++++++++-- .../records/my-records/my-records.component.html | 10 ++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.html b/apps/metadata-editor/src/app/records/all-records/all-records.component.html index 303d0280f8..b63f271f04 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.html +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.html @@ -28,10 +28,20 @@

class="flex flex-row items-center mx-[32px] my-[16px] py-[8px] gap-[16px]" >
- dashboard.results.listMetadata + dashboard.results.listMetadata
- dashboard.results.listResources + dashboard.results.listResources
class="flex flex-row items-center mx-[32px] my-[16px] py-[8px] gap-[16px]" >
- dashboard.myRecords.publishedMetadatas
- dashboard.myRecords.currentlyEdited
From 4c4597418057fae4843cd43f8442fdf84fd270a3 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 26 Sep 2024 09:33:12 +0200 Subject: [PATCH 44/49] docs: improve root README a bit more --- README.md | 113 +++++------------------------------------------------- 1 file changed, 9 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 101c4c0fe8..37793a7676 100644 --- a/README.md +++ b/README.md @@ -2,42 +2,15 @@ # GeoNetwork UI -GeoNetwork UI suite of applications provide a modern look for your GeoNetwork catalog. +GeoNetwork-UI provides several applications to improve the user experience of your GeoNetwork catalog. It also provides Web Components to embed various parts of your data catalog in third party websites. -End users are [invited](https://discourse.osgeo.org/t/about-the-geonetwork-ui-category/59280) to join us on the [geonetwork-ui](https://discourse.osgeo.org/c/geonetwork/ui/59) forum. +End users are [invited](https://discourse.osgeo.org/t/about-the-geonetwork-ui-category/59280) to join us on the GeoNetwork-UI forum on OsGeo Discourse: https://discourse.osgeo.org/c/geonetwork/ui -Developers should check out the [discussions](https://github.com/geonetwork/geonetwork-ui/discussions). +Developers should check out the [GitHub discussions](https://github.com/geonetwork/geonetwork-ui/discussions). -## Documentation - -To check out docs, visit [geonetwork-ui website](https://geonetwork.github.io/geonetwork-ui/main/docs/) - -## Requirements - -- GeoNetwork version 4.2.2 -- ElasticSearch version 7.11+ - -:warning: A bug currently in GeoNetwork 4.2.2 prevents the organizations of showing up correctly in the Datahub application. - -As a temporary workaround, the following change is necessary in GeoNetwork data directory: - -```diff -diff --git a/web/src/main/webResources/WEB-INF/data/config/index/records.json b/web/src/main/webResources/WEB-INF/data/config/index/records.json -index 1d7e499af7..78e682e3db 100644 ---- a/web/src/main/webResources/WEB-INF/data/config/index/records.json -+++ b/web/src/main/webResources/WEB-INF/data/config/index/records.json -@@ -1317,7 +1317,7 @@ - "mapping": { - "type": "nested", - "properties": { -- "org": { -+ "organisation": { - "type": "keyword" - }, - "role": { -``` +To learn more about the project, visit the [GeoNetwork-UI website](https://geonetwork.github.io/geonetwork-ui/main/docs/) ## Getting started @@ -103,67 +76,6 @@ npx nx build (app_name) The build artifacts will be stored in the `dist/` directory. Note: this always produces a production build. -### A word on authentication - -GeoNetwork-UI applications rely on the GeoNetwork authentication mechanism. This means that if the user is authenticated in GeoNetwork, they will have access to authenticated features in the corresponding GeoNetwork-UI apps. - -There are a few caveats, depending on the deployment scenario: - -#### 1. GeoNetwork and GeoNetwork-UI are deployed on the same host, e.g. https://my.host/geonetwork and https://my.host/datahub - -In this scenario, requests from the GeoNetwork-UI app to GeoNetwork are _not_ [cross-origin requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#what_requests_use_cors), so CORS rules do not apply. - -GeoNetwork has an XSRF protection by default, which _will_ make authenticated requests fail unless the following is done: - -- either make sure that the XSRF cookies sent by GeoNetwork have a `path` value of `/`; this is typically done like so in GeoNetwork: - - ```diff - --- a/web/src/main/webapp/WEB-INF/config-security/config-security-core.xml - +++ b/web/src/main/webapp/WEB-INF/config-security/config-security-core.xml - @@ -361,6 +361,7 @@ - - - + - - ``` - - Also make sure that the GeoNetwork API URL used by the application is _not_ an absolute URL; a relative URL should be enough in that scenario: - - ```diff - --- a/conf/default.toml - +++ b/conf/default.toml - @@ -5,7 +5,7 @@ - [global] - -geonetwork4_api_url = "https://my.host/geonetwork/srv/api" - +geonetwork4_api_url = "/geonetwork/srv/api" - ``` - -- or disable the XSRF protection selectively for non-critical endpoints of GeoNetwork, e.g. https://my.host/geonetwork/srv/api/userSelections for marking records as favorites; this is typically done like so in GeoNetwork: - - ```diff - --- a/web/src/main/webapp/WEB-INF/config-security/config-security-core.xml - +++ b/web/src/main/webapp/WEB-INF/config-security/config-security-core.xml - @@ -374,6 +374,9 @@ - /[a-zA-Z0-9_\-]+/[a-z]{2,3}/csw!?.* - /[a-zA-Z0-9_\-]+/api/search/.* - /[a-zA-Z0-9_\-]+/api/site - + /[a-zA-Z0-9_\-]+/api/userselections.* - - - - ``` - - :warning: Please do this responsibly as this could have security implications! :warning: - -#### 2. GeoNetwork and GeoNetwork-UI are _not_ deployed on the same host, e.g. https://my.host/geonetwork and https://another.org/datahub - -In this scenario, even if CORS settings are correctly set up on GeoNetwork side, most authenticated request will probably fail because by default they are not sent with the [`withCredentials: true`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials) option. - -As such, **authenticated requests are not yet supported in GeoNetwork-UI in the case of a cross-origin deployment**; non-authenticated requests (e.g. public search) should still work provided CORS settings were correctly set up on the GeoNetwork side (see [CORS resonse headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#the_http_response_headers)). - -Lastly, even if authenticated requests were cleared regarding CORS rules, it would still be needed to disable the XSRF mechanism for the endpoints that GeoNetwork-UI relies on; XSRF protections works by making the client read the content of an HTTP cookie, and that is forbidden in a cross-origin context - ### Tests #### Unit tests @@ -187,31 +99,24 @@ npx nx test --test-match=/data/dev/gn/ui/libs/common/src/lib/services/bootstrap. #### End-to-end-tests -You can test the datahub app by page : - -- home page -- search page -- organisations page -- dataset pages - -##### To run the tests with the interface : +##### To run the tests with the interface: -Start docker from 'support-services', and then in the 'geonetwork-ui' folder : +Start docker from 'support-services', and then in the project root folder : ```shell script -npx nx e2e appname --watch +npx nx e2e (app_name) --watch ``` Then select the file(s) you want to test in the interface. ##### To run the tests without interface : -Start docker from 'support-services', and then in the 'geonetwork-ui' folder : +Start docker from 'support-services', and then in the project root folder : --> ALl tests : ```shell script -npx nx e2e appname +npx nx e2e (app_name) ``` ## Project structure From 43f0d6fa24f972bd72467f014557528207f04707 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 26 Sep 2024 14:07:36 +0200 Subject: [PATCH 45/49] docs: improve project introduction, mention communication channels --- docs/.vitepress/config.ts | 6 +++--- docs/guide/introduction.md | 34 ++++++++++++++++++++++++++++++++++ docs/guide/why.md | 14 -------------- 3 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 docs/guide/introduction.md delete mode 100644 docs/guide/why.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index b7f751258a..88feb0711e 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -8,7 +8,7 @@ export default defineConfig({ themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ - { text: 'Guide', link: '/guide/why', activeMatch: '/guide/' }, + { text: 'Guide', link: '/guide/introduction', activeMatch: '/guide/' }, { text: 'Reference', link: '/reference/principles', @@ -49,7 +49,7 @@ function sidebarGuide() { { text: 'Guide', items: [ - { text: 'Why?', link: '/guide/why' }, + { text: 'Introduction', link: '/guide/introduction' }, { text: 'Prerequisites', link: '/guide/prerequisites' }, { text: 'Deploy', link: '/guide/deploy' }, { text: 'Configure', link: '/guide/configure' }, @@ -76,7 +76,7 @@ function sidebarGuide() { items: [ { text: 'Development environment', link: '/guide/dev-environment' }, { text: 'Create a Pull Request', link: '/guide/create-a-pr' }, - { text: 'Best practices', link: '/guide/best-practices' }, + { text: 'Code guide', link: '/guide/code-guide' }, { text: 'Versioning', link: '/guide/versioning' }, ], }, diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md new file mode 100644 index 0000000000..4fde0dffe7 --- /dev/null +++ b/docs/guide/introduction.md @@ -0,0 +1,34 @@ +--- +outline: deep +--- + +# Introduction + +## Why GeoNetwork-UI ? + +The GeoNetwork-UI project has been conceived as a way to depart from the long-standing and hard-to-use GeoNetwork interface, +and offer new functionalities and better user experience on top of the existing GeoNetwork API. Its core functionalities are +a **powerful search engine**, various **data visualization** components, and a better support for **non-geographic and open data** resources. + +Read the [Vision](./vision.html) section to understand better which approach is being adopted for this project and why. + +GeoNetwork-UI offers different applications suited to different use-cases. Applications are documented +in the [corresponding section](../apps/datahub.html). + +## Community + +GeoNetwork-UI is an open-source project just like GeoNetwork core, and is licensed under GPL-2.0. It is developed by a community of contributors from various organizations, mny of them also involved in the development of the GeoNetwork core project. + +### Contribution + +If you want to contribute, please read the guides in the "contributing" section of this website. You should also read the [Contribution Guide](https://github.com/geonetwork/geonetwork-ui/tree/main/CONTRIBUTING.md) in the GitHub repository. + +### Communication + +End users are [invited](https://discourse.osgeo.org/t/about-the-geonetwork-ui-category/59280) to join us on the GeoNetwork-UI forum on OsGeo Discourse: https://discourse.osgeo.org/c/geonetwork/ui + +For discussions on technical topics, you should head to the [GitHub discussions](https://github.com/geonetwork/geonetwork-ui/discussions) of the project. + +The community can also be reached through the [Gitter room chat](https://app.gitter.im/#/room/#geonetwork_geonetwork-ui:gitter.im) if needed. + +Lastly, bugs, issues and improvement requests should be reported on the [GitHub issue tracker](https://github.com/geonetwork/geonetwork-ui/issues). Thank you! diff --git a/docs/guide/why.md b/docs/guide/why.md deleted file mode 100644 index c5c85870ec..0000000000 --- a/docs/guide/why.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -outline: deep ---- - -# Why GeoNetwork-UI ? - -The GeoNetwork-UI project has been conceived as a way to depart from the long-standing GeoNetwork web-ui application based on AngularJS, -and offer new functionalities and better user experience on top of the existing GeoNetwork API. Its core functionalities are -a **powerful search engine**, various **data visualization** components, and a better support for **non-geographic and open data** resources. - -Read the [Vision](./vision.html) section to understand better which approach is being adopted for this project and why. - -GeoNetwork-UI offers different applications suited to different use-cases. Applications are documented -in the [corresponding section](../apps/datahub.html). From 9817a804dac03adb3558a01f48b83d05de00a350 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 26 Sep 2024 12:49:52 +0200 Subject: [PATCH 46/49] feat(me): redirect to login if needed --- .../src/e2e/dashboard.cy.ts | 20 ++- apps/metadata-editor/src/app/app.module.ts | 10 +- apps/metadata-editor/src/app/app.routes.ts | 159 +++++++++--------- .../src/app/auth-guard.service.spec.ts | 44 +++++ .../src/app/auth-guard.service.ts | 22 +++ .../app/sign-in/sign-in-page.component.css | 0 .../app/sign-in/sign-in-page.component.html | 1 - .../sign-in/sign-in-page.component.spec.ts | 22 --- .../src/app/sign-in/sign-in-page.component.ts | 9 - 9 files changed, 173 insertions(+), 114 deletions(-) create mode 100644 apps/metadata-editor/src/app/auth-guard.service.spec.ts create mode 100644 apps/metadata-editor/src/app/auth-guard.service.ts delete mode 100644 apps/metadata-editor/src/app/sign-in/sign-in-page.component.css delete mode 100644 apps/metadata-editor/src/app/sign-in/sign-in-page.component.html delete mode 100644 apps/metadata-editor/src/app/sign-in/sign-in-page.component.spec.ts delete mode 100644 apps/metadata-editor/src/app/sign-in/sign-in-page.component.ts diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index ad270663d4..053bf0a7f6 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -8,12 +8,15 @@ const fakeUser = { const gnBaseUrl = 'http://localhost:8080/geonetwork/srv/eng/' -describe('dashboard', () => { +describe('dashboard (authenticated)', () => { + beforeEach(() => { + cy.login('admin', 'admin', false) + }) + let pageOne describe('avatar', () => { describe('display avatar for user without gravatar hash', () => { it('should display placeholder url', () => { - cy.login('admin', 'admin', false) cy.visit(`${gnBaseUrl}admin.console#/organization`) cy.get('#gn-btn-user-add').click() cy.get('#username').type(fakeUser.username) @@ -35,7 +38,6 @@ describe('dashboard', () => { .should('eq', 'https://www.gravatar.com/avatar/?d=mp') }) it('should display monsterid', () => { - cy.login('admin', 'admin', false) cy.visit(`${gnBaseUrl}admin.console#/settings`) cy.get('[id="system/users/identicon"]').type( '{selectAll}gravatar:monsterid' @@ -160,7 +162,6 @@ describe('dashboard', () => { }) describe('columns', () => { beforeEach(() => { - cy.login('admin', 'admin', false) cy.visit('/catalog/search') }) it('should display the right info for unpublished records', () => { @@ -216,7 +217,6 @@ describe('dashboard', () => { describe('navigation', () => { beforeEach(() => { - cy.login('admin', 'admin', false) cy.visit('/catalog/search') }) describe('all records', () => { @@ -277,7 +277,6 @@ describe('dashboard', () => { } describe('allRecords search input', () => { beforeEach(() => { - cy.login('admin', 'admin', false) cy.visit('/catalog/search') }) it('should filter the dashboard based on the search input', () => { @@ -315,3 +314,12 @@ describe('dashboard', () => { }) }) }) + +describe('when the user is not logged in', () => { + beforeEach(() => { + cy.visit('/catalog/search') + }) + it('redirects to the login page', () => { + cy.url().should('include', '/catalog.signin?redirect=') + }) +}) diff --git a/apps/metadata-editor/src/app/app.module.ts b/apps/metadata-editor/src/app/app.module.ts index db79237697..c098de62f3 100644 --- a/apps/metadata-editor/src/app/app.module.ts +++ b/apps/metadata-editor/src/app/app.module.ts @@ -24,7 +24,11 @@ import { provideAnimations } from '@angular/platform-browser/animations' import { extModules } from './build-specifics' import { DashboardPageComponent } from './dashboard/dashboard-page.component' import { EditorRouterService } from './router.service' -import { provideGn4, provideRepositoryUrl } from '@geonetwork-ui/api/repository' +import { + LOGIN_URL, + provideGn4, + provideRepositoryUrl, +} from '@geonetwork-ui/api/repository' import { FeatureEditorModule } from '@geonetwork-ui/feature/editor' @NgModule({ @@ -62,6 +66,10 @@ import { FeatureEditorModule } from '@geonetwork-ui/feature/editor' importProvidersFrom(EffectsModule.forRoot()), provideGn4(), provideAnimations(), + { + provide: LOGIN_URL, + useFactory: () => getGlobalConfig().LOGIN_URL, + }, ], bootstrap: [AppComponent], }) diff --git a/apps/metadata-editor/src/app/app.routes.ts b/apps/metadata-editor/src/app/app.routes.ts index f2eac576b8..a4096ae9af 100644 --- a/apps/metadata-editor/src/app/app.routes.ts +++ b/apps/metadata-editor/src/app/app.routes.ts @@ -1,6 +1,5 @@ import { Route } from '@angular/router' import { DashboardPageComponent } from './dashboard/dashboard-page.component' -import { SignInPageComponent } from './sign-in/sign-in-page.component' import { EditPageComponent } from './edit/edit-page.component' import { EditRecordResolver } from './edit-record.resolver' import { MyDraftComponent } from './records/my-draft/my-draft.component' @@ -10,100 +9,110 @@ import { NewRecordResolver } from './new-record.resolver' import { DuplicateRecordResolver } from './duplicate-record.resolver' import { AllRecordsComponent } from './records/all-records/all-records.component' import { MyRecordsStateWrapperComponent } from './records/my-records/my-records-state-wrapper.component' +import { AuthGuardService } from './auth-guard.service' export const appRoutes: Route[] = [ - { path: '', redirectTo: 'catalog/search', pathMatch: 'prefix' }, { - path: 'catalog', - component: DashboardPageComponent, - outlet: 'primary', + path: '', + canActivate: [AuthGuardService], children: [ { path: '', - redirectTo: 'search', + redirectTo: 'catalog/search', pathMatch: 'prefix', }, { - path: 'discussion', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'catalog', + component: DashboardPageComponent, + outlet: 'primary', + children: [ + { + path: '', + redirectTo: 'search', + pathMatch: 'prefix', + }, + { + path: 'discussion', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'calendar', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'contacts', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'thesaurus', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'search', + title: 'Search Records', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + ], }, { - path: 'calendar', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'my-space', + component: DashboardPageComponent, + outlet: 'primary', + title: 'My space', + children: [ + { + path: 'my-records', + title: 'My Records', + component: MyRecordsStateWrapperComponent, + pathMatch: 'prefix', + }, + { + path: 'my-draft', + title: 'My Draft', + component: MyDraftComponent, + pathMatch: 'prefix', + }, + { + path: 'templates', + title: 'Templates', + component: TemplatesComponent, + pathMatch: 'prefix', + }, + ], }, { - path: 'contacts', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'users', + component: DashboardPageComponent, + outlet: 'primary', + title: 'Users', + children: [ + { + path: 'my-org', + component: MyOrgUsersComponent, + pathMatch: 'prefix', + }, + ], }, { - path: 'thesaurus', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'create', + component: EditPageComponent, + resolve: { record: NewRecordResolver }, }, { - path: 'search', - title: 'Search Records', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'duplicate/:uuid', + component: EditPageComponent, + resolve: { record: DuplicateRecordResolver }, }, - ], - }, - { - path: 'my-space', - component: DashboardPageComponent, - outlet: 'primary', - title: 'My space', - children: [ { - path: 'my-records', - title: 'My Records', - component: MyRecordsStateWrapperComponent, - pathMatch: 'prefix', - }, - { - path: 'my-draft', - title: 'My Draft', - component: MyDraftComponent, - pathMatch: 'prefix', - }, - { - path: 'templates', - title: 'Templates', - component: TemplatesComponent, - pathMatch: 'prefix', - }, - ], - }, - { - path: 'users', - component: DashboardPageComponent, - outlet: 'primary', - title: 'Users', - children: [ - { - path: 'my-org', - component: MyOrgUsersComponent, - pathMatch: 'prefix', + path: 'edit/:uuid', + component: EditPageComponent, + resolve: { record: EditRecordResolver }, }, ], }, - { path: 'sign-in', component: SignInPageComponent }, - { - path: 'create', - component: EditPageComponent, - resolve: { record: NewRecordResolver }, - }, - { - path: 'duplicate/:uuid', - component: EditPageComponent, - resolve: { record: DuplicateRecordResolver }, - }, - { - path: 'edit/:uuid', - component: EditPageComponent, - resolve: { record: EditRecordResolver }, - }, ] diff --git a/apps/metadata-editor/src/app/auth-guard.service.spec.ts b/apps/metadata-editor/src/app/auth-guard.service.spec.ts new file mode 100644 index 0000000000..8966947d11 --- /dev/null +++ b/apps/metadata-editor/src/app/auth-guard.service.spec.ts @@ -0,0 +1,44 @@ +import { AuthGuardService } from './auth-guard.service' +import { MockBuilder, MockProvider } from 'ng-mocks' +import { AuthService } from '@geonetwork-ui/api/repository' +import { TestBed } from '@angular/core/testing' +import { of } from 'rxjs' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' + +Object.defineProperty(window, 'location', { + value: new URL('http://localhost'), +}) + +describe('AuthGuardService', () => { + let service: AuthGuardService + beforeEach(() => { + return MockBuilder(AuthGuardService) + }) + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + MockProvider(AuthService, { + loginUrl: 'http://login.com/bla?', + }), + MockProvider(PlatformServiceInterface, { + isAnonymous: () => of(true), + }), + ], + }) + window.location.href = 'http://original.path' + service = TestBed.inject(AuthGuardService) + }) + + it('returns true if the user is logged in', async () => { + jest + .spyOn(TestBed.inject(PlatformServiceInterface), 'isAnonymous') + .mockReturnValue(of(false)) + expect(await service.canActivate()).toBe(true) + expect(window.location.href).toBe('http://original.path/') + }) + it('redirects the user to the login page if the user is not logged in', async () => { + expect(await service.canActivate()).toBe(false) + expect(window.location.href).toBe('http://login.com/bla?') + }) +}) diff --git a/apps/metadata-editor/src/app/auth-guard.service.ts b/apps/metadata-editor/src/app/auth-guard.service.ts new file mode 100644 index 0000000000..fa7087f610 --- /dev/null +++ b/apps/metadata-editor/src/app/auth-guard.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core' +import { AuthService } from '@geonetwork-ui/api/repository' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { firstValueFrom } from 'rxjs' + +@Injectable({ providedIn: 'root' }) +export class AuthGuardService { + constructor( + private platformService: PlatformServiceInterface, + private authService: AuthService + ) {} + + // this will redirect the user to the authentication form if required + async canActivate(): Promise { + const notLoggedIn = await firstValueFrom(this.platformService.isAnonymous()) + if (notLoggedIn) { + window.location.href = this.authService.loginUrl + return false + } + return true + } +} diff --git a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.css b/apps/metadata-editor/src/app/sign-in/sign-in-page.component.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.html b/apps/metadata-editor/src/app/sign-in/sign-in-page.component.html deleted file mode 100644 index 5b2382f971..0000000000 --- a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.html +++ /dev/null @@ -1 +0,0 @@ -

sign-in-page works!

diff --git a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.spec.ts b/apps/metadata-editor/src/app/sign-in/sign-in-page.component.spec.ts deleted file mode 100644 index 3dbbabbc30..0000000000 --- a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' - -import { SignInPageComponent } from './sign-in-page.component' - -describe('SignInPageComponent', () => { - let component: SignInPageComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [SignInPageComponent], - }).compileComponents() - - fixture = TestBed.createComponent(SignInPageComponent) - component = fixture.componentInstance - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) -}) diff --git a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.ts b/apps/metadata-editor/src/app/sign-in/sign-in-page.component.ts deleted file mode 100644 index 209e08e0f0..0000000000 --- a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core' - -@Component({ - selector: 'md-editor-sign-in', - templateUrl: './sign-in-page.component.html', - styleUrls: ['./sign-in-page.component.css'], - standalone: true, -}) -export class SignInPageComponent {} From c95043b7f097ba44ec01b37b0d725ad0aec4cd87 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 26 Sep 2024 14:25:02 +0200 Subject: [PATCH 47/49] docs: mention xsrf issue in faq --- docs/guide/faq.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/guide/faq.md b/docs/guide/faq.md index 2f3ee9bb1d..0bd1e3b2b4 100644 --- a/docs/guide/faq.md +++ b/docs/guide/faq.md @@ -2,8 +2,13 @@ outline: deep --- -# FAQ +# Frequently Asked Questions -## Chapter 1 +[[toc]] -## Chapter 2 +### _I have deployed Application Name alongside GeoNetwork, but somehow all the HTTP requests going to GeoNetwork end up failing with a 403 error, why?_ + +There are several possible reasons for this: + +- The attempted requests necessitate authentication (e.g. creating a record) but the session of the current user has expired; in this case, the user should log in again. +- The XSRF protection mechanism is not working correctly; this can be complicated to set up, please refer to [this part of the documentation](./deploy.md#authentication) to know more. From 84ef5d83f0dd447c1c9c0d92e16830b665581aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laure-H=C3=A9l=C3=A8ne=20Bruneton?= Date: Thu, 26 Sep 2024 09:32:58 +0200 Subject: [PATCH 48/49] feat(editor): record distribution as online resources --- apps/metadata-editor-e2e/src/e2e/edit.cy.ts | 82 ++++++ ...nline-service-resource-input.component.css | 0 ...line-service-resource-input.component.html | 31 +++ ...e-service-resource-input.component.spec.ts | 21 ++ ...online-service-resource-input.component.ts | 82 ++++++ ...field-online-link-resources.component.html | 31 ++- ...m-field-online-link-resources.component.ts | 3 + .../form-field-online-resources.component.css | 0 ...form-field-online-resources.component.html | 71 ++++++ ...m-field-online-resources.component.spec.ts | 55 ++++ .../form-field-online-resources.component.ts | 241 ++++++++++++++++++ .../form-field/form-field.component.html | 8 + .../form-field/form-field.component.ts | 8 +- libs/feature/editor/src/lib/fields.config.ts | 11 +- .../lib/file-input/file-input.component.html | 16 +- .../switch-toggle.component.html | 2 +- .../switch-toggle/switch-toggle.component.ts | 4 +- .../lib/url-input/url-input.component.html | 6 +- .../lib/url-input/url-input.component.spec.ts | 16 +- .../src/lib/url-input/url-input.component.ts | 27 +- translations/de.json | 7 + translations/en.json | 7 + translations/es.json | 7 + translations/fr.json | 9 +- translations/it.json | 7 + translations/nl.json | 7 + translations/pt.json | 7 + translations/sk.json | 7 + 28 files changed, 742 insertions(+), 31 deletions(-) create mode 100644 libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.css create mode 100644 libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html create mode 100644 libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.spec.ts create mode 100644 libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.css create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.spec.ts create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts diff --git a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts index 70ec2fef0a..a12a958693 100644 --- a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts @@ -404,6 +404,88 @@ describe('editor form', () => { }) }) }) + describe('distribution resources', () => { + beforeEach(() => { + cy.get('@resourcePageBtn').click() + }) + it('adds a resource', () => { + // item count before adding + cy.get( + 'gn-ui-form-field-online-resources gn-ui-online-resource-card' + ).should('have.length', 0) + cy.editor_wrapPreviousDraft() + // add a service distribution + cy.get('[data-cy="online-resources-type"] button').eq(1).click() + cy.get('gn-ui-online-service-resource-input mat-radio-button') + .contains('WMS') + .click() + cy.get('gn-ui-online-service-resource-input') + .find('[data-cy="identifier-in-service"]') + .type('A layer name as identifier in service') + cy.get('gn-ui-form-field-online-resources') + .find('gn-ui-url-input') + .find('input') + .type('http://example.com/wms') + cy.get('gn-ui-form-field-online-resources') + .find('gn-ui-url-input') + .find('button') + .click() + cy.editor_publishAndReload() + cy.get('@saveStatus').should('eq', 'record_up_to_date') + cy.get('@resourcePageBtn').click() + cy.get( + 'gn-ui-form-field-online-resources gn-ui-online-resource-card' + ).should('have.length', 1) + }) + it('modifies a resource', () => { + cy.get('gn-ui-form-field-online-resources gn-ui-online-resource-card') + .eq(0) + .as('wmsService') + cy.get('@wmsService') + .find('[data-test=card-title]') + .invoke('text') + .invoke('trim') + .should('eql', 'A layer name as identifier in service') + cy.editor_wrapPreviousDraft() + // open modify dialog + cy.get('@wmsService').find('button[data-test=card-modify]').click() + cy.get( + 'gn-ui-modal-dialog [data-cy="identifier-in-service"] input' + ).clear() + cy.get( + 'gn-ui-modal-dialog [data-cy="identifier-in-service"] input' + ).type('new identifier') + cy.get('gn-ui-modal-dialog [data-cy=confirm-button]').click() + cy.editor_publishAndReload() + cy.get('@resourcePageBtn').click() + cy.get('@wmsService') + .find('[data-test=card-title]') + .invoke('text') + .invoke('trim') + .should('eql', 'new identifier') + cy.get('@wmsService').scrollIntoView() + cy.screenshot({ capture: 'viewport' }) + }) + it('deletes a resource', () => { + // item count before deleting + cy.get( + 'gn-ui-form-field-online-resources gn-ui-online-resource-card' + ).should('have.length', 1) + cy.editor_wrapPreviousDraft() + // delete the first item + cy.get( + 'gn-ui-form-field-online-resources gn-ui-sortable-list [data-test=remove-item]' + ) + .eq(0) + .click() + cy.editor_publishAndReload() + cy.get('@saveStatus').should('eq', 'record_up_to_date') + cy.get('@resourcePageBtn').click() + cy.get( + 'gn-ui-form-field-online-resources gn-ui-online-resource-card' + ).should('have.length', 0) + }) + }) describe('attached resources', () => { beforeEach(() => { cy.get('@resourcePageBtn').click() diff --git a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.css b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html new file mode 100644 index 0000000000..4ee656c480 --- /dev/null +++ b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html @@ -0,0 +1,31 @@ +
+

+ editor.record.form.field.onlineResource.edit.protocol +

+ + help + +
+
+ + + {{ protocolOption.label | translate }} + + +
+ diff --git a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.spec.ts b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.spec.ts new file mode 100644 index 0000000000..d158c7e40a --- /dev/null +++ b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { TranslateModule } from '@ngx-translate/core' +import { OnlineServiceResourceInputComponent } from './online-service-resource-input.component' + +describe('OnlineServiceResourceInputComponent', () => { + let component: OnlineServiceResourceInputComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OnlineServiceResourceInputComponent, TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(OnlineServiceResourceInputComponent) + component = fixture.componentInstance + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts new file mode 100644 index 0000000000..a7eab8ec5b --- /dev/null +++ b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts @@ -0,0 +1,82 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, +} from '@angular/core' +import { FormsModule } from '@angular/forms' +import { MatIconModule } from '@angular/material/icon' +import { MatRadioModule } from '@angular/material/radio' +import { MatTooltipModule } from '@angular/material/tooltip' +import { marker } from '@biesbjerg/ngx-translate-extract-marker' +import { + DatasetServiceDistribution, + ServiceProtocol, +} from '@geonetwork-ui/common/domain/model/record' +import { TextInputComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' + +@Component({ + selector: 'gn-ui-online-service-resource-input', + templateUrl: './online-service-resource-input.component.html', + styleUrls: ['./online-service-resource-input.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + MatIconModule, + MatTooltipModule, + MatRadioModule, + FormsModule, + TextInputComponent, + TranslateModule, + ], +}) +export class OnlineServiceResourceInputComponent implements OnChanges { + @Input() service: Omit + @Input() protocolHint?: string + + selectedProtocol: ServiceProtocol + + protocolOptions: { + label: string + value: ServiceProtocol + }[] = [ + { + label: 'OGC API', + value: 'ogcFeatures', + }, + { + label: 'WFS', + value: 'wfs', + }, + { + label: 'WMS', + value: 'wms', + }, + { + label: 'WMTS', + value: 'wmts', + }, + { + label: 'WPS', + value: 'wps', + }, + { + label: 'ESRI REST', + value: 'esriRest', + }, + { + label: marker('editor.record.onlineResource.protocol.other'), + value: 'other', + }, + ] + + ngOnChanges() { + this.selectedProtocol = + this.protocolOptions.find( + (option) => option.value === this.service.accessServiceProtocol + )?.value ?? 'other' + } +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.html index d8df0ccf3c..6bd1486741 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.html @@ -20,15 +20,24 @@ -

- editor.record.form.field.onlineResource.edit.title -

- -

- editor.record.form.field.onlineResource.edit.description -

- +
+
+

+ editor.record.form.field.onlineResource.edit.title +

+ +
+
+

+ editor.record.form.field.onlineResource.edit.description +

+ +
+ + +
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.ts index a64b4aca96..a274dbc852 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.ts @@ -16,6 +16,7 @@ import { FileInputComponent, TextAreaComponent, TextInputComponent, + UrlInputComponent, } from '@geonetwork-ui/ui/inputs' import { CommonModule } from '@angular/common' import { OnlineResourceCardComponent } from '../../../online-resource-card/online-resource-card.component' @@ -43,6 +44,7 @@ import { MAX_UPLOAD_SIZE_MB } from '../../../../fields.config' OnlineResourceCardComponent, TextInputComponent, TextAreaComponent, + UrlInputComponent, TranslateModule, ], }) @@ -154,6 +156,7 @@ export class FormFieldOnlineLinkResourcesComponent { } this.dialog .open(ModalDialogComponent, { + width: '800px', data: { title: this.translateService.instant( 'editor.record.form.field.onlineResource.dialogTitle' diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html new file mode 100644 index 0000000000..8f40a8fc0f --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html @@ -0,0 +1,71 @@ + +
+ +
+ + + +
+
+ + + + + + + +
+
+

+ editor.record.form.field.onlineResource.edit.title +

+ +
+
+

+ editor.record.form.field.onlineResource.edit.description +

+ +
+ + + + + + +
+
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.spec.ts new file mode 100644 index 0000000000..307a0d5a62 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { TranslateModule } from '@ngx-translate/core' +import { FormFieldOnlineResourcesComponent } from './form-field-online-resources.component' +import { MockBuilder, MockProvider } from 'ng-mocks' +import { Subject } from 'rxjs' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { NotificationsService } from '@geonetwork-ui/feature/notifications' +import { MatDialog, MatDialogRef } from '@angular/material/dialog' + +let uploadSubject: Subject +class PlatformServiceInterfaceMock { + attachFileToRecord = jest.fn(() => { + uploadSubject = new Subject() + return uploadSubject + }) +} +export class MatDialogMock { + _subject = new Subject() + _closeWithValue = (v) => this._subject.next(v) + open = jest.fn(() => ({ + afterClosed: () => this._subject, + })) +} + +describe('FormFieldOnlineResourcesComponent', () => { + let component: FormFieldOnlineResourcesComponent + let fixture: ComponentFixture + + beforeEach(() => { + return MockBuilder(FormFieldOnlineResourcesComponent) + }) + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + MockProvider( + PlatformServiceInterface, + PlatformServiceInterfaceMock, + 'useClass' + ), + MockProvider(NotificationsService), + MockProvider(MatDialogRef), + MockProvider(MatDialog, MatDialogMock, 'useClass'), + ], + }).compileComponents() + + fixture = TestBed.createComponent(FormFieldOnlineResourcesComponent) + component = fixture.componentInstance + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts new file mode 100644 index 0000000000..afec5556fb --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts @@ -0,0 +1,241 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + Output, + TemplateRef, + ViewChild, +} from '@angular/core' +import { MatDialog } from '@angular/material/dialog' +import { marker } from '@biesbjerg/ngx-translate-extract-marker' +import { + DatasetDownloadDistribution, + DatasetServiceDistribution, + OnlineResource, + ServiceEndpoint, +} from '@geonetwork-ui/common/domain/model/record' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { NotificationsService } from '@geonetwork-ui/feature/notifications' +import { + FileInputComponent, + SwitchToggleComponent, + SwitchToggleOption, + TextAreaComponent, + TextInputComponent, + UrlInputComponent, +} from '@geonetwork-ui/ui/inputs' +import { + ModalDialogComponent, + SortableListComponent, +} from '@geonetwork-ui/ui/layout' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { Subscription } from 'rxjs' +import { MAX_UPLOAD_SIZE_MB } from '../../../../fields.config' +import { OnlineResourceCardComponent } from '../../../online-resource-card/online-resource-card.component' +import { OnlineServiceResourceInputComponent } from '../../../online-service-resource-input/online-service-resource-input.component' + +type OnlineNotLinkResource = + | DatasetDownloadDistribution + | DatasetServiceDistribution + | ServiceEndpoint + +@Component({ + selector: 'gn-ui-form-field-online-resources', + templateUrl: './form-field-online-resources.component.html', + styleUrls: ['./form-field-online-resources.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + SwitchToggleComponent, + FileInputComponent, + OnlineServiceResourceInputComponent, + UrlInputComponent, + SortableListComponent, + OnlineResourceCardComponent, + TextInputComponent, + TextAreaComponent, + TranslateModule, + ], +}) +export class FormFieldOnlineResourcesComponent { + @Input() metadataUuid: string + @Input() set value(onlineResources: Array) { + this.allResources = onlineResources + this.notLinkResources = onlineResources.filter( + (res): res is OnlineNotLinkResource => res.type !== 'link' + ) + } + @Output() valueChange: EventEmitter> = + new EventEmitter() + + @ViewChild('dialogTemplate') dialogTemplate: TemplateRef + + typeOptions: SwitchToggleOption[] = [ + { + label: marker('editor.record.form.field.onlineResource.toggle.dataset'), + value: 'download', + checked: true, + }, + { + label: marker('editor.record.form.field.onlineResource.toggle.service'), + value: 'service', + checked: false, + }, + ] + selectedType: 'download' | 'service' = 'download' + + private allResources: OnlineResource[] = [] + notLinkResources: OnlineNotLinkResource[] = [] + uploadProgress = undefined + uploadSubscription: Subscription = null + newService = { + type: 'service', + accessServiceProtocol: 'ogcFeatures', + identifierInService: '', + } as Omit + + protected MAX_UPLOAD_SIZE_MB = MAX_UPLOAD_SIZE_MB + + constructor( + private notificationsService: NotificationsService, + private translateService: TranslateService, + private platformService: PlatformServiceInterface, + private cd: ChangeDetectorRef, + private dialog: MatDialog + ) {} + + onSelectedTypeChange(selectedType: unknown) { + this.selectedType = selectedType as 'download' | 'service' + } + + handleFileChange(file: File) { + this.uploadProgress = 0 + this.uploadSubscription = this.platformService + .attachFileToRecord(this.metadataUuid, file) + .subscribe({ + next: (event) => { + if (event.type === 'progress') { + this.uploadProgress = event.progress + this.cd.detectChanges() + } else if (event.type === 'success') { + this.uploadProgress = undefined + this.cd.detectChanges() + const newResource: DatasetDownloadDistribution = { + type: 'download', + url: new URL(event.attachment.url), + name: event.attachment.fileName, + sizeBytes: event.sizeBytes, // WARNING: this is the only time that sizeBytes is set + } + this.valueChange.emit([...this.allResources, newResource]) + } + }, + error: (error: Error) => this.handleError(error.message), + }) + } + + handleUploadCancel() { + if (this.uploadSubscription) { + this.uploadProgress = undefined + this.uploadSubscription.unsubscribe() + } + } + + handleDownloadUrlChange(url: string) { + try { + const name = url.split('/').pop() + const newLink: DatasetDownloadDistribution = { + type: 'download', + url: new URL(url), + name, + } + this.valueChange.emit([...this.allResources, newLink]) + } catch (e) { + this.handleError((e as Error).message) + } + } + + handleServiceUrlChange(url: string) { + this.valueChange.emit([ + ...this.allResources, + { + ...this.newService, + url: new URL(url), + }, + ]) + } + + handleServiceModify( + oldService: DatasetServiceDistribution, + newService: DatasetServiceDistribution + ) { + oldService.accessServiceProtocol = newService.accessServiceProtocol + oldService.identifierInService = newService.identifierInService + oldService.url = newService.url + } + + handleResourcesChange(items: unknown[]) { + const notLinks = items as OnlineNotLinkResource[] + const newResources = [ + ...this.allResources.filter((r) => r.type === 'link'), + ...notLinks, + ] + this.valueChange.emit(newResources) + } + + handleResourceModify(resource: OnlineNotLinkResource, index: number) { + this.openEditDialog(resource, index) + } + + private handleError(error: string) { + this.uploadProgress = undefined + this.notificationsService.showNotification({ + type: 'error', + title: this.translateService.instant( + 'editor.record.onlineResourceError.title' + ), + text: `${this.translateService.instant( + 'editor.record.onlineResourceError.body' + )} ${error}`, + closeMessage: this.translateService.instant( + 'editor.record.onlineResourceError.closeMessage' + ), + }) + } + + private openEditDialog(resource: OnlineNotLinkResource, index: number) { + const resourceCopy = { + ...resource, + } + this.dialog + .open(ModalDialogComponent, { + width: '800px', + data: { + title: this.translateService.instant( + 'editor.record.form.field.onlineResource.dialogTitle' + ), + body: this.dialogTemplate, + bodyContext: resourceCopy, + confirmText: this.translateService.instant( + 'editor.record.form.field.onlineResource.confirm' + ), + cancelText: this.translateService.instant( + 'editor.record.form.field.onlineResource.cancel' + ), + }, + }) + .afterClosed() + .subscribe((confirmed: boolean) => { + if (!confirmed) return + const newNotLinks = [...this.notLinkResources] + newNotLinks.splice(index, 1, resourceCopy) + this.valueChange.emit([ + ...this.allResources.filter((r) => r.type === 'link'), + ...newNotLinks, + ]) + }) + } +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html index d5ef07d21a..01ea5566af 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html @@ -121,6 +121,14 @@ (valueChange)="valueChange.emit($event)" > + + + + +

- - {{ option.label }}{{ option.label | translate }} diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts index 72d8f9e161..4a0df6ba02 100644 --- a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts @@ -7,9 +7,11 @@ import { Output, } from '@angular/core' import { MatButtonToggleModule } from '@angular/material/button-toggle' +import { TranslateModule } from '@ngx-translate/core' export type SwitchToggleOption = { label: string + value?: unknown checked: boolean } @@ -19,7 +21,7 @@ export type SwitchToggleOption = { styleUrls: ['./switch-toggle.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [MatButtonToggleModule, CommonModule], + imports: [MatButtonToggleModule, CommonModule, TranslateModule], }) export class SwitchToggleComponent { @Input() options: SwitchToggleOption[] diff --git a/libs/ui/inputs/src/lib/url-input/url-input.component.html b/libs/ui/inputs/src/lib/url-input/url-input.component.html index 2f964788dc..9944a4b428 100644 --- a/libs/ui/inputs/src/lib/url-input/url-input.component.html +++ b/libs/ui/inputs/src/lib/url-input/url-input.component.html @@ -27,7 +27,11 @@ diff --git a/libs/ui/inputs/src/lib/url-input/url-input.component.spec.ts b/libs/ui/inputs/src/lib/url-input/url-input.component.spec.ts index d90106b83c..ba96445896 100644 --- a/libs/ui/inputs/src/lib/url-input/url-input.component.spec.ts +++ b/libs/ui/inputs/src/lib/url-input/url-input.component.spec.ts @@ -78,15 +78,27 @@ describe('UrlInputComponent', () => { }) describe('button', () => { - it('is disabled if value is input empty', () => { + it('is disabled if parent set it as disabled', () => { + component.disabled = true inputEl.value = '' fixture.detectChanges() expect(button.componentInstance.disabled).toBe(true) }) + it('is disabled if value is empty', () => { + inputEl.value = '' + fixture.detectChanges() + expect(button.componentInstance.disabled).toBe(true) + }) + it('is disabled if asking for parseable URL and value is not an URL', () => { + component.urlCanParse = true + inputEl.value = 'hello' + fixture.detectChanges() + expect(button.componentInstance.disabled).toBe(true) + }) it('is not disabled otherwise', () => { inputEl.value = 'hello' fixture.detectChanges() - expect(button.componentInstance.disabled).toBe(false) + expect(button.componentInstance.disabled).toBeFalsy() }) }) }) diff --git a/libs/ui/inputs/src/lib/url-input/url-input.component.ts b/libs/ui/inputs/src/lib/url-input/url-input.component.ts index 2cca056e24..9bafd36ad0 100644 --- a/libs/ui/inputs/src/lib/url-input/url-input.component.ts +++ b/libs/ui/inputs/src/lib/url-input/url-input.component.ts @@ -1,4 +1,11 @@ -import { ChangeDetectorRef, Component, Input, Output } from '@angular/core' +import { + ChangeDetectorRef, + Component, + Input, + OnChanges, + Output, + SimpleChanges, +} from '@angular/core' import { CommonModule } from '@angular/common' import { ButtonComponent } from '../button/button.component' import { MatIconModule } from '@angular/material/icon' @@ -12,16 +19,23 @@ import { Subject } from 'rxjs' standalone: true, imports: [CommonModule, ButtonComponent, MatIconModule], }) -export class UrlInputComponent { +export class UrlInputComponent implements OnChanges { @Input() value = '' @Input() extraClass = '' @Input() placeholder = 'https://' @Input() disabled: boolean + @Input() urlCanParse?: boolean rawChange = new Subject() @Output() valueChange = this.rawChange.pipe(filter((v) => !!v)) constructor(private cd: ChangeDetectorRef) {} + ngOnChanges(changes: SimpleChanges): void { + if (changes.value) { + console.log('changes.value', changes.value) + } + } + handleInput() { this.cd.markForCheck() } @@ -30,4 +44,13 @@ export class UrlInputComponent { const value = element.value this.rawChange.next(value) } + + URLcanParse(url: string): boolean { + try { + new URL(url) + return true + } catch (e) { + return false + } + } } diff --git a/translations/de.json b/translations/de.json index 4ba2803a1d..0095630ab3 100644 --- a/translations/de.json +++ b/translations/de.json @@ -1,4 +1,5 @@ { + "": "", "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{Datensätze} one{Datensatz} other{Datensätze}}", @@ -217,9 +218,13 @@ "editor.record.form.field.onlineResource.confirm": "", "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", + "editor.record.form.field.onlineResource.edit.protocol": "", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", "editor.record.form.field.onlineResource.modify": "", + "editor.record.form.field.onlineResource.toggle.dataset": "", + "editor.record.form.field.onlineResource.toggle.service": "", + "editor.record.form.field.onlineResources": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "Datensatz zuletzt aktualisiert", "editor.record.form.field.resourceUpdated": "Letztes Aktualisierungsdatum", @@ -242,6 +247,7 @@ "editor.record.form.page.ressources": "", "editor.record.form.section.about.description": "", "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.description": "", "editor.record.form.section.annexes.label": "", "editor.record.form.section.associatedResources.description": "", "editor.record.form.section.associatedResources.label": "", @@ -265,6 +271,7 @@ "editor.record.loadError.body": "Der Datensatz konnte nicht geladen werden:", "editor.record.loadError.closeMessage": "Verstanden", "editor.record.loadError.title": "Fehler beim Laden des Datensatzes", + "editor.record.onlineResource.protocol.other": "", "editor.record.onlineResourceError.body": "", "editor.record.onlineResourceError.closeMessage": "", "editor.record.onlineResourceError.title": "", diff --git a/translations/en.json b/translations/en.json index 4ad061ef09..1c0638f91d 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,4 +1,5 @@ { + "": "", "Add Layer As": "", "button.login": "Log in", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", @@ -217,9 +218,13 @@ "editor.record.form.field.onlineResource.confirm": "Confirm", "editor.record.form.field.onlineResource.dialogTitle": "Modify the resource preview", "editor.record.form.field.onlineResource.edit.description": "Description", + "editor.record.form.field.onlineResource.edit.protocol": "Protocol", "editor.record.form.field.onlineResource.edit.title": "Title", "editor.record.form.field.onlineResource.fileSize": "{sizeMB}MB", "editor.record.form.field.onlineResource.modify": "Modify", + "editor.record.form.field.onlineResource.toggle.dataset": "Link to a dataset", + "editor.record.form.field.onlineResource.toggle.service": "Link to a service", + "editor.record.form.field.onlineResources": "Distribution", "editor.record.form.field.overviews": "Overviews", "editor.record.form.field.recordUpdated": "Record Updated", "editor.record.form.field.resourceUpdated": "Resource Updated", @@ -242,6 +247,7 @@ "editor.record.form.page.ressources": "Resources", "editor.record.form.section.about.description": "This section describes the resource.", "editor.record.form.section.about.label": "About the resource", + "editor.record.form.section.annexes.description": "", "editor.record.form.section.annexes.label": "Annexes", "editor.record.form.section.associatedResources.description": "Drop files here to associate them with the resource.", "editor.record.form.section.associatedResources.label": "Associated resources", @@ -265,6 +271,7 @@ "editor.record.loadError.body": "The record could not be loaded:", "editor.record.loadError.closeMessage": "Understood", "editor.record.loadError.title": "Error loading record", + "editor.record.onlineResource.protocol.other": "Other", "editor.record.onlineResourceError.body": "An error occurred while adding the resource:", "editor.record.onlineResourceError.closeMessage": "Understood", "editor.record.onlineResourceError.title": "Error adding resource", diff --git a/translations/es.json b/translations/es.json index 49a8c44dbc..c0a6c50667 100644 --- a/translations/es.json +++ b/translations/es.json @@ -1,4 +1,5 @@ { + "": "", "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de datos", @@ -217,9 +218,13 @@ "editor.record.form.field.onlineResource.confirm": "", "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", + "editor.record.form.field.onlineResource.edit.protocol": "", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", "editor.record.form.field.onlineResource.modify": "", + "editor.record.form.field.onlineResource.toggle.dataset": "", + "editor.record.form.field.onlineResource.toggle.service": "", + "editor.record.form.field.onlineResources": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", "editor.record.form.field.resourceUpdated": "", @@ -242,6 +247,7 @@ "editor.record.form.page.ressources": "", "editor.record.form.section.about.description": "", "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.description": "", "editor.record.form.section.annexes.label": "", "editor.record.form.section.associatedResources.description": "", "editor.record.form.section.associatedResources.label": "", @@ -265,6 +271,7 @@ "editor.record.loadError.body": "", "editor.record.loadError.closeMessage": "", "editor.record.loadError.title": "", + "editor.record.onlineResource.protocol.other": "", "editor.record.onlineResourceError.body": "", "editor.record.onlineResourceError.closeMessage": "", "editor.record.onlineResourceError.title": "", diff --git a/translations/fr.json b/translations/fr.json index 29b8281384..1198b1c383 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -1,4 +1,5 @@ { + "": "", "Add Layer As": "", "button.login": "Se connecter", "catalog.figures.datasets": "{count, plural, =0{données} one{donnée} other{données}}", @@ -217,9 +218,13 @@ "editor.record.form.field.onlineResource.confirm": "Valider", "editor.record.form.field.onlineResource.dialogTitle": "Modifier l'aperçu de la ressource", "editor.record.form.field.onlineResource.edit.description": "Description", + "editor.record.form.field.onlineResource.edit.protocol": "Protocole", "editor.record.form.field.onlineResource.edit.title": "Titre", "editor.record.form.field.onlineResource.fileSize": "{sizeMB} Mo", "editor.record.form.field.onlineResource.modify": "Modifier", + "editor.record.form.field.onlineResource.toggle.dataset": "Lier un jeu de données", + "editor.record.form.field.onlineResource.toggle.service": "Lier un service", + "editor.record.form.field.onlineResources": "Distribution", "editor.record.form.field.overviews": "Aperçus", "editor.record.form.field.recordUpdated": "Date de dernière révision", "editor.record.form.field.resourceUpdated": "Date de dernière révision", @@ -242,8 +247,9 @@ "editor.record.form.page.ressources": "Ressources", "editor.record.form.section.about.description": "Ces informations concernent la donnée.", "editor.record.form.section.about.label": "À propos de la ressource", + "editor.record.form.section.annexes.description": "Les annexes sont optionnels. Ce sont des pièces jointes de la fiche de métadonnées qui peuvent aider à mieux comprendre la donnée (notice, etc.)", "editor.record.form.section.annexes.label": "Annexes", - "editor.record.form.section.associatedResources.description": "Déposez les jeux de données associées à cette fiche de métadonnées.", + "editor.record.form.section.associatedResources.description": "Liez des jeux de données ou des services associés à cette fiche de métadonnée.", "editor.record.form.section.associatedResources.label": "Ressources associées", "editor.record.form.section.classification.description": "La classification a un impact sur la recherche du jeu de données.", "editor.record.form.section.classification.label": "Classification", @@ -265,6 +271,7 @@ "editor.record.loadError.body": "La fiche n'a pas pu être chargée :", "editor.record.loadError.closeMessage": "Compris", "editor.record.loadError.title": "Erreur lors du chargement", + "editor.record.onlineResource.protocol.other": "Autre", "editor.record.onlineResourceError.body": "Une erreur est survenue lors de l'ajout de la ressource :", "editor.record.onlineResourceError.closeMessage": "Compris", "editor.record.onlineResourceError.title": "Erreur lors de l'ajout d'une ressource", diff --git a/translations/it.json b/translations/it.json index 55080f29f2..a5c3b93f39 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1,4 +1,5 @@ { + "": "", "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", @@ -217,9 +218,13 @@ "editor.record.form.field.onlineResource.confirm": "", "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", + "editor.record.form.field.onlineResource.edit.protocol": "", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", "editor.record.form.field.onlineResource.modify": "", + "editor.record.form.field.onlineResource.toggle.dataset": "", + "editor.record.form.field.onlineResource.toggle.service": "", + "editor.record.form.field.onlineResources": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", "editor.record.form.field.resourceUpdated": "", @@ -242,6 +247,7 @@ "editor.record.form.page.ressources": "", "editor.record.form.section.about.description": "", "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.description": "", "editor.record.form.section.annexes.label": "", "editor.record.form.section.associatedResources.description": "", "editor.record.form.section.associatedResources.label": "", @@ -265,6 +271,7 @@ "editor.record.loadError.body": "", "editor.record.loadError.closeMessage": "", "editor.record.loadError.title": "", + "editor.record.onlineResource.protocol.other": "", "editor.record.onlineResourceError.body": "", "editor.record.onlineResourceError.closeMessage": "", "editor.record.onlineResourceError.title": "", diff --git a/translations/nl.json b/translations/nl.json index 1174781c78..3f8cf3fb7c 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -1,4 +1,5 @@ { + "": "", "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "datasets", @@ -217,9 +218,13 @@ "editor.record.form.field.onlineResource.confirm": "", "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", + "editor.record.form.field.onlineResource.edit.protocol": "", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", "editor.record.form.field.onlineResource.modify": "", + "editor.record.form.field.onlineResource.toggle.dataset": "", + "editor.record.form.field.onlineResource.toggle.service": "", + "editor.record.form.field.onlineResources": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", "editor.record.form.field.resourceUpdated": "", @@ -242,6 +247,7 @@ "editor.record.form.page.ressources": "", "editor.record.form.section.about.description": "", "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.description": "", "editor.record.form.section.annexes.label": "", "editor.record.form.section.associatedResources.description": "", "editor.record.form.section.associatedResources.label": "", @@ -265,6 +271,7 @@ "editor.record.loadError.body": "", "editor.record.loadError.closeMessage": "", "editor.record.loadError.title": "", + "editor.record.onlineResource.protocol.other": "", "editor.record.onlineResourceError.body": "", "editor.record.onlineResourceError.closeMessage": "", "editor.record.onlineResourceError.title": "", diff --git a/translations/pt.json b/translations/pt.json index bb817d78a3..89d4ddc0c3 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -1,4 +1,5 @@ { + "": "", "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de dados", @@ -217,9 +218,13 @@ "editor.record.form.field.onlineResource.confirm": "", "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", + "editor.record.form.field.onlineResource.edit.protocol": "", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", "editor.record.form.field.onlineResource.modify": "", + "editor.record.form.field.onlineResource.toggle.dataset": "", + "editor.record.form.field.onlineResource.toggle.service": "", + "editor.record.form.field.onlineResources": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", "editor.record.form.field.resourceUpdated": "", @@ -242,6 +247,7 @@ "editor.record.form.page.ressources": "", "editor.record.form.section.about.description": "", "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.description": "", "editor.record.form.section.annexes.label": "", "editor.record.form.section.associatedResources.description": "", "editor.record.form.section.associatedResources.label": "", @@ -265,6 +271,7 @@ "editor.record.loadError.body": "", "editor.record.loadError.closeMessage": "", "editor.record.loadError.title": "", + "editor.record.onlineResource.protocol.other": "", "editor.record.onlineResourceError.body": "", "editor.record.onlineResourceError.closeMessage": "", "editor.record.onlineResourceError.title": "", diff --git a/translations/sk.json b/translations/sk.json index 51f07aff9d..7af8363d38 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -1,4 +1,5 @@ { + "": "", "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasety} one{dataset} other{datasety}}", @@ -217,9 +218,13 @@ "editor.record.form.field.onlineResource.confirm": "", "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", + "editor.record.form.field.onlineResource.edit.protocol": "", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", "editor.record.form.field.onlineResource.modify": "", + "editor.record.form.field.onlineResource.toggle.dataset": "", + "editor.record.form.field.onlineResource.toggle.service": "", + "editor.record.form.field.onlineResources": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", "editor.record.form.field.resourceUpdated": "", @@ -242,6 +247,7 @@ "editor.record.form.page.ressources": "", "editor.record.form.section.about.description": "", "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.description": "", "editor.record.form.section.annexes.label": "", "editor.record.form.section.associatedResources.description": "", "editor.record.form.section.associatedResources.label": "", @@ -265,6 +271,7 @@ "editor.record.loadError.body": "", "editor.record.loadError.closeMessage": "", "editor.record.loadError.title": "", + "editor.record.onlineResource.protocol.other": "", "editor.record.onlineResourceError.body": "", "editor.record.onlineResourceError.closeMessage": "", "editor.record.onlineResourceError.title": "", From 177e6ab86e81861f52a5d898350650c342798918 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 30 Sep 2024 10:31:11 +0200 Subject: [PATCH 49/49] e2e(me): fix edit test --- apps/metadata-editor-e2e/src/e2e/edit.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts index a12a958693..abed248ec3 100644 --- a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts @@ -454,7 +454,7 @@ describe('editor form', () => { ).clear() cy.get( 'gn-ui-modal-dialog [data-cy="identifier-in-service"] input' - ).type('new identifier') + ).type('{selectAll}{backspace}new identifier') cy.get('gn-ui-modal-dialog [data-cy=confirm-button]').click() cy.editor_publishAndReload() cy.get('@resourcePageBtn').click()