Skip to content

Commit 7fb63e8

Browse files
authored
Merge branch 'dev' into requires-dir
2 parents 9519aaa + 4a9f09a commit 7fb63e8

File tree

14 files changed

+275
-30
lines changed

14 files changed

+275
-30
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## UNRELEASED
6+
7+
## Fixed
8+
9+
- [#2860](https://github.com/plotly/dash/pull/2860) Fix dcc.Loading to apply overlay_style only to the children and not the spinner. Fixes [#2858](https://github.com/plotly/dash/issues/2858)
10+
- [#2854](https://github.com/plotly/dash/pull/2854) Fix dcc.Dropdown resetting empty values to null and triggering callbacks. Fixes [#2850](https://github.com/plotly/dash/issues/2850)
11+
- [#2859](https://github.com/plotly/dash/pull/2859) Fix base patch operators. fixes [#2855](https://github.com/plotly/dash/issues/2855)
12+
- [#2856](https://github.com/plotly/dash/pull/2856) Fix multiple consecutive calls with same id to set_props only keeping the last props. Fixes [#2852](https://github.com/plotly/dash/issues/2852)
13+
- [#2867](https://github.com/plotly/dash/pull/2867) Fix clientside no output callback. Fixes [#2866](https://github.com/plotly/dash/issues/2866)
14+
515
## [2.17.0] - 2024-05-03
616

717
## Added

components/dash-core-components/src/components/Loading.react.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import DefaultSpinner from '../fragments/Loading/spinners/DefaultSpinner.jsx';
55
import CubeSpinner from '../fragments/Loading/spinners/CubeSpinner.jsx';
66
import CircleSpinner from '../fragments/Loading/spinners/CircleSpinner.jsx';
77
import DotSpinner from '../fragments/Loading/spinners/DotSpinner.jsx';
8-
import {mergeRight} from 'ramda';
98

109
const spinnerComponentOptions = {
1110
graph: GraphSpinner,
@@ -49,10 +48,6 @@ const Loading = ({
4948
justifyContent: 'center',
5049
alignItems: 'center',
5150
};
52-
const hiddenContainer = mergeRight(
53-
{visibility: 'hidden', position: 'relative'},
54-
overlay_style
55-
);
5651

5752
/* Overrides default Loading behavior if target_components is set. By default,
5853
* Loading fires when any recursive child enters loading state. This makes loading
@@ -132,14 +127,23 @@ const Loading = ({
132127

133128
return (
134129
<div
130+
style={{position: 'relative', ...parent_style}}
135131
className={parent_className}
136-
style={
137-
showSpinner
138-
? mergeRight(hiddenContainer, parent_style)
139-
: parent_style
140-
}
141132
>
142-
{children}
133+
<div
134+
className={parent_className}
135+
style={
136+
showSpinner
137+
? {
138+
visibility: 'hidden',
139+
...overlay_style,
140+
...parent_style,
141+
}
142+
: parent_style
143+
}
144+
>
145+
{children}
146+
</div>
143147
<div style={showSpinner ? coveringSpinner : {}}>
144148
{showSpinner &&
145149
(custom_spinner || (

components/dash-core-components/src/fragments/Dropdown.react.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {isNil, pluck, without, pick} from 'ramda';
1+
import {isNil, pluck, without, pick, isEmpty} from 'ramda';
22
import React, {useState, useCallback, useEffect, useMemo, useRef} from 'react';
33
import ReactDropdown from 'react-virtualized-select';
44
import createFilterOptions from 'react-select-fast-filter-options';
@@ -125,7 +125,8 @@ const Dropdown = props => {
125125
!search_value &&
126126
!isNil(sanitizedOptions) &&
127127
optionsCheck !== sanitizedOptions &&
128-
!isNil(value)
128+
!isNil(value) &&
129+
!isEmpty(value)
129130
) {
130131
const values = sanitizedOptions.map(option => option.value);
131132
if (multi && Array.isArray(value)) {

components/dash-core-components/tests/integration/dropdown/test_remove_option.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
import pytest
44

5-
from dash import Dash, html, dcc, Output, Input, State
5+
from dash import Dash, html, dcc, Output, Input, State, Patch
66
from dash.exceptions import PreventUpdate
77

8+
from selenium.webdriver.common.keys import Keys
89

910
sample_dropdown_options = [
1011
{"label": "New York City", "value": "NYC"},
@@ -147,3 +148,44 @@ def print_value(n_clicks, options, value):
147148
btn.click()
148149
dash_dcc.wait_for_text_to_equal("#value-output", '["NYC"]')
149150
dash_dcc.wait_for_text_to_equal("#options-output", '["MTL", "NYC"]')
151+
152+
153+
def test_ddro004_empty_string_not_updated(dash_dcc):
154+
# The value should stay as empty string and not null.
155+
app = Dash()
156+
app.layout = html.Div(
157+
[
158+
dcc.Dropdown(["a", "b", "c"], value="", id="drop"),
159+
html.Div(id="output"),
160+
dcc.Store(data={"count": 0}, id="count"),
161+
html.Div(id="count-output"),
162+
]
163+
)
164+
165+
@app.callback(
166+
Output("output", "children"),
167+
Output("count", "data"),
168+
Input("drop", "value"),
169+
)
170+
def on_value(value):
171+
count = Patch()
172+
count.count += 1
173+
if value is None:
174+
return "Value is none", count
175+
return f"Value={value}", count
176+
177+
app.clientside_callback(
178+
"data => data.count", Output("count-output", "children"), Input("count", "data")
179+
)
180+
181+
dash_dcc.start_server(app)
182+
dash_dcc.wait_for_text_to_equal("#output", "Value=")
183+
184+
dash_dcc.wait_for_text_to_equal("#count-output", "1")
185+
186+
select_input = dash_dcc.find_element("#drop input")
187+
select_input.send_keys("a")
188+
select_input.send_keys(Keys.ENTER)
189+
190+
dash_dcc.wait_for_text_to_equal("#output", "Value=a")
191+
dash_dcc.wait_for_text_to_equal("#count-output", "2")

components/dash-core-components/tests/integration/loading/test_loading_component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ def updateDiv(n_clicks):
406406
dash_dcc.find_element(".loading .dash-spinner")
407407
# unlike the default, the content should be visible
408408
dash_dcc.wait_for_text_to_equal("#div-1", "content")
409-
dash_dcc.wait_for_style_to_equal("#root > div", "opacity", "0.5")
409+
dash_dcc.wait_for_style_to_equal("#root > div > div", "opacity", "0.5")
410410

411411
dash_dcc.wait_for_text_to_equal("#div-1", "changed")
412412

dash/_callback.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ def register_clientside_callback(
549549
**kwargs,
550550
):
551551
output, inputs, state, prevent_initial_call = handle_callback_args(args, kwargs)
552+
no_output = isinstance(output, (list,)) and len(output) == 0
552553
insert_callback(
553554
callback_list,
554555
callback_map,
@@ -559,6 +560,7 @@ def register_clientside_callback(
559560
state,
560561
None,
561562
prevent_initial_call,
563+
no_output=no_output,
562564
)
563565

564566
# If JS source is explicitly given, create a namespace and function

dash/_callback_context.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,11 @@ def timing_information(self):
252252
def set_props(self, component_id: typing.Union[str, dict], props: dict):
253253
ctx_value = _get_context_value()
254254
_id = stringify_id(component_id)
255-
ctx_value.updated_props[_id] = props
255+
existing = ctx_value.updated_props.get(_id)
256+
if existing is not None:
257+
ctx_value.updated_props[_id] = {**existing, **props}
258+
else:
259+
ctx_value.updated_props[_id] = props
256260

257261

258262
callback_context = CallbackContext()

dash/_patch.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,18 @@ def __getstate__(self):
3636
def __setstate__(self, state):
3737
vars(self).update(state)
3838

39-
def __getitem__(self, item):
39+
def __getitem__(self, item) -> "Patch":
4040
validate_slice(item)
4141
return Patch(location=self._location + [item], parent=self)
4242

43-
def __getattr__(self, item):
43+
def __getattr__(self, item) -> "Patch":
4444
if item == "tolist":
4545
# to_json fix
4646
raise AttributeError
4747
if item == "_location":
48-
return self._location
48+
return self._location # type: ignore
4949
if item == "_operations":
50-
return self._operations
50+
return self._operations # type: ignore
5151
return self.__getitem__(item)
5252

5353
def __setattr__(self, key, value):
@@ -81,22 +81,32 @@ def __iadd__(self, other):
8181
self.extend(other)
8282
else:
8383
self._operations.append(_operation("Add", self._location, value=other))
84+
if not self._location:
85+
return self
8486
return _noop
8587

8688
def __isub__(self, other):
8789
self._operations.append(_operation("Sub", self._location, value=other))
90+
if not self._location:
91+
return self
8892
return _noop
8993

9094
def __imul__(self, other):
9195
self._operations.append(_operation("Mul", self._location, value=other))
96+
if not self._location:
97+
return self
9298
return _noop
9399

94100
def __itruediv__(self, other):
95101
self._operations.append(_operation("Div", self._location, value=other))
102+
if not self._location:
103+
return self
96104
return _noop
97105

98106
def __ior__(self, other):
99107
self.update(E=other)
108+
if not self._location:
109+
return self
100110
return _noop
101111

102112
def __iter__(self):

dash/dash-renderer/src/actions/callbacks.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,16 +278,18 @@ async function handleClientside(
278278
returnValue = await returnValue;
279279
}
280280

281-
zipIfArray(outputs, returnValue).forEach(([outi, reti]) => {
282-
zipIfArray(outi, reti).forEach(([outij, retij]) => {
283-
const {id, property} = outij;
284-
const idStr = stringifyId(id);
285-
const dataForId = (result[idStr] = result[idStr] || {});
286-
if (retij !== dc.no_update) {
287-
dataForId[cleanOutputProp(property)] = retij;
288-
}
281+
if (outputs) {
282+
zipIfArray(outputs, returnValue).forEach(([outi, reti]) => {
283+
zipIfArray(outi, reti).forEach(([outij, retij]) => {
284+
const {id, property} = outij;
285+
const idStr = stringifyId(id);
286+
const dataForId = (result[idStr] = result[idStr] || {});
287+
if (retij !== dc.no_update) {
288+
dataForId[cleanOutputProp(property)] = retij;
289+
}
290+
});
289291
});
290-
});
292+
}
291293
} catch (e) {
292294
if (e === dc.PreventUpdate) {
293295
status = STATUS.PREVENT_UPDATE;

dash/long_callback/_proxy_set_props.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ class ProxySetProps(dict):
77
def __init__(self, on_change):
88
super().__init__()
99
self.on_change = on_change
10+
self._data = {}
1011

1112
def __setitem__(self, key, value):
1213
self.on_change(key, value)
14+
self._data.setdefault(key, {})
15+
self._data[key] = {**self._data[key], **value}
16+
17+
def get(self, key):
18+
return self._data.get(key)

0 commit comments

Comments
 (0)