diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 2e5e51b7ee..0400e765de 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -13,7 +13,7 @@ .row-element { padding: 12px; padding: 0.75em; - border-top: $table-border-width solid $table-border-color; + border-bottom: $table-border-width solid $table-border-color; } .drag-handle { diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index af0a8b809c..57b76dbcfc 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -17,35 +17,27 @@ -
-
+
+
- - - -
- -
- -
- {{'item.edit.bitstreams.bundle.displaying' | translate:{ amount: bitstreamsRD?.payload?.elementsPerPage, total: bitstreamsRD?.payload?.totalElements } }} -
- -
- {{'loading.bitstreams' | translate}} -
-
+ + +
-
-
+
+ diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 4b102f66ec..baff48ce5c 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -13,8 +13,9 @@ import { PaginatedList } from '../../../../core/data/paginated-list'; import { BundleDataService } from '../../../../core/data/bundle-data.service'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { combineLatest as observableCombineLatest } from 'rxjs'; -import { hasNoValue } from '../../../../shared/empty.util'; +import { hasNoValue, isEmpty } from '../../../../shared/empty.util'; import { PaginatedSearchOptions } from '../../../../shared/search/paginated-search-options.model'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; @Component({ selector: 'ds-item-edit-bitstream-bundle', @@ -55,33 +56,26 @@ export class ItemEditBitstreamBundleComponent implements OnInit { * The amount of one bitstreams one "batch" resembles * The user is able to increase the amount of bitstreams displayed per bundle by this batch size until all are shown */ - batchSize = 10; + batchSize = 2; /** * The page options to use for fetching the bitstreams */ - bitstreamsOptions = { + bitstreamsOptions = Object.assign(new PaginationComponentOptions(),{ id: 'bitstreams-pagination-options', currentPage: 1, pageSize: this.batchSize - } as any; + }); /** - * The current amount of bitstreams to display for this bundle - * Starts off with just one batch + * The current page we're displaying for this bundle */ - currentSize$ = new BehaviorSubject(this.batchSize); + currentPage$ = new BehaviorSubject(1); /** - * Are we currently loading more bitstreams? + * A list of pages that have been initialized in the field-update store */ - isLoadingMore$: Observable; - - /** - * What size were the object updates last initialized with? - * Used to check if the object updates need to be re-initialized when loading more bitstreams - */ - lastInitializedWithSize: number; + initializedPages: number[] = []; constructor(private objectUpdatesService: ObjectUpdatesService, private bundleService: BundleDataService, @@ -89,27 +83,39 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } ngOnInit(): void { - this.bitstreamsRD$ = this.currentSize$.pipe( - switchMap((size: number) => this.bundleService.getBitstreams(this.bundle.id, - new PaginatedSearchOptions({pagination: Object.assign({}, this.bitstreamsOptions, { pageSize: size })}))) + this.bitstreamsRD$ = this.currentPage$.pipe( + switchMap((page: number) => this.bundleService.getBitstreams(this.bundle.id, + new PaginatedSearchOptions({pagination: Object.assign({}, this.bitstreamsOptions, { currentPage: page })}))) ); this.updates$ = this.bitstreamsRD$.pipe( toBitstreamsArray(), tap((bitstreams: Bitstream[]) => { - if (hasNoValue(this.lastInitializedWithSize) || this.currentSize$.value !== this.lastInitializedWithSize) { - this.objectUpdatesService.initialize(this.bundle.self, bitstreams, new Date(), true); - this.lastInitializedWithSize = this.currentSize$.value; + // 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 bundle, initialize the first page + this.objectUpdatesService.initializeWithCustomOrder(this.bundle.self, bitstreams, new Date(), this.batchSize, updatesPage); + this.initializedPages.push(updatesPage); + } else if (this.initializedPages.indexOf(this.currentPage$.value) < 0) { + // Updates were initialized for this bundle, but not the page we're on. Add the current page to the field-update store for this bundle + this.objectUpdatesService.addPageToCustomOrder(this.bundle.self, bitstreams, updatesPage); + this.initializedPages.push(updatesPage); } }), - switchMap((bitstreams: Bitstream[]) => this.objectUpdatesService.getFieldUpdatesByCustomOrder(this.bundle.self, bitstreams)) - ); - this.isLoadingMore$ = observableCombineLatest(this.currentSize$, this.bitstreamsRD$).pipe( - map(([size, bitstreamsRD]: [number, RemoteData>]) => size > bitstreamsRD.payload.page.length) + switchMap((bitstreams: Bitstream[]) => this.objectUpdatesService.getFieldUpdatesByCustomOrder(this.bundle.self, bitstreams, this.currentPage$.value - 1)) ); this.viewContainerRef.createEmbeddedView(this.bundleView); } + /** + * Update the current page + * @param page + */ + switchPage(page: number) { + this.currentPage$.next(page); + } + /** * A bitstream was moved, send updates to the store * @param event @@ -117,18 +123,4 @@ export class ItemEditBitstreamBundleComponent implements OnInit { drop(event: CdkDragDrop) { this.objectUpdatesService.saveMoveFieldUpdate(this.bundle.self, event.previousIndex, event.currentIndex); } - - /** - * Load more bitstreams (current size + batchSize) - */ - loadMore() { - this.currentSize$.next(this.currentSize$.value + this.batchSize); - } - - /** - * Load all bitstreams - */ - loadAll() { - this.currentSize$.next(9999); - } } diff --git a/src/app/core/data/array-move-change-analyzer.service.ts b/src/app/core/data/array-move-change-analyzer.service.ts index cb8f0548d7..91dd7809e0 100644 --- a/src/app/core/data/array-move-change-analyzer.service.ts +++ b/src/app/core/data/array-move-change-analyzer.service.ts @@ -5,6 +5,7 @@ import { Injectable } from '@angular/core'; import { CacheableObject } from '../cache/object-cache.reducer'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { hasValue } from '../../shared/empty.util'; /** * A class to determine move operations between two arrays @@ -22,15 +23,17 @@ export class ArrayMoveChangeAnalyzer { const result = []; const moved = [...array1]; array1.forEach((value: T, index: number) => { - const otherIndex = array2.indexOf(value); - const movedIndex = moved.indexOf(value); - if (index !== otherIndex && movedIndex !== otherIndex) { - moveItemInArray(moved, movedIndex, otherIndex); - result.push(Object.assign({ - op: 'move', - from: '/' + movedIndex, - path: '/' + otherIndex - }) as MoveOperation) + if (hasValue(value)) { + const otherIndex = array2.indexOf(value); + const movedIndex = moved.indexOf(value); + if (index !== otherIndex && movedIndex !== otherIndex) { + moveItemInArray(moved, movedIndex, otherIndex); + result.push(Object.assign({ + op: 'move', + from: '/' + movedIndex, + path: '/' + otherIndex + }) as MoveOperation) + } } }); return result; diff --git a/src/app/core/data/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts index d48d66dbf8..045ce97974 100644 --- a/src/app/core/data/object-updates/object-updates.actions.ts +++ b/src/app/core/data/object-updates/object-updates.actions.ts @@ -8,6 +8,7 @@ import { INotification } from '../../../shared/notifications/models/notification */ export const ObjectUpdatesActionTypes = { 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_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'), ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'), @@ -39,7 +40,9 @@ export class InitializeFieldsAction implements Action { url: string, fields: Identifiable[], lastModified: Date, - order: string[] + order: string[], + pageSize: number, + page: number }; /** @@ -50,14 +53,48 @@ export class InitializeFieldsAction implements Action { * @param fields The identifiable fields of which the updates are kept track of * @param lastModified The last modified date of the object that belongs to the page * @param order A custom order to keep track of objects moving around + * @param pageSize The page size used to fill empty pages for the custom order + * @param page The first page to populate in the custom order */ constructor( url: string, fields: Identifiable[], lastModified: Date, - order: string[] = [] + order: string[] = [], + pageSize: number = 9999, + page: number = 0 ) { - this.payload = { url, fields, lastModified, order }; + this.payload = { url, fields, lastModified, order, pageSize, page }; + } +} + +/** + * 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 }; } } @@ -254,7 +291,9 @@ export class MoveFieldUpdateAction implements Action { payload: { url: string, from: number, - to: number + to: number, + fromPage: number, + toPage: number }; /** @@ -262,15 +301,19 @@ export class MoveFieldUpdateAction implements Action { * * @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 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 */ constructor( url: string, from: number, - to: number + to: number, + fromPage: number, + toPage: number ) { - this.payload = { url, from, to }; + this.payload = { url, from, to, fromPage, toPage }; } } @@ -286,4 +329,5 @@ export type ObjectUpdatesAction | ReinstateObjectUpdatesAction | RemoveObjectUpdatesAction | RemoveFieldUpdateAction - | MoveFieldUpdateAction; + | MoveFieldUpdateAction + | AddPageToCustomOrderAction; diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index 7350ba766f..47756c58d3 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -1,5 +1,5 @@ import { - AddFieldUpdateAction, + AddFieldUpdateAction, AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, MoveFieldUpdateAction, @@ -9,8 +9,9 @@ import { RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction } from './object-updates.actions'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; -import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; +import { from } from 'rxjs/internal/observable/from'; /** * Path where discarded objects are saved @@ -59,11 +60,16 @@ export interface FieldUpdates { * A custom order given to the list of objects */ export interface CustomOrder { - initialOrder: string[], - newOrder: string[], + initialOrderPages: OrderPage[], + newOrderPages: OrderPage[], + pageSize: number; changed: boolean } +export interface OrderPage { + order: string[] +} + /** * The updated state of a single page */ @@ -104,6 +110,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: { return initializeFieldsUpdate(state, action as InitializeFieldsAction); } + case ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER: { + return addPageToCustomOrder(state, action as AddPageToCustomOrderAction); + } case ObjectUpdatesActionTypes.ADD_FIELD: { return addFieldUpdate(state, action as AddFieldUpdateAction); } @@ -147,18 +156,47 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) { const fields: Identifiable[] = action.payload.fields; 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 initialOrderPages = addOrderToPages([], order, pageSize, page); const newPageState = Object.assign( {}, state[url], { fieldStates: fieldStates }, { fieldUpdates: {} }, { lastModified: lastModifiedServer }, - { customOrder: { initialOrder: order, newOrder: order, changed: false } } + { customOrder: { + initialOrderPages: initialOrderPages, + newOrderPages: initialOrderPages, + pageSize: 9999, + changed: false } + } ); 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 order = action.payload.order; + const page = action.payload.page; + const pageState: ObjectUpdatesEntry = state[url] || {}; + const newPageState = Object.assign({}, pageState, { + fieldStates: Object.assign({}, pageState.fieldStates, fields), + 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 * @param state The current state @@ -224,9 +262,9 @@ function discardObjectUpdatesFor(url: string, state: any) { const newCustomOrder = Object.assign({}, pageState.customOrder); if (pageState.customOrder.changed) { - const initialOrder = pageState.customOrder.initialOrder; + const initialOrder = pageState.customOrder.initialOrderPages; if (isNotEmpty(initialOrder)) { - newCustomOrder.newOrder = initialOrder; + newCustomOrder.newOrderPages = initialOrder; newCustomOrder.changed = false; } } @@ -389,6 +427,17 @@ function createInitialFieldStates(fields: Identifiable[]) { 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 @@ -396,23 +445,64 @@ function createInitialFieldStates(fields: Identifiable[]) { */ function moveFieldUpdate(state: any, action: MoveFieldUpdateAction) { const url = action.payload.url; - const from = action.payload.from; - const to = action.payload.to; + const fromIndex = action.payload.from; + const toIndex = action.payload.to; + const fromPage = action.payload.fromPage; + const toPage = action.payload.toPage; const pageState: ObjectUpdatesEntry = state[url]; - const initialOrder = pageState.customOrder.initialOrder; - const customOrder = [...pageState.customOrder.newOrder]; - if (isNotEmpty(customOrder) && isNotEmpty(customOrder[from]) && isNotEmpty(customOrder[to])) { - moveItemInArray(customOrder, from, to); + const initialOrderPages = pageState.customOrder.initialOrderPages; + const customOrderPages = [...pageState.customOrder.newOrderPages]; + if (fromPage === toPage) { + if (isNotEmpty(customOrderPages[fromPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex]) && isNotEmpty(customOrderPages[fromPage].order[toIndex])) { + moveItemInArray(customOrderPages[fromPage].order, fromIndex, toIndex); + } + } else { + if (isNotEmpty(customOrderPages[fromPage]) && isNotEmpty(customOrderPages[toPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex]) && isNotEmpty(customOrderPages[toPage].order[toIndex])) { + transferArrayItem(customOrderPages[fromPage].order, customOrderPages[toPage].order, fromIndex, toIndex); + } } let changed = false; - initialOrder.forEach((id: string, index: number) => { - if (id !== customOrder[index]) { - changed = true; - return; + 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 Object.assign({}, state, { [url]: Object.assign({}, pageState, { customOrder: Object.assign({}, pageState.customOrder, { newOrder: customOrder, changed: changed }) }) }) + return Object.assign({}, state, { [url]: Object.assign({}, pageState, { customOrder: Object.assign({}, pageState.customOrder, { newOrderPages: customOrderPages, changed: 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 emptyOrderPage: OrderPage = { order: [] }; + emptyOrderPage.order.fill(undefined, 0, pageSize); + result.fill(emptyOrderPage, result.length, page - 1); + result.push(orderPage); + } + return result; } 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 42ca0eedab..6e1d8d1fc1 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -8,11 +8,11 @@ import { Identifiable, OBJECT_UPDATES_TRASH_PATH, ObjectUpdatesEntry, - ObjectUpdatesState + ObjectUpdatesState, OrderPage } from './object-updates.reducer'; import { Observable } from 'rxjs'; import { - AddFieldUpdateAction, + AddFieldUpdateAction, AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, @@ -28,6 +28,7 @@ import { INotification } from '../../../shared/notifications/models/notification import { Operation } from 'fast-json-patch'; import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service'; import { MoveOperation } from 'fast-json-patch/lib/core'; +import { flatten } from '@angular/compiler'; function objectUpdatesStateSelector(): MemoizedSelector { return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); @@ -56,10 +57,25 @@ export class ObjectUpdatesService { * @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 addCustomOrder Add a custom order list to track move changes */ - initialize(url, fields: Identifiable[], lastModified: Date, addCustomOrder?: boolean): void { - this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, addCustomOrder ? fields.map((field) => field.uuid) : [])); + initialize(url, fields: Identifiable[], lastModified: Date): void { + 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)); + } + + addPageToCustomOrder(url, fields: Identifiable[], page: number): void { + this.store.dispatch(new AddPageToCustomOrderAction(url, fields, fields.map((field) => field.uuid), page)); } /** @@ -140,13 +156,14 @@ export class ObjectUpdatesService { * 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[]): Observable { + getFieldUpdatesByCustomOrder(url: string, initialFields: Identifiable[], page = 0): Observable { const objectUpdates = this.getObjectEntry(url); return objectUpdates.pipe(map((objectEntry) => { const fieldUpdates: FieldUpdates = {}; if (hasValue(objectEntry)) { - for (const uuid of objectEntry.customOrder.newOrder) { + 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); @@ -230,12 +247,14 @@ export class ObjectUpdatesService { /** * 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 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 */ - saveMoveFieldUpdate(url: string, from: number, to: number) { - this.store.dispatch(new MoveFieldUpdateAction(url, from, to)); + saveMoveFieldUpdate(url: string, from: number, to: number, fromPage = 0, toPage = 0) { + this.store.dispatch(new MoveFieldUpdateAction(url, from, to, fromPage, toPage)); } /** @@ -350,7 +369,10 @@ export class ObjectUpdatesService { getMoveOperations(url: string): Observable { return this.getObjectEntry(url).pipe( map((objectEntry) => objectEntry.customOrder), - map((customOrder) => this.comparator.diff(customOrder.initialOrder, customOrder.newOrder)) + map((customOrder) => this.comparator.diff( + flatten(customOrder.initialOrderPages.map((orderPage: OrderPage) => orderPage.order)), + flatten(customOrder.newOrderPages.map((orderPage: OrderPage) => orderPage.order))) + ) ); } diff --git a/src/app/shared/pagination-drag-and-drop/pagination-drag-and-drop.component.html b/src/app/shared/pagination-drag-and-drop/pagination-drag-and-drop.component.html new file mode 100644 index 0000000000..033ef90834 --- /dev/null +++ b/src/app/shared/pagination-drag-and-drop/pagination-drag-and-drop.component.html @@ -0,0 +1,11 @@ + + + diff --git a/src/app/shared/pagination-drag-and-drop/pagination-drag-and-drop.component.ts b/src/app/shared/pagination-drag-and-drop/pagination-drag-and-drop.component.ts new file mode 100644 index 0000000000..2d87aa89d8 --- /dev/null +++ b/src/app/shared/pagination-drag-and-drop/pagination-drag-and-drop.component.ts @@ -0,0 +1,55 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { PaginatedList } from '../../core/data/paginated-list'; + +@Component({ + selector: 'ds-pagination-drag-and-drop', + templateUrl: './pagination-drag-and-drop.component.html', +}) +export class PaginationDragAndDropComponent { + /** + * Configuration for the NgbPagination component. + */ + @Input() paginationOptions: PaginationComponentOptions; + + /** + * The paginated list being displayed + */ + @Input() paginatedList: PaginatedList; + + /** + * Option for hiding the pagination detail + */ + @Input() public hidePaginationDetail = false; + + /** + * Option for hiding the gear + */ + @Input() public hideGear = false; + + /** + * Option for hiding the pager when there is less than 2 pages + */ + @Input() public hidePagerWhenSinglePage = true; + + /** + * Option for disabling updating and reading route parameters on pagination changes + * In other words, changing pagination won't add or update the url parameters on the current page, and the url + * parameters won't affect the pagination of this component + */ + @Input() public disableRouteParameterUpdate = false; + + /** + * An event fired when the page is changed. + * Event's payload equals to the newly selected page. + */ + @Output() pageChange: EventEmitter = new EventEmitter(); + + /** + * Switch to a different page + * @param page Page to switch to + */ + switchPage(page: number) { + this.pageChange.emit(page); + } +} diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 9c378d1aff..04309b6f9f 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -99,6 +99,13 @@ export class PaginationComponent implements OnDestroy, OnInit { */ @Input() public hidePagerWhenSinglePage = true; + /** + * Option for disabling updating and reading route parameters on pagination changes + * In other words, changing pagination won't add or update the url parameters on the current page, and the url + * parameters won't affect the pagination of this component + */ + @Input() public disableRouteParameterUpdate = false; + /** * Current page. */ @@ -173,20 +180,35 @@ export class PaginationComponent implements OnDestroy, OnInit { this.checkConfig(this.paginationOptions); this.initializeConfig(); // Listen to changes - this.subs.push(this.route.queryParams - .subscribe((queryParams) => { - if (this.isEmptyPaginationParams(queryParams)) { - this.initializeConfig(queryParams); + if (!this.disableRouteParameterUpdate) { + this.subs.push(this.route.queryParams + .subscribe((queryParams) => { + this.initializeParams(queryParams); + })); + } + } + + /** + * Initialize the route and current parameters + * This method will fix any invalid or missing parameters + * @param params + */ + private initializeParams(params) { + if (this.isEmptyPaginationParams(params)) { + this.initializeConfig(params); + } else { + this.currentQueryParams = params; + const fixedProperties = this.validateParams(params); + if (isNotEmpty(fixedProperties)) { + if (!this.disableRouteParameterUpdate) { + this.fixRoute(fixedProperties); } else { - this.currentQueryParams = queryParams; - const fixedProperties = this.validateParams(queryParams); - if (isNotEmpty(fixedProperties)) { - this.fixRoute(fixedProperties); - } else { - this.setFields(); - } + this.initializeParams(fixedProperties); } - })); + } else { + this.setFields(); + } + } } private fixRoute(fixedProperties) { @@ -247,7 +269,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The page being navigated to. */ public doPageChange(page: number) { - this.updateRoute({ pageId: this.id, page: page.toString() }); + this.updateParams(Object.assign({}, this.currentQueryParams, { pageId: this.id, page: page.toString() })); } /** @@ -257,7 +279,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The page size being navigated to. */ public doPageSizeChange(pageSize: number) { - this.updateRoute({ pageId: this.id, page: 1, pageSize: pageSize }); + this.updateParams(Object.assign({}, this.currentQueryParams,{ pageId: this.id, page: 1, pageSize: pageSize })); } /** @@ -267,7 +289,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The sort direction being navigated to. */ public doSortDirectionChange(sortDirection: SortDirection) { - this.updateRoute({ pageId: this.id, page: 1, sortDirection: sortDirection }); + this.updateParams(Object.assign({}, this.currentQueryParams,{ pageId: this.id, page: 1, sortDirection: sortDirection })); } /** @@ -277,7 +299,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The sort field being navigated to. */ public doSortFieldChange(field: string) { - this.updateRoute({ pageId: this.id, page: 1, sortField: field }); + this.updateParams(Object.assign(this.currentQueryParams,{ pageId: this.id, page: 1, sortField: field })); } /** @@ -347,6 +369,20 @@ export class PaginationComponent implements OnDestroy, OnInit { }) } + /** + * Update the current query params and optionally update the route + * @param params + */ + private updateParams(params: {}) { + if (isNotEmpty(difference(params, this.currentQueryParams))) { + if (!this.disableRouteParameterUpdate) { + this.updateRoute(params); + } else { + this.initializeParams(params); + } + } + } + /** * Method to update the route parameters */ diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index cd6417d964..a02b77c0b8 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -178,6 +178,7 @@ import { ImportableListItemControlComponent } from './object-collection/shared/i import { DragDropModule } from '@angular/cdk/drag-drop'; import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; import { CustomSwitchComponent } from './form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component'; +import { PaginationDragAndDropComponent } from './pagination-drag-and-drop/pagination-drag-and-drop.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -263,6 +264,7 @@ const COMPONENTS = [ AbstractListableElementComponent, ObjectCollectionComponent, PaginationComponent, + PaginationDragAndDropComponent, SearchFormComponent, PageWithSidebarComponent, SidebarDropdownComponent,