Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Links "DE#nnn" prior to version 2.0 point to the Dash Enterprise closed-source D

### Fixed
- [#454](https://github.com/plotly/dash-ag-grid/pull/454) fixes issue where a rowCount of 0 would cause the grid not to display new data
- [#456](https://github.com/plotly/dash-ag-grid/pull/456) fixes `rowTransaction` pre-mount buffering/replay and prevents stale transaction retention in the wrapper

## [35.2.0] - 2026-04-03
### Added
Expand Down
29 changes: 23 additions & 6 deletions src/lib/components/AgGrid.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ function DashAgGrid(props) {
});

const buildArray = useCallback((arr1, arr2) => {
if (arr1) {
if (!arr1.includes(arr2)) {
return [...arr1, arr2];
}
if (!arr1) {
return [arr2];
}
const nextSerialized = JSON.stringify(arr2);
const serializedTransactions = new Set(
arr1.map((transaction) => JSON.stringify(transaction))
);
if (serializedTransactions.has(nextSerialized)) {
return arr1;
}
return [JSON.parse(JSON.stringify(arr2))];
return [...arr1, arr2];
}, []);
Comment thread
BSd3v marked this conversation as resolved.

useEffect(() => {
Expand All @@ -64,12 +68,25 @@ function DashAgGrid(props) {
}
}, [props.rowTransaction, state.mounted, buildArray]);

const onJsGridMounted = useCallback(() => {
setState((prevState) => ({
...prevState,
mounted: true,
rowTransaction: null,
}));
}, []);

const {enableEnterpriseModules} = props;
const RealComponent = getGrid(enableEnterpriseModules);

return (
<Suspense fallback={null}>
<RealComponent parentState={state} {...defaultProps} {...props} />
<RealComponent
parentState={state}
onJsGridMounted={onJsGridMounted}
{...defaultProps}
{...props}
/>
</Suspense>
);
}
Expand Down
61 changes: 46 additions & 15 deletions src/lib/fragments/AgGrid.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const xssMessage = (context) => {
};

const NO_CONVERT_PROPS = [...PASSTHRU_PROPS, ...PROPS_NOT_FOR_AG_GRID];
const OMIT_GRID_INTERNAL_PROPS = ['parentState', 'onJsGridMounted'];

const dash_clientside = window.dash_clientside || {};

Expand Down Expand Up @@ -273,7 +274,9 @@ export function DashAgGrid(props) {
const [, forceRerender] = useState({});
const [openGroups, setOpenGroups] = useState({});
const [columnState_push, setColumnState_push] = useState(true);
const [rowTransactionState, setRowTransactionState] = useState(null);
const [rowTransactionState, setRowTransactionState] = useState(
props.parentState?.rowTransaction || null
);
const resettingCount = useRef(false);
const prevRowCountRef = useRef(null);
const resetTimeoutRef = useRef(null);
Expand All @@ -299,6 +302,22 @@ export function DashAgGrid(props) {
const getDetailParams = useRef();
const getRowsParams = useRef(null);
const pendingCellValueChanges = useRef(null);
const onJsGridMountedRef = useRef(props.onJsGridMounted);
onJsGridMountedRef.current = props.onJsGridMounted;

useEffect(() => {
onJsGridMountedRef.current?.();
}, []);

useEffect(() => {
if (
!gridApi &&
props.parentState?.rowTransaction &&
props.parentState.rowTransaction !== rowTransactionState
) {
setRowTransactionState(props.parentState.rowTransaction);
}
}, [props.parentState, rowTransactionState, gridApi]);

const onPaginationChanged = useCallback(() => {
if (gridApi && !gridApi?.isDestroyed()) {
Expand Down Expand Up @@ -1235,33 +1254,38 @@ export function DashAgGrid(props) {
);

const buildArray = useCallback((arr1, arr2) => {
if (arr1) {
if (!arr1.includes(arr2)) {
return [...arr1, arr2];
}
if (!arr1) {
return [arr2];
}
const nextSerialized = JSON.stringify(arr2);
const serializedTransactions = new Set(
arr1.map((transaction) => JSON.stringify(transaction))
);
if (serializedTransactions.has(nextSerialized)) {
return arr1;
}
return [JSON.parse(JSON.stringify(arr2))];
return [...arr1, arr2];
}, []);

const rowTransaction = useCallback(
(data) => {
const rowTransaction = rowTransactionState;
if (gridApi && !gridApi?.isDestroyed()) {
if (rowTransaction) {
rowTransaction.forEach(applyRowTransaction);
setRowTransactionState(null);
const isAlreadyQueued =
rowTransaction &&
rowTransaction.some((transaction) =>
equals(transaction, data)
);
if (!isAlreadyQueued) {
applyRowTransaction(data);
}
applyRowTransaction(data);
customSetProps({
rowTransaction: null,
});
syncRowData();
Comment thread
BSd3v marked this conversation as resolved.
} else {
setRowTransactionState(
rowTransaction
? buildArray(rowTransaction, data)
: [JSON.parse(JSON.stringify(data))]
rowTransaction ? buildArray(rowTransaction, data) : [data]
);
}
},
Expand Down Expand Up @@ -1585,7 +1609,10 @@ export function DashAgGrid(props) {
const {id, style, className, dashGridOptions, ...restProps} = props;
const passingProps = pick(PASSTHRU_PROPS, restProps);
const convertedProps = convertAllProps(
omit(NO_CONVERT_PROPS, {...dashGridOptions, ...restProps})
omit([...NO_CONVERT_PROPS, ...OMIT_GRID_INTERNAL_PROPS], {
...dashGridOptions,
...restProps,
})
);

if ('theme' in convertedProps) {
Expand Down Expand Up @@ -1669,7 +1696,11 @@ export function DashAgGrid(props) {
);
}

DashAgGrid.propTypes = {parentState: PropTypes.any, ..._propTypes};
DashAgGrid.propTypes = {
parentState: PropTypes.any,
onJsGridMounted: PropTypes.func,
..._propTypes,
};

export const propTypes = DashAgGrid.propTypes;

Expand Down
111 changes: 111 additions & 0 deletions tests/test_selection_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,114 @@ def setSelections(n, n1, n2):
dash_duo.wait_for_text_to_equal(
"#selectedRows", '[{"make": "Ford", "model": "Mondeo", "price": 32000}]'
)


def test_sp002_row_transaction_before_grid_ready(dash_duo):
app = Dash(__name__)

rowData = [
{"id": "Toyota_0", "make": "Toyota", "model": "Celica", "price": 35000},
{"id": "Ford_0", "make": "Ford", "model": "Mondeo", "price": 32000},
{"id": "Porsche_0", "make": "Porsche", "model": "Boxster", "price": 72000},
]

app.layout = html.Div(
[
dcc.Interval(id="tick", interval=1, n_intervals=0, max_intervals=1),
html.Div(id="rowTransaction-state"),
dag.AgGrid(
id="grid",
rowData=rowData,
columnDefs=[
{"field": "id"},
{"field": "make"},
{"field": "model"},
{"field": "price"},
],
defaultColDef={"flex": 1},
getRowId="params.data.id",
),
]
)

@app.callback(Output("grid", "rowTransaction"), Input("tick", "n_intervals"))
def apply_transaction(n):
if not n:
return dash.no_update
return {
"add": [
{
"id": "Queued_1",
"make": "Queued",
"model": "Queued",
"price": 1,
}
]
}

@app.callback(
Output("rowTransaction-state", "children"), Input("grid", "rowTransaction")
)
def expose_row_transaction_state(value):
return json.dumps(value)

dash_duo.start_server(app)

grid = utils.Grid(dash_duo, "grid")
grid.wait_for_cell_text(0, 0, "Toyota_0")
grid.wait_for_cell_text(1, 0, "Ford_0")
grid.wait_for_cell_text(2, 0, "Porsche_0")
grid.wait_for_cell_text(3, 0, "Queued_1")
grid.wait_for_rendered_rows(4)
dash_duo.wait_for_text_to_equal("#rowTransaction-state", "null")


def test_sp003_duplicate_row_transaction_before_grid_ready(dash_duo):
app = Dash(__name__)

rowData = [
{"id": "Toyota_0", "make": "Toyota", "model": "Celica", "price": 35000},
{"id": "Ford_0", "make": "Ford", "model": "Mondeo", "price": 32000},
{"id": "Porsche_0", "make": "Porsche", "model": "Boxster", "price": 72000},
]

app.layout = html.Div(
[
dcc.Interval(id="tick", interval=1, n_intervals=0, max_intervals=2),
dag.AgGrid(
id="grid",
rowData=rowData,
columnDefs=[
{"field": "id"},
{"field": "make"},
{"field": "model"},
{"field": "price"},
],
defaultColDef={"flex": 1},
),
]
)

@app.callback(Output("grid", "rowTransaction"), Input("tick", "n_intervals"))
def apply_duplicate_transaction(n):
if not n:
return dash.no_update
return {
"add": [
{
"id": "Queued_1",
"make": "Queued",
"model": "Queued",
"price": 1,
}
]
}

dash_duo.start_server(app)

grid = utils.Grid(dash_duo, "grid")
grid.wait_for_cell_text(0, 0, "Toyota_0")
grid.wait_for_cell_text(1, 0, "Ford_0")
grid.wait_for_cell_text(2, 0, "Porsche_0")
grid.wait_for_cell_text(3, 0, "Queued_1")
grid.wait_for_rendered_rows(4)
Loading