UNPKG

@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
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