@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
JavaScript
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;
};