import { FieldUpdate, FieldUpdates } from '../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../empty.util'; import { paginatedListToArray } from '../../core/shared/operators'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { ElementRef, EventEmitter, OnDestroy, Output, ViewChild } from '@angular/core'; import { PaginationComponent } from '../pagination/pagination.component'; import { ObjectValuesPipe } from '../utils/object-values-pipe'; import { compareArraysUsing } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { Subscription } from 'rxjs/internal/Subscription'; /** * Operator used for comparing {@link FieldUpdate}s by their field's UUID */ export const compareArraysUsingFieldUuids = () => compareArraysUsing((fieldUpdate: FieldUpdate) => (hasValue(fieldUpdate) && hasValue(fieldUpdate.field)) ? fieldUpdate.field.uuid : undefined); /** * An abstract component containing general methods and logic to be able to drag and drop objects within a paginated * list. This implementation supports being able to drag and drop objects between pages. * Dragging an object on top of a page number will automatically detect the page it's being dropped on and send a * dropObject event to the parent component containing detailed information about the indexes the object was dropped from * and to. * * To extend this component, it is important to make sure to: * - Initialize objectsRD$ within the initializeObjectsRD() method * - Initialize a unique URL for this component/page within the initializeURL() method * - Add (cdkDropListDropped)="drop($event)" to the cdkDropList element in your template * - Add (pageChange)="switchPage($event)" to the ds-pagination element in your template * - Use the updates$ observable for building your list of cdkDrag elements in your template * * An example component extending from this abstract component: PaginatedDragAndDropBitstreamListComponent */ export abstract class AbstractPaginatedDragAndDropListComponent implements OnDestroy { /** * A view on the child pagination component */ @ViewChild(PaginationComponent, {static: false}) paginationComponent: PaginationComponent; /** * Send an event when the user drops an object on the pagination * The event contains details about the index the object came from and is dropped to (across the entirety of the list, * not just within a single page) */ @Output() dropObject: EventEmitter = new EventEmitter(); /** * The URL to use for accessing the object updates from this list */ url: string; /** * The objects to retrieve data for and transform into field updates */ objectsRD$: Observable>>; /** * The updates to the current list */ updates$: Observable; /** * A list of object UUIDs * This is the order the objects will be displayed in */ customOrder: string[]; /** * The amount of objects to display per page */ pageSize = 2; /** * The page options to use for fetching the objects * Start at page 1 and always use the set page size */ options = Object.assign(new PaginationComponentOptions(),{ id: 'paginated-drag-and-drop-options', currentPage: 1, pageSize: this.pageSize }); /** * The current page being displayed */ currentPage$ = new BehaviorSubject(1); /** * Whether or not we should display a loading animation * This is used to display a loading page when the user drops a bitstream onto a new page. The loading animation * should stop once the bitstream has moved to the new page and the new page's response has loaded and contains the * dropped object on top (see this.stopLoadingWhenFirstIs below) */ loading$: BehaviorSubject = new BehaviorSubject(false); /** * List of subscriptions */ subs: Subscription[] = []; protected constructor(protected objectUpdatesService: ObjectUpdatesService, protected elRef: ElementRef, protected objectValuesPipe: ObjectValuesPipe) { } /** * Initialize the observables */ ngOnInit() { this.initializeObjectsRD(); this.initializeURL(); this.initializeUpdates(); } /** * Overwrite this method to define how the list of objects is initialized and updated */ abstract initializeObjectsRD(): void; /** * Overwrite this method to define how the URL is set */ abstract initializeURL(): void; /** * Initialize the field-updates in the store */ initializeUpdates(): void { this.objectsRD$.pipe( paginatedListToArray(), take(1) ).subscribe((objects: T[]) => { this.objectUpdatesService.initialize(this.url, objects, new Date()); }); this.updates$ = this.objectsRD$.pipe( paginatedListToArray(), switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, objects)) ); this.subs.push( this.updates$.pipe( map((fieldUpdates) => this.objectValuesPipe.transform(fieldUpdates)), distinctUntilChanged(compareArraysUsingFieldUuids()) ).subscribe((updateValues) => { this.customOrder = updateValues.map((fieldUpdate) => fieldUpdate.field.uuid); // We received new values, stop displaying a loading indicator if it's present this.loading$.next(false); }), // Disable the pagination when objects are loading this.loading$.subscribe((loading) => this.options.disabled = loading) ); } /** * Update the current page * @param page */ switchPage(page: number) { this.currentPage$.next(page); } /** * An object was moved, send updates to the dropObject EventEmitter * When the object is dropped on a page within the pagination of this component, the object moves to the top of that * page and the pagination automatically loads and switches the view to that page (this is done by calling the event's * finish() method after sending patch requests to the REST API) * @param event */ drop(event: CdkDragDrop) { const dragIndex = event.previousIndex; let dropIndex = event.currentIndex; const dragPage = this.currentPage$.value - 1; let dropPage = this.currentPage$.value - 1; // Check if the user is hovering over any of the pagination's pages at the time of dropping the object const droppedOnElement = this.elRef.nativeElement.querySelector('.page-item:hover'); if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) { // The user is hovering over a page, fetch the page's number from the element const droppedPage = Number(droppedOnElement.textContent); if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) { dropPage = droppedPage - 1; dropIndex = 0; } } const isNewPage = dragPage !== dropPage; // Move the object in the custom order array if the drop happened within the same page // This allows us to instantly display a change in the order, instead of waiting for the REST API's response first if (!isNewPage && dragIndex !== dropIndex) { moveItemInArray(this.customOrder, dragIndex, dropIndex); } const redirectPage = dropPage + 1; const fromIndex = (dragPage * this.pageSize) + dragIndex; const toIndex = (dropPage * this.pageSize) + dropIndex; // Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other if (fromIndex !== toIndex) { if (isNewPage) { this.loading$.next(true); } this.dropObject.emit(Object.assign({ fromIndex, toIndex, finish: () => { if (isNewPage) { this.paginationComponent.doPageChange(redirectPage); } } })); } } /** * unsub all subscriptions */ ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } }