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