diff --git a/src/app/components/entity-detail/detail-entity/detail-entity.component.html b/src/app/components/entity-detail/detail-entity/detail-entity.component.html index a2b235ee..5f991343 100644 --- a/src/app/components/entity-detail/detail-entity/detail-entity.component.html +++ b/src/app/components/entity-detail/detail-entity/detail-entity.component.html @@ -62,7 +62,9 @@

{{ physicalEntity.title }}

@if (hasPersonsOrInstitutions()) { - {{ 'Persons and Institutions' | translate }} + + {{ 'Persons and Institutions' | translate }} + @if (persons(); as persons) { @@ -134,28 +136,6 @@

{{ physicalEntity.title }}

}
} - @if (hasOtherMetadata()) { - - - {{ 'Other' | translate }} - - - @for (entry of otherMetadata(); track entry) { -
- @if (entry.description) { -

- {{ entry.description }} -

- } - @if (entry.value) { -

- {{ entry.value }} -

- } -
- } -
- } @if (hasMetadataFiles()) { @@ -200,7 +180,9 @@

{{ physicalEntity.title }}

@if (hasPersonsOrInstitutions()) { - {{ 'Persons and Institutions' | translate }} + + {{ 'Persons and Institutions' | translate }} + @if (persons(); as persons) { @@ -272,28 +254,6 @@

{{ physicalEntity.title }}

}
} - @if (hasOtherMetadata()) { - - - {{ 'Other' | translate }} - - - @for (entry of otherMetadata(); track entry) { -
- @if (entry.description) { -

- {{ entry.description }} -

- } - @if (entry.value) { -

- {{ entry.value }} -

- } -
- } -
- } @if (hasMetadataFiles()) { diff --git a/src/app/components/entity-detail/detail-entity/detail-entity.component.ts b/src/app/components/entity-detail/detail-entity/detail-entity.component.ts index b5b42411..56c1fa3a 100644 --- a/src/app/components/entity-detail/detail-entity/detail-entity.component.ts +++ b/src/app/components/entity-detail/detail-entity/detail-entity.component.ts @@ -46,15 +46,6 @@ export class DetailEntityComponent { return this.metadataFiles().length > 0; }); - otherMetadata = computed(() => { - const entity = this.entity(); - return entity.other; - }); - - hasOtherMetadata = computed(() => { - return this.otherMetadata().length > 0; - }); - bibRefs = computed(() => { const entity = this.entity(); return entity.biblioRefs; diff --git a/src/app/components/metadata/address/address.component.html b/src/app/components/metadata/address/address.component.html index ab361e90..fd04d28b 100644 --- a/src/app/components/metadata/address/address.component.html +++ b/src/app/components/metadata/address/address.component.html @@ -9,75 +9,58 @@ @if (address) {
- - {{ 'Country' | translate }} - - - - - {{ 'Postal Code' | translate }} - - - - - {{ 'City' | translate }} - - - - - {{ 'Street' | translate }} - - - - - {{ 'Number' | translate }} - - - - - {{ 'Building' | translate }} - - - + + + + + + + + + + + + +
} diff --git a/src/app/components/metadata/address/address.component.ts b/src/app/components/metadata/address/address.component.ts index 7d15a706..e3bc034d 100644 --- a/src/app/components/metadata/address/address.component.ts +++ b/src/app/components/metadata/address/address.component.ts @@ -1,15 +1,14 @@ import { Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatFormField, MatLabel } from '@angular/material/form-field'; -import { MatInput } from '@angular/material/input'; import { Address } from 'src/app/metadata'; import { TranslatePipe } from '../../../pipes/translate.pipe'; +import { OutlinedInputComponent } from '../../outlined-input/outlined-input.component'; @Component({ selector: 'app-address', templateUrl: './address.component.html', styleUrls: ['./address.component.scss'], - imports: [MatFormField, MatLabel, MatInput, FormsModule, TranslatePipe], + imports: [FormsModule, TranslatePipe, OutlinedInputComponent], }) export class AddressComponent { @Input('address') diff --git a/src/app/components/metadata/agents/agents.component.html b/src/app/components/metadata/agents/agents.component.html index 00e75dbc..4973e080 100644 --- a/src/app/components/metadata/agents/agents.component.html +++ b/src/app/components/metadata/agents/agents.component.html @@ -1,3 +1,9 @@ +

+ {{ 'Related persons and institutions' | translate }} +

(); entityId = computed(() => this.entity()._id); + isPhysicalObject = input(false); + @ViewChild('agentGroup') agentGroup!: MatTabGroup; selectedType = signal<'person' | 'institution'>('person'); diff --git a/src/app/components/metadata/entity/entity.component.html b/src/app/components/metadata/entity/entity.component.html index 77cd0719..2dabb78c 100644 --- a/src/app/components/metadata/entity/entity.component.html +++ b/src/app/components/metadata/entity/entity.component.html @@ -61,139 +61,113 @@ } {{ 'Related persons and institutions' | translate }} + + + +

{{ 'Optional metadata:' | translate }}

+
+ -

{{ 'Optional metadata:' | translate }}

- - + + - - - - + - - - + - + {{ 'Metadata files' | translate }} + + +
@@ -212,11 +186,7 @@ @if (digitalEntity(); as digitalEntity) { -

- {{ - 'Select a license that fits the way other users are allowed to use it.' | translate - }} -

+
@for (item of availableLicences | keyvalue; track item.key) { @@ -241,23 +211,15 @@
- +
- - - @if (digitalEntity(); as digitalEntity) { - - } - - - @if (digitalEntity(); as digitalEntity) { - + } @@ -267,33 +229,27 @@ @if (digitalEntity(); as digitalEntity) { - + } - + - + - - - - - - @if (digitalEntity(); as digitalEntity) { - + } @@ -301,7 +257,7 @@ @if (digitalEntity(); as digitalEntity) { - + } diff --git a/src/app/components/metadata/entity/entity.component.scss b/src/app/components/metadata/entity/entity.component.scss index 88f41f33..5afdbabc 100644 --- a/src/app/components/metadata/entity/entity.component.scss +++ b/src/app/components/metadata/entity/entity.component.scss @@ -131,10 +131,19 @@ app-agent-card:not(:last-child) { } .mat-expansion-panel { + .mat-expansion-panel-header.mat-expanded { + border-color: transparent; + } + + .mat-expansion-panel-header, + .mat-expansion-panel-header.mat-expanded { + height: 44px; + padding: 0 16px; + } .mat-expansion-panel-body { - margin-top: 1rem; display: flex; flex-direction: column; + padding: 0; } &.single-column .mat-expansion-panel-body { > div.card + div.card { @@ -222,7 +231,7 @@ mat-tab-group.mat-mdc-tab-group.mat-primary.mat-mdc-tab-group-stretch-tabs { } .optional-text { - font-size: 14px; + font-size: 16px; font-weight: bold; } @@ -280,3 +289,7 @@ div.card .actions { .mat-mdc-list-item:focus { background-color: transparent !important; } + +.expansion-divider { + border-top-width: 2px; +} diff --git a/src/app/components/metadata/entity/entity.component.ts b/src/app/components/metadata/entity/entity.component.ts index ea87ea55..48a54bb1 100644 --- a/src/app/components/metadata/entity/entity.component.ts +++ b/src/app/components/metadata/entity/entity.component.ts @@ -36,12 +36,11 @@ import { AgentsComponent } from '../agents/agents.component'; import { GeneralComponent } from '../general/general.component'; import { BiblioRefComponent } from '../optional/biblio-ref/biblio-ref.component'; import { CreationComponent } from '../optional/creation/creation.component'; -import { DimensionComponent } from '../optional/dimension/dimension.component'; import { ExternalIdsComponent } from '../optional/external-ids/external-ids.component'; import { LinksComponent } from '../optional/links/links.component'; import { MetadataFilesComponent } from '../optional/metadata-files/metadata-files.component'; -import { OtherComponent } from '../optional/other/other.component'; import { PhysObjComponent } from '../optional/phys-obj/phys-obj.component'; +import { MatExpansionModule } from '@angular/material/expansion'; type AnyEntity = DigitalEntity | PhysicalEntity; @@ -64,12 +63,11 @@ type AnyEntity = DigitalEntity | PhysicalEntity; LinksComponent, PhysObjComponent, GeneralComponent, - DimensionComponent, ExternalIdsComponent, BiblioRefComponent, MetadataFilesComponent, - OtherComponent, KeyValuePipe, + MatExpansionModule, ], }) export class EntityComponent { @@ -89,12 +87,10 @@ export class EntityComponent { 'General', 'Licence', 'Related', - 'Dimensions', 'Creation', 'Ids', 'Links', 'References', - 'Other', 'Files', 'Physical', ]; @@ -319,11 +315,6 @@ export class EntityComponent { return undefined === entity.biblioRefs.find(c => !DescriptionValueTuple.checkIsValid(c, false)); } - get otherValid() { - const entity = this.entity(); - return undefined === entity.other.find(c => !DescriptionValueTuple.checkIsValid(c)); - } - get metadataFilesValid() { const entity = this.entity(); return undefined === entity.metadata_files.find(c => !FileTuple.checkIsValid(c)); @@ -381,8 +372,6 @@ export class EntityComponent { return entity.externalLink.push(new DescriptionValueTuple()); case 'biblioRefs': return entity.biblioRefs.push(new DescriptionValueTuple()); - case 'other': - return entity.other.push(new DescriptionValueTuple()); case 'metadata_files': const input = document.createElement('input'); input.type = 'file'; diff --git a/src/app/components/metadata/general/general.component.html b/src/app/components/metadata/general/general.component.html index 5a3ab8a8..0872e819 100644 --- a/src/app/components/metadata/general/general.component.html +++ b/src/app/components/metadata/general/general.component.html @@ -1,118 +1,68 @@ @if (entity(); as entity) { - - {{ 'Title' | translate }} - - - - {{ 'Description' | translate }} - - +

+ {{ 'General Information' | translate }} +

+ + + } @if (physicalEntity(); as physicalEntity) { - - {{ 'Collection' | translate }} - - + } @if (digitalEntity(); as digitalEntity) { - - {{ 'Statement' | translate }} - - - - - {{ 'Object type' | translate }} - - - - - {{ 'Tags' | translate }} - - @for (tag of digitalEntity.tags; track tag; let index = $index) { - - {{ tag.value }} - cancel - - } - - + @for (tag of filteredTags$ | async; track tag) { - - {{ tag.value }} - + {{ tag.value }} } - {{ 'Seperate many tags by pressing comma or enter/return' | translate }} - + - - {{ 'Discipline' | translate }} - - @for (discipline of digitalEntity.discipline; track discipline; let index = $index) { - - {{ discipline }} - cancel - - } - - - - {{ 'Seperate many disciplines by pressing comma or enter/return' | translate }} - - +

{{ 'Seperate many tags by pressing comma or enter/return' | translate }}

+ + + @for (tag of digitalEntity.tags; track tag; let index = $index) { + + {{ tag.value }} + cancel + + } + } diff --git a/src/app/components/metadata/general/general.component.scss b/src/app/components/metadata/general/general.component.scss index 862fb592..65eccce4 100644 --- a/src/app/components/metadata/general/general.component.scss +++ b/src/app/components/metadata/general/general.component.scss @@ -1,13 +1,3 @@ -::ng-deep { - .mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) .mdc-evolution-chip__text-label { - color: var(--mat-chip-label-text-color, var(--mat-app-on-surface-variant)) !important; - } - - .mat-mdc-form-field-hint-wrapper { - padding: 0 !important; - } -} - -mat-form-field { - margin-bottom: 20px; +.tag-hint { + font-size: 12px; } diff --git a/src/app/components/metadata/general/general.component.ts b/src/app/components/metadata/general/general.component.ts index 994df547..d9780e12 100644 --- a/src/app/components/metadata/general/general.component.ts +++ b/src/app/components/metadata/general/general.component.ts @@ -1,21 +1,12 @@ -import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { AsyncPipe } from '@angular/common'; -import { - Component, - computed, - ElementRef, - EventEmitter, - input, - Output, - ViewChild, -} from '@angular/core'; +import { Component, computed, EventEmitter, input, Output } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule, type MatAutocompleteSelectedEvent, } from '@angular/material/autocomplete'; -import { type MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; +import { MatChipsModule } from '@angular/material/chips'; import { MatOptionModule } from '@angular/material/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; @@ -27,6 +18,7 @@ import { TranslatePipe } from 'src/app/pipes'; import { ContentProviderService } from 'src/app/services'; import { MetadataCommunicationService } from 'src/app/services/metadata-communication.service'; import { isDigitalEntity, isPhysicalEntity } from '@kompakkt/common'; +import { OutlinedInputComponent } from '../../outlined-input/outlined-input.component'; @Component({ selector: 'app-general', @@ -42,6 +34,7 @@ import { isDigitalEntity, isPhysicalEntity } from '@kompakkt/common'; ReactiveFormsModule, FormsModule, AsyncPipe, + OutlinedInputComponent, ], templateUrl: './general.component.html', styleUrl: './general.component.scss', @@ -61,10 +54,7 @@ export class GeneralComponent { @Output() remove = new EventEmitter(); - @ViewChild('tagInput') tagInput!: ElementRef; - - public searchTag = new FormControl(''); - public separatorKeysCodes: number[] = [ENTER, COMMA]; + public searchTag = new FormControl('', { nonNullable: true }); public availableTags = new BehaviorSubject([]); @@ -72,6 +62,8 @@ export class GeneralComponent { private isInSelection: boolean = false; + public displayTag = () => ''; + constructor( public content: ContentProviderService, private metaService: MetadataCommunicationService, @@ -82,7 +74,7 @@ export class GeneralComponent { this.filteredTags$ = this.searchTag.valueChanges.pipe( startWith(''), - map(value => (value as string).toLowerCase()), + map(value => (value ?? '').toLowerCase()), // null-safe machen withLatestFrom(this.digitalEntity$), map(([value, digitalEntity]) => this.availableTags.value @@ -92,31 +84,13 @@ export class GeneralComponent { ); } - public addTag(event: MatChipInputEvent, digitalEntity: DigitalEntity) { - const tagText = event.value; - if (tagText !== '' && !this.isInSelection) { - const tag = new Tag(); - tag.value = tagText; - digitalEntity.addTag(tag); - // this.searchTag.patchValue(''); - // this.searchTag.setValue(''); - event.chipInput.inputElement.value = ''; - } - } - - public addDiscipline(event: MatChipInputEvent, digitalEntity: DigitalEntity) { - const discipline = event.value; - digitalEntity.discipline.push(discipline); - event.chipInput.inputElement.value = ''; - } - public async selectTag(event: MatAutocompleteSelectedEvent, digitalEntity: DigitalEntity) { const tagId = event.option.value; const tag = this.availableTags.value.find(t => t._id === tagId); - if (!tag) return console.warn(`Could not tag with id ${tagId}`); + if (!tag) return console.warn(`Could not find tag with id ${tagId}`); this.isInSelection = true; digitalEntity.addTag(tag); - this.tagInput.nativeElement.value = ''; + this.searchTag.setValue(''); setTimeout(() => (this.isInSelection = false)); } @@ -130,4 +104,14 @@ export class GeneralComponent { public onRemove(property: string, index: number) { this.remove.emit({ property, index }); } + + onChipKeydown(entity: DigitalEntity): void { + const inputValue = this.searchTag.value?.trim(); + if (!inputValue || this.isInSelection) return; + + const tag = new Tag(); + tag.value = inputValue; + entity.addTag(tag); + this.searchTag.setValue(''); + } } 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 ab2f039b..a98ef775 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 @@ -1,38 +1,43 @@
- - {{ 'Description' | translate }} - +

+ {{ 'Bibliographic references' | translate }} +

+ + + @if (dataIsEditable) { - } -
- - {{ 'Reference e.g. Title, Author, Year)' | translate }} - + + + @if (dataIsEditable) { - } - +
+
- +@if (!isPhysical()) { + +} diff --git a/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.scss b/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.scss index ad070749..fa150bdf 100644 --- a/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.scss +++ b/src/app/components/metadata/optional/biblio-ref/biblio-ref.component.scss @@ -2,6 +2,7 @@ margin: 20px 0; display: flex; justify-content: flex-end; + gap: 10px; } .card { @@ -26,8 +27,3 @@ div.card .actions { right: 5px !important; margin-top: auto; } - -.editButton { - border: none; - background-color: transparent; -} 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 3476839f..d8afb365 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, input } from '@angular/core'; +import { Component, computed, input } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -8,11 +8,12 @@ import { MatInputModule } from '@angular/material/input'; import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; import { Subscription } from 'rxjs'; -import { AnyEntity, DescriptionValueTuple } from 'src/app/metadata'; +import { AnyEntity, DescriptionValueTuple, PhysicalEntity } from 'src/app/metadata'; import { MetadataCommunicationService } from 'src/app/services/metadata-communication.service'; import { DataTuple } from '@kompakkt/common'; import { TranslatePipe } from '../../../../pipes/translate.pipe'; import { OptionalCardListComponent } from '../optional-card-list/optional-card-list.component'; +import { OutlinedInputComponent } from 'src/app/components/outlined-input/outlined-input.component'; @Component({ selector: 'app-biblio-ref', @@ -26,12 +27,14 @@ import { OptionalCardListComponent } from '../optional-card-list/optional-card-l ReactiveFormsModule, TranslatePipe, OptionalCardListComponent, + OutlinedInputComponent, ], templateUrl: './biblio-ref.component.html', styleUrl: './biblio-ref.component.scss', }) export class BiblioRefComponent { public entity = input.required(); + isPhysical = computed(() => this.entity() instanceof PhysicalEntity); public referenceControl = new FormControl('', { nonNullable: true }); public descriptionControl = new FormControl('', { nonNullable: true }); @@ -77,8 +80,6 @@ export class BiblioRefComponent { this.entity().biblioRefs[this.dataIndex].value = this.referenceControl.value ?? ''; this.resetFormFields(); - this.isUpdating = false; - this.dataIsEditable = false; } onEditData(inputElementString: string): void { diff --git a/src/app/components/metadata/optional/creation/creation.component.html b/src/app/components/metadata/optional/creation/creation.component.html index 964bdf27..9287b89f 100644 --- a/src/app/components/metadata/optional/creation/creation.component.html +++ b/src/app/components/metadata/optional/creation/creation.component.html @@ -1,35 +1,50 @@
- - {{ ' Add technique (e.g. Laserscan or Modelling)' | translate }} - - - - {{ 'Add program' | translate }} - - - - {{ 'Add equipment' | translate }} - - - - {{ 'Add creation date (format: mm/dd/yyyy)' | translate }} - - - - -
-
- -
+ + + + + + + + - +
+ + + + +
+ - +
+ +
+ + + + +
diff --git a/src/app/components/metadata/optional/creation/creation.component.scss b/src/app/components/metadata/optional/creation/creation.component.scss index fb9b65e9..62b85a14 100644 --- a/src/app/components/metadata/optional/creation/creation.component.scss +++ b/src/app/components/metadata/optional/creation/creation.component.scss @@ -3,3 +3,9 @@ display: flex; justify-content: flex-end; } + +.material-date-input { + position: absolute; + visibility: hidden; + pointer-events: none; +} diff --git a/src/app/components/metadata/optional/creation/creation.component.ts b/src/app/components/metadata/optional/creation/creation.component.ts index 10825be4..038853b6 100644 --- a/src/app/components/metadata/optional/creation/creation.component.ts +++ b/src/app/components/metadata/optional/creation/creation.component.ts @@ -1,12 +1,6 @@ import { formatDate } from '@angular/common'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import { - AbstractControl, - FormControl, - ReactiveFormsModule, - ValidationErrors, - ValidatorFn, -} from '@angular/forms'; +import { ChangeDetectionStrategy, Component, computed, input, OnInit } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { provideNativeDateAdapter } from '@angular/material/core'; @@ -19,6 +13,9 @@ import { CreationTuple, DigitalEntity } from 'src/app/metadata'; import { TranslatePipe } from '../../../../pipes/translate.pipe'; import { OptionalCardListComponent } from '../optional-card-list/optional-card-list.component'; +import { OutlinedInputComponent } from 'src/app/components/outlined-input/outlined-input.component'; +import { toSignal } from '@angular/core/rxjs-interop'; + @Component({ selector: 'app-creation', standalone: true, @@ -31,73 +28,94 @@ import { OptionalCardListComponent } from '../optional-card-list/optional-card-l ReactiveFormsModule, TranslatePipe, OptionalCardListComponent, + OutlinedInputComponent, ], templateUrl: './creation.component.html', styleUrl: './creation.component.scss', providers: [provideNativeDateAdapter()], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CreationComponent { +export class CreationComponent implements OnInit { public entity = input.required(); - public techniqueControl = new FormControl(''); - public softwareControl = new FormControl(''); - public equipmentControl = new FormControl(''); - public dateControl = new FormControl(''); + form = new FormGroup({ + technique: new FormControl(''), + software: new FormControl(''), + equipment: new FormControl(''), + start: new FormControl(null), + end: new FormControl(null), + }); + private readonly _formValues = toSignal(this.form.valueChanges, { + initialValue: this.form.value, + }); private readonly _currentYear = new Date().getFullYear(); readonly minDate = new Date(this._currentYear - 20, 0, 1); addNewCreationData() { - const value = this.dateControl.value; + const value = this.form.value; + + let formattedDate = ''; + if (value.start && value.end) { + const startStr = formatDate(value.start, 'MM/dd/yyyy', 'en-US'); + const endStr = formatDate(value.end, 'MM/dd/yyyy', 'en-US'); + formattedDate = `${startStr} - ${endStr}`; + } const creationInstance = new CreationTuple({ - technique: this.techniqueControl.value ?? '', - program: this.softwareControl.value ?? '', - equipment: this.equipmentControl.value ?? '', - date: value ? formatDate(value, 'yyyy-dd-MM', 'en-US') : '', + technique: value.technique ?? '', + program: value.software ?? '', + equipment: value.equipment ?? '', + date: formattedDate, }); this.resetFormFields(); - - console.log(creationInstance); this.entity().creation.push(creationInstance); } - get dateFormat(): boolean { - return this.dateControl.valid; - } + isFormValid = computed(() => { + const v = this._formValues(); + return !!v.technique || !!v.software || !!v.equipment || (!!v.start && !!v.end); + }); - get isFormValid(): boolean { - return ( - this.techniqueControl.value !== '' || - this.softwareControl.value !== '' || - this.equipmentControl.value !== '' || - this.dateControl.value !== '' - ); + resetFormFields() { + this.form.reset(); + this.dateRangeDisplay.reset(); } - dateFormValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - if (!control.value) { - return null; - } + dateRangeDisplay = new FormControl('', { nonNullable: true }); + + onDateInput(event: Event): void { + const value = (event.target as HTMLInputElement).value; + const parts = value.split(/\s*[–-]\s*/).map(s => s.trim()); + if (parts.length !== 2) return; - // const dateRegex = /^\d{4}-\d{2}-\d{2}$/; - const dateRegex = /^\d{2}\/\d{2}\/\d{4}$/; - const isValid = dateRegex.test(control.value); - return isValid ? null : { invalidDateFormat: true }; - }; + const start = this.parseDate(parts[0]); + const end = this.parseDate(parts[1]); + + if (start && end) { + this.form.patchValue({ start, end }); + this.dateRangeDisplay.setValue(value); + } } - // get dateAlreadySet(): boolean { - // return this.entity.creation.some(set => !!set.date); - // } + private parseDate(str: string): Date | null { + const [day, month, year] = str.split('.').map(Number); + if (!day || !month || !year) return null; - resetFormFields() { - this.techniqueControl.setValue(''); - this.softwareControl.setValue(''); - this.equipmentControl.setValue(''); - this.dateControl.setValue(''); + const fullYear = year < 100 ? 2000 + year : year; + + const date = new Date(fullYear, month - 1, day); + return isNaN(date.getTime()) ? null : date; + } + + ngOnInit(): void { + this.form.valueChanges.subscribe(({ start, end }) => { + if (start && end) { + this.dateRangeDisplay.setValue( + `${start.toLocaleDateString()} – ${end.toLocaleDateString()}`, + ); + } + }); } } diff --git a/src/app/components/metadata/optional/dimension/dimension.component.html b/src/app/components/metadata/optional/dimension/dimension.component.html index 0e91a0e8..f312d7cf 100644 --- a/src/app/components/metadata/optional/dimension/dimension.component.html +++ b/src/app/components/metadata/optional/dimension/dimension.component.html @@ -1,35 +1,28 @@
- - {{ 'Unit description (e.g. Width of base)' | translate }} - {{ 'Dimension' | translate }}

+ + +
+ + - -
- - {{ 'Unit value' | translate }} - - - - {{ 'Unit type' | translate }} - -
@@ -44,6 +37,4 @@
- - diff --git a/src/app/components/metadata/optional/dimension/dimension.component.ts b/src/app/components/metadata/optional/dimension/dimension.component.ts index 6c683537..686beff9 100644 --- a/src/app/components/metadata/optional/dimension/dimension.component.ts +++ b/src/app/components/metadata/optional/dimension/dimension.component.ts @@ -4,9 +4,10 @@ import { MatButtonModule } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { DigitalEntity, DimensionTuple } from 'src/app/metadata'; +import { DigitalEntity, DimensionTuple, PhysicalEntity } 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'; @Component({ selector: 'app-dimension', @@ -19,12 +20,13 @@ import { OptionalCardListComponent } from '../optional-card-list/optional-card-l ReactiveFormsModule, TranslatePipe, OptionalCardListComponent, + OutlinedInputComponent, ], templateUrl: './dimension.component.html', styleUrl: './dimension.component.scss', }) export class DimensionComponent { - public entity = input.required(); + public entity = input.required(); public nameControl = new FormControl('', { nonNullable: true }); public valueControl = new FormControl('', { nonNullable: true }); 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 45faa881..2d180698 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 @@ -1,23 +1,18 @@ - - {{ 'Type (e.g. DOI) ' | translate }} - - - - {{ 'Value' | translate }} - - +

+ {{ 'External identifiers' | translate }} +

+ +
- +@if (!isPhysical()) { + +} 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 d1342363..8e2a1c4c 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,14 +1,15 @@ import { AsyncPipe } from '@angular/common'; -import { Component, input } from '@angular/core'; +import { Component, computed, input } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { combineLatest, map, startWith } from 'rxjs'; -import { AnyEntity, TypeValueTuple } from 'src/app/metadata'; +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'; @Component({ selector: 'app-external-ids', @@ -22,6 +23,7 @@ import { OptionalCardListComponent } from '../optional-card-list/optional-card-l ReactiveFormsModule, TranslatePipe, OptionalCardListComponent, + OutlinedInputComponent, ], templateUrl: './external-ids.component.html', styleUrl: './external-ids.component.scss', @@ -32,6 +34,8 @@ export class ExternalIdsComponent { public valueControl = new FormControl('', { nonNullable: true }); public typeControl = new FormControl('', { nonNullable: true }); + isPhysical = computed(() => this.entity() instanceof PhysicalEntity); + public isExternalIdentifiersValid$ = combineLatest([ this.valueControl.valueChanges.pipe(startWith(this.valueControl.value)), this.typeControl.valueChanges.pipe(startWith(this.typeControl.value)), diff --git a/src/app/components/metadata/optional/links/links.component.html b/src/app/components/metadata/optional/links/links.component.html index 2d9d7ae1..e0c76097 100644 --- a/src/app/components/metadata/optional/links/links.component.html +++ b/src/app/components/metadata/optional/links/links.component.html @@ -1,24 +1,19 @@
- - {{ 'Description' | translate }} - - - - {{ 'URL' | translate }} - - +

+ {{ 'External links' | translate }} +

+ +
- +@if (!isPhysical()) { + +} diff --git a/src/app/components/metadata/optional/links/links.component.ts b/src/app/components/metadata/optional/links/links.component.ts index 89c06e24..5883246e 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, input } from '@angular/core'; +import { Component, computed, input } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -8,9 +8,10 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { combineLatest, map, startWith } from 'rxjs'; -import { AnyEntity, DescriptionValueTuple } from 'src/app/metadata'; +import { AnyEntity, DescriptionValueTuple, PhysicalEntity } from 'src/app/metadata'; import { TranslatePipe } from '../../../../pipes/translate.pipe'; import { OptionalCardListComponent } from '../optional-card-list/optional-card-list.component'; +import { OutlinedInputComponent } from 'src/app/components/outlined-input/outlined-input.component'; @Component({ selector: 'app-links', @@ -24,6 +25,7 @@ import { OptionalCardListComponent } from '../optional-card-list/optional-card-l ReactiveFormsModule, TranslatePipe, OptionalCardListComponent, + OutlinedInputComponent, ], templateUrl: './links.component.html', styleUrl: './links.component.scss', @@ -34,6 +36,8 @@ export class LinksComponent { public valueControl = new FormControl('', { nonNullable: true }); public descriptionControl = new FormControl('', { nonNullable: true }); + isPhysical = computed(() => this.entity() instanceof PhysicalEntity); + public isLinkDataValid$ = combineLatest([ this.valueControl.valueChanges.pipe(startWith(this.valueControl.value)), this.descriptionControl.valueChanges.pipe(startWith(this.descriptionControl.value)), 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 7fe52393..2cb19f59 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 @@ -1,4 +1,7 @@
+

+ {{ 'Metadatafiles' | translate }} +

{{ 'Add external meta data files. These files will be available for download.' | translate }}

@@ -8,7 +11,9 @@ {{ 'Add' | translate }}
- + @if (!isPhysical()) { + + }
@for (file of entity().metadata_files; track file; let index = $index) {
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 36e060b7..42a7e4c7 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,8 +1,8 @@ -import { Component, input } 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 } from 'src/app/metadata'; +import { AnyEntity, FileTuple, PhysicalEntity } from 'src/app/metadata'; import { FilesizePipe, TranslatePipe } from 'src/app/pipes'; @Component({ @@ -14,6 +14,7 @@ import { FilesizePipe, TranslatePipe } from 'src/app/pipes'; }) export class MetadataFilesComponent { public entity = input.required(); + isPhysical = computed(() => this.entity() instanceof PhysicalEntity); public removeProperty(property: string, index: number) { if (Array.isArray(this.entity()[property])) { diff --git a/src/app/components/metadata/optional/other/other.component.html b/src/app/components/metadata/optional/other/other.component.html deleted file mode 100644 index 042dd5f1..00000000 --- a/src/app/components/metadata/optional/other/other.component.html +++ /dev/null @@ -1,36 +0,0 @@ -
- - {{ 'Description' | translate }} - - - - {{ 'Value' | translate }} - - -
-
- -
- - - - diff --git a/src/app/components/metadata/optional/other/other.component.scss b/src/app/components/metadata/optional/other/other.component.scss deleted file mode 100644 index e1d29b71..00000000 --- a/src/app/components/metadata/optional/other/other.component.scss +++ /dev/null @@ -1,28 +0,0 @@ -.meta-button { - margin: 20px 0; - display: flex; - justify-content: flex-end; -} - -.card { - margin-top: 10px; - border-radius: 1rem !important; - justify-items: center; - justify-content: center; -} - -div.card .actions { - padding: 0 !important; - transform: none; -} - -.card p { - margin-block-end: 5px; - margin-block-start: 5px; -} - -.actions { - padding: 0 !important; - right: 5px !important; - margin-top: auto; -} diff --git a/src/app/components/metadata/optional/other/other.component.ts b/src/app/components/metadata/optional/other/other.component.ts deleted file mode 100644 index 2b2205d7..00000000 --- a/src/app/components/metadata/optional/other/other.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { AsyncPipe } from '@angular/common'; -import { Component, input } from '@angular/core'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; - -import { MatButtonModule } from '@angular/material/button'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; - -import { combineLatest, map, startWith } from 'rxjs'; -import { AnyEntity, DescriptionValueTuple } from 'src/app/metadata'; -import { TranslatePipe } from '../../../../pipes/translate.pipe'; -import { OptionalCardListComponent } from '../optional-card-list/optional-card-list.component'; - -@Component({ - selector: 'app-other', - standalone: true, - imports: [ - ReactiveFormsModule, - MatButtonModule, - MatDividerModule, - MatFormFieldModule, - MatInputModule, - TranslatePipe, - OptionalCardListComponent, - AsyncPipe, - ], - templateUrl: './other.component.html', - styleUrl: './other.component.scss', -}) -export class OtherComponent { - public entity = input.required(); - - public valueControl = new FormControl('', { nonNullable: true }); - public descriptionControl = new FormControl('', { nonNullable: true }); - - public isOtherDataValid$ = combineLatest([ - this.valueControl.valueChanges.pipe(startWith(this.valueControl.value)), - this.descriptionControl.valueChanges.pipe(startWith(this.descriptionControl.value)), - ]).pipe(map(([value, description]) => value !== '' && description !== '')); - - async addNewOtherData() { - const otherInstance = new DescriptionValueTuple({ - value: this.valueControl.value ?? '', - description: this.descriptionControl.value ?? '', - }); - - if (otherInstance.isValid) { - this.entity().other.push(otherInstance); - this.resetFormFields(); - } - } - - resetFormFields() { - this.valueControl.reset(); - this.descriptionControl.reset(); - } -} diff --git a/src/app/components/metadata/optional/phys-obj/phys-obj.component.html b/src/app/components/metadata/optional/phys-obj/phys-obj.component.html index 742259e5..8154a386 100644 --- a/src/app/components/metadata/optional/phys-obj/phys-obj.component.html +++ b/src/app/components/metadata/optional/phys-obj/phys-obj.component.html @@ -1,87 +1,37 @@ @if (physEntity(); as physEntity) { - - - - {{ 'General information' | translate }} - - - - + + - - - Place - + - - {{ 'Name' | translate }} - - - - {{ 'Geopolicital area' | translate }} - - - - + + + - - - - {{ 'Related persons and institutions' | translate }} - - - - + - - - {{ 'External Identifiers' | translate }} - - - + - - - {{ 'External Links' | translate }} - - - + - - - {{ 'Bibliographic References' | translate }} - - - + - - - {{ 'Other' | translate }} - - - + - - - {{ 'Metadata files' | translate }} - - - + } diff --git a/src/app/components/metadata/optional/phys-obj/phys-obj.component.ts b/src/app/components/metadata/optional/phys-obj/phys-obj.component.ts index 65f28fe7..9d0a1acf 100644 --- a/src/app/components/metadata/optional/phys-obj/phys-obj.component.ts +++ b/src/app/components/metadata/optional/phys-obj/phys-obj.component.ts @@ -14,7 +14,8 @@ import { BiblioRefComponent } from '../biblio-ref/biblio-ref.component'; import { ExternalIdsComponent } from '../external-ids/external-ids.component'; import { LinksComponent } from '../links/links.component'; import { MetadataFilesComponent } from '../metadata-files/metadata-files.component'; -import { OtherComponent } from '../other/other.component'; +import { OutlinedInputComponent } from '../../../outlined-input/outlined-input.component'; +import { DimensionComponent } from '../dimension/dimension.component'; @Component({ selector: 'app-phys-obj', @@ -33,7 +34,8 @@ import { OtherComponent } from '../other/other.component'; AddressComponent, ExternalIdsComponent, MetadataFilesComponent, - OtherComponent, + OutlinedInputComponent, + DimensionComponent, ], templateUrl: './phys-obj.component.html', styleUrl: './phys-obj.component.scss', diff --git a/src/app/components/outlined-input/outlined-input.component.html b/src/app/components/outlined-input/outlined-input.component.html index cc4bf25c..626255b5 100644 --- a/src/app/components/outlined-input/outlined-input.component.html +++ b/src/app/components/outlined-input/outlined-input.component.html @@ -1,11 +1,12 @@ @if (label(); as label) { - + } @switch (type()) { @case ('textarea') { } @default { @if (autocomplete(); as autocomplete) { } @else { } } @@ -50,5 +56,7 @@ >{{ icon }} } - +
+ +
diff --git a/src/app/components/outlined-input/outlined-input.component.scss b/src/app/components/outlined-input/outlined-input.component.scss index d5cb3b8b..1a0cf8eb 100644 --- a/src/app/components/outlined-input/outlined-input.component.scss +++ b/src/app/components/outlined-input/outlined-input.component.scss @@ -3,10 +3,11 @@ width: 100%; font-family: var(--font-stack); display: block; + padding-top: 32px; label { position: absolute; - top: 8px; + top: calc(32px + 0.5em); // padding top + half of font size left: 12px; font-size: 14px; color: #666; @@ -15,13 +16,26 @@ background-color: transparent; } + div.suffix { + position: absolute; + right: 0; + bottom: 0; + height: calc(100% - 32px); + display: flex; + justify-content: center; + align-content: center; + align-items: center; + width: fit-content; + max-width: 50%; + } + &:focus-within label, label.active { - top: -8px; - left: 12px; + top: 8px; + left: -4px; font-size: 12px; - color: var(--brand-color); - background-color: #f5f5f5; + // color: var(--brand-color); + // background-color: #f5f5f5; border-radius: 4px; padding: 0 4px; } @@ -72,7 +86,7 @@ mat-icon { position: absolute; right: 12px; - top: 50%; + top: calc(50% + 16px); transform: translateY(-50%); height: 24px; width: 24px; diff --git a/src/app/components/outlined-input/outlined-input.component.ts b/src/app/components/outlined-input/outlined-input.component.ts index d8fd04de..c49e28d8 100644 --- a/src/app/components/outlined-input/outlined-input.component.ts +++ b/src/app/components/outlined-input/outlined-input.component.ts @@ -1,7 +1,16 @@ -import { Component, computed, forwardRef, input, output } from '@angular/core'; +import { + Component, + computed, + effect, + ElementRef, + forwardRef, + input, + output, + signal, + viewChildren, +} from '@angular/core'; import { ControlValueAccessor, - FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, @@ -27,7 +36,6 @@ import { TranslatePipe } from 'src/app/pipes'; }, }) export class OutlinedInputComponent implements ControlValueAccessor { - // formControl = input.required>(); label = input(); hasLabel = computed(() => !!this.label()); placeholder = input(); @@ -35,25 +43,54 @@ export class OutlinedInputComponent implements ControlValueAccessor { icon = input(undefined); iconStyle = input<'filled' | 'outlined' | undefined>(undefined); autocomplete = input(undefined); - type = input<'text' | 'textarea' | 'password'>('text'); textareaRows = input(4); + seperators = input([]); + onSeperator = output({ alias: 'seperator' }); + onKeyDown = output({ alias: 'keydown' }); + + inputElements = viewChildren>('inputElement'); + // Browser autofill hints (e.g., "on", "off", "name", "email", etc.) // Sometimes name is required for autofill to work, so we provide it as an optional input autofillHint = input('on'); name = input(undefined); + // Track which value changes are from our inputs vs from external sources + #expectedValueChange = signal(false); + // Internal value and control value accessor implementation - value: string = ''; + value = signal(''); disabled = false; + _valueChangedEffectRef = effect(() => { + const value = this.value(); + this.onChange(value); + const expected = this.#expectedValueChange(); + if (expected) { + this.#expectedValueChange.set(false); + return; + } + this.#updateInputElementValues(value); + }); + + #updateInputElementValues(value: string) { + const inputElements = this.inputElements(); + inputElements.forEach(element => { + const nativeElement = element.nativeElement; + if (nativeElement.value !== value) { + nativeElement.value = value; + } + }); + } + private onChange = (value: string) => {}; private onTouched = () => {}; // ControlValueAccessor methods writeValue(value: string): void { - this.value = value || ''; + this.value.set(value || ''); } registerOnChange(fn: (value: string) => void): void { @@ -70,12 +107,20 @@ export class OutlinedInputComponent implements ControlValueAccessor { // Handle input changes onInputChange(event: Event): void { + this.#expectedValueChange.set(true); const target = event.target as HTMLInputElement | HTMLTextAreaElement; - this.value = target.value; - this.onChange(this.value); + this.value.set(target.value); } onBlur(): void { this.onTouched(); } + + onKeydown(event: KeyboardEvent): void { + this.onKeyDown.emit(event); + if (this.seperators().includes(event.key)) { + event.preventDefault(); + this.onSeperator.emit(event); + } + } } diff --git a/src/app/dialogs/add-to-compilation/add-to-compilation.component.html b/src/app/dialogs/add-to-compilation/add-to-compilation.component.html index 85489423..eb1e6dc7 100644 --- a/src/app/dialogs/add-to-compilation/add-to-compilation.component.html +++ b/src/app/dialogs/add-to-compilation/add-to-compilation.component.html @@ -61,7 +61,7 @@

type="text" label="Search for collections" #input - (input)="filterText.set(input.value)" + (input)="filterText.set(input.value())" /> @if (userCompilations$ | observableValue | async; as result) {
diff --git a/src/styles.scss b/src/styles.scss index 1ef29be0..4c6540f5 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -189,6 +189,17 @@ img { background-color: #fff; } +.metadata-header-digital { + color: var(--brand-color); + margin: 0; +} + +.metadata-header-physical { + color: black; + font-weight: bold; + margin-bottom: 0; +} + .metadata-container { display: flex; flex-direction: column; @@ -964,3 +975,9 @@ div.option-count-badge { border-radius: 50% !important; @include mat.elevation(1); } + +.editButton { + border: none; + background-color: transparent; + cursor: pointer; +}