Skip to content

Commit c79affd

Browse files
[DURACOM-304] improve loading logic for cc-licenses section and dynamic-list
1 parent 25b2f68 commit c79affd

File tree

7 files changed

+195
-129
lines changed

7 files changed

+195
-129
lines changed

src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
</div>
6363
<br>
6464
</div>
65+
</div>
6566

67+
<div class="d-flex justify-content-center" *ngIf="(showLoadMore$ | async)">
68+
<div *ngIf="(showLoadMore$ | async)" class="btn btn-link py-3" (click)="loadEntries()">{{'dynamic-list.load-more' | translate}}</div>
6669
</div>
70+
6771
</div>

src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts

Lines changed: 81 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AsyncPipe,
23
NgClass,
34
NgForOf,
45
NgIf,
@@ -25,12 +26,13 @@ import {
2526
DynamicFormLayoutService,
2627
DynamicFormValidationService,
2728
} from '@ng-dynamic-forms/core';
29+
import { TranslateModule } from '@ngx-translate/core';
2830
import findKey from 'lodash/findKey';
31+
import { BehaviorSubject } from 'rxjs';
2932
import {
30-
EMPTY,
31-
reduce,
32-
} from 'rxjs';
33-
import { expand } from 'rxjs/operators';
33+
map,
34+
tap,
35+
} from 'rxjs/operators';
3436

3537
import { PaginatedList } from '../../../../../../core/data/paginated-list.model';
3638
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
@@ -65,6 +67,8 @@ export interface ListItem {
6567
NgbButtonsModule,
6668
NgForOf,
6769
ReactiveFormsModule,
70+
AsyncPipe,
71+
TranslateModule,
6872
],
6973
standalone: true,
7074
})
@@ -78,7 +82,9 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
7882
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
7983

8084
public items: ListItem[][] = [];
81-
protected optionsList: VocabularyEntry[];
85+
public showLoadMore$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
86+
protected optionsList: VocabularyEntry[] = [];
87+
private nextPageInfo: PageInfo;
8288

8389
constructor(private vocabularyService: VocabularyService,
8490
private cdr: ChangeDetectorRef,
@@ -94,7 +100,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
94100
*/
95101
ngOnInit() {
96102
if (this.model.vocabularyOptions && hasValue(this.model.vocabularyOptions.name)) {
97-
this.setOptionsFromVocabulary();
103+
this.initOptionsFromVocabulary();
98104
}
99105
}
100106

@@ -141,70 +147,18 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
141147
/**
142148
* Setting up the field options from vocabulary
143149
*/
144-
protected setOptionsFromVocabulary() {
150+
protected initOptionsFromVocabulary() {
145151
if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) {
146152
const listGroup = this.group.controls[this.model.id] as UntypedFormGroup;
147153
if (this.model.repeatable && this.model.required) {
148154
listGroup.addValidators(this.hasAtLeastOneVocabularyEntry());
149155
}
150156

151-
const initialPageInfo: PageInfo = new PageInfo({
157+
this.nextPageInfo = new PageInfo({
152158
elementsPerPage: 20, currentPage: 1,
153159
} as PageInfo);
154160

155-
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, initialPageInfo).pipe(
156-
getFirstSucceededRemoteDataPayload(),
157-
expand((entries: PaginatedList<VocabularyEntry>) => {
158-
if (entries.pageInfo.currentPage < entries.pageInfo.totalPages) {
159-
const nextPageInfo: PageInfo = new PageInfo({
160-
elementsPerPage: 20, currentPage: entries.pageInfo.currentPage + 1,
161-
} as PageInfo);
162-
return this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, nextPageInfo).pipe(
163-
getFirstSucceededRemoteDataPayload(),
164-
);
165-
} else {
166-
return EMPTY;
167-
}
168-
}),
169-
reduce((acc: VocabularyEntry[], entries: PaginatedList<VocabularyEntry>) => acc.concat(entries.page), []),
170-
).subscribe((allEntries: VocabularyEntry[]) => {
171-
let groupCounter = 0;
172-
let itemsPerGroup = 0;
173-
let tempList: ListItem[] = [];
174-
this.optionsList = allEntries;
175-
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
176-
allEntries.forEach((option: VocabularyEntry, key: number) => {
177-
const value = option.authority || option.value;
178-
const checked: boolean = isNotEmpty(findKey(
179-
this.model.value,
180-
(v) => v.value === option.value));
181-
182-
const item: ListItem = {
183-
id: `${this.model.id}_${value}`,
184-
label: option.display,
185-
value: checked,
186-
index: key,
187-
};
188-
if (this.model.repeatable) {
189-
this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item));
190-
} else {
191-
(this.model as DynamicListRadioGroupModel).options.push({
192-
label: item.label,
193-
value: option,
194-
});
195-
}
196-
tempList.push(item);
197-
itemsPerGroup++;
198-
this.items[groupCounter] = tempList;
199-
if (itemsPerGroup === this.model.groupLength) {
200-
groupCounter++;
201-
itemsPerGroup = 0;
202-
tempList = [];
203-
}
204-
});
205-
this.cdr.markForCheck();
206-
});
207-
161+
this.loadEntries(listGroup);
208162
}
209163
}
210164

