UNPKG

@adaptabletools/adaptable

Version:

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

1,511 lines 85.2 kB
import * as Redux from 'redux';
import * as PluginsRedux from '../ActionsReducers/PluginsRedux';
import * as PopupRedux from '../ActionsReducers/PopupRedux';
import { PROGRESS_INDICATOR_HIDE, PROGRESS_INDICATOR_SHOW } from '../ActionsReducers/PopupRedux';
import { createEngine as createEngineLocal } from './AdaptableReduxLocalStorageEngine';
import { mergeReducer } from './AdaptableReduxMerger';
import { isAdaptableCellChangedAlert, isAdaptableRowChangedAlert, } from '../../AdaptableState/Common/AdaptableAlert';
import { ERROR_LAYOUT } from '../../Utilities/Constants/GeneralConstants';
import * as ModuleConstants from '../../Utilities/Constants/ModuleConstants';
import Emitter from '../../Utilities/Emitter';
import { StringExtensions } from '../../Utilities/Extensions/StringExtensions';
import { PreviewHelper } from '../../Utilities/Helpers/PreviewHelper';
import { ObjectFactory } from '../../Utilities/ObjectFactory';
import { showToast } from '../../View/Components/Popups/AdaptableToaster';
import * as AlertRedux from '../ActionsReducers/AlertRedux';
import * as ApplicationRedux from '../ActionsReducers/ApplicationRedux';
import * as BulkUpdateRedux from '../ActionsReducers/BulkUpdateRedux';
import * as CalculatedColumnRedux from '../ActionsReducers/CalculatedColumnRedux';
import * as ChartingRedux from '../ActionsReducers/ChartingRedux';
import * as CommentsRedux from '../ActionsReducers/CommentsRedux';
import * as CustomSortRedux from '../ActionsReducers/CustomSortRedux';
import * as DashboardRedux from '../ActionsReducers/DashboardRedux';
import * as ExportRedux from '../ActionsReducers/ExportRedux';
import * as FlashingCellRedux from '../ActionsReducers/FlashingCellRedux';
import * as FormatColumnRedux from '../ActionsReducers/FormatColumnRedux';
import * as FreeTextColumnRedux from '../ActionsReducers/FreeTextColumnRedux';
import * as LayoutRedux from '../ActionsReducers/LayoutRedux';
import * as NamedQueryRedux from '../ActionsReducers/NamedQueryRedux';
import * as NoteRedux from '../ActionsReducers/NoteRedux';
import * as PlusMinusRedux from '../ActionsReducers/PlusMinusRedux';
import * as QuickSearchRedux from '../ActionsReducers/QuickSearchRedux';
import * as ScheduleRedux from '../ActionsReducers/ScheduleRedux';
import * as ShortcutRedux from '../ActionsReducers/ShortcutRedux';
import * as SmartEditRedux from '../ActionsReducers/SmartEditRedux';
import * as StatusBarRedux from '../ActionsReducers/StatusBarRedux';
import * as StyledColumnRedux from '../ActionsReducers/StyledColumnRedux';
import * as InternalRedux from '../ActionsReducers/InternalRedux';
import * as TeamSharingRedux from '../ActionsReducers/TeamSharingRedux';
import * as ThemeRedux from '../ActionsReducers/ThemeRedux';
import * as ToolPanelRedux from '../ActionsReducers/ToolPanelRedux';
import { isAdaptableSharedEntity, isCustomSharedEntity, } from '../../AdaptableState/TeamSharingState';
export const INIT_STATE = 'INIT_STATE';
export const LOAD_STATE = 'LOAD_STATE';
const NON_PERSIST_ACTIONS = {
    '@@INIT': true,
    '@@redux/init': true,
    [LOAD_STATE]: true,
    [INIT_STATE]: true,
    // progress indicators should NOT interfere with state management as it may lead to race conditions due to load/persist state being async
    [PROGRESS_INDICATOR_SHOW]: true,
    [PROGRESS_INDICATOR_HIDE]: true,
};
const NON_PERSISTENT_STORE_KEYS = [
    'Internal',
    'Popup',
    'Comment',
    'Plugins',
];
export const InitState = () => ({
    type: INIT_STATE,
});
export const LoadState = (State) => ({
    type: LOAD_STATE,
    State,
});
export class AdaptableStore {
    /**
     *
     * @param adaptable The Adaptable instance
     * @param postLoadHook A function that hydrates the state after it has been loaded from storage
     */
    constructor(adaptable) {
        /*
          This is the main store for Adaptable State
        */
        this.loadStorageInProgress = false;
        this.loadStateOnStartup = true; // set to false if you want no state
        this.on = (eventName, callback) => {
            return this.emitter.on(eventName, callback);
        };
        this.onAny = (callback) => {
            return this.emitter.onAny(callback);
        };
        this.emit = (eventName, data) => {
            return this.emitter.emit(eventName, data);
        };
        this.loadStore = (config) => {
            const { adaptable, adaptableStateKey, initialState, postLoadHook } = config;
            const postProcessState = postLoadHook ?? ((state) => state);
            this.storageEngine.setStateKey(adaptableStateKey);
            // START STATE LOAD
            this.loadStorageInProgress = true;
            return (this.Load = this.storageEngine
                .load(initialState)
                .then((storedState) => {
                if (storedState && this.loadStateOnStartup) {
                    this.TheStore.dispatch(LoadState(postProcessState(adaptable.adaptableOptions.stateOptions.applyState(storedState))));
                }
            })
                .then(() => {
                this.TheStore.dispatch(InitState());
                // END STATE LOAD
                this.loadStorageInProgress = false;
            }, (e) => {
                adaptable.api.consoleError('Failed to load previous Adaptable State : ', e);
                //for now i'm still initializing Adaptable even if loading state has failed....
                //we may revisit that later
                this.TheStore.dispatch(InitState());
                // END STATE LOAD
                this.loadStorageInProgress = false;
                this.TheStore.dispatch(PopupRedux.PopupShowAlert({
                    alertType: 'generic',
                    header: 'Configuration',
                    message: 'Error loading your configuration:' + e,
                    alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Error'),
                }));
            }));
        };
        let rootReducerObject = {
            //  Reducers for Non-Persisted State
            Popup: PopupRedux.PopupReducer,
            Internal: InternalRedux.InternalReducer,
            Plugins: PluginsRedux.PluginsReducer,
            Comment: CommentsRedux.CommentsReducer,
            // Reducers for Persisted State
            Alert: AlertRedux.AlertReducer,
            Application: ApplicationRedux.ApplicationReducer,
            CalculatedColumn: CalculatedColumnRedux.CalculatedColumnReducer,
            Charting: ChartingRedux.ChartingReducer,
            CustomSort: CustomSortRedux.CustomSortReducer,
            Dashboard: DashboardRedux.DashboardReducer,
            Export: ExportRedux.ExportReducer,
            FlashingCell: FlashingCellRedux.FlashingCellReducer,
            FormatColumn: FormatColumnRedux.FormatColumnReducer,
            FreeTextColumn: FreeTextColumnRedux.FreeTextColumnReducer,
            Layout: LayoutRedux.LayoutReducer,
            NamedQuery: NamedQueryRedux.NamedQueryReducer,
            Note: NoteRedux.NoteReducer,
            PlusMinus: PlusMinusRedux.PlusMinusReducer,
            QuickSearch: QuickSearchRedux.QuickSearchReducer,
            Schedule: ScheduleRedux.ScheduleReducer,
            Shortcut: ShortcutRedux.ShortcutReducer,
            StatusBar: StatusBarRedux.StatusBarReducer,
            StyledColumn: StyledColumnRedux.StyledColumnReducer,
            TeamSharing: TeamSharingRedux.TeamSharingReducer,
            Theme: ThemeRedux.ThemeReducer,
            ToolPanel: ToolPanelRedux.ToolPanelReducer,
        };
        // allow plugins to participate in the root reducer
        adaptable.forPlugins((plugin) => {
            if (plugin.rootReducer) {
                rootReducerObject = {
                    ...rootReducerObject,
                    ...plugin.rootReducer(rootReducerObject),
                };
            }
        });
        const initialRootReducer = 
        // @ts-ignore
        Redux.combineReducers(rootReducerObject);
        const rootReducerWithResetManagement = (state, action) => {
            switch (action.type) {
                case LOAD_STATE:
                    const { State } = action;
                    Object.keys(State).forEach((key) => {
                        state[key] = State[key];
                    });
                    break;
            }
            return initialRootReducer(state, action);
        };
        let storageEngine;
        this.emitter = new Emitter();
        // If the user has remote storage set then we use Remote Engine, otherwise we use Local Enginge
        // not sure we can do this as we need to be backwardly compatible with existing users so need to stick with adaptable id (which should be unique)
        // const localStorageKey =  'adaptable-adaptable-state-' + adaptable.adaptableOptions.primaryKey;
        storageEngine = createEngineLocal({
            adaptableId: adaptable.adaptableOptions.adaptableId,
            adaptableStateKey: adaptable.adaptableOptions.adaptableStateKey,
            userName: adaptable.adaptableOptions.userName,
            initialState: adaptable.adaptableOptions.initialState,
            loadState: adaptable.adaptableOptions.stateOptions.loadState,
            persistState: adaptable.adaptableOptions.stateOptions.persistState,
            debounceStateDelay: adaptable.adaptableOptions.stateOptions.debounceStateDelay,
        });
        const didPersistentStateChange = (state, newState) => {
            return Object.keys(newState).some((key) => {
                if (NON_PERSISTENT_STORE_KEYS.includes(key)) {
                    return false;
                }
                return state?.[key] !== newState?.[key];
            });
        };
        // this is now VERY BADLY NAMED!
        let rootReducer = mergeReducer(rootReducerWithResetManagement, LOAD_STATE);
        const composeEnhancers = (x) => x;
        const persistedReducer = (state, action) => {
            if (adaptable.isDestroyed) {
                return state;
            }
            const init = state === undefined;
            const newState = rootReducer(state, action);
            // ideally the reducer should be pure,
            // but having the emitter emit the event here
            // is really useful
            const emitterArg = { action, state, newState };
            this.emitter.emit(action.type, emitterArg);
            const finalState = emitterArg.newState;
            const shouldPersist = state !== finalState &&
                didPersistentStateChange(state, finalState) &&
                !state?.Internal?.License?.disablePersistence &&
                !NON_PERSIST_ACTIONS[action.type] &&
                !init &&
                !this.loadStorageInProgress;
            if (shouldPersist) {
                const storageState = { ...finalState };
                NON_PERSISTENT_STORE_KEYS.forEach((key) => {
                    delete storageState[key];
                });
                this.currentStorageState = storageState;
                storageEngine.save(storageState, adaptable.adaptableOptions.stateOptions.saveState);
            }
            return finalState;
        };
        const pluginsMiddleware = [];
        adaptable.forPlugins((plugin) => {
            if (plugin.reduxMiddleware) {
                pluginsMiddleware.push(plugin.reduxMiddleware(adaptable));
            }
        });
        //TODO: need to check if we want the storage to be done before or after
        //we enrich the state with the AB middleware
        this.TheStore = Redux.createStore(persistedReducer, composeEnhancers(Redux.applyMiddleware(adaptableMiddleware(adaptable), // the main middleware that actually does stuff,
        ...pluginsMiddleware // the plugins middleware
        )));
        this.storageEngine = storageEngine;
    }
    destroy() {
        this.emitter?.clearListeners();
        this.emitter = null;
    }
    getCurrentStorageState() {
        return this.currentStorageState;
    }
    saveStateNow(adaptable) {
        const storageState = this.getCurrentStorageState();
        if (storageState) {
            return this.storageEngine.saveNow(storageState, adaptable.adaptableOptions.stateOptions.saveState);
        }
        return Promise.resolve(true);
    }
}
// this is the main function for dealing with Redux Actions which require additional functionality to be triggered.
// Please document each use case where we have to use the Store rather than a module or a popup screen
// This should ideally be the ONLY place where we LISTEN to store changes
const adaptableMiddleware = (adaptable) => (function(middlewareAPI) {
    return function (next) {
        return function (action) {
            switch (action.type) {
                /*******************
                 * NAMED QUERY ACTIONS
                 *******************/
                /**
                 * Use Case: User has deleted a Named Query
                 * Action: Check whether it is referenced elsewhere before deleting
                 */
                case NamedQueryRedux.NAMED_QUERY_DELETE: {
                    // check if Named Query is not referenced elsewhere
                    const actionTyped = action;
                    if (!adaptable.api.namedQueryApi.internalApi.validateDeletedNamedQuery(actionTyped.namedQuery.Name)) {
                        return;
                    }
                    const ret = next(action);
                    return ret;
                }
                /**
                 * Use Case: User has renamed a Named Query
                 * Action: Check whether it is referenced elsewhere before allowing rename
                 */
                case NamedQueryRedux.NAMED_QUERY_EDIT: {
                    const actionTyped = action;
                    const editedNamedQuery = actionTyped.namedQuery;
                    const previousNamedQueryName = middlewareAPI
                        .getState()
                        .NamedQuery.NamedQueries.find((namedQuery) => namedQuery.Uuid === editedNamedQuery.Uuid)?.Name ?? '';
                    if (editedNamedQuery.Name !== previousNamedQueryName) {
                        if (!adaptable.api.namedQueryApi.internalApi.validateRenamedNamedQuery(previousNamedQueryName)) {
                            return;
                        }
                    }
                    const ret = next(action);
                    return ret;
                }
                /*******************
                 * System Row Summary ACTIONS
                 *******************/
                case InternalRedux.SUMMARY_ROW_SET: {
                    let nextAction = next(action);
                    adaptable.api.layoutApi.internalApi.setupRowSummaries();
                    return nextAction;
                }
                /*******************
                 * System Quick Filter ACTIONS
                 *******************/
                case InternalRedux.QUICK_FILTER_BAR_SHOW: {
                    let nextAction = next(action);
                    adaptable.showQuickFilter();
                    return nextAction;
                }
                case InternalRedux.QUICK_FILTER_BAR_HIDE: {
                    let nextAction = next(action);
                    adaptable.hideQuickFilter();
                    return nextAction;
                }
                /*******************
                 * ALERT DEFINITION ACTIONS
                 *******************/
                case AlertRedux.ALERT_DEFINITION_ADD:
                case AlertRedux.ALERT_DEFINITION_EDIT:
                case AlertRedux.ALERT_DEFINITION_DELETE:
                case AlertRedux.ALERT_DEFINITION_UNSUSPEND:
                case AlertRedux.ALERT_DEFINITION_SUSPEND:
                case AlertRedux.ALERT_DEFINITION_UNSUSPEND_ALL:
                case AlertRedux.ALERT_DEFINITION_SUSPEND_ALL: {
                    const returnAction = next(action);
                    const alertDefinition = returnAction
                        .alertDefinition;
                    if (returnAction.type === AlertRedux.ALERT_DEFINITION_ADD ||
                        returnAction.type === AlertRedux.ALERT_DEFINITION_EDIT ||
                        returnAction.type === AlertRedux.ALERT_DEFINITION_UNSUSPEND) {
                        // in case of edit, the existing reactive alert will be deleted and recreated
                        adaptable.api.internalApi.getAlertService().createReactiveAlert(alertDefinition);
                    }
                    if (returnAction.type === AlertRedux.ALERT_DEFINITION_DELETE ||
                        returnAction.type === AlertRedux.ALERT_DEFINITION_SUSPEND) {
                        adaptable.api.internalApi.getAlertService().deleteReactiveAlert(alertDefinition);
                    }
                    if (returnAction.type === AlertRedux.ALERT_DEFINITION_SUSPEND_ALL) {
                        adaptable.api.internalApi
                            .getAlertService()
                            .getReactiveActiveAlerts()
                            .forEach((alertDefinition) => {
                            adaptable.api.internalApi.getAlertService().deleteReactiveAlert(alertDefinition);
                        });
                    }
                    if (returnAction.type === AlertRedux.ALERT_DEFINITION_UNSUSPEND_ALL) {
                        adaptable.api.alertApi.internalApi
                            .getReactiveAlertDefinitions()
                            .forEach((alertDefinition) => {
                            if (!adaptable.api.internalApi
                                .getAlertService()
                                .isReactiveAlertActive(alertDefinition)) {
                                adaptable.api.internalApi
                                    .getAlertService()
                                    .createReactiveAlert(alertDefinition);
                            }
                        });
                    }
                    // called also for rendered column actions, see RENDERED COLUMN ACTIONS block
                    adaptable.updateColumnModelAndRefreshGrid();
                    // tell Alert Module to check if need to listen to Cell Change
                    let module = (adaptable.adaptableModules.get(ModuleConstants.AlertModuleId));
                    if (module) {
                        module.checkListenToCellDataChanged();
                    }
                    return returnAction;
                }
                /*******************
                 * Flashing Cell ACTIONS
                 *******************/
                case FlashingCellRedux.FLASHING_CELL_DEFINITION_ADD:
                case FlashingCellRedux.FLASHING_CELL_DEFINITION_EDIT:
                case FlashingCellRedux.FLASHING_CELL_DEFINITION_DELETE:
                case FlashingCellRedux.FLASHING_CELL_DEFINITION_UNSUSPEND:
                case FlashingCellRedux.FLASHING_CELL_DEFINITION_SUSPEND:
                case FlashingCellRedux.FLASHING_CELL_DEFINITION_UNSUSPEND_ALL:
                case FlashingCellRedux.FLASHING_CELL_DEFINITION_SUSPEND_ALL: {
                    const returnAction = next(action);
                    // called also for rendered column actions, see RENDERED COLUMN ACTIONS block
                    adaptable.updateColumnModelAndRefreshGrid();
                    // check if cell data change listening needs to be activated
                    let module = (adaptable.adaptableModules.get(ModuleConstants.FlashingCellModuleId));
                    if (module) {
                        module.checkListenToCellDataChanged();
                    }
                    return returnAction;
                }
                /*******************
                 * FREE TEXT COLUMN ACTIONS
                 *******************/
                /**
                 * Use Case: We have added / edited / deleted a Free Text Column
                 * Actions: We check if a deleted Column has references and return if so
                 * Actions: We update the Special ColumnDefs and refresh the Layout
                 */
                case FreeTextColumnRedux.FREE_TEXT_COLUMN_ADD:
                case FreeTextColumnRedux.FREE_TEXT_COLUMN_EDIT:
                case FreeTextColumnRedux.FREE_TEXT_COLUMN_DELETE: {
                    // First check to see Deleted Free Text Column is referenced elsewhwere and return if so
                    if (action.type === FreeTextColumnRedux.FREE_TEXT_COLUMN_DELETE) {
                        const actionTyped = action;
                        if (!adaptable.api.freeTextColumnApi.internalApi.validateDeletedFreeTextColumn(actionTyped.freeTextColumn)) {
                            return;
                        }
                    }
                    const returnAction = next(action);
                    // Check listen to Cell Data Changes and refresh the layout
                    let module = (adaptable.adaptableModules.get(ModuleConstants.FreeTextColumnModuleId));
                    if (module) {
                        module.checkListenToCellDataChanged();
                    }
                    adaptable.api.layoutApi.internalApi.refreshLayout();
                    return returnAction;
                }
                /*******************
                 * CALCULATED COLUMN ACTIONS
                 *******************/
                /**
                 * Use Case: We have added / edited / deleted a Calculated Column
                 * Action: We update the Special ColumnDefs and refresh the Layout
                 */
                case CalculatedColumnRedux.CALCULATED_COLUMN_ADD:
                case CalculatedColumnRedux.CALCULATED_COLUMN_EDIT:
                case CalculatedColumnRedux.CALCULATED_COLUMN_DELETE: {
                    const actionTypedCC = action;
                    const calculatedColumn = actionTypedCC.calculatedColumn;
                    // First check for Deleted Calculated Columns with references and return if found
                    if (action.type === CalculatedColumnRedux.CALCULATED_COLUMN_DELETE) {
                        if (!adaptable.api.calculatedColumnApi.internalApi.validateDeletedCalculatedColumn(calculatedColumn)) {
                            return;
                        }
                    }
                    const returnAction = next(action);
                    // Manage Aggregations, fire Event, set up listener and refresh Layout
                    if (action.type === CalculatedColumnRedux.CALCULATED_COLUMN_ADD ||
                        action.type === CalculatedColumnRedux.CALCULATED_COLUMN_EDIT) {
                        adaptable.api.internalApi
                            .getCalculatedColumnExpressionService()
                            .createAggregatedScalarLiveValue(calculatedColumn);
                    }
                    else {
                        adaptable.api.internalApi
                            .getCalculatedColumnExpressionService()
                            .destroyAggregatedScalarLiveValue(calculatedColumn);
                    }
                    adaptable.api.eventApi.internalApi.fireCalculatedColumnChangedEvent(action.type, calculatedColumn);
                    let module = (adaptable.adaptableModules.get(ModuleConstants.CalculatedColumnModuleId));
                    if (module) {
                        module.checkListenToCellDataChanged();
                    }
                    adaptable.api.layoutApi.internalApi.refreshLayout();
                    return returnAction;
                }
                /*******************
                 * RENDERED COLUMN ACTIONS
                 *******************/
                /**
                 * Use Case: We have updated an AdapTable Module that affects rendering
                 * Action: We set up all columns again
                 */
                case QuickSearchRedux.QUICK_SEARCH_SET_STYLE:
                case FormatColumnRedux.FORMAT_COLUMN_ADD:
                case FormatColumnRedux.FORMAT_COLUMN_EDIT:
                case FormatColumnRedux.FORMAT_COLUMN_DELETE:
                case FormatColumnRedux.FORMAT_COLUMN_DELETE_ALL:
                case FormatColumnRedux.FORMAT_COLUMN_MOVE_DOWN:
                case FormatColumnRedux.FORMAT_COLUMN_MOVE_UP:
                case FormatColumnRedux.FORMAT_COLUMN_SUSPEND:
                case FormatColumnRedux.FORMAT_COLUMN_UNSUSPEND:
                case FormatColumnRedux.FORMAT_COLUMN_SUSPEND_ALL:
                case FormatColumnRedux.FORMAT_COLUMN_UNSUSPEND_ALL:
                case StyledColumnRedux.STYLED_COLUMN_ADD:
                case StyledColumnRedux.STYLED_COLUMN_EDIT:
                case StyledColumnRedux.STYLED_COLUMN_DELETE:
                case StyledColumnRedux.STYLED_COLUMN_SUSPEND:
                case StyledColumnRedux.STYLED_COLUMN_UNSUSPEND:
                case StyledColumnRedux.STYLED_COLUMN_SUSPEND_ALL:
                case StyledColumnRedux.STYLED_COLUMN_UNSUSPEND_ALL:
                case CustomSortRedux.CUSTOM_SORT_ADD:
                case CustomSortRedux.CUSTOM_SORT_EDIT:
                case CustomSortRedux.CUSTOM_SORT_DELETE:
                case CustomSortRedux.CUSTOM_SORT_SUSPEND:
                case CustomSortRedux.CUSTOM_SORT_UNSUSPEND:
                case CustomSortRedux.CUSTOM_SORT_SUSPEND_ALL:
                case CustomSortRedux.CUSTOM_SORT_UNSUSPEND_ALL: {
                    const returnAction = next(action);
                    // called also for alert actions, see ALERT ACTIONS block
                    adaptable.updateColumnModelAndRefreshGrid();
                    return returnAction;
                }
                /*******************
                 * QUICK SEARCH ACTIONS
                 *******************/
                /**
                 * Use Case: User has run a Quick Search
                 * Action1: Call Adaptable to redraw body so cells can be highlighted
                 * Action2: Run Query using Quick Search text
                 */
                case QuickSearchRedux.QUICK_SEARCH_RUN: {
                    let returnAction = next(action);
                    adaptable.api.gridApi.refreshAllCells(true);
                    // if set then return a query on the text
                    if (adaptable.adaptableOptions.quickSearchOptions.filterGridAfterQuickSearch) {
                        const actionTyped = action;
                        const searchText = actionTyped.quickSearchText;
                        if (StringExtensions.IsNotNullOrEmpty(searchText)) {
                            adaptable.setAgGridQuickSearch(searchText);
                        }
                        else {
                            adaptable.clearAgGridQuickSearch();
                        }
                    }
                    return returnAction;
                }
                /*******************
                 * INTERNAL ACTIONS
                 *******************/
                /**
                 * Use Case: User has deleted a System Alert which has a Highlight Cell or Highlight Row
                 * Action: Refresh the cell / Row (to clear the Highlight)
                 */
                case InternalRedux.ALERT_DELETE: {
                    const actionTyped = action;
                    let ret = next(action);
                    const adaptableAlert = actionTyped.alert;
                    if (adaptableAlert.alertDefinition.AlertProperties?.HighlightCell &&
                        isAdaptableCellChangedAlert(adaptableAlert) &&
                        adaptableAlert.cellDataChangedInfo) {
                        const rowNode = adaptableAlert.cellDataChangedInfo.rowNode;
                        adaptable.api.gridApi.refreshCell(rowNode, adaptableAlert.cellDataChangedInfo.column.columnId, true);
                    }
                    if (adaptableAlert.alertDefinition.AlertProperties?.HighlightRow &&
                        isAdaptableRowChangedAlert(adaptableAlert) &&
                        adaptableAlert.rowDataChangedInfo) {
                        adaptable.api.gridApi.refreshRowNodes(adaptableAlert.rowDataChangedInfo.rowNodes);
                    }
                    return ret;
                }
                /**
                 * Use Case: User has deleted all System Alerts some of which have a Highlight Cell
                 * Action: Refresh the cell (to clear the style)
                 */
                case InternalRedux.ALERT_DELETE_ALL: {
                    const actionTyped = action;
                    let ret = next(action);
                    let alerts = actionTyped.alerts;
                    alerts.forEach((alert) => {
                        if (alert.alertDefinition.AlertProperties?.HighlightCell &&
                            isAdaptableCellChangedAlert(alert) &&
                            alert.cellDataChangedInfo) {
                            let rowNode = alert.cellDataChangedInfo.rowNode;
                            adaptable.api.gridApi.refreshCell(rowNode, alert.cellDataChangedInfo.column.columnId, true);
                        }
                        if (alert.alertDefinition.AlertProperties?.HighlightRow &&
                            isAdaptableRowChangedAlert(alert) &&
                            alert.rowDataChangedInfo) {
                            adaptable.api.gridApi.refreshRowNodes(alert.rowDataChangedInfo.rowNodes);
                        }
                    });
                    return ret;
                }
                /**
                 * Use Case: A System Alert had a Highlight Cell with a limited duration
                 * Action: Refresh the cell (to clear the style)
                 */
                case InternalRedux.ALERT_REMOVE_CELL_HIGHLIGHT: {
                    let ret = next(action);
                    const actionTyped = action;
                    const adaptableAlert = actionTyped.alert;
                    if (adaptableAlert.alertDefinition.AlertProperties?.HighlightCell) {
                        if (isAdaptableCellChangedAlert(adaptableAlert) &&
                            adaptableAlert.cellDataChangedInfo) {
                            const rowNode = adaptableAlert.cellDataChangedInfo.rowNode;
                            adaptable.api.gridApi.refreshCell(rowNode, adaptableAlert.cellDataChangedInfo.column.columnId, true);
                        }
                    }
                    return ret;
                }
                case InternalRedux.ALERT_REMOVE_ROW_HIGHLIGHT: {
                    let ret = next(action);
                    const actionTyped = action;
                    const adaptableAlert = actionTyped.alert;
                    if (adaptableAlert.alertDefinition.AlertProperties?.HighlightRow) {
                        if (isAdaptableCellChangedAlert(adaptableAlert) &&
                            adaptableAlert.cellDataChangedInfo) {
                            adaptable.api.gridApi.refreshRowNodes([adaptableAlert.cellDataChangedInfo.rowNode]);
                        }
                        if (isAdaptableRowChangedAlert(adaptableAlert) && adaptableAlert.rowDataChangedInfo) {
                            adaptable.api.gridApi.refreshRowNodes(adaptableAlert.rowDataChangedInfo.rowNodes);
                        }
                    }
                    return ret;
                }
                case InternalRedux.HIGHLIGHT_CELL_ADD: {
                    const actionTyped = action;
                    const ret = next(action);
                    const cellHighlightInfo = actionTyped.cellHighlightInfo;
                    const rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(cellHighlightInfo.primaryKeyValue);
                    if (rowNode) {
                        adaptable.api.gridApi.refreshCell(rowNode, cellHighlightInfo.columnId, true);
                    }
                    return ret;
                }
                case InternalRedux.HIGHLIGHT_CELL_DELETE: {
                    const actionTyped = action;
                    const ret = next(action);
                    const rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(actionTyped.primaryKeyValue);
                    if (rowNode) {
                        adaptable.api.gridApi.refreshCell(rowNode, actionTyped.columnId, true);
                    }
                    return ret;
                }
                case InternalRedux.HIGHLIGHT_CELL_DELETE_ALL: {
                    const cellHighlightInfos = middlewareAPI.getState().Internal.CellHighlightInfo;
                    const ret = next(action);
                    cellHighlightInfos.forEach((cellHighlightInfo) => {
                        const rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(cellHighlightInfo.primaryKeyValue);
                        if (!rowNode) {
                            return;
                        }
                        adaptable.api.gridApi.refreshCell(rowNode, cellHighlightInfo.columnId, true);
                    });
                    return ret;
                }
                case InternalRedux.HIGHLIGHT_ROW_ADD: {
                    const actionTyped = action;
                    const ret = next(action);
                    const rowHighlightInfo = actionTyped.rowHighlightInfo;
                    const rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(rowHighlightInfo.primaryKeyValue);
                    if (rowNode) {
                        adaptable.api.gridApi.refreshRowNode(rowNode);
                    }
                    return ret;
                }
                case InternalRedux.HIGHLIGHT_ROW_DELETE: {
                    const actionTyped = action;
                    const ret = next(action);
                    const rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(actionTyped.primaryKeyValue);
                    if (rowNode) {
                        adaptable.api.gridApi.refreshRowNode(rowNode);
                    }
                    return ret;
                }
                case InternalRedux.HIGHLIGHT_ROWS_ADD: {
                    const actionTyped = action;
                    const ret = next(action);
                    const rowsHighlightInfo = actionTyped.rowsHighlightInfo;
                    const rowNodes = adaptable.api.gridApi.getRowNodesForPrimaryKeys(rowsHighlightInfo.primaryKeyValues);
                    if (rowNodes.length) {
                        adaptable.api.gridApi.refreshRowNodes(rowNodes);
                    }
                    return ret;
                }
                case InternalRedux.HIGHLIGHT_ROWS_DELETE: {
                    const actionTyped = action;
                    const ret = next(action);
                    const rowNodes = adaptable.api.gridApi.getRowNodesForPrimaryKeys(actionTyped.primaryKeyValues);
                    if (rowNodes.length) {
                        adaptable.api.gridApi.refreshRowNodes(rowNodes);
                    }
                    return ret;
                }
                case InternalRedux.HIGHLIGHT_ROW_DELETE_ALL: {
                    const rowHighlightInfos = middlewareAPI.getState().Internal.RowHighlightInfo;
                    const ret = next(action);
                    rowHighlightInfos.forEach((rowHighlightInfo) => {
                        const rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(rowHighlightInfo.primaryKeyValue);
                        if (!rowNode) {
                            return;
                        }
                        adaptable.api.gridApi.refreshRowNode(rowNode);
                    });
                    return ret;
                }
                case InternalRedux.DATA_CHANGE_HISTORY_UNDO:
                    const actionTypedUndo = action;
                    const cellDataChangedInfo = actionTypedUndo.changeInfo;
                    adaptable.api.gridApi.undoCellEdit(cellDataChangedInfo);
                    return next(action);
                case InternalRedux.DATA_CHANGE_HISTORY_ENABLE:
                case InternalRedux.DATA_CHANGE_HISTORY_DISABLE:
                case InternalRedux.DATA_CHANGE_HISTORY_SUSPEND:
                case InternalRedux.DATA_CHANGE_HISTORY_RESUME:
                    const returnAction = next(action);
                    let module = (adaptable.adaptableModules.get(ModuleConstants.DataChangeHistoryModuleId));
                    if (module) {
                        module.checkListenToCellDataChanged();
                    }
                    return returnAction;
                case InternalRedux.CREATE_CELL_SUMMARY_INFO: {
                    let module = (adaptable.adaptableModules.get(ModuleConstants.CellSummaryModuleId));
                    let returnAction = next(action);
                    let selectedCellInfo = middlewareAPI.getState().Internal.SelectedCellInfo;
                    let apiSummaryReturn = module.createCellSummaryInfo(selectedCellInfo);
                    adaptable.api.internalApi.setCellSummaryInfo(apiSummaryReturn);
                    return returnAction;
                }
                /*******************
                 * DATA SOURCE ACTIONS
                 *******************/
                /**
                 * Use Case: Data Sources have been amended
                 * Action1: Fire Data Source Changed event
                 * Action2: Display a DataSet form if supplied
                 */
                case InternalRedux.DATA_SET_SELECT: {
                    let returnAction = next(action);
                    const dataSet = adaptable.api.dataSetApi.getCurrentDataSet();
                    adaptable.api.eventApi.internalApi.fireDataSetSelectedEvent(dataSet);
                    requestAnimationFrame(() => {
                        adaptable.api.dataSetApi.internalApi.showDataSetForm(dataSet);
                    });
                    return returnAction;
                }
                /*******************
                 * THEME ACTIONS
                 *******************/
                /**
                 * Use Case: Theme has changed
                 * Action: Apply new Theme
                 */
                case ThemeRedux.THEME_SELECT: {
                    let returnAction = next(action);
                    adaptable.api.themeApi.applyCurrentTheme();
                    return returnAction;
                }
                /*******************
                 * Note ACTIONS
                 *******************/
                /**
                 * Use Case: Note has been edited/deleted/added
                 * Action: Triangle can be removed/added
                 */
                case NoteRedux.NOTE_ADD:
                case NoteRedux.NOTE_EDIT:
                case NoteRedux.NOTE_DELETE: {
                    let returnAction = next(action);
                    adaptable.AnnotationsService.checkListenToEvents();
                    const rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(returnAction.adaptableNote.PrimaryKeyValue);
                    adaptable.api.gridApi.refreshCell(rowNode, returnAction.adaptableNote.ColumnId, true);
                    return returnAction;
                }
                /*******************
                 * Comment ACTIONS
                 *******************/
                /**
                 * Use Case: Comments has been edited/deleted/added
                 * Action: Triangle can be removed/added
                 */
                case CommentsRedux.COMMENTS_ADD:
                case CommentsRedux.COMMENTS_EDIT:
                case CommentsRedux.COMMENTS_DELETE:
                case CommentsRedux.COMMENTS_CELL_DELETE:
                case CommentsRedux.COMMENTS_CELL_ADD: {
                    let returnAction = next(action);
                    adaptable.AnnotationsService.checkListenToEvents();
                    let rowNode = null;
                    let columnId = null;
                    if ('cellAddress' in returnAction) {
                        rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(returnAction.cellAddress.PrimaryKeyValue);
                        columnId = returnAction.cellAddress.ColumnId;
                    }
                    else if ('cellComments' in returnAction) {
                        rowNode = adaptable.api.gridApi.getRowNodeForPrimaryKey(returnAction.cellComments.PrimaryKeyValue);
                        columnId = returnAction.cellComments.ColumnId;
                    }
                    if (rowNode && columnId) {
                        adaptable.api.gridApi.refreshCell(rowNode, columnId, true);
                        requestAnimationFrame(() => {
                            const commentThreads = adaptable.api.commentApi.getAllComments();
                            adaptable.api.eventApi.emit('CommentChanged', commentThreads);
                            adaptable.api.optionsApi
                                .getCommentOptions()
                                ?.persistCommentThreads?.(commentThreads);
                        });
                    }
                    return returnAction;
                }
                case CommentsRedux.COMMENTS_LOAD: {
                    const previousCommentThreads = adaptable.api.commentApi.getAllComments();
                    let returnAction = next(action);
                    const newCommentThreads = adaptable.api.commentApi.getAllComments() ?? [];
                    requestAnimationFrame(() => {
                        let addedCommentThreads = [];
                        let deletedCommentThreads = [];
                        const prevCommentThreadsSet = new Set((previousCommentThreads ?? []).map((c) => c.Uuid));
                        const newCommentThreadsSet = new Set((newCommentThreads ?? []).map((c) => c.Uuid));
                        for (const commentThread of newCommentThreads) {
                            if (!prevCommentThreadsSet.has(commentThread.Uuid)) {
                                addedCommentThreads.push(commentThread);
                            }
                        }
                        for (const commentThread of previousCommentThreads) {
                            if (!newCommentThreadsSet.has(commentThread.Uuid)) {
                                deletedCommentThreads.push(commentThread);
                            }
                        }
                        // This cannot be called because it would trigger an infinite loop
                        // adaptable.api.eventApi.emit('CommentChanged', commentThreads);
                        // refresh all comments cells to show or hide the triangle
                        [...addedCommentThreads, ...deletedCommentThreads].forEach((commentThread) => {
                            const node = adaptable.api.gridApi.getRowNodeForPrimaryKey(commentThread.PrimaryKeyValue);
                            if (node && commentThread.ColumnId) {
                                adaptable.api.gridApi.refreshCell(node, commentThread.ColumnId, true);
                            }
                        });
                    });
                    return returnAction;
                }
                /*******************
                 * SCHEDULE ACTIONS
                 *******************/
                /**
                 * Use Case: Schedule has changed
                 * Action: Set up ALL jobs
                 */
                case ScheduleRedux.REMINDER_SCHEDULE_ADD:
                case ScheduleRedux.REMINDER_SCHEDULE_EDIT:
                case ScheduleRedux.REMINDER_SCHEDULE_DELETE:
                case ScheduleRedux.REMINDER_SCHEDULE_UNSUSPEND:
                case ScheduleRedux.REMINDER_SCHEDULE_SUSPEND:
                case ScheduleRedux.REMINDER_SCHEDULE_UNSUSPEND_ALL:
                case ScheduleRedux.REMINDER_SCHEDULE_SUSPEND_ALL:
                case ScheduleRedux.REPORT_SCHEDULE_ADD:
                case ScheduleRedux.REPORT_SCHEDULE_EDIT:
                case ScheduleRedux.REPORT_SCHEDULE_DELETE:
                case ScheduleRedux.REPORT_SCHEDULE_SUSPEND:
                case ScheduleRedux.REPORT_SCHEDULE_UNSUSPEND:
                case ScheduleRedux.REPORT_SCHEDULE_SUSPEND_ALL:
                case ScheduleRedux.REPORT_SCHEDULE_UNSUSPEND_ALL:
                case ScheduleRedux.IPUSHPULL_SCHEDULE_ADD:
                case ScheduleRedux.IPUSHPULL_SCHEDULE_EDIT:
                case ScheduleRedux.IPUSHPULL_SCHEDULE_DELETE:
                case ScheduleRedux.IPUSHPULL_SCHEDULE_SUSPEND:
                case ScheduleRedux.IPUSHPULL_SCHEDULE_UNSUSPEND:
                case ScheduleRedux.IPUSHPULL_SCHEDULE_SUSPEND_ALL:
                case ScheduleRedux.IPUSHPULL_SCHEDULE_UNSUSPEND_ALL:
                case ScheduleRedux.OPENFIN_SCHEDULE_ADD:
                case ScheduleRedux.OPENFIN_SCHEDULE_EDIT:
                case ScheduleRedux.OPENFIN_SCHEDULE_DELETE:
                case ScheduleRedux.OPENFIN_SCHEDULE_SUSPEND:
                case ScheduleRedux.OPENFIN_SCHEDULE_UNSUSPEND:
                case ScheduleRedux.OPENFIN_SCHEDULE_SUSPEND_ALL:
                case ScheduleRedux.OPENFIN_SCHEDULE_UNSUSPEND_ALL: {
                    let returnAction = next(action);
                    let module = (adaptable.adaptableModules.get(ModuleConstants.ScheduleModuleId));
                    module.setUpScheduleJobs();
                    return returnAction;
                }
                /*******************
                 * DASHBOARD ACTIONS
                 *******************/
                case DashboardRedux.DASHBOARD_SET_IS_COLLAPSED:
                case DashboardRedux.DASHBOARD_SET_MODULE_BUTTONS:
                case DashboardRedux.DASHBOARD_ACTIVE_TAB_INDEX_CHANGE:
                case DashboardRedux.DASHBOARD_SET_IS_FLOATING:
                case DashboardRedux.DASHBOARD_SET_IS_INLINE:
                case DashboardRedux.DASHBOARD_SET_IS_HIDDEN:
                case DashboardRedux.DASHBOARD_SET_FLOATING_POSITION:
                case DashboardRedux.DASHBOARD_SET_TABS:
                case DashboardRedux.DASHBOARD_CLOSE_TOOLBAR: {
                    const oldDashboardState = middlewareAPI.getState().Dashboard;
                    let returnAction = next(action);
                    const newDashboardState = middlewareAPI.getState().Dashboard;
                    adaptable.api.eventApi.internalApi.fireDashboardChangedEvent(action.type, oldDashboardState, newDashboardState);
                    return returnAction;
                }
                /*******************
                 * LAYOUT ACTIONS
                 *******************/
                case LayoutRedux.LAYOUT_ADD:
                case LayoutRedux.LAYOUT_EDIT:
                case LayoutRedux.LAYOUT_SAVE:
                case LayoutRedux.LAYOUT_DELETE:
                case LayoutRedux.LAYOUT_COLUMN_SET_CAPTION:
                case LayoutRedux.LAYOUT_SELECT: {
                    const oldLayoutState = middlewareAPI.getState().Layout;
                    // this must be called before 'next(action)'
                    const previousLayout = adaptable.api.layoutApi.getCurrentLayout();
                    let returnAction = next(action);
                    const newLayoutState = middlewareAPI.getState().Layout;
                    adaptable.api.eventApi.internalApi.fireLayoutChangedEvent(action.type, oldLayoutState, newLayoutState);
                    const oldLayout = (oldLayoutState.Layouts || []).find((l) => l.Name == oldLayoutState.CurrentLayout) || ERROR_LAYOUT;
                    const newLayout = (newLayoutState.Layouts || []).find((l) => l.Name == newLayoutState.CurrentLayout) ||
                        newLayoutState.Layouts[0] ||
                        ERROR_LAYOUT;
                    // Apply filtering if Column or Grid filters have been changed
                    let refreshFilters = false;
                    if (adaptable.api.filterApi.columnFilterApi.internalApi.areColumnFiltersDifferent(oldLayout.ColumnFilters, newLayout.ColumnFilters)) {
                        refreshFilters = true;
                    }
                    // Apply Grid filtering if Grid filter has been changed
                    if (!refreshFilters &&
                        adaptable.api.filterApi.gridFilterApi.internalApi.isGridFilterDifferent(oldLayout.GridFilter, newLayout.GridFilter)) {
                        refreshFilters = true;
                    }
                    if (refreshFilters) {
                        adaptable.applyFiltering();
                    }
                    if (returnAction.type == LayoutRedux.LAYOUT_SELECT ||
                        returnAction.type == LayoutRedux.LAYOUT_DELETE ||
                        returnAction.type == LayoutRedux.LAYOUT_COLUMN_SET_CAPTION) {
                        // tell AdapTable the Layout has been selected
                        if (newLayout) {
                            adaptable.setLayout(newLayout);
                        }
                    }
                    // when changing current layout via the api, the layout should update
                    if (returnAction.type == LayoutRedux.LAYOUT_SAVE) {
                        const savingLayout = returnAction.layout;
                        if (previousLayout.Name === savingLayout.Name &&
                            previousLayout !== savingLayout &&
                            // objects may have changed, but have the same contents
                            // this prevents pivot layout from infinite set-layout
                            !adaptable.api.layoutApi.internalApi.areLayoutsEqual(previousLayout, savingLayout)) {
                            adaptable.setLayout(savingLayout);
                        }
                    }
                    return returnAction;
                }
                /**
                 * Use Case: Column Filters have changed
                 * Action: Apply Column Filtering and fire associated events
                 */
                case LayoutRedux.LAYOUT_COLUMN_FILTER_ADD:
                // see ref "also-this-handles-the-LAYOUT_COLUMN_FILTER_EDIT-case"
                // case LayoutRedux.LAYOUT_COLUMN_FILTER_EDIT:
                case LayoutRedux.LAYOUT_COLUMN_FILTER_SET:
                case LayoutRedux.LAYOUT_COLUMN_FILTER_CLEAR:
                case LayoutRedux.LAYOUT_COLUMN_FILTER_CLEAR_ALL:
                case LayoutRedux.LAYOUT_COLUMN_FILTER_SUSPEND:
                case LayoutRedux.LAYOUT_COLUMN_FILTER_SUSPEND_ALL:
                case LayoutRedux.LAYOUT_COLUMN_FILTER_UNSUSPEND:
                case LayoutRedux.LAYOUT_COLUMN_FILTER_UNSUSPEND_ALL: {
                    let returnAction;
                    // needs to be called before 'next' so previous and next column filters are known
                    const shouldTriggerColumnFiltering = adaptable.api.filterApi.columnFilterApi.internalApi.shouldNewColumnFilterTriggerColumnFiltering(action);
                    const oldLayoutState = middlewareAPI.getState().Layout;
                    returnAction = next(action);
                    setTimeout(() => {
                        if (shouldTriggerColumnFiltering) {
                            adaptable.applyFiltering();
                        }
                    }, 50);
                    adaptable.api.eventApi.internalApi.fireColumnFilterAppliedEvent();
                    adaptable.api.eventApi.internalApi.fireLayoutChangedEvent(action.type, oldLayoutState, middlewareAPI.getState().Layout);
                    return returnAction;
                }
                /**
                 * Use Case: Grid Filter has changed
                 * Action: Apply Grid Filtering and fire associated events
                 */
                case LayoutRedux.LAYOUT_GRID_FILTER_SET:
                case LayoutRedux.LAYOUT_GRID_FILTER_CLEAR:
                case LayoutRedux.LAYOUT_GRID_FILTER_SUSPEND:
                case LayoutRedux.LAYOUT_GRID_FILTER_UNSUSPEND: {
                    let returnAction = next(action);
                    adaptable.api.eventApi.internalApi.fireGridFilterAppliedEvent();
                    adaptable.api.eventApi.internalApi.fireLayoutChangedEvent(action.type, null, middlewareAPI.getState().Layout);
                    setTimeout(() => {
                        adaptable.applyFiltering();
                    }, 5);
                    return next(returnAction);
                }
                /*******************
                 * SMART EDIT ACTIONS
                 *******************/
                /**
                 * Use Case: User wants to perform Smart Edit and we need to check if the cell selection is valid
                 * Action (1):  Get the result from the SmartEdit module
                 * If the return is an Alert:
                 * Action (2): If there is a popup open, close it and show the Alert; otherwise just set false valid selection
                 * If the return is valid:
                 * Action (2): Set the valid selection to true
                 * Action (3): Build the Preview Values (via Smart Edit module)
                 * Action (4): Set the Preview Values (this will populate the preview screen)
                 */
                case InternalRedux.SMARTEDIT_CHECK_CELL_SELECTION: {
                    let module = (adaptable.adaptableModules.get(ModuleConstants.SmartEditModuleId));
                    let state = middlewareAPI.getState();
                    let returnAction = next(action);
                    let apiReturn = module.CheckCorrectCellSelection();
                    let popup = state.Popup.ScreenPopup;
                    // this is a horrible hack and fix for a weird issue
                    // we really need to do smart edit and bulk update better
                    // but this fixes it for now
                    if (popup.ComponentName != 'LayoutPopup') {
                        if (apiReturn.Alert) {
                            // check if Smart Edit is showing as popup and then close and show error (dont want to do that if from toolbar)
                            if (popup.ComponentName == 'SmartEditPopup') {
                                // We are in SmartEditPopup so let's close it
                                adaptable.api.internalApi.hidePopupScreen();
                                // and now show the alert Popup
                                adaptable.api.alertApi.displayAdaptableAlertNotification(apiReturn.Alert);
                            }
                            middlewareAPI.dispatch(InternalRedux.SmartEditSetValidSelection(false));
                        }
                        else {
                            middlewareAPI.dispatch(InternalRedux.SmartEditSetValidSelection(true));
                            let apiPreviewReturn = module.BuildPreviewValues(state.Internal.SmartEdit.SmartEditValue, state.Internal.SmartEdit.SmartEditOperation);
                            middlewareAPI.dispatch(InternalRedux.SmartEditSetPreview(apiPreviewReturn));
                        }
                    }
                    return returnAction;
                }
                /**
                 * Use Case: User has changed a Smmart Edit property or requested a preview
                 * Action (1):  Get the new preview set from the Smart Edit module
                 * Action (2):  Set the Preview Values (this will populate the preview screen)
                 */
                case InternalRedux.SMART_EDIT_CHANGE_OPERATION:
                case InternalRedux.SMART_EDIT_CHANGE_VALUE:
                case InternalRedux.SMARTEDIT_FETCH_PREVIEW: {
                    //all our logic needs to be executed AFTER the main reducers
                    //so our state is up to date which allow us not to care about the data within each different action
                    let returnAction = next(action);
                    let module = (adaptable.adaptableModules.get(ModuleConstants.SmartEditModuleId));
                    let state = middlewareAPI.getState();
                    let apiReturn = module.BuildPreviewValues(state.Internal.SmartEdit.SmartEditValue, state.Internal.SmartEdit.SmartEditOperation);
                    middlewareAPI.dispatch(InternalRedux.SmartEditSetPreview(apiReturn));
                    return returnAction;
                }
                /**
                 * Use Case: User has clicked 'Apply' in Smart Edit popup or toolbar
                 * Action (1): Gets the values that need to be applied from the Preview Info and passes to Preview Helper (incl. whether to bypass validation)
                 * Action (2): Sets the cell values in the Grid
                 */
                case SmartEditRedux.SMART_EDIT_APPLY: {
                    const actionTyped = action;
                    let thePreview = middlewareAPI.getState().Internal.SmartEdit.SmartEditPreviewInfo;
                    let newValues = PreviewHelper.GetCellUpdateRequestsFromPreview(thePreview, actionTyped.bypassValidationWarnings);
                    adaptable.api.gridApi.setCellValues(newValues);
                    adaptable.api.internalApi.hidePopupScreen();
                    return next(action);
                }
                /*******************
                 * BULK UPDATE ACTIONS
                 *******************/
                case InternalRedux.BULK_UPDATE_CHECK_CELL_SELECTION: {
                    let module = (adaptable.adaptableModules.get(ModuleConstants.BulkUpdateModuleId));
                    let state = middlewareAPI.getState();
                    let returnAction = next(action);
                    let apiReturn = module.checkCorrectCellSelection();
                    let popup = state.Popup.ScreenPopup;
                    // this is a horrible hack and fix for a weird issue
                    // we really need to do smart edit and bulk update better
                    // but this fixes it for now
                    if (popup.ComponentName != 'LayoutPopup') {
                        if (apiReturn.Alert) {
                            // check if BulkUpdate is showing as popup
                            if (popup.ComponentName == 'BulkUpdatePopup') {
                                //We close the BulkUpdatePopup
                                adaptable.api.internalApi.hidePopupScreen();
                                //We show the Error Popup -- assume that will alwasy be an Error
                                adaptable.api.alertApi.displayAdaptableAlertNotification(apiReturn.Alert);
                            }
                        }
                        middlewareAPI.dispatch(InternalRedux.BulkUpdateSetValidSelection(apiReturn));
                    }
                    return returnAction;
                }
                // Here we have all actions that triggers a refresh of the BulkUpdatePreview
                case InternalRedux.BULK_UPDATE_CHANGE_VALUE: {
                    //all our logic needs to be executed AFTER the main reducers
                    //so our state is up to date which allow us not to care about the data within each different action
                    let returnAction = next(action);
                    let module = (adaptable.adaptableModules.get(ModuleConstants.BulkUpdateModuleId));
                    let state = middlewareAPI.getState();
                    let apiReturn = module.buildPreviewValues(state.Internal.BulkUpdate.BulkUpdateValue);
                    middlewareAPI.dispatch(InternalRedux.BulkUpdateSetPreview(apiReturn));
                    return returnAction;
                }
                case BulkUpdateRedux.BULK_UPDATE_APPLY: {
                    const actionTyped = action;
                    let thePreview = middlewareAPI.getState().Internal.BulkUpdate.BulkUpdatePreviewInfo;
                    let newValues = PreviewHelper.GetCellUpdateRequestsFromPreview(thePreview, actionTyped.bypassValidationWarnings);
                    adaptable.api.gridApi.setCellValues(newValues);
                    adaptable.api.internalApi.hidePopupScreen();
                    return next(action);
                }
                /*******************
                 * PLUS MINUS ACTIONS
                 *******************/
                /**
                 * Use Case: Plus Minus Rules have changed
                 * Action: Get Plus Minus module to know if it needs to listen to keydown
                 */
                case PlusMinusRedux.PLUS_MINUS_RULE_ADD:
                case PlusMinusRedux.PLUS_MINUS_RULE_EDIT:
                case PlusMinusRedux.PLUS_MINUS_RULE_DELETE: {
                    let returnAction = next(action);
                    let module = (adaptable.adaptableModules.get(ModuleConstants.PlusMinusModuleId));
                    if (module) {
                        module.checkListenToKeyDown();
                    }
                    return returnAction;
                }
                /**
                 * Use Case: Plus Minus has been applied
                 * Action: update the grid cells
                 */
                case PlusMinusRedux.PLUS_MINUS_APPLY: {
                    let returnAction = next(action);
                    const actionTyped = action;
                    adaptable.api.gridApi.setCellValues(actionTyped.cellUpdateRequests);
                    return returnAction;
                }
                /*******************
                 * SHORTCUT ACTIONS
                 *******************/
                /**
                 * Use Case: Shortcuts have changed
                 * Action: Get Shortcut module to know if it needs to listen to keydown
                 */
                case ShortcutRedux.SHORTCUT_ADD:
                case ShortcutRedux.SHORTCUT_EDIT:
                case ShortcutRedux.SHORTCUT_DELETE: {
                    let returnAction = next(action);
                    let module = (adaptable.adaptableModules.get(ModuleConstants.ShortcutModuleId));
                    if (module) {
                        module.checkListenToKeyDown();
                    }
                    return returnAction;
                }
                /*******************
                 * EXPORT ACTIONS
                 *******************/
                // When selected report is undefined, clear the format
                case ExportRedux.REPORT_SELECT: {
                    const actionTyped = action;
                    let returnAction = next(action);
                    const selectedReport = actionTyped.SelectedReport;
                    if (selectedReport == undefined) {
                        adaptable.api.exportApi.clearFormat();
                    }
                    return returnAction;
                }
                // When deleting a report, if it is currently selected clear it
                case ExportRedux.REPORT_DELETE: {
                    const actionTyped = action;
                    let report = actionTyped.report;
                    let currentReport = middlewareAPI.getState().Export.CurrentReport;
                    if (report && report.Name == currentReport) {
                        adaptable.api.exportApi.clearReport();
                    }
                    return next(action);
                }
                /*******************
                 * CHARTING ACTIONS
                 *******************/
                case ChartingRedux.CHARTING_DELETE_EXTERNAL_CHART: {
                    const returnAction = next(action);
                    adaptable.api.chartingApi.internalApi.onDeleteExternalChart(returnAction.chartDefinition);
                    return returnAction;
                }
                /*******************
                 * TEAM SHARING ACTIONS
                 *******************/
                case TeamSharingRedux.TEAMSHARING_GET: {
                    let returnAction = next(action);
                    adaptable.api.teamSharingApi
                        .loadSharedEntities()
                        .then((sharedEntities) => {
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingSet(sharedEntities));
                    })
                        .catch((error) => {
                        adaptable.api.logError('TeamSharing get error : ' + error.message);
                        const alert = {
                            alertType: 'generic',
                            header: 'Team Sharing',
                            message: "Couldn't get shared items: " + error.message,
                            alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Error'),
                        };
                        adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                    });
                    return returnAction;
                }
                case TeamSharingRedux.TEAMSHARING_SHARE_CUSTOM: {
                    const actionTyped = action;
                    let returnAction = next(action);
                    const { Entity, Configuration } = actionTyped;
                    let shareSuccessful;
                    // load latest state from TeamSharing server
                    adaptable.api.teamSharingApi
                        .loadSharedEntities()
                        .then((sharedEntities) => {
                        const customSharedEntity = adaptable.api.internalApi
                            .getTeamSharingService()
                            .buildCustomSharedEntity(Entity, Configuration);
                        let newCustomShare = true;
                        const updatedSharedEntities = sharedEntities.map((entity) => {
                            // if it has the same UUID, override it
                            if (isCustomSharedEntity(entity) && entity.Uuid === customSharedEntity.Uuid) {
                                newCustomShare = false;
                                return {
                                    ...customSharedEntity,
                                    ChangedAt: Date.now(),
                                    ChangedBy: adaptable.api.optionsApi.getUserName(),
                                };
                            }
                            return entity;
                        });
                        if (newCustomShare) {
                            updatedSharedEntities.push(customSharedEntity);
                        }
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingSet(updatedSharedEntities));
                        // update the server state
                        shareSuccessful = true;
                        return adaptable.api.teamSharingApi.persistSharedEntities(updatedSharedEntities);
                    })
                        .then(() => {
                        if (shareSuccessful) {
                            adaptable.api.logInfo(`Custom Shared Object '${Configuration.Name}' Shared Successfully`);
                        }
                    })
                        .catch((error) => {
                        adaptable.api.logError('TeamSharing share error : ' + error.message, actionTyped.Entity);
                    });
                    return returnAction;
                }
                // share the given AdaptableObject:
                // - if it has dependencies, share them as well
                // - if it's active, ensure that there is not already one present
                // - if it's active, all the dependencies will be active as well
                case TeamSharingRedux.TEAMSHARING_SHARE: {
                    const actionTyped = action;
                    let returnAction = next(action);
                    const { Entity, Configuration, Module } = actionTyped;
                    let shareSuccessful;
                    // load latest state from TeamSharing server
                    adaptable.api.teamSharingApi
                        .loadSharedEntities()
                        .then((sharedEntities) => {
                        // check if there is not already one active SharedEntity for this object
                        const existingActiveSharedEntity = Configuration.type === 'Active' &&
                            middlewareAPI.getState().TeamSharing.ActiveSharedEntityMap[Entity.Uuid];
                        if (existingActiveSharedEntity) {
                            const alert = {
                                alertType: 'generic',
                                header: 'Team Sharing',
                                message: `Couldn't share ${adaptable.ModuleService.getModuleFriendlyName(Module)}, there is already an existing active share for it: ${existingActiveSharedEntity.Description}`,
                                alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Error'),
                            };
                            adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                            shareSuccessful = false;
                            return;
                        }
                        // build the new shared entities incl. all their dependencies
                        const newSharedEntityInclDependencies = adaptable.api.internalApi
                            .getTeamSharingService()
                            .buildSharedEntityWithDependencies(Entity, Module, Configuration);
                        sharedEntities.push(...newSharedEntityInclDependencies);
                        // update the local state
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingLinkItem(...newSharedEntityInclDependencies.filter((sharedEntity) => sharedEntity.Type === 'Active')));
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingSet(sharedEntities));
                        // update the server state
                        shareSuccessful = true;
                        return adaptable.api.teamSharingApi.persistSharedEntities(sharedEntities);
                    })
                        .then(() => {
                        if (shareSuccessful) {
                            const alert = {
                                alertType: 'generic',
                                header: 'Team Sharing',
                                message: `${adaptable.ModuleService.getModuleFriendlyName(Module)} Shared Successfully`,
                                alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Success'),
                            };
                            adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                        }
                    })
                        .catch((error) => {
                        adaptable.api.logError('TeamSharing share error : ' + error.message, actionTyped.Entity);
                        const alert = {
                            alertType: 'generic',
                            header: 'Team Sharing',
                            message: "Couldn't share item: " + error.message,
                            alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Error'),
                        };
                        adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                    });
                    return returnAction;
                }
                // remove the given shared entity:
                // - ensure that no other shared entities depend on it
                case TeamSharingRedux.TEAMSHARING_REMOVE_ITEM: {
                    let returnAction = next(action);
                    const actionTyped = action;
                    const removedEntityId = actionTyped.Uuid;
                    // load latest state from TeamSharing server
                    adaptable.api.teamSharingApi
                        .loadSharedEntities()
                        .then((sharedEntities) => {
                        // ensure that no other SharedDependencies reference the to-be-deleted entity
                        const adaptableSharedEntities = sharedEntities.filter(isAdaptableSharedEntity);
                        const sharedEntityDependants = adaptable.api.internalApi
                            .getTeamSharingService()
                            .getSharedEntityDependants(removedEntityId, adaptableSharedEntities);
                        if (sharedEntityDependants.length) {
                            const alert = {
                                alertType: 'generic',
                                header: `Cannot remove the shared item as it is referenced in:`,
                                message: sharedEntityDependants
                                    .map((sharedEntity) => `${adaptable.ModuleService.getModuleFriendlyName(sharedEntity.Module)} ${sharedEntity.Description}`)
                                    .join('\n'),
                                alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Warning'),
                            };
                            adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                            return;
                        }
                        // remove the deleted SharedEntity from local state
                        const newSharedEntities = sharedEntities.filter((s) => s.Uuid !== removedEntityId);
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingSet(newSharedEntities));
                        // update TeamSharing server
                        return adaptable.api.teamSharingApi.persistSharedEntities(newSharedEntities);
                    })
                        .catch((error) => {
                        adaptable.api.logError('TeamSharing remove error : ' + error.message);
                        const alert = {
                            alertType: 'generic',
                            header: 'Team Sharing',
                            message: "Couldn't remove item: " + error.message,
                            alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Error'),
                        };
                        adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                    });
                    return returnAction;
                }
                // Import 1. step
                // prepare import of the given shared entity (the actual import is done in 'TEAMSHARING_PROCESS_IMPORT'):
                // - all dependencies will be also included in the import bundle
                // - if there is at least one local AdaptableObject which will be overwritten, display a confirmation pop
                case TeamSharingRedux.TEAMSHARING_IMPORT_ITEM: {
                    let returnAction = next(action);
                    const actionTyped = action;
                    const [importStepActions, needsOverwriteConfirmation] = adaptable.api.internalApi
                        .getTeamSharingService()
                        .buildSharedEntityImportActions(actionTyped.SharedEntity);
                    const processImportAction = TeamSharingRedux.TeamSharingProcessImport(importStepActions);
                    if (needsOverwriteConfirmation &&
                        !adaptable.api.optionsApi.getTeamSharingOptions().suppressOverrideConfigWarning) {
                        let confirmation = {
                            CancelButtonText: 'Cancel Import',
                            Header: 'Overwriting Existing Object',
                            Msg: 'This import will overwrite an existing object in your State. Do you wish to continue?',
                            ConfirmButtonText: 'Import',
                            CancelAction: null,
                            ConfirmAction: processImportAction,
                            ShowInputBox: false,
                            MessageType: 'Warning',
                        };
                        adaptable.api.internalApi.showPopupConfirmation(confirmation);
                    }
                    else {
                        middlewareAPI.dispatch(processImportAction);
                    }
                    return returnAction;
                }
                // Import 2. step
                // import the actual AdaptableObjects (either create or update them)
                case TeamSharingRedux.TEAMSHARING_PROCESS_IMPORT: {
                    let returnAction = next(action);
                    const actionTyped = action;
                    const { ImportSteps } = actionTyped;
                    const activeSharedEntities = [];
                    // iterate backwards to import the dependencies first
                    // it should work also the other way around, but nevertheless...
                    for (let i = ImportSteps.length - 1; i >= 0; i--) {
                        const { sharedEntity, importAction } = ImportSteps[i];
                        if (importAction) {
                            middlewareAPI.dispatch(importAction);
                            if (sharedEntity.Type === 'Active') {
                                activeSharedEntities.push(sharedEntity);
                            }
                        }
                        else {
                            adaptable.api.logError('Team Sharing Import Error: Unknown item type', sharedEntity);
                        }
                    }
                    if (activeSharedEntities.length) {
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingLinkItem(...activeSharedEntities));
                    }
                    // mark the import process as ended after (we assume that) the import is finished
                    adaptable.api.teamSharingApi.internalApi
                        .waitForTeamSharingImportEnd()
                        .then(() => middlewareAPI.dispatch(TeamSharingRedux.TeamSharingCommitImport()))
                        .catch(() => middlewareAPI.dispatch(TeamSharingRedux.TeamSharingCommitImport()));
                    const alert = {
                        alertType: 'generic',
                        header: 'Team Sharing',
                        message: `Item Successfully Imported`,
                        alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Success'),
                    };
                    adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                    return returnAction;
                }
                // update the active SharedEntity with the latest (updated) state of the local AdaptableObject
                // - if the remote server already has a newer version of it, the update is skipped
                case TeamSharingRedux.TEAMSHARING_UPDATE_ITEM: {
                    const returnAction = next(action);
                    const actionTyped = action;
                    if (middlewareAPI.getState().TeamSharing.importProcessInProgress) {
                        // we are in the middle of an import process, we don't have to react to these object changes
                        return returnAction;
                    }
                    const { ChangedAdaptableObject, UserName } = actionTyped;
                    adaptable.api.teamSharingApi
                        .loadSharedEntities()
                        .then((sharedEntities) => {
                        const adaptableSharedEntities = sharedEntities.filter(isAdaptableSharedEntity);
                        // check if remote server has a newer revision
                        const [localRevision, remoteRevision] = adaptable.api.internalApi
                            .getTeamSharingService()
                            .getSharedEntityLocalAndRemoteRevisions(ChangedAdaptableObject.Uuid, adaptableSharedEntities);
                        if (remoteRevision > localRevision) {
                            // if remote has a newer version, we will NOT overwrite it
                            return;
                        }
                        const [updatedSharedEntities, newActiveEntities] = adaptable.api.internalApi
                            .getTeamSharingService()
                            .updateActiveSharedEntity(ChangedAdaptableObject, UserName, adaptableSharedEntities);
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingLinkItem(...newActiveEntities));
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingSet(updatedSharedEntities));
                        return adaptable.api.teamSharingApi.persistSharedEntities(updatedSharedEntities);
                    })
                        .catch((error) => {
                        adaptable.api.logError('TeamSharing update active item error : ' + error.message);
                        const alert = {
                            alertType: 'generic',
                            header: 'Team Sharing',
                            message: "Couldn't update active item: " + error.message,
                            alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Error'),
                        };
                        adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                    });
                    return returnAction;
                }
                case TeamSharingRedux.TEAMSHARING_CHECK_FOR_UPDATES: {
                    const returnAction = next(action);
                    adaptable.api.teamSharingApi
                        .loadSharedEntities()
                        .then((sharedEntities) => {
                        // update local state
                        middlewareAPI.dispatch(TeamSharingRedux.TeamSharingSet(sharedEntities));
                        adaptable.api.internalApi.getTeamSharingService().showUpdateNotifications();
                    })
                        .catch((error) => {
                        adaptable.api.logError('TeamSharing update error : ' + error.message);
                        const alert = {
                            alertType: 'generic',
                            header: 'Team Sharing',
                            message: "Couldn't check for updates: " + error.message,
                            alertDefinition: ObjectFactory.CreateInternalAlertDefinitionForMessages('Error'),
                        };
                        adaptable.api.alertApi.displayAdaptableAlertNotification(alert);
                    });
                    return returnAction;
                }
                /*******************
                 * POPUP (INTERNAL) ACTIONS
                 *******************/
                case PopupRedux.POPUP_SHOW_ALERT: {
                    showToast({
                        api: adaptable.api,
                        adaptableAlert: action.alert,
                    });
                    return next(action);
                }
                case PopupRedux.POPUP_CONFIRM_PROMPT: {
                    let promptConfirmationAction = middlewareAPI.getState().Popup.PromptPopup.ConfirmAction;
                    if (promptConfirmationAction) {
                        let inputText = action.InputText;
                        promptConfirmationAction.InputText = inputText;
                        middlewareAPI.dispatch(promptConfirmationAction);
                    }
                    return next(action);
                }
                case PopupRedux.POPUP_CONFIRM_CONFIRMATION: {
                    let confirmationAction = middlewareAPI.getState().Popup.ConfirmationPopup.ConfirmAction;
                    if (confirmationAction) {
                        middlewareAPI.dispatch(confirmationAction);
                    }
                    return next(action);
                }
                case PopupRedux.POPUP_CANCEL_CONFIRMATION: {
                    let cancelAction = middlewareAPI.getState().Popup.ConfirmationPopup.CancelAction;
                    if (cancelAction) {
                        middlewareAPI.dispatch(cancelAction);
                    }
                    return next(action);
                }
                /*******************
                 * MANAGING STATE ACTIONS
                 *******************/
                case INIT_STATE: {
                    let returnAction = next(action);
                    if (adaptable.isReady) {
                        // we need this here for when the state key is changed
                        // since we need the transient state to be restored in the same
                        // way as when Adaptable is initialised.
                        adaptable.ModuleService.createModuleUIItems();
                    }
                    return returnAction;
                }
                default: {
                    return next(action);
                }
            }
        };
    };
});