From 82a3014af4f6f5ee81ea2d787d02b023ecb0c84e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 18 Jun 2020 12:11:20 +0200 Subject: [PATCH] 71380: drag-and-drop-list customOrder to avoid elements hopping back after drop + freeze fix --- .../edit-item-page/edit-item-page.module.ts | 4 +- .../item-bitstreams.component.ts | 31 ++++---- ...rag-and-drop-bitstream-list.component.html | 39 ++++++---- ...-and-drop-bitstream-list.component.spec.ts | 8 +- ...-drag-and-drop-bitstream-list.component.ts | 4 +- .../object-updates/object-updates.service.ts | 4 +- ...nated-drag-and-drop-list.component.spec.ts | 8 +- ...-paginated-drag-and-drop-list.component.ts | 77 ++++++++++++++++--- 8 files changed, 126 insertions(+), 49 deletions(-) diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index acb23fe592..44cb4099f0 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -32,6 +32,7 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; +import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -75,7 +76,8 @@ import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/cr ResourcePolicyCreateComponent, ], providers: [ - BundleDataService + BundleDataService, + ObjectValuesPipe ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 40602e0969..35da302961 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { Observable } from 'rxjs/internal/Observable'; @@ -88,7 +88,8 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme public objectCache: ObjectCacheService, public requestService: RequestService, public cdRef: ChangeDetectorRef, - public bundleService: BundleDataService + public bundleService: BundleDataService, + public zone: NgZone ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, route); } @@ -187,18 +188,20 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme * @param event The event containing the index the bitstream came from and was dropped to */ dropBitstream(bundle: Bundle, event: any) { - if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) { - const moveOperation = Object.assign({ - op: 'move', - from: `/_links/bitstreams/${event.fromIndex}/href`, - path: `/_links/bitstreams/${event.toIndex}/href` - }); - this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RestResponse) => { - this.displayNotifications('item.edit.bitstreams.notifications.move', [response]); - this.requestService.removeByHrefSubstring(bundle.self); - event.finish(); - }); - } + this.zone.runOutsideAngular(() => { + if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) { + const moveOperation = Object.assign({ + op: 'move', + from: `/_links/bitstreams/${event.fromIndex}/href`, + path: `/_links/bitstreams/${event.toIndex}/href` + }); + this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RestResponse) => { + this.displayNotifications('item.edit.bitstreams.notifications.move', [response]); + this.requestService.removeByHrefSubstring(bundle.self); + this.zone.run(() => event.finish()); + }); + } + }); } /** diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html index 25941f472e..9197b89796 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html @@ -7,24 +7,29 @@ [collectionSize]="(objectsRD$ | async)?.payload?.totalElements" [disableRouteParameterUpdate]="true" (pageChange)="switchPage($event)"> -
-
+
+ +
- -
- + +
+ +
+
-
+
-
+ + diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts index 03d1d00520..54171ed8af 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts @@ -22,6 +22,7 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => { let fixture: ComponentFixture; let objectUpdatesService: ObjectUpdatesService; let bundleService: BundleDataService; + let objectValuesPipe: ObjectValuesPipe; const columnSizes = new ResponsiveTableSizes([ new ResponsiveColumnSizes(2, 2, 3, 4, 4), @@ -100,12 +101,15 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => { getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])) }); + objectValuesPipe = new ObjectValuesPipe(); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], - declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective, ObjectValuesPipe], + declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective], providers: [ { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: BundleDataService, useValue: bundleService } + { provide: BundleDataService, useValue: bundleService }, + { provide: ObjectValuesPipe, useValue: objectValuesPipe } ], schemas: [ NO_ERRORS_SCHEMA ] diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts index 5548da4029..19cf3b27e4 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts @@ -8,6 +8,7 @@ import { switchMap } from 'rxjs/operators'; import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model'; import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; @Component({ selector: 'ds-paginated-drag-and-drop-bitstream-list', @@ -33,8 +34,9 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate constructor(protected objectUpdatesService: ObjectUpdatesService, protected elRef: ElementRef, + protected objectValuesPipe: ObjectValuesPipe, protected bundleService: BundleDataService) { - super(objectUpdatesService, elRef); + super(objectUpdatesService, elRef, objectValuesPipe); } ngOnInit() { diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 779a22fb5b..84f0f06035 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -24,7 +24,7 @@ import { SetValidFieldUpdateAction } from './object-updates.actions'; import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { INotification } from '../../../shared/notifications/models/notification.model'; function objectUpdatesStateSelector(): MemoizedSelector { @@ -125,7 +125,7 @@ export class ObjectUpdatesService { */ getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable { const objectUpdates = this.getObjectEntry(url); - return objectUpdates.pipe(map((objectEntry) => { + return objectUpdates.pipe(isNotEmptyOperator(), map((objectEntry) => { const fieldUpdates: FieldUpdates = {}; for (const object of initialFields) { let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts index 0c55afb2e0..5fd7f3ec56 100644 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts +++ b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts @@ -12,14 +12,16 @@ import { take } from 'rxjs/operators'; import { PaginationComponent } from '../pagination/pagination.component'; import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; import { createPaginatedList } from '../testing/utils.test'; +import { ObjectValuesPipe } from '../utils/object-values-pipe'; class MockAbstractPaginatedDragAndDropListComponent extends AbstractPaginatedDragAndDropListComponent { constructor(protected objectUpdatesService: ObjectUpdatesService, protected elRef: ElementRef, + protected objectValuesPipe: ObjectValuesPipe, protected mockUrl: string, protected mockObjectsRD$: Observable>>) { - super(objectUpdatesService, elRef); + super(objectUpdatesService, elRef, objectValuesPipe); } initializeObjectsRD(): void { @@ -35,6 +37,7 @@ describe('AbstractPaginatedDragAndDropListComponent', () => { let component: MockAbstractPaginatedDragAndDropListComponent; let objectUpdatesService: ObjectUpdatesService; let elRef: ElementRef; + let objectValuesPipe: ObjectValuesPipe; const url = 'mock-abstract-paginated-drag-and-drop-list-component'; @@ -60,11 +63,12 @@ describe('AbstractPaginatedDragAndDropListComponent', () => { querySelector: {} }) }; + objectValuesPipe = new ObjectValuesPipe(); paginationComponent = jasmine.createSpyObj('paginationComponent', { doPageChange: {} }); objectsRD$ = new BehaviorSubject(objectsRD); - component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, url, objectsRD$); + component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, objectValuesPipe, url, objectsRD$); component.paginationComponent = paginationComponent; component.ngOnInit(); }); diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts index 7f94a5eaa5..37279bcfed 100644 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts +++ b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts @@ -1,17 +1,26 @@ -import { FieldUpdates } from '../../core/data/object-updates/object-updates.reducer'; +import { FieldUpdate, FieldUpdates, Identifiable } 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 { switchMap, take } from 'rxjs/operators'; +import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators'; import { hasValue } from '../empty.util'; import { paginatedListToArray } from '../../core/shared/operators'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { ElementRef, EventEmitter, Output, ViewChild } from '@angular/core'; +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 @@ -29,7 +38,7 @@ import { PaginationComponent } from '../pagination/pagination.component'; * * An example component extending from this abstract component: PaginatedDragAndDropBitstreamListComponent */ -export abstract class AbstractPaginatedDragAndDropListComponent { +export abstract class AbstractPaginatedDragAndDropListComponent implements OnDestroy { /** * A view on the child pagination component */ @@ -57,10 +66,16 @@ export abstract class AbstractPaginatedDragAndDropListComponent; + /** + * 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 = 10; + pageSize = 3; /** * The page options to use for fetching the objects @@ -77,8 +92,21 @@ export abstract class AbstractPaginatedDragAndDropListComponent(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 + */ + loading$: BehaviorSubject = new BehaviorSubject(false); + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + protected constructor(protected objectUpdatesService: ObjectUpdatesService, - protected elRef: ElementRef) { + protected elRef: ElementRef, + protected objectValuesPipe: ObjectValuesPipe) { } /** @@ -114,6 +142,14 @@ export abstract class AbstractPaginatedDragAndDropListComponent 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); + }) + ); } /** @@ -148,19 +184,40 @@ export abstract class AbstractPaginatedDragAndDropListComponent { - this.currentPage$.next(redirectPage); - this.paginationComponent.doPageChange(redirectPage); + if (isNewPage) { + this.currentPage$.next(redirectPage); + this.loading$.next(false); + } } })); } } + + /** + * unsub all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } }