diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/data.ts b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/data.ts
new file mode 100644
index 00000000000..d3975ddec45
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/data.ts
@@ -0,0 +1,21 @@
+export const getData = () => [
+ { id: 'fr-paris', region: 'Europe', segment: 'Corporate', country: 'France', amount: 30 },
+ { id: 'fr-lyon', region: 'Europe', segment: 'Corporate', country: 'France', amount: 30 },
+ { id: 'de-berlin', region: 'Europe', segment: 'Corporate', country: 'Germany', amount: 35 },
+ { id: 'de-hamburg', region: 'Europe', segment: 'Corporate', country: 'Germany', amount: 25 },
+ { id: 'es-madrid', region: 'Europe', segment: 'Corporate', country: 'Spain', amount: 28 },
+ { id: 'es-barcelona', region: 'Europe', segment: 'Corporate', country: 'Spain', amount: 32 },
+ { id: 'it-rome', region: 'Europe', segment: 'Enterprise', country: 'Italy', amount: 40 },
+ { id: 'it-milan', region: 'Europe', segment: 'Enterprise', country: 'Italy', amount: 20 },
+ { id: 'pl-warsaw', region: 'Europe', segment: 'Enterprise', country: 'Poland', amount: 26 },
+ { id: 'pl-krakow', region: 'Europe', segment: 'Enterprise', country: 'Poland', amount: 24 },
+ { id: 'us-nyc', region: 'Americas', segment: 'Corporate', country: 'USA', amount: 70 },
+ { id: 'us-la', region: 'Americas', segment: 'Corporate', country: 'USA', amount: 30 },
+ { id: 'us-austin', region: 'Americas', segment: 'Corporate', country: 'USA', amount: 25 },
+ { id: 'ca-toronto', region: 'Americas', segment: 'Enterprise', country: 'Canada', amount: 35 },
+ { id: 'ca-vancouver', region: 'Americas', segment: 'Enterprise', country: 'Canada', amount: 25 },
+ { id: 'br-sao-paulo', region: 'Americas', segment: 'Enterprise', country: 'Brazil', amount: 30 },
+ { id: 'br-rio', region: 'Americas', segment: 'Enterprise', country: 'Brazil', amount: 22 },
+ { id: 'mx-tijuana', region: 'Americas', segment: 'Enterprise', country: 'Mexico', amount: 28 },
+ { id: 'mx-guadalajara', region: 'Americas', segment: 'Enterprise', country: 'Mexico', amount: 18 },
+];
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/example.spec.ts
new file mode 100644
index 00000000000..2c0eed06460
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/example.spec.ts
@@ -0,0 +1,8 @@
+import { ensureGridReady, test, waitForGridContent } from '@utils/grid/test-utils';
+
+test.agExample(import.meta, () => {
+ test.eachFramework('Example', async ({ page }) => {
+ await ensureGridReady(page);
+ await waitForGridContent(page);
+ });
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/index.html b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/index.html
new file mode 100644
index 00000000000..6c46dc75f2e
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/index.html
@@ -0,0 +1 @@
+
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/main.ts b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/main.ts
new file mode 100644
index 00000000000..bd448f2d54f
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/group-row-editable-totals/main.ts
@@ -0,0 +1,108 @@
+import type { GridOptions, GroupRowValueSetterFunc, ValueFormatterParams, ValueParserParams } from 'ag-grid-community';
+import {
+ ClientSideRowModelModule,
+ ModuleRegistry,
+ NumberFilterModule,
+ ValidationModule,
+ createGrid,
+} from 'ag-grid-community';
+import { RowGroupingModule, SetFilterModule } from 'ag-grid-enterprise';
+
+import { getData } from './data';
+
+interface SalesRecord {
+ id: string;
+ region: string;
+ segment: string;
+ country: string;
+ amount: number;
+}
+
+ModuleRegistry.registerModules([
+ RowGroupingModule,
+ ClientSideRowModelModule,
+ NumberFilterModule,
+ SetFilterModule,
+ ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
+]);
+
+const currencyFormatter = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ maximumFractionDigits: 0,
+});
+
+const amountValueFormatter = (params: ValueFormatterParams): string =>
+ typeof params.value === 'number' ? currencyFormatter.format(params.value) : params.value ?? '';
+
+const amountValueParser = (params: ValueParserParams): number | null => {
+ const numericValue = Number(params.newValue);
+ return Number.isFinite(numericValue) ? numericValue : params.oldValue ?? null;
+};
+
+const amountGroupRowValueSetter: GroupRowValueSetterFunc = ({ node, newValue, eventSource }) => {
+ const numericValue = Number(newValue);
+ if (!Number.isFinite(numericValue)) {
+ return false;
+ }
+
+ let result = false;
+ // distribute the new value equally amongst all filtered children
+ const children = node.childrenAfterSort;
+ if (children?.length) {
+ const perChild = numericValue / children.length;
+ for (const child of children) {
+ // If child is a leaf, setDataValue will update the underlying data item
+ // If child is a group, setDataValue will recursively call this value setter down the tree to update group values
+ if (child.setDataValue('amount', perChild, eventSource)) {
+ result = true;
+ }
+ }
+ }
+ return result;
+};
+
+const gridOptions: GridOptions = {
+ columnDefs: [
+ { field: 'region', rowGroup: true, hide: true },
+ { field: 'segment', rowGroup: true, hide: true, filter: 'agSetColumnFilter' },
+ { field: 'country', filter: 'agSetColumnFilter' },
+ {
+ headerName: 'Annual Budget',
+ field: 'amount',
+ aggFunc: 'sum',
+ editable: true,
+ groupRowEditable: true,
+ filter: 'agNumberColumnFilter',
+ valueParser: amountValueParser,
+ groupRowValueSetter: amountGroupRowValueSetter,
+ valueFormatter: amountValueFormatter,
+ },
+ ],
+ autoGroupColumnDef: {
+ minWidth: 260,
+ cellRendererParams: {
+ suppressCount: true,
+ },
+ },
+ defaultColDef: {
+ flex: 1,
+ sortable: true,
+ filter: true,
+ resizable: true,
+ },
+ rowData: getData(),
+ groupAggFiltering: true,
+ groupDefaultExpanded: -1,
+ animateRows: true,
+ getRowId: ({ data }) => data.id,
+};
+
+// setup the grid after the page has finished loading
+document.addEventListener('DOMContentLoaded', () => {
+ const gridDiv = document.querySelector('#myGrid');
+ if (!gridDiv) {
+ return;
+ }
+ createGrid(gridDiv, gridOptions);
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/index.mdoc b/documentation/ag-grid-docs/src/content/docs/grouping-edit/index.mdoc
index 85d4d629cb2..b47bd767f95 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-edit/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/index.mdoc
@@ -3,23 +3,65 @@ title: "Row Grouping - Editing Groups"
enterprise: true
---
-The Grid supports editing of grouped data when using the [Client-Side Row Model](./row-models/#client-side).
-When users edit values in grouped columns, the grid can automatically refresh the grouping hierarchy to reflect the changes.
+The grid supports editing grouped data when using the [Client-Side Row Model](./row-models/#client-side).
+This page explains how to keep the grouping hierarchy synchronised with edits and how to allow group rows themselves to be editable.
## Refreshing Groups After Editing
-When grouped columns are editable, enable `refreshAfterGroupEdit=true` to update the row data and regroup after each group field edit.
+When grouped columns are editable, set `refreshAfterGroupEdit=true` so the grid updates row data
+and recalculates the grouping after every committed edit.
{% gridExampleRunner title="Refresh After Group Edit" name="refresh-after-group-edit" exampleHeight=600 /%}
-In the example above, changing the `region` causes the grid to re-evaluate the grouping and move the row to the correct group instantly.
-Double click on a `region` cell to edit it.
+In the example above, changing the `region` causes the grid to re-evaluate the grouping and move
+the row to the correct group instantly. Double click on a `region` cell to edit it.
-By default, without `refreshAfterGroupEdit` enabled, the row data updates but the grouping does not get updated until the next full refresh.
+By default, without `refreshAfterGroupEdit` enabled, the row data updates but the grouping does not
+get updated until the next full refresh.
-When enabling `refreshAfterGroupEdit`, it is recommended to also set `getRowId` so that the grid can track rows by stable IDs while rebuilding the grouping hierarchy.
+When enabling `refreshAfterGroupEdit`, also provide `getRowId` so that the grid can track rows by
+stable IDs while rebuilding the grouping hierarchy.
-See also [Read Only Edit](./value-setters/#read-only-edit) for configuring immutable grouped data or connecting the grid with a store.
+See also [Read Only Edit](./value-setters/#read-only-edit) for configuring immutable grouped data or
+connecting the grid with a store.
+
+## Editing Group Row Cells
+
+Set `groupRowEditable` on any column that should accept edits on group nodes.
+Provide either a boolean or a callback; callbacks only run for nodes where `rowNode.group === true`,
+while leaf rows continue to honour `editable`.
+
+`groupRowValueSetter` mirrors the regular `valueSetter`, but it only fires for group rows. It runs
+whenever a group row cell changes through the UI, `rowNode.setDataValue`, or another API call,
+making it the right place to cascade edits to child nodes.
+
+Return `true` from the callback to inform the grid that the value changed and refresh is needed.
+
+Most grouped rows (and filler nodes in tree data) do not own `rowNode.data`, so their column
+`valueSetter` never runs even if `groupRowEditable` is enabled. Provide a `groupRowValueSetter`
+whenever the edit needs to persist or update aggregates; only group rows that own real data objects
+run the normal value pipeline.
+
+When a column defines both `groupRowEditable` and `editable`, AG Grid only evaluates the property
+that matches the current node type, enabling separate rules for group rows and leaves.
+
+## Custom Distribution of Edits
+
+In this example, `groupRowValueSetter` is used to distribute edits on group rows to their descendant rows.
+The example uses a simple strategy of dividing the value equally among all visible children and sub groups.
+
+{% gridExampleRunner title="Editable Group Totals" name="group-row-editable-totals" exampleHeight=620 /%}
+
+Key points:
+
+- Use `groupRowValueSetter` to redistribute committed values to descendants.
+ The callback receives the group row node and column.
+- Call `rowNode.setDataValue` on children to push the updated figures down. This triggers normal
+ aggregation to refresh parent totals and recurses further when a child is also a group that defines
+ its own `groupRowValueSetter`.
+- `rowNode.childrenAfterSort` contains the descendants in the node, groups or leaf rows.
+- Parent aggregates refresh automatically because child `data` changes re-run the column `aggFunc`,
+ so typing £600 into “Europe” instantly rebalances the filtered countries to reach that total.
## Next Up
Continue to the next section to learn [Row Dragging with Row Groups](./grouping-row-dragging/).
diff --git a/packages/ag-grid-community/src/columns/dataTypeService.ts b/packages/ag-grid-community/src/columns/dataTypeService.ts
index 8f1157c601a..79f90016b85 100644
--- a/packages/ag-grid-community/src/columns/dataTypeService.ts
+++ b/packages/ag-grid-community/src/columns/dataTypeService.ts
@@ -542,8 +542,8 @@ export class DataTypeService extends BeanStub implements NamedBean {
cellEditor: 'agCheckboxCellEditor',
cellRenderer: 'agCheckboxCellRenderer',
getFindText: () => null,
- suppressKeyboardEvent: (params: SuppressKeyboardEventParams) =>
- !!params.colDef.editable && params.event.key === KeyCode.SPACE,
+ suppressKeyboardEvent: ({ node, event, column }: SuppressKeyboardEventParams) =>
+ event.key === KeyCode.SPACE && column.isCellEditable(node),
};
},
date({ formatValue }) {
diff --git a/packages/ag-grid-community/src/edit/editService.ts b/packages/ag-grid-community/src/edit/editService.ts
index 8b60178af8a..4e38d80dbc9 100644
--- a/packages/ag-grid-community/src/edit/editService.ts
+++ b/packages/ag-grid-community/src/edit/editService.ts
@@ -718,9 +718,10 @@ export class EditService extends BeanStub implements NamedBean, IEditService {
}
public isCellEditable(position: Required, source: 'api' | 'ui' = 'ui'): boolean {
- const { rowNode } = position;
const { gos, beans } = this;
- if (rowNode.group) {
+
+ const rowNode = position.rowNode;
+ if (rowNode.group && position.column.getColDef().groupRowEditable == null) {
// This is a group - it could be a tree group or a grouping group...
if (gos.get('treeData')) {
// tree - allow editing of groups with data by default.
@@ -738,7 +739,7 @@ export class EditService extends BeanStub implements NamedBean, IEditService {
const isEditable =
getEditType(gos) === 'fullRow'
? isFullRowCellEditable(beans, position, source)
- : isCellEditable(beans, position, source);
+ : isCellEditable(beans, position);
if (isEditable) {
this.strategy ??= this.createStrategy();
diff --git a/packages/ag-grid-community/src/edit/strategy/strategyUtils.ts b/packages/ag-grid-community/src/edit/strategy/strategyUtils.ts
index 7d5dd80e621..e2dcd4fca23 100644
--- a/packages/ag-grid-community/src/edit/strategy/strategyUtils.ts
+++ b/packages/ag-grid-community/src/edit/strategy/strategyUtils.ts
@@ -66,17 +66,36 @@ function deriveClickCount(gos: GridOptionsService, colDef?: ColDef): number {
return 2;
}
-export function isCellEditable(
- beans: BeanCollection,
- { rowNode, column }: Required,
- _source: 'api' | 'ui' = 'ui'
-): boolean {
- const editable = column.getColDef().editable;
- const editModelSvc = beans.editModelSvc;
- return (
- (column as AgColumn).isColumnFunc(rowNode, editable) ||
- (!!editModelSvc && editModelSvc.hasEdits({ rowNode, column }, { withOpenEditor: true }))
- );
+function existingEditing(beans: BeanCollection, editPosition: Required): boolean {
+ return beans.editModelSvc?.hasEdits(editPosition, { withOpenEditor: true }) ?? false;
+}
+
+export function isCellEditable(beans: BeanCollection, editPosition: Required): boolean {
+ const column = editPosition.column as AgColumn;
+ const rowNode = editPosition.rowNode;
+ const colDef = column.getColDef();
+
+ if (!rowNode) {
+ return existingEditing(beans, editPosition);
+ }
+
+ const editable = colDef.editable;
+
+ if (rowNode.group) {
+ const groupRowEditable = colDef.groupRowEditable;
+ if (groupRowEditable != null) {
+ if (column.isColumnFunc(rowNode, groupRowEditable)) {
+ return true;
+ }
+ return existingEditing(beans, editPosition);
+ }
+ }
+
+ if (column.isColumnFunc(rowNode, editable)) {
+ return true;
+ }
+
+ return existingEditing(beans, editPosition);
}
export function isFullRowCellEditable(
@@ -84,13 +103,18 @@ export function isFullRowCellEditable(
position: Required,
source: 'api' | 'ui' = 'ui'
): boolean {
- const editable = isCellEditable(beans, position, source);
-
- if (editable === true || source === 'ui') {
+ const editable = isCellEditable(beans, position);
+ if (editable || source === 'ui') {
return editable;
}
// check if other cells in row are editable, so starting edit on uneditable cell will still work
- const columns = beans.colModel.getCols();
- return columns.some((col: AgColumn) => isCellEditable(beans, { rowNode: position.rowNode, column: col }, source));
+ const { rowNode, column } = position;
+ for (const col of beans.colModel.getCols()) {
+ if (col !== column && isCellEditable(beans, { rowNode, column: col })) {
+ return true;
+ }
+ }
+
+ return false;
}
diff --git a/packages/ag-grid-community/src/edit/utils/editors.ts b/packages/ag-grid-community/src/edit/utils/editors.ts
index 995fd0b05f7..825e039d077 100644
--- a/packages/ag-grid-community/src/edit/utils/editors.ts
+++ b/packages/ag-grid-community/src/edit/utils/editors.ts
@@ -548,7 +548,7 @@ function _columnDefsRequireValidation(columnDefs?: ColDef[]): boolean {
for (let i = 0, len = columnDefs.length; i < len; ++i) {
const colDef = columnDefs[i];
const params = colDef.cellEditorParams;
- if (!params || !colDef.editable) {
+ if (!params || (!colDef.editable && !colDef.groupRowEditable)) {
continue;
}
if (
diff --git a/packages/ag-grid-community/src/entities/colDef.ts b/packages/ag-grid-community/src/entities/colDef.ts
index 039c33ac8c5..5a31503f697 100644
--- a/packages/ag-grid-community/src/entities/colDef.ts
+++ b/packages/ag-grid-community/src/entities/colDef.ts
@@ -385,9 +385,21 @@ export interface ColDef extends AbstractColDef;
+ /**
+ * Works like `editable`, but is evaluated only for group rows. When provided, group rows use this property instead of `editable`.
+ * Set to `true` if this column is editable, otherwise `false`. Can also be a function to have different rows editable.
+ */
+ groupRowEditable?: boolean | GroupRowEditableCallback;
+ /**
+ * Runs after a group row value changes so custom code can push edits down to descendant rows.
+ * Fires for every `setDataValue` call when defined, regardless of `groupRowEditable`.
+ * Use this to mutate descendants directly; the grid always commits the group row value afterwards.
+ */
+ groupRowValueSetter?: GroupRowValueSetterFunc;
/** Function or expression. Sets the value into your data for saving. Return `true` if the data changed. */
valueSetter?: string | ValueSetterFunc;
/** Function or expression. Parses the value for saving. */
@@ -955,6 +967,28 @@ export interface EditableCallbackParams = (
params: EditableCallbackParams
) => boolean;
+export interface GroupRowEditableCallbackParams
+ extends ColumnFunctionCallbackParams {}
+export type GroupRowEditableCallback = (
+ params: GroupRowEditableCallbackParams
+) => boolean;
+export interface GroupRowValueSetterParams
+ extends Omit<
+ ChangedValueParams,
+ 'node' | 'data'
+ > {
+ /** Group row that triggered the callback. */
+ node: IRowNode;
+ /** Data associated with the group row. Undefined when the row does not own data. */
+ data?: TData | null;
+ /** Source string provided to `rowNode.setDataValue`. */
+ eventSource: string | undefined;
+ /** Whether the value actually changed. */
+ valueChanged: boolean;
+}
+export type GroupRowValueSetterFunc = (
+ params: GroupRowValueSetterParams
+) => void | boolean | undefined;
export interface SuppressPasteCallbackParams
extends ColumnFunctionCallbackParams {}
export type SuppressPasteCallback = (
diff --git a/packages/ag-grid-community/src/main.ts b/packages/ag-grid-community/src/main.ts
index 3749d552d9e..021b830c7ca 100644
--- a/packages/ag-grid-community/src/main.ts
+++ b/packages/ag-grid-community/src/main.ts
@@ -695,6 +695,10 @@ export {
ValueParserParams,
ValueSetterFunc,
ValueSetterParams,
+ GroupRowEditableCallback,
+ GroupRowEditableCallbackParams,
+ GroupRowValueSetterParams,
+ GroupRowValueSetterFunc,
} from './entities/colDef';
export {
BaseCellDataType,
diff --git a/packages/ag-grid-community/src/validation/rules/colDefValidations.ts b/packages/ag-grid-community/src/validation/rules/colDefValidations.ts
index fe66e185b69..9176daa8d4c 100644
--- a/packages/ag-grid-community/src/validation/rules/colDefValidations.ts
+++ b/packages/ag-grid-community/src/validation/rules/colDefValidations.ts
@@ -39,8 +39,9 @@ export const COLUMN_DEFINITION_MOD_VALIDATIONS: ModuleValidation {
- if (!editable) {
+ cellEditor: ({ cellEditor, editable, groupRowEditable }: ColDef) => {
+ const editingEnabled = !!editable || !!groupRowEditable;
+ if (!editingEnabled) {
return null;
}
if (typeof cellEditor === 'string') {
@@ -65,6 +66,12 @@ export const COLUMN_DEFINITION_MOD_VALIDATIONS: ModuleValidation {
+ if (groupRowEditable && !cellEditor) {
+ return 'TextEditor';
+ }
+ return null;
+ },
enableCellChangeFlash: 'HighlightChanges',
enablePivot: 'SharedPivot',
enableRowGroup: 'SharedRowGrouping',
@@ -253,6 +260,7 @@ const COLUMN_DEFINITION_VALIDATIONS: () => Validations = (
spanRows: {
dependencies: {
editable: { required: [false, undefined] },
+ groupRowEditable: { required: [false, undefined] },
rowDrag: { required: [false, undefined] },
colSpan: { required: [undefined] },
rowSpan: { required: [undefined] },
@@ -357,6 +365,8 @@ const colDefPropertyMap: Record = {
initialAggFunc: undefined,
defaultAggFunc: undefined,
aggFunc: undefined,
+ groupRowEditable: undefined,
+ groupRowValueSetter: undefined,
pinned: undefined,
initialPinned: undefined,
chartDataType: undefined,
diff --git a/packages/ag-grid-community/src/valueService/valueService.ts b/packages/ag-grid-community/src/valueService/valueService.ts
index f92c760befd..07854a0f84b 100644
--- a/packages/ag-grid-community/src/valueService/valueService.ts
+++ b/packages/ag-grid-community/src/valueService/valueService.ts
@@ -8,6 +8,7 @@ import { BeanStub } from '../context/beanStub';
import type { BeanCollection } from '../context/context';
import type { AgColumn } from '../entities/agColumn';
import type {
+ ColDef,
KeyCreatorParams,
ValueFormatterParams,
ValueGetterParams,
@@ -401,60 +402,99 @@ export class ValueService extends BeanStub implements NamedBean {
if (!rowNode || !column) {
return false;
}
- this.ensureRowData(rowNode);
const colDef = column.getColDef();
+
+ if (!rowNode.data && this.canCreateRowNodeData(rowNode, colDef)) {
+ rowNode.data = {}; // enableGroupEdit allows editing group rows without data.
+ }
+
if (!this.isSetValueSupported({ column, newValue, colDef })) {
return false;
}
+ const oldValue = this.getValue(column, rowNode, undefined, eventSource);
+
const params: ValueSetterParams = _addGridCommonParams(this.gos, {
node: rowNode,
data: rowNode.data,
- oldValue: this.getValue(column, rowNode, undefined, eventSource),
+ oldValue,
newValue: newValue,
colDef,
column: column,
});
- params.newValue = newValue;
+ const groupRowValueSetter = rowNode.group ? colDef.groupRowValueSetter : undefined;
- const externalFormulaResult = this.handleExternalFormulaChange({
- column,
- eventSource,
- newValue,
- setterParams: params,
- rowNode,
- });
- if (externalFormulaResult !== null) {
- return externalFormulaResult;
+ let valueSetterChanged = false;
+ let groupRowValueSetterChanged = false;
+
+ if (rowNode.data) {
+ const externalFormulaResult = this.handleExternalFormulaChange({
+ column,
+ eventSource,
+ newValue,
+ setterParams: params,
+ rowNode,
+ });
+ if (externalFormulaResult !== null) {
+ return externalFormulaResult;
+ }
+
+ const result = this.computeValueChange({
+ column,
+ rowNode,
+ newValue,
+ params,
+ rowData: rowNode.data,
+ valueSetter: colDef.valueSetter,
+ field: colDef.field,
+ });
+
+ // default to true if user forgot to return a value (possible without TypeScript)
+ valueSetterChanged = result ?? true;
}
- let valueWasDifferent = this.computeValueChange({
- column,
- newValue,
- params,
- rowData: rowNode.data,
- valueSetter: colDef.valueSetter,
- field: colDef.field,
- });
+ if (groupRowValueSetter) {
+ const result = groupRowValueSetter(
+ _addGridCommonParams(this.gos, {
+ node: rowNode,
+ data: rowNode.data,
+ oldValue,
+ newValue,
+ colDef,
+ column,
+ eventSource,
+ valueChanged: valueSetterChanged || newValue !== oldValue,
+ })
+ );
- // in case user forgot to return something (possible if they are not using TypeScript
- // and just forgot we default the return value to true, so we always refresh.
- if (valueWasDifferent === undefined) {
- valueWasDifferent = true;
+ // default to true if user forgot to return a value (possible without TypeScript)
+ groupRowValueSetterChanged = result ?? true;
}
- // if no change to the value, then no need to do the updating, or notifying via events.
- // otherwise the user could be tabbing around the grid, and cellValueChange would get called
- // all the time.
- if (!valueWasDifferent) {
+ if (!valueSetterChanged && !groupRowValueSetterChanged) {
+ // if no change to the value, then no need to do the updating, or notifying via events.
+ // otherwise the user could be tabbing around the grid, and cellValueChange would get called
+ // all the time.
return false;
}
return this.finishValueChange(rowNode, column, params, eventSource);
}
+ private canCreateRowNodeData(rowNode: IRowNode, colDef: ColDef): boolean {
+ if (!rowNode.group) {
+ return true; // not a group row
+ }
+
+ if (colDef.groupRowEditable != null || colDef.groupRowValueSetter != null) {
+ return false; // Do not create the row data for a group row automatically
+ }
+
+ return true; // create the rowData for groupRowEditable (default legacy behaviour)
+ }
+
private finishValueChange(
rowNode: IRowNode,
column: AgColumn,
@@ -476,13 +516,6 @@ export class ValueService extends BeanStub implements NamedBean {
return true;
}
- private ensureRowData(rowNode: IRowNode): void {
- // enableGroupEdit allows editing group rows without data.
- if (_missing(rowNode.data)) {
- rowNode.data = {};
- }
- }
-
private isSetValueSupported(params: {
column: AgColumn;
newValue: any;
@@ -540,6 +573,7 @@ export class ValueService extends BeanStub implements NamedBean {
const computedParams: ValueSetterParams = { ...setterParams, newValue: computedValue };
this.computeValueChange({
column,
+ rowNode,
newValue: computedValue,
params: computedParams,
rowData: rowNode.data,
@@ -563,6 +597,7 @@ export class ValueService extends BeanStub implements NamedBean {
params: ValueSetterParams;
rowData: any;
field: string | undefined;
+ rowNode: IRowNode;
column: AgColumn;
newValue: any;
}): boolean | undefined {
@@ -575,7 +610,7 @@ export class ValueService extends BeanStub implements NamedBean {
return this.expressionSvc?.evaluate(valueSetter, setterParams);
}
- return this.setValueUsingField(rowData, field, newValue, column.isFieldContainsDots());
+ return !!rowData && this.setValueUsingField(rowData, field, newValue, column.isFieldContainsDots());
}
private dispatchCellValueChangedEvent(
diff --git a/packages/ag-grid-enterprise/src/rowHierarchy/groupEditService.ts b/packages/ag-grid-enterprise/src/rowHierarchy/groupEditService.ts
index 016dfd28d9f..3075f3e76de 100644
--- a/packages/ag-grid-enterprise/src/rowHierarchy/groupEditService.ts
+++ b/packages/ag-grid-enterprise/src/rowHierarchy/groupEditService.ts
@@ -462,7 +462,7 @@ export class GroupEditService extends BeanStub implements _IGroupEditService {
if (maxLevel < 0) {
return false;
}
- const { valueSvc, editSvc } = this.beans;
+ const { valueSvc } = this.beans;
let changed = false;
for (let level = 0; level < columns.length; ++level) {
const column = columns[level];
@@ -479,8 +479,7 @@ export class GroupEditService extends BeanStub implements _IGroupEditService {
if (parsedValue !== undefined) {
valueToSet = parsedValue;
}
- const result = editSvc?.setDataValue({ rowNode: row, column }, valueToSet, 'rowDrag');
- const updated = result != null ? !!result : row.setDataValue(column, valueToSet, 'rowDrag');
+ const updated = row.setDataValue(column, valueToSet, 'rowDrag');
if (updated) {
changed = true;
}
diff --git a/testing/behavioural/src/cell-editing/cell-editing-refresh-after-group-edit.test.ts b/testing/behavioural/src/cell-editing/cell-editing-refresh-after-group-edit.test.ts
index fa514cbcab1..ca2412ce6af 100644
--- a/testing/behavioural/src/cell-editing/cell-editing-refresh-after-group-edit.test.ts
+++ b/testing/behavioural/src/cell-editing/cell-editing-refresh-after-group-edit.test.ts
@@ -1,6 +1,6 @@
import { userEvent } from '@testing-library/user-event';
-import { ClientSideRowModelModule } from 'ag-grid-community';
+import { ClientSideRowModelModule, UndoRedoEditModule } from 'ag-grid-community';
import type { GridOptions } from 'ag-grid-community';
import { BatchEditModule, RowGroupingModule } from 'ag-grid-enterprise';
@@ -8,7 +8,7 @@ import { GridRows, TestGridsManager, asyncSetTimeout, waitForInput } from '../te
describe('cell editing with refreshAfterGroupEdit', () => {
const gridsManager = new TestGridsManager({
- modules: [ClientSideRowModelModule, RowGroupingModule, BatchEditModule],
+ modules: [ClientSideRowModelModule, RowGroupingModule, BatchEditModule, UndoRedoEditModule],
});
beforeEach(() => {
@@ -151,4 +151,70 @@ describe('cell editing with refreshAfterGroupEdit', () => {
expect(api.getRowNode('2')?.parent?.key).toBe('B');
expect(api.getRowNode('3')?.parent?.key).toBe('A');
});
+
+ test('aggregation columns refresh when rows move', async () => {
+ const gridOptions: GridOptions = {
+ animateRows: true,
+ columnDefs: [
+ { field: 'group', rowGroup: true, editable: true },
+ { field: 'value', aggFunc: 'sum' },
+ ],
+ autoGroupColumnDef: {
+ field: 'group',
+ cellRendererParams: {
+ suppressDoubleClickExpand: true,
+ },
+ },
+ rowData: [
+ { id: '1', group: 'A', value: 10 },
+ { id: '2', group: 'A', value: 15 },
+ { id: '3', group: 'B', value: 7 },
+ ],
+ refreshAfterGroupEdit: true,
+ undoRedoCellEditing: true,
+ enableGroupEdit: true,
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data.id,
+ };
+
+ const api = gridsManager.createGrid('cell-edit-refresh-group-aggregation', gridOptions);
+ await asyncSetTimeout(0);
+
+ const gridDiv = TestGridsManager.getHTMLElement(api)!;
+ const editGroupCell = async (rowId: string, value: string) => {
+ const cell = gridDiv.querySelector(`[row-id="${rowId}"] [col-id="group"]`);
+ expect(cell).not.toBeNull();
+
+ await userEvent.dblClick(cell!);
+ const input = await waitForInput(gridDiv, cell!);
+ await userEvent.clear(input);
+ await userEvent.type(input, `${value}{Enter}`);
+ await asyncSetTimeout(0);
+ };
+
+ const initialSnapshot = `
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP id:row-group-group-A ag-Grid-AutoColumn:"A" value:25
+ │ ├── LEAF id:1 ag-Grid-AutoColumn:"A" group:"A" value:10
+ │ └── LEAF id:2 ag-Grid-AutoColumn:"A" group:"A" value:15
+ └─┬ LEAF_GROUP id:row-group-group-B ag-Grid-AutoColumn:"B" value:7
+ · └── LEAF id:3 ag-Grid-AutoColumn:"B" group:"B" value:7
+ `;
+
+ const afterEditSnapshot = `
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP id:row-group-group-A ag-Grid-AutoColumn:"A" value:10
+ │ └── LEAF id:1 ag-Grid-AutoColumn:"A" group:"A" value:10
+ └─┬ LEAF_GROUP id:row-group-group-B ag-Grid-AutoColumn:"B" value:22
+ · ├── LEAF id:2 ag-Grid-AutoColumn:"B" group:"B" value:15
+ · └── LEAF id:3 ag-Grid-AutoColumn:"B" group:"B" value:7
+ `;
+
+ await new GridRows(api, 'aggregation initial', { useFormatter: false }).check(initialSnapshot);
+
+ await editGroupCell('2', 'B');
+ await asyncSetTimeout(2);
+
+ await new GridRows(api, 'aggregation after edit', { useFormatter: false }).check(afterEditSnapshot);
+ });
});
diff --git a/testing/behavioural/src/cell-editing/group-edit/group-edit-test-utils.ts b/testing/behavioural/src/cell-editing/group-edit/group-edit-test-utils.ts
new file mode 100644
index 00000000000..4df97173b5c
--- /dev/null
+++ b/testing/behavioural/src/cell-editing/group-edit/group-edit-test-utils.ts
@@ -0,0 +1,116 @@
+import { userEvent } from '@testing-library/user-event';
+
+import type { ColDef, GridApi, IRowNode } from 'ag-grid-community';
+import { AllCommunityModule, ClientSideRowModelModule, UndoRedoEditModule } from 'ag-grid-community';
+import { RowGroupingModule, SetFilterModule, TreeDataModule } from 'ag-grid-enterprise';
+
+import { TestGridsManager, asyncSetTimeout, waitForInput } from '../../test-utils';
+import { expect } from '../../test-utils/matchers';
+
+export const gridsManager = new TestGridsManager({
+ modules: [
+ AllCommunityModule,
+ ClientSideRowModelModule,
+ RowGroupingModule,
+ TreeDataModule,
+ UndoRedoEditModule,
+ SetFilterModule,
+ ],
+});
+
+export const EDIT_MODES = ['ui', 'setDataValue'] as const;
+
+export type EditableCallback = Exclude, boolean>;
+export type GroupRowEditableCallback = Exclude, boolean>;
+export type GroupRowValueSetterCallback = Extract, (...args: any[]) => any>;
+export type ValueSetterCallback = Extract, (...args: any[]) => any>;
+export type ValueParserCallback = Extract, (...args: any[]) => any>;
+
+function locateCellElements(api: GridApi, rowNode: IRowNode, colId: string) {
+ const gridDiv = TestGridsManager.getHTMLElement(api);
+ expect(gridDiv).not.toBeNull();
+
+ const rowId = rowNode.id;
+ expect(rowId).toBeDefined();
+
+ const rowIndex = rowNode.rowIndex;
+ expect(rowIndex).not.toBeNull();
+
+ let cell = gridDiv!.querySelector(`[row-id="${rowId}"] [col-id="${colId}"]`);
+ if (!cell && rowIndex != null) {
+ const rowElement = gridDiv!.querySelector(`.ag-row[aria-rowindex="${rowIndex + 1}"]`);
+ cell = rowElement?.querySelector(`[col-id="${colId}"]`) ?? null;
+ }
+ expect(cell).not.toBeNull();
+
+ return { gridDiv: gridDiv!, cell: cell!, rowIndex: rowIndex! };
+}
+
+export async function editCell(api: GridApi, rowNode: IRowNode, colId: string, newValue: string) {
+ const { gridDiv, cell, rowIndex } = locateCellElements(api, rowNode, colId);
+
+ await userEvent.click(cell);
+
+ api.setFocusedCell(rowIndex, colId);
+ api.startEditingCell({ rowIndex, rowPinned: rowNode.rowPinned, colKey: colId });
+
+ const input = await waitForInput(gridDiv, cell ?? gridDiv);
+ await userEvent.clear(input);
+ await userEvent.type(input, `${newValue}{Enter}`);
+ await asyncSetTimeout(0);
+
+ return cell;
+}
+
+export function getGroupColumnDisplayValue(rowNode: IRowNode): string | undefined {
+ const groupValue = rowNode.groupData?.group;
+ if (groupValue !== undefined) {
+ return groupValue;
+ }
+ const data = rowNode.data as { label?: string } | undefined;
+ return data?.label;
+}
+
+export type CallbackArgs =
+ | Parameters
+ | Parameters
+ | Parameters;
+
+export function callsForRowNode(calls: CallbackArgs[], rowId?: string | null) {
+ if (!rowId) {
+ return [] as CallbackArgs[];
+ }
+ return calls.filter(([params]) => params?.node?.id === rowId);
+}
+
+export function createGroupRowData() {
+ return [
+ { id: 'fr-paris', region: 'Europe', country: 'France', amount: 30 },
+ { id: 'fr-lyon', region: 'Europe', country: 'France', amount: 30 },
+ { id: 'de-berlin', region: 'Europe', country: 'Germany', amount: 30 },
+ { id: 'de-hamburg', region: 'Europe', country: 'Germany', amount: 30 },
+ { id: 'it-rome', region: 'Europe', country: 'Italy', amount: 30 },
+ { id: 'it-milan', region: 'Europe', country: 'Italy', amount: 30 },
+ { id: 'us-nyc', region: 'Americas', country: 'USA', amount: 70 },
+ { id: 'us-la', region: 'Americas', country: 'USA', amount: 30 },
+ { id: 'ca-toronto', region: 'Americas', country: 'Canada', amount: 35 },
+ { id: 'ca-vancouver', region: 'Americas', country: 'Canada', amount: 25 },
+ ];
+}
+
+export const cascadeGroupRowValueSetter: GroupRowValueSetterCallback = ({ node, column, newValue, eventSource }) => {
+ const numericValue = Number(newValue);
+ if (!Number.isFinite(numericValue)) {
+ return;
+ }
+
+ const children = node.childrenAfterSort;
+ if (children) {
+ const perChild = numericValue / children.length;
+ for (const child of children) {
+ child.setDataValue(column, perChild, eventSource);
+ }
+ }
+};
+
+export { asyncSetTimeout };
diff --git a/testing/behavioural/src/cell-editing/group-edit/group-row-editable-aggregation.test.ts b/testing/behavioural/src/cell-editing/group-edit/group-row-editable-aggregation.test.ts
new file mode 100644
index 00000000000..5f017f90321
--- /dev/null
+++ b/testing/behavioural/src/cell-editing/group-edit/group-row-editable-aggregation.test.ts
@@ -0,0 +1,364 @@
+import type { NumberFilterModel, SetFilterModel } from 'ag-grid-community';
+
+import { GridRows } from '../../test-utils';
+import { expect } from '../../test-utils/matchers';
+import {
+ EDIT_MODES,
+ asyncSetTimeout,
+ cascadeGroupRowValueSetter,
+ createGroupRowData as createRowData,
+ editCell,
+ gridsManager,
+} from './group-edit-test-utils';
+
+afterEach(() => {
+ gridsManager.reset();
+});
+
+describe.each(EDIT_MODES)('groupRowEditable cascading edits (%s)', (editMode) => {
+ const baselineSnapshot = `
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-region-Europe amount:180
+ │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:60
+ │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:30
+ │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:30
+ │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:60
+ │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
+ │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
+ │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:60
+ │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:30
+ │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:30
+ └─┬ filler id:row-group-region-Americas amount:160
+ · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:100
+ · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:70
+ · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:30
+ · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:60
+ · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
+ · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
+ `;
+
+ test('group edits cascade through descendants and refresh aggregations', async () => {
+ const rowData = createRowData();
+
+ const api = await gridsManager.createGridAndWait('group-row-editable-changed-path', {
+ defaultColDef: {
+ cellEditor: 'agTextCellEditor',
+ },
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ cellRenderer: 'agGroupCellRenderer',
+ },
+ { field: 'region', rowGroup: true, hide: true },
+ { field: 'country', rowGroup: true, hide: true },
+ {
+ colId: 'amount',
+ field: 'amount',
+ aggFunc: 'sum',
+ editable: true,
+ groupRowEditable: true,
+ groupRowValueSetter: cascadeGroupRowValueSetter,
+ },
+ ],
+ rowData,
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data?.id,
+ });
+
+ await new GridRows(api, 'before edit').check(baselineSnapshot);
+
+ const europeNode = api.getRowNode('row-group-region-Europe');
+ expect(europeNode).toBeDefined();
+ expect(europeNode!.data).toBeUndefined();
+
+ const amountColId = 'amount';
+ const targetValue = 600;
+
+ if (editMode === 'ui') {
+ await editCell(api, europeNode!, amountColId, `${targetValue}`);
+ } else {
+ europeNode!.setDataValue(amountColId, targetValue, 'ui');
+ await asyncSetTimeout(0);
+ }
+ await asyncSetTimeout(0);
+
+ expect(europeNode!.data).toBeUndefined();
+
+ const afterEditSnapshot = `
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-region-Europe amount:600
+ │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:200
+ │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:100
+ │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:100
+ │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:200
+ │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:100
+ │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:100
+ │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:200
+ │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:100
+ │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:100
+ └─┬ filler id:row-group-region-Americas amount:160
+ · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:100
+ · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:70
+ · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:30
+ · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:60
+ · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
+ · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
+ `;
+ await new GridRows(api, 'after edit').check(afterEditSnapshot);
+
+ if (editMode === 'ui') {
+ api.undoCellEditing();
+ await asyncSetTimeout(0);
+ await new GridRows(api, 'after undo').check(baselineSnapshot);
+ expect(europeNode!.aggData?.amount ?? 0).toBe(180);
+ }
+ });
+
+ test('editing a single leaf updates its parent aggregations', async () => {
+ const rowData = createRowData();
+
+ const api = await gridsManager.createGridAndWait('group-row-editable-leaf-edit', {
+ defaultColDef: {
+ cellEditor: 'agTextCellEditor',
+ },
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ cellRenderer: 'agGroupCellRenderer',
+ },
+ { field: 'region', rowGroup: true, hide: true },
+ { field: 'country', rowGroup: true, hide: true },
+ {
+ colId: 'amount',
+ field: 'amount',
+ aggFunc: 'sum',
+ editable: true,
+ groupRowEditable: true,
+ groupRowValueSetter: cascadeGroupRowValueSetter,
+ },
+ ],
+ rowData,
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data?.id,
+ });
+
+ await new GridRows(api, 'before leaf edit').check(baselineSnapshot);
+
+ const parisNode = api.getRowNode('fr-paris');
+ expect(parisNode).toBeDefined();
+
+ const amountColId = 'amount';
+ if (editMode === 'ui') {
+ await editCell(api, parisNode!, amountColId, '45');
+ } else {
+ parisNode!.setDataValue(amountColId, 45, 'ui');
+ await asyncSetTimeout(0);
+ }
+ await asyncSetTimeout(0);
+
+ const snapshotAfterLeafEdit = `
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-region-Europe amount:195
+ │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:75
+ │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:45
+ │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:30
+ │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:60
+ │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
+ │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
+ │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:60
+ │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:30
+ │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:30
+ └─┬ filler id:row-group-region-Americas amount:160
+ · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:100
+ · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:70
+ · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:30
+ · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:60
+ · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
+ · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
+ `;
+ await new GridRows(api, 'after leaf edit').check(snapshotAfterLeafEdit);
+
+ await asyncSetTimeout(0);
+
+ const europeNode = api.getRowNode('row-group-region-Europe');
+ expect(europeNode?.data).toBeUndefined();
+ expect(europeNode?.aggData?.amount ?? 0).toBe(195);
+
+ const franceGroupNode = api.getRowNode('row-group-region-Europe-country-France');
+ expect(franceGroupNode?.aggData?.amount ?? 0).toBe(75);
+
+ expect(api.getRowNode('fr-paris')?.data?.amount).toBe(45);
+ expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(30);
+ });
+
+ test('group edits over filtered groups only adjust filtered descendants', async () => {
+ const rowData = createRowData();
+
+ const api = await gridsManager.createGridAndWait('group-row-editable-filtered', {
+ defaultColDef: {
+ cellEditor: 'agTextCellEditor',
+ },
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ groupAggFiltering: true,
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ cellRenderer: 'agGroupCellRenderer',
+ },
+ { field: 'region', rowGroup: true, hide: true },
+ { field: 'country', rowGroup: true, hide: true, filter: 'agSetColumnFilter' },
+ {
+ colId: 'amount',
+ field: 'amount',
+ aggFunc: 'sum',
+ editable: true,
+ groupRowEditable: true,
+ filter: 'agNumberColumnFilter',
+ groupRowValueSetter: cascadeGroupRowValueSetter,
+ },
+ ],
+ rowData,
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data?.id,
+ });
+
+ const filterModel: Record = {
+ country: {
+ filterType: 'set',
+ values: ['France', 'Germany'],
+ } as SetFilterModel,
+ amount: {
+ filterType: 'number',
+ type: 'greaterThan',
+ filter: 100,
+ } as NumberFilterModel,
+ };
+ api.setFilterModel(filterModel);
+ await asyncSetTimeout(0);
+
+ const filteredSnapshotBeforeEdit = `
+ ROOT id:ROOT_NODE_ID
+ └─┬ filler id:row-group-region-Europe amount:180
+ · ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:60
+ · │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:30
+ · │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:30
+ · └─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:60
+ · · ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
+ · · └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
+ `;
+ await new GridRows(api, 'after applying filters').check(filteredSnapshotBeforeEdit);
+
+ const europeNode = api.getRowNode('row-group-region-Europe');
+ expect(europeNode).toBeDefined();
+ expect(europeNode!.data).toBeUndefined();
+
+ const amountColId = 'amount';
+ if (editMode === 'ui') {
+ await editCell(api, europeNode!, amountColId, '240');
+ } else {
+ europeNode!.setDataValue(amountColId, 240, 'ui');
+ await asyncSetTimeout(0);
+ }
+ await asyncSetTimeout(0);
+
+ const filteredSnapshotAfterEdit = `
+ ROOT id:ROOT_NODE_ID
+ └─┬ filler id:row-group-region-Europe amount:300
+ · ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:120
+ · │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:60
+ · │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:60
+ · └─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:120
+ · · ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:60
+ · · └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:60
+ `;
+ // Aggregated value still reflects hidden Italy nodes even though the filter hides them.
+ await new GridRows(api, 'after filtered edit').check(filteredSnapshotAfterEdit);
+ expect(api.getRowNode('it-rome')?.data?.amount).toBe(30);
+ expect(api.getRowNode('it-milan')?.data?.amount).toBe(30);
+
+ api.setFilterModel(null);
+ await asyncSetTimeout(0);
+
+ const fullSnapshotAfterClearing = `
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-region-Europe amount:300
+ │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:120
+ │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:60
+ │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:60
+ │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:120
+ │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:60
+ │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:60
+ │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:60
+ │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:30
+ │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:30
+ └─┬ filler id:row-group-region-Americas amount:160
+ · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:100
+ · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:70
+ · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:30
+ · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:60
+ · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
+ · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
+ `;
+ await new GridRows(api, 'after clearing filters').check(fullSnapshotAfterClearing);
+
+ api.setFilterModel(filterModel);
+ await asyncSetTimeout(0);
+ await new GridRows(api, 'after reapplying filters').check(filteredSnapshotAfterEdit);
+ });
+
+ test('groupRowValueSetter returning false cancels the edit', async () => {
+ const rowData = createRowData();
+
+ const api = await gridsManager.createGridAndWait('group-row-editable-cancelled', {
+ defaultColDef: {
+ cellEditor: 'agTextCellEditor',
+ },
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ cellRenderer: 'agGroupCellRenderer',
+ },
+ { field: 'region', rowGroup: true, hide: true },
+ { field: 'country', rowGroup: true, hide: true },
+ {
+ colId: 'amount',
+ field: 'amount',
+ aggFunc: 'sum',
+ editable: true,
+ groupRowEditable: true,
+ groupRowValueSetter: () => false,
+ },
+ ],
+ rowData,
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data?.id,
+ });
+
+ await new GridRows(api, 'before cancelled edit').check(baselineSnapshot);
+
+ const europeNode = api.getRowNode('row-group-region-Europe');
+ expect(europeNode).toBeDefined();
+
+ const amountColId = 'amount';
+ if (editMode === 'ui') {
+ await editCell(api, europeNode!, amountColId, '999');
+ } else {
+ europeNode!.setDataValue(amountColId, 999, 'ui');
+ await asyncSetTimeout(0);
+ }
+ await asyncSetTimeout(0);
+
+ await new GridRows(api, 'after cancelled edit').check(baselineSnapshot);
+ });
+});
diff --git a/testing/behavioural/src/cell-editing/group-edit/group-row-editable.test.ts b/testing/behavioural/src/cell-editing/group-edit/group-row-editable.test.ts
new file mode 100644
index 00000000000..2a9758d6a07
--- /dev/null
+++ b/testing/behavioural/src/cell-editing/group-edit/group-row-editable.test.ts
@@ -0,0 +1,508 @@
+import type { GridOptions, ValueParserParams } from 'ag-grid-community';
+
+import { expect } from '../../test-utils/matchers';
+import type {
+ EditableCallback,
+ GroupRowEditableCallback,
+ GroupRowValueSetterCallback,
+ ValueParserCallback,
+ ValueSetterCallback,
+} from './group-edit-test-utils';
+import {
+ EDIT_MODES,
+ asyncSetTimeout,
+ callsForRowNode,
+ editCell,
+ getGroupColumnDisplayValue,
+ gridsManager,
+} from './group-edit-test-utils';
+
+afterEach(() => {
+ gridsManager.reset();
+});
+
+describe.each(EDIT_MODES)('groupRowEditable behaviour (%s)', (editMode) => {
+ test('row grouping group rows only invoke groupRowEditable', async () => {
+ const groupRowEditableCalls: Parameters[] = [];
+ const groupRowEditable: GroupRowEditableCallback = (...args) => {
+ groupRowEditableCalls.push(args);
+ return true;
+ };
+ const editableCalls: Parameters[] = [];
+ const editable: EditableCallback = (...args) => {
+ editableCalls.push(args);
+ return true;
+ };
+ const committedValues = new Map();
+ const valueSetterCalls: Parameters[] = [];
+ const valueSetter: ValueSetterCallback = (params) => {
+ valueSetterCalls.push([params]);
+ if (params.node?.id) {
+ committedValues.set(params.node.id, params.newValue);
+ }
+ if (params.data && params.colDef.field) {
+ (params.data as Record)[params.colDef.field] = params.newValue;
+ } else if (params.node?.groupData) {
+ params.node.groupData.group = params.newValue;
+ }
+ return true;
+ };
+
+ const gridOptions: GridOptions = {
+ defaultColDef: {
+ cellEditor: 'agTextCellEditor',
+ },
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ field: 'label',
+ cellRenderer: 'agGroupCellRenderer',
+ cellRendererParams: {
+ suppressCount: true,
+ },
+ editable,
+ groupRowEditable,
+ valueSetter,
+ },
+ { field: 'category', rowGroup: true, hide: true },
+ ],
+ rowData: [
+ { id: 'a-1', category: 'A', label: 'A1' },
+ { id: 'a-2', category: 'A', label: 'A2' },
+ ],
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data.id,
+ };
+
+ const api = await gridsManager.createGridAndWait('row-group-groupRowEditable', gridOptions);
+
+ const groupRowNode = api.getDisplayedRowAtIndex(0);
+ expect(groupRowNode).toBeDefined();
+ expect(groupRowNode!.group).toBe(true);
+ expect(groupRowNode!.data).toBeUndefined();
+ const originalGroupValue = getGroupColumnDisplayValue(groupRowNode!);
+
+ groupRowEditableCalls.length = 0;
+ editableCalls.length = 0;
+ valueSetterCalls.length = 0;
+ const groupColumn = api.getDisplayedCenterColumns()[0]!;
+ expect(groupColumn.getColDef().groupRowEditable).toBe(groupRowEditable);
+ expect(groupColumn.isCellEditable(groupRowNode!)).toBe(true);
+ const groupColId = groupColumn.getColId();
+ if (editMode === 'ui') {
+ await editCell(api, groupRowNode!, groupColId, 'Edited Group');
+ } else {
+ groupRowNode!.setDataValue(groupColId, 'Edited Group', 'ui');
+ await asyncSetTimeout(0);
+ }
+ expect(groupRowNode!.data).toBeUndefined();
+
+ const groupRowEditableCallsForGroup = callsForRowNode(groupRowEditableCalls, groupRowNode!.id);
+ const editableCallsForGroup = callsForRowNode(editableCalls, groupRowNode!.id);
+ if (editMode === 'ui') {
+ expect(groupRowEditableCallsForGroup.length).toBeGreaterThan(0);
+ }
+ expect(editableCallsForGroup.length).toBe(0);
+
+ if (editMode === 'ui') {
+ api.undoCellEditing();
+ await asyncSetTimeout(0);
+ expect(getGroupColumnDisplayValue(groupRowNode!)).toBe(originalGroupValue);
+ expect(groupRowNode!.data).toBeUndefined();
+ expect(committedValues.get(groupRowNode!.id!)).toBe(originalGroupValue);
+ }
+
+ const leafRowNode = api.getRowNode('a-1');
+ expect(leafRowNode).toBeDefined();
+ const originalLeafLabel = leafRowNode!.data!.label;
+
+ groupRowEditableCalls.length = 0;
+ editableCalls.length = 0;
+ valueSetterCalls.length = 0;
+ if (editMode === 'ui') {
+ await editCell(api, leafRowNode!, groupColId, 'Edited Leaf');
+ } else {
+ leafRowNode!.setDataValue(groupColId, 'Edited Leaf', 'ui');
+ await asyncSetTimeout(0);
+ }
+ expect(leafRowNode!.data!.label).toBe('Edited Leaf');
+
+ const groupRowEditableCallsForLeaf = callsForRowNode(groupRowEditableCalls, leafRowNode!.id);
+ expect(groupRowEditableCallsForLeaf.length).toBe(0);
+ const editableCallsForLeaf = callsForRowNode(editableCalls, leafRowNode!.id);
+ if (editMode === 'ui') {
+ expect(editableCallsForLeaf.length).toBeGreaterThan(0);
+ }
+ const valueSetterCallsForLeaf = callsForRowNode(valueSetterCalls, leafRowNode!.id);
+ expect(valueSetterCallsForLeaf.length).toBeGreaterThan(0);
+ expect(committedValues.get('a-1')).toBe('Edited Leaf');
+ if (editMode === 'ui') {
+ api.undoCellEditing();
+ await asyncSetTimeout(0);
+ expect(leafRowNode!.data!.label).toBe(originalLeafLabel);
+ expect(committedValues.get('a-1')).toBe(originalLeafLabel);
+ }
+ });
+
+ test('group row edits run valueParser before valueSetter', async () => {
+ const parserCalls: ValueParserParams[] = [];
+ const parserOutputs: string[] = [];
+ const valueParser: ValueParserCallback = (params) => {
+ parserCalls.push(params);
+ const parsed = String(params.newValue ?? '')
+ .trim()
+ .toUpperCase();
+ parserOutputs.push(parsed);
+ return parsed;
+ };
+ const valueSetterValues: string[] = [];
+ const valueSetter: ValueSetterCallback = (params) => {
+ if (typeof params.newValue === 'string') {
+ valueSetterValues.push(params.newValue);
+ }
+ if (params.data && params.colDef.field) {
+ (params.data as Record)[params.colDef.field] = params.newValue;
+ } else if (params.node?.groupData) {
+ params.node.groupData.group = params.newValue as string;
+ }
+ return true;
+ };
+
+ const gridOptions: GridOptions = {
+ defaultColDef: {
+ cellEditor: 'agTextCellEditor',
+ },
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ field: 'label',
+ cellRenderer: 'agGroupCellRenderer',
+ cellRendererParams: {
+ suppressCount: true,
+ },
+ editable: true,
+ groupRowEditable: true,
+ valueParser,
+ valueSetter,
+ },
+ { field: 'category', rowGroup: true, hide: true },
+ ],
+ rowData: [
+ { id: 'a-1', category: 'A', label: 'A1' },
+ { id: 'a-2', category: 'A', label: 'A2' },
+ ],
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data.id,
+ };
+
+ const api = await gridsManager.createGridAndWait('row-group-valueParser', gridOptions);
+
+ const groupRowNode = api.getDisplayedRowAtIndex(0);
+ expect(groupRowNode).toBeDefined();
+ const groupColumn = api.getDisplayedCenterColumns()[0]!;
+ const groupColId = groupColumn.getColId();
+ const rawInput = ' parsed group ';
+ const expectedParsed = 'PARSED GROUP';
+
+ parserCalls.length = 0;
+ parserOutputs.length = 0;
+ valueSetterValues.length = 0;
+
+ if (editMode === 'ui') {
+ await editCell(api, groupRowNode!, groupColId, rawInput);
+ expect(parserCalls.length).toBeGreaterThan(0);
+ expect(parserOutputs[parserOutputs.length - 1]).toBe(expectedParsed);
+ } else {
+ groupRowNode!.setDataValue(groupColId, rawInput, 'ui');
+ await asyncSetTimeout(0);
+ expect(parserCalls.length).toBe(0);
+ }
+ });
+
+ test('tree data filler rows only invoke groupRowEditable', async () => {
+ const editableCalls: Parameters[] = [];
+ const editable: EditableCallback = (...args) => {
+ editableCalls.push(args);
+ return true;
+ };
+ const groupRowEditableCalls: Parameters[] = [];
+ const groupRowEditable: GroupRowEditableCallback = (...args) => {
+ groupRowEditableCalls.push(args);
+ return true;
+ };
+ const valueSetterCalls: Parameters[] = [];
+ const valueSetter: ValueSetterCallback = (params) => {
+ valueSetterCalls.push([params]);
+ if (!params.data && params.node?.groupData) {
+ params.node.groupData.group = params.newValue;
+ }
+ return true;
+ };
+
+ const api = await gridsManager.createGridAndWait('tree-data-filler-groupRowEditable', {
+ defaultColDef: {
+ cellEditor: 'agTextCellEditor',
+ },
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ field: 'label',
+ cellRenderer: 'agGroupCellRenderer',
+ cellRendererParams: {
+ suppressCount: true,
+ },
+ editable,
+ groupRowEditable,
+ valueSetter,
+ },
+ ],
+ treeData: true,
+ rowData: [{ id: 'mars', path: ['Solar System', 'Mars'], label: 'Mars' }],
+ getDataPath: (data) => data.path,
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data?.id,
+ });
+
+ const fillerRowNode = api.getRowNode('row-group-0-Solar System');
+ expect(fillerRowNode).toBeDefined();
+ expect(fillerRowNode!.group).toBe(true);
+ expect(fillerRowNode!.data).toBeUndefined();
+ const originalFillerValue = getGroupColumnDisplayValue(fillerRowNode!);
+
+ const groupColumn = api.getDisplayedCenterColumns()[0]!;
+ expect(groupColumn.getColDef().groupRowEditable).toBe(groupRowEditable);
+
+ groupRowEditableCalls.length = 0;
+ editableCalls.length = 0;
+ valueSetterCalls.length = 0;
+ if (editMode === 'ui') {
+ await editCell(api, fillerRowNode!, 'group', 'Edited Filler');
+ } else {
+ fillerRowNode!.setDataValue('group', 'Edited Filler', 'ui');
+ await asyncSetTimeout(0);
+ }
+ expect(fillerRowNode!.data).toBeUndefined();
+
+ const groupRowEditableCallsForFiller = callsForRowNode(groupRowEditableCalls, fillerRowNode!.id);
+ const editableCallsForFiller = callsForRowNode(editableCalls, fillerRowNode!.id);
+ if (editMode === 'ui') {
+ expect(groupRowEditableCallsForFiller.length).toBeGreaterThan(0);
+ }
+ expect(editableCallsForFiller.length).toBe(0);
+
+ if (editMode === 'ui') {
+ api.undoCellEditing();
+ await asyncSetTimeout(0);
+ expect(getGroupColumnDisplayValue(fillerRowNode!)).toBe(originalFillerValue);
+ expect(fillerRowNode!.data).toBeUndefined();
+ }
+ });
+
+ test('tree data group rows with data prefer groupRowEditable when defined', async () => {
+ const editableCalls: Parameters[] = [];
+ const editable: EditableCallback = (...args) => {
+ editableCalls.push(args);
+ return true;
+ };
+ const groupRowEditableCalls: Parameters[] = [];
+ const groupRowEditable: GroupRowEditableCallback = (...args) => {
+ groupRowEditableCalls.push(args);
+ return true;
+ };
+ const rowData = [
+ { id: 'earth', path: ['Earth'], label: 'Earth label' },
+ { id: 'moon', path: ['Earth', 'Moon'], label: 'Moon label' },
+ ];
+ const originalEarthLabel = rowData[0].label;
+ const valueSetterCalls: Parameters[] = [];
+ const valueSetter: ValueSetterCallback = (params) => {
+ valueSetterCalls.push([params]);
+ if (params.data) {
+ (params.data as { label?: string }).label = params.newValue;
+ }
+ return true;
+ };
+
+ const api = await gridsManager.createGridAndWait('tree-data-groupRowEditable', {
+ defaultColDef: {
+ cellEditor: 'agTextCellEditor',
+ },
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ field: 'label',
+ cellRenderer: 'agGroupCellRenderer',
+ cellRendererParams: {
+ suppressCount: true,
+ },
+ editable,
+ groupRowEditable,
+ valueSetter,
+ },
+ ],
+ treeData: true,
+ rowData,
+ getDataPath: (data) => data.path,
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data?.id,
+ });
+
+ const earthRowNode = api.getRowNode('earth');
+ expect(earthRowNode).toBeDefined();
+
+ groupRowEditableCalls.length = 0;
+ editableCalls.length = 0;
+ valueSetterCalls.length = 0;
+ if (editMode === 'ui') {
+ await editCell(api, earthRowNode!, 'group', 'Edited Earth');
+ } else {
+ earthRowNode!.setDataValue('group', 'Edited Earth', 'ui');
+ await asyncSetTimeout(0);
+ }
+
+ const groupRowEditableCallsForEarth = callsForRowNode(groupRowEditableCalls, earthRowNode!.id);
+ const editableCallsForEarth = callsForRowNode(editableCalls, earthRowNode!.id);
+ if (editMode === 'ui') {
+ expect(groupRowEditableCallsForEarth.length).toBeGreaterThan(0);
+ }
+ expect(editableCallsForEarth.length).toBe(0);
+ const valueSetterCallsForEarth = callsForRowNode(valueSetterCalls, earthRowNode!.id);
+ expect(valueSetterCallsForEarth.length).toBeGreaterThan(0);
+ expect(rowData[0].label).toBe('Edited Earth');
+
+ if (editMode === 'ui') {
+ api.undoCellEditing();
+ await asyncSetTimeout(0);
+ expect(rowData[0].label).toBe(originalEarthLabel);
+ }
+ });
+
+ test('tree data group rows with data fall back to editable when groupRowEditable missing', async () => {
+ const editableCalls: Parameters[] = [];
+ const editable: EditableCallback = (...args) => {
+ editableCalls.push(args);
+ return true;
+ };
+ const rowData = [
+ { id: 'earth', path: ['Earth'], label: 'Earth label' },
+ { id: 'moon', path: ['Earth', 'Moon'], label: 'Moon label' },
+ ];
+ const originalEarthLabel = rowData[0].label;
+ const valueSetterCalls: Parameters[] = [];
+ const valueSetter: ValueSetterCallback = (params) => {
+ valueSetterCalls.push([params]);
+ if (params.data) {
+ (params.data as { label?: string }).label = params.newValue;
+ }
+ return true;
+ };
+
+ const api = await gridsManager.createGridAndWait('tree-data-groupRowEditable-fallback', {
+ enableGroupEdit: true,
+ undoRedoCellEditing: true,
+ groupDisplayType: 'custom',
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ field: 'label',
+ cellRenderer: 'agGroupCellRenderer',
+ cellRendererParams: {
+ suppressCount: true,
+ },
+ editable,
+ valueSetter,
+ },
+ ],
+ treeData: true,
+ rowData,
+ getDataPath: (data) => data.path,
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data?.id,
+ });
+
+ const earthRowNode = api.getRowNode('earth');
+ expect(earthRowNode).toBeDefined();
+
+ editableCalls.length = 0;
+ valueSetterCalls.length = 0;
+ if (editMode === 'ui') {
+ await editCell(api, earthRowNode!, 'group', 'Edited Earth');
+ } else {
+ earthRowNode!.setDataValue('group', 'Edited Earth', 'ui');
+ await asyncSetTimeout(0);
+ }
+
+ const editableCallsForEarth = callsForRowNode(editableCalls, earthRowNode!.id);
+ if (editMode === 'ui') {
+ expect(editableCallsForEarth.length).toBeGreaterThan(0);
+ }
+ const valueSetterCallsForEarth = callsForRowNode(valueSetterCalls, earthRowNode!.id);
+ expect(valueSetterCallsForEarth.length).toBeGreaterThan(0);
+ expect(rowData[0].label).toBe('Edited Earth');
+
+ if (editMode === 'ui') {
+ api.undoCellEditing();
+ await asyncSetTimeout(0);
+ expect(rowData[0].label).toBe(originalEarthLabel);
+ }
+ });
+
+ test('groupRowValueSetter fires even when groupRowEditable is false', async () => {
+ let invocationCount = 0;
+ const valueSetter: ValueSetterCallback = (params) => {
+ if (params.node?.group) {
+ const groupData = params.node.groupData ?? {};
+ groupData.group = params.newValue;
+ params.node.groupData = groupData;
+ }
+ return true;
+ };
+ const groupRowValueSetter: GroupRowValueSetterCallback = () => {
+ invocationCount += 1;
+ };
+
+ const api = await gridsManager.createGridAndWait('group-row-set-value-without-editable', {
+ columnDefs: [
+ {
+ colId: 'group',
+ headerName: 'Group',
+ cellRenderer: 'agGroupCellRenderer',
+ editable: false,
+ groupRowEditable: false,
+ valueSetter,
+ groupRowValueSetter,
+ },
+ { field: 'category', rowGroup: true, hide: true },
+ ],
+ rowData: [
+ { id: 'a-1', category: 'A', label: 'A1' },
+ { id: 'a-2', category: 'A', label: 'A2' },
+ ],
+ groupDefaultExpanded: -1,
+ getRowId: (params) => params.data.id,
+ });
+
+ const groupRowNode = api.getDisplayedRowAtIndex(0);
+ expect(groupRowNode?.group).toBe(true);
+ const targetColumn = api.getColumns()?.find((col) => col.getColId() === 'group');
+ expect(targetColumn).toBeDefined();
+
+ groupRowNode!.setDataValue(targetColumn!, 'Edited Group', 'ui');
+ await asyncSetTimeout(0);
+
+ expect(invocationCount).toBe(1);
+ });
+});
diff --git a/testing/behavioural/src/filters/aggregate-filters.test.ts b/testing/behavioural/src/filters/aggregate-filters.test.ts
index 5112321cb61..f6cc4214325 100644
--- a/testing/behavioural/src/filters/aggregate-filters.test.ts
+++ b/testing/behavioural/src/filters/aggregate-filters.test.ts
@@ -1,11 +1,11 @@
import { ClientSideRowModelModule, NumberFilterModule, TextFilterModule, setupAgTestIds } from 'ag-grid-community';
-import { RowGroupingModule } from 'ag-grid-enterprise';
+import { PivotModule, RowGroupingModule } from 'ag-grid-enterprise';
import { GridRows, TestGridsManager } from '../test-utils';
describe('Aggregate Filters', () => {
const gridsManager = new TestGridsManager({
- modules: [ClientSideRowModelModule, TextFilterModule, NumberFilterModule, RowGroupingModule],
+ modules: [ClientSideRowModelModule, TextFilterModule, NumberFilterModule, RowGroupingModule, PivotModule],
});
const rowData = [