From acf9beed846dd842bc1058148f578eb6d511f497 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:40:48 +0000 Subject: [PATCH 1/3] Initial plan From a0a08b50ef040082c377f4c6a19cab3df7d0e595 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:55:27 +0000 Subject: [PATCH 2/3] fix: robust getRowId parsing for selection and scrollTo flows Agent-Logs-Url: https://github.com/plotly/dash-ag-grid/sessions/7ee0283e-b2ea-42ea-ada6-1945cb19542e Co-authored-by: BSd3v <82055130+BSd3v@users.noreply.github.com> --- src/lib/fragments/AgGrid.react.js | 42 +++++++++++++++-------- tests/test_selected_rows.py | 57 ++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 6f7dad3a..5183c2cd 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -274,6 +274,22 @@ export function DashAgGrid(props) { const [openGroups, setOpenGroups] = useState({}); const [columnState_push, setColumnState_push] = useState(true); const [rowTransactionState, setRowTransactionState] = useState(null); + const evaluateGetRowId = useMemo(() => { + const {getRowId} = props; + if (typeof getRowId !== 'string') { + return null; + } + try { + const parsedCondition = esprima.parse(getRowId).body[0].expression; + return (data) => evaluate(parsedCondition, {params: {data}}); + } catch (err) { + console.error( + 'Failed to parse getRowId expression. Expected formats like params.data.field or params.data["field"]:', + err + ); + } + return null; + }, [props.getRowId]); const components = useMemo( () => ({ @@ -313,7 +329,6 @@ export function DashAgGrid(props) { const setSelection = useCallback( (selection) => { - const {getRowId} = props; if (gridApi && selection && !gridApi?.isDestroyed()) { pauseSelections.current = true; const nodeData = []; @@ -337,13 +352,13 @@ export function DashAgGrid(props) { }); } else { if (selection.length) { - if (getRowId) { - const parsedCondition = esprima.parse( - getRowId.replaceAll('params.data.', '') - ).body[0].expression; + if (evaluateGetRowId) { const mapId = {}; - selection.forEach((params) => { - mapId[evaluate(parsedCondition, params)] = true; + selection.forEach((data) => { + const rowId = evaluateGetRowId(data); + if (typeof rowId !== 'undefined') { + mapId[rowId] = true; + } }); gridApi.forEachNode((node) => { if (mapId[node.id]) { @@ -369,7 +384,7 @@ export function DashAgGrid(props) { }, 1); } }, - [gridApi, props.getRowId, parseFunction] + [gridApi, parseFunction, evaluateGetRowId] ); const memoizeOne = useCallback( @@ -1116,7 +1131,7 @@ export function DashAgGrid(props) { const scrollTo = useCallback( (reset = true) => { - const {scrollTo, getRowId} = props; + const {scrollTo} = props; if (!gridApi) { return; } @@ -1129,12 +1144,9 @@ export function DashAgGrid(props) { const node = gridApi.getRowNode(scrollTo.rowId); gridApi.ensureNodeVisible(node, rowPosition); } else if (scrollTo.data) { - if (getRowId) { - const parsedCondition = esprima.parse( - getRowId.replaceAll('params.data.', '') - ).body[0].expression; + if (evaluateGetRowId) { const node = gridApi.getRowNode( - evaluate(parsedCondition, scrollTo.data) + evaluateGetRowId(scrollTo.data) ); gridApi.ensureNodeVisible(node, rowPosition); } else { @@ -1159,7 +1171,7 @@ export function DashAgGrid(props) { }); } }, - [gridApi, props.scrollTo, props.getRowId, customSetProps] + [gridApi, props.scrollTo, customSetProps, evaluateGetRowId] ); const resetColumnState = useCallback( diff --git a/tests/test_selected_rows.py b/tests/test_selected_rows.py index 53fdafdd..1d2cfc23 100644 --- a/tests/test_selected_rows.py +++ b/tests/test_selected_rows.py @@ -289,4 +289,59 @@ def update_selected_row_info(selected_rows): assert 'ag-row-selected' not in grid.get_row(0).get_attribute('class') assert 'ag-row-selected' not in grid.get_row(1).get_attribute('class') - assert 'ag-row-selected' in grid.get_row(2).get_attribute('class') \ No newline at end of file + assert 'ag-row-selected' in grid.get_row(2).get_attribute('class') + + +def test_sr4_selected_rows_get_row_id_bracket_notation(dash_duo): + app = Dash(__name__) + + row_data = [ + {"Current canvas blend": "alpha", "B": 1}, + {"Current canvas blend": "O'Reilly", "B": 2}, + {"Current canvas blend": r"path\\name", "B": 3}, + ] + + app.layout = html.Div([ + html.Button('Select Special', id='select-special', n_clicks=0), + dag.AgGrid( + id='grid', + columnSize="sizeToFit", + columnDefs=[{'field': 'Current canvas blend'}, {'field': 'B'}], + rowData=row_data, + getRowId='params.data["Current canvas blend"]', + dashGridOptions={"rowSelection": "multiple"}, + style={"maxHeight": "200px", "overflow": "auto"}, + ), + html.Div(id='selected-row-info') + ]) + + @app.callback( + Output('grid', 'selectedRows'), + Input('select-special', 'n_clicks'), + prevent_initial_call=True + ) + def select_special_row(n_clicks): + return [{"Current canvas blend": "O'Reilly", "B": 2}] + + @app.callback( + Output('selected-row-info', 'children'), + Input('grid', 'selectedRows') + ) + def update_selected_row_info(selected_rows): + if isinstance(selected_rows, list) and selected_rows: + return f"Selected Row: {selected_rows[-1]['Current canvas blend']}" + return "No row selected" + + dash_duo.start_server(app) + + grid = utils.Grid(dash_duo, "grid") + grid.wait_for_cell_text(0, 0, "alpha") + + dash_duo.find_element('#select-special').click() + dash_duo.wait_for_text_to_equal( + "#selected-row-info", "Selected Row: O'Reilly" + ) + + assert 'ag-row-selected' not in grid.get_row(0).get_attribute('class') + assert 'ag-row-selected' in grid.get_row(1).get_attribute('class') + assert 'ag-row-selected' not in grid.get_row(2).get_attribute('class') From bc2c10802dfd591b42c9a61a13f932eef93dab5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:31:54 +0000 Subject: [PATCH 3/3] revert: drop unrelated getRowId selection/scroll parsing change Agent-Logs-Url: https://github.com/plotly/dash-ag-grid/sessions/83b1bbd8-00ae-4f65-9048-0c375ab3f081 Co-authored-by: BSd3v <82055130+BSd3v@users.noreply.github.com> --- src/lib/fragments/AgGrid.react.js | 42 ++++++++--------------- tests/test_selected_rows.py | 57 +------------------------------ 2 files changed, 16 insertions(+), 83 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 5183c2cd..6f7dad3a 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -274,22 +274,6 @@ export function DashAgGrid(props) { const [openGroups, setOpenGroups] = useState({}); const [columnState_push, setColumnState_push] = useState(true); const [rowTransactionState, setRowTransactionState] = useState(null); - const evaluateGetRowId = useMemo(() => { - const {getRowId} = props; - if (typeof getRowId !== 'string') { - return null; - } - try { - const parsedCondition = esprima.parse(getRowId).body[0].expression; - return (data) => evaluate(parsedCondition, {params: {data}}); - } catch (err) { - console.error( - 'Failed to parse getRowId expression. Expected formats like params.data.field or params.data["field"]:', - err - ); - } - return null; - }, [props.getRowId]); const components = useMemo( () => ({ @@ -329,6 +313,7 @@ export function DashAgGrid(props) { const setSelection = useCallback( (selection) => { + const {getRowId} = props; if (gridApi && selection && !gridApi?.isDestroyed()) { pauseSelections.current = true; const nodeData = []; @@ -352,13 +337,13 @@ export function DashAgGrid(props) { }); } else { if (selection.length) { - if (evaluateGetRowId) { + if (getRowId) { + const parsedCondition = esprima.parse( + getRowId.replaceAll('params.data.', '') + ).body[0].expression; const mapId = {}; - selection.forEach((data) => { - const rowId = evaluateGetRowId(data); - if (typeof rowId !== 'undefined') { - mapId[rowId] = true; - } + selection.forEach((params) => { + mapId[evaluate(parsedCondition, params)] = true; }); gridApi.forEachNode((node) => { if (mapId[node.id]) { @@ -384,7 +369,7 @@ export function DashAgGrid(props) { }, 1); } }, - [gridApi, parseFunction, evaluateGetRowId] + [gridApi, props.getRowId, parseFunction] ); const memoizeOne = useCallback( @@ -1131,7 +1116,7 @@ export function DashAgGrid(props) { const scrollTo = useCallback( (reset = true) => { - const {scrollTo} = props; + const {scrollTo, getRowId} = props; if (!gridApi) { return; } @@ -1144,9 +1129,12 @@ export function DashAgGrid(props) { const node = gridApi.getRowNode(scrollTo.rowId); gridApi.ensureNodeVisible(node, rowPosition); } else if (scrollTo.data) { - if (evaluateGetRowId) { + if (getRowId) { + const parsedCondition = esprima.parse( + getRowId.replaceAll('params.data.', '') + ).body[0].expression; const node = gridApi.getRowNode( - evaluateGetRowId(scrollTo.data) + evaluate(parsedCondition, scrollTo.data) ); gridApi.ensureNodeVisible(node, rowPosition); } else { @@ -1171,7 +1159,7 @@ export function DashAgGrid(props) { }); } }, - [gridApi, props.scrollTo, customSetProps, evaluateGetRowId] + [gridApi, props.scrollTo, props.getRowId, customSetProps] ); const resetColumnState = useCallback( diff --git a/tests/test_selected_rows.py b/tests/test_selected_rows.py index 1d2cfc23..53fdafdd 100644 --- a/tests/test_selected_rows.py +++ b/tests/test_selected_rows.py @@ -289,59 +289,4 @@ def update_selected_row_info(selected_rows): assert 'ag-row-selected' not in grid.get_row(0).get_attribute('class') assert 'ag-row-selected' not in grid.get_row(1).get_attribute('class') - assert 'ag-row-selected' in grid.get_row(2).get_attribute('class') - - -def test_sr4_selected_rows_get_row_id_bracket_notation(dash_duo): - app = Dash(__name__) - - row_data = [ - {"Current canvas blend": "alpha", "B": 1}, - {"Current canvas blend": "O'Reilly", "B": 2}, - {"Current canvas blend": r"path\\name", "B": 3}, - ] - - app.layout = html.Div([ - html.Button('Select Special', id='select-special', n_clicks=0), - dag.AgGrid( - id='grid', - columnSize="sizeToFit", - columnDefs=[{'field': 'Current canvas blend'}, {'field': 'B'}], - rowData=row_data, - getRowId='params.data["Current canvas blend"]', - dashGridOptions={"rowSelection": "multiple"}, - style={"maxHeight": "200px", "overflow": "auto"}, - ), - html.Div(id='selected-row-info') - ]) - - @app.callback( - Output('grid', 'selectedRows'), - Input('select-special', 'n_clicks'), - prevent_initial_call=True - ) - def select_special_row(n_clicks): - return [{"Current canvas blend": "O'Reilly", "B": 2}] - - @app.callback( - Output('selected-row-info', 'children'), - Input('grid', 'selectedRows') - ) - def update_selected_row_info(selected_rows): - if isinstance(selected_rows, list) and selected_rows: - return f"Selected Row: {selected_rows[-1]['Current canvas blend']}" - return "No row selected" - - dash_duo.start_server(app) - - grid = utils.Grid(dash_duo, "grid") - grid.wait_for_cell_text(0, 0, "alpha") - - dash_duo.find_element('#select-special').click() - dash_duo.wait_for_text_to_equal( - "#selected-row-info", "Selected Row: O'Reilly" - ) - - assert 'ag-row-selected' not in grid.get_row(0).get_attribute('class') - assert 'ag-row-selected' in grid.get_row(1).get_attribute('class') - assert 'ag-row-selected' not in grid.get_row(2).get_attribute('class') + assert 'ag-row-selected' in grid.get_row(2).get_attribute('class') \ No newline at end of file