From 97cc370ea8ba834fcf45486b301e5e07d26a69ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Rivet?= Date: Mon, 17 Jun 2019 09:51:24 -0400 Subject: [PATCH] Regression fix multi table typing (#468) --- packages/dash-table/demo/App.js | 75 ----------------- packages/dash-table/demo/App.tsx | 84 +++++++++++++++++++ packages/dash-table/demo/AppMode.ts | 2 + .../src/dash-table/dash/DataTable.js | 5 +- .../dash/{sanitize.ts => Sanitizer.ts} | 56 +++++++------ .../tests/standalone/edit_cell_test.ts | 51 +++++++---- .../cypress/tests/unit/formatting_test.ts | 2 +- 7 files changed, 157 insertions(+), 118 deletions(-) delete mode 100644 packages/dash-table/demo/App.js create mode 100644 packages/dash-table/demo/App.tsx rename packages/dash-table/src/dash-table/dash/{sanitize.ts => Sanitizer.ts} (53%) diff --git a/packages/dash-table/demo/App.js b/packages/dash-table/demo/App.js deleted file mode 100644 index 6c189180db..0000000000 --- a/packages/dash-table/demo/App.js +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint no-magic-numbers: 0 */ -import * as R from 'ramda'; -import React, {Component} from 'react'; -import { DataTable } from 'dash-table'; -import Environment from 'core/environment'; -import { memoizeOne } from 'core/memoizer'; -import Logger from 'core/Logger'; -import AppState, { AppMode } from './AppMode'; - -import './style.less'; - -class App extends Component { - constructor() { - super(); - - this.state = AppState; - this.state.temp_filtering = ''; - - const setProps = memoizeOne(() => { - return newProps => { - Logger.debug('--->', newProps); - this.setState(prevState => ({ - tableProps: R.merge(prevState.tableProps, newProps) - })); - }; - }); - - Object.defineProperty(this, 'setProps', { - get: () => setProps() - }); - } - - renderMode() { - const mode = Environment.searchParams.get('mode'); - - if (mode === AppMode.Filtering) { - return ( -
- - this.setState({ temp_filtering: e.target.value }) - } - onBlur={e => { - const tableProps = R.clone(this.state.tableProps); - tableProps.filter_query = e.target.value; - - this.setState({ tableProps }); - }} /> -
); - } - } - - render() { - return (
- {this.renderMode()} - -
); - } -} - -export default App; diff --git a/packages/dash-table/demo/App.tsx b/packages/dash-table/demo/App.tsx new file mode 100644 index 0000000000..160d0a341d --- /dev/null +++ b/packages/dash-table/demo/App.tsx @@ -0,0 +1,84 @@ +/* eslint no-magic-numbers: 0 */ +import * as R from 'ramda'; +import React, {Component} from 'react'; +import { DataTable } from 'dash-table/index'; +import Environment from 'core/environment'; +import { memoizeOne } from 'core/memoizer'; +import Logger from 'core/Logger'; +import AppState, { AppMode } from './AppMode'; +import memoizerCache from 'core/cache/memoizer'; + +import './style.less'; + +class App extends Component { + constructor(props: any) { + super(props); + + this.state = { + ...AppState, + temp_filtering: '' + }; + } + + renderMode() { + const mode = Environment.searchParams.get('mode'); + + if (mode === AppMode.Filtering) { + return (
+ + this.setState({ temp_filtering: e.target.value }) + } + onBlur={e => { + const tableProps = R.clone(this.state.tableProps); + tableProps.filter_query = e.target.value; + + this.setState({ tableProps }); + }} /> +
); + } else if (mode === AppMode.TaleOfTwoTables) { + const props: any = {}; + Object.entries(this.state.tableProps).forEach(([key, value]) => { + props[key] = this.propCache.get(key)(value); + }); + + return (); + } + } + + render() { + return (
+ {this.renderMode()} + +
); + } + + private propCache = memoizerCache<[string]>()(R.clone); + + private setProps = memoizeOne(() => { + return (newProps: any) => { + Logger.debug('--->', newProps); + this.setState((prevState: any) => ({ + tableProps: R.merge(prevState.tableProps, newProps) + })); + }; + }); +} + +export default App; diff --git a/packages/dash-table/demo/AppMode.ts b/packages/dash-table/demo/AppMode.ts index 97aec6b104..06f3fa4706 100644 --- a/packages/dash-table/demo/AppMode.ts +++ b/packages/dash-table/demo/AppMode.ts @@ -22,6 +22,7 @@ export enum AppMode { Formatting = 'formatting', ReadOnly = 'readonly', ColumnsInSpace = 'columnsInSpace', + TaleOfTwoTables = 'taleOfTwoTables', Tooltips = 'tooltips', Typed = 'typed', Virtualized = 'virtualized' @@ -335,6 +336,7 @@ function getState() { return getVirtualizedState(); case AppMode.Typed: return getTypedState(); + case AppMode.TaleOfTwoTables: case AppMode.Default: default: return getDefaultState(); diff --git a/packages/dash-table/src/dash-table/dash/DataTable.js b/packages/dash-table/src/dash-table/dash/DataTable.js index c95266b2e6..8f986558bd 100644 --- a/packages/dash-table/src/dash-table/dash/DataTable.js +++ b/packages/dash-table/src/dash-table/dash/DataTable.js @@ -8,13 +8,14 @@ import Logger from 'core/Logger'; import genRandomId from 'dash-table/utils/generate'; import isValidProps from './validate'; -import sanitizeProps from './sanitize'; +import Sanitizer from './Sanitizer'; export default class DataTable extends Component { constructor(props) { super(props); let id; this.getId = () => (id = id || genRandomId('table-')); + this.sanitizer = new Sanitizer(); } render() { @@ -22,7 +23,7 @@ export default class DataTable extends Component { return (
Invalid props combination
); } - const sanitizedProps = sanitizeProps(this.props); + const sanitizedProps = this.sanitizer.sanitize(this.props); return this.props.id ? () : (); diff --git a/packages/dash-table/src/dash-table/dash/sanitize.ts b/packages/dash-table/src/dash-table/dash/Sanitizer.ts similarity index 53% rename from packages/dash-table/src/dash-table/dash/sanitize.ts rename to packages/dash-table/src/dash-table/dash/Sanitizer.ts index 0e96a24445..ea35b471d0 100644 --- a/packages/dash-table/src/dash-table/dash/sanitize.ts +++ b/packages/dash-table/src/dash-table/dash/Sanitizer.ts @@ -29,23 +29,6 @@ const D3_DEFAULT_LOCALE: INumberLocale = { const DEFAULT_NULLY = ''; const DEFAULT_SPECIFIER = ''; -const applyDefaultToLocale = memoizeOne((locale: INumberLocale) => getLocale(locale)); - -const applyDefaultsToColumns = memoizeOne( - (defaultLocale: INumberLocale, defaultSort: SortAsNull, columns: Columns, editable: boolean ) => R.map(column => { - const c = R.clone(column); - c.editable = isEditable(editable, column.editable); - c.sort_as_null = c.sort_as_null || defaultSort; - - if (c.type === ColumnType.Numeric && c.format) { - c.format.locale = getLocale(defaultLocale, c.format.locale); - c.format.nully = getNully(c.format.nully); - c.format.specifier = getSpecifier(c.format.specifier); - } - return c; - }, columns) -); - const data2number = (data?: any) => +data || 0; const getFixedColumns = ( @@ -64,16 +47,37 @@ const getFixedRows = ( 0 : headerRows(columns) + (filter_action !== TableAction.None ? 1 : 0) + data2number(fixed.data); -export default (props: PropsWithDefaults): SanitizedProps => { - const locale_format = applyDefaultToLocale(props.locale_format); +const applyDefaultsToColumns = (defaultLocale: INumberLocale, defaultSort: SortAsNull, columns: Columns, editable: boolean) => R.map(column => { + const c = R.clone(column); + c.editable = isEditable(editable, column.editable); + c.sort_as_null = c.sort_as_null || defaultSort; - return R.merge(props, { - columns: applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable), - fixed_columns: getFixedColumns(props.fixed_columns, props.row_deletable, props.row_selectable), - fixed_rows: getFixedRows(props.fixed_rows, props.columns, props.filter_action), - locale_format - }); -}; + if (c.type === ColumnType.Numeric && c.format) { + c.format.locale = getLocale(defaultLocale, c.format.locale); + c.format.nully = getNully(c.format.nully); + c.format.specifier = getSpecifier(c.format.specifier); + } + return c; +}, columns); + +const applyDefaultToLocale = (locale: INumberLocale) => getLocale(locale); + +export default class Sanitizer { + sanitize(props: PropsWithDefaults): SanitizedProps { + const locale_format = this.applyDefaultToLocale(props.locale_format); + + return R.merge(props, { + columns: this.applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable), + fixed_columns: getFixedColumns(props.fixed_columns, props.row_deletable, props.row_selectable), + fixed_rows: getFixedRows(props.fixed_rows, props.columns, props.filter_action), + locale_format + }); + } + + private readonly applyDefaultToLocale = memoizeOne(applyDefaultToLocale); + + private readonly applyDefaultsToColumns = memoizeOne(applyDefaultsToColumns); +} export const getLocale = (...locales: Partial[]): INumberLocale => R.mergeAll([ diff --git a/packages/dash-table/tests/cypress/tests/standalone/edit_cell_test.ts b/packages/dash-table/tests/cypress/tests/standalone/edit_cell_test.ts index 1520630889..7f298f452c 100644 --- a/packages/dash-table/tests/cypress/tests/standalone/edit_cell_test.ts +++ b/packages/dash-table/tests/cypress/tests/standalone/edit_cell_test.ts @@ -4,6 +4,43 @@ import Key from 'cypress/Key'; import { AppMode, ReadWriteModes } from 'demo/AppMode'; +Object.values([ + ...ReadWriteModes, + AppMode.TaleOfTwoTables +]).forEach(mode => { + describe(`edit, mode=${mode}`, () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${mode}`); + DashTable.toggleScroll(false); + }); + + // Case: "Pressing enter to confirm change does not work on the last row" + // Issue: https://github.com/plotly/dash-table/issues/50 + it('can edit on "enter"', () => { + DashTable.getCell(0, 1).click(); + // Case: 2+ tables results in infinite rendering loop b/c of shared cache + // Issue: https://github.com/plotly/dash-table/pull/468 + // + // Artificial delay added to make sure re-rendering has time to occur. + cy.wait(1000); + DOM.focused.type(`abc${Key.Enter}`); + DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('have.html', `abc`)); + }); + + it('can edit when clicking outside of cell', () => { + DashTable.getCell(0, 1).click(); + DOM.focused.type(`abc`); + // Case: 2+ tables results in infinite rendering loop b/c of shared cache + // Issue: https://github.com/plotly/dash-table/pull/468 + // + // Artificial delay added to make sure re-rendering has time to occur. + cy.wait(1000); + DashTable.getCell(0, 0).click(); + DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('have.html', `abc`)); + }); + }); +}); + Object.values(ReadWriteModes).forEach(mode => { describe(`edit, mode=${mode}`, () => { beforeEach(() => { @@ -106,20 +143,6 @@ Object.values(ReadWriteModes).forEach(mode => { cy.get('.Select-value-label').should('have.html', expectedValue); }); }); - - // https://github.com/plotly/dash-table/issues/50 - it('can edit on "enter"', () => { - DashTable.getCell(0, 1).click(); - DOM.focused.type(`abc${Key.Enter}`); - DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('have.html', `abc`)); - }); - - it('can edit when clicking outside of cell', () => { - DashTable.getCell(0, 1).click(); - DOM.focused.type(`abc`); - DashTable.getCell(0, 0).click(); - DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('have.html', `abc`)); - }); }); }); diff --git a/packages/dash-table/tests/cypress/tests/unit/formatting_test.ts b/packages/dash-table/tests/cypress/tests/unit/formatting_test.ts index 3e76d6fc02..e0262fd9bd 100644 --- a/packages/dash-table/tests/cypress/tests/unit/formatting_test.ts +++ b/packages/dash-table/tests/cypress/tests/unit/formatting_test.ts @@ -1,5 +1,5 @@ import { getFormatter } from 'dash-table/type/number'; -import { getLocale, getNully, getSpecifier } from 'dash-table/dash/sanitize'; +import { getLocale, getNully, getSpecifier } from 'dash-table/dash/Sanitizer'; describe('formatting', () => { describe('number', () => {