From a3d965976eb495fb2dca4f5bf070a679be734c57 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Mon, 29 Jul 2024 14:11:13 +0200 Subject: [PATCH 1/5] feat(ui): Add editor contact card component --- libs/ui/elements/src/index.ts | 1 + .../editor-contact-card.component.css | 0 .../editor-contact-card.component.html | 21 ++++++++++ .../editor-contact-card.component.spec.ts | 32 +++++++++++++++ .../editor-contact-card.component.stories.ts | 39 +++++++++++++++++++ .../editor-contact-card.component.ts | 32 +++++++++++++++ .../ui/elements/src/lib/ui-elements.module.ts | 3 ++ 7 files changed, 128 insertions(+) create mode 100644 libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.css create mode 100644 libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.html create mode 100644 libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.spec.ts create mode 100644 libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.stories.ts create mode 100644 libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.ts diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index 327b9d5c81..63200bae90 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -4,6 +4,7 @@ export * from './lib/confirmation-dialog/confirmation-dialog.component' export * from './lib/content-ghost/content-ghost.component' export * from './lib/download-item/download-item.component' export * from './lib/downloads-list/downloads-list.component' +export * from './lib/editor-contact-card/editor-contact-card.component' export * from './lib/error/error.component' export * from './lib/image-overlay-preview/image-overlay-preview.component' export * from './lib/link-card/link-card.component' diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.css b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.html b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.html new file mode 100644 index 0000000000..ac23076b16 --- /dev/null +++ b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.html @@ -0,0 +1,21 @@ + +
+ +
+
+ {{ contact.firstName }} {{ contact.lastName }} + + +
+
{{ contact.email }}
+
+
+
diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.spec.ts b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.spec.ts new file mode 100644 index 0000000000..90e0e6e922 --- /dev/null +++ b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.spec.ts @@ -0,0 +1,32 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { EditorContactCardComponent } from './editor-contact-card.component' + +describe('LinkCardComponent', () => { + let component: EditorContactCardComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [EditorContactCardComponent], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(EditorContactCardComponent) + component = fixture.componentInstance + component.link = { + name: 'Consulter sur Géoclip', + description: + 'Lorem ipsum dolor sit amet, consect etur adipiscing elit. Donec id condim entum ex. Etiam sed molestie est.', + url: new URL('https://example.com/someurlpath'), + type: 'link', + } + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.stories.ts b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.stories.ts new file mode 100644 index 0000000000..d4ddcde93d --- /dev/null +++ b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.stories.ts @@ -0,0 +1,39 @@ +import { + applicationConfig, + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { EditorContactCardComponent } from './editor-contact-card.component' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { importProvidersFrom } from '@angular/core' + +export default { + title: 'Elements/LinkCardComponent', + component: EditorContactCardComponent, + decorators: [ + moduleMetadata({ + imports: [EditorContactCardComponent], + }), + applicationConfig({ + providers: [importProvidersFrom(BrowserAnimationsModule)], + }), + componentWrapperDecorator( + (story) => `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + compact: false, + link: { + type: 'link', + name: 'Consulter sur Géoclip', + description: + 'Lorem ipsum dolor sit amet, consect etur adipiscing elit. Donec id condim entum ex. Etiam sed molestie est.', + url: new URL('https://example.com/someurlpath'), + }, + }, +} diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.ts b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.ts new file mode 100644 index 0000000000..b919d8e7a7 --- /dev/null +++ b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.ts @@ -0,0 +1,32 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' +import { + Individual, + Organization, +} from '@geonetwork-ui/common/domain/model/record' +import { MatIconModule } from '@angular/material/icon' +import { CommonModule } from '@angular/common' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' + +@Component({ + selector: 'gn-ui-editor-contact-card', + templateUrl: './editor-contact-card.component.html', + styleUrls: ['./editor-contact-card.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, MatIconModule, ButtonComponent], +}) +export class EditorContactCardComponent { + @Input() contact: Individual + @Input() organization: Organization + @Output() contactRemoved = new EventEmitter() + + removeContact(contact: Individual) { + this.contactRemoved.emit(contact) + } +} diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index d99869b046..64b139821d 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -31,6 +31,7 @@ import { MarkdownParserComponent } from './markdown-parser/markdown-parser.compo import { ImageOverlayPreviewComponent } from './image-overlay-preview/image-overlay-preview.component' import { UserFeedbackItemComponent } from './user-feedback-item/user-feedback-item.component' import { TimeSincePipe } from './user-feedback-item/time-since.pipe' +import { EditorContactCardComponent } from './editor-contact-card/editor-contact-card.component' @NgModule({ imports: [ @@ -51,6 +52,7 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe' TimeSincePipe, BadgeComponent, MaxLinesComponent, + EditorContactCardComponent, ], declarations: [ MetadataInfoComponent, @@ -78,6 +80,7 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe' ContentGhostComponent, DownloadItemComponent, DownloadsListComponent, + EditorContactCardComponent, ApiCardComponent, RelatedRecordCardComponent, MetadataContactComponent, From 03118e88dcc8cd22ce4bdbc62377079b1515dc20 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Mon, 29 Jul 2024 14:12:05 +0200 Subject: [PATCH 2/5] feat(me): Add contacts for resource field --- apps/metadata-editor-e2e/src/e2e/edit.cy.ts | 76 +++++++ .../page-selector.component.html | 5 +- .../src/lib/model/record/contact.model.ts | 34 ++++ .../contact-card/contact-card.component.css} | 0 .../contact-card/contact-card.component.html} | 3 +- .../contact-card.component.spec.ts | 78 ++++++++ .../contact-card.component.stories.ts | 54 +++++ .../contact-card/contact-card.component.ts} | 8 +- ...-field-contacts-for-resource.component.css | 0 ...field-contacts-for-resource.component.html | 52 +++++ ...ld-contacts-for-resource.component.spec.ts | 181 +++++++++++++++++ ...m-field-contacts-for-resource.component.ts | 188 ++++++++++++++++++ .../form-field/form-field.component.html | 5 + .../form-field/form-field.component.ts | 5 + .../record-form/record-form.component.html | 2 +- libs/feature/editor/src/lib/fields.config.ts | 10 +- libs/ui/elements/src/index.ts | 1 - .../editor-contact-card.component.spec.ts | 32 --- .../editor-contact-card.component.stories.ts | 39 ---- .../ui/elements/src/lib/ui-elements.module.ts | 3 - .../autocomplete.component.spec.ts | 16 +- .../autocomplete/autocomplete.component.ts | 7 +- translations/en.json | 21 ++ translations/es.json | 21 ++ translations/fr.json | 23 ++- translations/it.json | 21 ++ translations/nl.json | 21 ++ translations/pt.json | 21 ++ translations/sk.json | 21 ++ 29 files changed, 861 insertions(+), 87 deletions(-) rename libs/{ui/elements/src/lib/editor-contact-card/editor-contact-card.component.css => feature/editor/src/lib/components/contact-card/contact-card.component.css} (100%) rename libs/{ui/elements/src/lib/editor-contact-card/editor-contact-card.component.html => feature/editor/src/lib/components/contact-card/contact-card.component.html} (92%) create mode 100644 libs/feature/editor/src/lib/components/contact-card/contact-card.component.spec.ts create mode 100644 libs/feature/editor/src/lib/components/contact-card/contact-card.component.stories.ts rename libs/{ui/elements/src/lib/editor-contact-card/editor-contact-card.component.ts => feature/editor/src/lib/components/contact-card/contact-card.component.ts} (78%) create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.css create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.spec.ts create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts delete mode 100644 libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.spec.ts delete mode 100644 libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.stories.ts diff --git a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts index 99d17c6149..ebd6b39f61 100644 --- a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts @@ -15,6 +15,18 @@ describe('editor form', () => { cy.get('[data-cy=save-status]') .invoke('attr', 'data-cy-value') .as('saveStatus') + cy.get('[data-test=pageSelectorButtons]') + .find('gn-ui-button') + .eq(0) + .as('resourceDescriptionPageSelectorButton') + cy.get('[data-test=pageSelectorButtons]') + .find('gn-ui-button') + .eq(1) + .as('resourcePageSelectorButton') + cy.get('[data-test=pageSelectorButtons]') + .find('gn-ui-button') + .eq(2) + .as('accessAndContactPageSelectorButton') }) it('form shows correctly', () => { @@ -58,6 +70,70 @@ describe('editor form', () => { cy.get('md-editor-publish-button').click() }) + describe('record fields', () => { + describe('contacts for resources', () => { + beforeEach(() => { + cy.login('admin', 'admin', false) + + // Alpine convention record + cy.visit('/edit/accroche_velos') + + cy.get('@accessAndContactPageSelectorButton').click() + }) + + it('show the contacts for resource of the dataset', () => { + cy.get('[data-test=displayedRoles]').children().should('have.length', 1) + }) + + it('show the 5 roles available to add', () => { + cy.get('[data-test=rolesToPick]').children().should('have.length', 5) + }) + + it('click on a role adds it to the list of displayed role', () => { + cy.get('[data-test="rolesToPick"]').children().eq(2).click() + + cy.get('[data-test=rolesToPick]').children().should('have.length', 4) + + cy.get('[data-test=displayedRoles]').children().should('have.length', 2) + }) + + it('add a contact for resource', () => { + cy.get('[data-test=displayedRoles]') + .children() + .find('gn-ui-contact-card') + .should('have.length', 1) + + cy.get('[data-test=displayedRoles]') + .find('gn-ui-autocomplete') + .type('bar') + + cy.get('mat-option').should('have.text', ' Barbara Roberts ').click() + + cy.get('[data-test=displayedRoles]') + .children() + .find('gn-ui-contact-card') + .should('have.length', 2) + }) + + it('delete a contact for resource', () => { + cy.get('[data-test=displayedRoles]') + .children() + .find('gn-ui-contact-card') + .should('have.length', 1) + + cy.get('[data-test=displayedRoles]') + .children() + .get('[data-test=removeContactButton]') + .click() + + cy.get('[data-test=displayedRoles]') + .children() + .find('gn-ui-contact-card') + .should('not.exist') + }) + }) + }) + describe('date range in sortable list', () => { it('should keep the date picker open when selecting the start date of a range', () => { // add a date range diff --git a/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.html b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.html index 279daf25d9..98cbaa381b 100644 --- a/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.html +++ b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.html @@ -1,4 +1,7 @@ -
+
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 ebe025902e..3372fb603c 100644 --- a/libs/common/domain/src/lib/model/record/contact.model.ts +++ b/libs/common/domain/src/lib/model/record/contact.model.ts @@ -1,4 +1,5 @@ import { Organization } from './organization.model' +import { marker } from '@biesbjerg/ngx-translate-extract-marker' export const RoleValues = [ 'unspecified', @@ -24,6 +25,39 @@ export const RoleValues = [ 'user', // Party who uses the resource ] +export const RoleLabels = new Map([ + ['unspecified', marker('metadata.contactForResource.role.unspecified')], + ['other', marker('metadata.contactForResource.role.other')], + ['author', marker('metadata.contactForResource.role.author')], + ['collaborator', marker('metadata.contactForResource.role.collaborator')], + ['contributor', marker('metadata.contactForResource.role.contributor')], + ['custodian', marker('metadata.contactForResource.role.custodian')], + ['distributor', marker('metadata.contactForResource.role.distributor')], + ['editor', marker('metadata.contactForResource.role.editor')], + ['funder', marker('metadata.contactForResource.role.funder')], + ['mediator', marker('metadata.contactForResource.role.mediator')], + ['originator', marker('metadata.contactForResource.role.originator')], + ['owner', marker('metadata.contactForResource.role.owner')], + [ + 'point_of_contact', + marker('metadata.contactForResource.role.point_of_contact'), + ], + [ + 'principal_investigator', + marker('metadata.contactForResource.role.principal_investigator'), + ], + ['processor', marker('metadata.contactForResource.role.processor')], + ['publisher', marker('metadata.contactForResource.role.publisher')], + [ + 'resource_provider', + marker('metadata.contactForResource.role.resource_provider'), + ], + ['rights_holder', marker('metadata.contactForResource.role.rights_holder')], + ['sponsor', marker('metadata.contactForResource.role.sponsor')], + ['stakeholder', marker('metadata.contactForResource.role.stakeholder')], + ['user', marker('metadata.contactForResource.role.user')], +]) + export type Role = typeof RoleValues[number] export interface Individual { diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.css b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.css similarity index 100% rename from libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.css rename to libs/feature/editor/src/lib/components/contact-card/contact-card.component.css diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.html b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.html similarity index 92% rename from libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.html rename to libs/feature/editor/src/lib/components/contact-card/contact-card.component.html index ac23076b16..849b22ef30 100644 --- a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.html +++ b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.html @@ -9,7 +9,8 @@ >{{ contact.firstName }} {{ contact.lastName }} diff --git a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.spec.ts b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.spec.ts new file mode 100644 index 0000000000..1ce24cb81e --- /dev/null +++ b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.spec.ts @@ -0,0 +1,78 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { ContactCardComponent } from './contact-card.component' +import { + Individual, + Organization, +} from '@geonetwork-ui/common/domain/model/record' +import { MatIconModule } from '@angular/material/icon' +import { CommonModule } from '@angular/common' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' + +describe('ContactCardComponent', () => { + let component: ContactCardComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + MatIconModule, + ButtonComponent, + ContactCardComponent, + ], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(ContactCardComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create the component', () => { + expect(component).toBeTruthy() + }) + + it('should have a defined contact input', () => { + const mockContact: Individual = { + firstName: 'John', + lastName: 'Doe', + organization: { name: 'Org1' } as Organization, + email: 'john.doe@example.com', + role: 'admin', + address: '', + phone: '', + position: '', + } + component.contact = mockContact + fixture.detectChanges() + expect(component.contact).toEqual(mockContact) + }) + + it('should have a defined organization input', () => { + const mockOrganization: Organization = { + name: 'Org1', + } + component.organization = mockOrganization + fixture.detectChanges() + expect(component.organization).toEqual(mockOrganization) + }) + + it('should emit contactRemoved event with the correct contact', () => { + const mockContact: Individual = { + firstName: 'John', + lastName: 'Doe', + organization: { name: 'Org1' } as Organization, + email: 'john.doe@example.com', + role: 'admin', + address: '', + phone: '', + position: '', + } + component.contact = mockContact + + const contactRemovedSpy = jest.spyOn(component.contactRemoved, 'emit') + component.removeContact(mockContact) + expect(contactRemovedSpy).toHaveBeenCalledWith(mockContact) + }) +}) diff --git a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.stories.ts b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.stories.ts new file mode 100644 index 0000000000..3c75b032e8 --- /dev/null +++ b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.stories.ts @@ -0,0 +1,54 @@ +import { + applicationConfig, + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { ContactCardComponent } from './contact-card.component' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { importProvidersFrom } from '@angular/core' +import { MatIconModule } from '@angular/material/icon' +import { CommonModule } from '@angular/common' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' + +export default { + title: 'Elements/ContactCardComponent', + component: ContactCardComponent, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + MatIconModule, + ButtonComponent, + ContactCardComponent, + ], + }), + applicationConfig({ + providers: [importProvidersFrom(BrowserAnimationsModule)], + }), + componentWrapperDecorator( + (story) => `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + contact: { + firstName: 'John', + lastName: 'Doe', + organization: { + name: 'Example Organization', + }, + email: 'john.doe@example.com', + role: 'Developer', + address: '123 Main St', + phone: '123-456-7890', + position: 'Senior Developer', + }, + organization: { + name: 'Example Organization', + }, + }, +} diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.ts b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts similarity index 78% rename from libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.ts rename to libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts index b919d8e7a7..a178564f4b 100644 --- a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.ts +++ b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts @@ -14,14 +14,14 @@ import { CommonModule } from '@angular/common' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' @Component({ - selector: 'gn-ui-editor-contact-card', - templateUrl: './editor-contact-card.component.html', - styleUrls: ['./editor-contact-card.component.css'], + selector: 'gn-ui-contact-card', + templateUrl: './contact-card.component.html', + styleUrls: ['./contact-card.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule, MatIconModule, ButtonComponent], }) -export class EditorContactCardComponent { +export class ContactCardComponent { @Input() contact: Individual @Input() organization: Organization @Output() contactRemoved = new EventEmitter() diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.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-for-resource/form-field-contacts-for-resource.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html new file mode 100644 index 0000000000..530521aa53 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html @@ -0,0 +1,52 @@ +
+
+ + +
+ + {{ roleToLabel(role) }} +
+
+
+
+
+
+
+ {{ + roleToLabel(role) + }} +
+ + + + + + + +
+
+
+
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.spec.ts new file mode 100644 index 0000000000..680de56f29 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.spec.ts @@ -0,0 +1,181 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { FormFieldContactsForResourceComponent } from './form-field-contacts-for-resource.component' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { BehaviorSubject } from 'rxjs' +import { + Individual, + Organization, +} from '@geonetwork-ui/common/domain/model/record' +import { ChangeDetectorRef, SimpleChanges } 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 { + 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: FormFieldContactsForResourceComponent + let fixture: ComponentFixture + + const contactJohn: Individual = { + firstName: 'John', + lastName: 'Doe', + organization: { name: 'Org1' } as Organization, + email: 'john.doe@example.com', + role: 'author', + } + + const contactJane: Individual = { + firstName: 'John', + lastName: 'Doe', + organization: { name: 'Org1' } as Organization, + email: 'john.doe@example.com', + role: 'custodian', + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + FormFieldContactsForResourceComponent, + CommonModule, + TranslateModule.forRoot(), + UiInputsModule, + ContactCardComponent, + DropdownSelectorComponent, + ], + providers: [ + { + provide: PlatformServiceInterface, + useClass: MockPlatformServiceInterface, + }, + { + provide: OrganizationsServiceInterface, + useClass: MockOrganizationsServiceInterface, + }, + ChangeDetectorRef, + ], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(FormFieldContactsForResourceComponent) + component = fixture.componentInstance + component.control = new FormControl([]) + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should initialize with default values', () => { + expect(component.rolesToPick).toEqual([ + 'resource_provider', + 'custodian', + 'owner', + 'point_of_contact', + 'author', + ]) + expect(component.rolesToDisplay).toEqual([]) + expect(component.allOrganizations.size).toBe(2) + }) + + it('should fetch users on initialization', () => { + const platformService = TestBed.inject(PlatformServiceInterface) + expect(platformService.getUsers).toHaveBeenCalled() + }) + + it('should subscribe to organizations and update the allOrganizations map', () => { + component.ngOnInit() + expect(component.allOrganizations.size).toBe(2) + expect(component.allOrganizations.get('Barbie Inc.')).toEqual( + organizationBarbie + ) + }) + + it('should add role to rolesToDisplay and remove from rolesToPick', () => { + component.addRoleToDisplay('owner') + expect(component.rolesToPick).not.toContain('owner') + expect(component.rolesToDisplay).toContain('owner') + }) + + it('should remove contact by index', () => { + component.control.setValue([ + { firstName: 'John', lastName: 'Doe' } as Individual, + ]) + component.removeContact(0) + expect(component.control.value.length).toBe(0) + }) + + it('should add a new contact with correct data', async () => { + const contact = { + name: 'John', + surname: 'Doe', + organisation: 'Org1', + email: 'john.doe@example.com', + } as UserModel + jest + .spyOn(component, 'getOrganizationByName') + .mockReturnValue({ name: 'Org1' } as Organization) + await component.addContact(contact, 'owner') + expect(component.control.value.length).toBe(1) + expect(component.control.value[0].firstName).toBe('John') + expect(component.control.value[0].lastName).toBe('Doe') + expect(component.control.value[0].organization.name).toBe('Org1') + }) + + it('should handle ngOnChanges and update rolesToDisplay', () => { + const changes = { + control: { + currentValue: { + value: [contactJane, contactJohn], + }, + previousValue: { value: [] }, + firstChange: false, + }, + } as unknown as SimpleChanges + component.ngOnChanges(changes) + expect(component.rolesToDisplay).toEqual([ + contactJane.role, + contactJohn.role, + ]) + }) + + it('should return contacts by role', () => { + const contacts: Individual[] = [ + { role: 'owner' } as Individual, + { role: 'custodian' } as Individual, + { role: 'owner' } as Individual, + ] + component.control.setValue(contacts) + const result = component.getContactsByRole('owner') + expect(result.length).toBe(2) + }) + + it('should return the correct organization by name', () => { + const org = { name: 'Org1' } as Organization + component.allOrganizations.set('Org1', org) + expect(component.getOrganizationByName('Org1')).toBe(org) + }) +}) 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 new file mode 100644 index 0000000000..cf0d9df6b4 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts @@ -0,0 +1,188 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnChanges, + 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, + RoleLabels, +} from '@geonetwork-ui/common/domain/model/record' +import { TranslateModule } from '@ngx-translate/core' +import { debounceTime, distinctUntilChanged, Observable, 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' + +@Component({ + selector: 'gn-ui-form-field-contacts-for-resource', + templateUrl: './form-field-contacts-for-resource.component.html', + styleUrls: ['./form-field-contacts-for-resource.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + DropdownSelectorComponent, + UiInputsModule, + CommonModule, + UiWidgetsModule, + AutocompleteComponent, + TranslateModule, + ContactCardComponent, + ], +}) +export class FormFieldContactsForResourceComponent + implements OnInit, OnChanges +{ + @Input() control: FormControl + + allUser$: Observable + + rolesToPick: string[] = [ + 'resource_provider', + 'custodian', + 'owner', + 'point_of_contact', + 'author', + ] + + rolesToDisplay = [] + + allOrganizations: Map = new Map() + + constructor( + private platformServiceInterface: PlatformServiceInterface, + private organizationsServiceInterface: OrganizationsServiceInterface, + private changeDetectorRef: ChangeDetectorRef + ) { + this.allUser$ = this.platformServiceInterface.getUsers() + } + + ngOnInit(): void { + this.organizationsServiceInterface.organisations$.subscribe( + (organizations) => { + this.allOrganizations = new Map( + organizations.map((organization) => [organization.name, organization]) + ) + this.changeDetectorRef.markForCheck() + } + ) + } + + async ngOnChanges(changes: SimpleChanges): Promise { + const contactsForResource = changes['control'] + + if ( + contactsForResource && + contactsForResource.currentValue !== contactsForResource.previousValue + ) { + const rolesToAdd = contactsForResource.currentValue.value.map( + (contact: Individual) => contact.role + ) + + rolesToAdd.forEach((role: string) => { + if (!this.rolesToDisplay.includes(role)) { + this.rolesToDisplay.push(role) + } + }) + + this.filterRolesToPick() + + this.changeDetectorRef.markForCheck() + } + } + + addRoleToDisplay(roleToAdd: string) { + this.rolesToDisplay.push(roleToAdd) + this.filterRolesToPick() + } + + filterRolesToPick() { + this.rolesToPick = this.rolesToPick.filter( + (role) => !this.rolesToDisplay.includes(role) + ) + } + + removeContact(index: number) { + const newContactsforRessource = this.control.value.filter( + (_, i) => i !== index + ) + this.control.setValue(newContactsforRessource) + } + + protected roleToLabel(role: string): string { + return RoleLabels.get(role) + } + + /** + * gn-ui-autocomplete + */ + displayWithFn: (user: UserModel) => string = (user) => + `${user.name} ${user.surname}` + + /** + * gn-ui-autocomplete + */ + autoCompleteAction = (query: string) => { + return this.allUser$.pipe( + switchMap((users) => [ + users.filter((user) => user.username.includes(query)), + ]), + debounceTime(300), + distinctUntilChanged() + ) + } + + /** + * gn-ui-autocomplete + */ + async addContact(contact: UserModel, role: string) { + let newContactsforRessource = { + firstName: contact.name ?? '', + lastName: contact.surname ?? '', + organization: { + name: contact.organisation, + }, + email: contact.email ?? '', + role, + address: '', + phone: '', + position: '', + } as Individual + + const newContactOrganization = this.getOrganizationByName( + contact.organisation + ) + + newContactsforRessource = { + ...newContactsforRessource, + organization: newContactOrganization, + } + + const newControlValue = [...this.control.value, newContactsforRessource] + + this.control.setValue(newControlValue) + } + + getContactsByRole(role: string): Individual[] { + return this.control.value.filter((contact: Individual) => { + return contact.role === role + }) + } + + getOrganizationByName(name: string): Organization { + return this.allOrganizations.get(name) + } +} 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 f6fa1f9ee2..c3b2b25d6f 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 @@ -87,4 +87,9 @@ [control]="formControl" >
+ + + 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 db576ccb2d..68c84caca4 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 @@ -32,6 +32,7 @@ import { FormFieldOverviewsComponent } from './form-field-overviews/form-field-o import { map, take } from 'rxjs/operators' import { EditorFacade } from '../../../+state/editor.facade' import { FormFieldConfig } from '../../../models' +import { FormFieldContactsForResourceComponent } from './form-field-contacts-for-resource/form-field-contacts-for-resource.component' @Component({ selector: 'gn-ui-form-field', @@ -59,6 +60,7 @@ import { FormFieldConfig } from '../../../models' FormFieldKeywordsComponent, TranslateModule, FormFieldOverviewsComponent, + FormFieldContactsForResourceComponent, ], }) export class FormFieldComponent { @@ -122,6 +124,9 @@ export class FormFieldComponent { get isKeywords() { return this.model === 'keywords' } + get isContactsForResource() { + return this.model === 'contactsForResource' + } get withoutWrapper() { return this.model === 'title' || this.model === 'abstract' diff --git a/libs/feature/editor/src/lib/components/record-form/record-form.component.html b/libs/feature/editor/src/lib/components/record-form/record-form.component.html index 277b79d049..208206241e 100644 --- a/libs/feature/editor/src/lib/components/record-form/record-form.component.html +++ b/libs/feature/editor/src/lib/components/record-form/record-form.component.html @@ -14,7 +14,7 @@ >
{{ section.labelKey }} diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index 1614eb1e05..0f4373ddc6 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -83,6 +83,14 @@ export const RECORD_ABSTRACT_FIELD: EditorField = { }, } +export const CONTACTS_FOR_RESOURCE_FIELD: EditorField = { + model: 'contactsForResource', + formFieldConfig: { + // labelKey: marker('editor.record.form.field.contactsForResource') + labelKey: '', + }, +} + export const RECORD_GRAPHICAL_OVERVIEW_FIELD: EditorField = { model: 'overviews', formFieldConfig: { @@ -157,7 +165,7 @@ export const DATA_MANAGERS_SECTION: EditorSection = { labelKey: marker('editor.record.form.section.dataManagers.label'), descriptionKey: marker('editor.record.form.section.dataManagers.description'), hidden: false, - fields: [], + fields: [CONTACTS_FOR_RESOURCE_FIELD], } export const DATA_POINT_OF_CONTACT_SECTION: EditorSection = { diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index 63200bae90..327b9d5c81 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -4,7 +4,6 @@ export * from './lib/confirmation-dialog/confirmation-dialog.component' export * from './lib/content-ghost/content-ghost.component' export * from './lib/download-item/download-item.component' export * from './lib/downloads-list/downloads-list.component' -export * from './lib/editor-contact-card/editor-contact-card.component' export * from './lib/error/error.component' export * from './lib/image-overlay-preview/image-overlay-preview.component' export * from './lib/link-card/link-card.component' diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.spec.ts b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.spec.ts deleted file mode 100644 index 90e0e6e922..0000000000 --- a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { EditorContactCardComponent } from './editor-contact-card.component' - -describe('LinkCardComponent', () => { - let component: EditorContactCardComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [EditorContactCardComponent], - }).compileComponents() - }) - - beforeEach(() => { - fixture = TestBed.createComponent(EditorContactCardComponent) - component = fixture.componentInstance - component.link = { - name: 'Consulter sur Géoclip', - description: - 'Lorem ipsum dolor sit amet, consect etur adipiscing elit. Donec id condim entum ex. Etiam sed molestie est.', - url: new URL('https://example.com/someurlpath'), - type: 'link', - } - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) -}) diff --git a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.stories.ts b/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.stories.ts deleted file mode 100644 index d4ddcde93d..0000000000 --- a/libs/ui/elements/src/lib/editor-contact-card/editor-contact-card.component.stories.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - applicationConfig, - componentWrapperDecorator, - Meta, - moduleMetadata, - StoryObj, -} from '@storybook/angular' -import { EditorContactCardComponent } from './editor-contact-card.component' -import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { importProvidersFrom } from '@angular/core' - -export default { - title: 'Elements/LinkCardComponent', - component: EditorContactCardComponent, - decorators: [ - moduleMetadata({ - imports: [EditorContactCardComponent], - }), - applicationConfig({ - providers: [importProvidersFrom(BrowserAnimationsModule)], - }), - componentWrapperDecorator( - (story) => `
${story}
` - ), - ], -} as Meta - -export const Primary: StoryObj = { - args: { - compact: false, - link: { - type: 'link', - name: 'Consulter sur Géoclip', - description: - 'Lorem ipsum dolor sit amet, consect etur adipiscing elit. Donec id condim entum ex. Etiam sed molestie est.', - url: new URL('https://example.com/someurlpath'), - }, - }, -} diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index 64b139821d..d99869b046 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -31,7 +31,6 @@ import { MarkdownParserComponent } from './markdown-parser/markdown-parser.compo import { ImageOverlayPreviewComponent } from './image-overlay-preview/image-overlay-preview.component' import { UserFeedbackItemComponent } from './user-feedback-item/user-feedback-item.component' import { TimeSincePipe } from './user-feedback-item/time-since.pipe' -import { EditorContactCardComponent } from './editor-contact-card/editor-contact-card.component' @NgModule({ imports: [ @@ -52,7 +51,6 @@ import { EditorContactCardComponent } from './editor-contact-card/editor-contact TimeSincePipe, BadgeComponent, MaxLinesComponent, - EditorContactCardComponent, ], declarations: [ MetadataInfoComponent, @@ -80,7 +78,6 @@ import { EditorContactCardComponent } from './editor-contact-card/editor-contact ContentGhostComponent, DownloadItemComponent, DownloadsListComponent, - EditorContactCardComponent, ApiCardComponent, RelatedRecordCardComponent, MetadataContactComponent, diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts index 138118521c..847aedbfed 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts @@ -302,7 +302,6 @@ describe('AutocompleteComponent', () => { } describe('when true', () => { beforeEach(() => { - component.clearOnSelection = true component.itemSelected.subscribe((event) => (selectionEmitted = event)) fixture.detectChanges() component.handleSelection(selectionEvent) @@ -313,8 +312,11 @@ describe('AutocompleteComponent', () => { it('emits selection event', () => { expect(selectionEmitted).toEqual('first') }) - describe('if clear on selection', () => { + describe('if completeOnSelection on selection', () => { it('set input value to last entered text', () => { + component.clearOnSelection = false + component.completeOnSelection = true + component.control.setValue('second') component.handleSelection(selectionEvent) expect(component.inputRef.nativeElement.value).toEqual('second') @@ -323,6 +325,16 @@ describe('AutocompleteComponent', () => { expect(component.inputRef.nativeElement.value).toEqual('second') }) }) + describe('if clearOnSelection on selection', () => { + it('set input value to empty string', () => { + component.clearOnSelection = true + component.completeOnSelection = false + + component.control.setValue('second') + component.handleSelection(selectionEvent) + expect(component.inputRef.nativeElement.value).toEqual('') + }) + }) }) }) diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts index 02761e908d..3e5efe0d8a 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts @@ -61,6 +61,7 @@ export class AutocompleteComponent @Input() action: (value: string) => Observable @Input() value?: AutocompleteItem @Input() clearOnSelection = false + @Input() completeOnSelection = false @Input() autoFocus = false @Input() minCharacterCount? = 3 @Input() allowSubmit = true @@ -209,10 +210,14 @@ export class AutocompleteComponent handleSelection(event: MatAutocompleteSelectedEvent) { this.cancelEnter = true this.itemSelected.emit(event.option.value) - if (this.clearOnSelection) { + if (this.completeOnSelection) { this.lastInputValue$.pipe(first()).subscribe((any) => { this.inputRef.nativeElement.value = any }) + return + } + if (this.clearOnSelection) { + this.inputRef.nativeElement.value = '' } } } diff --git a/translations/en.json b/translations/en.json index 526162dc6b..e783fe2a73 100644 --- a/translations/en.json +++ b/translations/en.json @@ -288,6 +288,27 @@ "map.select.layer": "Data source", "map.wfs.urlInput.hint": "Enter WFS service URL", "map.wms.urlInput.hint": "Enter WMS service URL", + "metadata.contactForResource.role.author": "Author", + "metadata.contactForResource.role.collaborator": "Collaborator", + "metadata.contactForResource.role.contributor": "Contributor", + "metadata.contactForResource.role.custodian": "Custodian", + "metadata.contactForResource.role.distributor": "Distributor", + "metadata.contactForResource.role.editor": "Editor", + "metadata.contactForResource.role.funder": "Funder", + "metadata.contactForResource.role.mediator": "Mediator", + "metadata.contactForResource.role.originator": "Originator", + "metadata.contactForResource.role.other": "Other", + "metadata.contactForResource.role.owner": "Owner", + "metadata.contactForResource.role.point_of_contact": "Point of contact", + "metadata.contactForResource.role.principal_investigator": "Principal investigator", + "metadata.contactForResource.role.processor": "Processor", + "metadata.contactForResource.role.publisher": "Publisher", + "metadata.contactForResource.role.resource_provider": "Resource provider", + "metadata.contactForResource.role.rights_holder": "Rights holder", + "metadata.contactForResource.role.sponsor": "Sponsor", + "metadata.contactForResource.role.stakeholder": "Stakeholder", + "metadata.contactForResource.role.unspecified": "Unspecified", + "metadata.contactForResource.role.user": "User", "multiselect.filter.placeholder": "Search", "nav.back": "Back", "next": "next", diff --git a/translations/es.json b/translations/es.json index e5ca041e48..d80e3140e4 100644 --- a/translations/es.json +++ b/translations/es.json @@ -288,6 +288,27 @@ "map.select.layer": "", "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", + "metadata.contactForResource.role.author": "", + "metadata.contactForResource.role.collaborator": "", + "metadata.contactForResource.role.contributor": "", + "metadata.contactForResource.role.custodian": "", + "metadata.contactForResource.role.distributor": "", + "metadata.contactForResource.role.editor": "", + "metadata.contactForResource.role.funder": "", + "metadata.contactForResource.role.mediator": "", + "metadata.contactForResource.role.originator": "", + "metadata.contactForResource.role.other": "", + "metadata.contactForResource.role.owner": "", + "metadata.contactForResource.role.point_of_contact": "", + "metadata.contactForResource.role.principal_investigator": "", + "metadata.contactForResource.role.processor": "", + "metadata.contactForResource.role.publisher": "", + "metadata.contactForResource.role.resource_provider": "", + "metadata.contactForResource.role.rights_holder": "", + "metadata.contactForResource.role.sponsor": "", + "metadata.contactForResource.role.stakeholder": "", + "metadata.contactForResource.role.unspecified": "", + "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "", "nav.back": "", "next": "", diff --git a/translations/fr.json b/translations/fr.json index cf1a8552d8..2be6cb9de4 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -184,7 +184,7 @@ "editor.record.form.field.recordUpdated": "Date de dernière révision", "editor.record.form.field.resourceUpdated": "Date de dernière révision", "editor.record.form.field.temporalExtents": "Étendue temporelle", - "editor.record.form.field.title": "Titre", + "editor.record.form.field.title": "", "editor.record.form.field.uniqueIdentifier": "Identifiant unique", "editor.record.form.field.updateFrequency": "Fréquence de mise à jour", "editor.record.form.license.cc-by": "", @@ -288,6 +288,27 @@ "map.select.layer": "Source de données", "map.wfs.urlInput.hint": "Entrez l'URL du service WFS", "map.wms.urlInput.hint": "Entrez l'URL du service WMS", + "metadata.contactForResource.role.author": "Auteur", + "metadata.contactForResource.role.collaborator": "Collaborateur", + "metadata.contactForResource.role.contributor": "Contributeur", + "metadata.contactForResource.role.custodian": "Gestionnaire", + "metadata.contactForResource.role.distributor": "Distributeur", + "metadata.contactForResource.role.editor": "Éditeur", + "metadata.contactForResource.role.funder": "Financeur", + "metadata.contactForResource.role.mediator": "Médiateur", + "metadata.contactForResource.role.originator": "Créateur", + "metadata.contactForResource.role.other": "Autre", + "metadata.contactForResource.role.owner": "Propriétaire", + "metadata.contactForResource.role.point_of_contact": "Point de contact", + "metadata.contactForResource.role.principal_investigator": "Chercheur principal", + "metadata.contactForResource.role.processor": "Processeur", + "metadata.contactForResource.role.publisher": "Éditeur", + "metadata.contactForResource.role.resource_provider": "Fournisseur", + "metadata.contactForResource.role.rights_holder": "Détenteur des droits", + "metadata.contactForResource.role.sponsor": "Sponsor", + "metadata.contactForResource.role.stakeholder": "Partie prenante", + "metadata.contactForResource.role.unspecified": "Non spécifié", + "metadata.contactForResource.role.user": "Utilisateur", "multiselect.filter.placeholder": "Rechercher", "nav.back": "Retour", "next": "suivant", diff --git a/translations/it.json b/translations/it.json index bee062e3f1..f6134a6b21 100644 --- a/translations/it.json +++ b/translations/it.json @@ -288,6 +288,27 @@ "map.select.layer": "Sorgente dati", "map.wfs.urlInput.hint": "Inserisci URL del servizio WFS", "map.wms.urlInput.hint": "Inserisci URL del servizio WMS", + "metadata.contactForResource.role.author": "", + "metadata.contactForResource.role.collaborator": "", + "metadata.contactForResource.role.contributor": "", + "metadata.contactForResource.role.custodian": "", + "metadata.contactForResource.role.distributor": "", + "metadata.contactForResource.role.editor": "", + "metadata.contactForResource.role.funder": "", + "metadata.contactForResource.role.mediator": "", + "metadata.contactForResource.role.originator": "", + "metadata.contactForResource.role.other": "", + "metadata.contactForResource.role.owner": "", + "metadata.contactForResource.role.point_of_contact": "", + "metadata.contactForResource.role.principal_investigator": "", + "metadata.contactForResource.role.processor": "", + "metadata.contactForResource.role.publisher": "", + "metadata.contactForResource.role.resource_provider": "", + "metadata.contactForResource.role.rights_holder": "", + "metadata.contactForResource.role.sponsor": "", + "metadata.contactForResource.role.stakeholder": "", + "metadata.contactForResource.role.unspecified": "", + "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "Cerca", "nav.back": "Indietro", "next": "successivo", diff --git a/translations/nl.json b/translations/nl.json index 77207ebb78..8529b70957 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -288,6 +288,27 @@ "map.select.layer": "", "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", + "metadata.contactForResource.role.author": "", + "metadata.contactForResource.role.collaborator": "", + "metadata.contactForResource.role.contributor": "", + "metadata.contactForResource.role.custodian": "", + "metadata.contactForResource.role.distributor": "", + "metadata.contactForResource.role.editor": "", + "metadata.contactForResource.role.funder": "", + "metadata.contactForResource.role.mediator": "", + "metadata.contactForResource.role.originator": "", + "metadata.contactForResource.role.other": "", + "metadata.contactForResource.role.owner": "", + "metadata.contactForResource.role.point_of_contact": "", + "metadata.contactForResource.role.principal_investigator": "", + "metadata.contactForResource.role.processor": "", + "metadata.contactForResource.role.publisher": "", + "metadata.contactForResource.role.resource_provider": "", + "metadata.contactForResource.role.rights_holder": "", + "metadata.contactForResource.role.sponsor": "", + "metadata.contactForResource.role.stakeholder": "", + "metadata.contactForResource.role.unspecified": "", + "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "", "nav.back": "", "next": "", diff --git a/translations/pt.json b/translations/pt.json index d0307afc9e..feab7216dc 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -288,6 +288,27 @@ "map.select.layer": "", "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", + "metadata.contactForResource.role.author": "", + "metadata.contactForResource.role.collaborator": "", + "metadata.contactForResource.role.contributor": "", + "metadata.contactForResource.role.custodian": "", + "metadata.contactForResource.role.distributor": "", + "metadata.contactForResource.role.editor": "", + "metadata.contactForResource.role.funder": "", + "metadata.contactForResource.role.mediator": "", + "metadata.contactForResource.role.originator": "", + "metadata.contactForResource.role.other": "", + "metadata.contactForResource.role.owner": "", + "metadata.contactForResource.role.point_of_contact": "", + "metadata.contactForResource.role.principal_investigator": "", + "metadata.contactForResource.role.processor": "", + "metadata.contactForResource.role.publisher": "", + "metadata.contactForResource.role.resource_provider": "", + "metadata.contactForResource.role.rights_holder": "", + "metadata.contactForResource.role.sponsor": "", + "metadata.contactForResource.role.stakeholder": "", + "metadata.contactForResource.role.unspecified": "", + "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "", "nav.back": "", "next": "", diff --git a/translations/sk.json b/translations/sk.json index 7abb992c59..fe18faaab6 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -288,6 +288,27 @@ "map.select.layer": "Zdroj dát", "map.wfs.urlInput.hint": "Zadajte URL adresu služby WFS", "map.wms.urlInput.hint": "Zadajte URL adresu služby WMS", + "metadata.contactForResource.role.author": "", + "metadata.contactForResource.role.collaborator": "", + "metadata.contactForResource.role.contributor": "", + "metadata.contactForResource.role.custodian": "", + "metadata.contactForResource.role.distributor": "", + "metadata.contactForResource.role.editor": "", + "metadata.contactForResource.role.funder": "", + "metadata.contactForResource.role.mediator": "", + "metadata.contactForResource.role.originator": "", + "metadata.contactForResource.role.other": "", + "metadata.contactForResource.role.owner": "", + "metadata.contactForResource.role.point_of_contact": "", + "metadata.contactForResource.role.principal_investigator": "", + "metadata.contactForResource.role.processor": "", + "metadata.contactForResource.role.publisher": "", + "metadata.contactForResource.role.resource_provider": "", + "metadata.contactForResource.role.rights_holder": "", + "metadata.contactForResource.role.sponsor": "", + "metadata.contactForResource.role.stakeholder": "", + "metadata.contactForResource.role.unspecified": "", + "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "Hľadať", "nav.back": "Späť", "next": "Ďalej", From d5256dd4581538a3457e966a767486ea505eb868 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Fri, 2 Aug 2024 11:46:15 +0200 Subject: [PATCH 3/5] wip --- .../contact-card/contact-card.component.html | 40 ++++++++------- .../contact-card/contact-card.component.ts | 1 + ...field-contacts-for-resource.component.html | 30 ++++++++--- ...m-field-contacts-for-resource.component.ts | 50 +++++++++++++++---- libs/feature/editor/src/lib/fields.config.ts | 1 - .../sortable-list/sortable-list.component.ts | 4 +- 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 + 14 files changed, 97 insertions(+), 37 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 849b22ef30..511e194a22 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,22 +1,28 @@ -
- -
-
- {{ contact.firstName }} {{ contact.lastName }} - - +
+
+ +
+
+ {{ contact.firstName }} {{ contact.lastName }} +
+
{{ contact.email }}
-
{{ contact.email }}
+ close +
diff --git a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts index a178564f4b..494bed0ad9 100644 --- a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts +++ b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts @@ -24,6 +24,7 @@ import { ButtonComponent } from '@geonetwork-ui/ui/inputs' export class ContactCardComponent { @Input() contact: Individual @Input() organization: Organization + @Input() removable = true @Output() contactRemoved = new EventEmitter() removeContact(contact: Individual) { diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html index 530521aa53..291b27bba4 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html @@ -16,7 +16,7 @@
- - - + + + + +
+ +
+ editor.record.form.field.contactsForResource.noContact +
+
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 cf0d9df6b4..bfd2fdccf8 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 @@ -7,6 +7,7 @@ import { OnChanges, OnInit, SimpleChanges, + Type, } from '@angular/core' import { FormControl } from '@angular/forms' import { @@ -26,6 +27,10 @@ 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' @Component({ selector: 'gn-ui-form-field-contacts-for-resource', @@ -41,6 +46,7 @@ import { ContactCardComponent } from '../../../contact-card/contact-card.compone AutocompleteComponent, TranslateModule, ContactCardComponent, + SortableListComponent, ], }) export class FormFieldContactsForResourceComponent @@ -48,7 +54,7 @@ export class FormFieldContactsForResourceComponent { @Input() control: FormControl - allUser$: Observable + allUsers$: Observable rolesToPick: string[] = [ 'resource_provider', @@ -62,12 +68,14 @@ export class FormFieldContactsForResourceComponent allOrganizations: Map = new Map() + addOptions: Array<{ buttonLabel: string; eventName: string }> = [] + constructor( private platformServiceInterface: PlatformServiceInterface, private organizationsServiceInterface: OrganizationsServiceInterface, private changeDetectorRef: ChangeDetectorRef ) { - this.allUser$ = this.platformServiceInterface.getUsers() + this.allUsers$ = this.platformServiceInterface.getUsers() } ngOnInit(): void { @@ -81,7 +89,7 @@ export class FormFieldContactsForResourceComponent ) } - async ngOnChanges(changes: SimpleChanges): Promise { + ngOnChanges(changes: SimpleChanges) { const contactsForResource = changes['control'] if ( @@ -92,6 +100,8 @@ export class FormFieldContactsForResourceComponent (contact: Individual) => contact.role ) + console.log(contactsForResource.currentValue) + rolesToAdd.forEach((role: string) => { if (!this.rolesToDisplay.includes(role)) { this.rolesToDisplay.push(role) @@ -136,7 +146,7 @@ export class FormFieldContactsForResourceComponent * gn-ui-autocomplete */ autoCompleteAction = (query: string) => { - return this.allUser$.pipe( + return this.allUsers$.pipe( switchMap((users) => [ users.filter((user) => user.username.includes(query)), ]), @@ -148,8 +158,8 @@ export class FormFieldContactsForResourceComponent /** * gn-ui-autocomplete */ - async addContact(contact: UserModel, role: string) { - let newContactsforRessource = { + addContact(contact: UserModel, role: string) { + let newContactsForRessource = { firstName: contact.name ?? '', lastName: contact.surname ?? '', organization: { @@ -166,12 +176,12 @@ export class FormFieldContactsForResourceComponent contact.organisation ) - newContactsforRessource = { - ...newContactsforRessource, + newContactsForRessource = { + ...newContactsForRessource, organization: newContactOrganization, } - const newControlValue = [...this.control.value, newContactsforRessource] + const newControlValue = [...this.control.value, newContactsForRessource] this.control.setValue(newControlValue) } @@ -185,4 +195,26 @@ export class FormFieldContactsForResourceComponent getOrganizationByName(name: string): Organization { return this.allOrganizations.get(name) } + + getContactByRoleForSortableList(role: string): Array { + return this.control.value + .filter((contact: Individual) => { + return contact.role === role + }) + .map((contact) => ({ + component: ContactCardComponent, + inputs: { + contact, + organization: contact.organization, + removable: false, + }, + })) as Array<{ + component: Type + inputs: Record + }> + } + + handleContactOrderChange(event) { + console.log(event) + } } diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index 0f4373ddc6..549506ba31 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -86,7 +86,6 @@ export const RECORD_ABSTRACT_FIELD: EditorField = { export const CONTACTS_FOR_RESOURCE_FIELD: EditorField = { model: 'contactsForResource', formFieldConfig: { - // labelKey: marker('editor.record.form.field.contactsForResource') labelKey: '', }, } diff --git a/libs/ui/elements/src/lib/sortable-list/sortable-list.component.ts b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.ts index 11acd5c480..fd318e1b68 100644 --- a/libs/ui/elements/src/lib/sortable-list/sortable-list.component.ts +++ b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.ts @@ -17,7 +17,7 @@ import { import { MatIconModule } from '@angular/material/icon' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -type DynamicElement = { +export type DynamicElement = { component: Type inputs: Record } @@ -40,7 +40,7 @@ type DynamicElement = { }) export class SortableListComponent { @Input() elements: Array - @Input() addOptions: Array<{ buttonLabel: string; eventName: string }> + @Input() addOptions: Array<{ buttonLabel: string; eventName: string }> = [] @Output() elementsChange = new EventEmitter>() @Output() add = new EventEmitter() diff --git a/translations/de.json b/translations/de.json index 680ebf6d45..f90b00a749 100644 --- a/translations/de.json +++ b/translations/de.json @@ -178,6 +178,7 @@ "editor.record.form.bottomButtons.next": "", "editor.record.form.bottomButtons.previous": "", "editor.record.form.field.abstract": "Kurzbeschreibung", + "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "Schlagwörter", "editor.record.form.field.license": "Lizenz", "editor.record.form.field.overviews": "", diff --git a/translations/en.json b/translations/en.json index e783fe2a73..9fa5e5364b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -178,6 +178,7 @@ "editor.record.form.bottomButtons.next": "Next", "editor.record.form.bottomButtons.previous": "Previous", "editor.record.form.field.abstract": "Abstract", + "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", "editor.record.form.field.overviews": "Overviews", diff --git a/translations/es.json b/translations/es.json index d80e3140e4..1a54e66f8e 100644 --- a/translations/es.json +++ b/translations/es.json @@ -178,6 +178,7 @@ "editor.record.form.bottomButtons.next": "", "editor.record.form.bottomButtons.previous": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "", "editor.record.form.field.overviews": "", diff --git a/translations/fr.json b/translations/fr.json index 2be6cb9de4..7f9e909f2f 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -178,6 +178,7 @@ "editor.record.form.bottomButtons.next": "Suivant", "editor.record.form.bottomButtons.previous": "Précédent", "editor.record.form.field.abstract": "Résumé", + "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", "editor.record.form.field.overviews": "Aperçus", diff --git a/translations/it.json b/translations/it.json index f6134a6b21..7649a843e9 100644 --- a/translations/it.json +++ b/translations/it.json @@ -178,6 +178,7 @@ "editor.record.form.bottomButtons.next": "", "editor.record.form.bottomButtons.previous": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "Licenza", "editor.record.form.field.overviews": "", diff --git a/translations/nl.json b/translations/nl.json index 8529b70957..49bba6bfed 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -178,6 +178,7 @@ "editor.record.form.bottomButtons.next": "", "editor.record.form.bottomButtons.previous": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "", "editor.record.form.field.overviews": "", diff --git a/translations/pt.json b/translations/pt.json index feab7216dc..9a6b7a257b 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -178,6 +178,7 @@ "editor.record.form.bottomButtons.next": "", "editor.record.form.bottomButtons.previous": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "", "editor.record.form.field.overviews": "", diff --git a/translations/sk.json b/translations/sk.json index fe18faaab6..5935c903af 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -178,6 +178,7 @@ "editor.record.form.bottomButtons.next": "", "editor.record.form.bottomButtons.previous": "", "editor.record.form.field.abstract": "", + "editor.record.form.field.contactsForResource.noContact": "", "editor.record.form.field.keywords": "", "editor.record.form.field.license": "Licencia", "editor.record.form.field.overviews": "", From 6bfecee9bf35da7e9b41de3039173f2ddb69b544 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Tue, 20 Aug 2024 08:52:14 +0200 Subject: [PATCH 4/5] fix code review --- .../src/lib/model/record/contact.model.ts | 50 ++++++++----------- translations/de.json | 21 ++++++++ translations/en.json | 42 ++++++++-------- translations/es.json | 42 ++++++++-------- translations/fr.json | 42 ++++++++-------- translations/it.json | 42 ++++++++-------- translations/nl.json | 42 ++++++++-------- translations/pt.json | 42 ++++++++-------- translations/sk.json | 42 ++++++++-------- 9 files changed, 190 insertions(+), 175 deletions(-) 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 3372fb603c..0be5481e4d 100644 --- a/libs/common/domain/src/lib/model/record/contact.model.ts +++ b/libs/common/domain/src/lib/model/record/contact.model.ts @@ -25,37 +25,31 @@ export const RoleValues = [ 'user', // Party who uses the resource ] -export const RoleLabels = new Map([ - ['unspecified', marker('metadata.contactForResource.role.unspecified')], - ['other', marker('metadata.contactForResource.role.other')], - ['author', marker('metadata.contactForResource.role.author')], - ['collaborator', marker('metadata.contactForResource.role.collaborator')], - ['contributor', marker('metadata.contactForResource.role.contributor')], - ['custodian', marker('metadata.contactForResource.role.custodian')], - ['distributor', marker('metadata.contactForResource.role.distributor')], - ['editor', marker('metadata.contactForResource.role.editor')], - ['funder', marker('metadata.contactForResource.role.funder')], - ['mediator', marker('metadata.contactForResource.role.mediator')], - ['originator', marker('metadata.contactForResource.role.originator')], - ['owner', marker('metadata.contactForResource.role.owner')], - [ - 'point_of_contact', - marker('metadata.contactForResource.role.point_of_contact'), - ], +export const RoleLabels = new Map([ + ['unspecified', marker('domain.contact.role.unspecified')], + ['other', marker('domain.contact.role.other')], + ['author', marker('domain.contact.role.author')], + ['collaborator', marker('domain.contact.role.collaborator')], + ['contributor', marker('domain.contact.role.contributor')], + ['custodian', marker('domain.contact.role.custodian')], + ['distributor', marker('domain.contact.role.distributor')], + ['editor', marker('domain.contact.role.editor')], + ['funder', marker('domain.contact.role.funder')], + ['mediator', marker('domain.contact.role.mediator')], + ['originator', marker('domain.contact.role.originator')], + ['owner', marker('domain.contact.role.owner')], + ['point_of_contact', marker('domain.contact.role.point_of_contact')], [ 'principal_investigator', - marker('metadata.contactForResource.role.principal_investigator'), - ], - ['processor', marker('metadata.contactForResource.role.processor')], - ['publisher', marker('metadata.contactForResource.role.publisher')], - [ - 'resource_provider', - marker('metadata.contactForResource.role.resource_provider'), + marker('domain.contact.role.principal_investigator'), ], - ['rights_holder', marker('metadata.contactForResource.role.rights_holder')], - ['sponsor', marker('metadata.contactForResource.role.sponsor')], - ['stakeholder', marker('metadata.contactForResource.role.stakeholder')], - ['user', marker('metadata.contactForResource.role.user')], + ['processor', marker('domain.contact.role.processor')], + ['publisher', marker('domain.contact.role.publisher')], + ['resource_provider', marker('domain.contact.role.resource_provider')], + ['rights_holder', marker('domain.contact.role.rights_holder')], + ['sponsor', marker('domain.contact.role.sponsor')], + ['stakeholder', marker('domain.contact.role.stakeholder')], + ['user', marker('domain.contact.role.user')], ]) export type Role = typeof RoleValues[number] diff --git a/translations/de.json b/translations/de.json index f90b00a749..9fbb918c90 100644 --- a/translations/de.json +++ b/translations/de.json @@ -147,6 +147,27 @@ "dataset.error.parse": "Die Daten wurden geladen, konnten aber nicht gelesen werden: \"{ info }\"", "dataset.error.unknown": "Die Daten können nicht angezeigt werden: \"{ info }\"", "dataset.error.unsupportedType": "Der folgende Inhaltstyp wird nicht unterstützt: \"{ info }\"", + "domain.contact.role.author": "", + "domain.contact.role.collaborator": "", + "domain.contact.role.contributor": "", + "domain.contact.role.custodian": "", + "domain.contact.role.distributor": "", + "domain.contact.role.editor": "", + "domain.contact.role.funder": "", + "domain.contact.role.mediator": "", + "domain.contact.role.originator": "", + "domain.contact.role.other": "", + "domain.contact.role.owner": "", + "domain.contact.role.point_of_contact": "", + "domain.contact.role.principal_investigator": "", + "domain.contact.role.processor": "", + "domain.contact.role.publisher": "", + "domain.contact.role.resource_provider": "", + "domain.contact.role.rights_holder": "", + "domain.contact.role.sponsor": "", + "domain.contact.role.stakeholder": "", + "domain.contact.role.unspecified": "", + "domain.contact.role.user": "", "domain.record.status.completed": "Abgeschlossen", "domain.record.status.deprecated": "Veraltet", "domain.record.status.ongoing": "Kontinuierliche Aktualisierung", diff --git a/translations/en.json b/translations/en.json index 9fa5e5364b..658432cba6 100644 --- a/translations/en.json +++ b/translations/en.json @@ -147,6 +147,27 @@ "dataset.error.parse": "The data was loaded but could not be parsed: \"{ info }\"", "dataset.error.unknown": "The data cannot be displayed: \"{ info }\"", "dataset.error.unsupportedType": "The following content type is unsupported: \"{ info }\"", + "domain.contact.role.author": "Author", + "domain.contact.role.collaborator": "Collaborator", + "domain.contact.role.contributor": "Contributor", + "domain.contact.role.custodian": "Custodian", + "domain.contact.role.distributor": "Distributor", + "domain.contact.role.editor": "Editor", + "domain.contact.role.funder": "Funder", + "domain.contact.role.mediator": "Mediator", + "domain.contact.role.originator": "Originator", + "domain.contact.role.other": "Other", + "domain.contact.role.owner": "Owner", + "domain.contact.role.point_of_contact": "Point of contact", + "domain.contact.role.principal_investigator": "Principal investigator", + "domain.contact.role.processor": "Processor", + "domain.contact.role.publisher": "Publisher", + "domain.contact.role.resource_provider": "Resource provider", + "domain.contact.role.rights_holder": "Rights holder", + "domain.contact.role.sponsor": "Sponsor", + "domain.contact.role.stakeholder": "Stakeholder", + "domain.contact.role.unspecified": "Unspecified", + "domain.contact.role.user": "User", "domain.record.status.completed": "Completed", "domain.record.status.deprecated": "Deprecated", "domain.record.status.ongoing": "On going", @@ -289,27 +310,6 @@ "map.select.layer": "Data source", "map.wfs.urlInput.hint": "Enter WFS service URL", "map.wms.urlInput.hint": "Enter WMS service URL", - "metadata.contactForResource.role.author": "Author", - "metadata.contactForResource.role.collaborator": "Collaborator", - "metadata.contactForResource.role.contributor": "Contributor", - "metadata.contactForResource.role.custodian": "Custodian", - "metadata.contactForResource.role.distributor": "Distributor", - "metadata.contactForResource.role.editor": "Editor", - "metadata.contactForResource.role.funder": "Funder", - "metadata.contactForResource.role.mediator": "Mediator", - "metadata.contactForResource.role.originator": "Originator", - "metadata.contactForResource.role.other": "Other", - "metadata.contactForResource.role.owner": "Owner", - "metadata.contactForResource.role.point_of_contact": "Point of contact", - "metadata.contactForResource.role.principal_investigator": "Principal investigator", - "metadata.contactForResource.role.processor": "Processor", - "metadata.contactForResource.role.publisher": "Publisher", - "metadata.contactForResource.role.resource_provider": "Resource provider", - "metadata.contactForResource.role.rights_holder": "Rights holder", - "metadata.contactForResource.role.sponsor": "Sponsor", - "metadata.contactForResource.role.stakeholder": "Stakeholder", - "metadata.contactForResource.role.unspecified": "Unspecified", - "metadata.contactForResource.role.user": "User", "multiselect.filter.placeholder": "Search", "nav.back": "Back", "next": "next", diff --git a/translations/es.json b/translations/es.json index 1a54e66f8e..3a20085291 100644 --- a/translations/es.json +++ b/translations/es.json @@ -147,6 +147,27 @@ "dataset.error.parse": "", "dataset.error.unknown": "", "dataset.error.unsupportedType": "", + "domain.contact.role.author": "", + "domain.contact.role.collaborator": "", + "domain.contact.role.contributor": "", + "domain.contact.role.custodian": "", + "domain.contact.role.distributor": "", + "domain.contact.role.editor": "", + "domain.contact.role.funder": "", + "domain.contact.role.mediator": "", + "domain.contact.role.originator": "", + "domain.contact.role.other": "", + "domain.contact.role.owner": "", + "domain.contact.role.point_of_contact": "", + "domain.contact.role.principal_investigator": "", + "domain.contact.role.processor": "", + "domain.contact.role.publisher": "", + "domain.contact.role.resource_provider": "", + "domain.contact.role.rights_holder": "", + "domain.contact.role.sponsor": "", + "domain.contact.role.stakeholder": "", + "domain.contact.role.unspecified": "", + "domain.contact.role.user": "", "domain.record.status.completed": "", "domain.record.status.deprecated": "", "domain.record.status.ongoing": "", @@ -289,27 +310,6 @@ "map.select.layer": "", "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", - "metadata.contactForResource.role.author": "", - "metadata.contactForResource.role.collaborator": "", - "metadata.contactForResource.role.contributor": "", - "metadata.contactForResource.role.custodian": "", - "metadata.contactForResource.role.distributor": "", - "metadata.contactForResource.role.editor": "", - "metadata.contactForResource.role.funder": "", - "metadata.contactForResource.role.mediator": "", - "metadata.contactForResource.role.originator": "", - "metadata.contactForResource.role.other": "", - "metadata.contactForResource.role.owner": "", - "metadata.contactForResource.role.point_of_contact": "", - "metadata.contactForResource.role.principal_investigator": "", - "metadata.contactForResource.role.processor": "", - "metadata.contactForResource.role.publisher": "", - "metadata.contactForResource.role.resource_provider": "", - "metadata.contactForResource.role.rights_holder": "", - "metadata.contactForResource.role.sponsor": "", - "metadata.contactForResource.role.stakeholder": "", - "metadata.contactForResource.role.unspecified": "", - "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "", "nav.back": "", "next": "", diff --git a/translations/fr.json b/translations/fr.json index 7f9e909f2f..bfb59572ed 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -147,6 +147,27 @@ "dataset.error.parse": "Les données ont été chargées mais leur décodage a échoué: \"{ info }\"", "dataset.error.unknown": "Les données ne peuvent être affichées: \"{ info }\"", "dataset.error.unsupportedType": "Le type de contenu suivant n'est pas pris en charge: \"{ info }\"", + "domain.contact.role.author": "Auteur", + "domain.contact.role.collaborator": "Collaborateur", + "domain.contact.role.contributor": "Contributeur", + "domain.contact.role.custodian": "Gestionnaire", + "domain.contact.role.distributor": "Distributeur", + "domain.contact.role.editor": "Éditeur", + "domain.contact.role.funder": "Financeur", + "domain.contact.role.mediator": "Médiateur", + "domain.contact.role.originator": "Créateur", + "domain.contact.role.other": "Autre", + "domain.contact.role.owner": "Propriétaire", + "domain.contact.role.point_of_contact": "Point de contact", + "domain.contact.role.principal_investigator": "Chercheur principal", + "domain.contact.role.processor": "Processeur", + "domain.contact.role.publisher": "Éditeur", + "domain.contact.role.resource_provider": "Fournisseur", + "domain.contact.role.rights_holder": "Détenteur des droits", + "domain.contact.role.sponsor": "Sponsor", + "domain.contact.role.stakeholder": "Partie prenante", + "domain.contact.role.unspecified": "Non spécifié", + "domain.contact.role.user": "Utilisateur", "domain.record.status.completed": "Finalisé", "domain.record.status.deprecated": "Obsolète", "domain.record.status.ongoing": "Mise à jour continue", @@ -289,27 +310,6 @@ "map.select.layer": "Source de données", "map.wfs.urlInput.hint": "Entrez l'URL du service WFS", "map.wms.urlInput.hint": "Entrez l'URL du service WMS", - "metadata.contactForResource.role.author": "Auteur", - "metadata.contactForResource.role.collaborator": "Collaborateur", - "metadata.contactForResource.role.contributor": "Contributeur", - "metadata.contactForResource.role.custodian": "Gestionnaire", - "metadata.contactForResource.role.distributor": "Distributeur", - "metadata.contactForResource.role.editor": "Éditeur", - "metadata.contactForResource.role.funder": "Financeur", - "metadata.contactForResource.role.mediator": "Médiateur", - "metadata.contactForResource.role.originator": "Créateur", - "metadata.contactForResource.role.other": "Autre", - "metadata.contactForResource.role.owner": "Propriétaire", - "metadata.contactForResource.role.point_of_contact": "Point de contact", - "metadata.contactForResource.role.principal_investigator": "Chercheur principal", - "metadata.contactForResource.role.processor": "Processeur", - "metadata.contactForResource.role.publisher": "Éditeur", - "metadata.contactForResource.role.resource_provider": "Fournisseur", - "metadata.contactForResource.role.rights_holder": "Détenteur des droits", - "metadata.contactForResource.role.sponsor": "Sponsor", - "metadata.contactForResource.role.stakeholder": "Partie prenante", - "metadata.contactForResource.role.unspecified": "Non spécifié", - "metadata.contactForResource.role.user": "Utilisateur", "multiselect.filter.placeholder": "Rechercher", "nav.back": "Retour", "next": "suivant", diff --git a/translations/it.json b/translations/it.json index 7649a843e9..d48108d20b 100644 --- a/translations/it.json +++ b/translations/it.json @@ -147,6 +147,27 @@ "dataset.error.parse": "I dati sono stati caricati ma la decodifica non è riuscita: \"{info}\"", "dataset.error.unknown": "Impossibile visualizzare i dati: \"{info}\"", "dataset.error.unsupportedType": "Il seguente tipo di contenuto non è supportato: \"{info}\"", + "domain.contact.role.author": "", + "domain.contact.role.collaborator": "", + "domain.contact.role.contributor": "", + "domain.contact.role.custodian": "", + "domain.contact.role.distributor": "", + "domain.contact.role.editor": "", + "domain.contact.role.funder": "", + "domain.contact.role.mediator": "", + "domain.contact.role.originator": "", + "domain.contact.role.other": "", + "domain.contact.role.owner": "", + "domain.contact.role.point_of_contact": "", + "domain.contact.role.principal_investigator": "", + "domain.contact.role.processor": "", + "domain.contact.role.publisher": "", + "domain.contact.role.resource_provider": "", + "domain.contact.role.rights_holder": "", + "domain.contact.role.sponsor": "", + "domain.contact.role.stakeholder": "", + "domain.contact.role.unspecified": "", + "domain.contact.role.user": "", "domain.record.status.completed": "Completato", "domain.record.status.deprecated": "Deprecato", "domain.record.status.ongoing": "Aggiornamento continuo", @@ -289,27 +310,6 @@ "map.select.layer": "Sorgente dati", "map.wfs.urlInput.hint": "Inserisci URL del servizio WFS", "map.wms.urlInput.hint": "Inserisci URL del servizio WMS", - "metadata.contactForResource.role.author": "", - "metadata.contactForResource.role.collaborator": "", - "metadata.contactForResource.role.contributor": "", - "metadata.contactForResource.role.custodian": "", - "metadata.contactForResource.role.distributor": "", - "metadata.contactForResource.role.editor": "", - "metadata.contactForResource.role.funder": "", - "metadata.contactForResource.role.mediator": "", - "metadata.contactForResource.role.originator": "", - "metadata.contactForResource.role.other": "", - "metadata.contactForResource.role.owner": "", - "metadata.contactForResource.role.point_of_contact": "", - "metadata.contactForResource.role.principal_investigator": "", - "metadata.contactForResource.role.processor": "", - "metadata.contactForResource.role.publisher": "", - "metadata.contactForResource.role.resource_provider": "", - "metadata.contactForResource.role.rights_holder": "", - "metadata.contactForResource.role.sponsor": "", - "metadata.contactForResource.role.stakeholder": "", - "metadata.contactForResource.role.unspecified": "", - "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "Cerca", "nav.back": "Indietro", "next": "successivo", diff --git a/translations/nl.json b/translations/nl.json index 49bba6bfed..d17a7136c7 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -147,6 +147,27 @@ "dataset.error.parse": "", "dataset.error.unknown": "", "dataset.error.unsupportedType": "", + "domain.contact.role.author": "", + "domain.contact.role.collaborator": "", + "domain.contact.role.contributor": "", + "domain.contact.role.custodian": "", + "domain.contact.role.distributor": "", + "domain.contact.role.editor": "", + "domain.contact.role.funder": "", + "domain.contact.role.mediator": "", + "domain.contact.role.originator": "", + "domain.contact.role.other": "", + "domain.contact.role.owner": "", + "domain.contact.role.point_of_contact": "", + "domain.contact.role.principal_investigator": "", + "domain.contact.role.processor": "", + "domain.contact.role.publisher": "", + "domain.contact.role.resource_provider": "", + "domain.contact.role.rights_holder": "", + "domain.contact.role.sponsor": "", + "domain.contact.role.stakeholder": "", + "domain.contact.role.unspecified": "", + "domain.contact.role.user": "", "domain.record.status.completed": "", "domain.record.status.deprecated": "", "domain.record.status.ongoing": "", @@ -289,27 +310,6 @@ "map.select.layer": "", "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", - "metadata.contactForResource.role.author": "", - "metadata.contactForResource.role.collaborator": "", - "metadata.contactForResource.role.contributor": "", - "metadata.contactForResource.role.custodian": "", - "metadata.contactForResource.role.distributor": "", - "metadata.contactForResource.role.editor": "", - "metadata.contactForResource.role.funder": "", - "metadata.contactForResource.role.mediator": "", - "metadata.contactForResource.role.originator": "", - "metadata.contactForResource.role.other": "", - "metadata.contactForResource.role.owner": "", - "metadata.contactForResource.role.point_of_contact": "", - "metadata.contactForResource.role.principal_investigator": "", - "metadata.contactForResource.role.processor": "", - "metadata.contactForResource.role.publisher": "", - "metadata.contactForResource.role.resource_provider": "", - "metadata.contactForResource.role.rights_holder": "", - "metadata.contactForResource.role.sponsor": "", - "metadata.contactForResource.role.stakeholder": "", - "metadata.contactForResource.role.unspecified": "", - "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "", "nav.back": "", "next": "", diff --git a/translations/pt.json b/translations/pt.json index 9a6b7a257b..652b6adacf 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -147,6 +147,27 @@ "dataset.error.parse": "", "dataset.error.unknown": "", "dataset.error.unsupportedType": "", + "domain.contact.role.author": "", + "domain.contact.role.collaborator": "", + "domain.contact.role.contributor": "", + "domain.contact.role.custodian": "", + "domain.contact.role.distributor": "", + "domain.contact.role.editor": "", + "domain.contact.role.funder": "", + "domain.contact.role.mediator": "", + "domain.contact.role.originator": "", + "domain.contact.role.other": "", + "domain.contact.role.owner": "", + "domain.contact.role.point_of_contact": "", + "domain.contact.role.principal_investigator": "", + "domain.contact.role.processor": "", + "domain.contact.role.publisher": "", + "domain.contact.role.resource_provider": "", + "domain.contact.role.rights_holder": "", + "domain.contact.role.sponsor": "", + "domain.contact.role.stakeholder": "", + "domain.contact.role.unspecified": "", + "domain.contact.role.user": "", "domain.record.status.completed": "", "domain.record.status.deprecated": "", "domain.record.status.ongoing": "", @@ -289,27 +310,6 @@ "map.select.layer": "", "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", - "metadata.contactForResource.role.author": "", - "metadata.contactForResource.role.collaborator": "", - "metadata.contactForResource.role.contributor": "", - "metadata.contactForResource.role.custodian": "", - "metadata.contactForResource.role.distributor": "", - "metadata.contactForResource.role.editor": "", - "metadata.contactForResource.role.funder": "", - "metadata.contactForResource.role.mediator": "", - "metadata.contactForResource.role.originator": "", - "metadata.contactForResource.role.other": "", - "metadata.contactForResource.role.owner": "", - "metadata.contactForResource.role.point_of_contact": "", - "metadata.contactForResource.role.principal_investigator": "", - "metadata.contactForResource.role.processor": "", - "metadata.contactForResource.role.publisher": "", - "metadata.contactForResource.role.resource_provider": "", - "metadata.contactForResource.role.rights_holder": "", - "metadata.contactForResource.role.sponsor": "", - "metadata.contactForResource.role.stakeholder": "", - "metadata.contactForResource.role.unspecified": "", - "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "", "nav.back": "", "next": "", diff --git a/translations/sk.json b/translations/sk.json index 5935c903af..60da63833e 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -147,6 +147,27 @@ "dataset.error.parse": "Dáta boli načítané, ale nedajú sa analyzovať: \"{ info }\"", "dataset.error.unknown": "Dáta nie je možné zobraziť: \"{ info }\"", "dataset.error.unsupportedType": "Nepodporovaný typ obsahu: \"{ info }\"", + "domain.contact.role.author": "", + "domain.contact.role.collaborator": "", + "domain.contact.role.contributor": "", + "domain.contact.role.custodian": "", + "domain.contact.role.distributor": "", + "domain.contact.role.editor": "", + "domain.contact.role.funder": "", + "domain.contact.role.mediator": "", + "domain.contact.role.originator": "", + "domain.contact.role.other": "", + "domain.contact.role.owner": "", + "domain.contact.role.point_of_contact": "", + "domain.contact.role.principal_investigator": "", + "domain.contact.role.processor": "", + "domain.contact.role.publisher": "", + "domain.contact.role.resource_provider": "", + "domain.contact.role.rights_holder": "", + "domain.contact.role.sponsor": "", + "domain.contact.role.stakeholder": "", + "domain.contact.role.unspecified": "", + "domain.contact.role.user": "", "domain.record.status.completed": "Dokončené", "domain.record.status.deprecated": "Zastarané", "domain.record.status.ongoing": "Prebiehajúce", @@ -289,27 +310,6 @@ "map.select.layer": "Zdroj dát", "map.wfs.urlInput.hint": "Zadajte URL adresu služby WFS", "map.wms.urlInput.hint": "Zadajte URL adresu služby WMS", - "metadata.contactForResource.role.author": "", - "metadata.contactForResource.role.collaborator": "", - "metadata.contactForResource.role.contributor": "", - "metadata.contactForResource.role.custodian": "", - "metadata.contactForResource.role.distributor": "", - "metadata.contactForResource.role.editor": "", - "metadata.contactForResource.role.funder": "", - "metadata.contactForResource.role.mediator": "", - "metadata.contactForResource.role.originator": "", - "metadata.contactForResource.role.other": "", - "metadata.contactForResource.role.owner": "", - "metadata.contactForResource.role.point_of_contact": "", - "metadata.contactForResource.role.principal_investigator": "", - "metadata.contactForResource.role.processor": "", - "metadata.contactForResource.role.publisher": "", - "metadata.contactForResource.role.resource_provider": "", - "metadata.contactForResource.role.rights_holder": "", - "metadata.contactForResource.role.sponsor": "", - "metadata.contactForResource.role.stakeholder": "", - "metadata.contactForResource.role.unspecified": "", - "metadata.contactForResource.role.user": "", "multiselect.filter.placeholder": "Hľadať", "nav.back": "Späť", "next": "Ďalej", From d81a54715663bf654371a09225c16a6b443887c1 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Tue, 20 Aug 2024 08:52:48 +0200 Subject: [PATCH 5/5] contacts may now be ordered --- apps/metadata-editor-e2e/src/e2e/edit.cy.ts | 4 +- .../contact-card/contact-card.component.html | 47 ++-- .../contact-card.component.spec.ts | 61 ++--- .../contact-card/contact-card.component.ts | 9 +- ...field-contacts-for-resource.component.html | 40 +-- ...ld-contacts-for-resource.component.spec.ts | 229 +++++++++++------- ...m-field-contacts-for-resource.component.ts | 221 ++++++++++------- .../form-field-keywords.component.html | 2 +- .../fuzzy-search/fuzzy-search.component.html | 2 +- .../autocomplete.component.spec.ts | 6 +- .../autocomplete/autocomplete.component.ts | 15 +- 11 files changed, 363 insertions(+), 273 deletions(-) diff --git a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts index ebd6b39f61..d0b5ff0033 100644 --- a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts @@ -107,7 +107,9 @@ describe('editor form', () => { .find('gn-ui-autocomplete') .type('bar') - cy.get('mat-option').should('have.text', ' Barbara Roberts ').click() + cy.get('mat-option') + .should('have.text', ' Barbara Roberts (Barbie Inc.) ') + .click() cy.get('[data-test=displayedRoles]') .children() 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 511e194a22..a205e4c41f 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,28 +1,25 @@ - -
-
- -
-
- {{ contact.firstName }} {{ contact.lastName }} -
-
{{ contact.email }}
+
+
+ +
+
+ {{ contact.firstName }} {{ contact.lastName }}
+
{{ contact.email }}
- close -
- + close + +
diff --git a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.spec.ts b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.spec.ts index 1ce24cb81e..320fc59d43 100644 --- a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.spec.ts +++ b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.spec.ts @@ -6,12 +6,27 @@ import { } from '@geonetwork-ui/common/domain/model/record' import { MatIconModule } from '@angular/material/icon' import { CommonModule } from '@angular/common' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { + AutocompleteComponent, + ButtonComponent, +} from '@geonetwork-ui/ui/inputs' +import { ChangeDetectionStrategy } from '@angular/core' describe('ContactCardComponent', () => { let component: ContactCardComponent let fixture: ComponentFixture + const mockContact: Individual = { + firstName: 'John', + lastName: 'Doe', + organization: { name: 'Org1' } as Organization, + email: 'john.doe@example.com', + role: 'admin', + address: '', + phone: '', + position: '', + } + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ @@ -20,12 +35,17 @@ describe('ContactCardComponent', () => { ButtonComponent, ContactCardComponent, ], - }).compileComponents() + }) + .overrideComponent(AutocompleteComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents() }) beforeEach(() => { fixture = TestBed.createComponent(ContactCardComponent) component = fixture.componentInstance + component.contact = mockContact fixture.detectChanges() }) @@ -33,44 +53,7 @@ describe('ContactCardComponent', () => { expect(component).toBeTruthy() }) - it('should have a defined contact input', () => { - const mockContact: Individual = { - firstName: 'John', - lastName: 'Doe', - organization: { name: 'Org1' } as Organization, - email: 'john.doe@example.com', - role: 'admin', - address: '', - phone: '', - position: '', - } - component.contact = mockContact - fixture.detectChanges() - expect(component.contact).toEqual(mockContact) - }) - - it('should have a defined organization input', () => { - const mockOrganization: Organization = { - name: 'Org1', - } - component.organization = mockOrganization - fixture.detectChanges() - expect(component.organization).toEqual(mockOrganization) - }) - it('should emit contactRemoved event with the correct contact', () => { - const mockContact: Individual = { - firstName: 'John', - lastName: 'Doe', - organization: { name: 'Org1' } as Organization, - email: 'john.doe@example.com', - role: 'admin', - address: '', - phone: '', - position: '', - } - component.contact = mockContact - const contactRemovedSpy = jest.spyOn(component.contactRemoved, 'emit') component.removeContact(mockContact) expect(contactRemovedSpy).toHaveBeenCalledWith(mockContact) diff --git a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts index 494bed0ad9..10fb9c6ffa 100644 --- a/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts +++ b/libs/feature/editor/src/lib/components/contact-card/contact-card.component.ts @@ -5,13 +5,11 @@ import { Input, Output, } from '@angular/core' -import { - Individual, - Organization, -} from '@geonetwork-ui/common/domain/model/record' +import { Individual } from '@geonetwork-ui/common/domain/model/record' import { MatIconModule } from '@angular/material/icon' import { CommonModule } from '@angular/common' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { ThumbnailComponent } from '@geonetwork-ui/ui/elements' @Component({ selector: 'gn-ui-contact-card', @@ -19,11 +17,10 @@ import { ButtonComponent } from '@geonetwork-ui/ui/inputs' styleUrls: ['./contact-card.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, MatIconModule, ButtonComponent], + imports: [CommonModule, MatIconModule, ButtonComponent, ThumbnailComponent], }) export class ContactCardComponent { @Input() contact: Individual - @Input() organization: Organization @Input() removable = true @Output() contactRemoved = new EventEmitter() diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html index 291b27bba4..69d8a2f22e 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html @@ -16,11 +16,18 @@
@@ -38,20 +45,23 @@ [clearOnSelection]="true" > - - + + + + + + + + - - - +
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.spec.ts index 680de56f29..97f597a4c0 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.spec.ts @@ -5,13 +5,15 @@ import { BehaviorSubject } from 'rxjs' import { Individual, Organization, + Role, } from '@geonetwork-ui/common/domain/model/record' -import { ChangeDetectorRef, SimpleChanges } from '@angular/core' +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' @@ -38,22 +40,6 @@ describe('FormFieldContactsForResourceComponent', () => { let component: FormFieldContactsForResourceComponent let fixture: ComponentFixture - const contactJohn: Individual = { - firstName: 'John', - lastName: 'Doe', - organization: { name: 'Org1' } as Organization, - email: 'john.doe@example.com', - role: 'author', - } - - const contactJane: Individual = { - firstName: 'John', - lastName: 'Doe', - organization: { name: 'Org1' } as Organization, - email: 'john.doe@example.com', - role: 'custodian', - } - beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ @@ -75,107 +61,164 @@ describe('FormFieldContactsForResourceComponent', () => { }, ChangeDetectorRef, ], - }).compileComponents() - }) + }) + .overrideComponent(AutocompleteComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents() - beforeEach(() => { fixture = TestBed.createComponent(FormFieldContactsForResourceComponent) component = fixture.componentInstance component.control = new FormControl([]) fixture.detectChanges() }) - it('should create', () => { + it('should create the component', () => { expect(component).toBeTruthy() }) - it('should initialize with default values', () => { - expect(component.rolesToPick).toEqual([ - 'resource_provider', - 'custodian', - 'owner', - 'point_of_contact', - 'author', - ]) - expect(component.rolesToDisplay).toEqual([]) - expect(component.allOrganizations.size).toBe(2) + describe('ngOnInit', () => { + it('should initialize organizations', async () => { + await component.ngOnInit() + + expect(component.allOrganizations.size).toBe(2) + }) }) - it('should fetch users on initialization', () => { - const platformService = TestBed.inject(PlatformServiceInterface) - expect(platformService.getUsers).toHaveBeenCalled() + 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) + }) }) - it('should subscribe to organizations and update the allOrganizations map', () => { - component.ngOnInit() - expect(component.allOrganizations.size).toBe(2) - expect(component.allOrganizations.get('Barbie Inc.')).toEqual( - organizationBarbie - ) + 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']) + }) }) - it('should add role to rolesToDisplay and remove from rolesToPick', () => { - component.addRoleToDisplay('owner') - expect(component.rolesToPick).not.toContain('owner') - expect(component.rolesToDisplay).toContain('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.updateContactsForRessource() + + expect(component.contactsForRessourceByRole.get('owner')).toEqual([ + mockContact, + ]) + expect(component.contactsAsDynElemByRole.get('owner').length).toBe(1) + }) }) - it('should remove contact by index', () => { - component.control.setValue([ - { firstName: 'John', lastName: 'Doe' } as Individual, - ]) - component.removeContact(0) - expect(component.control.value.length).toBe(0) + 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') + }) }) - it('should add a new contact with correct data', async () => { - const contact = { - name: 'John', - surname: 'Doe', - organisation: 'Org1', - email: 'john.doe@example.com', - } as UserModel - jest - .spyOn(component, 'getOrganizationByName') - .mockReturnValue({ name: 'Org1' } as Organization) - await component.addContact(contact, 'owner') - expect(component.control.value.length).toBe(1) - expect(component.control.value[0].firstName).toBe('John') - expect(component.control.value[0].lastName).toBe('Doe') - expect(component.control.value[0].organization.name).toBe('Org1') + 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]) + }) }) - it('should handle ngOnChanges and update rolesToDisplay', () => { - const changes = { - control: { - currentValue: { - value: [contactJane, contactJohn], - }, - previousValue: { value: [] }, - firstChange: false, - }, - } as unknown as SimpleChanges - component.ngOnChanges(changes) - expect(component.rolesToDisplay).toEqual([ - contactJane.role, - contactJohn.role, - ]) + 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]) + }) }) - it('should return contacts by role', () => { - const contacts: Individual[] = [ - { role: 'owner' } as Individual, - { role: 'custodian' } as Individual, - { role: 'owner' } as Individual, - ] - component.control.setValue(contacts) - const result = component.getContactsByRole('owner') - expect(result.length).toBe(2) + 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' + ) + }) }) - it('should return the correct organization by name', () => { - const org = { name: 'Org1' } as Organization - component.allOrganizations.set('Org1', org) - expect(component.getOrganizationByName('Org1')).toBe(org) + 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-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 bfd2fdccf8..9890c61fae 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 @@ -4,10 +4,8 @@ import { ChangeDetectorRef, Component, Input, - OnChanges, + OnDestroy, OnInit, - SimpleChanges, - Type, } from '@angular/core' import { FormControl } from '@angular/forms' import { @@ -19,10 +17,18 @@ import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { Individual, Organization, + Role, RoleLabels, } from '@geonetwork-ui/common/domain/model/record' import { TranslateModule } from '@ngx-translate/core' -import { debounceTime, distinctUntilChanged, Observable, switchMap } from 'rxjs' +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' @@ -31,6 +37,8 @@ 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-for-resource', @@ -50,13 +58,19 @@ import { ], }) export class FormFieldContactsForResourceComponent - implements OnInit, OnChanges + implements OnInit, OnDestroy { @Input() control: FormControl + subscription: Subscription = new Subscription() + allUsers$: Observable - rolesToPick: string[] = [ + contactsForRessourceByRole: Map = new Map() + + contactsAsDynElemByRole: Map = new Map() + + rolesToPick: Role[] = [ 'resource_provider', 'custodian', 'owner', @@ -64,12 +78,10 @@ export class FormFieldContactsForResourceComponent 'author', ] - rolesToDisplay = [] + roleSectionsToDisplay: Role[] = [] allOrganizations: Map = new Map() - addOptions: Array<{ buttonLabel: string; eventName: string }> = [] - constructor( private platformServiceInterface: PlatformServiceInterface, private organizationsServiceInterface: OrganizationsServiceInterface, @@ -78,53 +90,109 @@ export class FormFieldContactsForResourceComponent this.allUsers$ = this.platformServiceInterface.getUsers() } - ngOnInit(): void { - this.organizationsServiceInterface.organisations$.subscribe( - (organizations) => { - this.allOrganizations = new Map( - organizations.map((organization) => [organization.name, organization]) - ) - this.changeDetectorRef.markForCheck() - } + async ngOnInit(): Promise { + this.allOrganizations = new Map( + ( + await firstValueFrom(this.organizationsServiceInterface.organisations$) + ).map((organization) => [organization.name, organization]) ) - } - - ngOnChanges(changes: SimpleChanges) { - const contactsForResource = changes['control'] + this.updateContactsForRessource() + this.manageRoleSectionsToDisplay(this.control.value) + this.filterRolesToPick() - if ( - contactsForResource && - contactsForResource.currentValue !== contactsForResource.previousValue - ) { - const rolesToAdd = contactsForResource.currentValue.value.map( - (contact: Individual) => contact.role - ) + this.changeDetectorRef.markForCheck() - console.log(contactsForResource.currentValue) + this.subscription.add( + this.control.valueChanges.subscribe((contactsForResource) => { + this.updateContactsForRessource() + this.manageRoleSectionsToDisplay(contactsForResource) + this.filterRolesToPick() - rolesToAdd.forEach((role: string) => { - if (!this.rolesToDisplay.includes(role)) { - this.rolesToDisplay.push(role) - } + this.changeDetectorRef.markForCheck() }) - - this.filterRolesToPick() - - this.changeDetectorRef.markForCheck() - } + ) } addRoleToDisplay(roleToAdd: string) { - this.rolesToDisplay.push(roleToAdd) + this.roleSectionsToDisplay.push(roleToAdd) this.filterRolesToPick() } filterRolesToPick() { this.rolesToPick = this.rolesToPick.filter( - (role) => !this.rolesToDisplay.includes(role) + (role) => !this.roleSectionsToDisplay.includes(role) ) } + updateContactsForRessource() { + this.contactsForRessourceByRole = 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), + } + + if (!acc.has(contact.role)) { + acc.set(contact.role, []) + } + + acc.get(contact.role).push(updatedContact) + + return acc + }, + new Map() + ) + + this.contactsAsDynElemByRole = 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 + + if (!acc.has(contact.role)) { + acc.set(contact.role, []) + } + + acc.get(contact.role).push(contactAsDynElem) + + return acc + }, new Map()) + + this.changeDetectorRef.markForCheck() + } + + manageRoleSectionsToDisplay(contactsForResource: Individual[]) { + const roles = contactsForResource.map( + (contact: Individual) => contact.role + ) as Role[] + + roles.forEach((role: Role) => { + if (!this.roleSectionsToDisplay.includes(role)) { + this.roleSectionsToDisplay.push(role) + } + }) + } + removeContact(index: number) { const newContactsforRessource = this.control.value.filter( (_, i) => i !== index @@ -132,6 +200,22 @@ export class FormFieldContactsForResourceComponent this.control.setValue(newContactsforRessource) } + handleContactsChanged(event: DynamicElement[]) { + const newContactsOrdered = event.map( + (contactAsDynElem) => contactAsDynElem.inputs['contact'] + ) as Individual[] + + const role = newContactsOrdered[0].role + + this.contactsForRessourceByRole.set(role, newContactsOrdered) + + const newControlValue = Array.from( + this.contactsForRessourceByRole.values() + ).flat() + + this.control.setValue(newControlValue) + } + protected roleToLabel(role: string): string { return RoleLabels.get(role) } @@ -140,16 +224,20 @@ export class FormFieldContactsForResourceComponent * gn-ui-autocomplete */ displayWithFn: (user: UserModel) => string = (user) => - `${user.name} ${user.surname}` + `${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) => user.username.includes(query)), + users.filter((user) => fuzzyFilter(user.username)), ]), + map((results) => results.slice(0, 10)), debounceTime(300), distinctUntilChanged() ) @@ -159,12 +247,12 @@ export class FormFieldContactsForResourceComponent * gn-ui-autocomplete */ addContact(contact: UserModel, role: string) { - let newContactsForRessource = { + const newContactsForRessource = { firstName: contact.name ?? '', lastName: contact.surname ?? '', - organization: { - name: contact.organisation, - }, + organization: + this.allOrganizations.get(contact.organisation) ?? + ({ name: contact.organisation } as Organization), email: contact.email ?? '', role, address: '', @@ -172,49 +260,12 @@ export class FormFieldContactsForResourceComponent position: '', } as Individual - const newContactOrganization = this.getOrganizationByName( - contact.organisation - ) - - newContactsForRessource = { - ...newContactsForRessource, - organization: newContactOrganization, - } - const newControlValue = [...this.control.value, newContactsForRessource] this.control.setValue(newControlValue) } - getContactsByRole(role: string): Individual[] { - return this.control.value.filter((contact: Individual) => { - return contact.role === role - }) - } - - getOrganizationByName(name: string): Organization { - return this.allOrganizations.get(name) - } - - getContactByRoleForSortableList(role: string): Array { - return this.control.value - .filter((contact: Individual) => { - return contact.role === role - }) - .map((contact) => ({ - component: ContactCardComponent, - inputs: { - contact, - organization: contact.organization, - removable: false, - }, - })) as Array<{ - component: Type - inputs: Record - }> - } - - handleContactOrderChange(event) { - console.log(event) + ngOnDestroy(): void { + this.subscription.unsubscribe() } } diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.html index ce21d8e1b7..a2ea32005e 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.html @@ -4,7 +4,7 @@ [displayWithFn]="displayWithFn" [action]="autoCompleteAction" (itemSelected)="handleItemSelection($event)" - [clearOnSelection]="true" + [preventCompleteOnSelection]="true" [minCharacterCount]="0" [allowSubmit]="false" > diff --git a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.html b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.html index 11bf550ee3..7c11bb2881 100644 --- a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.html +++ b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.html @@ -6,6 +6,6 @@ (inputSubmitted)="handleInputSubmission($event)" (inputCleared)="handleInputCleared()" [value]="searchInputValue$ | async" - [clearOnSelection]="true" + [preventCompleteOnSelection]="true" [autoFocus]="autoFocus" > diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts index 847aedbfed..f97d73fc49 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts @@ -312,10 +312,10 @@ describe('AutocompleteComponent', () => { it('emits selection event', () => { expect(selectionEmitted).toEqual('first') }) - describe('if completeOnSelection on selection', () => { + describe('if preventCompleteOnSelection on selection', () => { it('set input value to last entered text', () => { component.clearOnSelection = false - component.completeOnSelection = true + component.preventCompleteOnSelection = true component.control.setValue('second') component.handleSelection(selectionEvent) @@ -328,7 +328,7 @@ describe('AutocompleteComponent', () => { describe('if clearOnSelection on selection', () => { it('set input value to empty string', () => { component.clearOnSelection = true - component.completeOnSelection = false + component.preventCompleteOnSelection = false component.control.setValue('second') component.handleSelection(selectionEvent) diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts index 3e5efe0d8a..3d6a2114c3 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts @@ -61,7 +61,7 @@ export class AutocompleteComponent @Input() action: (value: string) => Observable @Input() value?: AutocompleteItem @Input() clearOnSelection = false - @Input() completeOnSelection = false + @Input() preventCompleteOnSelection = false @Input() autoFocus = false @Input() minCharacterCount? = 3 @Input() allowSubmit = true @@ -207,12 +207,19 @@ export class AutocompleteComponent this.inputSubmitted.emit(this.inputRef.nativeElement.value) } + /** + * This function is triggered when an item is selected in the list of displayed items. + * If preventCompleteOnSelection is true then the input will be left as entered by the user. + * If preventCompleteOnSelection is false (by default) then the input will be completed with the item selected by the user. + * If clearOnSelection is true then the input will be cleared upon selection. + * @param event + */ handleSelection(event: MatAutocompleteSelectedEvent) { this.cancelEnter = true this.itemSelected.emit(event.option.value) - if (this.completeOnSelection) { - this.lastInputValue$.pipe(first()).subscribe((any) => { - this.inputRef.nativeElement.value = any + if (this.preventCompleteOnSelection) { + this.lastInputValue$.pipe(first()).subscribe((lastInputValue) => { + this.inputRef.nativeElement.value = lastInputValue }) return }