diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c1600f8b..5224136239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,14 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased - ### Added - [#2389](https://github.com/plotly/dash/pull/2389) Added `disable_n_clicks` prop to all html components to make it possible to remove onclick event listeners +## Fixed + +- [#2388](https://github.com/plotly/dash/pull/2388) Fix [#2368](https://github.com/plotly/dash/issues/2368) ordering or Pattern Matching ALL after update to the subtree. + ### Updated - [#2367](https://github.com/plotly/dash/pull/2367) Updated the default `favicon.ico` to the current Plotly logo diff --git a/dash/dash-renderer/src/actions/paths.js b/dash/dash-renderer/src/actions/paths.js index ead7507b45..dd0c5d4cfd 100644 --- a/dash/dash-renderer/src/actions/paths.js +++ b/dash/dash-renderer/src/actions/paths.js @@ -3,9 +3,11 @@ import { filter, find, forEachObjIndexed, + insert, path, propEq, - props + props, + indexOf } from 'ramda'; import {crawlLayout} from './utils'; @@ -46,7 +48,14 @@ export function computePaths(subTree, startingPath, oldPaths, events) { const values = props(keys, id); const keyStr = keys.join(','); const paths = (objs[keyStr] = objs[keyStr] || []); - paths.push({values, path: concat(startingPath, itempath)}); + const oldie = oldObjs[keyStr] || []; + const item = {values, path: concat(startingPath, itempath)}; + const index = indexOf(item, oldie); + if (index === -1) { + paths.push(item); + } else { + objs[keyStr] = insert(index, item, paths); + } } else { strs[id] = concat(startingPath, itempath); } diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index b1bf5c71d5..d807e0bef3 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -483,3 +483,72 @@ def assert_callback_context(items_text): dash_duo.wait_for_text_to_equal("#totals", "3 total item(s)") assert_count(3) assert_callback_context(["apples", "bananas", "carrots"]) + + +def test_cbwc007_pmc_update_subtree_ordering(dash_duo): + # Test for regression bug #2368, updated pmc subtree should keep order. + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + html.Button("refresh options", id="refresh-options"), + html.Br(), + html.Div( + [ + *[ + dcc.Dropdown( + id={"type": "demo-options", "index": i}, + placeholder=f"dropdown-{i}", + style={"width": "200px"}, + ) + for i in range(2) + ], + dcc.Dropdown( + id={"type": "demo-options", "index": 2}, + options=[f"option2-{i}" for i in range(3)], + placeholder="dropdown-2", + style={"width": "200px"}, + ), + ], + id="dropdown-container", + ), + html.Br(), + html.Pre(id="selected-values"), + ], + style={"padding": "50px"}, + ) + + @app.callback( + [ + Output({"type": "demo-options", "index": 0}, "options"), + Output({"type": "demo-options", "index": 1}, "options"), + ], + Input("refresh-options", "n_clicks"), + prevent_initial_call=True, + ) + def refresh_options(_): + return [[f"option0-{i}" for i in range(3)], [f"option1-{i}" for i in range(3)]] + + @app.callback( + Output("selected-values", "children"), + Input({"type": "demo-options", "index": ALL}, "value"), + ) + def update_selected_values(values): + return str(values) + + dash_duo.start_server(app) + dash_duo.select_dcc_dropdown(".dash-dropdown:nth-child(3)", index=2) + + dash_duo.wait_for_text_to_equal("#selected-values", "[None, None, 'option2-2']") + + dash_duo.wait_for_element("#refresh-options").click() + + dash_duo.select_dcc_dropdown(".dash-dropdown:nth-child(2)", index=2) + dash_duo.wait_for_text_to_equal( + "#selected-values", "[None, 'option1-2', 'option2-2']" + ) + + dash_duo.select_dcc_dropdown(".dash-dropdown:nth-child(1)", index=2) + dash_duo.wait_for_text_to_equal( + "#selected-values", "['option0-2', 'option1-2', 'option2-2']" + )