1
0
Files
yel-dspace-angular/src/app/core/data/object-updates/object-updates.reducer.ts
Kristof De Langhe c8be50614a Merge branch 'master' into w2p-68346_Bundles-in-edit-item-Updates
Conflicts:
	src/app/+item-page/edit-item-page/edit-item-page.module.ts
	src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts
	src/app/core/core.module.ts
	src/app/core/data/data.service.ts
	src/app/core/data/object-updates/object-updates.actions.ts
	src/app/core/data/object-updates/object-updates.reducer.spec.ts
	src/app/core/data/object-updates/object-updates.reducer.ts
	src/app/core/data/object-updates/object-updates.service.spec.ts
	src/app/core/data/object-updates/object-updates.service.ts
2020-02-13 14:34:45 +01:00

625 lines
21 KiB
TypeScript

import {
AddFieldUpdateAction, AddPageToCustomOrderAction,
DiscardObjectUpdatesAction,
FieldChangeType,
InitializeFieldsAction, MoveFieldUpdateAction,
ObjectUpdatesAction,
ObjectUpdatesActionTypes,
ReinstateObjectUpdatesAction,
RemoveFieldUpdateAction,
RemoveObjectUpdatesAction,
SetEditableFieldUpdateAction,
SetValidFieldUpdateAction,
SelectVirtualMetadataAction,
} from './object-updates.actions';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } 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';
/**
* Path where discarded objects are saved
*/
export const OBJECT_UPDATES_TRASH_PATH = '/trash';
/**
* The state for a single field
*/
export interface FieldState {
editable: boolean,
isNew: boolean,
isValid: boolean
}
/**
* A list of states for all the fields for a single page, mapped by uuid
*/
export interface FieldStates {
[uuid: string]: FieldState;
}
/**
* Represents every object that has a UUID
*/
export interface Identifiable {
uuid: string
}
/**
* The state of a single field update
*/
export interface FieldUpdate {
field: Identifiable,
changeType: FieldChangeType
}
/**
* The states of all field updates available for a single page, mapped by uuid
*/
export interface FieldUpdates {
[uuid: string]: FieldUpdate;
}
/**
* The states of all virtual metadata selections available for a single page, mapped by the relationship uuid
*/
export interface VirtualMetadataSources {
[source: string]: VirtualMetadataSource
}
/**
* The selection of virtual metadata for a relationship, mapped by the uuid of either the item or the relationship type
*/
export interface VirtualMetadataSource {
[uuid: string]: boolean,
}
/**
* A fieldupdate interface which represents a relationship selected to be deleted,
* along with a selection of the virtual metadata to keep
*/
export interface DeleteRelationship extends Relationship {
keepLeftVirtualMetadata: 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
*/
export interface ObjectUpdatesEntry {
fieldStates: FieldStates;
fieldUpdates: FieldUpdates;
virtualMetadataSources: VirtualMetadataSources;
lastModified: Date;
customOrder: CustomOrder
}
/**
* The updated state of all pages, mapped by the page URL
*/
export interface ObjectUpdatesState {
[url: string]: ObjectUpdatesEntry;
}
/**
* Initial state for an existing initialized field
*/
const initialFieldState = { editable: false, isNew: false, isValid: true };
/**
* Initial state for a newly added field
*/
const initialNewFieldState = { editable: true, isNew: true, isValid: undefined };
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null);
/**
* Reducer method to calculate the next ObjectUpdates state, based on the current state and the ObjectUpdatesAction
* @param state The current state
* @param action The action to perform on the current state
*/
export function objectUpdatesReducer(state = initialState, action: ObjectUpdatesAction): ObjectUpdatesState {
switch (action.type) {
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);
}
case ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA: {
return selectVirtualMetadata(state, action as SelectVirtualMetadataAction);
}
case ObjectUpdatesActionTypes.DISCARD: {
return discardObjectUpdates(state, action as DiscardObjectUpdatesAction);
}
case ObjectUpdatesActionTypes.REINSTATE: {
return reinstateObjectUpdates(state, action as ReinstateObjectUpdatesAction);
}
case ObjectUpdatesActionTypes.REMOVE: {
return removeObjectUpdates(state, action as RemoveObjectUpdatesAction);
}
case ObjectUpdatesActionTypes.REMOVE_ALL: {
return removeAllObjectUpdates(state);
}
case ObjectUpdatesActionTypes.REMOVE_FIELD: {
return removeFieldUpdate(state, action as RemoveFieldUpdateAction);
}
case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: {
return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction);
}
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
}
case ObjectUpdatesActionTypes.MOVE: {
return moveFieldUpdate(state, action as MoveFieldUpdateAction);
}
default: {
return state;
}
}
}
/**
* Initialize the state for a specific url and store all its fields in the store
* @param state The current state
* @param action The action to perform on the current state
*/
function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
const url: string = action.payload.url;
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: {} },
{ virtualMetadataSources: {} },
{ lastModified: lastModifiedServer },
{ customOrder: {
initialOrderPages: initialOrderPages,
newOrderPages: initialOrderPages,
pageSize: pageSize,
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 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
* @param state The current state
* @param action The action to perform on the current state
*/
function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
const url: string = action.payload.url;
const field: Identifiable = action.payload.field;
const changeType: FieldChangeType = action.payload.changeType;
const pageState: ObjectUpdatesEntry = state[url] || {};
let states = pageState.fieldStates;
if (changeType === FieldChangeType.ADD) {
states = Object.assign({}, { [field.uuid]: initialNewFieldState }, pageState.fieldStates)
}
let fieldUpdate: any = pageState.fieldUpdates[field.uuid] || {};
const newChangeType = determineChangeType(fieldUpdate.changeType, changeType);
fieldUpdate = Object.assign({}, { field, changeType: newChangeType });
const fieldUpdates = Object.assign({}, pageState.fieldUpdates, { [field.uuid]: fieldUpdate });
const newPageState = Object.assign({}, pageState,
{ fieldStates: states },
{ fieldUpdates: fieldUpdates });
return Object.assign({}, state, { [url]: newPageState });
}
/**
* Update the selected virtual metadata in the store
* @param state The current state
* @param action The action to perform on the current state
*/
function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) {
const url: string = action.payload.url;
const source: string = action.payload.source;
const uuid: string = action.payload.uuid;
const select: boolean = action.payload.select;
const pageState: ObjectUpdatesEntry = state[url] || {};
const virtualMetadataSource = Object.assign(
{},
pageState.virtualMetadataSources[source],
{
[uuid]: select,
},
);
const virtualMetadataSources = Object.assign(
{},
pageState.virtualMetadataSources,
{
[source]: virtualMetadataSource,
},
);
const newPageState = Object.assign(
{},
pageState,
{virtualMetadataSources: virtualMetadataSources},
);
return Object.assign(
{},
state,
{
[url]: newPageState,
}
);
}
/**
* Discard all updates for a specific action's url in the store
* @param state The current state
* @param action The action to perform on the current state
*/
function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) {
if (action.payload.discardAll) {
let newState = Object.assign({}, state);
Object.keys(state).filter((path: string) => !path.endsWith(OBJECT_UPDATES_TRASH_PATH)).forEach((path: string) => {
newState = discardObjectUpdatesFor(path, newState);
});
return newState;
} else {
const url: string = action.payload.url;
return discardObjectUpdatesFor(url, state);
}
}
/**
* Discard all updates for a specific action's url in the store
* @param url The action's url
* @param state The current state
*/
function discardObjectUpdatesFor(url: string, state: any) {
const pageState: ObjectUpdatesEntry = state[url];
const newFieldStates = {};
Object.keys(pageState.fieldStates).forEach((uuid: string) => {
const fieldState: FieldState = pageState.fieldStates[uuid];
if (!fieldState.isNew) {
/* After discarding we don't want the reset fields to stay editable or invalid */
newFieldStates[uuid] = Object.assign({}, fieldState, { editable: false, isValid: true });
}
});
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, {
fieldUpdates: {},
fieldStates: newFieldStates,
customOrder: newCustomOrder
});
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
}
/**
* Reinstate all updates for a specific action's url in the store
* @param state The current state
* @param action The action to perform on the current state
*/
function reinstateObjectUpdates(state: any, action: ReinstateObjectUpdatesAction) {
const url: string = action.payload.url;
const trashState = state[url + OBJECT_UPDATES_TRASH_PATH];
const newState = Object.assign({}, state, { [url]: trashState });
delete newState[url + OBJECT_UPDATES_TRASH_PATH];
return newState;
}
/**
* Remove all updates for a specific action's url in the store
* @param state The current state
* @param action The action to perform on the current state
*/
function removeObjectUpdates(state: any, action: RemoveObjectUpdatesAction) {
const url: string = action.payload.url;
return removeObjectUpdatesByURL(state, url);
}
/**
* Remove all updates for a specific url in the store
* @param state The current state
* @param action The action to perform on the current state
*/
function removeObjectUpdatesByURL(state: any, url: string) {
const newState = Object.assign({}, state);
delete newState[url + OBJECT_UPDATES_TRASH_PATH];
return newState;
}
/**
* Remove all updates in the store
* @param state The current state
*/
function removeAllObjectUpdates(state: any) {
const newState = Object.assign({}, state);
Object.keys(state).filter((path: string) => path.endsWith(OBJECT_UPDATES_TRASH_PATH)).forEach((path: string) => {
delete newState[path];
});
return newState;
}
/**
* Discard the update for a specific action's url and field UUID in the store
* @param state The current state
* @param action The action to perform on the current state
*/
function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) {
const url: string = action.payload.url;
const uuid: string = action.payload.uuid;
let newPageState: ObjectUpdatesEntry = state[url];
if (hasValue(newPageState)) {
const newUpdates: FieldUpdates = Object.assign({}, newPageState.fieldUpdates);
if (hasValue(newUpdates[uuid])) {
delete newUpdates[uuid];
}
const newFieldStates: FieldStates = Object.assign({}, newPageState.fieldStates);
if (hasValue(newFieldStates[uuid])) {
/* When resetting, make field not editable */
if (newFieldStates[uuid].isNew) {
/* If this field was added, just throw it away */
delete newFieldStates[uuid];
} else {
newFieldStates[uuid] = Object.assign({}, newFieldStates[uuid], { editable: false, isValid: true });
}
}
newPageState = Object.assign({}, state[url], {
fieldUpdates: newUpdates,
fieldStates: newFieldStates
});
}
return Object.assign({}, state, { [url]: newPageState });
}
/**
* Determine the most prominent FieldChangeType, ordered as follows:
* undefined < UPDATE < ADD < REMOVE
* @param oldType The current type
* @param newType The new type that should possibly override the new type
*/
function determineChangeType(oldType: FieldChangeType, newType: FieldChangeType): FieldChangeType {
if (hasNoValue(newType)) {
return oldType;
}
if (hasNoValue(oldType)) {
return newType;
}
return oldType.valueOf() > newType.valueOf() ? oldType : newType;
}
/**
* Set the editable state of a specific action's url and uuid to false or true
* @param state The current state
* @param action The action to perform on the current state
*/
function setEditableFieldUpdate(state: any, action: SetEditableFieldUpdateAction) {
const url: string = action.payload.url;
const uuid: string = action.payload.uuid;
const editable: boolean = action.payload.editable;
const pageState: ObjectUpdatesEntry = state[url];
const fieldState = pageState.fieldStates[uuid];
const newFieldState = Object.assign({}, fieldState, { editable });
const newFieldStates = Object.assign({}, pageState.fieldStates, { [uuid]: newFieldState });
const newPageState = Object.assign({}, pageState, { fieldStates: newFieldStates });
return Object.assign({}, state, { [url]: newPageState });
}
/**
* Set the isValid state of a specific action's url and uuid to false or true
* @param state The current state
* @param action The action to perform on the current state
*/
function setValidFieldUpdate(state: any, action: SetValidFieldUpdateAction) {
const url: string = action.payload.url;
const uuid: string = action.payload.uuid;
const isValid: boolean = action.payload.isValid;
const pageState: ObjectUpdatesEntry = state[url];
const fieldState = pageState.fieldStates[uuid];
const newFieldState = Object.assign({}, fieldState, { isValid });
const newFieldStates = Object.assign({}, pageState.fieldStates, { [uuid]: newFieldState });
const newPageState = Object.assign({}, pageState, { fieldStates: newFieldStates });
return Object.assign({}, state, { [url]: newPageState });
}
/**
* Method to create an initial FieldStates object based on a list of Identifiable objects
* @param fields Identifiable objects
*/
function createInitialFieldStates(fields: Identifiable[]) {
const uuids = fields.map((field: Identifiable) => field.uuid);
const fieldStates = {};
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
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;
}