@@ -217,4 +171,70 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
217171
};
218172
}
219173

174+
/**
175+
* Update current page state to keep track of which one to load next
176+
* @param response
177+
*/
178+
setPaginationInfo(response: PaginatedList<VocabularyEntry>) {
179+
if (response.pageInfo.currentPage < response.pageInfo.totalPages) {
180+
this.nextPageInfo = Object.assign(new PageInfo(), response.pageInfo, { currentPage: response.currentPage + 1 });
181+
this.showLoadMore$.next(true);
182+
} else {
183+
this.showLoadMore$.next(false);
184+
}
185+
}
186+
187+
/**
188+
* Load entries page
189+
*
190+
* @param listGroup
191+
*/
192+
loadEntries(listGroup?: UntypedFormGroup) {
193+
if (!hasValue(listGroup)) {
194+
listGroup = this.group.controls[this.model.id] as UntypedFormGroup;
195+
}
196+
197+
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.nextPageInfo).pipe(
198+
getFirstSucceededRemoteDataPayload(),
199+
tap((response) => this.setPaginationInfo(response)),
200+
map(entries => entries.page),
201+
).subscribe((allEntries: VocabularyEntry[]) => {
202+
this.optionsList = [...this.optionsList, ...allEntries];
203+
let groupCounter = (this.items.length > 0) ? (this.items.length - 1) : 0;
204+
let itemsPerGroup = 0;
205+
let tempList: ListItem[] = [];
206+
207+
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
208+
allEntries.forEach((option: VocabularyEntry, key: number) => {
209+
const value = option.authority || option.value;
210+
const checked: boolean = isNotEmpty(findKey(
211+
this.model.value,
212+
(v) => v?.value === option.value));
213+
214+
const item: ListItem = {
215+
id: `${this.model.id}_${value}`,
216+
label: option.display,
217+
value: checked,
218+
index: key,
219+
};
220+
if (this.model.repeatable) {
221+
this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item));
222+
} else {
223+
(this.model as DynamicListRadioGroupModel).options.push({
224+
label: item.label,
225+
value: option,
226+
});
227+
}
228+
tempList.push(item);
229+
itemsPerGroup++;
230+
this.items[groupCounter] = tempList;
231+
if (itemsPerGroup === this.model.groupLength) {
232+
groupCounter++;
233+
itemsPerGroup = 0;
234+
tempList = [];
235+
}
236+
});
237+
this.cdr.markForCheck();
238+
});
239+
}
220240
}

