From 94b7a913ca0d9ce5d4c58539954afef55d21c3a7 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 14 Mar 2022 16:20:23 -0400 Subject: [PATCH 1/9] Add test dropdown remove options. --- .../dropdown/test_remove_option.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 components/dash-core-components/tests/integration/dropdown/test_remove_option.py diff --git a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py new file mode 100644 index 0000000000..7d32e5fcb4 --- /dev/null +++ b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py @@ -0,0 +1,92 @@ +import json + +from dash import Dash, html, dcc, Output, Input +from dash.exceptions import PreventUpdate + + +sample_dropdown_options = [ + {"label": "New York City", "value": "NYC"}, + {"label": "Montreal", "value": "MTL"}, + {"label": "San Francisco", "value": "SF"}, +] + + +def test_ddro001_remove_option_single(dash_dcc): + dropdown_options = sample_dropdown_options + + app = Dash(__name__) + value = 'SF' + + app.layout = html.Div([ + dcc.Dropdown( + options=dropdown_options, + value=value, + id='dropdown', + ), + html.Button('Remove option', id='remove'), + html.Div(id='value-output') + ]) + + @app.callback( + Output('dropdown', 'options'), + [Input('remove', 'n_clicks')] + ) + def on_click(n_clicks): + if not n_clicks: + raise PreventUpdate + return sample_dropdown_options[:-1] + + @app.callback( + Output('value-output', 'children'), + [Input('dropdown', 'value')] + ) + def on_change(val): + if not val: + raise PreventUpdate + return val or 'None' + + dash_dcc.start_server(app) + btn = dash_dcc.wait_for_element('#remove') + btn.click() + + dash_dcc.wait_for_text_to_equal('#value-output', 'None') + + +def test_ddro002_remove_option_multi(dash_dcc): + dropdown_options = sample_dropdown_options + + app = Dash(__name__) + value = ['MTL', 'SF'] + + app.layout = html.Div([ + dcc.Dropdown( + options=dropdown_options, + value=value, + multi=True, + id='dropdown', + ), + html.Button('Remove option', id='remove'), + html.Div(id='value-output') + ]) + + @app.callback( + Output('dropdown', 'options'), + [Input('remove', 'n_clicks')] + ) + def on_click(n_clicks): + if not n_clicks: + raise PreventUpdate + return sample_dropdown_options[:-1] + + @app.callback( + Output('value-output', 'children'), + [Input('dropdown', 'value')] + ) + def on_change(val): + return json.dumps(val) + + dash_dcc.start_server(app) + btn = dash_dcc.wait_for_element('#remove') + btn.click() + + dash_dcc.wait_for_text_to_equal('#value-output', '["MTL"]') From 08fd6302460ee116fbded18bdfe36a8d558b84fc Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 14 Mar 2022 16:47:03 -0400 Subject: [PATCH 2/9] Run black on components from main npm run format. --- .../dropdown/test_remove_option.py | 80 +++++++++---------- .../dash_html_components_base/__init__.py | 22 ++--- components/dash-html-components/setup.py | 20 ++--- .../tests/test_dash_html_components.py | 2 +- .../dash-html-components/tests/utils.py | 12 +-- package.json | 2 +- 6 files changed, 63 insertions(+), 75 deletions(-) diff --git a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py index 7d32e5fcb4..f56371b6ab 100644 --- a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py +++ b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py @@ -15,78 +15,70 @@ def test_ddro001_remove_option_single(dash_dcc): dropdown_options = sample_dropdown_options app = Dash(__name__) - value = 'SF' - - app.layout = html.Div([ - dcc.Dropdown( - options=dropdown_options, - value=value, - id='dropdown', - ), - html.Button('Remove option', id='remove'), - html.Div(id='value-output') - ]) - - @app.callback( - Output('dropdown', 'options'), - [Input('remove', 'n_clicks')] + value = "SF" + + app.layout = html.Div( + [ + dcc.Dropdown( + options=dropdown_options, + value=value, + id="dropdown", + ), + html.Button("Remove option", id="remove"), + html.Div(id="value-output"), + ] ) + + @app.callback(Output("dropdown", "options"), [Input("remove", "n_clicks")]) def on_click(n_clicks): if not n_clicks: raise PreventUpdate return sample_dropdown_options[:-1] - @app.callback( - Output('value-output', 'children'), - [Input('dropdown', 'value')] - ) + @app.callback(Output("value-output", "children"), [Input("dropdown", "value")]) def on_change(val): if not val: raise PreventUpdate - return val or 'None' + return val or "None" dash_dcc.start_server(app) - btn = dash_dcc.wait_for_element('#remove') + btn = dash_dcc.wait_for_element("#remove") btn.click() - dash_dcc.wait_for_text_to_equal('#value-output', 'None') + dash_dcc.wait_for_text_to_equal("#value-output", "None") def test_ddro002_remove_option_multi(dash_dcc): dropdown_options = sample_dropdown_options app = Dash(__name__) - value = ['MTL', 'SF'] - - app.layout = html.Div([ - dcc.Dropdown( - options=dropdown_options, - value=value, - multi=True, - id='dropdown', - ), - html.Button('Remove option', id='remove'), - html.Div(id='value-output') - ]) - - @app.callback( - Output('dropdown', 'options'), - [Input('remove', 'n_clicks')] + value = ["MTL", "SF"] + + app.layout = html.Div( + [ + dcc.Dropdown( + options=dropdown_options, + value=value, + multi=True, + id="dropdown", + ), + html.Button("Remove option", id="remove"), + html.Div(id="value-output"), + ] ) + + @app.callback(Output("dropdown", "options"), [Input("remove", "n_clicks")]) def on_click(n_clicks): if not n_clicks: raise PreventUpdate return sample_dropdown_options[:-1] - @app.callback( - Output('value-output', 'children'), - [Input('dropdown', 'value')] - ) + @app.callback(Output("value-output", "children"), [Input("dropdown", "value")]) def on_change(val): return json.dumps(val) dash_dcc.start_server(app) - btn = dash_dcc.wait_for_element('#remove') + btn = dash_dcc.wait_for_element("#remove") btn.click() - dash_dcc.wait_for_text_to_equal('#value-output', '["MTL"]') + dash_dcc.wait_for_text_to_equal("#value-output", '["MTL"]') diff --git a/components/dash-html-components/dash_html_components_base/__init__.py b/components/dash-html-components/dash_html_components_base/__init__.py index dee9b02959..fb4145dccf 100644 --- a/components/dash-html-components/dash_html_components_base/__init__.py +++ b/components/dash-html-components/dash_html_components_base/__init__.py @@ -33,27 +33,27 @@ _js_dist = [ { - "relative_package_path": 'html/{}.min.js'.format(_this_module), + "relative_package_path": "html/{}.min.js".format(_this_module), "external_url": ( "https://unpkg.com/dash-html-components@{}" "/dash_html_components/dash_html_components.min.js" ).format(__version__), - "namespace": "dash" + "namespace": "dash", }, { - 'relative_package_path': 'html/{}.min.js.map'.format(_this_module), - 'external_url': ( - 'https://unpkg.com/dash-html-components@{}' - '/dash_html_components/dash_html_components.min.js.map' + "relative_package_path": "html/{}.min.js.map".format(_this_module), + "external_url": ( + "https://unpkg.com/dash-html-components@{}" + "/dash_html_components/dash_html_components.min.js.map" ).format(__version__), - 'namespace': 'dash', - 'dynamic': True - } + "namespace": "dash", + "dynamic": True, + }, ] _css_dist = [] for _component in __all__: - setattr(locals()[_component], '_js_dist', _js_dist) - setattr(locals()[_component], '_css_dist', _css_dist) + setattr(locals()[_component], "_js_dist", _js_dist) + setattr(locals()[_component], "_css_dist", _css_dist) diff --git a/components/dash-html-components/setup.py b/components/dash-html-components/setup.py index ecd37e2b84..cf8fc48205 100644 --- a/components/dash-html-components/setup.py +++ b/components/dash-html-components/setup.py @@ -2,22 +2,22 @@ import json from setuptools import setup -with open('package.json') as f: +with open("package.json") as f: package = json.load(f) package_name = str(package["name"].replace(" ", "_").replace("-", "_")) setup( - name='dash_html_components', + name="dash_html_components", version=package["version"], - author=package['author'], - author_email='chris@plotly.com', + author=package["author"], + author_email="chris@plotly.com", packages=[package_name], - url='https://github.com/plotly/dash-html-components', + url="https://github.com/plotly/dash-html-components", include_package_data=True, - license=package['license'], - description=package['description'] if 'description' in package else package_name, - long_description=io.open('README.md', encoding='utf-8').read(), - long_description_content_type='text/markdown', - install_requires=[] + license=package["license"], + description=package["description"] if "description" in package else package_name, + long_description=io.open("README.md", encoding="utf-8").read(), + long_description_content_type="text/markdown", + install_requires=[], ) diff --git a/components/dash-html-components/tests/test_dash_html_components.py b/components/dash-html-components/tests/test_dash_html_components.py index 8ce332d445..206f4fb3e8 100644 --- a/components/dash-html-components/tests/test_dash_html_components.py +++ b/components/dash-html-components/tests/test_dash_html_components.py @@ -23,7 +23,7 @@ def test_imports(): def test_sample_items(): layout = html.Div( html.Div(html.Img(src="https://plotly.com/~chris/1638.png")), - style={"color": "red"} + style={"color": "red"}, ) expected = ( diff --git a/components/dash-html-components/tests/utils.py b/components/dash-html-components/tests/utils.py index 349264fd26..89791183ce 100644 --- a/components/dash-html-components/tests/utils.py +++ b/components/dash-html-components/tests/utils.py @@ -4,18 +4,14 @@ def assert_clean_console(TestClass): def assert_no_console_errors(TestClass): TestClass.assertEqual( - TestClass.driver.execute_script( - 'return window.tests.console.error.length' - ), - 0 + TestClass.driver.execute_script("return window.tests.console.error.length"), + 0, ) def assert_no_console_warnings(TestClass): TestClass.assertEqual( - TestClass.driver.execute_script( - 'return window.tests.console.warn.length' - ), - 0 + TestClass.driver.execute_script("return window.tests.console.warn.length"), + 0, ) assert_no_console_warnings(TestClass) diff --git a/package.json b/package.json index 1c9bfbdcf8..2d717fb1c8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "license": "UNLICENSED", "scripts": { - "private::format.black": "black dash tests --exclude metadata_test.py", + "private::format.black": "black dash tests components --exclude metadata_test.py", "private::format.renderer": "cd dash/dash-renderer && npm run format", "private::initialize.renderer": "cd dash/dash-renderer && npm ci", "private::build.components": "python dash/development/update_components.py 'all'", From 64220367cfc432516cb336b836dcde877dfcd118 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 14 Mar 2022 16:49:24 -0400 Subject: [PATCH 3/9] Refactor Dropdown & Fix multi option removed. --- .../src/fragments/Dropdown.react.js | 174 ++++++++++-------- 1 file changed, 95 insertions(+), 79 deletions(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 43dd15e006..8578110619 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -1,5 +1,5 @@ -import {isNil, pluck, omit, type} from 'ramda'; -import React, {Component} from 'react'; +import {isNil, pluck, omit, type, without} from 'ramda'; +import React, {useState, useCallback, useEffect, useMemo} from 'react'; import ReactDropdown from 'react-virtualized-select'; import createFilterOptions from 'react-select-fast-filter-options'; import '../components/css/react-virtualized-select@3.1.0.css'; @@ -23,88 +23,104 @@ const TOKENIZER = { const DELIMITER = ','; -export default class Dropdown extends Component { - constructor(props) { - super(props); - this.state = { - filterOptions: createFilterOptions({ - options: sanitizeOptions(props.options), +const Dropdown = props => { + const { + id, + clearable, + multi, + options, + setProps, + style, + loading_state, + value, + } = props; + const [optionsCheck, setOptionsCheck] = useState(null); + const [sanitizedOptions, filterOptions] = useMemo(() => { + const sanitized = sanitizeOptions(options); + return [ + sanitized, + createFilterOptions({ + options: options, tokenizer: TOKENIZER, }), - }; - } + ]; + }, [options]); - UNSAFE_componentWillReceiveProps(newProps) { - if (newProps.options !== this.props.options) { - this.setState({ - filterOptions: createFilterOptions({ - options: sanitizeOptions(newProps.options), - tokenizer: TOKENIZER, - }), - }); - } - } + const selectedValue = useMemo( + () => (type(value) === 'Array' ? value.join(DELIMITER) : value), + [value] + ); - render() { - const { - id, - clearable, - multi, - options, - setProps, - style, - loading_state, - value, - } = this.props; - const {filterOptions} = this.state; - let selectedValue; - if (type(value) === 'Array') { - selectedValue = value.join(DELIMITER); - } else { - selectedValue = value; - } - return ( -
{ + if (multi) { + let value; + if (isNil(selectedOption)) { + value = []; + } else { + value = pluck('value', selectedOption); } - > - { - if (multi) { - let value; - if (isNil(selectedOption)) { - value = []; - } else { - value = pluck('value', selectedOption); - } - setProps({value}); - } else { - let value; - if (isNil(selectedOption)) { - value = null; - } else { - value = selectedOption.value; - } - setProps({value}); - } - }} - onInputChange={search_value => setProps({search_value})} - backspaceRemoves={clearable} - deleteRemoves={clearable} - inputProps={{autoComplete: 'off'}} - {...omit(['setProps', 'value', 'options'], this.props)} - /> -
- ); - } -} + setProps({value}); + } else { + let value; + if (isNil(selectedOption)) { + value = null; + } else { + value = selectedOption.value; + } + setProps({value}); + } + }, + [multi] + ); + + const onInputChange = useCallback( + search_value => setProps({search_value}), + [] + ); + + useEffect(() => { + if (optionsCheck !== sanitizedOptions && !isNil(value)) { + const values = sanitizedOptions.map(option => option.value); + if (multi) { + const invalids = value.filter(v => !values.includes(v)); + if (invalids.length) { + setProps({value: without(invalids, value)}); + } + } else { + if (!values.includes(selectedValue)) { + setProps({value: null}); + } + } + setOptionsCheck(sanitizedOptions); + } + }, [sanitizedOptions, optionsCheck, multi, value, selectedValue]); + + return ( +
+ +
+ ); +}; Dropdown.propTypes = propTypes; Dropdown.defaultProps = defaultProps; + +export default Dropdown; From 0801dad01659e9a77e5c53e578c551c0703c9e56 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 15 Mar 2022 09:12:12 -0400 Subject: [PATCH 4/9] Handle case where multi is true and value is not an array. --- .../dash-core-components/src/fragments/Dropdown.react.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 8578110619..59faf171ee 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -40,7 +40,7 @@ const Dropdown = props => { return [ sanitized, createFilterOptions({ - options: options, + options: sanitized, tokenizer: TOKENIZER, }), ]; @@ -82,7 +82,7 @@ const Dropdown = props => { useEffect(() => { if (optionsCheck !== sanitizedOptions && !isNil(value)) { const values = sanitizedOptions.map(option => option.value); - if (multi) { + if (multi && Array.isArray(value)) { const invalids = value.filter(v => !values.includes(v)); if (invalids.length) { setProps({value: without(invalids, value)}); From 136f12041cbc43973517a3475ba6834a7b17c45d Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 15 Mar 2022 13:04:32 -0400 Subject: [PATCH 5/9] Update components/dash-core-components/src/fragments/Dropdown.react.js Fix autoComplete typo. Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com> --- components/dash-core-components/src/fragments/Dropdown.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 59faf171ee..1f41d61cd7 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -113,7 +113,7 @@ const Dropdown = props => { onInputChange={onInputChange} backspaceRemoves={clearable} deleteRemoves={clearable} - inputProps={{autoComplete: 'of'}} + inputProps={{autoComplete: 'off'}} {...omit(['setProps', 'value', 'options'], props)} /> From 7eaa9be046e5980a0d69796c385605d257a00e5d Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 15 Mar 2022 13:45:04 -0400 Subject: [PATCH 6/9] Fix Dropdown comma in value. #1908 --- .../src/fragments/Dropdown.react.js | 15 ++++----------- .../dropdown/test_dynamic_options.py | 18 ++++++------------ 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 1f41d61cd7..599610dfc1 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -1,4 +1,4 @@ -import {isNil, pluck, omit, type, without} from 'ramda'; +import {isNil, pluck, omit, without} from 'ramda'; import React, {useState, useCallback, useEffect, useMemo} from 'react'; import ReactDropdown from 'react-virtualized-select'; import createFilterOptions from 'react-select-fast-filter-options'; @@ -21,8 +21,6 @@ const TOKENIZER = { }, }; -const DELIMITER = ','; - const Dropdown = props => { const { id, @@ -46,11 +44,6 @@ const Dropdown = props => { ]; }, [options]); - const selectedValue = useMemo( - () => (type(value) === 'Array' ? value.join(DELIMITER) : value), - [value] - ); - const onChange = useCallback( selectedOption => { if (multi) { @@ -88,13 +81,13 @@ const Dropdown = props => { setProps({value: without(invalids, value)}); } } else { - if (!values.includes(selectedValue)) { + if (!values.includes(value)) { setProps({value: null}); } } setOptionsCheck(sanitizedOptions); } - }, [sanitizedOptions, optionsCheck, multi, value, selectedValue]); + }, [sanitizedOptions, optionsCheck, multi, value]); return (
{ Date: Tue, 15 Mar 2022 13:56:45 -0400 Subject: [PATCH 7/9] Update changelog for 1970. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab0315eb0..eb46a1c33d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ This project adheres to [Semantic Versioning](https://semver.org/). - Upgrade `black` to v22.3.0 for Python 3.7+ - if you use `dash[ci]` and you call `black`, this may alter your code formatting slightly, including more consistently breaking Python 2 compatibility. - Many other mainly JS dependency upgrades to the internals of Dash renderer and components. These may patch bugs or improve performance. +### Fixed + +- [#1970](https://github.com/plotly/dash/pull/1970) dcc.Dropdown Refactor fixes: + - Fix bug [#1868](https://github.com/plotly/dash/issues/1868) value does not update when selected option removed from options. + - Fix bug [#1908](https://github.com/plotly/dash/issues/1908) Selected options not showing when the value contains a comma. + ## [2.3.1] - 2022-03-29 ### Fixed From 5ce9455e618180c8e8620ec3ae34d5714aa59610 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 21 Apr 2022 11:38:11 -0400 Subject: [PATCH 8/9] Add private::format.dcc root command. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d717fb1c8..383a2240c0 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,9 @@ "private": true, "license": "UNLICENSED", "scripts": { - "private::format.black": "black dash tests components --exclude metadata_test.py", + "private::format.black": "black dash tests --exclude metadata_test.py", "private::format.renderer": "cd dash/dash-renderer && npm run format", + "private::format.dcc": "cd components/dash-core-components && npm run format", "private::initialize.renderer": "cd dash/dash-renderer && npm ci", "private::build.components": "python dash/development/update_components.py 'all'", "private::build.renderer": "cd dash/dash-renderer && renderer build", From ed5942ea58ec859cdc0f723e00108167135b406f Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 21 Apr 2022 11:40:13 -0400 Subject: [PATCH 9/9] Replace omit with pick --- .../src/fragments/Dropdown.react.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 599610dfc1..c7ca287d59 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -1,4 +1,4 @@ -import {isNil, pluck, omit, without} from 'ramda'; +import {isNil, pluck, without, pick} from 'ramda'; import React, {useState, useCallback, useEffect, useMemo} from 'react'; import ReactDropdown from 'react-virtualized-select'; import createFilterOptions from 'react-select-fast-filter-options'; @@ -21,6 +21,18 @@ const TOKENIZER = { }, }; +const RDProps = [ + 'multi', + 'clearable', + 'searchable', + 'search_value', + 'placeholder', + 'disabled', + 'optionHeight', + 'style', + 'className', +]; + const Dropdown = props => { const { id, @@ -107,7 +119,7 @@ const Dropdown = props => { backspaceRemoves={clearable} deleteRemoves={clearable} inputProps={{autoComplete: 'off'}} - {...omit(['setProps', 'value', 'options'], props)} + {...pick(RDProps, props)} />
);