Hi everyone,
I’m working on a Dash app that uses dash‑ag‑grid alongside Dash Mantine Components, and I’ve run into a usability issue with the undo functionality. This example grid has three editable columns (Object, Color, and Height) and I’ve enabled undo/redo cell editing with 'undoRedoCellEditing': True
and a limit of 20 operations. Above the grid there’s a radio group offering two modes: Change Single Value, which only updates the one cell (and can be easily undone with Ctrl+Z), and Change All Instances, which finds all rows in the same column that shared the original value and updates them to the new value in one batch.
The problem is that as soon as my cellValueChanged
callback returns the modified rowData
to propagate the batch update, the grid’s internal undo history is wiped out, so users can no longer press Ctrl+Z to revert that change. What I’d like is for users to be able to undo the Change All Instances operation, ideally as many steps as the undo limit allows, just like they can for single-cell edits.
Any advice or code examples would be greatly appreciated!
import dash
from dash import html, Input, Output, State, no_update, callback
import dash_ag_grid as dag
import dash_mantine_components as dmc
row_data = [
{"Object": f"Item {i}",
"Color": color,
"Height": height}
for i, (color, height) in enumerate(
[
("Red", 10), ("Blue", 15), ("Green", 20), ("Red", 25), ("Blue", 30),
("Green", 35), ("Red", 40), ("Blue", 45), ("Green", 50), ("Red", 55),
("Blue", 10), ("Green", 15), ("Red", 20), ("Blue", 25), ("Green", 30),
("Red", 35), ("Blue", 40), ("Green", 45), ("Red", 50), ("Blue", 55),
], start=1
)
]
column_defs = [
{"field": "Object", "editable": True},
{"field": "Color", "editable": True},
{"field": "Height", "editable": True, "type": "numericColumn"},
]
app = dash.Dash(__name__)
app.layout = dmc.MantineProvider(
html.Div([
dmc.RadioGroup(
id="mode-selector",
value="single",
label="Edit Mode",
style={"marginBottom": "1rem"},
children=[
dmc.Radio(label="Change Single Value", value="single"),
dmc.Radio(label="Change All Instances", value="all"),
],
),
dag.AgGrid(
id="grid",
columnDefs=column_defs,
rowData=row_data,
dashGridOptions={
'undoRedoCellEditing': True,
'undoRedoCellEditingLimit': 20
}
),
])
)
@callback(
Output("grid", "rowData"),
Input("grid", "cellValueChanged"),
State("mode-selector", "value"),
State("grid", "rowData"),
prevent_initial_call=True
)
def on_cell_change(changes, mode, row_data):
change = changes[0]
col_id = change.get('colId')
row_index = change.get('rowIndex')
old_value = change.get('oldValue')
new_value = change.get('value')
updated_data = [row.copy() for row in row_data]
if mode=="all":
# Change all instances in the column matching old_value
for row in updated_data:
if row.get(col_id) == old_value:
row[col_id] = new_value
return updated_data
else:
return no_update
if __name__ == "__main__":
app.run(debug=True)