Skip to content

Commit d972c02

Browse files
authored
Merge pull request #944 from plotly/persistence-fix
Persistence fix
2 parents ad982ef + f96c3ec commit d972c02

File tree

7 files changed

+194
-34
lines changed

7 files changed

+194
-34
lines changed

Diff for: .circleci/config.yml

+9-3
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ jobs:
108108
name: ️️🏗️ build core
109109
command: |
110110
. venv/bin/activate && pip install --no-cache-dir --upgrade -e . --progress-bar off && mkdir packages
111-
python setup.py sdist && mv dist/* packages/
112111
cd dash-renderer && renderer build && python setup.py sdist && mv dist/* ../packages/ && cd ..
113112
git clone --depth 1 https://github.com/plotly/dash-core-components.git
114113
cd dash-core-components && npm install --ignore-scripts && npm run build && python setup.py sdist && mv dist/* ../packages/ && cd ..
@@ -200,10 +199,17 @@ jobs:
200199
- attach_workspace:
201200
at: ~/dash
202201
- run:
203-
name: 🧪 Run Integration Tests
202+
name: ️️🏗️ Install packages
204203
command: |
205204
. venv/bin/activate && cd packages && ls -la
206-
find . -name "*.gz" | xargs pip install --no-cache-dir --ignore-installed && pip list | grep dash && cd ..
205+
find . -name "*.gz" | xargs pip install --no-cache-dir --ignore-installed && cd ..
206+
sed -i '/dash/d' requires-install.txt
207+
pip install --no-cache-dir --ignore-installed .
208+
pip list | grep dash
209+
- run:
210+
name: 🧪 Run Integration Tests
211+
command: |
212+
. venv/bin/activate
207213
TESTFILES=$(circleci tests glob "tests/integration/**/test_*.py" | circleci tests split --split-by=timings)
208214
pytest --headless --nopercyfinalize --junitxml=test-reports/junit_intg.xml ${TESTFILES}
209215
- store_artifacts:

