Skip to content

Commit

Permalink
[DURACOM-304] improve loading logic for cc-licenses section and dynam…
Browse files Browse the repository at this point in the history
…ic-list
  • Loading branch information
FrancescoMolinaro committed Jan 15, 2025
1 parent 25b2f68 commit c79affd
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
</div>
<br>
</div>
</div>

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

</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
Expand All @@ -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';
Expand Down Expand Up @@ -65,6 +67,8 @@ export interface ListItem {
NgbButtonsModule,
NgForOf,
ReactiveFormsModule,
AsyncPipe,
TranslateModule,
],
standalone: true,
})
Expand All @@ -78,7 +82,9 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
@Output() focus: EventEmitter<any> = new EventEmitter<any>();

public items: ListItem[][] = [];
protected optionsList: VocabularyEntry[];
public showLoadMore$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
protected optionsList: VocabularyEntry[] = [];
private nextPageInfo: PageInfo;

constructor(private vocabularyService: VocabularyService,
private cdr: ChangeDetectorRef,
Expand All @@ -94,7 +100,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
*/
ngOnInit() {
if (this.model.vocabularyOptions && hasValue(this.model.vocabularyOptions.name)) {
this.setOptionsFromVocabulary();
this.initOptionsFromVocabulary();
}
}

Expand Down Expand Up @@ -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<VocabularyEntry>) => {
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<VocabularyEntry>) => 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);
}
}

Expand All @@ -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<VocabularyEntry>) {
if (response.pageInfo.currentPage < response.pageInfo.totalPages) {
this.nextPageInfo = Object.assign(new PageInfo(), response.pageInfo, { currentPage: response.currentPage + 1 });
this.showLoadMore$.next(true);

Check warning on line 181 in src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L180 - L181 were not covered by tests
} 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;

Check warning on line 194 in src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L194 was not covered by tests
}

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 = [];

Check warning on line 234 in src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L232 - L234 were not covered by tests
}
});
this.cdr.markForCheck();
});
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
<div class="mb-4 ccLicense-select">
<ds-select
[disabled]="!submissionCcLicenses">

<ng-container class="selection">
<span *ngIf="!submissionCcLicenses">
<ds-loading></ds-loading>
</span>
<span *ngIf="getSelectedCcLicense()">
{{ getSelectedCcLicense().name }}
</span>
<span *ngIf="submissionCcLicenses && !getSelectedCcLicense()">
<ng-container *ngIf="storedCcLicenseLink">
{{ 'submission.sections.ccLicense.change' | translate }}
</ng-container>
<ng-container *ngIf="!storedCcLicenseLink">
{{ 'submission.sections.ccLicense.select' | translate }}
</ng-container>
</span>
</ng-container>

<ng-container class="menu">
<button *ngIf="submissionCcLicenses?.length === 0"
class="dropdown-item disabled">
{{ 'submission.sections.ccLicense.none' | translate }}
</button>
<button *ngFor="let license of submissionCcLicenses"
class="dropdown-item"
(click)="selectCcLicense(license)">
{{ license.name }}
</button>
</ng-container>

</ds-select>
</div>
@if (submissionCcLicenses) {
<div class="mb-4 ccLicense-select">
<div ngbDropdown>
<input id="cc-license-dropdown"
class="form-control"
[(ngModel)]="selectedCcLicense.name"
placeholder="{{ !storedCcLicenseLink ? ('submission.sections.ccLicense.select' | translate) : ('submission.sections.ccLicense.change' | translate)}}"
[ngModelOptions]="{standalone: true}"
ngbDropdownToggle
role="combobox"
#script="ngModel">
<div ngbDropdownMenu aria-labelledby="cc-license-dropdown" class="w-100 scrollable-menu"
role="menu"
infiniteScroll
(scroll)="onScroll($event)"
[infiniteScrollDistance]="5"
[infiniteScrollThrottle]="300"
[infiniteScrollUpDistance]="1.5"
[fromRoot]="true"
[scrollWindow]="false">

@if(submissionCcLicenses?.length === 0) {
<button class="dropdown-item disabled">
{{ 'submission.sections.ccLicense.none' | translate }}
</button>
} @else {
@for(license of submissionCcLicenses; track license.id) {
<button class="dropdown-item" (click)="selectCcLicense(license)">
{{ license.name }}
</button>
}
}
</div>
</div>
</div>
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand All @@ -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', () => {
Expand Down
Loading

0 comments on commit c79affd

Please sign in to comment.