UNPKG

@adaptabletools/adaptable

Version:

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

967 lines 65.3 kB
import kebabCase from 'lodash/kebabCase'; import merge from 'lodash/merge'; import { convertAdaptableStyleToCSS, getVariableColor, normalizeStyleForAgGrid, } from '../Utilities/Helpers/StyleHelper'; import StringExtensions from '../Utilities/Extensions/StringExtensions'; import { ACTION_COLUMN_TYPE, CALCULATED_COLUMN_TYPE, FDC3_COLUMN_TYPE, FREE_TEXT_COLUMN_TYPE, } from '../AdaptableState/Common/AdaptableColumn'; import tinycolor from 'tinycolor2'; import UIHelper from '../View/UIHelper'; import { getPercentBarRendererForColumn } from './cellRenderers/PercentBarRenderer'; import { getBadgeRendererForColumn } from './cellRenderers/BadgeRenderer'; import Helper from '../Utilities/Helpers/Helper'; import { AdaptableNumberEditor, AdaptableReactNumberEditor } from './editors/AdaptableNumberEditor'; import { AdaptableDateEditor, AdaptableReactDateEditor } from './editors/AdaptableDateEditor'; import { AgGridExportAdapter } from './AgGridExportAdapter'; import { AdaptableHelper } from '../Utilities/Helpers/AdaptableHelper'; import { isProvidedByAdaptable } from '../Utilities/adaptableOverrideCheck'; import { AdaptableFilterHandler } from './AdaptableFilterHandler'; import { AgGridFilterAdapterFactory } from './AgGridFilterAdapter'; import { AgGridFloatingFilterAdapterFactory } from './AgGridFloatingFilterAdapter'; import { errorOnce } from './AdaptableLogger'; import { isWeightedAverageAggFuncName } from '../AdaptableState/Common/AggregationColumns'; export function getEditorForColumnDataType(columnDataType, variant) { if (columnDataType === 'number') { return variant === 'react' ? AdaptableReactNumberEditor : AdaptableNumberEditor; } if (columnDataType === 'date') { return variant === 'react' ? AdaptableReactDateEditor : AdaptableDateEditor; } } export class AgGridColumnAdapter { constructor(adaptableInstance) { this.adaptableInstance = adaptableInstance; this.colDefPropertyCache = new Map(); } getVariant() { return this.adaptableInstance.variant; } destroy() { this.adaptableInstance = null; this.colDefPropertyCache.clear(); this.colDefPropertyCache = null; } get adaptableApi() { return this.adaptableInstance.api; } get adaptableOptions() { return this.adaptableInstance.adaptableOptions; } get agGridApi() { return this.adaptableApi.agGridApi; } setColDefProperty(col, propertyName, propertyGetter) { const colId = col.getColId(); const colDef = col.getColDef(); const colSetupInfo = { col, colDef, colId, }; const userKey = `user.${colId}.${propertyName}`; const adaptableKey = `adaptable.${colId}.${propertyName}`; const value = colDef[propertyName]; const isUserDefined = value !== this.colDefPropertyCache.get(adaptableKey); if (isUserDefined) { this.colDefPropertyCache.set(userKey, value); } const userValue = this.colDefPropertyCache.get(userKey); const adaptableValue = propertyGetter(userValue); if (adaptableValue != null) { this.colDefPropertyCache.set(adaptableKey, adaptableValue); } let theValue = adaptableValue ?? userValue; this.adaptableInstance.forPlugins((plugin) => { if (plugin.interceptSetupColumnProperty) { theValue = plugin.interceptSetupColumnProperty(colSetupInfo, propertyName, theValue, this.adaptableApi); } }); if (propertyName === 'aggFunc') { if (colDef[propertyName] !== (theValue ?? null)) { this.agGridApi?.setColumnAggFunc(colId, theValue ?? null); } } if (theValue === undefined && colDef[propertyName] === undefined) { // already undefined, so don't set an own property to the same undefined value return; } colDef[propertyName] = theValue; } getUserColDefProperty(columnId, propertyName) { const userKey = `user.${columnId}.${propertyName}`; return this.colDefPropertyCache.get(userKey); } shouldSkipColumn(colId) { /** * This skips special columns that ag grid will likely implement in the future * * BUT DOES NOT SKIP GROUP COLUMNS or SELECTION COLUMNS!!! * * It's probably here for historical reasons - previously it used to skip the selection column * but now it's not skipping it anymore */ return (colId.startsWith('ag-Grid-') && !this.adaptableApi.columnApi.isAutoRowGroupColumn(colId) && !this.adaptableApi.columnApi.isSelectionColumn(colId)); } setupColumns() { const pivotMode = this.adaptableInstance.isInPivotMode(); let cols = pivotMode ? // for pivot mode, we take only the initial columns this.agGridApi.getColumns() : // but for non-pivot mode, we ask for all columns // which also includes those that are generated for row grouping this.agGridApi.getAllGridColumns(); if (pivotMode) { const pivotResultColumns = this.agGridApi.getPivotResultColumns() || []; const autoGroupColumns = this.agGridApi .getAllGridColumns() .filter((column) => this.adaptableApi.columnApi.isAutoRowGroupColumn(column.getColId())); cols = [...autoGroupColumns, ...cols, ...pivotResultColumns]; } // this needs to be here, before the other setup below // so the setup methods below reference the correct columns in adaptable store cols.forEach((col) => { const colDef = col.getColDef(); const colId = col.getColId(); if (this.shouldSkipColumn(colId)) { return; } const abColumn = this.adaptableApi.columnApi.getColumnWithColumnId(colId); const colSetupInfo = { col, colDef, colId, abColumn, }; this.setupColumnCellRenderer(colSetupInfo); this.setupColumnCellStyle(colSetupInfo); this.setupColumnCellClass(colSetupInfo); this.setupColumnHeaderStyle(colSetupInfo); this.setupColumnHeaderClass(colSetupInfo); this.setupColumnValueGetter(colSetupInfo); this.setupColumnTooltipValueGetter(colSetupInfo); this.setupColumnFilter(colSetupInfo); this.setupColumnFloatingFilter(colSetupInfo); this.setupColumnValueFormatter(colSetupInfo); this.setupColumnEditable(colSetupInfo); this.setupColumnValueSetter(colSetupInfo); this.setupColumnComparator(colSetupInfo); this.setupColumnGetFindText(colSetupInfo); this.setupColumnCellEditor(colSetupInfo); this.setupColumnHeader(colSetupInfo); this.setupColumnQuickFilterText(colSetupInfo); this.setupColumnAllowedAggFuncs(colSetupInfo); this.setupColumnType(colSetupInfo); this.setupColumnCellDataType(colSetupInfo); }); } setupColumnValueGetter({ col }) { // need this here if we want plugins to intercept this.setColDefProperty(col, 'valueGetter', (userValue) => { return userValue; }); } setupColumnCellClass({ col, colId, abColumn }) { this.setColDefProperty(col, 'cellClass', (userCellClass) => { const formatColumns = this.adaptableApi.formatColumnApi.internalApi.getFormatColumnWithStyleClassNameForColumn(abColumn, { target: 'cell' }); const quickSearchTextMatchStyle = this.getQuickSearchTextMatchStyle(); const quickSearchCurrentTextMatchStyle = this.getQuickSearchCurrentTextMatchStyle(); const cellClass = (params) => { const gridCell = this.adaptableApi.gridApi.getGridCellFromRowNode(params.node, abColumn.columnId); if (!gridCell.column) { return null; } const isExcelExport = this.adaptableApi.exportApi.internalApi.isExcelExportInProgress(); const isVisualDataExport = this.adaptableApi.exportApi.internalApi.isVisualDataExportInProgress(); if (isExcelExport || isVisualDataExport) { const excelStyleClasses = []; const userDefinedCellClass = typeof userCellClass === 'function' ? userCellClass(params) : userCellClass; if (userDefinedCellClass) { if (Array.isArray(userDefinedCellClass)) { excelStyleClasses.push(...userDefinedCellClass.filter(Boolean)); } else { excelStyleClasses.push(userDefinedCellClass); } } if (isVisualDataExport) { const cellClassKey = AgGridExportAdapter.getExcelClassNameForCell(colId, gridCell.primaryKeyValue, userDefinedCellClass); const customCellClass = this.adaptableInstance.agGridExportAdapter.getExcelStyleIdForClassKey(cellClassKey); if (customCellClass) { excelStyleClasses.push(customCellClass); } } return excelStyleClasses.length ? excelStyleClasses : null; } const isQuickSearchActive = this.isQuickSearchActive(gridCell); const editableClassName = this.getEditableCellClass(gridCell, params); const readonlyClassName = this.getReadonlyCellClass(gridCell, params); const editedClassName = this.getEditedCellClass(gridCell, params); const highlightAlertClassName = this.getAlertCellClass(gridCell, params); const flashingClassName = this.getFlashingCellClass(gridCell, params); const styledColumn = this.adaptableApi.styledColumnApi.getStyledColumnForColumnId(colId); const hasStyledColumn = !!styledColumn && !styledColumn.IsSuspended; const noteClassName = this.getNoteCellClassName(gridCell, params); const commentsClassName = this.getCommentCellClassName(gridCell, params); const returnValue = [ typeof userCellClass === 'function' ? userCellClass(params) : userCellClass, !hasStyledColumn && formatColumns.length ? this.getFormatColumnCellClass(formatColumns, abColumn, params) : null, // isQuickSearchActive && hasQuickSearchStyleClassName ? quickSearchStyleClassName : null, isQuickSearchActive && (quickSearchTextMatchStyle || quickSearchCurrentTextMatchStyle) ? 'ab-QuickSearchFind' : null, editableClassName, readonlyClassName, editedClassName, highlightAlertClassName, flashingClassName, noteClassName, commentsClassName, ] // we flatten the array because some rules ('userCellClass' etc) might return a string[] .flat() .filter((x) => !!x); const result = returnValue.length ? returnValue : undefined; return result; }; return cellClass; }); } setupColumnHeaderClass({ col, colId, abColumn }) { this.setColDefProperty(col, 'headerClass', (userHeaderClass) => { const headerClass = (params) => { let baseHeaderClass = []; // inherit styles from user provided colDef property const userDefinedHeaderClass = typeof userHeaderClass === 'function' ? userHeaderClass(params) : userHeaderClass; if (userDefinedHeaderClass) { baseHeaderClass = typeof userDefinedHeaderClass === 'string' ? [userDefinedHeaderClass] : Array.isArray(userDefinedHeaderClass) ? userDefinedHeaderClass : [userDefinedHeaderClass]; } if (params.floatingFilter) { // we NEVER style floating filters return baseHeaderClass; } if (params.columnGroup) { // we TEMPORARILY do NOT style column groups // see https://github.com/AdaptableTools/adaptable/issues/2947#issuecomment-3062304655 return baseHeaderClass; } const isExcelExport = this.adaptableApi.exportApi.internalApi.isExcelExportInProgress(); const isVisualDataExport = this.adaptableApi.exportApi.internalApi.isVisualDataExportInProgress(); if (isExcelExport || isVisualDataExport) { const excelStyleHeaderClasses = []; if (userDefinedHeaderClass) { if (Array.isArray(userDefinedHeaderClass)) { excelStyleHeaderClasses.push(...userDefinedHeaderClass.filter(Boolean)); } else { excelStyleHeaderClasses.push(userDefinedHeaderClass); } } if (isVisualDataExport) { const headerClassKey = AgGridExportAdapter.getExcelClassNameForHeader(colId, userDefinedHeaderClass); const customHeaderClass = this.adaptableInstance.agGridExportAdapter.getExcelStyleIdForClassKey(headerClassKey); if (customHeaderClass) { excelStyleHeaderClasses.push(customHeaderClass); } } return excelStyleHeaderClasses.length ? excelStyleHeaderClasses : null; } const columnId = params.column.getColId(); const adaptableColumn = this.adaptableApi.columnApi.getColumnWithColumnId(columnId); if (!adaptableColumn) { return baseHeaderClass; } const target = 'columnHeader'; // handle special case of headers with text alignment // in this case, we add a specific class to the header // see #header_text_align const formatColumnWithTextAlignment = this.getRelevantFormatColumnHeaderStyles(abColumn) // we take the first one only, even if multiple are defined .find((fc) => fc.CellAlignment != undefined); if (formatColumnWithTextAlignment) { baseHeaderClass = [ ...baseHeaderClass, `ab-header__align-${formatColumnWithTextAlignment.CellAlignment.toLowerCase()}`, ]; } const formatColumns = this.adaptableApi.formatColumnApi.internalApi.getFormatColumnWithStyleClassNameForColumn(abColumn, { target, }); if (!formatColumns.length) { return baseHeaderClass; } const formatColumnClasses = formatColumns .map((formatColumn) => { if (formatColumn.Style?.ClassName && this.adaptableApi.formatColumnApi.internalApi.formatColumnShouldRenderInHeader(formatColumn, abColumn)) { return formatColumn.Style?.ClassName; } }) .filter((x) => !!x); return [...baseHeaderClass, ...formatColumnClasses]; }; return headerClass; }); } setupColumnCellStyle({ col }) { this.setColDefProperty(col, 'cellStyle', (userCellStyle) => { const quickSearchStyle = this.getQuickSearchCellStyle(); const quickSearchTextMatchStyle = this.getQuickSearchTextMatchStyle(); const quickSearchCurrentTextMatchStyle = this.getQuickSearchCurrentTextMatchStyle(); const textMatchStyle = quickSearchTextMatchStyle ? Object.entries(quickSearchTextMatchStyle).reduce((acc, [key, value]) => { // needed as AG-Grid vanilla turns all CSS props // to kebab, while AG Grid React does not // @ts-ignore acc[`--ab-dynamic-${kebabCase(key)}`] = value; return acc; }, {}) : undefined; const currentTextMatchStyle = quickSearchCurrentTextMatchStyle ? Object.entries(quickSearchCurrentTextMatchStyle).reduce((acc, [key, value]) => { // @ts-ignore acc[`--ab-dynamic-${kebabCase(key)}`] = value; return acc; }, {}) : undefined; const hasQuickSearchStyle = quickSearchStyle != undefined || quickSearchCurrentTextMatchStyle != undefined; const cellStyle = (params) => { const columnId = params.column.getColId(); const gridCell = this.adaptableApi.gridApi.getGridCellFromRowNode(params.node, columnId); if (!gridCell || !gridCell.column) { return {}; } const isQuickSearchActive = hasQuickSearchStyle && this.isQuickSearchActive(gridCell); const isQuickSearchAvailable = this.adaptableApi.internalApi .getModuleService() .isAdapTableModulePresent('QuickSearch'); const isCurrentMatch = isQuickSearchAvailable && this.adaptableApi.agGridApi.findGetActiveMatch()?.node === params.node; const textStyleToApply = isCurrentMatch ? { ...textMatchStyle, ...currentTextMatchStyle } : textMatchStyle; let baseStyles = {}; // this is required because otherwise, when AG Grid filters, it refreshed the pivotResultColDef and the base styles get lost // if pivot result col: inherit styles from base column if (this.adaptableApi.columnApi.isPivotResultColumn(columnId)) { const baseColumn = params.column.getColDef()?.pivotValueColumn; if (baseColumn) { const baseColDefCellStyle = baseColumn?.getColDef()?.cellStyle; const baseColParams = { ...params, column: baseColumn, // @ts-ignore // #derived_pivot_cell_style __pivotResultColumn: params.column, }; baseStyles = typeof baseColDefCellStyle === 'function' ? baseColDefCellStyle(baseColParams) : baseColDefCellStyle; } } else { // inherit styles from user provided colDef property baseStyles = typeof userCellStyle === 'function' ? userCellStyle(params) : userCellStyle; } const result = { ...baseStyles, ...this.getReadOnlyCellStyle(gridCell, params), ...this.getEditableCellStyle(gridCell, params), ...this.getEditedCellStyle(gridCell, params), ...this.getFormatColumnAndStyledColumnCellStyle(gridCell.column, params), ...(isQuickSearchActive ? quickSearchStyle : {}), ...(isQuickSearchActive && textStyleToApply ? textStyleToApply : {}), ...this.getAlertCellStyle(gridCell, params), ...this.getFlashingCellStyle(gridCell, params), ...this.getCellHighlightStyle(gridCell, params), }; return normalizeStyleForAgGrid(result); }; return cellStyle; }); } setupColumnHeaderStyle({ col }) { this.setColDefProperty(col, 'headerStyle', (userHeaderStyle) => { const headerStyleFunc = (params) => { let baseStyles = {}; // inherit styles from user provided colDef property baseStyles = typeof userHeaderStyle === 'function' ? userHeaderStyle(params) : userHeaderStyle; if (params.floatingFilter) { // we NEVER style floating filters return baseStyles; } const columnId = params.column.getColId(); const adaptableColumn = this.adaptableApi.columnApi.getColumnWithColumnId(columnId); if (!adaptableColumn) { return baseStyles; } const result = { ...baseStyles, ...this.getFormatColumnHeaderStyle(adaptableColumn, params), }; if (result['borderColor']) { // by default, header cells don't have a border // if the user defines a border-color, we assume he wants to show a border result['borderWidth'] = `1px`; result['borderStyle'] = 'solid'; } return normalizeStyleForAgGrid(result); }; return headerStyleFunc; }); } setupColumnCellEditor({ colId, col, colDef, abColumn }) { const shouldShowSelectCellEditor = this.adaptableApi.userInterfaceApi.internalApi.shouldShowSelectCellEditor(abColumn); const hasRichSelectCellEditor = this.adaptableApi.internalApi .getAgGridModulesAdapter() .isAgGridModuleRegistered('RichSelectModule'); this.setColDefProperty(col, 'cellEditor', () => { if (shouldShowSelectCellEditor) { if (hasRichSelectCellEditor) { return 'agRichSelectCellEditor'; } else { this.adaptableApi.logWarn(`Cannot show Select Editor as missing required AG Grid module: RichSelect`); return colDef.cellEditor; } } else { if (colDef.cellEditor) { return colDef.cellEditor; } const cellDataTypeEditor = getEditorForColumnDataType(abColumn.dataType, this.getVariant()); return cellDataTypeEditor; } }); this.setColDefProperty(col, 'cellEditorParams', (params) => { if (shouldShowSelectCellEditor) { return (params) => { const gridCell = this.adaptableApi.gridApi.getGridCellFromRowNode(params?.node, colId); const options = this.adaptableApi.gridApi.internalApi.getDistinctEditDisplayValuesForColumn({ columnId: colId, gridCell, currentSearchValue: '', }); const valueToLabelMap = new Map(); const values = options.then((options) => options.map((option) => { valueToLabelMap.set(option.value, option.label); return option.value; })); return { values, formatValue: (value) => valueToLabelMap.get(value) ?? value, }; }; } }); } setupColumnCellRenderer({ col, colId, abColumn }) { this.setColDefProperty(col, 'cellRenderer', () => { const styledColumn = this.adaptableApi.styledColumnApi.getStyledColumnForColumnId(abColumn.columnId); if (styledColumn && !styledColumn.IsSuspended) { if (styledColumn.PercentBarStyle) { return getPercentBarRendererForColumn(styledColumn, abColumn, this.adaptableApi); } if (styledColumn.BadgeStyle) { return getBadgeRendererForColumn(styledColumn.BadgeStyle, abColumn, this.adaptableApi); } if (styledColumn.SparklineStyle) { return 'agSparklineCellRenderer'; } } }); this.setColDefProperty(col, 'cellRendererParams', (userDefined) => { const styledColumn = this.adaptableApi.styledColumnApi.getStyledColumnForColumnId(abColumn.columnId); if (styledColumn && !styledColumn.IsSuspended) { if (styledColumn.SparklineStyle) { const sanitizedSparklineOptions = AdaptableHelper.removeAdaptableObjectPrimitives(styledColumn.SparklineStyle.options); const sparklineOptions = merge({}, userDefined?.sparklineOptions, sanitizedSparklineOptions); return { ...userDefined, sparklineOptions, }; } } }); } setupColumnTooltipValueGetter({ col, colId, abColumn }) { let hasTooptip = false; this.setColDefProperty(col, 'tooltipValueGetter', () => { const styledColumn = this.adaptableApi.styledColumnApi.getStyledColumnForColumnId(colId); if (styledColumn && !styledColumn.IsSuspended && styledColumn.PercentBarStyle && styledColumn.PercentBarStyle.ToolTipText) { hasTooptip = true; if (styledColumn?.PercentBarStyle) { return (params) => { const min = this.adaptableApi.styledColumnApi.internalApi.getNumericStyleMinValue(styledColumn, abColumn, params.node, params.value); const max = this.adaptableApi.styledColumnApi.internalApi.getNumericStyleMaxValue(styledColumn, abColumn, params.node, params.value); const textOptions = styledColumn.PercentBarStyle.ToolTipText; let returnValue = ''; if (textOptions.includes('CellValue')) { returnValue = params.value; } if (textOptions.includes('PercentageValue')) { const clampedValue = Helper.clamp(params.value, min, max); const percentageValue = ((clampedValue - min) / (max - min)) * 100; returnValue += ' ' + `(${percentageValue.toFixed(0)}%)`; } return returnValue ? returnValue : params.value; }; } } }); } setupColumnQuickFilterText({ col, abColumn }) { this.setColDefProperty(col, 'getQuickFilterText', (userGetQuickFilterText) => { if (userGetQuickFilterText) { return userGetQuickFilterText; } return (params) => { const visibleColumnsMap = this.adaptableApi.layoutApi.getCurrentVisibleColumnIdsMapForTableLayout(); const isVisible = visibleColumnsMap[abColumn.columnId]; if (!isVisible) { return ''; } return this.adaptableApi.gridApi.getDisplayValueFromRowNode(params.node, abColumn.columnId); }; }); } setupColumnAllowedAggFuncs({ col, abColumn }) { this.setColDefProperty(col, 'allowedAggFuncs', () => { if (!abColumn.availableAggregationFunctions) { return undefined; } return abColumn.availableAggregationFunctions.filter((func) => !isWeightedAverageAggFuncName(func)); }); } setupColumnType(columnSetupInfo) { const { col, colId } = columnSetupInfo; // AG Grid introduced since v30.x an inferred cellDataType // the problem is that it breaks the default value formatter and/or editor (especially for Date columns) this.setColDefProperty(col, 'type', (original_columnType) => { const originalTypes = original_columnType == undefined ? [] : Array.isArray(original_columnType) ? original_columnType : [original_columnType]; const columnTypes = new Set(originalTypes); if (this.adaptableApi.columnApi.isCalculatedColumn(colId)) { columnTypes.add(CALCULATED_COLUMN_TYPE); } if (this.adaptableApi.columnApi.isFreeTextColumn(colId)) { columnTypes.add(FREE_TEXT_COLUMN_TYPE); } if (this.adaptableApi.columnApi.isActionColumn(colId)) { columnTypes.add(ACTION_COLUMN_TYPE); } if (this.adaptableApi.columnApi.isFdc3Column(colId)) { columnTypes.add(FDC3_COLUMN_TYPE); } return Array.from(columnTypes); }); } setupColumnCellDataType(columnSetupInfo) { const { col, colId } = columnSetupInfo; this.setColDefProperty(col, 'cellDataType', (original_cellDataType) => { // #map_dateString_to_date if (original_cellDataType === 'dateString') { errorOnce(`AG Grid: Column '${colId}' specifies cellDataType='dateString' which is no longer supported. It has been automatically replaced with cellDataType='date'. Please update your column definition.`); return 'date'; } return original_cellDataType ?? true; }); } setupColumnHeader({ col }) { this.setColDefProperty(col, 'headerValueGetter', (original_headerValueGetter) => { // see #customize_header if (!isProvidedByAdaptable(original_headerValueGetter)) { this.adaptableApi.logWarn(`colDef.headerValueGetter is defined for column '${col.getColId()}', and overrides the Adaptable custom header mechanism! We recommend using a ColumnOptions.columnHeader instead!`); } return original_headerValueGetter; }); } setupColumnFilter(columnSetup) { const { col, colDef, abColumn, colId } = columnSetup; const useAdaptableFilter = this.adaptableOptions.filterOptions.useAdaptableFiltering; if (!useAdaptableFilter) { return; } // setup Auto Group Column Filter if (this.adaptableApi.columnApi.isAutoRowGroupColumn(col.getColId())) { if (this.adaptableApi.gridApi.isTreeDataGrid()) { this.setColDefProperty(col, 'filter', (original_filter) => { const autoGroupColumnDef = this.agGridApi.getGridOption('autoGroupColumnDef'); if (autoGroupColumnDef?.filter != undefined) { // we plan to provide a TreeListColumnFilter // until then, it's the user's responsibility to explicitly set the filter in the provided `autoGroupColumnDef` return original_filter; } else { // if no filter is explicitly set, we do NOT provide a filter return false; } }); } else { // TODO only set auto group column as filterable if at least one group columns is filterable this.setColDefProperty(col, 'filter', () => { return 'agGroupColumnFilter'; }); } if (this.adaptableApi.columnApi.isAutoRowGroupColumnForMulti(colId)) { return; } } // setup "normal" column filter this.setColDefProperty(col, 'filter', (original_filter) => { const pivotMode = this.adaptableInstance.isInPivotMode(); if (!useAdaptableFilter) { return original_filter; } if (!colDef.filter) { return; } if (typeof original_filter !== 'boolean' && typeof original_filter?.handler !== 'function' && !pivotMode) { this.adaptableApi.consoleError(`Column '${colId}' has a custom filter defined in colDef.filter, but Adaptable Filtering accepts only the TRUE/FALSE values!`); return false; } return { component: AgGridFilterAdapterFactory(this.adaptableInstance), handler: () => new AdaptableFilterHandler(this.adaptableApi, columnSetup), }; }); } setupColumnFloatingFilter({ col, colDef }) { const userProvidedFilterProp = this.getUserColDefProperty(col.getColId(), 'filter'); const hasInvalidFilterProp = typeof userProvidedFilterProp !== 'boolean' && typeof userProvidedFilterProp?.handler !== 'function' && !this.adaptableInstance.isInPivotMode(); if (hasInvalidFilterProp) { // warning is logged in the 'setupColumnFilter' method return false; } const isFloatingFilterDisabled = !colDef.filter || !colDef.floatingFilter || !this.adaptableOptions.filterOptions.useAdaptableFiltering || !this.adaptableApi.filterApi.columnFilterApi.isQuickFilterVisible(); if (this.adaptableApi.columnApi.isAutoRowGroupColumnForMulti(col.getColId())) { this.setColDefProperty(col, 'floatingFilter', (original_floatingFilter) => { // the floating filter for the group column is "inherited" from the base column // via the colDef.filter = 'agGroupColumnFilter' // see #group_inherit_column_filter // https://www.ag-grid.com/javascript-data-grid/grouping-single-group-column/#inherit-row-grouped-columns-filters // https://www.ag-grid.com/javascript-data-grid/grouping-multiple-group-columns/#filtering return original_floatingFilter; }); this.setColDefProperty(col, 'suppressFloatingFilterButton', () => { // hide button for multi column groups return true; }); return; } this.setColDefProperty(col, 'floatingFilterComponent', () => { if (isFloatingFilterDisabled) { return; } return AgGridFloatingFilterAdapterFactory(this.adaptableInstance); }); this.setColDefProperty(col, 'floatingFilter', () => { if (isFloatingFilterDisabled) { return; } return AgGridFloatingFilterAdapterFactory(this.adaptableInstance); }); this.setColDefProperty(col, 'suppressFloatingFilterButton', () => { return !isFloatingFilterDisabled; }); } setupColumnValueFormatter({ col, abColumn }) { this.setColDefProperty(col, 'valueFormatter', (userPropertyValue) => { const activeFormatColumnsWithDisplayFormat = this.adaptableApi.formatColumnApi.internalApi.getFormatColumnsWithDisplayFormatForColumn(abColumn, { target: 'cell' }); if (!activeFormatColumnsWithDisplayFormat.length) { return; } return (params) => { const { node, value } = params; const mostRelevantFormatColumn = this.adaptableApi.formatColumnApi.internalApi.getMostRelevantFormatColumnForColumn(activeFormatColumnsWithDisplayFormat, abColumn, { node, value }); if (!mostRelevantFormatColumn) { // ALL FormatColumns are conditional and NONE of them are relevant for this row return value; } const formatterOptions = mostRelevantFormatColumn.DisplayFormat.Options; if (mostRelevantFormatColumn.DisplayFormat.Formatter === 'NumberFormatter') { // change the Number format - if the scope allows it if (this.adaptableApi.columnScopeApi.isColumnInNumericScope(abColumn, mostRelevantFormatColumn.Scope)) { let cellValue = params.value; if (typeof params.value?.toNumber === 'function' && typeof params.value?.toString === 'function') { // aggregation values are wrapped in an AG Grid specific object cellValue = params.value.toNumber(); } return this.adaptableApi.formatColumnApi.internalApi.getNumberFormattedValue(cellValue, params.node, abColumn, formatterOptions); } } if (mostRelevantFormatColumn.DisplayFormat.Formatter === 'DateFormatter') { // change the Date format - if the scope allows it if (this.adaptableApi.columnScopeApi.isColumnInDateScope(abColumn, mostRelevantFormatColumn.Scope)) { let cellValue = params.value; if (typeof params.value?.toNumber === 'function' && typeof params.value?.toString === 'function') { // aggregation values are wrapped in an AG Grid specific object cellValue = params.value.toString(); } return this.adaptableApi.formatColumnApi.internalApi.getDateFormattedValue(cellValue, params.node, abColumn, formatterOptions); } } if (mostRelevantFormatColumn.DisplayFormat.Formatter === 'StringFormatter') { // change the String format - if the scope allows it if (this.adaptableApi.columnScopeApi.isColumnInTextScope(abColumn, mostRelevantFormatColumn.Scope)) { let cellValue = params.value; if (typeof params.value?.toNumber === 'function' && typeof params.value?.toString === 'function') { // aggregation values are wrapped in an AG Grid specific object cellValue = params.value.toString(); } return this.adaptableApi.formatColumnApi.internalApi.getStringFormattedValue(cellValue, params.node, abColumn, formatterOptions); } } // should NEVER arrive at this line, but just to be sure return value; }; }); } setupColumnEditable({ col }) { this.setColDefProperty(col, 'editable', (original_editable) => { // cell is NOT editable by default const editableCallback = (params) => { const getOriginalColDefEditable = () => { if (typeof original_editable === 'function') { return original_editable(params); } else { return original_editable; } }; // 1. evaluate EditOptions.isCellEditable if provided if (this.adaptableApi.gridApi.internalApi.hasCellEditableAccordingToEditOptions()) { const gridCell = this.adaptableApi.gridApi.getGridCellFromRowNode(params.node, params.column.getColId()); const editOptionsEditability = this.adaptableApi.gridApi.internalApi.isCellEditableAccordingToEditOptions(gridCell, getOriginalColDefEditable()); if (editOptionsEditability) { return editOptionsEditability; } } // 2. otherwise, fallback to colDef.editable return getOriginalColDefEditable(); }; return editableCallback; }); } setupColumnValueSetter({ col, colId, abColumn }) { this.setColDefProperty(col, 'valueSetter', (userValueSetter) => { const preventEditAlertsForColumn = this.adaptableApi.alertApi.internalApi .getAlertDefinitionsWithPreventEdit() .filter((alertDefinition) => { return this.adaptableApi.columnScopeApi.isColumnInScope(abColumn, alertDefinition.Scope); }); const noValidations = !preventEditAlertsForColumn.length && !this.adaptableOptions.editOptions?.validateOnServer; if (noValidations) { return; } const valueSetter = (params) => { const field = params.column.getColDef().field; if (noValidations) { //TODO also consider the case when userValueSetter is a string if (typeof userValueSetter === 'function') { return userValueSetter(params); } // we allowed it go reach this point // just to run isCellEditable // and since this has already run and we have no other validations // just assign the new value and exit if (field) { params.data[field] = params.newValue; } return true; } const cellDataChangedInfo = this.adaptableApi.internalApi.buildCellDataChangedInfo({ oldValue: params.oldValue, newValue: params.newValue, column: this.adaptableApi.columnApi.getColumnWithColumnId(params.column.getColId()), primaryKeyValue: this.adaptableApi.gridApi.getPrimaryKeyValueForRowNode(params.node), rowNode: params.node, trigger: 'edit', }); if (cellDataChangedInfo.oldValue === cellDataChangedInfo.newValue) { return true; } /** * Validate on the future row, with the new value. * structuredClone fails, it contains functions. */ const newRow = { ...params.node, data: { ...params.node.data } }; newRow.data[field] = params.newValue; const cellDataChangeInfoForSyncValidation = { ...cellDataChangedInfo, rowNode: newRow, }; if (!this.adaptableApi.internalApi .getValidationService() .performValidation(cellDataChangeInfoForSyncValidation)) { return false; } const onServerValidationCompleted = () => { }; if (this.adaptableOptions.editOptions?.validateOnServer) { this.adaptableApi.internalApi .getValidationService() .performServerValidation(cellDataChangedInfo, { onServerValidationCompleted, })(); } //TODO also consider the case when userValueSetter is a string if (typeof userValueSetter === 'function') { return userValueSetter(params); } if (field) { params.data[field] = params.newValue; } else { throw `Cannot edit a column without a field - column id was ${colId}`; } return true; }; return valueSetter; }); } setupColumnComparator({ col, colId, abColumn }) { const customSort = this.adaptableApi.customSortApi.getCustomSortForColumn(colId); const customSortComparer = this.adaptableApi.customSortApi.internalApi.getCustomSortComparer(abColumn.columnId); const memoizedDateValues = new Map(); const getDateValue = (rawDateValue) => { if (memoizedDateValues.has(rawDateValue)) { return memoizedDateValues.get(rawDateValue); } const dateValue = this.adaptableApi.internalApi.parseDateValue(rawDateValue); memoizedDateValues.set(rawDateValue, dateValue); return dateValue; }; const comparatorGetter = (propName) => { return (userPropertyValue) => { // CustomSort Comparer function takes precedence over CustomSort SortedValues if (customSortComparer) { return customSortComparer.comparer; } if (customSort && !customSort.IsSuspended) { return this.adaptableApi.customSortApi.internalApi.getDefaultCustomSortComparer(customSort.ColumnId, customSort.SortedValues); } if (userPropertyValue) { return userPropertyValue; } // for DATE columns we have to use the normalised values (Date objects) to compare if (abColumn.dataType === 'date') { return (valueA, valueB) => { const dateA = getDateValue(valueA); const dateB = getDateValue(valueB); if (dateA && dateB) { return dateA.getTime() - dateB.getTime(); } else if (dateA) { return 1; // consider non-null dates as greater than null/undefined } else if (dateB) { return -1; // consider non-null dates as greater than null/undefined } else { return 0; // both are null/undefined, considered equal } }; } }; }; this.setColDefProperty(col, 'comparator', comparatorGetter('comparator')); this.setColDefProperty(col, 'pivotComparator', comparatorGetter('pivotComparator')); } setupColumnGetFindText({ col, abColumn }) { this.setColDefProperty(col, 'getFindText', (userGetFindText) => { return (params) => { const gridCell = this.adaptableApi.gridApi.getGridCellFromRowNode(params.node, abColumn.columnId); if (!this.isCellSearchable(gridCell)) { return null; } const getCellSearchText = this.adaptableOptions.quickSearchOptions.getCellSearchText; if (getCellSearchText) { return this.getCellSearchText(gridCell); } return userGetFindText?.(params) ?? gridCell.displayValue; }; }); } getCellSearchText(gridCell) { const getCellSearchText = this.adaptableOptions.quickSearchOptions.getCellSearchText; if (getCellSearchText) { const quickSearchValue = this.adaptableApi.quickSearchApi.getQuickSearchValue(); const quickSearchContext = { ...this.adaptableApi.internalApi.buildBaseContext(), gridCell, quickSearchValue, }; return getCellSearchText(quickSearchContext); } return gridCell.displayValue; } isCellSearchable(gridCell) { const isCellSearchableFn = this.adaptableOptions.quickSearchOptions.isCellSearchable; if (!gridCell.column) { return false; } if (isCellSearchableFn) { const quickSearchValue = this.adaptableApi.quickSearchApi.getQuickSearchValue(); const quickSearchContext = { ...this.adaptableApi.internalApi.buildBaseContext(), gridCell, quickSearchValue, }; if (!isCellSearchableFn(quickSearchContext)) { return false; } } return true; } isQuickSearchActive(gridCell) { const isQuickSearchAvailable = this.adaptableApi.internalApi .getModuleService() .isAdapTableModulePresent('QuickSearch'); if (!isQuickSearchAvailable) { return false; } const quickSearchValue = this.adaptableApi.quickSearchApi.getQuickSearchValue(); if (!quickSearchValue) { return false; } if (!this.isCellSearchable(gridCell)) { return false; } let column = this.agGridApi.getColumn(gridCell.column.columnId); if (!column && this.adaptableApi.layoutApi.isCurrentLayoutPivot()) { column = this.agGridApi .getAllGridColumns() .find((col) => col.getColId() === gridCell.column.columnId); } if (!column) { return false; } const isServerSideRowModel = this.adaptableApi.gridApi.getAgGridRowModelType() === 'serverSide'; if (isServerSideRowModel) { const isCaseSensitive = this.adaptableOptions.quickSearchOptions.isQuickSearchCaseSensitive; const cellDisplayValue = String(this.getCellSearch