diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.ts b/libs/feature/editor/src/lib/+state/editor.reducer.ts
index d2b4e96a5c..dfb4835ae5 100644
--- a/libs/feature/editor/src/lib/+state/editor.reducer.ts
+++ b/libs/feature/editor/src/lib/+state/editor.reducer.ts
@@ -38,7 +38,7 @@ export const initialEditorState: EditorState = {
saveError: null,
changedSinceSave: false,
editorConfig: DEFAULT_CONFIGURATION,
- currentPage: 0,
+ currentPage: 2, //todo: remove before merge
}
const reducer = createReducer(
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-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 69d8a2f22e..adf7d78296 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
@@ -47,12 +47,6 @@
- 1">
-
-
+
+ 1">
+
+
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html
new file mode 100644
index 0000000000..86db0afdf0
--- /dev/null
+++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0; else noContact">
+
+
+
+
+ 1">
+
+
+
+
+
+
+ editor.record.form.field.contacts.noContact
+
+
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts
new file mode 100644
index 0000000000..0f21913bb4
--- /dev/null
+++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.spec.ts
@@ -0,0 +1,224 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { FormFieldContactsComponent } from './form-field-contacts.component'
+import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface'
+import { BehaviorSubject } from 'rxjs'
+import {
+ Individual,
+ Organization,
+ Role,
+} from '@geonetwork-ui/common/domain/model/record'
+import { ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'
+import { UserModel } from '@geonetwork-ui/common/domain/model/user'
+import { CommonModule } from '@angular/common'
+import { TranslateModule } from '@ngx-translate/core'
+import { ContactCardComponent } from '../../../contact-card/contact-card.component'
+import {
+ AutocompleteComponent,
+ DropdownSelectorComponent,
+ UiInputsModule,
+} from '@geonetwork-ui/ui/inputs'
+import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface'
+import { FormControl } from '@angular/forms'
+
+const organizationBarbie: Organization = {
+ name: 'Barbie Inc.',
+}
+
+const organizationGoogle: Organization = {
+ name: 'Google',
+}
+
+class MockPlatformServiceInterface {
+ getUsers = jest.fn(() => new BehaviorSubject([]))
+}
+
+class MockOrganizationsServiceInterface {
+ organisations$ = new BehaviorSubject([organizationBarbie, organizationGoogle])
+}
+
+describe('FormFieldContactsForResourceComponent', () => {
+ let component: FormFieldContactsComponent
+ let fixture: ComponentFixture
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ FormFieldContactsComponent,
+ CommonModule,
+ TranslateModule.forRoot(),
+ UiInputsModule,
+ ContactCardComponent,
+ DropdownSelectorComponent,
+ ],
+ providers: [
+ {
+ provide: PlatformServiceInterface,
+ useClass: MockPlatformServiceInterface,
+ },
+ {
+ provide: OrganizationsServiceInterface,
+ useClass: MockOrganizationsServiceInterface,
+ },
+ ChangeDetectorRef,
+ ],
+ })
+ .overrideComponent(AutocompleteComponent, {
+ set: { changeDetection: ChangeDetectionStrategy.Default },
+ })
+ .compileComponents()
+
+ fixture = TestBed.createComponent(FormFieldContactsComponent)
+ component = fixture.componentInstance
+ component.control = new FormControl([])
+ fixture.detectChanges()
+ })
+
+ it('should create the component', () => {
+ expect(component).toBeTruthy()
+ })
+
+ describe('ngOnInit', () => {
+ it('should initialize organizations', async () => {
+ await component.ngOnInit()
+
+ expect(component.allOrganizations.size).toBe(2)
+ })
+ })
+
+ describe('addRoleToDisplay', () => {
+ it('should add role to display and filter roles to pick', () => {
+ const initialRolesToPick = [...component.rolesToPick]
+ const roleToAdd = initialRolesToPick[0]
+
+ component.addRoleToDisplay(roleToAdd)
+
+ expect(component.roleSectionsToDisplay).toContain(roleToAdd)
+ expect(component.rolesToPick).not.toContain(roleToAdd)
+ })
+ })
+
+ describe('filterRolesToPick', () => {
+ it('should filter roles already in roleSectionsToDisplay', () => {
+ component.rolesToPick = ['custodian', 'owner'] as Role[]
+ component.roleSectionsToDisplay = ['custodian'] as Role[]
+
+ component.filterRolesToPick()
+
+ expect(component.rolesToPick).toEqual(['owner'])
+ })
+ })
+
+ describe('updateContactsForRessource', () => {
+ it('should update contactsForRessourceByRole and contactsAsDynElemByRole', () => {
+ const mockContact: Individual = {
+ role: 'owner',
+ organization: { name: 'Org1' } as Organization,
+ } as Individual
+
+ component.allOrganizations.set('Org1', { name: 'Org1' } as Organization)
+ component.control.setValue([mockContact])
+
+ component.updateContacts()
+
+ expect(component.contactsForRessourceByRole.get('owner')).toEqual([
+ mockContact,
+ ])
+ expect(component.contactsAsDynElemByRole.get('owner').length).toBe(1)
+ })
+ })
+
+ describe('manageRoleSectionsToDisplay', () => {
+ it('should add new roles to roleSectionsToDisplay', () => {
+ const mockContact: Individual = {
+ role: 'owner',
+ organization: { name: 'Org1' } as Organization,
+ } as Individual
+
+ component.manageRoleSectionsToDisplay([mockContact])
+
+ expect(component.roleSectionsToDisplay).toContain('owner')
+ })
+ })
+
+ describe('removeContact', () => {
+ it('should remove contact at specified index', () => {
+ const mockContacts: Individual[] = [
+ {
+ role: 'owner',
+ organization: { name: 'Org1' } as Organization,
+ } as Individual,
+ {
+ role: 'custodian',
+ organization: { name: 'Org2' } as Organization,
+ } as Individual,
+ ]
+
+ component.control.setValue(mockContacts)
+ component.removeContact(0)
+
+ expect(component.control.value.length).toBe(1)
+ expect(component.control.value[0]).toEqual(mockContacts[1])
+ })
+ })
+
+ describe('handleContactsChanged', () => {
+ it('should update contacts based on reordered dynamic elements', () => {
+ const mockContacts: Individual[] = [
+ {
+ role: 'owner',
+ organization: { name: 'Org1' } as Organization,
+ } as Individual,
+ {
+ role: 'owner',
+ organization: { name: 'Org2' } as Organization,
+ } as Individual,
+ ]
+
+ component.contactsForRessourceByRole.set('owner', [mockContacts[0]])
+ component.contactsForRessourceByRole.set('owner', [mockContacts[1]])
+
+ const reorderedElements = [
+ { inputs: { contact: mockContacts[1] } } as any,
+ { inputs: { contact: mockContacts[0] } } as any,
+ ]
+
+ component.handleContactsChanged(reorderedElements)
+
+ const newControlValue = component.control.value
+ expect(newControlValue[0]).toEqual(mockContacts[1])
+ expect(newControlValue[1]).toEqual(mockContacts[0])
+ })
+ })
+
+ describe('addContact', () => {
+ it('should add a new contact to the control value', () => {
+ const mockUser: UserModel = {
+ username: 'user1',
+ name: 'John',
+ surname: 'Doe',
+ organisation: 'Org1',
+ } as UserModel
+
+ component.allOrganizations.set('Org1', { name: 'Org1' } as Organization)
+ const initialContacts = component.control.value.length
+
+ component.addContact(mockUser, 'owner')
+
+ expect(component.control.value.length).toBe(initialContacts + 1)
+ expect(component.control.value[initialContacts].role).toBe('owner')
+ expect(component.control.value[initialContacts].organization.name).toBe(
+ 'Org1'
+ )
+ })
+ })
+
+ describe('ngOnDestroy', () => {
+ it('should unsubscribe from all subscriptions', () => {
+ const subscriptionSpy = jest.spyOn(component.subscription, 'unsubscribe')
+
+ component.ngOnDestroy()
+
+ expect(subscriptionSpy).toHaveBeenCalled()
+ })
+ })
+})
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts
new file mode 100644
index 0000000000..371379b611
--- /dev/null
+++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts
@@ -0,0 +1,217 @@
+import { CommonModule } from '@angular/common'
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ SimpleChanges,
+} from '@angular/core'
+import { FormControl } from '@angular/forms'
+import {
+ AutocompleteComponent,
+ DropdownSelectorComponent,
+ UiInputsModule,
+} from '@geonetwork-ui/ui/inputs'
+import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets'
+import {
+ Individual,
+ Organization,
+ Role,
+} from '@geonetwork-ui/common/domain/model/record'
+import { TranslateModule } from '@ngx-translate/core'
+import {
+ debounceTime,
+ distinctUntilChanged,
+ firstValueFrom,
+ Observable,
+ Subscription,
+ switchMap,
+} from 'rxjs'
+import { UserModel } from '@geonetwork-ui/common/domain/model/user'
+import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface'
+import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface'
+import { ContactCardComponent } from '../../../contact-card/contact-card.component'
+import {
+ DynamicElement,
+ SortableListComponent,
+} from '@geonetwork-ui/ui/elements'
+import { createFuzzyFilter } from '@geonetwork-ui/util/shared'
+import { map } from 'rxjs/operators'
+
+@Component({
+ selector: 'gn-ui-form-field-contacts',
+ templateUrl: './form-field-contacts.component.html',
+ styleUrls: ['./form-field-contacts.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [
+ DropdownSelectorComponent,
+ UiInputsModule,
+ CommonModule,
+ UiWidgetsModule,
+ AutocompleteComponent,
+ TranslateModule,
+ ContactCardComponent,
+ SortableListComponent,
+ ],
+})
+export class FormFieldContactsComponent
+ implements OnInit, OnDestroy, OnChanges
+{
+ @Input() control: FormControl
+
+ contacts: Individual[] = []
+ contactsAsDynElem: DynamicElement[] = []
+
+ subscription: Subscription = new Subscription()
+
+ allUsers$: Observable
+
+ rolesToPick: Role[] = ['point_of_contact']
+
+ allOrganizations: Map = new Map()
+
+ constructor(
+ private platformServiceInterface: PlatformServiceInterface,
+ private organizationsServiceInterface: OrganizationsServiceInterface,
+ private changeDetectorRef: ChangeDetectorRef
+ ) {
+ this.allUsers$ = this.platformServiceInterface.getUsers()
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ console.log(changes['control'])
+ }
+
+ async ngOnInit(): Promise {
+ this.allOrganizations = new Map(
+ (
+ await firstValueFrom(this.organizationsServiceInterface.organisations$)
+ ).map((organization) => [organization.name, organization])
+ )
+
+ this.updateContacts()
+
+ this.changeDetectorRef.markForCheck()
+
+ this.subscription.add(
+ this.control.valueChanges.subscribe((contacts) => {
+ console.log('new contacts (valueChange): ', contacts)
+ this.updateContacts()
+ this.changeDetectorRef.markForCheck()
+ })
+ )
+ }
+
+ updateContacts() {
+ this.contacts = this.control.value.reduce((acc, contact) => {
+ const completeOrganization = this.allOrganizations.get(
+ contact.organization.name
+ )
+
+ const updatedContact = {
+ ...contact,
+ organization:
+ completeOrganization ??
+ ({ name: contact.organization.name } as Organization),
+ }
+
+ acc.push(updatedContact)
+
+ return acc
+ }, [] as Individual[])
+
+ this.contactsAsDynElem = this.control.value.reduce((acc, contact) => {
+ const completeOrganization = this.allOrganizations.get(
+ contact.organization.name
+ )
+
+ const updatedContact = {
+ ...contact,
+ organization:
+ completeOrganization ??
+ ({ name: contact.organization.name } as Organization),
+ }
+
+ const contactAsDynElem = {
+ component: ContactCardComponent,
+ inputs: {
+ contact: updatedContact,
+ removable: false,
+ },
+ } as DynamicElement
+
+ acc.push(contactAsDynElem)
+
+ return acc
+ }, [] as DynamicElement[])
+
+ this.changeDetectorRef.markForCheck()
+ }
+
+ removeContact() {
+ this.control.setValue([])
+ }
+
+ handleContactsChanged(event: DynamicElement[]) {
+ const newContactsOrdered = event.map(
+ (contactAsDynElem) => contactAsDynElem.inputs['contact']
+ ) as Individual[]
+
+ console.log('newContactsOrdered :', newContactsOrdered)
+
+ this.control.setValue(newContactsOrdered)
+ }
+
+ /**
+ * gn-ui-autocomplete
+ */
+ displayWithFn: (user: UserModel) => string = (user) =>
+ `${user.name} ${user.surname} ${
+ user.organisation ? `(${user.organisation})` : ''
+ }`
+
+ /**
+ * gn-ui-autocomplete
+ */
+ autoCompleteAction = (query: string) => {
+ const fuzzyFilter = createFuzzyFilter(query)
+ return this.allUsers$.pipe(
+ switchMap((users) => [
+ users.filter((user) => fuzzyFilter(user.username)),
+ ]),
+ map((results) => results.slice(0, 10)),
+ debounceTime(300),
+ distinctUntilChanged()
+ )
+ }
+
+ /**
+ * gn-ui-autocomplete
+ */
+ addContact(contact: UserModel) {
+ const newContacts = {
+ firstName: contact.name ?? '',
+ lastName: contact.surname ?? '',
+ organization:
+ this.allOrganizations.get(contact.organisation) ??
+ ({ name: contact.organisation } as Organization),
+ email: contact.email ?? '',
+ role: 'point_of_contact',
+ address: '',
+ phone: '',
+ position: '',
+ } as Individual
+
+ const newControlValue = [...this.control.value, newContacts]
+
+ this.control.setValue(newControlValue)
+ }
+
+ ngOnDestroy(): void {
+ this.subscription.unsubscribe()
+ }
+}
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html
index 4ae77a7993..4a7a090ebb 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
@@ -116,4 +116,9 @@
>
+
+
+
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 cdd7ea3568..2c72a6406b 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
@@ -39,6 +39,7 @@ import { FormFieldSimpleComponent } from './form-field-simple/form-field-simple.
import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/form-field-spatial-extent.component'
import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency/form-field-update-frequency.component'
import { FormFieldOpenDataComponent } from './form-field-open-data/form-field-open-data.component'
+import { FormFieldContactsComponent } from './form-field-contacts/form-field-contacts.component'
@Component({
selector: 'gn-ui-form-field',
@@ -67,6 +68,7 @@ import { FormFieldOpenDataComponent } from './form-field-open-data/form-field-op
FormFieldOverviewsComponent,
FormFieldContactsForResourceComponent,
FormFieldOpenDataComponent,
+ FormFieldContactsComponent,
],
})
export class FormFieldComponent {
@@ -85,8 +87,58 @@ export class FormFieldComponent {
this.titleInput.nativeElement.children[0].focus()
}
+ onVisibilityChange(visibility: boolean) {
+ this.isHidden = visibility
+ }
+
+ get isTitle() {
+ return this.model === 'title'
+ }
+ get isAbstract() {
+ return this.model === 'abstract'
+ }
+ get isLicenses() {
+ return this.model === 'licenses'
+ }
+ get isResourceUpdated() {
+ return this.model === 'resourceUpdated'
+ }
+ get isUpdateFrequency() {
+ return this.model === 'updateFrequency'
+ }
+ get isTemporalExtents() {
+ return this.model === 'temporalExtents'
+ }
+ get isSpatialExtentField() {
+ return this.model === 'spatialExtents'
+ }
+ get isGraphicOverview() {
+ return this.model === 'overviews'
+ }
+ get isSimpleField() {
+ return this.model === 'uniqueIdentifier' || this.model === 'recordUpdated'
+ }
+ get isReadOnly() {
+ return this.model === 'uniqueIdentifier' || this.model === 'recordUpdated'
+ }
+ get isKeywords() {
+ return this.model === 'keywords'
+ }
+ get isContactsForResource() {
+ return this.model === 'contactsForResource'
+ }
+
+ get isContacts() {
+ return this.model === 'contacts'
+ }
+
get withoutWrapper() {
- return this.model === 'title' || this.model === 'abstract'
+ return (
+ this.model === 'title' ||
+ this.model === 'abstract' ||
+ this.model === 'contactsForResource' ||
+ this.model === 'contacts'
+ )
}
get valueAsString() {
diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts
index 32e7c78e2f..7ac8813f4f 100644
--- a/libs/feature/editor/src/lib/fields.config.ts
+++ b/libs/feature/editor/src/lib/fields.config.ts
@@ -90,6 +90,13 @@ export const CONTACTS_FOR_RESOURCE_FIELD: EditorField = {
},
}
+export const CONTACTS: EditorField = {
+ model: 'contacts',
+ formFieldConfig: {
+ labelKey: '',
+ },
+}
+
export const RECORD_GRAPHICAL_OVERVIEW_FIELD: EditorField = {
model: 'overviews',
formFieldConfig: {
@@ -180,7 +187,7 @@ export const DATA_POINT_OF_CONTACT_SECTION: EditorSection = {
'editor.record.form.section.dataPointOfContact.description'
),
hidden: false,
- fields: [],
+ fields: [CONTACTS],
}
/************************************************************