From e6c41da9f829b931bcb7d8c99ad2ebca6e170115 Mon Sep 17 00:00:00 2001 From: Vera Malieske Date: Wed, 29 Apr 2026 20:18:15 +0200 Subject: [PATCH 1/4] Update Metadata: AgentForm - delete unneccassary roles - implement outlined-input for agent-form - implement functionality for custom-roles - make sure custom-roles will be displayed in list after adding a new one - fix issue when selecting an agent from autocomplete where the value field didn't update accordingly - add a pipe to transform the role type of an agent in a label-form to serve outputs on multiple components --- .../actionbar/actionbar.component.ts | 10 +- .../agent-list/agent-list.component.html | 45 +--- .../metadata/agents/agents.component.html | 235 ++++++++---------- .../metadata/agents/agents.component.scss | 10 + .../metadata/agents/agents.component.ts | 62 ++++- .../outlined-input.component.ts | 3 + src/app/metadata/index.ts | 51 ++-- src/app/pipes/role-label.pipe.ts | 11 + 8 files changed, 212 insertions(+), 215 deletions(-) create mode 100644 src/app/pipes/role-label.pipe.ts diff --git a/src/app/components/actionbar/actionbar.component.ts b/src/app/components/actionbar/actionbar.component.ts index 4dfabfc4..c3c64b00 100644 --- a/src/app/components/actionbar/actionbar.component.ts +++ b/src/app/components/actionbar/actionbar.component.ts @@ -1,12 +1,4 @@ -import { - filter, - firstValueFrom, - map, - of, - shareReplay, - switchMap, - tap, -} from 'rxjs'; +import { filter, firstValueFrom, map, of, shareReplay, switchMap, tap } from 'rxjs'; import { Component, computed, input, signal } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; diff --git a/src/app/components/metadata/agents/agent-list/agent-list.component.html b/src/app/components/metadata/agents/agent-list/agent-list.component.html index 6a0a8c3f..41a34cdd 100644 --- a/src/app/components/metadata/agents/agent-list/agent-list.component.html +++ b/src/app/components/metadata/agents/agent-list/agent-list.component.html @@ -44,41 +44,18 @@ } } - @if (entity.creatorList(); as creatorList) { - @if (creatorList.length > 0) { -

Creator:

