mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
71380: Remove object updates for drag-and-drop and send out immediate patch requests for bitstream drag-and-drop
This commit is contained in:
@@ -36,7 +36,8 @@
|
|||||||
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
|
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
|
||||||
[bundle]="bundle"
|
[bundle]="bundle"
|
||||||
[item]="item"
|
[item]="item"
|
||||||
[columnSizes]="columnSizes">
|
[columnSizes]="columnSizes"
|
||||||
|
(dropObject)="dropBitstream(bundle, $event)">
|
||||||
</ds-item-edit-bitstream-bundle>
|
</ds-item-edit-bitstream-bundle>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="bundles?.length === 0"
|
<div *ngIf="bundles?.length === 0"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { Subscription } from 'rxjs/internal/Subscription';
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
@@ -9,8 +9,8 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
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 { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
import { zip as observableZip, of as observableOf } from 'rxjs';
|
||||||
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
||||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
@@ -22,8 +22,6 @@ import { Bundle } from '../../../core/shared/bundle.model';
|
|||||||
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||||
import { Operation } from 'fast-json-patch';
|
|
||||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
|
||||||
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
||||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||||
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||||
@@ -143,7 +141,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit the current changes
|
* Submit the current changes
|
||||||
* Bitstreams that were dragged around send out a patch request with move operations to the rest API
|
|
||||||
* Bitstreams marked as deleted send out a delete request to the rest API
|
* Bitstreams marked as deleted send out a delete request to the rest API
|
||||||
* Display notifications and reset the current item/updates
|
* Display notifications and reset the current item/updates
|
||||||
*/
|
*/
|
||||||
@@ -151,32 +148,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
this.submitting = true;
|
this.submitting = true;
|
||||||
const bundlesOnce$ = this.bundles$.pipe(take(1));
|
const bundlesOnce$ = this.bundles$.pipe(take(1));
|
||||||
|
|
||||||
// Fetch all move operations for each bundle
|
|
||||||
const moveOperations$ = bundlesOnce$.pipe(
|
|
||||||
switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) =>
|
|
||||||
this.objectUpdatesService.getMoveOperations(bundle.self).pipe(
|
|
||||||
take(1),
|
|
||||||
map((operations: MoveOperation[]) => [...operations.map((operation: MoveOperation) => Object.assign(operation, {
|
|
||||||
from: `/_links/bitstreams${operation.from}/href`,
|
|
||||||
path: `/_links/bitstreams${operation.path}/href`
|
|
||||||
}))])
|
|
||||||
)
|
|
||||||
)))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send out an immediate patch request for each bundle
|
|
||||||
const patchResponses$ = observableCombineLatest(bundlesOnce$, moveOperations$).pipe(
|
|
||||||
switchMap(([bundles, moveOperationList]: [Bundle[], Operation[][]]) =>
|
|
||||||
observableZip(...bundles.map((bundle: Bundle, index: number) => {
|
|
||||||
if (isNotEmpty(moveOperationList[index])) {
|
|
||||||
return this.bundleService.patch(bundle, moveOperationList[index]);
|
|
||||||
} else {
|
|
||||||
return observableOf(undefined);
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch all removed bitstreams from the object update service
|
// Fetch all removed bitstreams from the object update service
|
||||||
const removedBitstreams$ = bundlesOnce$.pipe(
|
const removedBitstreams$ = bundlesOnce$.pipe(
|
||||||
switchMap((bundles: Bundle[]) => observableZip(
|
switchMap((bundles: Bundle[]) => observableZip(
|
||||||
@@ -201,19 +172,35 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Perform the setup actions from above in order and display notifications
|
// Perform the setup actions from above in order and display notifications
|
||||||
patchResponses$.pipe(
|
removedResponses$.pipe(take(1)).subscribe((responses: RestResponse[]) => {
|
||||||
switchMap((responses: RestResponse[]) => {
|
|
||||||
this.displayNotifications('item.edit.bitstreams.notifications.move', responses);
|
|
||||||
return removedResponses$
|
|
||||||
}),
|
|
||||||
take(1)
|
|
||||||
).subscribe((responses: RestResponse[]) => {
|
|
||||||
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
||||||
this.reset();
|
this.reset();
|
||||||
this.submitting = false;
|
this.submitting = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
|
||||||
|
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
|
||||||
|
* navigate the user to the correct page)
|
||||||
|
* @param bundle The bundle to send patch requests to
|
||||||
|
* @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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display notifications
|
* Display notifications
|
||||||
* - Error notification for each failed response with their message
|
* - Error notification for each failed response with their message
|
||||||
|
@@ -17,5 +17,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-paginated-drag-and-drop-bitstream-list [bundle]="bundle" [columnSizes]="columnSizes"></ds-paginated-drag-and-drop-bitstream-list>
|
<ds-paginated-drag-and-drop-bitstream-list [bundle]="bundle" [columnSizes]="columnSizes" (dropObject)="dropObject.emit($event)"></ds-paginated-drag-and-drop-bitstream-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core';
|
||||||
import { Bundle } from '../../../../core/shared/bundle.model';
|
import { Bundle } from '../../../../core/shared/bundle.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||||
@@ -36,6 +36,13 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() columnSizes: ResponsiveTableSizes;
|
@Input() columnSizes: ResponsiveTableSizes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bootstrap sizes used for the Bundle Name column
|
* The bootstrap sizes used for the Bundle Name column
|
||||||
* This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit
|
* This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit
|
||||||
|
@@ -8,7 +8,6 @@ import {INotification} from '../../../shared/notifications/models/notification.m
|
|||||||
*/
|
*/
|
||||||
export const ObjectUpdatesActionTypes = {
|
export const ObjectUpdatesActionTypes = {
|
||||||
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||||
ADD_PAGE_TO_CUSTOM_ORDER: type('dspace/core/cache/object-updates/ADD_PAGE_TO_CUSTOM_ORDER'),
|
|
||||||
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||||
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
||||||
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
||||||
@@ -17,8 +16,7 @@ export const ObjectUpdatesActionTypes = {
|
|||||||
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
||||||
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
||||||
REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'),
|
REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'),
|
||||||
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'),
|
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD')
|
||||||
MOVE: type('dspace/core/cache/object-updates/MOVE'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -29,8 +27,7 @@ export const ObjectUpdatesActionTypes = {
|
|||||||
export enum FieldChangeType {
|
export enum FieldChangeType {
|
||||||
UPDATE = 0,
|
UPDATE = 0,
|
||||||
ADD = 1,
|
ADD = 1,
|
||||||
REMOVE = 2,
|
REMOVE = 2
|
||||||
MOVE = 3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,10 +38,7 @@ export class InitializeFieldsAction implements Action {
|
|||||||
payload: {
|
payload: {
|
||||||
url: string,
|
url: string,
|
||||||
fields: Identifiable[],
|
fields: Identifiable[],
|
||||||
lastModified: Date,
|
lastModified: Date
|
||||||
order: string[],
|
|
||||||
pageSize: number,
|
|
||||||
page: number
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,42 +55,9 @@ export class InitializeFieldsAction implements Action {
|
|||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
fields: Identifiable[],
|
fields: Identifiable[],
|
||||||
lastModified: Date,
|
lastModified: Date
|
||||||
order: string[] = [],
|
|
||||||
pageSize: number = 9999,
|
|
||||||
page: number = 0
|
|
||||||
) {
|
) {
|
||||||
this.payload = { url, fields, lastModified, order, pageSize, page };
|
this.payload = { url, fields, lastModified };
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to initialize a new page's fields in the ObjectUpdates state
|
|
||||||
*/
|
|
||||||
export class AddPageToCustomOrderAction implements Action {
|
|
||||||
type = ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER;
|
|
||||||
payload: {
|
|
||||||
url: string,
|
|
||||||
fields: Identifiable[],
|
|
||||||
order: string[],
|
|
||||||
page: number
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new AddPageToCustomOrderAction
|
|
||||||
*
|
|
||||||
* @param url The unique url of the page for which the fields are being added
|
|
||||||
* @param fields The identifiable fields of which the updates are kept track of
|
|
||||||
* @param order A custom order to keep track of objects moving around
|
|
||||||
* @param page The page to populate in the custom order
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
url: string,
|
|
||||||
fields: Identifiable[],
|
|
||||||
order: string[] = [],
|
|
||||||
page: number = 0
|
|
||||||
) {
|
|
||||||
this.payload = { url, fields, order, page };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,43 +281,6 @@ export class RemoveFieldUpdateAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to remove a single field update in the ObjectUpdates state for a certain page url and field uuid
|
|
||||||
*/
|
|
||||||
export class MoveFieldUpdateAction implements Action {
|
|
||||||
type = ObjectUpdatesActionTypes.MOVE;
|
|
||||||
payload: {
|
|
||||||
url: string,
|
|
||||||
from: number,
|
|
||||||
to: number,
|
|
||||||
fromPage: number,
|
|
||||||
toPage: number,
|
|
||||||
field?: Identifiable
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new RemoveObjectUpdatesAction
|
|
||||||
*
|
|
||||||
* @param url
|
|
||||||
* the unique url of the page for which a field's change should be removed
|
|
||||||
* @param from The index of the object to move
|
|
||||||
* @param to The index to move the object to
|
|
||||||
* @param fromPage The page to move the object from
|
|
||||||
* @param toPage The page to move the object to
|
|
||||||
* @param field Optional field to add to the fieldUpdates list (useful when we want to track updates across multiple pages)
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
url: string,
|
|
||||||
from: number,
|
|
||||||
to: number,
|
|
||||||
fromPage: number,
|
|
||||||
toPage: number,
|
|
||||||
field?: Identifiable
|
|
||||||
) {
|
|
||||||
this.payload = { url, from, to, fromPage, toPage, field };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -369,8 +293,6 @@ export type ObjectUpdatesAction
|
|||||||
| ReinstateObjectUpdatesAction
|
| ReinstateObjectUpdatesAction
|
||||||
| RemoveObjectUpdatesAction
|
| RemoveObjectUpdatesAction
|
||||||
| RemoveFieldUpdateAction
|
| RemoveFieldUpdateAction
|
||||||
| MoveFieldUpdateAction
|
|
||||||
| AddPageToCustomOrderAction
|
|
||||||
| RemoveAllObjectUpdatesAction
|
| RemoveAllObjectUpdatesAction
|
||||||
| SelectVirtualMetadataAction
|
| SelectVirtualMetadataAction
|
||||||
| SetEditableFieldUpdateAction
|
| SetEditableFieldUpdateAction
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
AddFieldUpdateAction,
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
FieldChangeType,
|
FieldChangeType,
|
||||||
InitializeFieldsAction, MoveFieldUpdateAction,
|
InitializeFieldsAction,
|
||||||
ObjectUpdatesAction,
|
ObjectUpdatesAction,
|
||||||
ObjectUpdatesActionTypes,
|
ObjectUpdatesActionTypes,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
@@ -12,9 +12,7 @@ import {
|
|||||||
SetValidFieldUpdateAction,
|
SetValidFieldUpdateAction,
|
||||||
SelectVirtualMetadataAction,
|
SelectVirtualMetadataAction,
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
|
||||||
import { from } from 'rxjs/internal/observable/from';
|
|
||||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,20 +81,6 @@ export interface DeleteRelationship extends Relationship {
|
|||||||
keepRightVirtualMetadata: boolean,
|
keepRightVirtualMetadata: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom order given to the list of objects
|
|
||||||
*/
|
|
||||||
export interface CustomOrder {
|
|
||||||
initialOrderPages: OrderPage[],
|
|
||||||
newOrderPages: OrderPage[],
|
|
||||||
pageSize: number;
|
|
||||||
changed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrderPage {
|
|
||||||
order: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The updated state of a single page
|
* The updated state of a single page
|
||||||
*/
|
*/
|
||||||
@@ -105,7 +89,6 @@ export interface ObjectUpdatesEntry {
|
|||||||
fieldUpdates: FieldUpdates;
|
fieldUpdates: FieldUpdates;
|
||||||
virtualMetadataSources: VirtualMetadataSources;
|
virtualMetadataSources: VirtualMetadataSources;
|
||||||
lastModified: Date;
|
lastModified: Date;
|
||||||
customOrder: CustomOrder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,9 +121,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
|||||||
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
||||||
return initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
return initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER: {
|
|
||||||
return addPageToCustomOrder(state, action as AddPageToCustomOrderAction);
|
|
||||||
}
|
|
||||||
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
||||||
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
||||||
}
|
}
|
||||||
@@ -168,9 +148,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
|||||||
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
|
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
|
||||||
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
|
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.MOVE: {
|
|
||||||
return moveFieldUpdate(state, action as MoveFieldUpdateAction);
|
|
||||||
}
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -186,50 +163,18 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
|||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
const fields: Identifiable[] = action.payload.fields;
|
const fields: Identifiable[] = action.payload.fields;
|
||||||
const lastModifiedServer: Date = action.payload.lastModified;
|
const lastModifiedServer: Date = action.payload.lastModified;
|
||||||
const order = action.payload.order;
|
|
||||||
const pageSize = action.payload.pageSize;
|
|
||||||
const page = action.payload.page;
|
|
||||||
const fieldStates = createInitialFieldStates(fields);
|
const fieldStates = createInitialFieldStates(fields);
|
||||||
const initialOrderPages = addOrderToPages([], order, pageSize, page);
|
|
||||||
const newPageState = Object.assign(
|
const newPageState = Object.assign(
|
||||||
{},
|
{},
|
||||||
state[url],
|
state[url],
|
||||||
{ fieldStates: fieldStates },
|
{ fieldStates: fieldStates },
|
||||||
{ fieldUpdates: {} },
|
{ fieldUpdates: {} },
|
||||||
{ virtualMetadataSources: {} },
|
{ virtualMetadataSources: {} },
|
||||||
{ lastModified: lastModifiedServer },
|
{ lastModified: lastModifiedServer }
|
||||||
{ customOrder: {
|
|
||||||
initialOrderPages: initialOrderPages,
|
|
||||||
newOrderPages: initialOrderPages,
|
|
||||||
pageSize: pageSize,
|
|
||||||
changed: false }
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a page of objects to the state of a specific url and update a specific page of the custom order
|
|
||||||
* @param state The current state
|
|
||||||
* @param action The action to perform on the current state
|
|
||||||
*/
|
|
||||||
function addPageToCustomOrder(state: any, action: AddPageToCustomOrderAction) {
|
|
||||||
const url: string = action.payload.url;
|
|
||||||
const fields: Identifiable[] = action.payload.fields;
|
|
||||||
const fieldStates = createInitialFieldStates(fields);
|
|
||||||
const order = action.payload.order;
|
|
||||||
const page = action.payload.page;
|
|
||||||
const pageState: ObjectUpdatesEntry = state[url] || {};
|
|
||||||
const newPageState = Object.assign({}, pageState, {
|
|
||||||
fieldStates: Object.assign({}, pageState.fieldStates, fieldStates),
|
|
||||||
customOrder: Object.assign({}, pageState.customOrder, {
|
|
||||||
newOrderPages: addOrderToPages(pageState.customOrder.newOrderPages, order, pageState.customOrder.pageSize, page),
|
|
||||||
initialOrderPages: addOrderToPages(pageState.customOrder.initialOrderPages, order, pageState.customOrder.pageSize, page)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
return Object.assign({}, state, { [url]: newPageState });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new update for a specific field to the store
|
* Add a new update for a specific field to the store
|
||||||
* @param state The current state
|
* @param state The current state
|
||||||
@@ -338,19 +283,9 @@ function discardObjectUpdatesFor(url: string, state: any) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCustomOrder = Object.assign({}, pageState.customOrder);
|
|
||||||
if (pageState.customOrder.changed) {
|
|
||||||
const initialOrder = pageState.customOrder.initialOrderPages;
|
|
||||||
if (isNotEmpty(initialOrder)) {
|
|
||||||
newCustomOrder.newOrderPages = initialOrder;
|
|
||||||
newCustomOrder.changed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const discardedPageState = Object.assign({}, pageState, {
|
const discardedPageState = Object.assign({}, pageState, {
|
||||||
fieldUpdates: {},
|
fieldUpdates: {},
|
||||||
fieldStates: newFieldStates,
|
fieldStates: newFieldStates
|
||||||
customOrder: newCustomOrder
|
|
||||||
});
|
});
|
||||||
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
||||||
}
|
}
|
||||||
@@ -504,121 +439,3 @@ function createInitialFieldStates(fields: Identifiable[]) {
|
|||||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
||||||
return fieldStates;
|
return fieldStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to add a list of objects to an existing FieldStates object
|
|
||||||
* @param fieldStates FieldStates to add states to
|
|
||||||
* @param fields Identifiable objects The list of objects to add to the FieldStates
|
|
||||||
*/
|
|
||||||
function addFieldStates(fieldStates: FieldStates, fields: Identifiable[]) {
|
|
||||||
const uuids = fields.map((field: Identifiable) => field.uuid);
|
|
||||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
|
||||||
return fieldStates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move an object within the custom order of a page state
|
|
||||||
* @param state The current state
|
|
||||||
* @param action The move action to perform
|
|
||||||
*/
|
|
||||||
function moveFieldUpdate(state: any, action: MoveFieldUpdateAction) {
|
|
||||||
const url = action.payload.url;
|
|
||||||
const fromIndex = action.payload.from;
|
|
||||||
const toIndex = action.payload.to;
|
|
||||||
const fromPage = action.payload.fromPage;
|
|
||||||
const toPage = action.payload.toPage;
|
|
||||||
const field = action.payload.field;
|
|
||||||
|
|
||||||
const pageState: ObjectUpdatesEntry = state[url];
|
|
||||||
const initialOrderPages = pageState.customOrder.initialOrderPages;
|
|
||||||
const customOrderPages = [...pageState.customOrder.newOrderPages];
|
|
||||||
|
|
||||||
// Create a copy of the custom orders for the from- and to-pages
|
|
||||||
const fromPageOrder = [...customOrderPages[fromPage].order];
|
|
||||||
const toPageOrder = [...customOrderPages[toPage].order];
|
|
||||||
if (fromPage === toPage) {
|
|
||||||
if (isNotEmpty(customOrderPages[fromPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex]) && isNotEmpty(customOrderPages[fromPage].order[toIndex])) {
|
|
||||||
// Move an item from one index to another within the same page
|
|
||||||
moveItemInArray(fromPageOrder, fromIndex, toIndex);
|
|
||||||
// Update the custom order for this page
|
|
||||||
customOrderPages[fromPage] = { order: fromPageOrder };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isNotEmpty(customOrderPages[fromPage]) && hasValue(customOrderPages[toPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex])) {
|
|
||||||
// Move an item from one index of one page to an index in another page
|
|
||||||
transferArrayItem(fromPageOrder, toPageOrder, fromIndex, toIndex);
|
|
||||||
// Update the custom order for both pages
|
|
||||||
customOrderPages[fromPage] = { order: fromPageOrder };
|
|
||||||
customOrderPages[toPage] = { order: toPageOrder };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a field update if it doesn't exist for this field yet
|
|
||||||
let fieldUpdate = {};
|
|
||||||
if (hasValue(field)) {
|
|
||||||
fieldUpdate = pageState.fieldUpdates[field.uuid];
|
|
||||||
if (hasNoValue(fieldUpdate)) {
|
|
||||||
fieldUpdate = { field: field, changeType: undefined }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the store's state with new values and return
|
|
||||||
return Object.assign({}, state, { [url]: Object.assign({}, pageState, {
|
|
||||||
fieldUpdates: Object.assign({}, pageState.fieldUpdates, hasValue(field) ? { [field.uuid]: fieldUpdate } : {}),
|
|
||||||
customOrder: Object.assign({}, pageState.customOrder, { newOrderPages: customOrderPages, changed: checkForOrderChanges(initialOrderPages, customOrderPages) })
|
|
||||||
})})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two lists of OrderPage objects and return whether there's at least one change in the order of objects within
|
|
||||||
* @param initialOrderPages The initial list of OrderPages
|
|
||||||
* @param customOrderPages The changed list of OrderPages
|
|
||||||
*/
|
|
||||||
function checkForOrderChanges(initialOrderPages: OrderPage[], customOrderPages: OrderPage[]) {
|
|
||||||
let changed = false;
|
|
||||||
initialOrderPages.forEach((orderPage: OrderPage, page: number) => {
|
|
||||||
if (isNotEmpty(orderPage) && isNotEmpty(orderPage.order) && isNotEmpty(customOrderPages[page]) && isNotEmpty(customOrderPages[page].order)) {
|
|
||||||
orderPage.order.forEach((id: string, index: number) => {
|
|
||||||
if (id !== customOrderPages[page].order[index]) {
|
|
||||||
changed = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (changed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a custom order page by providing the list of all pages, a list of UUIDs, pageSize and the page to populate
|
|
||||||
* @param initialPages The initial list of OrderPage objects
|
|
||||||
* @param order The list of UUIDs to create a page for
|
|
||||||
* @param pageSize The pageSize used to populate empty spacer pages
|
|
||||||
* @param page The index of the page to add
|
|
||||||
*/
|
|
||||||
function addOrderToPages(initialPages: OrderPage[], order: string[], pageSize: number, page: number): OrderPage[] {
|
|
||||||
const result = [...initialPages];
|
|
||||||
const orderPage: OrderPage = { order: order };
|
|
||||||
if (page < result.length) {
|
|
||||||
// The page we're trying to add already exists in the list. Overwrite it.
|
|
||||||
result[page] = orderPage;
|
|
||||||
} else if (page === result.length) {
|
|
||||||
// The page we're trying to add is the next page in the list, add it.
|
|
||||||
result.push(orderPage);
|
|
||||||
} else {
|
|
||||||
// The page we're trying to add is at least one page ahead of the list, fill the list with empty pages before adding the page.
|
|
||||||
const emptyOrder = [];
|
|
||||||
for (let i = 0; i < pageSize; i++) {
|
|
||||||
emptyOrder.push(undefined);
|
|
||||||
}
|
|
||||||
const emptyOrderPage: OrderPage = { order: emptyOrder };
|
|
||||||
for (let i = result.length; i < page; i++) {
|
|
||||||
result.push(emptyOrderPage);
|
|
||||||
}
|
|
||||||
result.push(orderPage);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
@@ -8,16 +8,15 @@ import {
|
|||||||
Identifiable,
|
Identifiable,
|
||||||
OBJECT_UPDATES_TRASH_PATH,
|
OBJECT_UPDATES_TRASH_PATH,
|
||||||
ObjectUpdatesEntry,
|
ObjectUpdatesEntry,
|
||||||
ObjectUpdatesState, OrderPage,
|
ObjectUpdatesState,
|
||||||
VirtualMetadataSource
|
VirtualMetadataSource
|
||||||
} from './object-updates.reducer';
|
} from './object-updates.reducer';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
AddFieldUpdateAction,
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
FieldChangeType,
|
FieldChangeType,
|
||||||
InitializeFieldsAction,
|
InitializeFieldsAction,
|
||||||
MoveFieldUpdateAction,
|
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction,
|
RemoveFieldUpdateAction,
|
||||||
SelectVirtualMetadataAction,
|
SelectVirtualMetadataAction,
|
||||||
@@ -27,9 +26,6 @@ import {
|
|||||||
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||||
import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service';
|
|
||||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
|
||||||
import { flatten } from '@angular/compiler';
|
|
||||||
|
|
||||||
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
||||||
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
||||||
@@ -52,9 +48,7 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ObjectUpdatesService {
|
export class ObjectUpdatesService {
|
||||||
constructor(private store: Store<CoreState>,
|
constructor(private store: Store<CoreState>) {
|
||||||
private comparator: ArrayMoveChangeAnalyzer<string>) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,28 +61,6 @@ export class ObjectUpdatesService {
|
|||||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to dispatch an InitializeFieldsAction to the store and keeping track of the order objects are stored
|
|
||||||
* @param url The page's URL for which the changes are being mapped
|
|
||||||
* @param fields The initial fields for the page's object
|
|
||||||
* @param lastModified The date the object was last modified
|
|
||||||
* @param pageSize The page size to use for adding pages to the custom order
|
|
||||||
* @param page The first page to populate the custom order with
|
|
||||||
*/
|
|
||||||
initializeWithCustomOrder(url, fields: Identifiable[], lastModified: Date, pageSize = 9999, page = 0): void {
|
|
||||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, fields.map((field) => field.uuid), pageSize, page));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to dispatch an AddPageToCustomOrderAction, adding a new page to an already existing custom order tracking
|
|
||||||
* @param url The URL for which the changes are being mapped
|
|
||||||
* @param fields The fields to add a new page for
|
|
||||||
* @param page The page number (starting from index 0)
|
|
||||||
*/
|
|
||||||
addPageToCustomOrder(url, fields: Identifiable[], page: number): void {
|
|
||||||
this.store.dispatch(new AddPageToCustomOrderAction(url, fields, fields.map((field) => field.uuid), page));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to dispatch an AddFieldUpdateAction to the store
|
* Method to dispatch an AddFieldUpdateAction to the store
|
||||||
* @param url The page's URL for which the changes are saved
|
* @param url The page's URL for which the changes are saved
|
||||||
@@ -166,31 +138,6 @@ export class ObjectUpdatesService {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method that combines the state's updates with the initial values (when there's no update),
|
|
||||||
* sorted by their custom order to create a FieldUpdates object
|
|
||||||
* @param url The URL of the page for which the FieldUpdates should be requested
|
|
||||||
* @param initialFields The initial values of the fields
|
|
||||||
* @param page The page to retrieve
|
|
||||||
*/
|
|
||||||
getFieldUpdatesByCustomOrder(url: string, initialFields: Identifiable[], page = 0): Observable<FieldUpdates> {
|
|
||||||
const objectUpdates = this.getObjectEntry(url);
|
|
||||||
return objectUpdates.pipe(map((objectEntry) => {
|
|
||||||
const fieldUpdates: FieldUpdates = {};
|
|
||||||
if (hasValue(objectEntry) && hasValue(objectEntry.customOrder) && isNotEmpty(objectEntry.customOrder.newOrderPages) && page < objectEntry.customOrder.newOrderPages.length) {
|
|
||||||
for (const uuid of objectEntry.customOrder.newOrderPages[page].order) {
|
|
||||||
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
|
||||||
if (isEmpty(fieldUpdate)) {
|
|
||||||
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
|
||||||
fieldUpdate = {field: identifiable, changeType: undefined};
|
|
||||||
}
|
|
||||||
fieldUpdates[uuid] = fieldUpdate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fieldUpdates;
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to check if a specific field is currently editable in the store
|
* Method to check if a specific field is currently editable in the store
|
||||||
* @param url The URL of the page on which the field resides
|
* @param url The URL of the page on which the field resides
|
||||||
@@ -260,19 +207,6 @@ export class ObjectUpdatesService {
|
|||||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a MoveFieldUpdateAction
|
|
||||||
* @param url The page's URL for which the changes are saved
|
|
||||||
* @param from The index of the object to move
|
|
||||||
* @param to The index to move the object to
|
|
||||||
* @param fromPage The page to move the object from
|
|
||||||
* @param toPage The page to move the object to
|
|
||||||
* @param field Optional field to add to the fieldUpdates list (useful if we want to track updates across multiple pages)
|
|
||||||
*/
|
|
||||||
saveMoveFieldUpdate(url: string, from: number, to: number, fromPage = 0, toPage = 0, field?: Identifiable) {
|
|
||||||
this.store.dispatch(new MoveFieldUpdateAction(url, from, to, fromPage, toPage, field));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
||||||
* @param url The URL of the page on which the field resides
|
* @param url The URL of the page on which the field resides
|
||||||
@@ -387,7 +321,7 @@ export class ObjectUpdatesService {
|
|||||||
* @param url The page's url to check for in the store
|
* @param url The page's url to check for in the store
|
||||||
*/
|
*/
|
||||||
hasUpdates(url: string): Observable<boolean> {
|
hasUpdates(url: string): Observable<boolean> {
|
||||||
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && (isNotEmpty(objectEntry.fieldUpdates) || objectEntry.customOrder.changed)));
|
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -405,19 +339,4 @@ export class ObjectUpdatesService {
|
|||||||
getLastModified(url: string): Observable<Date> {
|
getLastModified(url: string): Observable<Date> {
|
||||||
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get move operations based on the custom order
|
|
||||||
* @param url The page's url
|
|
||||||
*/
|
|
||||||
getMoveOperations(url: string): Observable<MoveOperation[]> {
|
|
||||||
return this.getObjectEntry(url).pipe(
|
|
||||||
map((objectEntry) => objectEntry.customOrder),
|
|
||||||
map((customOrder) => this.comparator.diff(
|
|
||||||
flatten(customOrder.initialOrderPages.map((orderPage: OrderPage) => orderPage.order)),
|
|
||||||
flatten(customOrder.newOrderPages.map((orderPage: OrderPage) => orderPage.order)))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -5,19 +5,20 @@ import { PaginatedList } from '../../core/data/paginated-list';
|
|||||||
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service';
|
||||||
import { switchMap, take, tap } from 'rxjs/operators';
|
import { switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
|
import { hasValue } from '../empty.util';
|
||||||
import { paginatedListToArray } from '../../core/shared/operators';
|
import { paginatedListToArray } from '../../core/shared/operators';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||||
import { ElementRef, ViewChild } from '@angular/core';
|
import { ElementRef, EventEmitter, Output, ViewChild } from '@angular/core';
|
||||||
import { PaginationComponent } from '../pagination/pagination.component';
|
import { PaginationComponent } from '../pagination/pagination.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract component containing general methods and logic to be able to drag and drop objects within a paginated
|
* 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.
|
* 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, send an update
|
* Dragging an object on top of a page number will automatically detect the page it's being dropped on and send a
|
||||||
* to the store and add the object on top of that page.
|
* 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:
|
* To extend this component, it is important to make sure to:
|
||||||
* - Initialize objectsRD$ within the initializeObjectsRD() method
|
* - Initialize objectsRD$ within the initializeObjectsRD() method
|
||||||
@@ -34,6 +35,13 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
|||||||
*/
|
*/
|
||||||
@ViewChild(PaginationComponent, {static: false}) paginationComponent: PaginationComponent;
|
@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<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL to use for accessing the object updates from this list
|
* The URL to use for accessing the object updates from this list
|
||||||
*/
|
*/
|
||||||
@@ -52,7 +60,7 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
|||||||
/**
|
/**
|
||||||
* The amount of objects to display per page
|
* The amount of objects to display per page
|
||||||
*/
|
*/
|
||||||
pageSize = 10;
|
pageSize = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page options to use for fetching the objects
|
* The page options to use for fetching the objects
|
||||||
@@ -69,22 +77,6 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
|||||||
*/
|
*/
|
||||||
currentPage$ = new BehaviorSubject<number>(1);
|
currentPage$ = new BehaviorSubject<number>(1);
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of pages that have been initialized in the field-update store
|
|
||||||
*/
|
|
||||||
initializedPages: number[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object storing information about an update that should be fired whenever fireToUpdate is called
|
|
||||||
*/
|
|
||||||
toUpdate: {
|
|
||||||
fromIndex: number,
|
|
||||||
toIndex: number,
|
|
||||||
fromPage: number,
|
|
||||||
toPage: number,
|
|
||||||
field?: T
|
|
||||||
};
|
|
||||||
|
|
||||||
protected constructor(protected objectUpdatesService: ObjectUpdatesService,
|
protected constructor(protected objectUpdatesService: ObjectUpdatesService,
|
||||||
protected elRef: ElementRef) {
|
protected elRef: ElementRef) {
|
||||||
}
|
}
|
||||||
@@ -110,28 +102,17 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the field-updates in the store
|
* Initialize the field-updates in the store
|
||||||
* This method ensures (new) pages displayed are automatically added to the field-update store when the objectsRD updates
|
|
||||||
*/
|
*/
|
||||||
initializeUpdates(): void {
|
initializeUpdates(): void {
|
||||||
|
this.objectsRD$.pipe(
|
||||||
|
paginatedListToArray(),
|
||||||
|
take(1)
|
||||||
|
).subscribe((objects: T[]) => {
|
||||||
|
this.objectUpdatesService.initialize(this.url, objects, new Date());
|
||||||
|
});
|
||||||
this.updates$ = this.objectsRD$.pipe(
|
this.updates$ = this.objectsRD$.pipe(
|
||||||
paginatedListToArray(),
|
paginatedListToArray(),
|
||||||
tap((objects: T[]) => {
|
switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, objects))
|
||||||
// Pages in the field-update store are indexed starting at 0 (because they're stored in an array of pages)
|
|
||||||
const updatesPage = this.currentPage$.value - 1;
|
|
||||||
if (isEmpty(this.initializedPages)) {
|
|
||||||
// No updates have been initialized yet for this list, initialize the first page
|
|
||||||
this.objectUpdatesService.initializeWithCustomOrder(this.url, objects, new Date(), this.pageSize, updatesPage);
|
|
||||||
this.initializedPages.push(updatesPage);
|
|
||||||
} else if (this.initializedPages.indexOf(updatesPage) < 0) {
|
|
||||||
// Updates were initialized for this list, but not the page we're on. Add the current page to the field-update store for this list
|
|
||||||
this.objectUpdatesService.addPageToCustomOrder(this.url, objects, updatesPage);
|
|
||||||
this.initializedPages.push(updatesPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The new page is loaded into the store, check if there are any updates waiting and fire those as well
|
|
||||||
this.fireToUpdate();
|
|
||||||
}),
|
|
||||||
switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesByCustomOrder(this.url, objects, this.currentPage$.value - 1))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,52 +125,42 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object was moved, send updates to the store.
|
* 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
|
* 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.
|
* 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
|
* @param event
|
||||||
*/
|
*/
|
||||||
drop(event: CdkDragDrop<any>) {
|
drop(event: CdkDragDrop<any>) {
|
||||||
|
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
|
// 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');
|
const droppedOnElement = this.elRef.nativeElement.querySelector('.page-item:hover');
|
||||||
if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) {
|
if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) {
|
||||||
// The user is hovering over a page, fetch the page's number from the element
|
// The user is hovering over a page, fetch the page's number from the element
|
||||||
const page = Number(droppedOnElement.textContent);
|
const droppedPage = Number(droppedOnElement.textContent);
|
||||||
if (hasValue(page) && !Number.isNaN(page)) {
|
if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) {
|
||||||
const id = event.item.element.nativeElement.id;
|
dropPage = droppedPage - 1;
|
||||||
this.updates$.pipe(take(1)).subscribe((updates: FieldUpdates) => {
|
dropIndex = 0;
|
||||||
const field = hasValue(updates[id]) ? updates[id].field : undefined;
|
|
||||||
this.toUpdate = Object.assign({
|
|
||||||
fromIndex: event.previousIndex,
|
|
||||||
toIndex: 0,
|
|
||||||
fromPage: this.currentPage$.value - 1,
|
|
||||||
toPage: page - 1,
|
|
||||||
field
|
|
||||||
});
|
|
||||||
// Switch to the dropped-on page and force a page update for the pagination component
|
|
||||||
this.currentPage$.next(page);
|
|
||||||
this.paginationComponent.doPageChange(page);
|
|
||||||
if (this.initializedPages.indexOf(page - 1) >= 0) {
|
|
||||||
// The page the object is being dropped to has already been loaded before, directly fire an update to the store.
|
|
||||||
// For pages that haven't been loaded before, the updates$ observable will call fireToUpdate after the new page
|
|
||||||
// has loaded
|
|
||||||
this.fireToUpdate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.objectUpdatesService.saveMoveFieldUpdate(this.url, event.previousIndex, event.currentIndex, this.currentPage$.value - 1, this.currentPage$.value - 1);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const redirectPage = dropPage + 1;
|
||||||
* Method checking if there's an update ready to be fired. Send out a MoveFieldUpdate to the store if there's an
|
const fromIndex = (dragPage * this.pageSize) + dragIndex;
|
||||||
* update present and clear the update afterwards.
|
const toIndex = (dropPage * this.pageSize) + dropIndex;
|
||||||
*/
|
// Send out a drop event when the field exists and the "from" and "to" indexes are different from each other
|
||||||
fireToUpdate() {
|
if (fromIndex !== toIndex) {
|
||||||
if (hasValue(this.toUpdate)) {
|
this.dropObject.emit(Object.assign({
|
||||||
this.objectUpdatesService.saveMoveFieldUpdate(this.url, this.toUpdate.fromIndex, this.toUpdate.toIndex, this.toUpdate.fromPage, this.toUpdate.toPage, this.toUpdate.field);
|
fromIndex,
|
||||||
this.toUpdate = undefined;
|
toIndex,
|
||||||
|
finish: () => {
|
||||||
|
this.currentPage$.next(redirectPage);
|
||||||
|
this.paginationComponent.doPageChange(redirectPage);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user