Diff for: dash-renderer/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
# Unreleased
6+
### Fixed
7+
- [#944](https://github.com/plotly/dash/pull/944) fixed bug with persistence being toggled on/off on an existing component
8+
59
## [1.1.0] - 2019-09-17
610
### Added
711
- [#903](https://github.com/plotly/dash/pull/903) enables props edited by the user to persist across recreating the component or reloading the page. Components need to define three new props: `persistence`, `persisted_props`, and `persistence_type` as described in the lead comment of `src/persistence.js`. App developers then enable this behavior by, in the simplest case, setting `persistence: true` on the component. First use case is table, see [dash-table#566](https://github.com/plotly/dash-table/pull/566)

Diff for: dash-renderer/src/persistence.js

+37-23
Original file line numberDiff line numberDiff line change
@@ -280,38 +280,41 @@ const getValsKey = (id, persistedProp, persistence) =>
280280
`${id}.${persistedProp}.${JSON.stringify(persistence)}`;
281281

282282
const getProps = layout => {
283-
const {props} = layout;
284-
const {id, persistence} = props;
285-
if (!id || !persistence) {
286-
// This component doesn't have persistence. To make downstream
287-
// tests more efficient don't return either one, so we just have to
288-
// test for truthy persistence.
289-
// But we still need to return props for consumers that look for
290-
// nested components
283+
const {props, type, namespace} = layout;
284+
if (!type || !namespace) {
285+
// not a real component - just need the props for recursion
291286
return {props};
292287
}
288+
const {id, persistence} = props;
293289

294290
const element = Registry.resolve(layout);
295-
const persisted_props =
296-
props.persisted_props || element.defaultProps.persisted_props;
297-
const persistence_type =
298-
props.persistence_type || element.defaultProps.persistence_type;
299-
if (!persisted_props || !persistence_type) {
300-
return {props};
301-
}
302-
return {id, props, element, persistence, persisted_props, persistence_type};
291+
const getVal = prop => props[prop] || (element.defaultProps || {})[prop];
292+
const persisted_props = getVal('persisted_props');
293+
const persistence_type = getVal('persistence_type');
294+
const canPersist = id && persisted_props && persistence_type;
295+
296+
return {
297+
canPersist,
298+
id,
299+
props,
300+
element,
301+
persistence,
302+
persisted_props,
303+
persistence_type,
304+
};
303305
};
304306

305307
export function recordUiEdit(layout, newProps, dispatch) {
306308
const {
309+
canPersist,
307310
id,
308311
props,
309312
element,
310313
persistence,
311314
persisted_props,
312315
persistence_type,
313316
} = getProps(layout);
314-
if (!persistence) {
317+
if (!canPersist || !persistence) {
315318
return;
316319
}
317320

@@ -378,6 +381,7 @@ function modProp(key, storage, element, props, persistedProp, update, undo) {
378381

379382
function persistenceMods(layout, component, path, dispatch) {
380383
const {
384+
canPersist,
381385
id,
382386
props,
383387
element,
@@ -387,7 +391,7 @@ function persistenceMods(layout, component, path, dispatch) {
387391
} = getProps(component);
388392

389393
let layoutOut = layout;
390-
if (persistence) {
394+
if (canPersist && persistence) {
391395
const storage = getStore(persistence_type, dispatch);
392396
const update = {};
393397
forEach(
@@ -443,6 +447,7 @@ function persistenceMods(layout, component, path, dispatch) {
443447
*/
444448
export function prunePersistence(layout, newProps, dispatch) {
445449
const {
450+
canPersist,
446451
id,
447452
props,
448453
persistence,
@@ -455,7 +460,7 @@ export function prunePersistence(layout, newProps, dispatch) {
455460
propName in newProps ? newProps[propName] : prevVal;
456461
const finalPersistence = getFinal('persistence', persistence);
457462

458-
if (!persistence && !finalPersistence) {
463+
if (!canPersist || !(persistence || finalPersistence)) {
459464
return newProps;
460465
}
461466

@@ -471,6 +476,8 @@ export function prunePersistence(layout, newProps, dispatch) {
471476

472477
const update = {};
473478

479+
let depersistedProps = props;
480+
474481
if (persistenceChanged && persistence) {
475482
// clear previously-applied persistence
476483
const storage = getStore(persistence_type, dispatch);
@@ -487,6 +494,7 @@ export function prunePersistence(layout, newProps, dispatch) {
487494
),
488495
filter(notInNewProps, persisted_props)
489496
);
497+
depersistedProps = mergeRight(props, update);
490498
}
491499

492500
if (finalPersistence) {
@@ -497,10 +505,10 @@ export function prunePersistence(layout, newProps, dispatch) {
497505
forEach(
498506
persistedProp =>
499507
modProp(
500-
getValsKey(id, persistedProp, persistence),
508+
getValsKey(id, persistedProp, finalPersistence),
501509
finalStorage,
502510
element,
503-
props,
511+
depersistedProps,
504512
persistedProp,
505513
update
506514
),
@@ -516,11 +524,17 @@ export function prunePersistence(layout, newProps, dispatch) {
516524
if (propTransforms) {
517525
for (const propPart in propTransforms) {
518526
finalStorage.removeItem(
519-
getValsKey(id, `${propName}.${propPart}`, persistence)
527+
getValsKey(
528+
id,
529+
`${propName}.${propPart}`,
530+
finalPersistence
531+
)
520532
);
521533
}
522534
} else {
523-
finalStorage.removeItem(getValsKey(id, propName, persistence));
535+
finalStorage.removeItem(
536+
getValsKey(id, propName, finalPersistence)
537+
);
524538
}
525539
}
526540
}

Diff for: dash/CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
## Unreleased
22

33
### Added
4-
4+
- [#944](https://github.com/plotly/dash/pull/944)
5+
- relevant `dash.testing` methods can now be called with either an element or a CSS selector: `select_dcc_dropdown`, `multiple_click`, `clear_input`, `zoom_in_graph_by_ratio`, `click_at_coord_fractions`.
6+
- Three new `dash.testing` methods: `clear_local_storage`, `clear_session_storage`, and `clear_storage` (to clear both together)
57
- [#937](https://github.com/plotly/dash/pull/937) `dash.testing` adds two APIs `zoom_in_graph_by_ratio` and `click_at_coord_fractions` about advanced interactions using mouse `ActionChain`
68
- [#938](https://github.com/plotly/dash/issues/938) Adds debugging traces to dash backend about serving component suites, so we can use it to verify the installed packages whenever in doubt.
79

Diff for: dash/testing/browser.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ def find_elements(self, selector):
145145
"""
146146
return self.driver.find_elements_by_css_selector(selector)
147147

148+
def _get_element(self, elem_or_selector):
149+
if isinstance(elem_or_selector, str):
150+
return self.find_element(elem_or_selector)
151+
return elem_or_selector
152+
148153
def _wait_for(self, method, args, timeout, msg):
149154
"""abstract generic pattern for explicit WebDriverWait"""
150155
_wait = (
@@ -262,8 +267,8 @@ def wait_for_page(self, url=None, timeout=10):
262267
)
263268
)
264269

265-
def select_dcc_dropdown(self, selector, value=None, index=None):
266-
dropdown = self.driver.find_element_by_css_selector(selector)
270+
def select_dcc_dropdown(self, elem_or_selector, value=None, index=None):
271+
dropdown = self._get_element(elem_or_selector)
267272
dropdown.click()
268273

269274
menu = dropdown.find_element_by_css_selector("div.Select-menu-outer")
@@ -416,13 +421,15 @@ def _get_firefox(self):
416421
def _is_windows():
417422
return sys.platform == "win32"
418423

419-
def multiple_click(self, selector, clicks):
424+
def multiple_click(self, elem_or_selector, clicks):
420425
"""multiple_click click the element with number of `clicks`"""
421426
for _ in range(clicks):
422-
self.find_element(selector).click()
427+
self._get_element(elem_or_selector).click()
423428

424-
def clear_input(self, elem):
429+
def clear_input(self, elem_or_selector):
425430
"""simulate key press to clear the input"""
431+
elem = self._get_element(elem_or_selector)
432+
426433
(
427434
ActionChains(self.driver)
428435
.click(elem)
@@ -434,12 +441,18 @@ def clear_input(self, elem):
434441
).perform()
435442

436443
def zoom_in_graph_by_ratio(
437-
self, elem, start_fraction=0.5, zoom_box_fraction=0.2, compare=True
444+
self,
445+
elem_or_selector,
446+
start_fraction=0.5,
447+
zoom_box_fraction=0.2,
448+
compare=True
438449
):
439450
"""zoom out a graph with a zoom box fraction of component dimension
440451
default start at middle with a rectangle of 1/5 of the dimension
441452
use `compare` to control if we check the svg get changed
442453
"""
454+
elem = self._get_element(elem_or_selector)
455+
443456
prev = elem.get_attribute("innerHTML")
444457
w, h = elem.size["width"], elem.size["height"]
445458
try:
@@ -455,7 +468,9 @@ def zoom_in_graph_by_ratio(
455468
"innerHTML"
456469
), "SVG content should be different after zoom"
457470

458-
def click_at_coord_fractions(self, elem, fx, fy):
471+
def click_at_coord_fractions(self, elem_or_selector, fx, fy):
472+
elem = self._get_element(elem_or_selector)
473+
459474
ActionChains(self.driver).move_to_element_with_offset(
460475
elem, elem.size["width"] * fx, elem.size["height"] * fy
461476
).click().perform()

Diff for: dash/testing/dash_page.py

+10
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,13 @@ def get_session_storage(self, session_id="session"):
4949
session_id
5050
)
5151
)
52+
53+
def clear_local_storage(self):
54+
self.driver.execute_script("window.localStorage.clear()")
55+
56+
def clear_session_storage(self):
57+
self.driver.execute_script("window.sessionStorage.clear()")
58+
59+
def clear_storage(self):
60+
self.clear_local_storage()
61+
self.clear_session_storage()

0 commit comments

Comments
 (0)