diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa7f20..0115265 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [UNRELEASED] +### Changed +- Undo/redo toolbar is removed by default, unless `config.show_undo_redo=true` is provided. The CSS hack `._dash-undo-redo:{display:none;}` is no longer needed [#175](https://github.com/plotly/dash-renderer/pull/175) + ## [0.24.0] - 2019-05-15 ### Fixed - Fix regression on handling PreventUpdate (204 NO CONTENT) [#170](https://github.com/plotly/dash-renderer/pull/170) diff --git a/src/AppContainer.react.js b/src/AppContainer.react.js index 1c65b35..3b436af 100644 --- a/src/AppContainer.react.js +++ b/src/AppContainer.react.js @@ -30,9 +30,10 @@ class UnconnectedAppContainer extends React.Component { if (type(config) === 'Null') { return
Loading...
; } + const {show_undo_redo} = config; return ( - + {show_undo_redo ? : null} diff --git a/src/actions/index.js b/src/actions/index.js index c2fd5cd..b70ffa2 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -131,34 +131,20 @@ export function redo() { }; } +const UNDO = createAction('UNDO')(); export function undo() { - return function(dispatch, getState) { - const history = getState().history; - dispatch(createAction('UNDO')()); - const previous = history.past[history.past.length - 1]; - - // Update props - dispatch( - createAction('UNDO_PROP_CHANGE')({ - itempath: getState().paths[previous.id], - props: previous.props, - }) - ); - - // Notify observers - dispatch( - notifyObservers({ - id: previous.id, - props: previous.props, - }) - ); - }; + return undo_revert(UNDO); } +const REVERT = createAction('REVERT')(); export function revert() { + return undo_revert(REVERT); +} + +function undo_revert(undo_or_revert) { return function(dispatch, getState) { const history = getState().history; - dispatch(createAction('REVERT')()); + dispatch(undo_or_revert); const previous = history.past[history.past.length - 1]; // Update props diff --git a/tests/test_assets/initial_state_dash_app_content.html b/tests/test_assets/initial_state_dash_app_content.html index 43180f4..16ab434 100644 --- a/tests/test_assets/initial_state_dash_app_content.html +++ b/tests/test_assets/initial_state_dash_app_content.html @@ -1 +1 @@ -
Basic string3.14
Child div with basic string
Grandchild div
Great grandchild
3.14159another basic string
\ No newline at end of file +
Basic string3.14
Child div with basic string
Grandchild div
Great grandchild
3.14159another basic string
diff --git a/tests/test_render.py b/tests/test_render.py index ce6ba5a..9c8eca6 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -1,3 +1,4 @@ +# -*- coding: UTF-8 -*- import os import textwrap @@ -109,7 +110,6 @@ def request_queue_assertions( if expected_length is not None: self.assertEqual(len(request_queue), expected_length) - def test_initial_state(self): app = Dash(__name__) my_class_attrs = { @@ -154,11 +154,12 @@ def test_initial_state(self): self.startServer(app) el = self.wait_for_element_by_css_selector('#react-entry-point') + # Note: this .html file shows there's no undo/redo button by default _dash_app_content_html = os.path.join( os.path.dirname(__file__), 'test_assets', 'initial_state_dash_app_content.html') with open(_dash_app_content_html) as fp: - rendered_dom = BeautifulSoup(fp.read(), 'lxml') + rendered_dom = BeautifulSoup(fp.read().strip(), 'lxml') fetched_dom = BeautifulSoup(el.get_attribute('outerHTML'), 'lxml') self.assertEqual( @@ -211,6 +212,79 @@ def test_initial_state(self): self.assertTrue(self.is_console_clean()) + def click_undo(self): + undo_selector = '._dash-undo-redo span:first-child div:last-child' + undo = self.wait_for_element_by_css_selector(undo_selector) + self.wait_for_text_to_equal(undo_selector, 'undo') + undo.click() + + def click_redo(self): + redo_selector = '._dash-undo-redo span:last-child div:last-child' + self.wait_for_text_to_equal(redo_selector, 'redo') + redo = self.wait_for_element_by_css_selector(redo_selector) + redo.click() + + def check_undo_redo_exist(self, has_undo, has_redo): + selector = '._dash-undo-redo span div:last-child' + els = self.driver.find_elements_by_css_selector(selector) + texts = (['undo'] if has_undo else []) + (['redo'] if has_redo else []) + + self.assertEqual(len(els), len(texts)) + for el, text in zip(els, texts): + self.assertEqual(el.text, text) + + def test_undo_redo(self): + app = Dash(__name__, show_undo_redo=True) + app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) + + @app.callback(Output('b', 'children'), [Input('a', 'value')]) + def set_b(a): + return a + + self.startServer(app) + + a = self.wait_for_element_by_css_selector('#a') + a.send_keys('xyz') + + self.wait_for_text_to_equal('#b', 'xyz') + self.check_undo_redo_exist(True, False) + + self.click_undo() + self.wait_for_text_to_equal('#b', 'xy') + self.check_undo_redo_exist(True, True) + + self.click_undo() + self.wait_for_text_to_equal('#b', 'x') + self.check_undo_redo_exist(True, True) + + self.click_redo() + self.wait_for_text_to_equal('#b', 'xy') + self.check_undo_redo_exist(True, True) + + self.percy_snapshot(name='undo-redo') + + self.click_undo() + self.click_undo() + self.wait_for_text_to_equal('#b', '') + self.check_undo_redo_exist(False, True) + + def test_no_undo_redo(self): + app = Dash(__name__) + app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) + + @app.callback(Output('b', 'children'), [Input('a', 'value')]) + def set_b(a): + return a + + self.startServer(app) + + a = self.wait_for_element_by_css_selector('#a') + a.send_keys('xyz') + + self.wait_for_text_to_equal('#b', 'xyz') + toolbar = self.driver.find_elements_by_css_selector('._dash-undo-redo') + self.assertEqual(len(toolbar), 0) + def test_array_of_falsy_child(self): app = Dash(__name__) app.layout = html.Div(id='nully-wrapper', children=[0]) @@ -328,10 +402,9 @@ def update_input(value): '#react-entry-point').get_attribute('innerHTML'), 'lxml').select_one('#output > div').contents - self.assertTrue( - pad_input.attrs == {'type': 'text', 'id': 'sub-input-1', 'value': 'sub input initial value'} - and pad_input.name == 'input', - "pad input is correctly rendered") + self.assertEqual(pad_input.attrs['value'], 'sub input initial value') + self.assertEqual(pad_input.attrs['id'], 'sub-input-1') + self.assertEqual(pad_input.name, 'input') self.assertTrue( pad_div.text == pad_input.attrs['value']