Merge pull request #3824 from tdonohue/port_3096_to_7x

[Port dspace-7_x] Change - Metadata field selector, add infinite scroll for data paginated
This commit is contained in:
Tim Donohue
2025-01-10 16:23:13 -06:00
committed by GitHub
4 changed files with 128 additions and 42 deletions

View File

@@ -6,13 +6,30 @@
[formControl]="input" [formControl]="input"
(focusin)="query$.next(mdField)" (focusin)="query$.next(mdField)"
(dsClickOutside)="query$.next(null)" (dsClickOutside)="query$.next(null)"
(click)="$event.stopPropagation();" /> (click)="$event.stopPropagation();"
(keyup)="this.selectedValueLoading = false"
/>
<div class="invalid-feedback show-feedback" *ngIf="showInvalid">{{ dsoType + '.edit.metadata.metadatafield.invalid' | translate }}</div> <div class="invalid-feedback show-feedback" *ngIf="showInvalid">{{ dsoType + '.edit.metadata.metadatafield.invalid' | translate }}</div>
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (mdFieldOptions$ | async)?.length > 0}"> <div id="scrollable-metadata-field-selector" class="dropdown-menu scrollable-menu" [ngClass]="{'show': (mdFieldOptions$ | async)?.length > 0}">
<div class="dropdown-list"> <div class="dropdown-list">
<div *ngFor="let mdFieldOption of (mdFieldOptions$ | async)"> <div
<button class="d-block dropdown-item" (click)="select(mdFieldOption)"> infiniteScroll
<span [innerHTML]="mdFieldOption"></span> [infiniteScrollDistance]="1"
[infiniteScrollThrottle]="0"
[infiniteScrollContainer]="'#scrollable-metadata-field-selector'"
[fromRoot]="true"
(scrolled)="onScrollDown()">
<ng-container *ngIf="mdFieldOptions$ | async">
<button *ngFor="let listEntry of (mdFieldOptions$ | async)"
class="d-block dropdown-item"
dsHoverClass="ds-hover"
(click)="select(listEntry)" #listEntryElement>
<span [innerHTML]="listEntry"></span>
</button>
</ng-container>
<button *ngIf="loading"
class="list-group-item list-group-item-action border-0 list-entry">
<ds-loading [showMessage]="false"></ds-loading>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,5 @@
.scrollable-menu {
height: auto;
max-height: var(--ds-dso-selector-list-max-height);
overflow: scroll;
}

View File

