Skip to content

Commit

Permalink
Merge pull request #951 from geonetwork/me-record-field-contacts-for-…
Browse files Browse the repository at this point in the history
…resource

EDITOR - Record field: Contacts for resource
  • Loading branch information
rcaplier authored Aug 21, 2024
2 parents d5b28b1 + d81a547 commit 2cc6f5b
Show file tree
Hide file tree
Showing 29 changed files with 1,080 additions and 13 deletions.
78 changes: 78 additions & 0 deletions apps/metadata-editor-e2e/src/e2e/edit.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -58,6 +70,72 @@ 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 (Barbie Inc.) ')
.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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<div class="w-full flex flex-row p-8 items-center gap-6">
<div
class="w-full flex flex-row p-8 items-center gap-6"
data-test="pageSelectorButtons"
>
<ng-container
*ngFor="let page of pages$ | async; let index = index; let isLast = last"
>
Expand Down
28 changes: 28 additions & 0 deletions libs/common/domain/src/lib/model/record/contact.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Organization } from './organization.model'
import { marker } from '@biesbjerg/ngx-translate-extract-marker'

export const RoleValues = [
'unspecified',
Expand All @@ -24,6 +25,33 @@ export const RoleValues = [
'user', // Party who uses the resource
]

export const RoleLabels = new Map<Role, string>([
['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('domain.contact.role.principal_investigator'),
],
['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]

export interface Individual {
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="flex flex-row gap-4 items-center">
<div class="flex flex-row border border-gray-200 rounded-xl p-4 gap-4 w-full">
<gn-ui-thumbnail
class="w-[56px] h-[56px] rounded-[4px]"
[thumbnailUrl]="contact.organization.logoUrl?.href"
[fit]="'contain'"
></gn-ui-thumbnail>
<div class="flex flex-col w-full">
<div class="flex flex-row justify-between">
<span class="flex flex-wrap font-bold w-full"
>{{ contact.firstName }} {{ contact.lastName }}</span
>
</div>
<div>{{ contact.email }}</div>
</div>
</div>
<gn-ui-button
*ngIf="removable"
data-test="removeContactButton"
type="light"
extraClass="w-[20px] h-[20px] flex items-center justify-center"
(buttonClick)="removeContact(contact)"
><span class="material-symbols-outlined"> close </span>
</gn-ui-button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 {
AutocompleteComponent,
ButtonComponent,
} from '@geonetwork-ui/ui/inputs'
import { ChangeDetectionStrategy } from '@angular/core'

describe('ContactCardComponent', () => {
let component: ContactCardComponent
let fixture: ComponentFixture<ContactCardComponent>

const mockContact: Individual = {
firstName: 'John',
lastName: 'Doe',
organization: { name: 'Org1' } as Organization,
email: '[email protected]',
role: 'admin',
address: '',
phone: '',
position: '',
}

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
CommonModule,
MatIconModule,
ButtonComponent,
ContactCardComponent,
],
})
.overrideComponent(AutocompleteComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default },
})
.compileComponents()
})

beforeEach(() => {
fixture = TestBed.createComponent(ContactCardComponent)
component = fixture.componentInstance
component.contact = mockContact
fixture.detectChanges()
})

it('should create the component', () => {
expect(component).toBeTruthy()
})

it('should emit contactRemoved event with the correct contact', () => {
const contactRemovedSpy = jest.spyOn(component.contactRemoved, 'emit')
component.removeContact(mockContact)
expect(contactRemovedSpy).toHaveBeenCalledWith(mockContact)
})
})
Original file line number Diff line number Diff line change
@@ -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) => `<div style="max-width: 400px; margin: auto;">${story}</div>`
),
],
} as Meta<ContactCardComponent>

export const Primary: StoryObj<ContactCardComponent> = {
args: {
contact: {
firstName: 'John',
lastName: 'Doe',
organization: {
name: 'Example Organization',
},
email: '[email protected]',
role: 'Developer',
address: '123 Main St',
phone: '123-456-7890',
position: 'Senior Developer',
},
organization: {
name: 'Example Organization',
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
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',
templateUrl: './contact-card.component.html',
styleUrls: ['./contact-card.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, MatIconModule, ButtonComponent, ThumbnailComponent],
})
export class ContactCardComponent {
@Input() contact: Individual
@Input() removable = true
@Output() contactRemoved = new EventEmitter<Individual>()

removeContact(contact: Individual) {
this.contactRemoved.emit(contact)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<div class="flex flex-col gap-3">
<div class="flex flex-row flex-wrap gap-2" data-test="rolesToPick">
<ng-container *ngFor="let role of rolesToPick">
<gn-ui-button
extraClass="px-2 py-1.5"
(buttonClick)="addRoleToDisplay(role)"
>
<div class="flex flex-row gap-1 items-center">
<span class="text-primary text-[20px] leading-[0] font-bold pb-[5px]"
>&#8330;</span
>
<span class="font-bold" translate>{{ roleToLabel(role) }}</span>
</div>
</gn-ui-button>
</ng-container>
</div>
<div
class="mt-8"
*ngIf="
roleSectionsToDisplay && roleSectionsToDisplay.length > 0;
else noContact
"
data-test="displayedRoles"
>
<div
*ngFor="
let role of roleSectionsToDisplay;
let index = index;
let isLast = last
"
class="flex flex-col gap-4"
>
<div class="flex flex-row justify-between">
<span class="font-bold text-base" translate>{{
roleToLabel(role)
}}</span>
</div>

<gn-ui-autocomplete
[placeholder]="'Choose a contact'"
[action]="autoCompleteAction"
(itemSelected)="addContact($event, role)"
[displayWithFn]="displayWithFn"
[minCharacterCount]="1"
[clearOnSelection]="true"
>
</gn-ui-autocomplete>

<ng-container *ngIf="contactsForRessourceByRole.get(role) as contacts">
<ng-container *ngIf="contacts.length > 1">
<gn-ui-sortable-list
[elements]="contactsAsDynElemByRole.get(role)"
(elementsChange)="handleContactsChanged($event)"
></gn-ui-sortable-list>
</ng-container>
<ng-container *ngIf="contacts.length === 1">
<ng-container *ngFor="let contact of contacts">
<gn-ui-contact-card
[contact]="contact"
(contactRemoved)="removeContact(index)"
></gn-ui-contact-card> </ng-container
></ng-container>
</ng-container>

<hr class="border-t-[#D6D3D1] mt-4 mb-6" *ngIf="!isLast" />
</div>
</div>
<ng-template #noContact>
<div
class="p-4 border border-primary bg-primary-lightest rounded-lg"
translate
>
editor.record.form.field.contactsForResource.noContact
</div>
</ng-template>
</div>
Loading

0 comments on commit 2cc6f5b

Please sign in to comment.