mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Change - Metadata field selector, add infinite scroll for data paginated (#3096)
* Change - Metadata field selector add infinite scroll for data paginated * Update change on new BRANCH * Fix - LINT ERRORS * Fix - LINT ERRORS
This commit is contained in:

committed by
Tim Donohue

parent
58e9a60812
commit
05d5a0816d
@@ -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>
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
.scrollable-menu {
|
||||||
|
height: auto;
|
||||||
|
max-height: var(--ds-dso-selector-list-max-height);
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
@@ -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'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user