From 0eabd6b9abfd0556a7643f8e83bfbfd2cbccfa35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 24 Sep 2019 09:09:46 -0400 Subject: [PATCH] - fix nully validation default - attempt reconciliation for delete - additional tests - update changelog --- CHANGELOG.md | 4 +++ .../components/ControlledTable/index.tsx | 25 +++++++++++++------ src/dash-table/type/reconcile.ts | 20 ++++++++++----- .../cypress/tests/unit/numberCoercion_test.ts | 3 ++- .../tests/unit/numberValidation_test.ts | 3 ++- tests/cypress/tests/unit/reconcile_test.ts | 22 ++++++++++++++++ 6 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 tests/cypress/tests/unit/reconcile_test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d4724a721..1a6c837cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). [#591](https://github.com/plotly/dash-table/issues/591) - Fixed row and column selection when multiple tables are present +[#600](https://github.com/plotly/dash-table/issues/600) +- Fixed reconciliation when validation default value is `0` (number) +- Apply reconciliation value when deleting cells, if possible + ## [4.3.0] - 2019-09-17 ### Added [#566](https://github.com/plotly/dash-table/pull/566) diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index a3ce46156..e7f5d924d 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -36,6 +36,7 @@ import TableTooltip from './fragments/TableTooltip'; import queryLexicon from 'dash-table/syntax-tree/lexicon/query'; import dataLoading from 'dash-table/derived/table/data_loading'; +import reconcile from 'dash-table/type/reconcile'; const DEFAULT_STYLE = { width: '100%' @@ -410,7 +411,7 @@ export default class ControlledTable extends PureComponent // else we are navigating with arrow keys and extending selection // with shift. - let {minRow, minCol, maxRow, maxCol} = selectionBounds(selected_cells); + let { minRow, minCol, maxRow, maxCol } = selectionBounds(selected_cells); const selectingDown = e.keyCode === KEY_CODES.ARROW_DOWN || e.keyCode === KEY_CODES.ENTER; const selectingUp = e.keyCode === KEY_CODES.ARROW_UP; @@ -459,7 +460,7 @@ export default class ControlledTable extends PureComponent } const finalSelected = makeSelection( - {minRow, maxRow, minCol, maxCol}, + { minRow, maxRow, minCol, maxCol }, visibleColumns, viewport ); @@ -503,10 +504,18 @@ export default class ControlledTable extends PureComponent ); realCells.forEach(cell => { - if (visibleColumns[cell[1]].editable) { + const column = visibleColumns[cell[1]]; + + if (column.editable) { + /** + * If the cell can reconcile `null`, use this reconciliation value, + * otherwise use the default `''`. + */ + const result = reconcile(null, column); + newData = R.set( - R.lensPath([cell[0], visibleColumns[cell[1]].id]), - '', + R.lensPath([cell[0], column.id]), + result.success ? result.value : '', newData ); } @@ -522,7 +531,7 @@ export default class ControlledTable extends PureComponent const e = event; - const {row, column} = currentCell; + const { row, column } = currentCell; let nextCoords; switch (e.keyCode) { @@ -787,7 +796,7 @@ export default class ControlledTable extends PureComponent scrollbarWidth ); - /* Tooltip */ + /* Tooltip */ let tableTooltip = derivedTooltips( currentTooltip, tooltip_data, @@ -844,7 +853,7 @@ export default class ControlledTable extends PureComponent ref={`r${rowIndex}c${columnIndex}`} className={`cell cell-${rowIndex}-${columnIndex} ${c}`} > - {g ? React.cloneElement(g, { style: s.cell }) : g} + {g ? React.cloneElement(g, { style: s.cell }) : g} ))} ))} diff --git a/src/dash-table/type/reconcile.ts b/src/dash-table/type/reconcile.ts index cf8808eed..abdd58341 100644 --- a/src/dash-table/type/reconcile.ts +++ b/src/dash-table/type/reconcile.ts @@ -1,3 +1,5 @@ +import * as R from 'ramda'; + import { ChangeAction, ChangeFailure, @@ -9,6 +11,7 @@ import reconcileAny from './any'; import { coerce as coerceNumber, validate as validateNumber } from './number'; import { coerce as coerceText, validate as validateText } from './text'; import { coerce as coerceDate, validate as validateDate } from './date'; +import { OptionalPluck } from 'core/type'; export interface IReconciliation { action?: ChangeAction; @@ -17,7 +20,11 @@ export interface IReconciliation { value?: any; } -function getCoercer(c: IColumnType): (value: any, c?: any) => IReconciliation { +type Options = OptionalPluck & + OptionalPluck & + OptionalPluck; + +function getCoercer(c: Options): (value: any, c?: any) => IReconciliation { switch (c.type) { case ColumnType.Numeric: return coerceNumber; @@ -31,7 +38,7 @@ function getCoercer(c: IColumnType): (value: any, c?: any) => IReconciliation { } } -function getValidator(c: IColumnType): (value: any, c?: any) => IReconciliation { +function getValidator(c: Options): (value: any, c?: any) => IReconciliation { switch (c.type) { case ColumnType.Numeric: return validateNumber; @@ -45,7 +52,7 @@ function getValidator(c: IColumnType): (value: any, c?: any) => IReconciliation } } -function doAction(value: any, c: IColumnType) { +function doAction(value: any, c: Options) { const action = (c && c.on_change && c.on_change.action) || ChangeAction.Coerce; switch (action) { @@ -58,13 +65,14 @@ function doAction(value: any, c: IColumnType) { } } -function doFailureRecovery(result: IReconciliation, c: IColumnType) { +function doFailureRecovery(result: IReconciliation, c: Options) { // If c/v unsuccessful, process failure const failure = (c && c.on_change && c.on_change.failure) || ChangeFailure.Reject; result.failure = failure; if (failure === ChangeFailure.Default) { - const defaultValue = (c && c.validation && c.validation.default) || null; + const validationDefault = c && c.validation && c.validation.default; + const defaultValue = R.isNil(validationDefault) ? null : validationDefault; result.success = true; result.value = defaultValue; } else if (failure === ChangeFailure.Accept) { @@ -74,7 +82,7 @@ function doFailureRecovery(result: IReconciliation, c: IColumnType) { return result; } -export default (value: any, c: IColumnType) => { +export default (value: any, c: Options) => { let res: IReconciliation = doAction(value, c); if (res.success) { diff --git a/tests/cypress/tests/unit/numberCoercion_test.ts b/tests/cypress/tests/unit/numberCoercion_test.ts index c8ea74c69..270176b1b 100644 --- a/tests/cypress/tests/unit/numberCoercion_test.ts +++ b/tests/cypress/tests/unit/numberCoercion_test.ts @@ -3,7 +3,8 @@ import { isNully } from 'dash-table/type/null'; import { coerce } from 'dash-table/type/number'; const DEFAULT_COERCE_SUCCESS = [ - { input: 42, output: 42, name: 'from number' }, + { input: 0, output: 0, name: 'from number (0)' }, + { input: 42, output: 42, name: 'from number (42)' }, { input: '42', output: 42, name: 'from number string' }, { input: '-42', output: -42, name: 'from negative number string' }, { input: '4.242', output: 4.242, name: 'from float string' }, diff --git a/tests/cypress/tests/unit/numberValidation_test.ts b/tests/cypress/tests/unit/numberValidation_test.ts index 4e6c577ab..1b71077e2 100644 --- a/tests/cypress/tests/unit/numberValidation_test.ts +++ b/tests/cypress/tests/unit/numberValidation_test.ts @@ -3,7 +3,8 @@ import { isNully } from 'dash-table/type/null'; import { validate } from 'dash-table/type/number'; const DEFAULT_VALIDATE_SUCCESS = [ - { input: 42, output: 42, name: 'from number' } + { input: 0, output: 0, name: 'from number (0)' }, + { input: 42, output: 42, name: 'from number (42)' } ]; const ALLOW_NULL_VALIDATE_SUCCESS = [ diff --git a/tests/cypress/tests/unit/reconcile_test.ts b/tests/cypress/tests/unit/reconcile_test.ts new file mode 100644 index 000000000..05f2437b5 --- /dev/null +++ b/tests/cypress/tests/unit/reconcile_test.ts @@ -0,0 +1,22 @@ +import reconcile from 'dash-table/type/reconcile'; +import { ColumnType, ChangeAction, ChangeFailure } from 'dash-table/components/Table/props'; + +describe('reconcile', () => { + describe('coerce/validate', () => { + it('applies default ', () => { + const res = reconcile(null, { + type: ColumnType.Numeric, + on_change: { + action: ChangeAction.Coerce, + failure: ChangeFailure.Default + }, + validation: { + default: 0 + } + }); + + expect(res.success).to.equal(true); + expect(res.value).to.equal(0); + }); + }); +}); \ No newline at end of file