From 04473070c871e91ccf9327bd5f733a15b5f58691 Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Wed, 26 Aug 2026 15:08:43 +0200 Subject: [PATCH 1/2] Clarin9/Save submission immediately when a type/sponsor/author field changes In the DSpace 9 upgrade, the CLARIN section-form lost the logic that persisted the submission as soon as a type-bind controlling field (e.g. dc.type) or a sponsor/author autocomplete value was picked. In v7, selecting such a value in a dropdown fired an immediate section save (verified live on dev-5: choosing a resource type triggers a PATCH). In v9 nothing was saved until the whole section lost focus, so the choice could be lost. Restore the v7 behaviour in SubmissionSectionFormComponent.onChange: when the changed field is a type-bind controlling field or the sponsor/author field, dispatch a section save immediately. The set of type-bind controlling fields is owned by FormBuilderService in v9 (populated from `submit.type-bind.field`), so expose it via a new getTypeFieldValues() accessor rather than duplicating the map in the component. Form reconfiguration on type change is already handled reactively by v9's type-bind machinery, so only the save is restored here. Co-Authored-By: Claude Opus 4.8 --- .../form/builder/form-builder.service.ts | 5 ++++ .../shared/mocks/form-builder-service.mock.ts | 1 + .../form/section-form.component.spec.ts | 30 +++++++++++++++++++ .../sections/form/section-form.component.ts | 9 ++++++ 4 files changed, 45 insertions(+) diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index a3dc3626a57..a9deaf9781e 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -708,4 +708,9 @@ export class FormBuilderService extends DynamicFormService { return this.getDefaultTypeBindModelId(); } + /** Type-bind controlling fields (underscore notation, e.g. `dc_type`, `edm_type`) from `submit.type-bind.field`. */ + getTypeFieldValues(): string[] { + return Array.from(this.typeFields.values()); + } + } diff --git a/src/app/shared/mocks/form-builder-service.mock.ts b/src/app/shared/mocks/form-builder-service.mock.ts index ce60600f2ce..618ccaab74a 100644 --- a/src/app/shared/mocks/form-builder-service.mock.ts +++ b/src/app/shared/mocks/form-builder-service.mock.ts @@ -48,6 +48,7 @@ export function getMockFormBuilderService(): FormBuilderService { ), getTypeBindModelUpdates: EMPTY, resolveTypeBindModelId: undefined, + getTypeFieldValues: ['dc_type'], }); // as the real implementation behaves for a reference the type field map does not remap diff --git a/src/app/submission/sections/form/section-form.component.spec.ts b/src/app/submission/sections/form/section-form.component.spec.ts index 2b123cc985d..1bf43f27fd5 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -608,6 +608,36 @@ describe('SubmissionSectionFormComponent test suite', () => { }); + it('should call dispatchSaveSection on form change when a type-bind field (e.g. dc.type) changes', () => { + spyOn(comp, 'hasStoredValue').and.returnValue(false); + formOperationsService.getFieldPathSegmentedFromChangeEvent.and.returnValue('dc.type'); + formOperationsService.getFieldValueFromChangeEvent.and.returnValue({ value: 'Corpus' }); + + comp.onChange(dynamicFormControlEvent); + + expect(submissionServiceStub.dispatchSaveSection).toHaveBeenCalledWith(submissionId, sectionObject.id); + }); + + it('should call dispatchSaveSection on form change when a sponsor value changes', () => { + spyOn(comp, 'hasStoredValue').and.returnValue(false); + formOperationsService.getFieldPathSegmentedFromChangeEvent.and.returnValue('local.sponsor'); + formOperationsService.getFieldValueFromChangeEvent.and.returnValue({ value: 'EU' }); + + comp.onChange(dynamicFormControlEvent); + + expect(submissionServiceStub.dispatchSaveSection).toHaveBeenCalledWith(submissionId, sectionObject.id); + }); + + it('should not call dispatchSaveSection on form change for a regular field', () => { + spyOn(comp, 'hasStoredValue').and.returnValue(false); + formOperationsService.getFieldPathSegmentedFromChangeEvent.and.returnValue('dc.description'); + formOperationsService.getFieldValueFromChangeEvent.and.returnValue('some text'); + + comp.onChange(dynamicFormControlEvent); + + expect(submissionServiceStub.dispatchSaveSection).not.toHaveBeenCalled(); + }); + it('should set previousValue on form focus event', () => { formBuilderService.hasMappedGroupValue.and.returnValue(false); formOperationsService.getFieldValueFromChangeEvent.and.returnValue('test'); diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 973225c8f94..4293f0ec7af 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -52,6 +52,8 @@ import { isNotEmpty, isUndefined, } from '../../../shared/empty.util'; +import { AUTHOR_METADATA_FIELD_NAME } from '../../../shared/form/builder/ds-dynamic-form-ui/models/clarin-name.model'; +import { SPONSOR_METADATA_NAME } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-complex.model'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; import { FormComponent } from '../../../shared/form/form.component'; @@ -446,6 +448,13 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { if ((environment.submission.autosave.metadata.indexOf(metadata) !== -1 && isNotEmpty(value)) || this.hasRelatedCustomError(metadata)) { this.submissionService.dispatchSave(this.submissionId); } + + // Save immediately when a type-bind field (values like `dc_type`, `edm_type`) or a sponsor/author value changes. + const isTypeBindField = this.formBuilderService.getTypeFieldValues() + .some((typeValue) => typeValue.replace('_', '.') === metadata); + if (isTypeBindField || [SPONSOR_METADATA_NAME, AUTHOR_METADATA_FIELD_NAME].includes(metadata)) { + this.submissionService.dispatchSaveSection(this.submissionId, this.sectionData.id); + } } private hasRelatedCustomError(medatata): boolean { From d65584e190396e818f0f8aa6d84f069e586a0d2e Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Tue, 1 Sep 2026 16:24:28 +0200 Subject: [PATCH 2/2] Fix type-bind match to handle multi-segment controlling fields Convert metadata to underscore notation instead of replacing only the first underscore in the field id (addresses Copilot review). Co-Authored-By: Claude Opus 4.8 --- src/app/submission/sections/form/section-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 4293f0ec7af..8e6006f8edf 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -451,7 +451,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { // Save immediately when a type-bind field (values like `dc_type`, `edm_type`) or a sponsor/author value changes. const isTypeBindField = this.formBuilderService.getTypeFieldValues() - .some((typeValue) => typeValue.replace('_', '.') === metadata); + .some((typeValue) => typeValue === metadata.replace(/\./g, '_')); if (isTypeBindField || [SPONSOR_METADATA_NAME, AUTHOR_METADATA_FIELD_NAME].includes(metadata)) { this.submissionService.dispatchSaveSection(this.submissionId, this.sectionData.id); }