@@ -28,7 +28,8 @@ describe('MetadataFieldSelectorComponent', () => {
metadataSchema = Object.assign(new MetadataSchema(), { metadataSchema = Object.assign(new MetadataSchema(), {
id: 0, id: 0,
prefix: 'dc', prefix: 'dc',
namespace: 'http://dublincore.org/documents/dcmi-terms/', namespace: 'https://schema.org/CreativeWork',
field: '.',
}); });
metadataFields = [ metadataFields = [
Object.assign(new MetadataField(), { Object.assign(new MetadataField(), {
@@ -68,10 +69,10 @@ describe('MetadataFieldSelectorComponent', () => {
}); });
describe('when a query is entered', () => { describe('when a query is entered', () => {
const query = 'test query'; const query = 'dc.d';
beforeEach(() => { beforeEach(() => {
component.showInvalid = true; component.showInvalid = false;
component.query$.next(query); component.query$.next(query);
}); });
@@ -80,7 +81,7 @@ describe('MetadataFieldSelectorComponent', () => {
}); });
it('should query the registry service for metadata fields and include the schema', () => { it('should query the registry service for metadata fields and include the schema', () => {
expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, { elementsPerPage: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema')); expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, { elementsPerPage: 20, sort: new SortOptions('fieldName', SortDirection.ASC), currentPage: 1 }, true, false, followLink('schema'));
}); });
}); });

View File

@@ -9,20 +9,23 @@ import {
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { debounceTime, distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators'; import { debounceTime, map, startWith, switchMap, take, tap } from 'rxjs/operators';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { import {
getAllSucceededRemoteData, getAllSucceededRemoteData,
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
metadataFieldsToString metadataFieldsToString
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { Observable } from 'rxjs/internal/Observable'; import {
BehaviorSubject,
combineLatest as observableCombineLatest,
Observable,
of,
Subscription,
} from 'rxjs';
import { RegistryService } from '../../../core/registry/registry.service'; import { RegistryService } from '../../../core/registry/registry.service';
import { UntypedFormControl } from '@angular/forms'; import { UntypedFormControl } from '@angular/forms';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { Subscription } from 'rxjs/internal/Subscription';
import { of } from 'rxjs/internal/observable/of';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
@@ -30,7 +33,7 @@ import { SortDirection, SortOptions } from '../../../core/cache/models/sort-opti
@Component({ @Component({
selector: 'ds-metadata-field-selector', selector: 'ds-metadata-field-selector',
styleUrls: ['./metadata-field-selector.component.scss'], styleUrls: ['./metadata-field-selector.component.scss'],
templateUrl: './metadata-field-selector.component.html' templateUrl: './metadata-field-selector.component.html',
}) })
/** /**
* Component displaying a searchable input for metadata-fields * Component displaying a searchable input for metadata-fields
@@ -67,7 +70,7 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
* List of available metadata field options to choose from, dependent on the current query the user entered * List of available metadata field options to choose from, dependent on the current query the user entered
* Shows up in a dropdown below the input * Shows up in a dropdown below the input
*/ */
mdFieldOptions$: Observable<string[]>; mdFieldOptions$: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
/** /**
* FormControl for the input * FormControl for the input
@@ -102,6 +105,30 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
*/ */
subs: Subscription[] = []; subs: Subscription[] = [];
/**
* The current page to load
* Dynamically goes up as the user scrolls down until it reaches the last page possible
*/
currentPage$ = new BehaviorSubject(1);
/**
* Whether or not the list contains a next page to load
* This allows us to avoid next pages from trying to load when there are none
*/
hasNextPage = false;
/**
* Whether or not new results are currently loading
*/
loading = false;
/**
* Default page option for this feature
*/
pageOptions = { elementsPerPage: 20, sort: new SortOptions('fieldName', SortDirection.ASC) };
constructor(protected registryService: RegistryService, constructor(protected registryService: RegistryService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translate: TranslateService) { protected translate: TranslateService) {
@@ -112,32 +139,33 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
* Update the mdFieldOptions$ depending on the query$ fired by querying the server * Update the mdFieldOptions$ depending on the query$ fired by querying the server
*/ */
ngOnInit(): void { ngOnInit(): void {
this.subs.push(this.input.valueChanges.pipe(
debounceTime(this.debounceTime),
startWith(''),
).subscribe((valueChange) => {
this.currentPage$.next(1);
if (!this.selectedValueLoading) {
this.query$.next(valueChange);
}
this.mdField = valueChange;
this.mdFieldChange.emit(this.mdField);
}));
this.subs.push( this.subs.push(
this.input.valueChanges.pipe( observableCombineLatest(
debounceTime(this.debounceTime), this.query$,
).subscribe((valueChange) => { this.currentPage$,
if (!this.selectedValueLoading) { )
this.query$.next(valueChange); .pipe(
} switchMap(([query, page]: [string, number]) => {
this.selectedValueLoading = false; this.loading = true;
this.mdField = valueChange; if (page === 1) {
this.mdFieldChange.emit(this.mdField); this.mdFieldOptions$.next([]);
}), }
); return this.search(query as string, page as number);
this.mdFieldOptions$ = this.query$.pipe( }),
distinctUntilChanged(), ).subscribe((rd ) => {
switchMap((query: string) => { if (!this.selectedValueLoading) {this.updateList(rd);}
this.showInvalid = false; }));
if (query !== null) {
return this.registryService.queryMetadataFields(query, { elementsPerPage: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema')).pipe(
getAllSucceededRemoteData(),
metadataFieldsToString(),
);
} else {
return [[]];
}
}),
);
} }
/** /**
@@ -181,6 +209,41 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
this.input.setValue(mdFieldOption); this.input.setValue(mdFieldOption);
} }
/**
* When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page
*/
onScrollDown() {
if (this.hasNextPage && !this.loading) {
this.currentPage$.next(this.currentPage$.value + 1);
}
}
/**
* @Description It update the mdFieldOptions$ according the query result page
* */
updateList(list: string[]) {
this.loading = false;
this.hasNextPage = list.length > 0;
const currentEntries = this.mdFieldOptions$.getValue();
this.mdFieldOptions$.next([...currentEntries, ...list]);
this.selectedValueLoading = false;
}
/**
* Perform a search for the current query and page
* @param query Query to search objects for
* @param page Page to retrieve
* @param useCache Whether or not to use the cache
*/
search(query: string, page: number, useCache: boolean = true) {
return this.registryService.queryMetadataFields(query,{
elementsPerPage: this.pageOptions.elementsPerPage, sort: this.pageOptions.sort,
currentPage: page }, useCache, false, followLink('schema'))
.pipe(
getAllSucceededRemoteData(),
metadataFieldsToString(),
);
}
/** /**
* Unsubscribe from any open subscriptions * Unsubscribe from any open subscriptions
*/ */