diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html index 728c59aa460..2407a740226 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html @@ -62,6 +62,10 @@
+ +
+
+ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts index 79af663cfc4..49b4bd11be9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -1,4 +1,5 @@ import { + AsyncPipe, NgClass, NgForOf, NgIf, @@ -25,12 +26,13 @@ import { DynamicFormLayoutService, DynamicFormValidationService, } from '@ng-dynamic-forms/core'; +import { TranslateModule } from '@ngx-translate/core'; import findKey from 'lodash/findKey'; +import { BehaviorSubject } from 'rxjs'; import { - EMPTY, - reduce, -} from 'rxjs'; -import { expand } from 'rxjs/operators'; + map, + tap, +} from 'rxjs/operators'; import { PaginatedList } from '../../../../../../core/data/paginated-list.model'; import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; @@ -65,6 +67,8 @@ export interface ListItem { NgbButtonsModule, NgForOf, ReactiveFormsModule, + AsyncPipe, + TranslateModule, ], standalone: true, }) @@ -78,7 +82,9 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen @Output() focus: EventEmitter = new EventEmitter(); public items: ListItem[][] = []; - protected optionsList: VocabularyEntry[]; + public showLoadMore$: BehaviorSubject = new BehaviorSubject(false); + protected optionsList: VocabularyEntry[] = []; + private nextPageInfo: PageInfo; constructor(private vocabularyService: VocabularyService, private cdr: ChangeDetectorRef, @@ -94,7 +100,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen */ ngOnInit() { if (this.model.vocabularyOptions && hasValue(this.model.vocabularyOptions.name)) { - this.setOptionsFromVocabulary(); + this.initOptionsFromVocabulary(); } } @@ -141,70 +147,18 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen /** * Setting up the field options from vocabulary */ - protected setOptionsFromVocabulary() { + protected initOptionsFromVocabulary() { if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) { const listGroup = this.group.controls[this.model.id] as UntypedFormGroup; if (this.model.repeatable && this.model.required) { listGroup.addValidators(this.hasAtLeastOneVocabularyEntry()); } - const initialPageInfo: PageInfo = new PageInfo({ + this.nextPageInfo = new PageInfo({ elementsPerPage: 20, currentPage: 1, } as PageInfo); - this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, initialPageInfo).pipe( - getFirstSucceededRemoteDataPayload(), - expand((entries: PaginatedList) => { - if (entries.pageInfo.currentPage < entries.pageInfo.totalPages) { - const nextPageInfo: PageInfo = new PageInfo({ - elementsPerPage: 20, currentPage: entries.pageInfo.currentPage + 1, - } as PageInfo); - return this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, nextPageInfo).pipe( - getFirstSucceededRemoteDataPayload(), - ); - } else { - return EMPTY; - } - }), - reduce((acc: VocabularyEntry[], entries: PaginatedList) => acc.concat(entries.page), []), - ).subscribe((allEntries: VocabularyEntry[]) => { - let groupCounter = 0; - let itemsPerGroup = 0; - let tempList: ListItem[] = []; - this.optionsList = allEntries; - // Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength' - allEntries.forEach((option: VocabularyEntry, key: number) => { - const value = option.authority || option.value; - const checked: boolean = isNotEmpty(findKey( - this.model.value, - (v) => v.value === option.value)); - - const item: ListItem = { - id: `${this.model.id}_${value}`, - label: option.display, - value: checked, - index: key, - }; - if (this.model.repeatable) { - this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item)); - } else { - (this.model as DynamicListRadioGroupModel).options.push({ - label: item.label, - value: option, - }); - } - tempList.push(item); - itemsPerGroup++; - this.items[groupCounter] = tempList; - if (itemsPerGroup === this.model.groupLength) { - groupCounter++; - itemsPerGroup = 0; - tempList = []; - } - }); - this.cdr.markForCheck(); - }); - + this.loadEntries(listGroup); } } @@ -217,4 +171,70 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen }; } + /** + * Update current page state to keep track of which one to load next + * @param response + */ + setPaginationInfo(response: PaginatedList) { + if (response.pageInfo.currentPage < response.pageInfo.totalPages) { + this.nextPageInfo = Object.assign(new PageInfo(), response.pageInfo, { currentPage: response.currentPage + 1 }); + this.showLoadMore$.next(true); + } else { + this.showLoadMore$.next(false); + } + } + + /** + * Load entries page + * + * @param listGroup + */ + loadEntries(listGroup?: UntypedFormGroup) { + if (!hasValue(listGroup)) { + listGroup = this.group.controls[this.model.id] as UntypedFormGroup; + } + + this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.nextPageInfo).pipe( + getFirstSucceededRemoteDataPayload(), + tap((response) => this.setPaginationInfo(response)), + map(entries => entries.page), + ).subscribe((allEntries: VocabularyEntry[]) => { + this.optionsList = [...this.optionsList, ...allEntries]; + let groupCounter = (this.items.length > 0) ? (this.items.length - 1) : 0; + let itemsPerGroup = 0; + let tempList: ListItem[] = []; + + // Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength' + allEntries.forEach((option: VocabularyEntry, key: number) => { + const value = option.authority || option.value; + const checked: boolean = isNotEmpty(findKey( + this.model.value, + (v) => v?.value === option.value)); + + const item: ListItem = { + id: `${this.model.id}_${value}`, + label: option.display, + value: checked, + index: key, + }; + if (this.model.repeatable) { + this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item)); + } else { + (this.model as DynamicListRadioGroupModel).options.push({ + label: item.label, + value: option, + }); + } + tempList.push(item); + itemsPerGroup++; + this.items[groupCounter] = tempList; + if (itemsPerGroup === this.model.groupLength) { + groupCounter++; + itemsPerGroup = 0; + tempList = []; + } + }); + this.cdr.markForCheck(); + }); + } } diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html index 0c8c1abeeb2..a8d6ef32621 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html @@ -1,38 +1,39 @@ -
- - - - - - - - {{ getSelectedCcLicense().name }} - - - - {{ 'submission.sections.ccLicense.change' | translate }} - - - {{ 'submission.sections.ccLicense.select' | translate }} - - - - - - - - - - -
+@if (submissionCcLicenses) { +
+
+ + +
+
+} diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss index 62a902b79a3..142cd82822a 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss @@ -1,3 +1,13 @@ .options-select-menu { max-height: 25vh; } + +.ccLicense-select { + width: fit-content; +} + +.scrollable-menu { + height: auto; + max-height: var(--ds-dropdown-menu-max-height); + overflow-x: hidden; +} diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts index 7b540ecd910..2e82948c149 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts @@ -209,10 +209,10 @@ describe('SubmissionSectionCcLicensesComponent', () => { it('should display a dropdown with the different cc licenses', () => { expect( - de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(1)')).nativeElement.innerText, + de.query(By.css('.ccLicense-select .scrollable-menu button:nth-child(1)')).nativeElement.innerText, ).toContain('test license name 1'); expect( - de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(2)')).nativeElement.innerText, + de.query(By.css('.ccLicense-select .scrollable-menu button:nth-child(2)')).nativeElement.innerText, ).toContain('test license name 2'); }); @@ -226,9 +226,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { }); it('should display the selected cc license', () => { - expect( - de.query(By.css('.ccLicense-select ds-select button.selection')).nativeElement.innerText, - ).toContain('test license name 2'); + expect(component.selectedCcLicense.name).toContain('test license name 2'); }); it('should display all field labels of the selected cc license only', () => { diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts index 04ea4b7274e..cc1925a4a66 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts @@ -4,38 +4,38 @@ import { NgIf, } from '@angular/common'; import { + ChangeDetectorRef, Component, Inject, } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { NgbDropdownModule, NgbModal, NgbModalRef, } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { - EMPTY, Observable, of as observableOf, Subscription, } from 'rxjs'; import { distinctUntilChanged, - expand, filter, map, - reduce, take, + tap, } from 'rxjs/operators'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../core/data/remote-data'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { getFirstCompletedRemoteData, - getFirstSucceededRemoteData, + getFirstSucceededRemoteDataPayload, getRemoteDataPayload, } from '../../../core/shared/operators'; import { @@ -71,6 +71,8 @@ import { SectionsType } from '../sections-type'; NgForOf, DsSelectComponent, NgbDropdownModule, + FormsModule, + InfiniteScrollModule, ], standalone: true, }) @@ -102,7 +104,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent /** * Cache of the available Creative Commons licenses. */ - submissionCcLicenses: SubmissionCcLicence[]; + submissionCcLicenses: SubmissionCcLicence[] = []; /** * Reference to NgbModal @@ -114,6 +116,25 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent */ defaultJurisdiction: string; + /** + * The currently selected cc licence + */ + selectedCcLicense: SubmissionCcLicence = new SubmissionCcLicence(); + + /** + * Options for paginated data loading + */ + ccLicenceOptions: FindListOptions = { + elementsPerPage: 20, + currentPage: 1, + }; + /** + * Check to stop paginated search + * + * @private + */ + private _isLastPage: boolean; + /** * The Creative Commons link saved in the workspace item. */ @@ -138,6 +159,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent protected submissionCcLicenseUrlDataService: SubmissionCcLicenseUrlDataService, protected operationsBuilder: JsonPatchOperationsBuilder, protected configService: ConfigurationDataService, + protected ref: ChangeDetectorRef, @Inject('collectionIdProvider') public injectedCollectionId: string, @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, @Inject('submissionIdProvider') public injectedSubmissionId: string, @@ -161,9 +183,10 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent * @param ccLicense the Creative Commons license to select. */ selectCcLicense(ccLicense: SubmissionCcLicence) { - if (!!this.getSelectedCcLicense() && this.getSelectedCcLicense().id === ccLicense.id) { + if (this.selectedCcLicense.id === ccLicense.id) { return; } + this.selectedCcLicense = ccLicense; this.setAccepted(false); this.updateSectionData({ ccLicense: { @@ -307,26 +330,6 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent } this.sectionData.data = data; }), - this.submissionCcLicensesDataService.findAll({ currentPage: 1, elementsPerPage: 20 }).pipe( - getFirstSucceededRemoteData(), - expand((typeListRD: RemoteData>) => { - const currentPage = typeListRD.payload.pageInfo.currentPage; - const totalPages = typeListRD.payload.pageInfo.totalPages; - if (currentPage < totalPages) { - const nextPageInfo = { currentPage: currentPage + 1, elementsPerPage: 20 }; - return this.submissionCcLicensesDataService.findAll(nextPageInfo).pipe( - getFirstSucceededRemoteData(), - ); - } else { - return EMPTY; - } - }), - reduce((acc: SubmissionCcLicence[], typeListRD: RemoteData>) => acc.concat(typeListRD.payload.page), []), - ).subscribe( - (licenses) => { - this.submissionCcLicenses = licenses; - }, - ), this.configService.findByPropertyName('cc.license.jurisdiction').pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), @@ -339,6 +342,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent } }), ); + this.loadCcLicences(); } /** @@ -358,4 +362,31 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent updateSectionData(data: WorkspaceitemSectionCcLicenseObject) { this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, Object.assign({}, this.data, data)); } + + onScroll(event) { + if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) { + if (!this.isLoading && !this._isLastPage) { + this.ccLicenceOptions.currentPage++; + this.loadCcLicences(); + } + } + } + + loadCcLicences() { + this.isLoading = true; + + this.subscriptions.push( + this.submissionCcLicensesDataService.findAll(this.ccLicenceOptions).pipe( + getFirstSucceededRemoteDataPayload(), + tap((response) => this._isLastPage = response.pageInfo.currentPage === response.pageInfo.totalPages), + map((list) => list.page), + ).subscribe( + (licenses) => { + this.submissionCcLicenses = [...this.submissionCcLicenses, ...licenses]; + this.isLoading = false; + this.ref.detectChanges(); + }, + ), + ); + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 5515f7f801e..987da565489 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1684,6 +1684,8 @@ "deny-request-copy.success": "Successfully denied item request", + "dynamic-list.load-more": "Load more", + "dropdown.clear": "Clear selection", "dropdown.clear.tooltip": "Clear the selected option",