@adaptabletools/adaptable
Version:
Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
972 lines (969 loc) • 152 kB
JavaScript
import throttle from 'lodash/throttle';
import debounce from 'lodash/debounce';
import { createGrid, LocalEventService, } from 'ag-grid-enterprise';
import { AdaptableLogger } from './AdaptableLogger';
import { PrimaryKeyDocsLink } from '../Utilities/Constants/DocumentationLinkConstants';
import StringExtensions from '../Utilities/Extensions/StringExtensions';
import Emitter from '../Utilities/Emitter';
import { applyDefaultAdaptableOptions } from '../AdaptableOptions/DefaultAdaptableOptions';
import { AgGridAdapter } from './AgGridAdapter';
import * as GeneralConstants from '../Utilities/Constants/GeneralConstants';
import { AUTOGENERATED_PK_COLUMN, ERROR_LAYOUT, GROUP_PATH_SEPARATOR, HALF_SECOND, QUARTER_SECOND, } from '../Utilities/Constants/GeneralConstants';
import { DataService } from '../Utilities/Services/DataService';
import { AdaptableStore } from '../Redux/Store/AdaptableStore';
import { AdaptableApiImpl } from '../Api/Implementation/AdaptableApiImpl';
import { Fdc3Service } from '../Utilities/Services/Fdc3Service';
import { AnnotationsService } from '../Utilities/Services/AnnotationsService';
import { ChartingService } from '../Utilities/Services/ChartingService';
import { ThemeService } from '../Utilities/Services/ThemeService';
import { ValidationService } from '../Utilities/Services/ValidationService';
import { ModuleService } from '../Utilities/Services/ModuleService';
import { CalculatedColumnExpressionService } from '../Utilities/Services/CalculatedColumnExpressionService';
import { QueryLanguageService } from '../Utilities/Services/QueryLanguageService';
import { AlertService } from '../Utilities/Services/AlertService';
import { TeamSharingService } from '../Utilities/Services/TeamSharingService';
import { MetamodelService } from '../Utilities/Services/MetamodelService';
import { LicenseService } from '../Utilities/Services/LicenseService';
import { ALL_TOOL_PANELS } from '../AdaptableState/Common/Types';
import * as ModuleConstants from '../Utilities/Constants/ModuleConstants';
import { GridFilterModuleId } from '../Utilities/Constants/ModuleConstants';
import { DashboardModule } from '../Strategy/DashboardModule';
import { AlertModule } from '../Strategy/AlertModule';
import { FlashingCellModule } from '../Strategy/FlashingCellModule';
import { BulkUpdateModule } from '../Strategy/BulkUpdateModule';
import { CalculatedColumnModule } from '../Strategy/CalculatedColumnModule';
import { CellSummaryModule } from '../Strategy/CellSummaryModule';
import { CustomSortModule } from '../Strategy/CustomSortModule';
import { DataChangeHistoryModule } from '../Strategy/DataChangeHistoryModule';
import { DataImportModule } from '../Strategy/DataImportModule';
import { DataSetModule } from '../Strategy/DataSetModule';
import { ExportModule } from '../Strategy/ExportModule';
import { ColumnFilterModule } from '../Strategy/ColumnFilterModule';
import { FormatColumnModule } from '../Strategy/FormatColumnModule';
import { FreeTextColumnModule } from '../Strategy/FreeTextColumnModule';
import { LayoutModule } from '../Strategy/LayoutModule';
import { PlusMinusModule } from '../Strategy/PlusMinusModule';
import { QuickSearchModule } from '../Strategy/QuickSearchModule';
import { ScheduleModule } from '../Strategy/ScheduleModule';
import { SmartEditModule } from '../Strategy/SmartEditModule';
import { ShortcutModule } from '../Strategy/ShortcutModule';
import { StateManagementModule } from '../Strategy/StateManagementModule';
import { TeamSharingModule } from '../Strategy/TeamSharingModule';
import { ToolPanelModule } from '../Strategy/ToolPanelModule';
import { SystemStatusModule } from '../Strategy/SystemStatusModule';
import { ThemeModule } from '../Strategy/ThemeModule';
import { GridInfoModule } from '../Strategy/GridInfoModule';
import { ColumnInfoModule } from '../Strategy/ColumnInfoModule';
import { SettingsPanelModule } from '../Strategy/SettingsPanelModule';
import { StatusBarModule } from '../Strategy/StatusBarModule';
import { ChartingModule } from '../Strategy/ChartingModule';
import { NoteModule } from '../Strategy/NoteModule';
import { StyledColumnModule } from '../Strategy/StyledColumnModule';
import { Fdc3Module } from '../Strategy/Fdc3Module';
import { GridFilterModule } from '../Strategy/GridFilterModule';
import { NamedQueryModule } from '../Strategy/NamedQueryModule';
import { CommentModule } from '../Strategy/CommentModule';
import { Helper } from '../Utilities/Helpers/Helper';
import { createUuid } from '../components/utils/uuid';
import UIHelper from '../View/UIHelper';
import { getAdaptableToolPanelAgGridComponent } from '../View/Components/ToolPanel/AdaptableToolPanel';
import { ADAPTABLE_STATUS_PANEL } from '../AdaptableState/StatusBarState';
import { AdaptableStatusBar } from '../View/StatusBar/AdaptableStatusBar';
import ArrayExtensions from '../Utilities/Extensions/ArrayExtensions';
import { AgGridMenuAdapter } from './AgGridMenuAdapter';
import { AdaptableApp } from '../View/AdaptableView';
import { renderReactRoot as defaultRenderReactRoot } from '../renderReactRoot';
import { AgGridOptionsService } from './AgGridOptionsService';
import { parseDateValue } from '../Utilities/Helpers/DateHelper';
import { AgGridColumnAdapter } from './AgGridColumnAdapter';
import getScrollbarSize from '../Utilities/getScrollbarSize';
import { WEIGHTED_AVERAGE_AGG_FN_NAME, } from '../AdaptableState/Common/AggregationColumns';
import { RowFormService } from '../Utilities/Services/RowFormService';
import { FilterOnDataChangeOptions } from '../AdaptableState/Common/Enums';
import { ADAPTABLE_PUBLISH_TIMESTAMP } from '../EnvVars';
import { AdaptableUpgradeHelper } from '../migration/AdaptableUpgradeHelper';
import { ensureLoadingScreenPortalElement } from '../components/Modal';
import { AdaptableLoadingScreen } from '../View/Components/Popups/AdaptableLoadingScreen';
import { createElement } from 'react';
import { createAgStatusPanelComponent } from '../Utilities/createAgStatusPanelComponent';
import { weightedAverage } from '../Utilities/weightedAverage';
import { ROW_SUMMARY_ROW_ID } from '../AdaptableState/Common/RowSummary';
import { FlashingCellService } from '../Utilities/Services/FlashingCellService';
import { AgGridExportAdapter } from './AgGridExportAdapter';
import { checkForDuplicateColumns, isPivotLayout, layoutModelToLayoutState, layoutStateToLayoutModel, normalizeLayout, } from '../Api/Implementation/LayoutHelpers';
import { LayoutManager } from '../layout-manager/src';
import { isPivotLayoutModel } from '../layout-manager/src/isPivotLayoutModel';
import { ACTION_COLUMN_TYPE, CALCULATED_COLUMN_TYPE, FDC3_COLUMN_TYPE, FREE_TEXT_COLUMN_TYPE, } from '../AdaptableState/Common/AdaptableColumn';
import { agGridDataTypeDefinitions } from './agGridDataTypeDefinitions';
import { AgGridThemeAdapter } from './AgGridThemeAdapter';
import { mapOldTypeToDataType } from '../migration/VersionUpgrade20';
const LocalEventService_Prototype = LocalEventService.prototype;
const LocalEventService_dispatchEvent = LocalEventService_Prototype.dispatchEvent;
LocalEventService_Prototype.dispatchEvent = function (event) {
const agGridApi = event.api;
if (agGridApi?.isDestroyed()) {
// do nothing if AG Grid was destroyed in the meantime
return;
}
LocalEventService_dispatchEvent.apply(this, arguments);
if (event.type === 'cellChanged' || event.type === 'dataChanged') {
const eventRowNode = event.node;
const extractGridApiFromRowNode = (rowNode) => {
const rowNodeApi = rowNode?.beans?.gridApi;
if (!rowNodeApi) {
AdaptableLogger.consoleErrorBase(`No GridAPI found in passed RowNode, this should never happen!`, rowNode);
}
return rowNodeApi;
};
// we don't know from which instance of aggrid this is coming,
// as this fn is shared by all instances
if (eventRowNode) {
AdaptableAgGrid.forEachAdaptable((adaptable) => {
if (extractGridApiFromRowNode(eventRowNode) !== adaptable.agGridAdapter?.getAgGridApi(true)) {
// the event is coming from another aggrid instance
// so IGNORE IT
return;
}
if (adaptable.isDestroyed) {
// do nothing if adaptable is destroyed (this is a rare case and happens when Adaptable is quickly destroyed and recreated)
return;
}
// we're on the correct instance, so do this
//@ts-ignore
const fn = adaptable.rowListeners ? adaptable.rowListeners[event.type] : null;
if (fn) {
fn(event);
}
});
}
}
};
const adaptableInstances = {};
const publishTimestamp = Number(ADAPTABLE_PUBLISH_TIMESTAMP);
export class AdaptableAgGrid {
constructor(config) {
this.filteredOutPrimaryKeys = new Set();
this.columnMinMaxValuesCache = {};
this.renderReactRoot = (node, container) => defaultRenderReactRoot(node, container);
/**
* Temporary, these are MIGRATION technical debts, and should be removed as soon as possible
*/
this.adaptableStatusPanelKeys = [];
// only for our private / internal events used within Adaptable
// public events are emitted through the EventApi
this._emit = (eventName, data) => {
if (this.emitter) {
return this.emitter.emit(eventName, data);
}
};
this._emitSync = (eventName, data) => {
if (this.emitter) {
return this.emitter.emitSync(eventName, data);
}
};
this._on = (eventName, callback) => {
if (!this.emitter) {
return () => { };
}
return this.emitter.on(eventName, callback);
};
this._onIncludeFired = (eventName, callback) => {
if (!this.emitter) {
return () => { };
}
return this.emitter.onIncludeFired(eventName, callback);
};
this.lifecycleState = 'initial';
this.emitter = new Emitter();
this.agGridOptionsService = new AgGridOptionsService(this);
this.agGridAdapter = new AgGridAdapter(this, config);
this.agGridMenuAdapter = new AgGridMenuAdapter(this);
this.agGridColumnAdapter = new AgGridColumnAdapter(this);
this.agGridExportAdapter = new AgGridExportAdapter(this);
this.agGridThemeAdapter = new AgGridThemeAdapter(this);
this.DataService = new DataService(this);
}
static forEachAdaptable(fn) {
Object.keys(adaptableInstances).forEach((key) => {
fn(adaptableInstances[key]);
});
}
static collectInstance(adaptable, adaptableId) {
adaptable._id = adaptableId;
adaptableInstances[adaptable._id] = adaptable;
}
static dismissInstance(adaptable) {
delete adaptableInstances[adaptable._id];
}
get isAgGridInitialising() {
return this.lifecycleState === 'initAgGrid';
}
get isReady() {
return this.lifecycleState === 'ready';
}
get isAvailable() {
return this.lifecycleState === 'available' || this.lifecycleState === 'ready';
}
get isDestroyed() {
return this.lifecycleState === 'preDestroyed';
}
/**
* Internal initializer for Adaptable, directly called by the React and Angular Adaptable wrappers
* @private
*/
static async _initInternal(config) {
let promise = null;
if (Array.isArray(config.adaptableOptions.plugins)) {
const agGridOptions = {
gridOptions: config.gridOptions,
modules: config.modules,
};
for (let plugin of config.adaptableOptions.plugins) {
promise =
promise && promise.then
? promise.then(() => {
return plugin.beforeInit(config.adaptableOptions, agGridOptions);
})
: plugin.beforeInit(config.adaptableOptions, agGridOptions);
}
// if gridOptions changed, we need to update the runtimeConfig
if (agGridOptions.gridOptions !== config.gridOptions) {
// This allows plugins to modify
// FIXME AFL MIG: clarify if this is still needed (for NoCode Plugin?)
// it looks like a code smell, ideally we should get rid of it
config.gridOptions = agGridOptions.gridOptions;
}
}
const doInit = (adaptableInstance) => {
return adaptableInstance._initAdaptableAgGrid(config).then((api) => {
if (Array.isArray(config.adaptableOptions.plugins)) {
config.adaptableOptions.plugins.forEach((plugin) => {
plugin.afterInit(adaptableInstance);
});
}
return api;
});
};
if (promise && promise.then) {
return promise.then(() => {
const adaptableInstance = new AdaptableAgGrid({
getAgGridColumnApiModuleReference: config.getAgGridColumnApiModuleReference,
});
return doInit(adaptableInstance);
});
}
else {
const adaptableInstance = new AdaptableAgGrid({
getAgGridColumnApiModuleReference: config.getAgGridColumnApiModuleReference,
});
return doInit(adaptableInstance);
}
}
async _initAdaptableAgGrid(config) {
// Phase 1: Preprocess Adaptable Options
this._isDetailGrid = config.isDetailGrid === true;
this._isDetailGridForIndex = config.isDetailGridForRowIndex;
this.lifecycleState = 'preprocessOptions';
this._rawAdaptableOptions = config.adaptableOptions;
if (StringExtensions.IsNullOrEmptyOrWhiteSpace(this._rawAdaptableOptions.adaptableId)) {
this._rawAdaptableOptions.adaptableId = `adaptable_id_${Date.now()}`;
}
this.logger = this.logger ?? new AdaptableLogger(this._rawAdaptableOptions.adaptableId);
const perfInitAdaptableAgGrid = this.logger.beginPerf(`Adaptable._initAdaptableAgGrid()`);
AdaptableAgGrid.collectInstance(this, this._rawAdaptableOptions.adaptableId);
this.variant = config.variant;
this.initWithLazyData =
config.gridOptions.rowData == undefined || config.gridOptions.rowData.length === 0;
this.hasAutogeneratedPrimaryKey = !!this._rawAdaptableOptions.autogeneratePrimaryKey;
this.adaptableOptions = applyDefaultAdaptableOptions(this._rawAdaptableOptions);
this.adaptableOptions = this.normalizeAdaptableOptions(this.adaptableOptions);
const { showLoadingScreen, loadingScreenDelay, loadingScreenText, loadingScreenTitle } = this.adaptableOptions.userInterfaceOptions.loadingScreenOptions;
if (showLoadingScreen) {
this.logger.info(`Show Loading Screen`);
// it's important to use ensureLoadingScreenPortalElement
// and not ensurePortalElement, because multiple adaptable instances share the same portal element
// so when displaying the second one, the react root associated to the portal element
// seems to be somewhat shared via the html element, so the portal element of the first one is destroyed
// resulting in the settings popup not being displayed anymore
const portalElement = ensureLoadingScreenPortalElement();
if (portalElement) {
this.unmountLoadingScreen = this.renderReactRoot(createElement(AdaptableLoadingScreen, {
showLoadingScreen,
loadingScreenDelay,
loadingScreenText,
loadingScreenTitle,
}), portalElement);
}
else {
this.logger.consoleError(`Adaptable failed to show the loading screen!`);
}
}
this.forPlugins((plugin) => plugin.afterInitOptions(this, this.adaptableOptions));
this.api = new AdaptableApiImpl(this);
this.forPlugins((plugin) => plugin.afterInitApi(this, this.api));
this.lifecycleState = 'initAdaptableState';
this.initServices();
this.forPlugins((plugin) => plugin.afterInitServices(this));
this.adaptableModules = this.initModules();
this.forPlugins((plugin) => plugin.afterInitModules(this, this.adaptableModules));
const perfLoadStore = this.logger.beginPerf(`loadStore()`);
this.adaptableStore = this.initAdaptableStore();
this.forPlugins((plugin) => plugin.afterInitStore(this));
await this.adaptableStore.loadStore({
adaptable: this,
adaptableStateKey: this.adaptableOptions.adaptableStateKey,
/**
* This method is called after the store is loaded;
* it allows to modify the state before it is used by the application
* e.g. migrating deprecated state, etc.
*/
postLoadHook: (state) => {
if (this.adaptableOptions.stateOptions.autoMigrateState) {
this.api.logError;
const config = {
// version 16 actually includes all versions up until 16
fromVersion: 16,
logger: this.logger,
};
state = AdaptableUpgradeHelper.migrateAdaptableState(state, config);
}
state = this.normalizeAdaptableState(state, config.gridOptions);
return state;
},
});
perfLoadStore.end();
// just in case Adaptable was destroyed while loading the store (which is an async operation)
if (this.isDestroyed) {
this.midwayDestroy();
return Promise.reject('Adaptable was destroyed while loading the store.');
// FIXME AFL MIG: is this enough?! talk with the team
}
this.forPlugins((plugin) => plugin.afterInitialStateLoaded(this));
// do this now so it sets module entitlements
this.api.entitlementApi.internalApi.setModulesEntitlements();
/**
* At this point it's mandatory to have the ALL the Adaptable blocks initialized:
* Store, APIs, Services, Modules
*/
this.lifecycleState = 'setupAgGrid';
const gridOptions = config.gridOptions;
// Needed here because special column defs are required for deriving the adaptable column state
const columnDefs = this.agGridAdapter.getColumnDefinitionsInclSpecialColumns(gridOptions.columnDefs || []);
gridOptions.columnDefs = columnDefs;
this.setInitialGridOptions(gridOptions, config.variant);
const { gridState: initialGridState, layoutModel } = this.mapAdaptableStateToAgGridState(this.adaptableStore.TheStore.getState(), gridOptions.columnDefs, { isTree: !!gridOptions.treeData });
gridOptions.initialState = initialGridState;
if (layoutModel) {
if (isPivotLayoutModel(layoutModel)) {
gridOptions.pivotDefaultExpanded = layoutModel.PivotExpandLevel;
}
else {
gridOptions.groupDisplayType =
layoutModel.RowGroupDisplayType === 'multi' ? 'multipleColumns' : 'singleColumn';
}
}
this.lifecycleState = 'initAgGrid';
this.agGridAdapter.initialGridOptions = gridOptions;
const perfInitAgGrid = this.logger.beginPerf(`initAgGrid()`);
// AG Grid evaluates early on the floatingFilter params, so we need to "suppress" the floating filter temporarily
// we will reset it once Adaptable is ready
this.agGridColumnAdapter.setupColumnFloatingFilterTemporarily(gridOptions);
this.validateColumnDefTypes(gridOptions.columnDefs);
const agGridApi = await this.initializeAgGrid(gridOptions, config.modules, config.renderAgGridFrameworkComponent);
if (agGridApi === false) {
this.midwayDestroy();
this.logger.consoleError(`Adaptable failed to initialize AG Grid!`);
return Promise.reject('Adaptable failed to initialize AG Grid!');
}
this.layoutManager = new LayoutManager({
gridApi: agGridApi,
debugId: this.adaptableOptions.adaptableId,
});
this.layoutManager.silentSetCurrentLayout(layoutModel, {
normalize: true,
});
// this shouldn't be needed
// but AG Grid has a bug, and in pivot layout,
// even if we provide an initial AG Grid state with
// the aggregations in the correct order,
// they will end up in the wrong order
// so we need to force the layout to be applied again
if (isPivotLayoutModel(layoutModel)) {
this.layoutManager.setLayout(layoutModel, {
force: true,
});
}
this.layoutManager.onChange((layoutModel) => {
const currentLayout = this.api.layoutApi.getCurrentLayout();
const newLayoutObject = layoutModelToLayoutState(layoutModel, currentLayout);
this.onLayoutChange(newLayoutObject);
});
this.layoutManager.onColumnDefsChanged(() => {
this.updateColumnModelAndRefreshGrid();
});
this.logger.info(`Hide Loading Screen`);
this.unmountLoadingScreen?.();
perfInitAgGrid.end();
// we need to intercept several AG Grid Api methods and trigger Adaptale state changes
this.agGridAdapter.setAgGridApi(agGridApi);
this.agGridAdapter.monkeyPatchingGridOptionsUpdates(agGridApi);
this.lifecycleState = 'agGridReady';
this.logger.info(`Registered AG Grid modules: `, this.agGridAdapter.getAgGridRegisteredModuleNames().sort());
/**
* At this point AG Grid is initialized!
*/
this.deriveAdaptableColumnStateFromAgGrid();
this.agGridColumnAdapter.setupColumns();
// we need this because we need the internal Column state to be ready before doing any extra business logic
this.lifecycleState = 'available';
this.api.themeApi.applyCurrentTheme();
this.validatePrimaryKey();
// TODO AFL MIG: we could just patch the defautl Layout on init? instead
this.checkShouldClearExistingFiltersOrSearches();
this.applyFiltering();
this.addGridEventListeners();
this.temporaryAdaptableStateUpdates();
this.redrawBody();
this.refreshHeader();
const currentLayout = this.api.layoutApi.getCurrentLayout();
checkForDuplicateColumns(currentLayout);
this.layoutManager.silentSetCurrentLayout(layoutStateToLayoutModel(currentLayout));
if (isPivotLayout(currentLayout)) {
// this is very very strange!
// for some projects, if the initial layout is pivot, the columnDefs of the pivot resutl columns are NOT derived correctly from the main colDefs
// doing the following line fixes the issue because it foces the pivot columns to be created again
// this proj works without the hack: /tests/pages/format-column/initial-pivot-layout.page.tsx
// but this proj needs the hack: /tests/pages/format-column/initial-pivot-layout-docs.page.tsx
this.agGridAdapter.setGridOption('pivotMode', false);
this.agGridAdapter.setGridOption('pivotMode', true);
// also quick search is not working initially, although the setupColumns is called correctly
// so we need to do this to make it work
// see test /tests/pages/quick-search/pivot-search.spec.ts
this.updateColumnModelAndRefreshGrid();
}
this.layoutManager.applyRowGroupValues(layoutStateToLayoutModel(currentLayout).RowGroupValues);
this.autoSizeLayoutIfNeeded();
this.ModuleService.createModuleUIItems();
const adaptableContainerElem = this.getAdaptableContainerElement();
if (adaptableContainerElem != null) {
adaptableContainerElem.innerHTML = '';
if (this.variant === 'react') {
/**
* #no_additional_react_root
* This is only used for the React variant
* Where we don't want to create a new React render tree here
* by rendering it as a React root, but instead we want to
* render it as is in the React tree of our AdaptableReact component
*/
this._PRIVATE_adaptableJSXElement = AdaptableApp({ Adaptable: this });
}
else {
this.unmountReactRoot = this.renderReactRoot(AdaptableApp({ Adaptable: this }), adaptableContainerElem);
}
}
this.lifecycleState = 'ready';
this.forPlugins((plugin) => plugin.onAdaptableReady(this, this.adaptableOptions));
setTimeout(() => {
// without the setTimeout, calling autoSizeAllColumns immediately in the onAdaptableReady
// does not work. (I prefer setTimeout to rAF, as raf is not running when you switch tabs)
//
// it also makes it possible to listen to CALCULATED_COLUMN_READY, DASHBOARD_READY, etc.
// in onAdaptableReady - without this those event listeners are not triggered
this.api?.eventApi?.emit('AdaptableReady', {
adaptableApi: this.api,
agGridApi: this.agGridAdapter.getAgGridApi(),
});
});
perfInitAdaptableAgGrid.end();
return Promise.resolve(this.api);
}
midwayDestroy() {
this.destroy({
destroyAgGrid: false,
unmount: false,
});
}
normalizeAdaptableState(state, agGridOptions) {
state = this.normaliseLayoutState(state, agGridOptions);
state = this.normaliseToolPanelState(state);
return state;
}
normaliseLayoutState(state, gridOptions) {
const layoutState = state.Layout;
// ensure that at least one Layout has been provided
if (!layoutState || !layoutState.Layouts?.length) {
this.logger
.consoleError(`You have not defined any Layout in your InitialState.Layout.Layouts[] state!
You need to define at least one Layout!`);
}
// ensure CurrentLayout is valid
if (!layoutState.CurrentLayout ||
!layoutState.Layouts.find((l) => l.Name === layoutState.CurrentLayout)) {
layoutState.CurrentLayout = layoutState.Layouts?.[0]?.Name;
}
/**
* Viewport mode does not support a few AG Grid features which are contained in a Layout
* Accordingly we remove this when using this Row Model
*/
if (gridOptions.rowModelType === 'viewport') {
if (state.Layout.Layouts) {
state.Layout.Layouts = state.Layout.Layouts.filter((layout) => {
if (isPivotLayout(layout)) {
return false;
}
if (layout.RowGroupedColumns) {
delete layout.RowGroupedColumns;
}
if (layout.TableAggregationColumns) {
delete layout.TableAggregationColumns;
}
return true;
});
}
}
if (state.Layout.Layouts) {
const normalizeOptions = {
isTree: !!gridOptions.treeData,
};
// it's very important that we do this here
// as the layout may not be fully specified in the initialState
// eg: might not include the generated row group columns in the column order
// but the normalization does this for us
state.Layout.Layouts = state.Layout.Layouts.map((layout) => normalizeLayout(layout, normalizeOptions));
}
return state;
}
normaliseToolPanelState(state) {
if (state?.ToolPanel?.ToolPanels) {
return state;
}
// no Initial Adaptable State provided, we will display all the panels collapsed (custom & module)
const defaultToolPanels = [];
this.adaptableOptions.toolPanelOptions?.customToolPanels?.forEach((customToolPanel) => defaultToolPanels.push({ Name: customToolPanel.name }));
ALL_TOOL_PANELS.forEach((moduleToolPanel) => defaultToolPanels.push({ Name: moduleToolPanel }));
const toolPanelState = state.ToolPanel || {};
toolPanelState.ToolPanels = defaultToolPanels;
state.ToolPanel = toolPanelState;
return state;
}
applyFiltering() {
const agGridApi = this.agGridAdapter.getAgGridApi();
this._emit('AdapTableFiltersApplied');
this.refreshSelectedCellsState();
this.refreshSelectedRowsState();
this.agGridAdapter.updateColumnFilterActiveState();
agGridApi.onFilterChanged();
}
// refreshAgGridWithAdaptableState() {
// this.refreshColDefs();
// this.api.themeApi.applyCurrentTheme();
// this.api.internalApi.setTreeMode(this.agGridAdapter.initialGridOptions.treeData);
// this.checkShouldClearExistingFiltersOrSearches();
// this.applyColumnFiltering();
// }
showQuickFilter() {
const height = this.api.optionsApi.getFilterOptions().columnFilterOptions.quickFilterHeight;
this.agGridAdapter.getAgGridApi().setGridOption('floatingFiltersHeight', height);
}
hideQuickFilter() {
this.agGridAdapter.getAgGridApi().setGridOption('floatingFiltersHeight', 0);
}
normalizeAdaptableOptions(adaptableOptions) {
if (this.hasAutogeneratedPrimaryKey) {
this.logger
.warn(`Autogenerated primary key (adaptableOptions.autogeneratedPrimaryKey = TRUE) should be used only as a last resort,
when no unique column is available, as it limits some Adaptable functionalities!
For more details see: ${PrimaryKeyDocsLink}`);
this.adaptableOptions.primaryKey = AUTOGENERATED_PK_COLUMN;
return this.adaptableOptions;
}
if (StringExtensions.IsNullOrEmpty(adaptableOptions.primaryKey)) {
this.logger.consoleError(`AdaptableOptions.primaryKey is required and cannot be empty or null!
As a fallback, you can set adaptableOptions.autogeneratedPrimaryKey = TRUE
For more details see: ${PrimaryKeyDocsLink}`);
}
return adaptableOptions;
}
setInitialGridOptions(gridOptions, variant) {
/**
* set Adaptable instance on the AG Grid context
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'context', (original_context) => {
const userContext = original_context || {};
return {
...userContext,
__adaptable: this,
adaptableApi: this.api,
};
});
/**
* `gridId`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'gridId', (original_gridId) => {
let agGridId = original_gridId || this.adaptableOptions.adaptableId;
if (this._isDetailGridForIndex != null) {
agGridId = `${agGridId}_detail-${this._isDetailGridForIndex}`;
}
this.agGridAdapter.setAgGridId(agGridId);
return agGridId;
});
/**
* `theme`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'theme', (original_theme) => {
this.agGridThemeAdapter.setAgGridThemeMode(original_theme === 'legacy' ? 'legacy' : 'themingApi');
return original_theme;
});
/**
* `getRowId`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'getRowId', (original_getRowId) => {
if (original_getRowId) {
return original_getRowId;
}
const primaryKey = this.adaptableOptions.primaryKey;
if (StringExtensions.IsNullOrEmpty(primaryKey)) {
// if no valid PK then do nothing
return original_getRowId;
}
if (this.hasAutogeneratedPrimaryKey) {
return (params) => {
// if the PK value is autogenerated, we need to make sure that the rowData has a valid PK value
// this should be taken care of in the Adaptable.[loadDataSource/setDataSource/updateRows]() methods, but the users will always make silly decisions
// so just to be safe we'll check here and add a PK value is missing
// thus adding a side-effect in a getter, but what can we do against it?! :)
if (Helper.objectNotExists(params.data[primaryKey])) {
params.data[primaryKey] = createUuid();
}
return params.data[primaryKey];
};
}
return (params) => {
if (params.data?.[primaryKey]) {
const primaryKeyValue = params.data[primaryKey];
return typeof primaryKeyValue === 'number'
? `${primaryKeyValue}`
: params.data[primaryKey];
}
// might be a summary row
if (params.data?.[ROW_SUMMARY_ROW_ID]) {
return params.data[ROW_SUMMARY_ROW_ID];
}
// AFL 2024.08.17 - no idea why is this here and when it's used
// might be a group row
const parentKeys = params.parentKeys ?? [];
const values = Object.values(params.data);
if (values.length) {
const id = [...parentKeys, values[0]].join('/');
return id;
}
};
});
/**
* `suppressAggFuncInHeader`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'suppressAggFuncInHeader', (original_suppressAggFuncInHeader) => {
const currentLayout = this.api.layoutApi.getCurrentLayout();
if (!currentLayout) {
return original_suppressAggFuncInHeader;
}
return currentLayout.SuppressAggFuncInHeader;
});
/**
* `aggFuncs`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'aggFuncs', (original_aggFuncs) => {
const aggregationFunctions = original_aggFuncs || {};
aggregationFunctions[WEIGHTED_AVERAGE_AGG_FN_NAME] = (params) => {
const columnId = params.column.getColId();
const adaptableAggFunc = this.getActiveAdaptableAggFuncForCol(columnId);
if (!adaptableAggFunc) {
return undefined;
}
if (adaptableAggFunc.type === 'weightedAverage') {
return weightedAverage(params, params.colDef.colId, adaptableAggFunc.weightedColumnId);
}
return undefined;
};
return aggregationFunctions;
});
/**
* `allowContextMenuWithControlKey`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'allowContextMenuWithControlKey', (original_allowContextMenuWithControlKey) => {
return original_allowContextMenuWithControlKey === undefined
? true
: original_allowContextMenuWithControlKey;
});
/**
* `isExternalFilterPresent`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'isExternalFilterPresent', (original_isExternalFilterPresent) => {
return (params) => {
if (!this.isAvailable) {
return true;
}
const isColumnFiltersActive = ArrayExtensions.IsNotNullOrEmpty(this.api.filterApi.columnFilterApi.getActiveColumnFilters());
const isGridFilterActive = StringExtensions.IsNotNullOrEmpty(this.api.filterApi.gridFilterApi.getCurrentGridFilterExpression());
return (isColumnFiltersActive ||
isGridFilterActive ||
// it means that userPropertyValue will be called so we re-init that collection
(original_isExternalFilterPresent ? original_isExternalFilterPresent(params) : false));
};
});
/**
* `doesExternalFilterPass`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'doesExternalFilterPass', (original_doesExternalFilterPass) => {
const { filteredOutPrimaryKeys } = this;
filteredOutPrimaryKeys.clear();
return (node) => {
if (node.rowIndex === 0) {
filteredOutPrimaryKeys.clear();
}
if (!this.isAvailable) {
return true;
}
// Note: not sure we need this check as doubtful AG Grid ever passes in a Group Row node to filter...
// But if it does, then we might change this IF we allow Column filtering (but not Grid Filters) for Group nodes
if (this.isGroupRowNode(node)) {
return true;
}
// first assess if the Row i s filterable - if not, then return true so it appears in Grid
const isRowFilterable = this.api.optionsApi.getFilterOptions().isRowFilterable;
if (typeof isRowFilterable === 'function') {
const rowFilterableContext = {
...this.api.internalApi.buildBaseContext(),
rowNode: node,
data: node.data,
};
if (!isRowFilterable(rowFilterableContext)) {
return true;
}
}
// get the Primary Key Value for the Row Node being evaluated
const primaryKey = this.getPrimaryKeyValueFromRowNode(node);
// next we assess a Grid Filter (if its running locally)
const currentGridFilterExpression = this.api.filterApi.gridFilterApi.getCurrentGridFilterExpression();
if (StringExtensions.IsNotNullOrEmpty(currentGridFilterExpression)) {
const evaluateGridFilterOnClient = this.api.expressionApi.internalApi.evaluateExpressionInAdaptableQL('GridFilter', undefined, currentGridFilterExpression);
if (evaluateGridFilterOnClient) {
const isCurrentGridFilterValid = this.api.expressionApi.isValidBooleanExpression(currentGridFilterExpression, GridFilterModuleId, `Invalid Grid Filter '${currentGridFilterExpression}'`);
// Not sure about this - what should we do with an invalid Grid Filter?
// Here we essentially clear the Grid for invalid Grid Filter by returning false for each row
if (!isCurrentGridFilterValid) {
filteredOutPrimaryKeys.add(primaryKey);
return false;
}
const gridFilterEvaluationResult = this.api.internalApi
.getQueryLanguageService()
.evaluateBooleanExpression(currentGridFilterExpression, GridFilterModuleId, node);
if (!gridFilterEvaluationResult) {
filteredOutPrimaryKeys.add(primaryKey);
return false;
}
}
}
// finally we evaluate column filters
const columnFilters = this.api.filterApi.columnFilterApi.getActiveColumnFilters();
try {
if (columnFilters.length > 0) {
for (const columnFilter of columnFilters) {
const evaluateColumnFilterOnClient = this.api.expressionApi.internalApi.evaluatePredicatesInAdaptableQL('ColumnFilter', columnFilter, columnFilter.Predicates);
if (evaluateColumnFilterOnClient) {
const columnFilterEvaluationResult = this.api.filterApi.columnFilterApi.internalApi.evaluateColumnFilter(columnFilter, node);
if (!columnFilterEvaluationResult) {
filteredOutPrimaryKeys.add(primaryKey);
return false;
}
}
}
}
}
catch (ex) {
this.logger.error(ex);
filteredOutPrimaryKeys.add(primaryKey);
return false;
}
const result = original_doesExternalFilterPass
? original_doesExternalFilterPass(node)
: true;
if (!result) {
filteredOutPrimaryKeys.add(primaryKey);
}
return result;
};
});
/**
* `getMainMenuItems`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'getMainMenuItems', (original_getMainMenuItems) => {
return (params) => {
// couldnt find a way to listen for menu close. There is a Menu Item Select, but you can also close menu from filter and clicking outside menu....
return this.agGridMenuAdapter.buildColumnMenu(params, original_getMainMenuItems);
};
});
/**
* `getContextMenuItems`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'getContextMenuItems', (original_getContextMenuItems) => {
return (params) => {
return this.agGridMenuAdapter.buildContextMenu(params, original_getContextMenuItems);
};
});
/**
* `components`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'components', (original_components) => {
const AdaptableToolPanel = getAdaptableToolPanelAgGridComponent(this);
const components = original_components || {};
const adaptableComponents = {
...components,
AdaptableToolPanel,
};
return adaptableComponents;
});
if (variant === 'react') {
// TODO very soon we have to transition to reactiveCustomComponents in React
// but for now, if we simply set it to true, it will break our editors, etc
// this.agGridOptionsService.setGridOptionsProperty(
// gridOptions,
// 'reactiveCustomComponents',
// () => true
// );
}
/**
* `sidebar`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'sideBar', (original_sideBar) => {
if (!original_sideBar) {
// lucky us, no sideBar is defined, so we don't have to do anything
return original_sideBar;
}
const isAdaptableToolPanelHidden = this.api.entitlementApi.isModuleHiddenEntitlement('ToolPanel');
const adaptableToolPanelDef = {
id: GeneralConstants.ADAPTABLE_TOOLPANEL_ID,
toolPanel: GeneralConstants.ADAPTABLE_TOOLPANEL_COMPONENT,
labelDefault: GeneralConstants.ADAPTABLE,
labelKey: 'adaptable',
iconKey: 'menu',
width: UIHelper.getAdaptableToolPanelWidth(),
minWidth: UIHelper.getAdaptableToolPanelWidth(),
// maxWidth = undefined,
};
const mapToolPanelDefs = (toolPanelDefs = []) => {
// if it's an alias for the adaptable tool panel, map it to a ToolPanelDef, otherwise return it as it is
return toolPanelDefs.map((toolPanelDef) => toolPanelDef === GeneralConstants.ADAPTABLE_TOOLPANEL_ID
? adaptableToolPanelDef
: toolPanelDef);
};
const isSideBarDefObject = (sidebarDef) => {
return Array.isArray(sidebarDef?.toolPanels);
};
let result;
if (original_sideBar === true) {
// create all tool panels with default settings
const toolPanels = [];
toolPanels.push(GeneralConstants.AGGRID_TOOLPANEL_FILTERS);
toolPanels.push(GeneralConstants.AGGRID_TOOLPANEL_COLUMNS);
if (!isAdaptableToolPanelHidden) {
toolPanels.push(adaptableToolPanelDef);
}
result = {
toolPanels: toolPanels,
};
}
// if there is only one tool panel, and it's the adaptable one => we have to handle it
else if (typeof original_sideBar === 'string') {
if (gridOptions.sideBar === GeneralConstants.ADAPTABLE_TOOLPANEL_ID) {
if (!isAdaptableToolPanelHidden)
result = {
toolPanels: [adaptableToolPanelDef],
};
}
else {
result = original_sideBar;
}
}
// if it's an array, process the tool panel definitions
else if (Array.isArray(original_sideBar)) {
if (!original_sideBar.includes(GeneralConstants.ADAPTABLE_TOOLPANEL_ID) ||
isAdaptableToolPanelHidden) {
result = original_sideBar;
}
// if it's an array, process the tool panel definitions
const sidebarDef = {};
sidebarDef.toolPanels = mapToolPanelDefs(original_sideBar);
result = sidebarDef;
}
// if it's fully-fledged SideBarDef, process its tool panel definitions
else if (isSideBarDefObject(original_sideBar)) {
if (original_sideBar.toolPanels?.some((toolpanelDef) => typeof toolpanelDef !== 'string' &&
toolpanelDef.id === GeneralConstants.ADAPTABLE_TOOLPANEL_ID &&
!isAdaptableToolPanelHidden)) {
// if there is an Adaptable SideBarDef, don't touch it as it may contain user-defined properties
result = original_sideBar;
}
else {
result = {
...original_sideBar,
toolPanels: mapToolPanelDefs(original_sideBar.toolPanels),
};
}
}
this.hasAdaptableToolPanel =
isSideBarDefObject(result) &&
result.toolPanels?.some((toolPanelDef) => typeof toolPanelDef !== 'string' &&
toolPanelDef.id === GeneralConstants.ADAPTABLE_TOOLPANEL_ID);
return result;
});
/**
* `statusBar`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'statusBar', (original_statusBar) => {
const statusPanels = (original_statusBar?.statusPanels ?? [])?.map((statusPanel) => {
if (statusPanel.statusPanel === ADAPTABLE_STATUS_PANEL) {
this.adaptableStatusPanelKeys.push(statusPanel.key);
const context = {
Key: statusPanel.key,
};
return {
...statusPanel,
statusPanel: createAgStatusPanelComponent(AdaptableStatusBar, this, context),
};
}
return statusPanel;
});
return { ...original_statusBar, statusPanels };
});
/**
* `getRowStyle`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'getRowStyle', (original_getRowStyle) => {
return (params) => {
const result = {
...original_getRowStyle?.(params),
...this.api.gridApi.internalApi.getRowHighlightStyle(params),
...this.api.gridApi.internalApi.getAlertRowStyle(params),
};
return result;
};
});
/**
* `getRowClass`
*/
this.agGridOptionsService.setGridOptionsProperty(gridOptions, 'getRowClass', (original_getRowClass) => {
return (params) => {
const alertHighlightClassName = this.api.gridApi.internalApi.getAlertRowClass(params);
const highlightClassName = this.api.gridApi.internalApi.getRowHighlightClass(params);
const returnValue = [
typeof original_getRowClass === 'function'
? original_getRowClass(params)
: original_getRowClass,
highlightClassName,
alertHighlightClassName,
]
// we flatten it because 'original_getRowClass' might return a string[]
.flat()
.filter((x) => !!x);
return retur