UNPKG

@adaptabletools/adaptable

Version:

Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

152 lines (151 loc) 6.9 kB
import mergeWith from 'lodash/mergeWith'; import merge from 'lodash/merge'; import isArray from 'lodash/isArray'; import extend from 'lodash/extend'; import isObject from 'lodash/isObject'; import { AdaptableLogger } from '../../agGrid/AdaptableLogger'; import AdaptableHelper from '../../Utilities/Helpers/AdaptableHelper'; function customizer(objValue, srcValue) { if (isArray(objValue)) { if (!Array.isArray(srcValue)) { // TODO AFL: clarify if this is still relevant, after the state & options refactoring /** * the new value might be a function: eg: UserInterface.ContextMenuItems defaults to an array * in the redux state, while the user might provide a function * so in this case, make the user provided value win */ return srcValue; } const length = srcValue ? srcValue.length : null; const result = mergeWith(objValue, srcValue, customizer); if (length != null) { // when merging arrays, lodash result has the length of the // longest array, but we don't want that to happen // so we restrict to the current length result.length = length; } return result; } } export function AddStateSource(stateObject = {}, source) { const traverseStateObject = (object) => { Object.values(object).forEach((value) => { // add a Source only to AdaptableObjects which do NOT already have a source defined if (AdaptableHelper.isAdaptableObject(value) && value.Source == null) { value.Source = source; } if (value !== null && typeof value === 'object') { traverseStateObject(value); } }); }; traverseStateObject(stateObject); return stateObject; } export function ProcessKeepUserDefinedRevision(configState, currentUserState) { const traverseStateObject = (configObject, stateObject) => { Object.entries(configObject).forEach(([key, configValue]) => { const stateValue = stateObject[key]; if (Array.isArray(configValue) && Array.isArray(stateValue)) { const userDefinedItems = stateValue.filter((item) => { return AdaptableHelper.isAdaptableObject(item) && item.Source !== 'InitialState'; }); configObject[key] = configValue.concat(userDefinedItems); // we probably don't need to call traverseStateObject on the array as well, // so we do the traversing on the else branch only } else { if (configValue !== null && typeof configValue === 'object' && typeof stateValue === 'object') { traverseStateObject(configValue, stateValue); } } }); }; traverseStateObject(configState, currentUserState); return configState; } function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } export function MergeStateFunction(oldState, newState) { // return MergeState(oldState, newState); if (newState === '') { newState = {}; } // add source 'InitialState' only to initial state objects const initialState = AddStateSource(oldState, 'InitialState'); // source 'User' will be added to all other objects, after the merge (see bottom of this function) let state = newState; if (!state || typeof state !== 'object') { // in case loadState returns something different than an empty object AdaptableLogger.consoleWarnBase('State is something different than expected; expected an object, but received: ', state); state = {}; } // any Module in config that doesn't exist in state will be added for (const initialStateModuleName in initialState) { state[initialStateModuleName] = state[initialStateModuleName] ?? initialState[initialStateModuleName]; } // any Module in state that has an older revision than the config will be replaced for (const stateModuleName in state) { if (initialState[stateModuleName] != undefined) { // explicitly use double equals, as we want to avoid null as well const stateRevision = state[stateModuleName].Revision ?? 0; const initialStateRevision = initialState[stateModuleName].Revision ?? 0; const stateRevisionKey = typeof stateRevision === 'object' ? stateRevision.Key : stateRevision; const initialStateRevisionBehaviour = typeof initialStateRevision === 'object' ? initialStateRevision.Behavior : 'Override'; const initialStateRevisionKey = typeof initialStateRevision === 'object' ? initialStateRevision.Key : initialStateRevision; if (initialStateRevisionKey > stateRevisionKey) { if (initialStateRevisionBehaviour === 'Override') { state[stateModuleName] = initialState[stateModuleName]; } else { state[stateModuleName] = ProcessKeepUserDefinedRevision(deepClone(initialState[stateModuleName]), state[stateModuleName]); } } } } // add 'User' source to all state objects which do NOT have 'InitialState' const finalState = AddStateSource(state, 'User'); return finalState; } // main merge function export function MergeState(oldState, newState) { const result = extend({}, oldState); for (const key in newState) { if (!newState.hasOwnProperty(key)) { continue; } const value = newState[key]; // Assign if we don't need to merge at all if (!result.hasOwnProperty(key)) { result[key] = isObject(value) && !Array.isArray(value) ? merge({}, value) : value; continue; } const oldValue = result[key]; if (isObject(value) && !Array.isArray(value)) { // use both lodash functions so that we can merge from State onto Initial Adaptable State where it exists but from the former where it doesnt. result[key] = mergeWith({}, oldValue, value, customizer); } else { result[key] = value; } } return result; } let initialState; export const mergeReducer = (rootReducer, LOAD_STATE_TYPE) => { let finalReducer = rootReducer; finalReducer = (state, action) => { if (action.type === LOAD_STATE_TYPE) { initialState = initialState ?? state; state = MergeState(initialState, action.State); // put this new state on the action, since the root reducer further copies // keys from action.State to the new state action.State = state; } return rootReducer(state, action); }; return finalReducer; };