src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
1-
<div class="mb-4 ccLicense-select">
2-
<ds-select
3-
[disabled]="!submissionCcLicenses">
4-
5-
<ng-container class="selection">
6-
<span *ngIf="!submissionCcLicenses">
7-
<ds-loading></ds-loading>
8-
</span>
9-
<span *ngIf="getSelectedCcLicense()">
10-
{{ getSelectedCcLicense().name }}
11-
</span>
12-
<span *ngIf="submissionCcLicenses && !getSelectedCcLicense()">
13-
<ng-container *ngIf="storedCcLicenseLink">
14-
{{ 'submission.sections.ccLicense.change' | translate }}
15-
</ng-container>
16-
<ng-container *ngIf="!storedCcLicenseLink">
17-
{{ 'submission.sections.ccLicense.select' | translate }}
18-
</ng-container>
19-
</span>
20-
</ng-container>
21-
22-
<ng-container class="menu">
23-
<button *ngIf="submissionCcLicenses?.length === 0"
24-
class="dropdown-item disabled">
25-
{{ 'submission.sections.ccLicense.none' | translate }}
26-
</button>
27-
<button *ngFor="let license of submissionCcLicenses"
28-
class="dropdown-item"
29-
(click)="selectCcLicense(license)">
30-
{{ license.name }}
31-
</button>
32-
</ng-container>
33-
34-
</ds-select>
35-
</div>
1+
@if (submissionCcLicenses) {
2+
<div class="mb-4 ccLicense-select">
3+
<div ngbDropdown>
4+
<input id="cc-license-dropdown"
5+
class="form-control"
6+
[(ngModel)]="selectedCcLicense.name"
7+
placeholder="{{ !storedCcLicenseLink ? ('submission.sections.ccLicense.select' | translate) : ('submission.sections.ccLicense.change' | translate)}}"
8+
[ngModelOptions]="{standalone: true}"
9+
ngbDropdownToggle
10+
role="combobox"
11+
#script="ngModel">
12+
<div ngbDropdownMenu aria-labelledby="cc-license-dropdown" class="w-100 scrollable-menu"
13+
role="menu"
14+
infiniteScroll
15+
(scroll)="onScroll($event)"
16+
[infiniteScrollDistance]="5"
17+
[infiniteScrollThrottle]="300"
18+
[infiniteScrollUpDistance]="1.5"
19+
[fromRoot]="true"
20+
[scrollWindow]="false">
21+
22+
@if(submissionCcLicenses?.length === 0) {
23+
<button class="dropdown-item disabled">
24+
{{ 'submission.sections.ccLicense.none' | translate }}
25+
</button>
26+
} @else {
27+
@for(license of submissionCcLicenses; track license.id) {
28+
<button class="dropdown-item" (click)="selectCcLicense(license)">
29+
{{ license.name }}
30+
</button>
31+
}
32+
}
33+
</div>
34+
</div>
35+
</div>
36+
}
3637

3738
<ng-container *ngIf="getSelectedCcLicense()">
3839

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
.options-select-menu {
22
max-height: 25vh;
33
}
4+
5+
.ccLicense-select {
6+
width: fit-content;
7+
}
8+
9+
.scrollable-menu {
10+
height: auto;
11+
max-height: var(--ds-dropdown-menu-max-height);
12+
overflow-x: hidden;
13+
}

src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,10 @@ describe('SubmissionSectionCcLicensesComponent', () => {
209209

210210
it('should display a dropdown with the different cc licenses', () => {
211211
expect(
212-
de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(1)')).nativeElement.innerText,
212+
de.query(By.css('.ccLicense-select .scrollable-menu button:nth-child(1)')).nativeElement.innerText,
213213
).toContain('test license name 1');
214214
expect(
215-
de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(2)')).nativeElement.innerText,
215+
de.query(By.css('.ccLicense-select .scrollable-menu button:nth-child(2)')).nativeElement.innerText,
216216
).toContain('test license name 2');
217217
});
218218

@@ -226,9 +226,7 @@ describe('SubmissionSectionCcLicensesComponent', () => {
226226
});
227227

228228
it('should display the selected cc license', () => {
229-
expect(
230-
de.query(By.css('.ccLicense-select ds-select button.selection')).nativeElement.innerText,
231-
).toContain('test license name 2');
229+
expect(component.selectedCcLicense.name).toContain('test license name 2');
232230
});
233231

234232
it('should display all field labels of the selected cc license only', () => {

0 commit comments

Comments
 (0)