UNPKG

@adaptabletools/adaptable

Version:

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

919 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 returnAct