From a2341878b680b8f3de07b5a076e4c76079bb755b Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 4 Mar 2025 10:58:45 -0500 Subject: [PATCH 1/3] Fix initial props reset back --- .../src/fragments/Dropdown.react.js | 3 +- dash/dash-renderer/src/reducers/reducer.js | 7 +- .../dash-renderer/src/wrapper/DashWrapper.tsx | 89 ++++++++++--------- dash/dash-renderer/src/wrapper/selectors.ts | 12 ++- dash/dash-renderer/src/wrapper/wrapping.ts | 14 ++- tests/integration/test_integration.py | 34 +++++++ 6 files changed, 105 insertions(+), 54 deletions(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 4d003863fd..44c45e92a4 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -45,6 +45,7 @@ const Dropdown = props => { search_value, style, value, + searchable, } = props; const [optionsCheck, setOptionsCheck] = useState(null); const persistentOptions = useRef(null); @@ -157,7 +158,7 @@ const Dropdown = props => { options={sanitizedOptions} value={value} onChange={onChange} - onInputChange={onInputChange} + onInputChange={searchable ? onInputChange : undefined} backspaceRemoves={clearable} deleteRemoves={clearable} inputProps={{autoComplete: 'off'}} diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index c8f02b804a..908cd87bc6 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -18,6 +18,7 @@ import layout from './layout'; import paths from './paths'; import callbackJobs from './callbackJobs'; import loading from './loading'; +import {stringifyPath} from '../wrapper/wrapping'; export const apiRequests = [ 'dependenciesRequest', @@ -36,9 +37,9 @@ function layoutHashes(state = {}, action) { ) { // Let us compare the paths sums to get updates without triggering // render on the parent containers. - const jsonPath = JSON.stringify(action.payload.itempath); - const prev = pathOr(0, [jsonPath], state); - return assoc(jsonPath, prev + 1, state); + const strPath = stringifyPath(action.payload.itempath); + const prev = pathOr(0, [strPath], state); + return assoc(strPath, prev + 1, state); } return state; } diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 2756ff54a3..86c34cc7c6 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -24,7 +24,7 @@ import {DashConfig} from '../config'; import {notifyObservers, onError, updateProps} from '../actions'; import {getWatchedKeys, stringifyId} from '../actions/dependencies'; import {recordUiEdit} from '../persistence'; -import {createElement, isDryComponent} from './wrapping'; +import {createElement, getComponentLayout, isDryComponent} from './wrapping'; import Registry from '../registry'; import isSimpleComponent from '../isSimpleComponent'; import { @@ -62,56 +62,61 @@ function DashWrapper({ const setProps = (newProps: UpdatePropsPayload) => { const {id} = componentProps; const {_dash_error, ...restProps} = newProps; - const oldProps = componentProps; - const changedProps = pickBy( - (val, key) => !equals(val, oldProps[key]), - restProps - ); - if (_dash_error) { - dispatch( - onError({ - type: 'frontEnd', - error: _dash_error - }) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + dispatch((dispatch, getState) => { + const currentState = getState(); + const {graphs} = currentState; + + const {props: oldProps} = getComponentLayout( + componentPath, + currentState ); - } - if (!isEmpty(changedProps)) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - dispatch((dispatch, getState) => { - const {graphs} = getState(); - // Identify the modified props that are required for callbacks - const watchedKeys = getWatchedKeys( - id, - keys(changedProps), - graphs + const changedProps = pickBy( + (val, key) => !equals(val, oldProps[key]), + restProps + ); + if (_dash_error) { + dispatch( + onError({ + type: 'frontEnd', + error: _dash_error + }) ); + } - batch(() => { - // setProps here is triggered by the UI - record these changes - // for persistence - recordUiEdit(component, newProps, dispatch); + if (isEmpty(changedProps)) { + return; + } - // Only dispatch changes to Dash if a watched prop changed - if (watchedKeys.length) { - dispatch( - notifyObservers({ - id, - props: pick(watchedKeys, changedProps) - }) - ); - } + // Identify the modified props that are required for callbacks + const watchedKeys = getWatchedKeys(id, keys(changedProps), graphs); + + batch(() => { + // setProps here is triggered by the UI - record these changes + // for persistence + recordUiEdit(component, newProps, dispatch); - // Always update this component's props + // Only dispatch changes to Dash if a watched prop changed + if (watchedKeys.length) { dispatch( - updateProps({ - props: changedProps, - itempath: componentPath + notifyObservers({ + id, + props: pick(watchedKeys, changedProps) }) ); - }); + } + + // Always update this component's props + dispatch( + updateProps({ + props: changedProps, + itempath: componentPath + }) + ); }); - } + }); }; const createContainer = useCallback( diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 8278bb09eb..293af71264 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -1,22 +1,20 @@ -import {path} from 'ramda'; - import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component'; +import {getComponentLayout, stringifyPath} from './wrapping'; type SelectDashProps = [DashComponent, BaseDashProps, number]; export const selectDashProps = (componentPath: DashLayoutPath) => (state: any): SelectDashProps => { - const c = path(componentPath, state.layout) as DashComponent; + const c = getComponentLayout(componentPath, state); // Layout hashes records the number of times a path has been updated. // sum with the parents hash (match without the last ']') to get the real hash // Then it can be easily compared without having to compare the props. - let jsonPath = JSON.stringify(componentPath); - jsonPath = jsonPath.substring(0, jsonPath.length - 1); + const strPath = stringifyPath(componentPath); const h = Object.entries(state.layoutHashes).reduce( - (acc, [path, pathHash]) => - jsonPath.startsWith(path.substring(0, path.length - 1)) + (acc, [updatedPath, pathHash]) => + strPath.startsWith(updatedPath) ? (pathHash as number) + acc : acc, 0 diff --git a/dash/dash-renderer/src/wrapper/wrapping.ts b/dash/dash-renderer/src/wrapper/wrapping.ts index 3514331163..baf0e4f6f3 100644 --- a/dash/dash-renderer/src/wrapper/wrapping.ts +++ b/dash/dash-renderer/src/wrapper/wrapping.ts @@ -1,5 +1,6 @@ import React from 'react'; -import {mergeRight, type, has} from 'ramda'; +import {mergeRight, path, type, has, join} from 'ramda'; +import {DashComponent, DashLayoutPath} from '../types/component'; export function createElement( element: any, @@ -49,3 +50,14 @@ export function validateComponent(componentDefinition: any) { ); } } + +export function stringifyPath(layoutPath: DashLayoutPath) { + return join(',', layoutPath); +} + +export function getComponentLayout( + componentPath: DashLayoutPath, + state: any +): DashComponent { + return path(componentPath, state.layout) as DashComponent; +} diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 53c42db3ba..cbe4181ca5 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -472,3 +472,37 @@ def my_route_f(): response = requests.post(url) assert response.status_code == 200 assert response.text == "hello" + + +def test_inin031_initial_value_set_back(dash_duo): + # Test for regression on the initial value to be able to + # set back to initial after changing again. + app = Dash(__name__) + + app.layout = html.Div( + [ + dcc.Dropdown( + id="dropdown", + options=["Toronto", "Montréal", "Vancouver"], + value="Toronto", + searchable=False, + ), + html.Div(id="output"), + ] + ) + + @app.callback(Output("output", "children"), [Input("dropdown", "value")]) + def callback(value): + return f"You have selected {value}" + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#output", "You have selected Toronto") + + dash_duo.select_dcc_dropdown("#dropdown", "Vancouver") + dash_duo.wait_for_text_to_equal("#output", "You have selected Vancouver") + + dash_duo.select_dcc_dropdown("#dropdown", "Toronto") + dash_duo.wait_for_text_to_equal("#output", "You have selected Toronto") + + assert dash_duo.get_logs() == [] From 2d2e3d69c9002aeadd514b9e5fbe581be1306a86 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 4 Mar 2025 11:49:28 -0500 Subject: [PATCH 2/3] Set rdps005 flaky --- tests/integration/renderer/test_persistence.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/renderer/test_persistence.py b/tests/integration/renderer/test_persistence.py index 701d4886e4..5aff9d5c33 100644 --- a/tests/integration/renderer/test_persistence.py +++ b/tests/integration/renderer/test_persistence.py @@ -1,4 +1,5 @@ from multiprocessing import Value +import flaky import pytest import time @@ -206,6 +207,7 @@ def toggle_table(n): check_table_names(dash_duo, ["a", "b"]) +@flaky.flaky(max_runs=3) def test_rdps005_persisted_props(dash_duo): app = Dash(__name__) app.layout = html.Div( From 0bd7d4a9bcbfcd51948e8b0dbad281e0361c9bfe Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 4 Mar 2025 14:33:51 -0500 Subject: [PATCH 3/3] Version 3.0.0rc4 --- CHANGELOG.md | 13 +++++++++++++ components/dash-core-components/package-lock.json | 4 ++-- components/dash-core-components/package.json | 2 +- dash/_dash_renderer.py | 4 ++-- dash/dash-renderer/package-lock.json | 4 ++-- dash/dash-renderer/package.json | 2 +- dash/version.py | 2 +- 7 files changed, 22 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839cb2e4d1..859456f21e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## [3.0.0-rc4] - 2025-03-04 + +## Fixed + +- [#3197](https://github.com/plotly/dash/pull/3197) Fix initial props not updated in setProps causing the initial value of props to not be able to be set again. +- [#3183](https://github.com/plotly/dash/pull/3183) Fix external wrapper requiring id. +- [#3184](https://github.com/plotly/dash/pull/3184) Fix devtools dark mode button color issue and other ui fixes for the version checker. + +## Changed + +- [#3183](https://github.com/plotly/dash/pull/3183) Change ExternalWrapper props to component, componentPath. +- [#3197](https://github.com/plotly/dash/pull/3197) Improved layout path sum stringify of paths. + ## [3.0.0-rc3] - 2025-02-21 ## Added diff --git a/components/dash-core-components/package-lock.json b/components/dash-core-components/package-lock.json index e637e99f8c..1b13a73665 100644 --- a/components/dash-core-components/package-lock.json +++ b/components/dash-core-components/package-lock.json @@ -1,12 +1,12 @@ { "name": "dash-core-components", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "dash-core-components", - "version": "3.0.1", + "version": "3.0.2", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-svg-core": "1.2.36", diff --git a/components/dash-core-components/package.json b/components/dash-core-components/package.json index 4269fcde3f..f0ba509b1a 100644 --- a/components/dash-core-components/package.json +++ b/components/dash-core-components/package.json @@ -1,6 +1,6 @@ { "name": "dash-core-components", - "version": "3.0.1", + "version": "3.0.2", "description": "Core component suite for Dash", "repository": { "type": "git", diff --git a/dash/_dash_renderer.py b/dash/_dash_renderer.py index 36588b30b5..c8a7b0515f 100644 --- a/dash/_dash_renderer.py +++ b/dash/_dash_renderer.py @@ -1,6 +1,6 @@ import os -__version__ = "2.0.2" +__version__ = "2.0.3" _available_react_versions = {"18.3.1", "18.2.0", "16.14.0"} _available_reactdom_versions = {"18.3.1", "18.2.0", "16.14.0"} @@ -64,7 +64,7 @@ def _set_react_version(v_react, v_reactdom=None): { "relative_package_path": "dash-renderer/build/dash_renderer.min.js", "dev_package_path": "dash-renderer/build/dash_renderer.dev.js", - "external_url": "https://unpkg.com/dash-renderer@2.0.2" + "external_url": "https://unpkg.com/dash-renderer@2.0.3" "/build/dash_renderer.min.js", "namespace": "dash", }, diff --git a/dash/dash-renderer/package-lock.json b/dash/dash-renderer/package-lock.json index a4821a1f19..ae73ffd922 100644 --- a/dash/dash-renderer/package-lock.json +++ b/dash/dash-renderer/package-lock.json @@ -1,12 +1,12 @@ { "name": "dash-renderer", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "dash-renderer", - "version": "2.0.2", + "version": "2.0.3", "license": "MIT", "dependencies": { "@babel/polyfill": "^7.12.1", diff --git a/dash/dash-renderer/package.json b/dash/dash-renderer/package.json index 2d481f8522..ba40a968b1 100644 --- a/dash/dash-renderer/package.json +++ b/dash/dash-renderer/package.json @@ -1,6 +1,6 @@ { "name": "dash-renderer", - "version": "2.0.2", + "version": "2.0.3", "description": "render dash components in react", "main": "build/dash_renderer.min.js", "scripts": { diff --git a/dash/version.py b/dash/version.py index 0b609eb386..82ad02aaab 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = "3.0.0rc3" +__version__ = "3.0.0rc4"