- @for (agent of creatorList; track $index) { - - } - } - } + @if (entity.customRolesList(); as customRoleList) { + @if (customRoleList.length > 0) { + @for (item of customRoleList; track item.role) { +

{{ item.label + ':' }}

- @if (entity.editorList(); as editorList) { - @if (editorList.length > 0) { -

Editor:

- @for (agent of editorList; track $index) { - - } - } - } - - @if (entity.dataCreatorList(); as dataCreatorList) { - @if (dataCreatorList.length > 0) { -

Data Creator:

- @for (agent of dataCreatorList; track $index) { - + @for (agent of item.agents; track agent) { + + } } } } diff --git a/src/app/components/metadata/agents/agents.component.html b/src/app/components/metadata/agents/agents.component.html index 4973e080..5c900a13 100644 --- a/src/app/components/metadata/agents/agents.component.html +++ b/src/app/components/metadata/agents/agents.component.html @@ -12,15 +12,11 @@
@if (personSelected()) { - - {{ 'Prename' | translate }} - + - + } - - {{ 'Name' | translate }} - + + - +
- - {{ 'E-Mail address' | translate }} - + @if (agentIsEditable()) { - } - - - {{ 'Phone number' }} - + + + @if (agentIsEditable()) { - } - +
- - {{ 'Name' | translate }} - + - + - - {{ 'Department' | translate }} - + @if (agentIsEditable()) { - } - +
- - {{ 'Country' | translate }} - + @if (agentIsEditable()) { - } - - - {{ 'Postal Code' | translate }} - + + + @if (agentIsEditable()) { - } - - - {{ 'City' | translate }} - + + + @if (agentIsEditable()) { - } - +
- - {{ 'Street' | translate }} - + @if (agentIsEditable()) { - } - + - - {{ 'Number' | translate }} - + @if (agentIsEditable()) { - } - + - - {{ 'Building' | translate }} - + @if (agentIsEditable()) { - } - +
@@ -291,6 +245,15 @@ {{ role.value }} } +
+ + {{ 'Other:' | translate }} + + +
diff --git a/src/app/components/metadata/agents/agents.component.scss b/src/app/components/metadata/agents/agents.component.scss index e184caba..858ecd2d 100644 --- a/src/app/components/metadata/agents/agents.component.scss +++ b/src/app/components/metadata/agents/agents.component.scss @@ -53,6 +53,16 @@ display: none; } +.custom-role-row { + display: flex; + align-items: center; + gap: 8px; + + app-outlined-input { + padding-top: calc((32px - 12px) / 2); + } +} + ::ng-deep { .mdc-dialog__container .mdc-text-field--filled .mdc-floating-label { font-size: 14px !important; diff --git a/src/app/components/metadata/agents/agents.component.ts b/src/app/components/metadata/agents/agents.component.ts index 79eea1d3..f80876e2 100644 --- a/src/app/components/metadata/agents/agents.component.ts +++ b/src/app/components/metadata/agents/agents.component.ts @@ -5,6 +5,7 @@ import { input, OnChanges, OnDestroy, + OnInit, signal, SimpleChanges, ViewChild, @@ -55,6 +56,8 @@ import { IsInstitutionPipe } from 'src/app/pipes/is-institution.pipe'; import { CacheManagerService } from 'src/app/services/cache-manager.service'; import { IsAgentInEntityPipe } from './is-agent-in-entity.pipe'; import { IsAgentSelfPipe } from './is-agent-self.pipe'; +import { OutlinedInputComponent } from '../../outlined-input/outlined-input.component'; +import { RoleLabelPipe } from 'src/app/pipes/role-label.pipe'; const withoutProps = (obj: T, ...props: K[]): Omit => { const copy = { ...obj }; @@ -86,11 +89,12 @@ const withoutProps = (obj: T, ...props: K[]): Omit = AsyncPipe, IsAgentInEntityPipe, IsAgentSelfPipe, + OutlinedInputComponent, ], templateUrl: './agents.component.html', styleUrl: './agents.component.scss', }) -export class AgentsComponent implements OnDestroy, OnChanges { +export class AgentsComponent implements OnDestroy, OnChanges, OnInit { entity = input.required(); entityId = computed(() => this.entity()._id); @@ -272,12 +276,11 @@ export class AgentsComponent implements OnDestroy, OnChanges { availableRoles = [ { type: 'RIGHTS_OWNER', value: 'Rightsowner', checked: false }, - { type: 'CREATOR', value: 'Creator', checked: false }, - { type: 'EDITOR', value: 'Editor', checked: false }, - { type: 'DATA_CREATOR', value: 'Data creator', checked: false }, { type: 'CONTACT_PERSON', value: 'Contact person', checked: false }, ]; + newCustomRole = { value: '', checked: false }; + constructor( public account: AccountService, public backend: BackendService, @@ -309,7 +312,10 @@ export class AgentsComponent implements OnDestroy, OnChanges { } get atLeastOneRoleSelected(): boolean { - return this.availableRoles.some(role => role.checked); + return ( + this.availableRoles.some(role => role.checked) || + (this.newCustomRole.checked && this.newCustomRole.value != '') + ); } get selectionIsValid(): boolean { @@ -317,7 +323,16 @@ export class AgentsComponent implements OnDestroy, OnChanges { } get currentRoleSelection(): string[] { - return this.availableRoles.filter(role => role.checked).map(role => role.type); + const availableRoleTypes = this.availableRoles + .filter(role => role.checked) + .map(role => role.type); + + const customRole = + this.newCustomRole.checked && this.newCustomRole.value.trim() + ? this.newCustomRole.value.trim().toUpperCase().replace(/\s+/g, '_') + : ''; + + return customRole ? [...availableRoleTypes, customRole] : availableRoleTypes; } get newContactRef(): Address | ContactReference { @@ -369,6 +384,7 @@ export class AgentsComponent implements OnDestroy, OnChanges { : institutions.find(i => i._id === agentId); if (!currentAgent) throw new Error(`Agent with ID ${agentId} not found`); + this.agentIsSelected.set(true); this.setAgentInForm(currentAgent); } @@ -459,6 +475,7 @@ export class AgentsComponent implements OnDestroy, OnChanges { this.entity().addPerson(personInstance); this.metaDataCommunicationService.setEntity(this.entity()); + this.addCustomRole(); } private addInstitution() { @@ -471,6 +488,25 @@ export class AgentsComponent implements OnDestroy, OnChanges { this.entity().addInstitution(institutionInstance); this.metaDataCommunicationService.setEntity(this.entity()); + this.addCustomRole(); + } + + private addCustomRole() { + if (!this.newCustomRole.checked || !this.newCustomRole.value.trim()) return; + + const type = this.newCustomRole.value.trim().toUpperCase().replace(/\s+/g, '_'); + this.addRoleIfNotExists(type); + } + + private addRoleIfNotExists(type: string) { + if (this.availableRoles.some(r => r.type === type)) return; + + const roleLabelPipe = new RoleLabelPipe(); + this.availableRoles.push({ + type, + value: roleLabelPipe.transform(type), + checked: false, + }); } public updateAgent() { @@ -539,6 +575,7 @@ export class AgentsComponent implements OnDestroy, OnChanges { this.availableRoles.forEach(role => { role.checked = false; }); + this.newCustomRole = { value: '', checked: false }; } clearAgentInformation() { @@ -557,6 +594,19 @@ export class AgentsComponent implements OnDestroy, OnChanges { this.metaDataCommunicationService.setEntity(currentEntity); } + ngOnInit(): void { + const fixedRoles = this.availableRoles.map(r => r.type); + const allAgents = [...this.entity().persons, ...this.entity().institutions]; + + for (const agent of allAgents) { + for (const role of agent.roles?.[this.entityId()] ?? []) { + if (!fixedRoles.includes(role)) { + this.addRoleIfNotExists(role); + } + } + } + } + ngOnDestroy(): void { this.resetAgentForm(); this.selectedAgent.set(null); diff --git a/src/app/components/outlined-input/outlined-input.component.ts b/src/app/components/outlined-input/outlined-input.component.ts index c49e28d8..233c720f 100644 --- a/src/app/components/outlined-input/outlined-input.component.ts +++ b/src/app/components/outlined-input/outlined-input.component.ts @@ -91,6 +91,9 @@ export class OutlinedInputComponent implements ControlValueAccessor { // ControlValueAccessor methods writeValue(value: string): void { this.value.set(value || ''); + //To be sure to update the native input properly + // (issue with displaying id + ,person sometimes when autocomplete by prename) + this.#updateInputElementValues(value); } registerOnChange(fn: (value: string) => void): void { diff --git a/src/app/metadata/index.ts b/src/app/metadata/index.ts index 7cb88f75..ce3c7f42 100644 --- a/src/app/metadata/index.ts +++ b/src/app/metadata/index.ts @@ -18,6 +18,7 @@ import { ITag, ITypeValueTuple, } from '@kompakkt/common'; +import { RoleLabelPipe } from '../pipes/role-label.pipe'; const getObjectId = () => new ObjectID().toString(); @@ -151,13 +152,6 @@ class BaseEntity implements IBaseEntity { return true; } - public hasCreator(): boolean { - const { persons, institutions, _id } = this; - if (!persons.some(p => Person.hasRole(p, _id, 'CREATOR'))) - if (!institutions.some(i => Institution.hasRole(i, _id, 'CREATOR'))) return false; - return true; - } - public rightsOwnerList(): (Person | Institution)[] { const { persons, institutions, _id } = this; const filteredPersons = persons.filter(p => Person.hasRole(p, _id, 'RIGHTS_OWNER')); @@ -178,30 +172,27 @@ class BaseEntity implements IBaseEntity { return [...filteredPersons, ...filteredInstitutions]; } - public creatorList(): (Person | Institution)[] { - const { persons, institutions, _id } = this; - const filteredPersons = persons.filter(p => Person.hasRole(p, _id, 'CREATOR')); - const filteredInstitutions = institutions.filter(i => Institution.hasRole(i, _id, 'CREATOR')); - - return [...filteredPersons, ...filteredInstitutions]; - } - - public editorList(): (Person | Institution)[] { + public customRolesList(): { role: string; label: string; agents: (Person | Institution)[] }[] { const { persons, institutions, _id } = this; - const filteredPersons = persons.filter(p => Person.hasRole(p, _id, 'EDITOR')); - const filteredInstitutions = institutions.filter(i => Institution.hasRole(i, _id, 'EDITOR')); - - return [...filteredPersons, ...filteredInstitutions]; - } - - public dataCreatorList(): (Person | Institution)[] { - const { persons, institutions, _id } = this; - const filteredPersons = persons.filter(p => Person.hasRole(p, _id, 'DATA_CREATOR')); - const filteredInstitutions = institutions.filter(i => - Institution.hasRole(i, _id, 'DATA_CREATOR'), - ); - - return [...filteredPersons, ...filteredInstitutions]; + const fixedRoles = ['RIGHTS_OWNER', 'CONTACT_PERSON']; + + const customRoles = new Set(); + [...persons, ...institutions].forEach(agent => { + (agent.roles?.[_id] ?? []) + .filter(r => !fixedRoles.includes(r)) + .forEach(r => customRoles.add(r)); + }); + + const roleLabelPipe = new RoleLabelPipe(); + + return Array.from(customRoles).map(role => ({ + role, + label: roleLabelPipe.transform(role), + agents: [ + ...persons.filter(p => Person.hasRole(p, _id, role)), + ...institutions.filter(i => Institution.hasRole(i, _id, role)), + ], + })); } // eslint-disable-next-line @typescript-eslint/ban-ts-ignore diff --git a/src/app/pipes/role-label.pipe.ts b/src/app/pipes/role-label.pipe.ts new file mode 100644 index 00000000..6376fde7 --- /dev/null +++ b/src/app/pipes/role-label.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'roleLabel', standalone: true }) +export class RoleLabelPipe implements PipeTransform { + transform(role: string): string { + return role + .replace(/_/g, ' ') + .toLowerCase() + .replace(/^\w/, c => c.toUpperCase()); + } +} From 8b451ff4f4dd36e0cd3eb2e8899f660618120cc4 Mon Sep 17 00:00:00 2001 From: Vera Malieske Date: Mon, 4 May 2026 16:59:04 +0200 Subject: [PATCH 2/4] Update MetaData: Optional Counter - implement counter for all optional metadata (physObject is just a (1) when there is one) - add a service for any countable objects - move optional metadataType-String into class to make passing the value easier --- .../metadata/entity/entity.component.html | 36 ++++++++++++--- .../metadata/entity/entity.component.ts | 16 ++++++- .../biblio-ref/biblio-ref.component.html | 2 +- .../biblio-ref/biblio-ref.component.ts | 5 ++- .../optional/creation/creation.component.html | 2 +- .../optional/creation/creation.component.ts | 16 ++++++- .../external-ids/external-ids.component.html | 2 +- .../external-ids/external-ids.component.ts | 6 ++- .../optional/links/links.component.html | 2 +- .../optional/links/links.component.ts | 5 ++- .../metadata-files.component.html | 2 +- .../metadata-files.component.ts | 14 +++++- .../optional-card-list.component.ts | 8 ++++ src/app/services/dialog-helper.service.ts | 3 ++ .../services/single-number-counter.service.ts | 45 +++++++++++++++++++ 15 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 src/app/services/single-number-counter.service.ts diff --git a/src/app/components/metadata/entity/entity.component.html b/src/app/components/metadata/entity/entity.component.html index 2dabb78c..e39f4af5 100644 --- a/src/app/components/metadata/entity/entity.component.html +++ b/src/app/components/metadata/entity/entity.component.html @@ -82,6 +82,9 @@ label_important {{ 'Creation' | translate }} + @if (creationCount()) { + {{ '(' + creationCount() + ')' }} + } } @@ -100,6 +103,9 @@ label_important {{ 'External Identifiers' | translate }} + @if (idCount()) { + {{ '(' + idCount() + ')' }} + } @@ -219,7 +237,9 @@ @if (digitalEntity(); as digitalEntity) { - + + + } @@ -229,27 +249,33 @@ @if (digitalEntity(); as digitalEntity) { - + } - + - + @if (digitalEntity(); as digitalEntity) { - + } diff --git a/src/app/components/metadata/entity/entity.component.ts b/src/app/components/metadata/entity/entity.component.ts index 48a54bb1..ce2fb8c3 100644 --- a/src/app/components/metadata/entity/entity.component.ts +++ b/src/app/components/metadata/entity/entity.component.ts @@ -1,5 +1,5 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { Component, computed, input } from '@angular/core'; +import { Component, computed, effect, input } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; @@ -41,6 +41,7 @@ import { LinksComponent } from '../optional/links/links.component'; import { MetadataFilesComponent } from '../optional/metadata-files/metadata-files.component'; import { PhysObjComponent } from '../optional/phys-obj/phys-obj.component'; import { MatExpansionModule } from '@angular/material/expansion'; +import { CounterService } from 'src/app/services/single-number-counter.service'; type AnyEntity = DigitalEntity | PhysicalEntity; @@ -123,11 +124,19 @@ export class EntityComponent { public filteredTags$: Observable; public separatorKeysCodes: number[] = [ENTER, COMMA]; + //optional counter values + biblioCount = this.counterService.getCounter('biblio'); + creationCount = this.counterService.getCounter('creation'); + idCount = this.counterService.getCounter('externalId'); + linkCount = this.counterService.getCounter('link'); + filesCount = this.counterService.getCounter('metadata_files'); + constructor( public content: ContentProviderService, public dialog: MatDialog, private snackbar: SnackbarService, public metaService: MetadataCommunicationService, + public counterService: CounterService, ) { (window as any)['printEntity'] = () => console.log(this.entity()); @@ -399,4 +408,9 @@ export class EntityComponent { const { property, index } = data; this.removeProperty(entity, property, index); } + + onItemAdded(event: { item: object; type: string }) { + this.counterService.trackAsNew(event.item); + this.counterService.incrementCounter(event.type); + } } diff --git a/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.html b/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.html index a98ef775..a253fa81 100644 --- a/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.html +++ b/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.html @@ -52,4 +52,4 @@ } - + diff --git a/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.ts b/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.ts index d8afb365..5417529b 100644 --- a/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.ts +++ b/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, input } from '@angular/core'; +import { Component, computed, EventEmitter, input, Output } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -33,7 +33,9 @@ import { OutlinedInputComponent } from 'src/app/components/outlined-input/outlin styleUrl: './biblio-ref.component.scss', }) export class BiblioRefComponent { + public propertyType = 'biblio'; public entity = input.required(); + @Output() itemAdded = new EventEmitter<{ item: object; type: string }>(); isPhysical = computed(() => this.entity() instanceof PhysicalEntity); public referenceControl = new FormControl('', { nonNullable: true }); @@ -69,6 +71,7 @@ export class BiblioRefComponent { if (this.isBiblioDataValid && DescriptionValueTuple.checkIsValid(biblioInstance)) { this.entity().biblioRefs.push(biblioInstance); this.resetFormFields(); + this.itemAdded.emit({ item: biblioInstance, type: this.propertyType }); } } diff --git a/src/app/components/metadata/optional/creation/creation.component.html b/src/app/components/metadata/optional/creation/creation.component.html index 9287b89f..e9af1b95 100644 --- a/src/app/components/metadata/optional/creation/creation.component.html +++ b/src/app/components/metadata/optional/creation/creation.component.html @@ -46,5 +46,5 @@ - +
diff --git a/src/app/components/metadata/optional/creation/creation.component.ts b/src/app/components/metadata/optional/creation/creation.component.ts index 038853b6..a8e79d81 100644 --- a/src/app/components/metadata/optional/creation/creation.component.ts +++ b/src/app/components/metadata/optional/creation/creation.component.ts @@ -1,5 +1,14 @@ import { formatDate } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, input, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + EventEmitter, + inject, + input, + OnInit, + Output, +} from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -15,6 +24,7 @@ import { OptionalCardListComponent } from '../optional-card-list/optional-card-l import { OutlinedInputComponent } from 'src/app/components/outlined-input/outlined-input.component'; import { toSignal } from '@angular/core/rxjs-interop'; +import { CounterService } from 'src/app/services/single-number-counter.service'; @Component({ selector: 'app-creation', @@ -36,8 +46,11 @@ import { toSignal } from '@angular/core/rxjs-interop'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CreationComponent implements OnInit { + public propertyType = 'creation'; public entity = input.required(); + @Output() itemAdded = new EventEmitter<{ item: object; type: string }>(); + form = new FormGroup({ technique: new FormControl(''), software: new FormControl(''), @@ -71,6 +84,7 @@ export class CreationComponent implements OnInit { this.resetFormFields(); this.entity().creation.push(creationInstance); + this.itemAdded.emit({ item: creationInstance, type: this.propertyType }); } isFormValid = computed(() => { diff --git a/src/app/components/metadata/optional/external-ids/external-ids.component.html b/src/app/components/metadata/optional/external-ids/external-ids.component.html index 2d180698..2da3ba62 100644 --- a/src/app/components/metadata/optional/external-ids/external-ids.component.html +++ b/src/app/components/metadata/optional/external-ids/external-ids.component.html @@ -29,4 +29,4 @@ } - + diff --git a/src/app/components/metadata/optional/external-ids/external-ids.component.ts b/src/app/components/metadata/optional/external-ids/external-ids.component.ts index 8e2a1c4c..c229098c 100644 --- a/src/app/components/metadata/optional/external-ids/external-ids.component.ts +++ b/src/app/components/metadata/optional/external-ids/external-ids.component.ts @@ -1,5 +1,5 @@ import { AsyncPipe } from '@angular/common'; -import { Component, computed, input } from '@angular/core'; +import { Component, computed, EventEmitter, inject, input, Output } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; @@ -10,6 +10,7 @@ import { AnyEntity, PhysicalEntity, TypeValueTuple } from 'src/app/metadata'; import { TranslatePipe } from 'src/app/pipes'; import { OptionalCardListComponent } from '../optional-card-list/optional-card-list.component'; import { OutlinedInputComponent } from 'src/app/components/outlined-input/outlined-input.component'; +import { CounterService } from 'src/app/services/single-number-counter.service'; @Component({ selector: 'app-external-ids', @@ -29,7 +30,9 @@ import { OutlinedInputComponent } from 'src/app/components/outlined-input/outlin styleUrl: './external-ids.component.scss', }) export class ExternalIdsComponent { + public propertyType = 'externalId'; public entity = input.required(); + @Output() itemAdded = new EventEmitter<{ item: object; type: string }>(); public valueControl = new FormControl('', { nonNullable: true }); public typeControl = new FormControl('', { nonNullable: true }); @@ -50,6 +53,7 @@ export class ExternalIdsComponent { if (identifierInstance.isValid) { this.entity().externalId.push(identifierInstance); this.resetFormFields(); + this.itemAdded.emit({ item: identifierInstance, type: this.propertyType }); } } diff --git a/src/app/components/metadata/optional/links/links.component.html b/src/app/components/metadata/optional/links/links.component.html index e0c76097..d8eb10f5 100644 --- a/src/app/components/metadata/optional/links/links.component.html +++ b/src/app/components/metadata/optional/links/links.component.html @@ -30,4 +30,4 @@ } - + diff --git a/src/app/components/metadata/optional/links/links.component.ts b/src/app/components/metadata/optional/links/links.component.ts index 5883246e..240e5bd1 100644 --- a/src/app/components/metadata/optional/links/links.component.ts +++ b/src/app/components/metadata/optional/links/links.component.ts @@ -1,5 +1,5 @@ import { AsyncPipe } from '@angular/common'; -import { Component, computed, input } from '@angular/core'; +import { Component, computed, EventEmitter, input, Output } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -32,6 +32,8 @@ import { OutlinedInputComponent } from 'src/app/components/outlined-input/outlin }) export class LinksComponent { public entity = input.required(); + public propertyType = 'link'; + @Output() itemAdded = new EventEmitter<{ item: object; type: string }>(); public valueControl = new FormControl('', { nonNullable: true }); public descriptionControl = new FormControl('', { nonNullable: true }); @@ -52,6 +54,7 @@ export class LinksComponent { if (linkInstance.isValid) { this.entity().externalLink.push(linkInstance); this.resetFormFields(); + this.itemAdded.emit({ item: linkInstance, type: this.propertyType }); } } diff --git a/src/app/components/metadata/optional/metadata-files/metadata-files.component.html b/src/app/components/metadata/optional/metadata-files/metadata-files.component.html index 2cb19f59..5b578a7c 100644 --- a/src/app/components/metadata/optional/metadata-files/metadata-files.component.html +++ b/src/app/components/metadata/optional/metadata-files/metadata-files.component.html @@ -22,7 +22,7 @@ ({{ file.file_size | filesize }})

-
diff --git a/src/app/components/metadata/optional/metadata-files/metadata-files.component.ts b/src/app/components/metadata/optional/metadata-files/metadata-files.component.ts index 42a7e4c7..5c5130ba 100644 --- a/src/app/components/metadata/optional/metadata-files/metadata-files.component.ts +++ b/src/app/components/metadata/optional/metadata-files/metadata-files.component.ts @@ -1,9 +1,10 @@ -import { Component, computed, input } from '@angular/core'; +import { Component, computed, EventEmitter, inject, input, Output } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; import { AnyEntity, FileTuple, PhysicalEntity } from 'src/app/metadata'; import { FilesizePipe, TranslatePipe } from 'src/app/pipes'; +import { CounterService } from 'src/app/services/single-number-counter.service'; @Component({ selector: 'app-metadata-files', @@ -14,11 +15,21 @@ import { FilesizePipe, TranslatePipe } from 'src/app/pipes'; }) export class MetadataFilesComponent { public entity = input.required(); + public propertyType = 'metadata_files'; + @Output() itemAdded = new EventEmitter<{ item: object; type: string }>(); isPhysical = computed(() => this.entity() instanceof PhysicalEntity); + #counterService = inject(CounterService); + public removeProperty(property: string, index: number) { if (Array.isArray(this.entity()[property])) { const removed = this.entity()[property].splice(index, 1)[0]; + + if (this.#counterService.isNew(removed)) { + this.#counterService.decrementCounter(this.propertyType); + this.#counterService.removeTracking(removed); + } + if (!removed) { return console.warn('No item removed'); } @@ -77,6 +88,7 @@ export class MetadataFilesComponent { const metadataFile = await readfile(file); if (!metadataFile) continue; this.entity().metadata_files.push(metadataFile); + this.itemAdded.emit({ item: metadataFile, type: this.propertyType }); } } } diff --git a/src/app/components/metadata/optional/optional-card-list/optional-card-list.component.ts b/src/app/components/metadata/optional/optional-card-list/optional-card-list.component.ts index 0d767a50..c1717924 100644 --- a/src/app/components/metadata/optional/optional-card-list/optional-card-list.component.ts +++ b/src/app/components/metadata/optional/optional-card-list/optional-card-list.component.ts @@ -10,6 +10,7 @@ import { } from 'src/app/pipes/tuple-helper.pipes'; import { MetadataCommunicationService } from 'src/app/services/metadata-communication.service'; import type { DataTuple, IDescriptionValueTuple, IDimensionTuple } from '@kompakkt/common'; +import { CounterService } from 'src/app/services/single-number-counter.service'; @Component({ selector: 'app-optional-card-list', @@ -26,11 +27,18 @@ import type { DataTuple, IDescriptionValueTuple, IDimensionTuple } from '@kompak }) export class OptionalCardListComponent { #metadataCommunicationService = inject(MetadataCommunicationService); + #counterService = inject(CounterService); optionalData = input.required(); propertyType = input(''); public onRemove(index: number) { + const item = this.optionalData()[index]; + + if (this.#counterService.isNew(item)) { + this.#counterService.decrementCounter(this.propertyType()); + this.#counterService.removeTracking(item); + } this.optionalData().splice(index, 1); } diff --git a/src/app/services/dialog-helper.service.ts b/src/app/services/dialog-helper.service.ts index 604b947b..90d352d9 100644 --- a/src/app/services/dialog-helper.service.ts +++ b/src/app/services/dialog-helper.service.ts @@ -32,6 +32,7 @@ import { RemoveFromCompilationResult, } from '../dialogs/remove-from-compilation/remove-from-compilation.component'; import { ManageOwnershipComponent } from '../dialogs/manage-ownership/manage-ownership.component'; +import { CounterService } from './single-number-counter.service'; @Injectable({ providedIn: 'root', @@ -40,6 +41,7 @@ export class DialogHelperService { #account = inject(AccountService); #dialog = inject(MatDialog); #events = inject(EventsService); + #counter = inject(CounterService); public openLoginDialog() { const ref = this.#dialog.open(AuthDialogComponent); @@ -120,6 +122,7 @@ export class DialogHelperService { firstValueFrom(ref.afterClosed()).then(() => { this.#account.updateTrigger$.next(Collection.entity); this.#events.updateSearchEvent(); + this.#counter.resetAll(); }); return ref; diff --git a/src/app/services/single-number-counter.service.ts b/src/app/services/single-number-counter.service.ts new file mode 100644 index 00000000..f3299000 --- /dev/null +++ b/src/app/services/single-number-counter.service.ts @@ -0,0 +1,45 @@ +import { Injectable, signal, WritableSignal } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class CounterService { + private counters = new Map>(); + private newItems = new Set(); + + trackAsNew(item: object) { + this.newItems.add(item); + } + + isNew(item: object): boolean { + return this.newItems.has(item); + } + + removeTracking(item: object) { + this.newItems.delete(item); + } + + getCounter(key: string): WritableSignal { + if (!this.counters.has(key)) { + const newSignal = signal(0); + this.counters.set(key, newSignal); + return newSignal; + } + return this.counters.get(key)!; + } + + incrementCounter(key: string) { + const counter = this.getCounter(key); + if (!counter) return; + counter.update(value => value + 1); + } + + decrementCounter(key: string) { + const counter = this.getCounter(key); + if (!counter) return; + counter.update(value => Math.max(0, value - 1)); + } + + resetAll(): void { + this.counters.forEach(counter => counter.set(0)); + this.newItems.clear(); + } +} From 62f422d1844a1cdbe864b907986892ad62b44578 Mon Sep 17 00:00:00 2001 From: Vera Malieske Date: Mon, 11 May 2026 15:43:51 +0200 Subject: [PATCH 3/4] Corrections - change name of editable-field-font-color to display the purpose and make it global - fix issue where existing agents were not found in autoselect by changing the transform params to check for contact-references and name - delete unnecessary rolePipe and get the right display-format from function on entity - delete overpowered counter-system and change it to a regular display of all saved optional data on the specific data-button - update getter on biblioRef as signal to prevent error of change-detection-cycle - fix issue where changes on agent were not detected by updating the specific agent as a new Agent to kick the signal-detection --- .../metadata/agents/agents.component.html | 32 ++++----- .../metadata/agents/agents.component.scss | 4 -- .../metadata/agents/agents.component.ts | 65 +++++++++++++------ .../agents/is-agent-in-entity.pipe.ts | 15 ++++- .../metadata/entity/entity.component.html | 41 +++++------- .../metadata/entity/entity.component.ts | 16 +---- .../biblio-ref/biblio-ref.component.html | 6 +- .../biblio-ref/biblio-ref.component.ts | 22 ++++--- .../optional/creation/creation.component.html | 2 +- .../optional/creation/creation.component.ts | 16 +---- .../external-ids/external-ids.component.html | 2 +- .../external-ids/external-ids.component.ts | 6 +- .../optional/links/links.component.html | 2 +- .../optional/links/links.component.ts | 5 +- .../metadata-files.component.html | 2 +- .../metadata-files.component.ts | 13 +--- .../optional-card-list.component.ts | 13 +--- src/app/metadata/index.ts | 48 ++++++++++++-- src/app/pipes/role-label.pipe.ts | 11 ---- src/app/services/dialog-helper.service.ts | 8 +-- .../services/single-number-counter.service.ts | 45 ------------- src/styles.scss | 5 ++ 22 files changed, 169 insertions(+), 210 deletions(-) delete mode 100644 src/app/pipes/role-label.pipe.ts delete mode 100644 src/app/services/single-number-counter.service.ts diff --git a/src/app/components/metadata/agents/agents.component.html b/src/app/components/metadata/agents/agents.component.html index 5c900a13..63de60a0 100644 --- a/src/app/components/metadata/agents/agents.component.html +++ b/src/app/components/metadata/agents/agents.component.html @@ -27,13 +27,13 @@ @if (agent | isAgentSelf) { (you) } - @if (entity() | isAgentInEntity: agent) { + @if (entity() | isAgentInEntity: agent : entityId()) { (already added, see below to edit) } {{ agent.fullName }} @@ -65,12 +65,12 @@ @if (agent | isAgentSelf) { (you) } - @if (entity() | isAgentInEntity: agent) { + @if (entity() | isAgentInEntity: agent : entityId()) { (already added, see below to edit) } {{ agent.fullName }} @@ -90,7 +90,7 @@ name="email" label="{{ 'E-Mail address' | translate }}" [formControl]="formControls.mail" - [class.font-color-on-edit]="formControls.mail.disabled && formControls.mail.value" + [class.editable-input-field]="formControls.mail.disabled && formControls.mail.value" > @if (agentIsEditable()) { @@ -103,8 +103,8 @@ label_important {{ 'External Identifiers' | translate }} - @if (idCount()) { - {{ '(' + idCount() + ')' }} + @if (entity.externalId.length; as count) { + {{ '(' + count + ')' }} } @@ -123,8 +123,8 @@ label_important {{ 'External Links' | translate }} - @if (linkCount()) { - {{ '(' + linkCount() + ')' }} + @if (entity.externalLink.length; as count) { + {{ '(' + count + ')' }} } diff --git a/src/app/components/metadata/optional/metadata-files/metadata-files.component.ts b/src/app/components/metadata/optional/metadata-files/metadata-files.component.ts index 5c5130ba..26e5205e 100644 --- a/src/app/components/metadata/optional/metadata-files/metadata-files.component.ts +++ b/src/app/components/metadata/optional/metadata-files/metadata-files.component.ts @@ -1,10 +1,9 @@ -import { Component, computed, EventEmitter, inject, input, Output } from '@angular/core'; +import { Component, computed, input } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; import { AnyEntity, FileTuple, PhysicalEntity } from 'src/app/metadata'; import { FilesizePipe, TranslatePipe } from 'src/app/pipes'; -import { CounterService } from 'src/app/services/single-number-counter.service'; @Component({ selector: 'app-metadata-files', @@ -15,21 +14,12 @@ import { CounterService } from 'src/app/services/single-number-counter.service'; }) export class MetadataFilesComponent { public entity = input.required(); - public propertyType = 'metadata_files'; - @Output() itemAdded = new EventEmitter<{ item: object; type: string }>(); isPhysical = computed(() => this.entity() instanceof PhysicalEntity); - #counterService = inject(CounterService); - public removeProperty(property: string, index: number) { if (Array.isArray(this.entity()[property])) { const removed = this.entity()[property].splice(index, 1)[0]; - if (this.#counterService.isNew(removed)) { - this.#counterService.decrementCounter(this.propertyType); - this.#counterService.removeTracking(removed); - } - if (!removed) { return console.warn('No item removed'); } @@ -88,7 +78,6 @@ export class MetadataFilesComponent { const metadataFile = await readfile(file); if (!metadataFile) continue; this.entity().metadata_files.push(metadataFile); - this.itemAdded.emit({ item: metadataFile, type: this.propertyType }); } } } diff --git a/src/app/components/metadata/optional/optional-card-list/optional-card-list.component.ts b/src/app/components/metadata/optional/optional-card-list/optional-card-list.component.ts index c1717924..1164fd2f 100644 --- a/src/app/components/metadata/optional/optional-card-list/optional-card-list.component.ts +++ b/src/app/components/metadata/optional/optional-card-list/optional-card-list.component.ts @@ -1,5 +1,4 @@ -import { KeyValuePipe } from '@angular/common'; -import { Component, inject, input, Pipe, PipeTransform } from '@angular/core'; +import { Component, inject, input } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -9,8 +8,7 @@ import { IsDimensionTuple, } from 'src/app/pipes/tuple-helper.pipes'; import { MetadataCommunicationService } from 'src/app/services/metadata-communication.service'; -import type { DataTuple, IDescriptionValueTuple, IDimensionTuple } from '@kompakkt/common'; -import { CounterService } from 'src/app/services/single-number-counter.service'; +import type { DataTuple } from '@kompakkt/common'; @Component({ selector: 'app-optional-card-list', @@ -27,18 +25,11 @@ import { CounterService } from 'src/app/services/single-number-counter.service'; }) export class OptionalCardListComponent { #metadataCommunicationService = inject(MetadataCommunicationService); - #counterService = inject(CounterService); optionalData = input.required(); propertyType = input(''); public onRemove(index: number) { - const item = this.optionalData()[index]; - - if (this.#counterService.isNew(item)) { - this.#counterService.decrementCounter(this.propertyType()); - this.#counterService.removeTracking(item); - } this.optionalData().splice(index, 1); } diff --git a/src/app/metadata/index.ts b/src/app/metadata/index.ts index ce3c7f42..9ea4aa3d 100644 --- a/src/app/metadata/index.ts +++ b/src/app/metadata/index.ts @@ -18,7 +18,6 @@ import { ITag, ITypeValueTuple, } from '@kompakkt/common'; -import { RoleLabelPipe } from '../pipes/role-label.pipe'; const getObjectId = () => new ObjectID().toString(); @@ -51,6 +50,13 @@ class BaseEntity implements IBaseEntity { persons = new Array(); institutions = new Array(); + public formatRoleLabel(role: string): string { + return role + .replace(/_/g, ' ') + .toLowerCase() + .replace(/^\w/, c => c.toUpperCase()); + } + constructor(obj: Partial = {}) { this._id = obj._id?.toString() ?? this._id; @@ -94,6 +100,36 @@ class BaseEntity implements IBaseEntity { this.institutions.push(new Institution(institution)); } + public updatePerson( + agentId: string, + entityId: string, + changes: Partial, + ): Person | undefined { + const index = this.persons.findIndex(p => p._id.toString() === agentId); + if (index === -1) return undefined; + + const existing = this.persons[index]; + Object.assign(existing, changes); + + this.persons[index] = new Person(existing); + return this.persons[index]; + } + + public updateInstitution( + agentId: string, + entityId: string, + changes: Partial, + ): Institution | undefined { + const index = this.institutions.findIndex(i => i._id.toString() === agentId); + if (index === -1) return undefined; + + const existing = this.institutions[index]; + Object.assign(existing, changes); + + this.institutions[index] = new Institution(existing); + return this.institutions[index]; + } + public static checkIsValid(entity: BaseEntity): boolean { const { title, @@ -172,7 +208,11 @@ class BaseEntity implements IBaseEntity { return [...filteredPersons, ...filteredInstitutions]; } - public customRolesList(): { role: string; label: string; agents: (Person | Institution)[] }[] { + public customRolesList(): { + role: string; + label: string; + agents: (Person | Institution)[]; + }[] { const { persons, institutions, _id } = this; const fixedRoles = ['RIGHTS_OWNER', 'CONTACT_PERSON']; @@ -183,11 +223,9 @@ class BaseEntity implements IBaseEntity { .forEach(r => customRoles.add(r)); }); - const roleLabelPipe = new RoleLabelPipe(); - return Array.from(customRoles).map(role => ({ role, - label: roleLabelPipe.transform(role), + label: this.formatRoleLabel(role.trim()), agents: [ ...persons.filter(p => Person.hasRole(p, _id, role)), ...institutions.filter(i => Institution.hasRole(i, _id, role)), diff --git a/src/app/pipes/role-label.pipe.ts b/src/app/pipes/role-label.pipe.ts deleted file mode 100644 index 6376fde7..00000000 --- a/src/app/pipes/role-label.pipe.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ name: 'roleLabel', standalone: true }) -export class RoleLabelPipe implements PipeTransform { - transform(role: string): string { - return role - .replace(/_/g, ' ') - .toLowerCase() - .replace(/^\w/, c => c.toUpperCase()); - } -} diff --git a/src/app/services/dialog-helper.service.ts b/src/app/services/dialog-helper.service.ts index 90d352d9..ff17dbd8 100644 --- a/src/app/services/dialog-helper.service.ts +++ b/src/app/services/dialog-helper.service.ts @@ -32,7 +32,6 @@ import { RemoveFromCompilationResult, } from '../dialogs/remove-from-compilation/remove-from-compilation.component'; import { ManageOwnershipComponent } from '../dialogs/manage-ownership/manage-ownership.component'; -import { CounterService } from './single-number-counter.service'; @Injectable({ providedIn: 'root', @@ -41,7 +40,6 @@ export class DialogHelperService { #account = inject(AccountService); #dialog = inject(MatDialog); #events = inject(EventsService); - #counter = inject(CounterService); public openLoginDialog() { const ref = this.#dialog.open(AuthDialogComponent); @@ -122,7 +120,6 @@ export class DialogHelperService { firstValueFrom(ref.afterClosed()).then(() => { this.#account.updateTrigger$.next(Collection.entity); this.#events.updateSearchEvent(); - this.#counter.resetAll(); }); return ref; @@ -131,7 +128,10 @@ export class DialogHelperService { public editVisibilityAndAccess(element: IEntity[] | ICompilation[]) { const ref = this.#dialog.open( VisibilityAndAccessDialogComponent, - { data: element, disableClose: true }, + { + data: element, + disableClose: true, + }, ); firstValueFrom(ref.afterClosed()).then(() => { diff --git a/src/app/services/single-number-counter.service.ts b/src/app/services/single-number-counter.service.ts deleted file mode 100644 index f3299000..00000000 --- a/src/app/services/single-number-counter.service.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable, signal, WritableSignal } from '@angular/core'; - -@Injectable({ providedIn: 'root' }) -export class CounterService { - private counters = new Map>(); - private newItems = new Set(); - - trackAsNew(item: object) { - this.newItems.add(item); - } - - isNew(item: object): boolean { - return this.newItems.has(item); - } - - removeTracking(item: object) { - this.newItems.delete(item); - } - - getCounter(key: string): WritableSignal { - if (!this.counters.has(key)) { - const newSignal = signal(0); - this.counters.set(key, newSignal); - return newSignal; - } - return this.counters.get(key)!; - } - - incrementCounter(key: string) { - const counter = this.getCounter(key); - if (!counter) return; - counter.update(value => value + 1); - } - - decrementCounter(key: string) { - const counter = this.getCounter(key); - if (!counter) return; - counter.update(value => Math.max(0, value - 1)); - } - - resetAll(): void { - this.counters.forEach(counter => counter.set(0)); - this.newItems.clear(); - } -} diff --git a/src/styles.scss b/src/styles.scss index 2d943e66..a334388d 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -985,3 +985,8 @@ div.option-count-badge { background-color: transparent; cursor: pointer; } +.editable-input-field { + input:disabled { + color: black !important; + } +} From 6d53d106e2e0c1785691ce12550d042b164b1733 Mon Sep 17 00:00:00 2001 From: Vera Malieske Date: Tue, 12 May 2026 21:51:10 +0200 Subject: [PATCH 4/4] Corrections Part 2 - cleanup unused methods - set optional chaining when referencing a specific field on entity to prevent legacy-errors - revision roleName-formatting for simplicity and clarity as well as displaying custom names the way users provide them --- .../metadata/agents/agents.component.ts | 17 ++++----- .../agents/is-agent-in-entity.pipe.ts | 6 +-- src/app/metadata/index.ts | 37 +------------------ 3 files changed, 13 insertions(+), 47 deletions(-) diff --git a/src/app/components/metadata/agents/agents.component.ts b/src/app/components/metadata/agents/agents.component.ts index 6a70a4fa..38e75b3b 100644 --- a/src/app/components/metadata/agents/agents.component.ts +++ b/src/app/components/metadata/agents/agents.component.ts @@ -341,7 +341,7 @@ export class AgentsComponent implements OnDestroy, OnChanges, OnInit { const customRole = this.newCustomRole.checked && this.newCustomRole.value.trim() - ? this.newCustomRole.value.trim().toUpperCase().replace(/\s+/g, '_') + ? this.entity().formatRoleLabel(this.newCustomRole.value) : ''; return customRole ? [...availableRoleTypes, customRole] : availableRoleTypes; @@ -504,21 +504,20 @@ export class AgentsComponent implements OnDestroy, OnChanges, OnInit { } private addCustomRole() { - if (!this.newCustomRole.checked || !this.newCustomRole.value.trim()) return; + const newCustomRole = this.newCustomRole; + if (!newCustomRole.checked || !newCustomRole.value.trim()) return; - const type = this.newCustomRole.value.trim().toUpperCase().replace(/\s+/g, '_'); - this.addRoleIfNotExists(type); + this.addRoleIfNotExists(newCustomRole.value); } private addRoleIfNotExists(newRoleName: string) { - const normalizedType = newRoleName.trim().toUpperCase(); - if (!normalizedType || this.availableRoles.some(r => r.type === normalizedType)) return; - const entity = this.entity(); + const cleanedUpRoleName = entity.formatRoleLabel(newRoleName); + if (!cleanedUpRoleName || this.availableRoles.some(r => r.value === cleanedUpRoleName)) return; this.availableRoles.push({ - type: normalizedType, - value: entity.formatRoleLabel(newRoleName), + type: cleanedUpRoleName, + value: cleanedUpRoleName, checked: false, }); } diff --git a/src/app/components/metadata/agents/is-agent-in-entity.pipe.ts b/src/app/components/metadata/agents/is-agent-in-entity.pipe.ts index 127317fb..716c4949 100644 --- a/src/app/components/metadata/agents/is-agent-in-entity.pipe.ts +++ b/src/app/components/metadata/agents/is-agent-in-entity.pipe.ts @@ -8,15 +8,15 @@ export class IsAgentInEntityPipe implements PipeTransform { if (isPerson(agent)) { return value.persons.some( p => - p.contact_references[entityId].mail === agent.contact_references[entityId].mail && + p.contact_references[entityId]?.mail === agent.contact_references[entityId]?.mail && p.fullName === agent.fullName, ); } else if (isInstitution(agent)) { return value.institutions.some( i => i.name === agent.name && - i.addresses[entityId].street === agent.addresses[entityId].street && - i.addresses[entityId].number === agent.addresses[entityId].number, + i.addresses[entityId]?.street === agent.addresses[entityId]?.street && + i.addresses[entityId]?.number === agent.addresses[entityId]?.number, ); } return false; diff --git a/src/app/metadata/index.ts b/src/app/metadata/index.ts index 9ea4aa3d..b12f00a1 100644 --- a/src/app/metadata/index.ts +++ b/src/app/metadata/index.ts @@ -51,10 +51,7 @@ class BaseEntity implements IBaseEntity { institutions = new Array(); public formatRoleLabel(role: string): string { - return role - .replace(/_/g, ' ') - .toLowerCase() - .replace(/^\w/, c => c.toUpperCase()); + return role.trim().replace(/\s+/g, ' '); } constructor(obj: Partial = {}) { @@ -100,36 +97,6 @@ class BaseEntity implements IBaseEntity { this.institutions.push(new Institution(institution)); } - public updatePerson( - agentId: string, - entityId: string, - changes: Partial, - ): Person | undefined { - const index = this.persons.findIndex(p => p._id.toString() === agentId); - if (index === -1) return undefined; - - const existing = this.persons[index]; - Object.assign(existing, changes); - - this.persons[index] = new Person(existing); - return this.persons[index]; - } - - public updateInstitution( - agentId: string, - entityId: string, - changes: Partial, - ): Institution | undefined { - const index = this.institutions.findIndex(i => i._id.toString() === agentId); - if (index === -1) return undefined; - - const existing = this.institutions[index]; - Object.assign(existing, changes); - - this.institutions[index] = new Institution(existing); - return this.institutions[index]; - } - public static checkIsValid(entity: BaseEntity): boolean { const { title, @@ -225,7 +192,7 @@ class BaseEntity implements IBaseEntity { return Array.from(customRoles).map(role => ({ role, - label: this.formatRoleLabel(role.trim()), + label: this.formatRoleLabel(role), agents: [ ...persons.filter(p => Person.hasRole(p, _id, role)), ...institutions.filter(i => Institution.hasRole(i, _id, role)),