diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html index b306eb2721..1ac3300bca 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html @@ -1,6 +1,6 @@ -
-
+
+
@@ -27,7 +27,7 @@
-
diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index b83f2b9664..c81926d83d 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -239,7 +239,7 @@ describe('EditBitstreamPageComponent', () => { }); it('should select the correct format', () => { - expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id); + expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.shortDescription); }); it('should put the \"New Format\" input on invisible', () => { @@ -270,7 +270,13 @@ describe('EditBitstreamPageComponent', () => { describe('when an unknown format is selected', () => { beforeEach(() => { - comp.updateNewFormatLayout(allFormats[0].id); + comp.onChange({ + model: { + id: 'selectedFormat', + value: allFormats[0], + }, + }); + comp.updateNewFormatLayout(); }); it('should remove the invisible class from the \"New Format\" input', () => { @@ -372,10 +378,11 @@ describe('EditBitstreamPageComponent', () => { describe('when selected format has changed', () => { beforeEach(() => { - comp.formGroup.patchValue({ - formatContainer: { - selectedFormat: allFormats[2].id - } + comp.onChange({ + model: { + id: 'selectedFormat', + value: allFormats[2], + }, }); fixture.detectChanges(); comp.onSubmit(); diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index f8a1718e16..78893c9281 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -8,7 +8,7 @@ import { expand, Observable, of as observableOf, reduce, - Subscription + Subscription, take } from 'rxjs'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; import { UntypedFormGroup } from '@angular/forms'; @@ -33,6 +33,8 @@ import { Item } from '../../core/shared/item.model'; import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service'; +import { DynamicScrollableDropdownModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; +import { FindAllDataImpl } from "../../core/data/base/find-all-data"; @Component({ selector: 'ds-edit-bitstream-page', @@ -51,12 +53,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ bitstreamRD$: Observable>; - /** - * The formats their remote data observable - * Tracks changes and updates the view - */ - bitstreamFormatsRD$: Observable>>; - /** * The UUID of the primary bitstream for this bundle */ @@ -72,11 +68,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ originalFormat: BitstreamFormat; - /** - * A list of all available bitstream formats - */ - formats: BitstreamFormat[]; - /** * @type {string} Key prefix used to generate form messages */ @@ -163,9 +154,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { /** * The Dynamic Input Model for the selected format */ - selectedFormatModel = new DynamicSelectModel({ + selectedFormatModel = new DynamicScrollableDropdownModel({ id: 'selectedFormat', - name: 'selectedFormat' + name: 'selectedFormat', + displayKey: 'shortDescription', + repeatable: false, + metadataFields: [], + submissionId: '', + hasSelectableMetadata: false, + findAllFactory: this.findAllFormatsServiceFactory(), + formatFunction: (format: BitstreamFormat | string) => { + if (format instanceof BitstreamFormat) { + return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription; + } else { + return format; + } + }, }); /** @@ -380,6 +384,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * @private */ private bundle: Bundle; + /** + * The currently selected format + * @private + */ + private selectedFormat: BitstreamFormat; constructor(private route: ActivatedRoute, private router: Router, @@ -407,25 +416,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.entityType = this.route.snapshot.queryParams.entityType; this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream)); - this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions).pipe( - getFirstSucceededRemoteData(), - expand((response: RemoteData>) => { - const pageInfo = response.payload.pageInfo; - if (pageInfo.currentPage < pageInfo.totalPages) { - const nextPageOptions = { ...this.findAllOptions, currentPage: pageInfo.currentPage + 1 }; - return this.bitstreamFormatService.findAll(nextPageOptions).pipe(getFirstSucceededRemoteData()); - } else { - return EMPTY; - } - }), - ); - - const bitstreamFormats$ = this.bitstreamFormatsRD$.pipe( - reduce((acc: BitstreamFormat[], response: RemoteData>) => { - return acc.concat(response.payload.page); - }, []) - ) - const bitstream$ = this.bitstreamRD$.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), @@ -446,24 +436,31 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { switchMap((bundle: Bundle) => bundle.item), getFirstSucceededRemoteDataPayload(), ); + const format$ = bitstream$.pipe( + switchMap(bitstream => bitstream.format), + getFirstSucceededRemoteDataPayload(), + ); + this.subs.push( observableCombineLatest( bitstream$, - bitstreamFormats$, bundle$, primaryBitstream$, item$, - ).pipe() - .subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => { - this.bitstream = bitstream as Bitstream; - this.formats = allFormats; - this.bundle = bundle; - // hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will - // be a success response, but empty - this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null; - this.itemId = item.uuid; - this.setIiifStatus(this.bitstream); - }) + format$, + ).subscribe(([bitstream, bundle, primaryBitstream, item, format]) => { + this.bitstream = bitstream as Bitstream; + this.bundle = bundle; + this.selectedFormat = format; + // hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will + // be a success response, but empty + this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null; + this.itemId = item.uuid; + this.setIiifStatus(this.bitstream); + }), + format$.pipe(take(1)).subscribe( + (format) => this.originalFormat = format, + ), ); this.subs.push( @@ -479,7 +476,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ setForm() { this.formGroup = this.formService.createFormGroup(this.formModel); - this.updateFormatModel(); this.updateForm(this.bitstream); this.updateFieldTranslations(); } @@ -498,8 +494,9 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { description: bitstream.firstMetadataValue('dc.description') }, formatContainer: { - newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined - } + selectedFormat: this.selectedFormat.shortDescription, + newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined, + }, }); if (this.isIIIF) { this.formGroup.patchValue({ @@ -517,36 +514,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } }); } - this.bitstream.format.pipe( - getAllSucceededRemoteDataPayload() - ).subscribe((format: BitstreamFormat) => { - this.originalFormat = format; - this.formGroup.patchValue({ - formatContainer: { - selectedFormat: format.id - } - }); - this.updateNewFormatLayout(format.id); - }); + this.updateNewFormatLayout(); } - /** - * Create the list of unknown format IDs an add options to the selectedFormatModel - */ - updateFormatModel() { - this.selectedFormatModel.options = this.formats.map((format: BitstreamFormat) => - Object.assign({ - value: format.id, - label: this.isUnknownFormat(format.id) ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription - })); - } /** * Update the layout of the "Other Format" input depending on the selected format * @param selectedId */ - updateNewFormatLayout(selectedId: string) { - if (this.isUnknownFormat(selectedId)) { + updateNewFormatLayout() { + if (this.isUnknownFormat()) { this.formLayout.newFormat.grid.host = this.newFormatBaseLayout; } else { this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible'; @@ -557,9 +534,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * Is the provided format (id) part of the list of unknown formats? * @param id */ - isUnknownFormat(id: string): boolean { - const format = this.formats.find((f: BitstreamFormat) => f.id === id); - return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown; + isUnknownFormat(): boolean { + return hasValue(this.selectedFormat) && this.selectedFormat.supportLevel === BitstreamFormatSupportLevel.Unknown; } /** @@ -591,7 +567,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { onChange(event) { const model = event.model; if (model.id === this.selectedFormatModel.id) { - this.updateNewFormatLayout(model.value); + this.selectedFormat = model.value; + this.updateNewFormatLayout(); } } @@ -601,8 +578,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { onSubmit() { const updatedValues = this.formGroup.getRawValue(); const updatedBitstream = this.formToBitstream(updatedValues); - const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat); - const isNewFormat = selectedFormat.id !== this.originalFormat.id; + const isNewFormat = this.selectedFormat.id !== this.originalFormat.id; const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream; const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid; @@ -654,7 +630,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { bundle$ = observableOf(this.bundle); } if (isNewFormat) { - bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe( + bitstream$ = this.bitstreamService.updateFormat(this.bitstream, this.selectedFormat).pipe( getFirstCompletedRemoteData(), map((formatResponse: RemoteData) => { if (hasValue(formatResponse) && formatResponse.hasFailed) { @@ -812,4 +788,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { .forEach((subscription) => subscription.unsubscribe()); } + findAllFormatsServiceFactory() { + return () => this.bitstreamFormatService as any as FindAllDataImpl; + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html index bcb83cea11..f8cdb06dac 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html @@ -40,20 +40,18 @@ (scrolled)="onScroll()" [scrollWindow]="false"> - +

{{'form.loading' | translate}}

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 c902807cd4..8ac5d0c2ea 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 @@ -18,7 +18,7 @@ import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dyna import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model'; import { PageInfo } from '../../../../../../core/shared/page-info.model'; -import { isEmpty } from '../../../../../empty.util'; +import { hasValue, isEmpty } from '../../../../../empty.util'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; import { @@ -27,6 +27,8 @@ import { } from '../../../../../../core/data/paginated-list.model'; import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; +import { FindAllData } from "../../../../../../core/data/base/find-all-data"; +import { CacheableObject } from "../../../../../../core/cache/cacheable-object.model"; /** * Component representing a dropdown input field @@ -55,6 +57,21 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom public selectedIndex = 0; public acceptableKeys = ['Space', 'NumpadMultiply', 'NumpadAdd', 'NumpadSubtract', 'NumpadDecimal', 'Semicolon', 'Equal', 'Comma', 'Minus', 'Period', 'Quote', 'Backquote']; + /** + * If true the component can rely on the findAll method for data loading. + * This is a behaviour activated by dependency injection through the dropdown config. + * If a service that implements findAll is not provided in the config the component falls back on the standard vocabulary service. + * + * @private + */ + private useFindAllService: boolean; + /** + * A service that implements FindAllData. + * If is provided in the config will be used for data loading in stead of the VocabularyService + * @private + */ + private findAllService: FindAllData; + constructor(protected vocabularyService: VocabularyService, protected cdr: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService, @@ -67,6 +84,9 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom * Initialize the component, setting up the init form value */ ngOnInit() { + this.findAllService = this.model?.findAllFactory(); + this.useFindAllService = hasValue(this.findAllService?.findAll) && typeof this.findAllService.findAll === 'function'; + this.updatePageInfo(this.model.maxOptions, 1); this.loadOptions(true); this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged()) @@ -75,13 +95,24 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom }); } + /** + * Get service and method to use to retrieve dropdown options + */ + getDataFromService(): Observable>> { + if (this.useFindAllService) { + return this.findAllService.findAll({ elementsPerPage: this.pageInfo.elementsPerPage, currentPage: this.pageInfo.currentPage }); + } else { + return this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo); + } + } + loadOptions(fromInit: boolean) { this.loading = true; - this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe( + this.getDataFromService().pipe( getFirstSucceededRemoteDataPayload(), catchError(() => observableOf(buildPaginatedList(new PageInfo(), []))), - tap(() => this.loading = false) - ).subscribe((list: PaginatedList) => { + tap(() => this.loading = false), + ).subscribe((list: PaginatedList) => { this.optionsList = list.page; if (fromInit && this.model.value) { this.setCurrentValue(this.model.value, true); @@ -101,7 +132,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom /** * Converts an item from the result list to a `string` to display in the `` field. */ - inputFormatter = (x: VocabularyEntry): string => x.display || x.value; + inputFormatter = (x: any): string => (this.model.formatFunction ? this.model.formatFunction(x) : (x.display || x.value)); /** * Opens dropdown menu @@ -204,7 +235,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom this.pageInfo.totalElements, this.pageInfo.totalPages ); - this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe( + this.getDataFromService().pipe( getFirstSucceededRemoteDataPayload(), catchError(() => observableOf(buildPaginatedList( new PageInfo(), @@ -212,7 +243,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom )) ), tap(() => this.loading = false)) - .subscribe((list: PaginatedList) => { + .subscribe((list: PaginatedList) => { this.optionsList = this.optionsList.concat(list.page); this.updatePageInfo( list.pageInfo.elementsPerPage, @@ -243,7 +274,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom setCurrentValue(value: any, init = false): void { let result: Observable; - if (init) { + if (init && !this.useFindAllService) { result = this.getInitValueFromModel().pipe( map((formValue: FormFieldMetadataValueObject) => formValue.display) ); @@ -252,6 +283,8 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom result = observableOf(''); } else if (typeof value === 'string') { result = observableOf(value); + } else if (this.useFindAllService) { + result = observableOf(value[this.model.displayKey]); } else { result = observableOf(value.display); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts index a9974717e4..873744d870 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts @@ -1,19 +1,33 @@ import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { FindAllDataImpl } from "../../../../../../core/data/base/find-all-data"; +import { CacheableObject } from "../../../../../../core/cache/cacheable-object.model"; export const DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN = 'SCROLLABLE_DROPDOWN'; export interface DynamicScrollableDropdownModelConfig extends DsDynamicInputModelConfig { - vocabularyOptions: VocabularyOptions; + vocabularyOptions?: VocabularyOptions; maxOptions?: number; value?: any; + displayKey?: string; + formatFunction?: (value: any) => string; + findAllFactory?: () => FindAllDataImpl; } export class DynamicScrollableDropdownModel extends DsDynamicInputModel { @serializable() maxOptions: number; @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN; + @serializable() displayKey: string; + /** + * Configurable function for display value formatting in input + */ + formatFunction: (value: any) => string; + /** + * Factory for a service that implements FindAllData + */ + findAllFactory: () => FindAllDataImpl; constructor(config: DynamicScrollableDropdownModelConfig, layout?: DynamicFormControlLayout) { @@ -22,6 +36,9 @@ export class DynamicScrollableDropdownModel extends DsDynamicInputModel { this.autoComplete = AUTOCOMPLETE_OFF; this.vocabularyOptions = config.vocabularyOptions; this.maxOptions = config.maxOptions || 10; + this.displayKey = config.displayKey || 'display'; + this.formatFunction = config.formatFunction; + this.findAllFactory = config.findAllFactory || (() => null); } }