From 5513d9633f0ed6aa7843a6b3140881d0a467734b Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 9 Sep 2026 16:21:23 +0200 Subject: [PATCH] Port #1336 to dtq-dev-9-base: UFAL/fix(submission): keep dc.type dropdown display after change (ufal/dspace-angular#157) (#1336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "type of resource" scrollable dropdown blanked out after its value changed when the backend was slow: the reloaded model value came back with a populated value/authority but an empty display, and setCurrentValue rendered that empty display verbatim. Both branches of setCurrentValue now fall back to the underlying value when display is empty, which is the convention inputFormatter (r.181) already follows. Spec: 8 it() -> 11. One covers the valueChanges path, one the init path, and one is the actual regression for ufal/clarin-dspace#1377 — it pushes a value with an empty display through group.valueChanges, the real reload entry point, and asserts the rendered input still reads "Corpus". The init test stubs getInitValueFromModel rather than relying on isNotEmpty(new VocabularyEntry()), and builds its value with Object.assign around an empty FormFieldMetadataValueObject, because that constructor would otherwise apply its own `display || value` and hide the very case under test. Negative control run on this branch: with the two component lines reverted and the new specs in place, karma reports "TOTAL: 3 FAILED, 8 SUCCESS" and names exactly the three new tests; with the fix, "TOTAL: 11 SUCCESS". The specs fail for the right reason. v9 notes: this is NOT a clean cherry-pick, despite the change being two lines. `git cherry-pick -x 41ead07451` conflicts in BOTH files — the 9-base component has the vanilla trailing comma after the `map(...)` argument and uses `of` where the fork uses `of as observableOf`, so neither line matches. The hunks were applied by hand; the result differs from the source only by those two adaptations. In the spec, `observableOf` becomes `of` and the FormFieldMetadataValueObject import is placed where simple-import-sort wants it on this branch (after the ../../../../../testing/ block). Card FE-19 (tranche T3). Source: 41ead074510cc67e796f90ee51ad97e1b637bf95 (dtq-dev PR #1336) Co-Authored-By: Claude Opus 5 (1M context) --- ...amic-scrollable-dropdown.component.spec.ts | 46 +++++++++++++++++++ .../dynamic-scrollable-dropdown.component.ts | 4 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts index f8d3611bb03..1edd044931d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts @@ -28,6 +28,7 @@ import { } from '@ng-dynamic-forms/core'; import { TranslateModule } from '@ngx-translate/core'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; +import { of } from 'rxjs'; import { APP_DATA_SERVICES_MAP } from '../../../../../../../config/app-config.interface'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; @@ -42,6 +43,7 @@ import { hasClass, } from '../../../../../testing/utils.test'; import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; +import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { DsDynamicScrollableDropdownComponent } from './dynamic-scrollable-dropdown.component'; import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model'; @@ -232,6 +234,50 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { expect(scrollableDropdownComp.optionsList).toEqual(vocabularyServiceStub.getList()); expect(scrollableDropdownComp.model.value).toEqual(modelValue); }); + + it('should fall back to the underlying value when display is empty on value change', () => { + const valueWithEmptyDisplay = { display: '', value: 'Corpus' }; + scrollableDropdownComp.setCurrentValue(valueWithEmptyDisplay); + + let currentValue; + scrollableDropdownComp.currentValue.subscribe((v) => currentValue = v); + + expect(currentValue).toBe('Corpus'); + }); + + it('should fall back to the underlying value when display is empty on init', () => { + // Build the value bypassing the FormFieldMetadataValueObject constructor (which would + // otherwise apply its own `display || value`), so the init branch genuinely receives an + // empty display and exercises the fallback in setCurrentValue. + const emptyDisplayValue = Object.assign( + new FormFieldMetadataValueObject(), + { display: '', value: 'Corpus' }, + ); + spyOn(scrollableDropdownComp, 'getInitValueFromModel').and.returnValue(of(emptyDisplayValue)); + scrollableDropdownComp.setCurrentValue(emptyDisplayValue, true); + + let currentValue; + scrollableDropdownComp.currentValue.subscribe((v) => currentValue = v); + + expect(currentValue).toBe('Corpus'); + }); + + // Regression for ufal/clarin-dspace#1377: after a dc.type change the section + // reloads and pushes a new value with an empty display through the form control. + // The rendered input must not blank out. + it('should keep the rendered value when a value with empty display arrives via valueChanges', fakeAsync(() => { + const reloadedValue = Object.assign( + new FormFieldMetadataValueObject(), + { display: '', value: 'Corpus', authority: 'corpus-auth' }, + ); + + scrollableDropdownComp.group.get(scrollableDropdownComp.model.id).setValue(reloadedValue); + tick(); + scrollableDropdownFixture.detectChanges(); + + const input = scrollableDropdownFixture.debugElement.query(By.css('input.form-control')).nativeElement; + expect(input.value).toBe('Corpus'); + })); }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts index 243efcc8963..bad01bd3b86 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts @@ -360,7 +360,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom if (init && !this.useFindAllService) { result = this.getInitValueFromModel().pipe( - map((formValue: FormFieldMetadataValueObject) => formValue.display), + map((formValue: FormFieldMetadataValueObject) => formValue.display || formValue.value), ); } else { if (isEmpty(value)) { @@ -370,7 +370,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom } else if (this.useFindAllService) { result = of(value[this.model.displayKey]); } else { - result = of(value.display); + result = of(value.display || value.value); } }