diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b7d78ad1..dfb7517ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,16 +3,90 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Changed +[#446](https://github.com/plotly/dash-table/pull/446) +- Table API rework +#### NEW + - `column.sort_as_null`: Allows sorting behavior customization. + Accepts an array of string, number or booleans. + +#### REMOVED + - `column.clearable`: Allows clearing the value of a dropdown cell. + Removed in favor of `dropdown_**` `clearable` nested property. + - `column.options` + Removed. Redundant with `dropdown`. + - `pagination_settings` + Replaced by two props `page_current` and `page_size`. + +#### RENAMED + - `column_static_tooltip` + Renamed to `tooltip`. + - `column_conditional_tooltips` + Renamed to `tooltip_conditional`. + - `filter` + Renamed to `filter_query`. + - `sort_type` + Renamed to `sort_mode`. + - `derived_filter_structure` + Renamed to `derived_filter_query_structure`. + +#### MODIFIED + - `column.deletable`: Allows column deletion. + Now accepts a boolean or an array of booleans (for multi-line headers). + For example, if there are multiple headers and you want the second header row to be deletable, this would be `[False, True]`. + - `column.editable_name`: Allows column renaming. + Renamed to `column.renamable` + Now accepts a boolean or an array of booleans (for multi-line headers). + For example, if there are multiple headers and you want the second row's header's name to be editable, this would be `[False, True]`. + - `column.id` + Now accepts `string` only -- `number` column ids can be casted to string. + - `n_fixed_columns`: Will fix columns to the left. + Renamed to `fixed_columns` + Now accepts an object { headers: boolean, data: number } instead of a number. + { headers: true } determines the number of columns to fix automatically. For example, if the rows are selectable or deletable, { headers: true } would fix those columns automatically. If { headers: true, data: 2 }, it would fix the first two data columns in addition to the selectable and deletable if visible. + - `n_fixed_rows`: Will fix rows to the top. + Renamed to `fixed_rows` + Now accepts an object { headers: boolean, data: number } instead of a number. + { headers: true } determines the number of rows to fix automatically (i.e. if there are multiple headers, it will fix all of them as well as the filter row). + { headers: true, data: 2} would fix all of the header rows as well as the first 2 data rows. + - `pagination_mode` + Renamed to `page_action`. + `'fe'` is now `'native'`, `'be'` is now `'custom'`, and `false` is now '`none'` + - `column_static_dropdown` + Renamed to `dropdown`. + Now an object with each entry refering to a Column ID. Each nested prop expects. + `clearable` and `options`. + - `column_conditional_dropdowns` + Renamed to `dropdown_conditional`. + `condition` changed to the same `if` nested prop used by styles. + `dropdown` renamed to `options`. + - `dropdown_properties` + Renamed to `dropdown_data`. + Matches the `data` structure. + - `tooltips` + Renamed to `tooltip_data`. + Matches the `data` structure. + - `filtering` + Renamed to `filter_action`. + - `sorting` + Renamed to `sort_action`. + - `sorting_treat_empty_string_as_none` + Renamed to `sort_as_null`. + Now accepts an array of string, number or booleans that can be ignored during sort. + Table-level prop for the `column.sort_as_null` column nested prop. + - `style_data_conditional` + Renamed `filter` to `filter_query`. + ### Added [#456](https://github.com/plotly/dash-table/issues/456) - Support for dash-table is now available for R users of Dash. ### Fixed [#434](https://github.com/plotly/dash-table/issues/434) -- Fix CSS borders propeties overwrite style_* borders properties. +- Fix CSS borders properties overwrite style_* borders properties. [#435](https://github.com/plotly/dash-table/issues/435) -- selected_cells background color is set through styling pipeline / derivations. +- selected_cells background color is set through styling pipeline / derivations. ## [3.7.0] - 2019-05-15 ### Added @@ -315,8 +389,8 @@ The remote URL path for the bundle was incorrect. } A+B = { - background_color: 'floralwhite', // from A, not overriden - color: 'black', // from B, A overriden + background_color: 'floralwhite', // from A, not overridden + color: 'black', // from B, A overridden font_size: 22, // from B font_type: 'monospace', // from A width: 100 // from A @@ -352,7 +426,7 @@ The remote URL path for the bundle was incorrect. if: { column_id: string | number, header_index: number | 'odd' | 'even' }, ...CSSProperties }] - - All CSSProperties are supported in kebab-cass, camelCase and snake_case + - All CSSProperties are supported in kebab-case, camelCase and snake_case ### Changed - Renaming 'dataframe' props to 'data' @@ -637,7 +711,7 @@ Freeze Top Rows (Limitations) Freeze Left Columns (Limitations) - performance is highly impacted if the table is in a scrollable container as the frozen columns position has to be recalculated on each scroll event; impact is minimal up to 50-100 items and makes the table difficult to use with 250-500 items - can't freeze rows and columns at the same time -- when using merged headers, make sure that the number of fixed columns respects the merged headers, otherwise there will be some unresolved visual bugs/artefacts +- when using merged headers, make sure that the number of fixed columns respects the merged headers, otherwise there will be some unresolved visual bugs/artifacts - rows are assumed to all have the same height Deletable Columns (Limitations) diff --git a/demo/App.js b/demo/App.js index 07dc6acc2..6c189180d 100644 --- a/demo/App.js +++ b/demo/App.js @@ -40,7 +40,7 @@ class App extends Component { className='clear-filters' onClick={() => { const tableProps = R.clone(this.state.tableProps); - tableProps.filter = ''; + tableProps.filter_query = ''; this.setState({ tableProps }); }} @@ -53,7 +53,7 @@ class App extends Component { } onBlur={e => { const tableProps = R.clone(this.state.tableProps); - tableProps.filter = e.target.value; + tableProps.filter_query = e.target.value; this.setState({ tableProps }); }} /> diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 277419d9a..97aec6b10 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -4,12 +4,12 @@ import Environment from 'core/environment'; import { generateMockData, IDataMock, generateSpaceMockData } from './data'; import { - ContentStyle, PropsWithDefaults, ChangeAction, ChangeFailure, IVisibleColumn, - ColumnType + ColumnType, + TableAction } from 'dash-table/components/Table/props'; import { TooltipSyntax } from 'dash-table/tooltips/props'; @@ -46,19 +46,19 @@ function getBaseTableProps(mock: IDataMock) { on_change: { action: ChangeAction.None }, - editable_name: true, + renamable: true, deletable: true })), - column_static_dropdown: [ - { - id: 'bbb', - dropdown: ['Humid', 'Wet', 'Snowy', 'Tropical Beaches'].map(i => ({ + dropdown: { + bbb: { + clearable: true, + options: ['Humid', 'Wet', 'Snowy', 'Tropical Beaches'].map(i => ({ label: i, value: i })) } - ], - pagination_mode: false, + }, + page_action: TableAction.None, style_table: { max_height: '800px', height: '800px', @@ -77,24 +77,23 @@ function getBaseTableProps(mock: IDataMock) { function getDefaultState( generateData: Function = generateMockData ): { - filter: string, + filter_query: string, tableProps: Partial } { const mock = generateData(5000); return { - filter: '', + filter_query: '', tableProps: R.merge(getBaseTableProps(mock), { data: mock.data, editable: true, - sorting: true, - n_fixed_rows: 3, - n_fixed_columns: 2, + sort_action: TableAction.Native, + fixed_rows: { headers: true }, + fixed_columns: { headers: true }, merge_duplicate_headers: false, row_deletable: true, row_selectable: 'single', - content_style: ContentStyle.Fit, - pagination_mode: 'fe' + page_action: TableAction.Native }) as Partial }; } @@ -113,7 +112,7 @@ function getReadonlyState() { function getSpaceInColumn() { const state = getDefaultState(generateSpaceMockData); - state.tableProps.filtering = true; + state.tableProps.filter_action = TableAction.Native; return state; } @@ -121,8 +120,8 @@ function getSpaceInColumn() { function getFixedTooltipsState() { const state = getTooltipsState(); - state.tableProps.n_fixed_columns = 3; - state.tableProps.n_fixed_rows = 4; + state.tableProps.fixed_columns = { headers: true, data: 1 }; + state.tableProps.fixed_rows = { headers: true, data: 1 }; return state; } @@ -132,25 +131,23 @@ function getTooltipsState() { state.tableProps.tooltip_delay = 250; state.tableProps.tooltip_duration = 1000; - state.tableProps.tooltips = { - ccc: [ - { type: TooltipSyntax.Markdown, value: `### Go Proverb\nThe enemy's key point is yours` }, - { type: TooltipSyntax.Markdown, value: `### Go Proverb\nPlay on the point of symmetry` }, - { type: TooltipSyntax.Markdown, value: `### Go Proverb\nSente gains nothing` }, - { type: TooltipSyntax.Text, value: `Beware of going back to patch up` }, - { type: TooltipSyntax.Text, value: `When in doubt, Tenuki` }, - `People in glass houses shouldn't throw stones` - ] - }; - state.tableProps.column_static_tooltip = { + state.tableProps.tooltip_data = [ + { ccc: { type: TooltipSyntax.Markdown, value: `### Go Proverb\nThe enemy's key point is yours` } }, + { ccc: { type: TooltipSyntax.Markdown, value: `### Go Proverb\nPlay on the point of symmetry` } }, + { ccc: { type: TooltipSyntax.Markdown, value: `### Go Proverb\nSente gains nothing` } }, + { ccc: { type: TooltipSyntax.Text, value: `Beware of going back to patch up` } }, + { ccc: { type: TooltipSyntax.Text, value: `When in doubt, Tenuki` } }, + { ccc: `People in glass houses should not throw stones` } + ]; + state.tableProps.tooltip = { ccc: { type: TooltipSyntax.Text, value: `There is death in the hane` }, ddd: { type: TooltipSyntax.Markdown, value: `Hane, Cut, Placement` }, rows: `Learn the eyestealing tesuji` }; - state.tableProps.column_conditional_tooltips = [{ + state.tableProps.tooltip_conditional = [{ if: { column_id: 'aaa-readonly', - filter: `{aaa} is prime` + filter_query: `{aaa} is prime` }, type: TooltipSyntax.Markdown, value: `### Go Proverbs\nCapture three to get an eye` @@ -218,7 +215,7 @@ function getDateState() { function getFilteringState() { const state = getDefaultState(); - state.tableProps.filtering = true; + state.tableProps.filter_action = TableAction.Native; return state; } @@ -227,15 +224,14 @@ function getVirtualizedState() { const mock = generateMockData(5000); return { - filter: '', + filter_query: '', tableProps: R.merge(getBaseTableProps(mock), { data: mock.data, editable: true, - sorting: true, + sort_action: TableAction.Native, merge_duplicate_headers: false, row_deletable: true, row_selectable: 'single', - content_style: 'fit', virtualization: true }) }; @@ -245,17 +241,16 @@ function getFixedVirtualizedState() { const mock = generateMockData(5000); return { - filter: '', + filter_query: '', tableProps: R.merge(getBaseTableProps(mock), { data: mock.data, editable: true, - sorting: true, - n_fixed_rows: 3, - n_fixed_columns: 2, + sort_action: TableAction.Native, + fixed_rows: { headers: true }, + fixed_columns: { headers: true }, merge_duplicate_headers: false, row_deletable: true, row_selectable: 'single', - content_style: 'fit', virtualization: true }) }; diff --git a/demo/data.ts b/demo/data.ts index c38b8a2d2..0b3df6de5 100644 --- a/demo/data.ts +++ b/demo/data.ts @@ -58,7 +58,6 @@ export const generateMockData = (rows: number) => unpackIntoColumnsAndData([ name: ['', 'Weather', 'Climate'], type: ColumnType.Text, presentation: 'dropdown', - clearable: true, data: gendata( i => ['Humid', 'Wet', 'Snowy', 'Tropical Beaches'][i % 4], rows @@ -141,7 +140,6 @@ export const generateSpaceMockData = (rows: number) => unpackIntoColumnsAndData( name: ['', 'Weather', 'Climate'], type: ColumnType.Text, presentation: 'dropdown', - clearable: true, data: gendata( i => ['Humid', 'Wet', 'Snowy', 'Tropical Beaches'][i % 4], rows @@ -162,11 +160,6 @@ export const mockDataSimple = (rows: number) => unpackIntoColumnsAndData([ name: 'Climate', type: ColumnType.Text, presentation: 'dropdown', - options: ['Humid', 'Wet', 'Snowy', 'Tropical Beaches'].map(i => ({ - label: i, - value: i - })), - clearable: true, data: gendata( i => ['Humid', 'Wet', 'Snowy', 'Tropical Beaches'][i % 4], rows diff --git a/generator/cssPropertiesGenerator.js b/generator/cssPropertiesGenerator.js index 0ebffdc45..410c25fb8 100644 --- a/generator/cssPropertiesGenerator.js +++ b/generator/cssPropertiesGenerator.js @@ -302,7 +302,7 @@ camels.forEach(([camel]) => map.set(camel, camel)); const fs = require('fs'); -var stream1 = fs.createWriteStream("src/dash-table/derived/style/py2jsCssProperties.ts"); +var stream1 = fs.createWriteStream('src/dash-table/derived/style/py2jsCssProperties.ts'); stream1.once('open', () => { stream1.write('export type StyleProperty = string | number;\n'); stream1.write('\n'); @@ -317,12 +317,12 @@ stream1.once('open', () => { first = false; stream1.write(` ['${key}', '${value}']`); }); - stream1.write('\n]);') + stream1.write('\n]);'); stream1.end(); }); -var stream2 = fs.createWriteStream("src/dash-table/derived/style/IStyle.ts"); +var stream2 = fs.createWriteStream('src/dash-table/derived/style/IStyle.ts'); stream2.once('open', () => { stream2.write(`import { StyleProperty } from './ py2jsCssProperties';\n`); stream2.write('\n'); @@ -330,12 +330,12 @@ stream2.once('open', () => { camels.forEach(([key]) => { stream2.write(` ${key}: StyleProperty;\n`); }); - stream2.write('}') + stream2.write('}'); stream2.end(); }); -var stream3 = fs.createWriteStream("proptypes.js"); +var stream3 = fs.createWriteStream('proptypes.js'); stream3.once('open', () => { let first = true; map.forEach((value, key) => { @@ -350,7 +350,7 @@ stream3.once('open', () => { stream3.write(` ${key}: PropTypes.oneOfType([PropTypes.string, PropTypes.number])`); } }); - stream3.write('\n') + stream3.write('\n'); stream3.end(); }); \ No newline at end of file diff --git a/package.json b/package.json index 278f504c2..08bf50412 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "private::host_dash8082": "python tests/cypress/dash/v_copy_paste.py", "private::host_dash8083": "python tests/cypress/dash/v_fe_page.py", "private::host_js": "http-server ./dash_table -c-1 --silent", - "private::lint:ts": "tslint '{src,demo,tests}/**/*.{js,ts,tsx}' --exclude '**/@Types/*.*'", + "private::lint:ts": "tslint --project tsconfig.json --config tslint.json", "private::lint:py": "flake8 --exclude=DataTable.py,__init__.py,_imports_.py dash_table", "private::wait_dash8081": "wait-on http://localhost:8081", "private::wait_dash8082": "wait-on http://localhost:8082", diff --git a/src/core/sorting/index.ts b/src/core/sorting/index.ts index f0bdd2c94..0037127d5 100644 --- a/src/core/sorting/index.ts +++ b/src/core/sorting/index.ts @@ -2,7 +2,7 @@ import * as R from 'ramda'; import { ColumnId } from 'dash-table/components/Table/props'; -export interface ISortSetting { +export interface ISortBy { column_id: ColumnId; direction: SortDirection; } @@ -13,46 +13,46 @@ export enum SortDirection { None = 'none' } -export type SortSettings = ISortSetting[]; -type IsNullyFn = (value: any) => boolean; -export const defaultIsNully: IsNullyFn = (value: any) => value === undefined || value === null; -export default (data: any[], settings: SortSettings, isNully: IsNullyFn = defaultIsNully): any[] => { - if (!settings.length) { +export type SortBy = ISortBy[]; +type IsNullyFn = (value: any, id: string) => boolean; +export const defaultIsNully: IsNullyFn = (value: any, _: string) => R.isNil(value); +export default (data: any[], sortBy: SortBy, isNully: IsNullyFn = defaultIsNully): any[] => { + if (!sortBy.length) { return data; } return R.sortWith( - R.map(setting => { - return setting.direction === SortDirection.Descending ? + R.map(sort => { + return sort.direction === SortDirection.Descending ? R.comparator((d1: any, d2: any) => { - const id = setting.column_id; + const id = sort.column_id; const prop1 = d1[id]; const prop2 = d2[id]; - if (isNully(prop1)) { + if (isNully(prop1, sort.column_id)) { return false; - } else if (isNully(prop2)) { + } else if (isNully(prop2, sort.column_id)) { return true; } return prop1 > prop2; }) : R.comparator((d1: any, d2: any) => { - const id = setting.column_id; + const id = sort.column_id; const prop1 = d1[id]; const prop2 = d2[id]; - if (isNully(prop1)) { + if (isNully(prop1, sort.column_id)) { return false; - } else if (isNully(prop2)) { + } else if (isNully(prop2, sort.column_id)) { return true; } return prop1 < prop2; }); - }, settings), + }, sortBy), data ); }; \ No newline at end of file diff --git a/src/core/sorting/multi.ts b/src/core/sorting/multi.ts index c02a9682d..b254ebce6 100644 --- a/src/core/sorting/multi.ts +++ b/src/core/sorting/multi.ts @@ -1,31 +1,31 @@ import * as R from 'ramda'; import Logger from 'core/Logger'; -import { SortSettings, ISortSetting, SortDirection } from 'core/sorting'; +import { SortBy, ISortBy, SortDirection } from 'core/sorting'; export default ( - settings: SortSettings, - setting: ISortSetting -): SortSettings => { - Logger.trace('multi - updateSettings', settings, setting); + sortBy: SortBy, + sort: ISortBy +): SortBy => { + Logger.trace('multi - update sortBy', sortBy, sort); - settings = R.clone(settings); + sortBy = R.clone(sortBy); - if (setting.direction === SortDirection.None) { - const currentIndex = R.findIndex(s => s.column_id === setting.column_id, settings); + if (sort.direction === SortDirection.None) { + const currentIndex = R.findIndex(s => s.column_id === sort.column_id, sortBy); if (currentIndex !== -1) { - settings.splice(currentIndex, 1); + sortBy.splice(currentIndex, 1); } } else { - const currentSetting = R.find(s => s.column_id === setting.column_id, settings); + const current = R.find(s => s.column_id === sort.column_id, sortBy); - if (currentSetting) { - currentSetting.direction = setting.direction; + if (current) { + current.direction = sort.direction; } else { - settings.push(setting); + sortBy.push(sort); } } - return settings; + return sortBy; }; \ No newline at end of file diff --git a/src/core/sorting/single.ts b/src/core/sorting/single.ts index bbb1a2de5..d8a498731 100644 --- a/src/core/sorting/single.ts +++ b/src/core/sorting/single.ts @@ -1,13 +1,13 @@ import Logger from 'core/Logger'; -import { SortSettings, ISortSetting, SortDirection } from 'core/sorting'; +import { SortBy, ISortBy, SortDirection } from 'core/sorting'; export default ( - settings: SortSettings, - setting: ISortSetting -): SortSettings => { - Logger.trace('single - updateSettings', settings, setting); + sortBy: SortBy, + sort: ISortBy +): SortBy => { + Logger.trace('single - update sortBy', sortBy, sort); - return setting.direction === SortDirection.None ? + return sort.direction === SortDirection.None ? [] : - [setting]; + [sort]; }; \ No newline at end of file diff --git a/src/core/type/index.ts b/src/core/type/index.ts index b9e54c774..fa2f45c5b 100644 --- a/src/core/type/index.ts +++ b/src/core/type/index.ts @@ -7,4 +7,6 @@ export type OptionalPluck = { [r in R]?: T[r] }; export type RequiredProp = T[R]; export type OptionalProp = T[R] | undefined; -export type PropOf = R; \ No newline at end of file +export type PropOf = R; + +export type Merge = Omit> & N; \ No newline at end of file diff --git a/src/dash-table/components/CellDropdown/index.tsx b/src/dash-table/components/CellDropdown/index.tsx index e98fb08fc..1ba7dbe7f 100644 --- a/src/dash-table/components/CellDropdown/index.tsx +++ b/src/dash-table/components/CellDropdown/index.tsx @@ -8,12 +8,12 @@ import DOM from 'core/browser/DOM'; import dropdownHelper from 'dash-table/components/dropdownHelper'; -import { DropdownValues } from '../Table/props'; +import { IDropdownValue } from '../Table/props'; interface IProps { active: boolean; clearable?: boolean; - dropdown?: DropdownValues; + dropdown?: IDropdownValue[]; onChange: (e: ChangeEvent) => void; value: any; } diff --git a/src/dash-table/components/CellDropdown/types.ts b/src/dash-table/components/CellDropdown/types.ts deleted file mode 100644 index 3e3489c50..000000000 --- a/src/dash-table/components/CellDropdown/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DropdownValues } from '../Table/props'; - -export interface IConditionalDropdown { - condition: string; - dropdown: DropdownValues; -} \ No newline at end of file diff --git a/src/dash-table/components/CellFactory.tsx b/src/dash-table/components/CellFactory.tsx index a00fa59a2..dbcdbb6fa 100644 --- a/src/dash-table/components/CellFactory.tsx +++ b/src/dash-table/components/CellFactory.tsx @@ -34,10 +34,10 @@ export default class CellFactory { const { active_cell, columns, - column_conditional_dropdowns, - column_static_dropdown, + dropdown_conditional, + dropdown, data, - dropdown_properties, // legacy + dropdown_data, editable, is_focused, row_deletable, @@ -78,9 +78,9 @@ export default class CellFactory { columns, virtualized.data, virtualized.indices, - column_conditional_dropdowns, - column_static_dropdown, - dropdown_properties + dropdown_conditional, + dropdown, + dropdown_data ); const operations = this.cellOperations( diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 80d233dc7..3f2021e16 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -18,7 +18,7 @@ import { memoizeOne } from 'core/memoizer'; import lexer from 'core/syntax-tree/lexer'; import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper'; -import { ControlledTableProps, ICellFactoryProps } from 'dash-table/components/Table/props'; +import { ControlledTableProps, ICellFactoryProps, TableAction } from 'dash-table/components/Table/props'; import dropdownHelper from 'dash-table/components/dropdownHelper'; import derivedTable from 'dash-table/derived/table'; @@ -54,9 +54,9 @@ export default class ControlledTable extends PureComponent getLexerResult = memoizeOne(lexer.bind(undefined, queryLexicon)); get lexerResult() { - const { filter } = this.props; + const { filter_query } = this.props; - return this.getLexerResult(filter); + return this.getLexerResult(filter_query); } private updateStylesheet() { @@ -550,7 +550,7 @@ export default class ControlledTable extends PureComponent columns, data, editable, - filter, + filter_query, setProps, sort_by, viewport @@ -567,7 +567,7 @@ export default class ControlledTable extends PureComponent columns, data, true, - !sort_by.length || !filter.length + !sort_by.length || !filter_query.length ); if (result) { @@ -578,16 +578,14 @@ export default class ControlledTable extends PureComponent get displayPagination() { const { data, - navigation, - pagination_mode, - pagination_settings + page_action, + page_size } = this.props; - return navigation === 'page' && - ( - (pagination_mode === 'fe' && pagination_settings.page_size < data.length) || - pagination_mode === 'be' - ); + return ( + page_action === TableAction.Native && + page_size < data.length + ) || page_action === TableAction.Custom; } loadNext = () => { @@ -604,8 +602,8 @@ export default class ControlledTable extends PureComponent applyStyle = () => { const { - n_fixed_columns, - n_fixed_rows, + fixed_columns, + fixed_rows, row_deletable, row_selectable } = this.props; @@ -637,7 +635,7 @@ export default class ControlledTable extends PureComponent } // Adjust the width of the fixed row header - if (n_fixed_rows) { + if (fixed_rows) { Array.from(r1c1.querySelectorAll('tr:first-of-type td, tr:first-of-type th')).forEach((td, index) => { const style = getComputedStyle(td); const width = style.width; @@ -650,7 +648,7 @@ export default class ControlledTable extends PureComponent } // Adjust the width of the fixed row / fixed columns header - if (n_fixed_columns && n_fixed_rows) { + if (fixed_columns && fixed_rows) { Array.from(r1c0.querySelectorAll('tr:first-of-type td, tr:first-of-type th')).forEach((td, index) => { const style = getComputedStyle(td); const width = style.width; @@ -684,19 +682,18 @@ export default class ControlledTable extends PureComponent const { id, columns, - column_conditional_tooltips, - column_static_tooltip, - content_style, - filtering, - n_fixed_columns, - n_fixed_rows, + tooltip_conditional, + tooltip, + currentTooltip, + filter_action, + fixed_columns, + fixed_rows, scrollbarWidth, style_as_list_view, style_table, - tooltip, tooltip_delay, tooltip_duration, - tooltips, + tooltip_data, uiCell, uiHeaders, uiViewport, @@ -707,19 +704,19 @@ export default class ControlledTable extends PureComponent const fragmentClasses = [ [ - n_fixed_rows && n_fixed_columns ? 'dash-fixed-row dash-fixed-column' : '', - n_fixed_rows ? 'dash-fixed-row' : '' + fixed_rows && fixed_columns ? 'dash-fixed-row dash-fixed-column' : '', + fixed_rows ? 'dash-fixed-row' : '' ], [ - n_fixed_columns ? 'dash-fixed-column' : '', + fixed_columns ? 'dash-fixed-column' : '', 'dash-fixed-content' ] ]; const rawTable = this.tableFn(); const { grid, empty } = derivedTableFragments( - n_fixed_columns, - n_fixed_rows, + fixed_columns, + fixed_rows, rawTable, virtualized.offset.rows ); @@ -727,15 +724,14 @@ export default class ControlledTable extends PureComponent const classes = [ 'dash-spreadsheet', ...(virtualization ? ['dash-virtualized'] : []), - ...(n_fixed_rows ? ['dash-freeze-top'] : []), - ...(n_fixed_columns ? ['dash-freeze-left'] : []), + ...(fixed_rows ? ['dash-freeze-top'] : []), + ...(fixed_columns ? ['dash-freeze-left'] : []), ...(style_as_list_view ? ['dash-list-view'] : []), ...(empty[0][1] ? ['dash-empty-01'] : []), ...(empty[1][1] ? ['dash-empty-11'] : []), ...(columns.length ? [] : ['dash-no-columns']), ...(virtualized.data.length ? [] : ['dash-no-data']), - ...(filtering ? [] : ['dash-no-filter']), - [`dash-${content_style}`] + ...(filter_action !== TableAction.None ? [] : ['dash-no-filter']) ]; const containerClasses = ['dash-spreadsheet-container', ...classes]; @@ -754,10 +750,10 @@ export default class ControlledTable extends PureComponent /* Tooltip */ let tableTooltip = derivedTooltips( + currentTooltip, + tooltip_data, + tooltip_conditional, tooltip, - tooltips, - column_conditional_tooltips, - column_static_tooltip, virtualized, tooltip_delay, tooltip_duration @@ -809,14 +805,14 @@ export default class ControlledTable extends PureComponent } private adjustTooltipPosition() { - const { tooltip, virtualized } = this.props; + const { currentTooltip, virtualized } = this.props; - if (!tooltip) { + if (!currentTooltip) { return; } - const id = tooltip.id; - const row = tooltip.row - virtualized.offset.rows; + const id = currentTooltip.id; + const row = currentTooltip.row - virtualized.offset.rows; const { table, tooltip: t } = this.refs as { [key: string]: any }; diff --git a/src/dash-table/components/EdgeFactory.tsx b/src/dash-table/components/EdgeFactory.tsx index 3918e129f..08da253d4 100644 --- a/src/dash-table/components/EdgeFactory.tsx +++ b/src/dash-table/components/EdgeFactory.tsx @@ -15,7 +15,7 @@ import getHeaderRows from 'dash-table/derived/header/headerRows'; import { derivedRelevantCellStyles, derivedRelevantFilterStyles, derivedRelevantHeaderStyles } from 'dash-table/derived/style'; import { Style, Cells, DataCells, BasicFilters, Headers } from 'dash-table/derived/style/props'; -import { ControlledTableProps, VisibleColumns, IViewportOffset, Data, ICellCoordinates } from './Table/props'; +import { ControlledTableProps, VisibleColumns, IViewportOffset, Data, ICellCoordinates, TableAction } from './Table/props'; import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; type EdgesMatricesOp = EdgesMatrices | undefined; @@ -140,10 +140,10 @@ export default class EdgeFactory { const { active_cell, columns, - filtering, + filter_action, workFilter, - n_fixed_columns, - n_fixed_rows, + fixed_columns, + fixed_rows, row_deletable, row_selectable, style_as_list_view, @@ -162,10 +162,10 @@ export default class EdgeFactory { active_cell, columns, (row_deletable ? 1 : 0) + (row_selectable ? 1 : 0), - !!filtering, + filter_action !== TableAction.None, workFilter.map, - n_fixed_columns, - n_fixed_rows, + fixed_columns, + fixed_rows, style_as_list_view, style_cell, style_cell_conditional, @@ -184,10 +184,10 @@ export default class EdgeFactory { active_cell: ICellCoordinates, columns: VisibleColumns, operations: number, - filtering: boolean, + filter_action: boolean, filterMap: Map, - _n_fixed_columns: number, - n_fixed_rows: number, + fixed_columns: number, + fixed_rows: number, style_as_list_view: boolean, style_cell: Style, style_cell_conditional: Cells, @@ -242,7 +242,7 @@ export default class EdgeFactory { let filterEdges = this.getFilterEdges( columns, - filtering, + filter_action, filterMap, filterStyles, style_as_list_view @@ -250,7 +250,7 @@ export default class EdgeFactory { let filterOpEdges = this.getFilterOpEdges( operations, - filtering, + filter_action, filterStyles, style_as_list_view ); @@ -287,20 +287,20 @@ export default class EdgeFactory { this.vReconcile(filterOpEdges, filterEdges, cutoffWeight); this.vReconcile(dataOpEdges, dataEdges, cutoffWeight); - if (n_fixed_rows === headerRows) { - if (filtering) { + if (fixed_rows === headerRows) { + if (filter_action) { this.hOverride(headerEdges, filterEdges, cutoffWeight); this.hOverride(headerOpEdges, filterOpEdges, cutoffWeight); } else { this.hOverride(headerEdges, dataEdges, cutoffWeight); this.hOverride(headerOpEdges, dataOpEdges, cutoffWeight); } - } else if (filtering && n_fixed_rows === headerRows + 1) { + } else if (filter_action && fixed_rows === headerRows + 1) { this.hOverride(filterEdges, dataEdges, cutoffWeight); this.hOverride(filterOpEdges, dataOpEdges, cutoffWeight); } - if (_n_fixed_columns === operations) { + if (fixed_columns === operations) { this.vOverride(headerOpEdges, headerEdges, cutoffWeight); this.vOverride(filterOpEdges, filterEdges, cutoffWeight); this.vOverride(dataOpEdges, dataEdges, cutoffWeight); diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 4c774f4f8..206bde1d4 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -7,7 +7,7 @@ import memoizerCache from 'core/cache/memoizer'; import { memoizeOne } from 'core/memoizer'; import ColumnFilter from 'dash-table/components/Filter/Column'; -import { ColumnId, Filtering, FilteringType, IVisibleColumn, VisibleColumns, RowSelection } from 'dash-table/components/Table/props'; +import { ColumnId, IVisibleColumn, VisibleColumns, RowSelection, TableAction } from 'dash-table/components/Table/props'; import derivedFilterStyles, { derivedFilterOpStyles } from 'dash-table/derived/filter/wrapperStyles'; import derivedHeaderOperations from 'dash-table/derived/header/operations'; import { derivedRelevantFilterStyles } from 'dash-table/derived/style'; @@ -18,16 +18,15 @@ import { IEdgesMatrices } from 'dash-table/derived/edges/type'; import { updateMap } from 'dash-table/derived/filter/map'; type SetFilter = ( - filter: string, + filter_query: string, rawFilter: string, map: Map ) => void; export interface IFilterOptions { columns: VisibleColumns; - filter: string; - filtering: Filtering; - filtering_type: FilteringType; + filter_query: string; + filter_action: TableAction; id: string; map: Map; rawFilterQuery: string; @@ -107,8 +106,7 @@ export default class FilterFactory { ) { const { columns, - filtering, - filtering_type, + filter_action, map, row_deletable, row_selectable, @@ -119,67 +117,63 @@ export default class FilterFactory { style_filter_conditional } = this.props; - if (!filtering) { + if (filter_action === TableAction.None) { return []; } - if (filtering_type === FilteringType.Basic) { - const relevantStyles = this.relevantStyles( - style_cell, - style_filter, - style_cell_conditional, - style_filter_conditional - ); - - const wrapperStyles = this.wrapperStyles( - this.filterStyles(columns, relevantStyles), - filterEdges - ); - - const opStyles = this.filterOpStyles( - 1, - (row_selectable ? 1 : 0) + (row_deletable ? 1 : 0), - relevantStyles - )[0]; - - const filters = R.addIndex(R.map)((column, index) => { - return this.filter.get(column.id, index)( - column, - index, - map, - setFilter - ); - }, columns); - - const styledFilters = arrayMap2( - filters, - wrapperStyles, - (f, s) => React.cloneElement(f, { - style: s - }) - ); - - const operations = this.headerOperations( - 1, - row_selectable, - row_deletable - )[0]; - - const operators = arrayMap2( - operations, - opStyles, - (o, s, j) => React.cloneElement(o, { - style: R.mergeAll([ - filterOpEdges && filterOpEdges.getStyle(0, j), - s, - o.props.style - ]) - }) + const relevantStyles = this.relevantStyles( + style_cell, + style_filter, + style_cell_conditional, + style_filter_conditional + ); + + const wrapperStyles = this.wrapperStyles( + this.filterStyles(columns, relevantStyles), + filterEdges + ); + + const opStyles = this.filterOpStyles( + 1, + (row_selectable ? 1 : 0) + (row_deletable ? 1 : 0), + relevantStyles + )[0]; + + const filters = R.addIndex(R.map)((column, index) => { + return this.filter.get(column.id, index)( + column, + index, + map, + setFilter ); - - return [operators.concat(styledFilters)]; - } else { - return [[]]; - } + }, columns); + + const styledFilters = arrayMap2( + filters, + wrapperStyles, + (f, s) => React.cloneElement(f, { + style: s + }) + ); + + const operations = this.headerOperations( + 1, + row_selectable, + row_deletable + )[0]; + + const operators = arrayMap2( + operations, + opStyles, + (o, s, j) => React.cloneElement(o, { + style: R.mergeAll([ + filterOpEdges && filterOpEdges.getStyle(0, j), + s, + o.props.style + ]) + }) + ); + + return [operators.concat(styledFilters)]; } } \ No newline at end of file diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index 59db68ddf..c7d1bfabe 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -38,13 +38,13 @@ export default class HeaderFactory { const { columns, merge_duplicate_headers, - pagination_mode, + page_action, row_deletable, row_selectable, setProps, - sorting, + sort_action, sort_by, - sorting_type, + sort_mode, style_cell, style_cell_conditional, style_header, @@ -92,10 +92,10 @@ export default class HeaderFactory { const content = this.headerContent( columns, labelsAndIndices, - sorting, - sorting_type, + sort_action, + sort_mode, sort_by, - pagination_mode, + page_action, setProps, props ); diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index 3b6a69667..f0cbe2c7c 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -233,17 +233,6 @@ } } - &.dash-grow { - .cell-0-1, - .cell-1-1 { - flex: 1 0 auto; - } - - table { - width: 100%; - } - } - tr { background-color: white; } @@ -320,6 +309,7 @@ // cell content styling td, th { + background-clip: padding-box; padding: 2px; white-space: nowrap; overflow-x: hidden; diff --git a/src/dash-table/components/Table/index.tsx b/src/dash-table/components/Table/index.tsx index aa105baf3..f53b5195f 100644 --- a/src/dash-table/components/Table/index.tsx +++ b/src/dash-table/components/Table/index.tsx @@ -20,11 +20,11 @@ import QuerySyntaxTree from 'dash-table/syntax-tree/QuerySyntaxTree'; import { ControlledTableProps, - PropsWithDefaultsAndDerived, SetProps, IState, StandaloneState, - PropsWithDefaults + SanitizedAndDerivedProps, + TableAction } from './props'; import 'react-select/dist/react-select.css'; @@ -36,17 +36,17 @@ import derivedFilterMap from 'dash-table/derived/filter/map'; const DERIVED_REGEX = /^derived_/; -export default class Table extends Component { - constructor(props: PropsWithDefaultsAndDerived) { +export default class Table extends Component { + constructor(props: SanitizedAndDerivedProps) { super(props); this.state = { forcedResizeOnly: false, workFilter: { - value: props.filter, + value: props.filter_query, map: this.filterMap( new Map(), - props.filter, + props.filter_query, props.columns ) }, @@ -55,18 +55,18 @@ export default class Table extends Component { const { workFilter: { map: currentMap, value } } = state; - if (value !== nextProps.filter) { + if (value !== nextProps.filter_query) { const map = this.filterMap( currentMap, - nextProps.filter, + nextProps.filter_query, nextProps.columns ); @@ -103,32 +103,33 @@ export default class Table extends Component = {}; + let newProps: Partial = {}; if (!derivedStructureCache.cached) { newProps.derived_filter_structure = derivedStructureCache.result; @@ -285,7 +288,7 @@ export default class Table extends Component) => { + } : (newProps: Partial) => { /*#if DEV*/ const props: any = this.state; R.forEach( @@ -309,8 +312,8 @@ export default class Table extends Component filter); - private readonly paginationCache = memoizeOneWithFlag(pagination => pagination); + private readonly filterCache = memoizeOneWithFlag(filter_query => filter_query); + private readonly paginationCache = memoizeOneWithFlag((page_current, page_size) => [page_current, page_size]); private readonly sortCache = memoizeOneWithFlag(sort => sort); private readonly viewportCache = memoizeOneWithFlag(viewport => viewport); private readonly viewportSelectedRowsCache = memoizeOneWithFlag(viewport => viewport); diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 3b3fc3e15..8f62fc665 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -1,4 +1,4 @@ -import { SortSettings } from 'core/sorting'; +import { SortBy } from 'core/sorting'; import { IPaginator } from 'dash-table/derived/paginator'; import { Table, @@ -13,6 +13,8 @@ import { Tooltip } from 'dash-table/tooltips/props'; import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; +import { IConditionalElement, INamedElement } from 'dash-table/conditional'; +import { Merge } from 'core/type'; export enum ColumnType { Any = 'any', @@ -21,9 +23,15 @@ export enum ColumnType { Datetime = 'datetime' } -export enum FilteringType { - Advanced = 'advanced', - Basic = 'basic' +export enum SortMode { + Single = 'single', + Multi = 'multi' +} + +export enum TableAction { + Custom = 'custom', + Native = 'native', + None = 'none' } export interface IDerivedData { @@ -48,11 +56,6 @@ export interface IVirtualizedDerivedData extends IDerivedData { }; } -export enum ContentStyle { - Fit = 'fit', - Grow = 'grow' -} - export interface ICellCoordinates { row: number; column: number; @@ -60,21 +63,17 @@ export interface ICellCoordinates { column_id: ColumnId; } -export type ColumnId = string | number; +export type ColumnId = string; export type Columns = IColumn[]; export type Data = Datum[]; export type Datum = IDatumObject | any; -export type Filtering = 'fe' | 'be' | boolean; export type Indices = number[]; export type RowId = string | number; -export type Navigation = 'page'; -export type PaginationMode = 'fe' | 'be' | boolean; export type RowSelection = 'single' | 'multi' | false; export type SelectedCells = ICellCoordinates[]; export type SetProps = (...args: any[]) => void; export type SetState = (state: Partial) => void; -export type Sorting = 'fe' | 'be' | boolean; -export type SortingType = 'multi' | 'single'; +export type SortAsNull = (string | number | boolean)[]; export type VisibleColumns = IVisibleColumn[]; export enum ChangeAction { @@ -155,15 +154,20 @@ export interface IDatetimeColumn extends ITypeColumn { } export interface IBaseVisibleColumn { - clearable?: boolean; - deletable?: boolean | number; + deletable?: boolean | boolean[]; editable?: boolean; - editable_name?: boolean | number; + renamable?: boolean | boolean[]; + sort_as_null: SortAsNull; id: ColumnId; name: string | string[]; - options?: IDropdownValue[]; // legacy } +export type ConditionalDropdowns = IConditionalDropdown[]; +export type DataDropdowns = Partial[]; +export type DataTooltips = Partial[]; +export type StaticDropdowns = Partial; + +export type Fixed = { headers: false, data?: 0 } | { headers: true, data?: number }; export type IColumnType = INumberColumn | ITextColumn | IDatetimeColumn | IAnyColumn; export type IVisibleColumn = IBaseVisibleColumn & IColumnType; @@ -180,29 +184,25 @@ export interface IDropdownValue { value: string | number; } -export type DropdownValues = IDropdownValue[]; - -interface IConditionalDropdown { - condition: string; - dropdown: IDropdownValue[]; +export interface IDropdown { + clearable?: boolean; + options: IDropdownValue[]; } -export interface IColumnDropdown { - id: string; - dropdown: IDropdownValue[]; +export interface IConditionalDropdown extends IDropdown { + if: Partial; } -export interface IConditionalColumnDropdown { - id: string; - dropdowns: IConditionalDropdown[]; +export interface IDataDropdowns { + [key: string]: IDropdown; } -export interface IDropdownProperties { - [key: string]: { options: IDropdownValue[] }[]; +export interface IStaticDropdowns { + [key: string]: IDropdown; } export interface ITableTooltips { - [key: string]: Tooltip[]; + [key: string]: Tooltip; } export interface ITableStaticTooltips { @@ -214,11 +214,6 @@ interface IStylesheetRule { rule: string; } -export interface IPaginationSettings { - current_page: number; - page_size: number; -} - export interface IUserInterfaceCell { height: number; } @@ -238,20 +233,20 @@ export interface IUSerInterfaceTooltip { } export interface IState { + currentTooltip?: IUSerInterfaceTooltip; forcedResizeOnly: boolean; - workFilter: { - value: string, - map: Map - }; rawFilterQuery: string; scrollbarWidth: number; - tooltip?: IUSerInterfaceTooltip; - uiViewport?: IUserInterfaceViewport; uiCell?: IUserInterfaceCell; uiHeaders?: IUserInterfaceCell[]; + uiViewport?: IUserInterfaceViewport; + workFilter: { + value: string, + map: Map + }; } -export type StandaloneState = IState & Partial; +export type StandaloneState = IState & Partial; export interface IProps { data_previous?: any[]; @@ -262,43 +257,40 @@ export interface IProps { id: string; - tooltips?: ITableTooltips; + tooltip_data?: DataTooltips; tooltip_delay: number | null; tooltip_duration: number | null; - column_static_tooltip: ITableStaticTooltips; - column_conditional_tooltips: ConditionalTooltip[]; + tooltip: ITableStaticTooltips; + tooltip_conditional: ConditionalTooltip[]; active_cell?: ICellCoordinates; columns?: Columns; - column_conditional_dropdowns?: IConditionalColumnDropdown[]; - column_static_dropdown?: IColumnDropdown[]; - content_style: ContentStyle; + dropdown?: StaticDropdowns; + dropdown_conditional?: ConditionalDropdowns; + dropdown_data: DataDropdowns; css?: IStylesheetRule[]; data?: Data; - dropdown_properties: any; // legacy editable?: boolean; - filter?: string; - filtering?: Filtering; - filtering_type?: FilteringType; - filtering_types?: FilteringType[]; + filter_query?: string; + filter_action?: TableAction; locale_format: INumberLocale; merge_duplicate_headers?: boolean; - navigation?: Navigation; - n_fixed_columns?: number; - n_fixed_rows?: number; + fixed_columns?: Fixed; + fixed_rows?: Fixed; row_deletable?: boolean; row_selectable?: RowSelection; selected_cells?: SelectedCells; selected_rows?: Indices; selected_row_ids?: RowId[]; setProps?: SetProps; - sorting?: Sorting; - sort_by?: SortSettings; - sorting_type?: SortingType; - sorting_treat_empty_string_as_none?: boolean; + sort_action?: TableAction; + sort_by?: SortBy; + sort_mode?: SortMode; + sort_as_null?: SortAsNull; style_as_list_view?: boolean; - pagination_mode?: PaginationMode; - pagination_settings?: IPaginationSettings; + page_action?: TableAction; + page_current?: number; + page_size: number; style_data?: Style; style_cell?: Style; @@ -316,19 +308,17 @@ export interface IProps { interface IDefaultProps { active_cell: ICellCoordinates; columns: Columns; - column_conditional_dropdowns: IConditionalColumnDropdown[]; - column_static_dropdown: IColumnDropdown[]; + dropdown: StaticDropdowns; + dropdown_conditional: ConditionalDropdowns; + dropdown_data: DataDropdowns; css: IStylesheetRule[]; data: Data; editable: boolean; - filter: string; - filtering: Filtering; - filtering_type: FilteringType; - filtering_types: FilteringType[]; + filter_query: string; + filter_action: TableAction; merge_duplicate_headers: boolean; - navigation: Navigation; - n_fixed_columns: number; - n_fixed_rows: number; + fixed_columns: Fixed; + fixed_rows: Fixed; row_deletable: boolean; row_selectable: RowSelection; selected_cells: SelectedCells; @@ -336,14 +326,16 @@ interface IDefaultProps { end_cell: ICellCoordinates; selected_rows: Indices; selected_row_ids: RowId[]; - sorting: Sorting; - sort_by: SortSettings; - sorting_type: SortingType; - sorting_treat_empty_string_as_none: boolean; + sort_action: TableAction; + sort_by: SortBy; + sort_mode: SortMode; + sort_as_null: SortAsNull; style_as_list_view: boolean; + tooltip_data: DataTooltips; - pagination_mode: PaginationMode; - pagination_settings: IPaginationSettings; + page_action: TableAction; + page_current: number; + page_size: number; style_data: Style; style_cell: Style; @@ -374,15 +366,24 @@ interface IDerivedProps { } export type PropsWithDefaults = IProps & IDefaultProps; -export type PropsWithDefaultsAndDerived = PropsWithDefaults & IDerivedProps; -export type ControlledTableProps = PropsWithDefaults & IState & { +export type SanitizedProps = Omit< + Omit< + Merge, + 'locale_format' + >, + 'sort_as_null' +>; + +export type SanitizedAndDerivedProps = SanitizedProps & IDerivedProps; + +export type ControlledTableProps = SanitizedProps & IState & { setProps: SetProps; setState: SetState; columns: VisibleColumns; + currentTooltip: IUSerInterfaceTooltip; paginator: IPaginator; - tooltip: IUSerInterfaceTooltip; viewport: IDerivedData; viewport_selected_rows: Indices; virtual: IDerivedData; @@ -393,17 +394,17 @@ export type ControlledTableProps = PropsWithDefaults & IState & { export interface ICellFactoryProps { active_cell: ICellCoordinates; columns: VisibleColumns; - column_conditional_dropdowns: IConditionalColumnDropdown[]; - column_conditional_tooltips: ConditionalTooltip[]; - column_static_dropdown: IColumnDropdown[]; - column_static_tooltip: ITableStaticTooltips; + dropdown: StaticDropdowns; + dropdown_conditional: ConditionalDropdowns; + dropdown_data: DataDropdowns; + tooltip: ITableStaticTooltips; + currentTooltip: IUSerInterfaceTooltip; data: Data; - dropdown_properties: any; // legacy editable: boolean; id: string; is_focused?: boolean; - n_fixed_columns: number; - n_fixed_rows: number; + fixed_columns: number; + fixed_rows: number; paginator: IPaginator; row_deletable: boolean; row_selectable: RowSelection; @@ -422,8 +423,7 @@ export interface ICellFactoryProps { style_filter_conditional: BasicFilters; style_header_conditional: Headers; style_table: Table; - tooltip: IUSerInterfaceTooltip; - tooltips?: ITableTooltips; + tooltip_data: DataTooltips; uiCell?: IUserInterfaceCell; uiViewport?: IUserInterfaceViewport; viewport: IDerivedData; diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index f575435e4..a61f412e4 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -2,7 +2,7 @@ import { ColumnId, Datum, ColumnType } from 'dash-table/components/Table/props'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; export interface IConditionalElement { - filter?: string; + filter_query?: string; } export interface IIndexedHeaderElement { @@ -68,6 +68,6 @@ export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, head export function ifFilter(condition: IConditionalElement | undefined, datum: Datum) { return !condition || - condition.filter === undefined || - ifAstFilter(new QuerySyntaxTree(condition.filter), datum); + condition.filter_query === undefined || + ifAstFilter(new QuerySyntaxTree(condition.filter_query), datum); } \ No newline at end of file diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index ac731aef3..351adde46 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -31,21 +31,16 @@ export default class DataTable extends Component { } export const defaultProps = { - pagination_mode: 'fe', - pagination_settings: { - current_page: 0, - page_size: 250 - }, - navigation: 'page', + page_action: 'native', + page_current: 0, + page_size: 250, - content_style: 'grow', css: [], - filter: '', - filtering: false, - filtering_type: 'basic', - filtering_types: ['basic'], - sorting: false, - sorting_type: 'single', + filter_query: '', + filter_action: 'none', + sort_as_null: [], + sort_action: 'none', + sort_mode: 'single', sort_by: [], style_as_list_view: false, @@ -60,11 +55,22 @@ export const defaultProps = { derived_virtual_selected_rows: [], derived_virtual_selected_row_ids: [], - column_conditional_dropdowns: [], - column_static_dropdown: [], + dropdown: {}, + dropdown_conditional: [], + dropdown_data: [], + + fixed_columns: { + headers: false, + data: 0 + }, + fixed_rows: { + headers: false, + data: 0 + }, - column_static_tooltip: {}, - column_conditional_tooltips: [], + tooltip: {}, + tooltip_conditional: [], + tooltip_data: [], tooltip_delay: 350, tooltip_duration: 2000, @@ -92,40 +98,29 @@ export const propTypes = { row: PropTypes.number, column: PropTypes.number, row_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + column_id: PropTypes.string }), /** * Columns describes various aspects about each individual column. * `name` and `id` are the only required parameters. */ - columns: PropTypes.arrayOf(PropTypes.shape({ - - /** - * If the column is rendered as dropdowns, then the - * `clearable` property determines whether or not - * the dropdown value can be cleared or not. - * - * NOTE - The name of this property may change in the future, - * subscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) - * for more information. - */ - clearable: PropTypes.bool, + columns: PropTypes.arrayOf(PropTypes.exact({ /** * If True, the user can delete the column by clicking on a little `x` * button on the column. * If there are merged, multi-header columns then you can choose * which column header row to display the "x" in by - * supplying a column row index. - * For example, `0` will display the "x" on the first row, - * `1` on the second row. + * supplying an array of booleans. + * For example, `[true, false]` will display the "x" on the first row, + * but not the second row. * If the "x" appears on a merged column, then clicking on that button * will delete *all* of the merged columns associated with it. */ deletable: PropTypes.oneOfType([ PropTypes.bool, - PropTypes.number + PropTypes.arrayOf(PropTypes.bool) ]), /** @@ -144,14 +139,16 @@ export const propTypes = { /** * If True, then the name of this column is editable. * If there are multiple column headers (if `name` is a list of strings), - * then `editable_name` can refer to _which_ column header should be - * editable by setting it to the column header index. + * then `renamable` can refer to _which_ column header should be + * editable by defining an array of booleans. + * For example, `[true, false]` will make the first row's name editable, + * but not the second row. * Also, updating the name in a merged column header cell will * update the name of each column. */ - editable_name: PropTypes.oneOfType([ + renamable: PropTypes.oneOfType([ PropTypes.bool, - PropTypes.number + PropTypes.arrayOf(PropTypes.bool) ]), /** @@ -183,8 +180,8 @@ export const propTypes = { * dash_table.FormatTemplate contains helper functions to rapidly use certain * typical number formats. */ - format: PropTypes.shape({ - locale: PropTypes.shape({ + format: PropTypes.exact({ + locale: PropTypes.exact({ symbol: PropTypes.arrayOf(PropTypes.string), decimal: PropTypes.string, group: PropTypes.string, @@ -243,7 +240,7 @@ export const propTypes = { * default: replace the provided value with `validation.default` * reject: do not modify the existing value */ - on_change: PropTypes.shape({ + on_change: PropTypes.exact({ action: PropTypes.oneOf([ 'coerce', 'none', @@ -256,6 +253,17 @@ export const propTypes = { ]) }), + /** + * An array of string, number and boolean values that are treated as `null` + * (i.e. ignored and always displayed last) when sorting. + * This value overrides the table-level `sort_as_null`. + */ + sort_as_null: PropTypes.arrayOf(PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool + ])), + /** * The `validation` options. * 'allow_null': Allow the use of nully values (undefined, null, NaN) (default: false) @@ -265,30 +273,12 @@ export const propTypes = { * this is 1949 to 2048 but in 2020 it will be different. If used with * `action: 'coerce'`, will convert user input to a 4-digit year. */ - validation: PropTypes.shape({ + validation: PropTypes.exact({ allow_null: PropTypes.bool, default: PropTypes.any, allow_YY: PropTypes.bool }), - /** - * DEPRECATED - * Please use `column_static_dropdown` instead. - * NOTE - Dropdown behavior will likely change in the future, - * subscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) - * for more information. - */ - options: PropTypes.arrayOf(PropTypes.shape({ - label: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]).isRequired, - value: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]).isRequired - })), - /** * The data-type of the column's data. * 'numeric': represents both floats and ints @@ -335,7 +325,7 @@ export const propTypes = { * 'percent': (default: '%') the string used for the percentage symbol * 'separate_4digits': (default: True) separate integers with 4-digits or less */ - locale_format: PropTypes.shape({ + locale_format: PropTypes.exact({ symbol: PropTypes.arrayOf(PropTypes.string), decimal: PropTypes.string, group: PropTypes.string, @@ -345,17 +335,6 @@ export const propTypes = { separate_4digits: PropTypes.bool }), - /** - * `content_style` toggles between a set of CSS styles for - * two common behaviors: - * - `fit`: The table container's width be equal to the width of its content. - * - `grow`: The table container's width will grow to be the size of the container. - * - * NOTE - This property will likely change in the future, - * subscribe to [https://github.com/plotly/dash-table/issues/176](https://github.com/plotly/dash-table/issues/176) - * for more details. - */ - content_style: PropTypes.oneOf(['fit', 'grow']), /** * The `css` property is a way to embed CSS selectors and rules * onto the page. @@ -368,7 +347,7 @@ export const propTypes = { * ] * */ - css: PropTypes.arrayOf(PropTypes.shape({ + css: PropTypes.arrayOf(PropTypes.exact({ selector: PropTypes.string.isRequired, rule: PropTypes.string.isRequired })), @@ -432,7 +411,7 @@ export const propTypes = { row: PropTypes.number, column: PropTypes.number, row_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + column_id: PropTypes.string }), /** @@ -454,34 +433,62 @@ export const propTypes = { merge_duplicate_headers: PropTypes.bool, /** - * `n_fixed_columns` will "fix" the set of columns so that + * `fixed_columns` will "fix" the set of columns so that * they remain visible when scrolling horizontally across - * the unfixed columns. `n_fixed_columns` fixes columns - * from left-to-right, so `n_fixed_columns=3` will fix - * the first 3 columns. + * the unfixed columns. `fixed_columns` fixes columns + * from left-to-right. + * + * If `headers` is False, no columns are fixed. + * If `headers` is True, all operation columns (see `row_deletable` and `row_selectable`) + * are fixed. Additional data columns can be fixed by + * assigning a number to `data`. + * + * Defaults to `{ headers: False }`. * * Note that fixing columns introduces some changes to the * underlying markup of the table and may impact the * way that your columns are rendered or sized. * View the documentation examples to learn more. */ - n_fixed_columns: PropTypes.number, + fixed_columns: PropTypes.oneOfType([ + PropTypes.exact({ + headers: PropTypes.oneOf([false]), + data: PropTypes.oneOf([0]) + }), + PropTypes.exact({ + headers: PropTypes.oneOf([true]).isRequired, + data: PropTypes.number + }) + ]), /** - * `n_fixed_rows` will "fix" the set of rows so that + * `fixed_rows` will "fix" the set of rows so that * they remain visible when scrolling vertically down - * the table. `n_fixed_rows` fixes rows - * from top-to-bottom, starting from the headers, - * so `n_fixed_rows=1` will fix the header row, - * `n_fixed_rows=2` will fix the header row and the first row, - * or the first two header rows (if there are multiple headers). + * the table. `fixed_rows` fixes rows + * from top-to-bottom, starting from the headers. + * + * If `headers` is False, no rows are fixed. + * If `headers` is True, all header and filter rows (see `filter_action`) are + * fixed. Additional data rows can be fixed by assigning + * a number to `data`. + * + * Defaults to `{ headers: False }`. * * Note that fixing rows introduces some changes to the * underlying markup of the table and may impact the * way that your columns are rendered or sized. * View the documentation examples to learn more. */ - n_fixed_rows: PropTypes.number, + fixed_rows: PropTypes.oneOfType([ + PropTypes.exact({ + headers: PropTypes.oneOf([false]), + data: PropTypes.oneOf([0]) + }), + PropTypes.exact({ + headers: PropTypes.oneOf([true]).isRequired, + data: PropTypes.number + }) + ]), /** * If True, then a `x` will appear next to each `row` @@ -513,7 +520,7 @@ export const propTypes = { row: PropTypes.number, column: PropTypes.number, row_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + column_id: PropTypes.string })), /** @@ -545,7 +552,7 @@ export const propTypes = { row: PropTypes.number, column: PropTypes.number, row_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + column_id: PropTypes.string }), /** @@ -555,7 +562,7 @@ export const propTypes = { style_as_list_view: PropTypes.bool, /** - * "pagination" refers to a mode of the table where + * `page_action` refers to a mode of the table where * not all of the rows are displayed at once: only a subset * are displayed (a "page") and the next subset of rows * can viewed by clicking "Next" or "Previous" buttons @@ -569,78 +576,61 @@ export const propTypes = { * in the table (e.g. page through `10,000` rows in `data` `100` rows at a time) * or we can update the data on-the-fly with callbacks * when the user clicks on the "Previous" or "Next" buttons. - * These modes can be toggled with this `pagination_mode` parameter: - * - `'fe'` refers to "front-end" paging: passing large data up-front - * - `'be'` refers to "back-end" paging: updating the data on the fly via callbacks - * - `False` will disable paging, attempting to render all of the data at once - * - `True` is the same as `fe` - * - * NOTE: The `fe` and `be` names may change in the future. - * Tune in to [https://github.com/plotly/dash-table/issues/167](https://github.com/plotly/dash-table/issues/167) for more. + * These modes can be toggled with this `page_action` parameter: + * - `'native'`: all data is passed to the table up-front, paging logic is + * handled by the table + * - `'custom'`: data is passed to the table one page at a time, paging logic + * is handled via callbacks + * - `none`: disables paging, render all of the data at once */ - pagination_mode: PropTypes.oneOf(['fe', 'be', true, false]), + page_action: PropTypes.oneOf(['custom', 'native', 'none']), /** - * `pagination_settings` controls the pagination settings - * _and_ represents the current state of the pagination UI. - * - `page_size` represents the number of rows that will be - * displayed on a particular page. - * - `current_page` represents which page the user is on. + * `page_current` represents which page the user is on. * Use this property to index through data in your callbacks with * backend paging. */ - pagination_settings: PropTypes.shape({ - current_page: PropTypes.number.isRequired, - page_size: PropTypes.number.isRequired - }), + page_current: PropTypes.number, /** - * DEPRECATED + * `page_size` represents the number of rows that will be + * displayed on a particular page when `page_action` is `'custom'` or `'native'` */ - navigation: PropTypes.string, + page_size: PropTypes.number, /** - * `column_conditional_dropdowns` specifies the available options - * for dropdowns in various columns and cells. - * This property allows you to specify different dropdowns - * depending on certain conditions. For example, you may - * render different "city" dropdowns in a row depending on the - * current value in the "state" column. + * `dropdown` specifies dropdown options for different columns. * - * NOTE: The naming and the behavior of this option may change - * in the future. - * Tune in to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) + * Each entry refers to the column ID. + * The `clearable` property defines whether the value can be deleted. + * The `options` property refers to the `options` of the dropdown. */ - column_conditional_dropdowns: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - // .exact - dropdowns: PropTypes.arrayOf(PropTypes.shape({ - condition: PropTypes.string.isRequired, - // .exact - dropdown: PropTypes.arrayOf(PropTypes.shape({ - label: PropTypes.string.isRequired, - value: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]).isRequired - })).isRequired + dropdown: PropTypes.objectOf(PropTypes.exact({ + clearable: PropTypes.bool, + options: PropTypes.arrayOf(PropTypes.exact({ + label: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string + ]).isRequired })).isRequired })), + /** - * `column_static_dropdown` represents the available dropdown - * options for different columns. - * The `id` property refers to the column ID. - * The `dropdown` property refers to the `options` of the - * dropdown. + * `dropdown_conditional` specifies dropdown options in various columns and cells. * - * NOTE: The naming and the behavior of this option may change - * in the future. - * Tune in to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) + * This property allows you to specify different dropdowns + * depending on certain conditions. For example, you may + * render different "city" dropdowns in a row depending on the + * current value in the "state" column. */ - column_static_dropdown: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - // .exact - dropdown: PropTypes.arrayOf(PropTypes.shape({ + dropdown_conditional: PropTypes.arrayOf(PropTypes.exact({ + clearable: PropTypes.bool, + if: PropTypes.exact({ + column_id: PropTypes.string, + filter_query: PropTypes.string + }), + options: PropTypes.arrayOf(PropTypes.exact({ label: PropTypes.string.isRequired, value: PropTypes.oneOfType([ PropTypes.number, @@ -650,7 +640,28 @@ export const propTypes = { })), /** - * `column_static_tooltip` represents the tooltip shown + * `dropdown_data` specifies dropdown options on a row-by-row, column-by-column basis. + * + * Each item in the array corresponds to the corresponding dropdowns for the `data` item + * at the same index. Each entry in the item refers to the Column ID. + */ + dropdown_data: PropTypes.arrayOf( + PropTypes.objectOf( + PropTypes.exact({ + clearable: PropTypes.bool, + options: PropTypes.arrayOf(PropTypes.exact({ + label: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string + ]).isRequired + })).isRequired + }) + ) + ), + + /** + * `tooltip` represents the tooltip shown * for different columns. * The `property` name refers to the column ID. * The `type` refers to the type of tooltip syntax used @@ -671,9 +682,9 @@ export const propTypes = { * a plain string. The `text` syntax will be used in * that case. */ - column_static_tooltip: PropTypes.objectOf( + tooltip: PropTypes.objectOf( PropTypes.oneOfType([ - PropTypes.shape({ + PropTypes.exact({ delay: PropTypes.number, duration: PropTypes.number, type: PropTypes.oneOf([ @@ -687,7 +698,7 @@ export const propTypes = { ), /** - * `column_conditional_tooltips` represents the tooltip shown + * `tooltip_conditional` represents the tooltip shown * for different columns and cells. * * This property allows you to specify different tooltips for @@ -708,7 +719,7 @@ export const propTypes = { * ID that must be matched. * The `if` nested property `row_index` refers to the index * of the row in the source `data`. - * The `if` nested property `filter` refers to the query that + * The `if` nested property `filter_query` refers to the query that * must evaluate to True. * * The `type` refers to the type of tooltip syntax used @@ -725,20 +736,20 @@ export const propTypes = { * This overrides the table's `tooltip_duration` property. * If set to `null`, the tooltip will not disappear. */ - column_conditional_tooltips: PropTypes.arrayOf(PropTypes.shape({ - if: PropTypes.shape({ - filter: PropTypes.string, + tooltip_conditional: PropTypes.arrayOf(PropTypes.exact({ + delay: PropTypes.number, + duration: PropTypes.number, + if: PropTypes.exact({ + column_id: PropTypes.string, + filter_query: PropTypes.string, row_index: PropTypes.oneOfType([ PropTypes.number, PropTypes.oneOf([ 'odd', 'even' ]) - ]), - column_id: PropTypes.string + ]) }).isRequired, - delay: PropTypes.number, - duration: PropTypes.number, type: PropTypes.oneOf([ 'text', 'markdown' @@ -747,7 +758,7 @@ export const propTypes = { })), /** - * `tooltips` represents the tooltip shown + * `tooltip_data` represents the tooltip shown * for different columns and cells. * The `property` name refers to the column ID. Each property * contains a list of tooltips mapped to the source `data` @@ -771,10 +782,10 @@ export const propTypes = { * a plain string. The `text` syntax will be used in * that case. */ - tooltips: PropTypes.objectOf(PropTypes.arrayOf( + tooltip_data: PropTypes.arrayOf(PropTypes.objectOf( PropTypes.oneOfType([ PropTypes.string, - PropTypes.shape({ + PropTypes.exact({ delay: PropTypes.number, duration: PropTypes.number, type: PropTypes.oneOf([ @@ -805,68 +816,43 @@ export const propTypes = { tooltip_duration: PropTypes.number, /** - * If `filtering` is enabled, then the current filtering - * string is represented in this `filter` + * If `filter_action` is enabled, then the current filtering + * string is represented in this `filter_query` * property. - * NOTE: The shape and structure of this property will - * likely change in the future. - * Stay tuned in [https://github.com/plotly/dash-table/issues/169](https://github.com/plotly/dash-table/issues/169) */ - filter: PropTypes.string, + filter_query: PropTypes.string, /** - * The `filtering` property controls the behavior of the `filtering` UI. - * If `False`, then the filtering UI is not displayed - * If `fe` or True, then the filtering UI is displayed and the filtering - * happens in the "front-end". That is, it is performed on the data + * The `filter_action` property controls the behavior of the `filtering` UI. + * + * If `'none'`, then the filtering UI is not displayed + * If `'native'`, then the filtering UI is displayed and the filtering + * logic is handled by the table. That is, it is performed on the data * that exists in the `data` property. - * If `be`, then the filtering UI is displayed but it is the + * If `'custom'`, then the filtering UI is displayed but it is the * responsibility of the developer to program the filtering - * through a callback (where `filter` would be the input + * through a callback (where `filter_query` or `derived_filter_query_structure` would be the input * and `data` would be the output). - * - * NOTE - Several aspects of filtering may change in the future, - * including the naming of this property. - * Tune in to [https://github.com/plotly/dash-table/issues/167](https://github.com/plotly/dash-table/issues/167) */ - filtering: PropTypes.oneOf(['fe', 'be', true, false]), + filter_action: PropTypes.oneOf(['custom', 'native', 'none']), /** - * UNSTABLE - * In the future, there may be several modes of the - * filtering UI like `basic`, `advanced`, etc. - * Currently, we only `basic`. - * NOTE - This will likely change in the future, - * subscribe to changes here: - * [https://github.com/plotly/dash-table/issues/169](https://github.com/plotly/dash-table/issues/169) - */ - filtering_type: PropTypes.oneOf(['basic']), - - /** - * UNSTABLE - * In the future, there may be several modes of the - * filtering UI like `basic`, `advanced`, etc - * NOTE - This will likely change in the future, - * subscribe to changes here: - * [https://github.com/plotly/dash-table/issues/169](https://github.com/plotly/dash-table/issues/169) - */ - filtering_types: PropTypes.arrayOf(PropTypes.oneOf([ - 'basic' - ])), - - /** - * The `sorting` property enables data to be + * The `sort_action` property enables data to be * sorted on a per-column basis. - * Enabling `sorting` will display a UI element - * on each of the columns (up and down arrows). * - * Sorting can be performed in the "front-end" - * with the `fe` (or True) setting or via a callback in your - * python "back-end" with the `be` setting. + * If `'none'`, then the sorting UI is not displayed. + * If `'native'`, then the sorting UI is displayed and the sorting + * logic is hanled by the table. That is, it is performed on the data + * that exists in the `data` property. + * If `'custom'`, the the sorting UI is displayed but it is the + * responsibility of the developer to program the sorting + * through a callback (where `sort_by` would be the input and `data` + * would be the output). + * * Clicking on the sort arrows will update the * `sort_by` property. */ - sorting: PropTypes.oneOf(['fe', 'be', true, false]), + sort_action: PropTypes.oneOf(['custom', 'native', 'none']), /** * Sorting can be performed across multiple columns @@ -878,7 +864,7 @@ export const propTypes = { * the columns were sorted through the UI. * See [https://github.com/plotly/dash-table/issues/170](https://github.com/plotly/dash-table/issues/170) */ - sorting_type: PropTypes.oneOf(['single', 'multi']), + sort_mode: PropTypes.oneOf(['single', 'multi']), /** * `sort_by` describes the current state @@ -892,19 +878,23 @@ export const propTypes = { * clicked. */ sort_by: PropTypes.arrayOf( - // .exact - PropTypes.shape({ - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + PropTypes.exact({ + column_id: PropTypes.string.isRequired, direction: PropTypes.oneOf(['asc', 'desc']).isRequired })), /** - * If False, then empty strings (`''`) are considered - * valid values (they will appear first when sorting ascending). - * If True, empty strings will be ignored, causing these cells to always - * appear last. + * An array of string, number and boolean values that are treated as `null` + * (i.e. ignored and always displayed last) when sorting. + * This value will be used by columns without `sort_as_null`. + * + * Defaults to `[]`. */ - sorting_treat_empty_string_as_none: PropTypes.bool, + sort_as_null: PropTypes.arrayOf(PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool + ])), /** * CSS styles to be applied to the outer `table` container. @@ -950,9 +940,8 @@ export const propTypes = { * This can be used to apply styles to cells on a per-column basis. */ style_cell_conditional: PropTypes.arrayOf(PropTypes.shape({ - // .exact - if: PropTypes.shape({ - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + if: PropTypes.exact({ + column_id: PropTypes.string, column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']) }) })), @@ -963,11 +952,10 @@ export const propTypes = { * This can be used to apply styles to data cells on a per-column basis. */ style_data_conditional: PropTypes.arrayOf(PropTypes.shape({ - // .exact - if: PropTypes.shape({ - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + if: PropTypes.exact({ + column_id: PropTypes.string, column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), - filter: PropTypes.string, + filter_query: PropTypes.string, row_index: PropTypes.oneOfType([ PropTypes.number, PropTypes.oneOf(['odd', 'even']) @@ -981,9 +969,8 @@ export const propTypes = { * This can be used to apply styles to filter cells on a per-column basis. */ style_filter_conditional: PropTypes.arrayOf(PropTypes.shape({ - // .exact - if: PropTypes.shape({ - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + if: PropTypes.exact({ + column_id: PropTypes.string, column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']) }) })), @@ -994,9 +981,8 @@ export const propTypes = { * This can be used to apply styles to header cells on a per-column basis. */ style_header_conditional: PropTypes.arrayOf(PropTypes.shape({ - // .exact - if: PropTypes.shape({ - column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + if: PropTypes.exact({ + column_id: PropTypes.string, column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), header_index: PropTypes.oneOfType([ PropTypes.number, @@ -1017,7 +1003,7 @@ export const propTypes = { /** * This property represents the current structure of - * `filter` as a tree structure. Each node of the + * `filter_query` as a tree structure. Each node of the * query structure have: * - type (string; required) * - 'open-block' @@ -1039,10 +1025,10 @@ export const propTypes = { * - left (nested query structure; optional) * - right (nested query structure; optional) * - * If the query is invalid or empty, the `derived_filter_structure` will + * If the query is invalid or empty, the `derived_filter_query_structure` will * be null. */ - derived_filter_structure: PropTypes.object, + derived_filter_query_structure: PropTypes.object, /** * This property represents the current state of `data` @@ -1121,14 +1107,7 @@ export const propTypes = { */ derived_virtual_selected_row_ids: PropTypes.arrayOf( PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - ), - - /** - * DEPRECATED - * Subscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) - * for updates on the dropdown API. - */ - dropdown_properties: PropTypes.any + ) }; DataTable.defaultProps = defaultProps; diff --git a/src/dash-table/dash/sanitize.ts b/src/dash-table/dash/sanitize.ts index 78f2f2d98..e90579bf1 100644 --- a/src/dash-table/dash/sanitize.ts +++ b/src/dash-table/dash/sanitize.ts @@ -2,7 +2,19 @@ import * as R from 'ramda'; import { memoizeOne } from 'core/memoizer'; -import { Columns, ColumnType, INumberLocale } from 'dash-table/components/Table/props'; +import { + Columns, + ColumnType, + Fixed, + IColumn, + INumberLocale, + PropsWithDefaults, + RowSelection, + SanitizedProps, + SortAsNull, + TableAction +} from 'dash-table/components/Table/props'; +import headerRows from 'dash-table/derived/header/headerRows'; const D3_DEFAULT_LOCALE: INumberLocale = { symbol: ['$', ''], @@ -19,9 +31,11 @@ const DEFAULT_SPECIFIER = ''; const applyDefaultToLocale = memoizeOne((locale: INumberLocale) => getLocale(locale)); const applyDefaultsToColumns = memoizeOne( - (defaultLocale: INumberLocale, columns: Columns) => R.map(column => { + (defaultLocale: INumberLocale, defaultSort: SortAsNull, columns: Columns) => R.map(column => { const c = R.clone(column); + 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); @@ -31,16 +45,33 @@ const applyDefaultsToColumns = memoizeOne( }, columns) ); -export default (props: any) => { +const data2number = (data?: any) => +data || 0; + +const getFixedColumns = ( + fixed: Fixed, + row_deletable: boolean, + row_selectable: RowSelection +) => !fixed.headers ? + 0 : + (row_deletable ? 1 : 0) + (row_selectable ? 1 : 0) + data2number(fixed.data); + +const getFixedRows = ( + fixed: Fixed, + columns: IColumn[], + filter_action: TableAction +) => !fixed.headers ? + 0 : + headerRows(columns) + (filter_action !== TableAction.None ? 1 : 0) + data2number(fixed.data); + +export default (props: PropsWithDefaults): SanitizedProps => { const locale_format = applyDefaultToLocale(props.locale_format); - return R.mergeAll([ - props, - { - columns: applyDefaultsToColumns(locale_format, props.columns), - locale_format - } - ]); + return R.merge(props, { + columns: applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns), + 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 + }); }; export const getLocale = (...locales: Partial[]): INumberLocale => diff --git a/src/dash-table/dash/validate.ts b/src/dash-table/dash/validate.ts index 6fc29134e..0bf444ffe 100644 --- a/src/dash-table/dash/validate.ts +++ b/src/dash-table/dash/validate.ts @@ -1,13 +1,14 @@ import * as R from 'ramda'; import Logger from 'core/Logger'; +import { TableAction } from 'dash-table/components/Table/props'; -function isFrontEnd(value: any) { - return ['fe', true, false].indexOf(value) !== -1; +function isFrontEnd(value: TableAction) { + return value !== TableAction.Custom; } -function isBackEnd(value: any) { - return ['be', false].indexOf(value) !== -1; +function isBackEnd(value: TableAction) { + return value !== TableAction.Native; } function validColumns(props: any) { @@ -32,18 +33,18 @@ function validColumns(props: any) { function validFSP(props: any) { const { - filtering, - sorting, - pagination_mode + filter_action, + sort_action, + page_action } = props; - return isFrontEnd(pagination_mode) || - (isBackEnd(filtering) && isBackEnd(sorting)); + return isFrontEnd(page_action) || + (isBackEnd(filter_action) && isBackEnd(sort_action)); } export default (props: any): boolean => { if (!validFSP(props)) { - Logger.error(`Invalid combination of filtering / sorting / pagination`); + Logger.error(`Invalid combination of filter_action / sort_action / page_action`); return false; } diff --git a/src/dash-table/derived/cell/contents.tsx b/src/dash-table/derived/cell/contents.tsx index dfc1ebac5..ffe4a8067 100644 --- a/src/dash-table/derived/cell/contents.tsx +++ b/src/dash-table/derived/cell/contents.tsx @@ -5,8 +5,9 @@ import { ICellCoordinates, Data, Datum, - DropdownValues, ICellFactoryProps, + IDropdown, + IDropdownValue, IViewportOffset, IVisibleColumn, Presentation, @@ -33,7 +34,7 @@ enum CellType { function getCellType( active: boolean, editable: boolean, - dropdown: DropdownValues | undefined, + dropdown: IDropdownValue[] | undefined, presentation: Presentation | undefined ): CellType { switch (presentation) { @@ -63,7 +64,7 @@ class Contents { offset: IViewportOffset, editable: boolean, isFocused: boolean, - dropdowns: (DropdownValues | undefined)[][] + dropdowns: (IDropdown | undefined)[][] ): JSX.Element[][] => { const formatters = R.map(getFormatter, columns); @@ -82,13 +83,13 @@ class Contents { 'dash-cell-value' ].join(' '); - switch (getCellType(active, isEditable, dropdown, column.presentation)) { + switch (getCellType(active, isEditable, dropdown && dropdown.options, column.presentation)) { case CellType.Dropdown: return (); diff --git a/src/dash-table/derived/cell/dropdowns.ts b/src/dash-table/derived/cell/dropdowns.ts index 64961c99f..fa6cfb364 100644 --- a/src/dash-table/derived/cell/dropdowns.ts +++ b/src/dash-table/derived/cell/dropdowns.ts @@ -4,22 +4,22 @@ import { memoizeOne } from 'core/memoizer'; import memoizerCache from 'core/cache/memoizer'; import { - IConditionalDropdown -} from 'dash-table/components/CellDropdown/types'; - -import { + ColumnId, + ConditionalDropdowns, Data, + DataDropdowns, Datum, - VisibleColumns, - ColumnId, + IConditionalDropdown, + IDropdown, Indices, - DropdownValues, - IBaseVisibleColumn, - IVisibleColumn + IVisibleColumn, + StaticDropdowns, + VisibleColumns } from 'dash-table/components/Table/props'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; +import { ifColumnId } from 'dash-table/conditional'; -const mapData = R.addIndex(R.map); +const mapData = R.addIndex(R.map); export default () => new Dropdowns().get; @@ -31,84 +31,55 @@ class Dropdowns { columns: VisibleColumns, data: Data, indices: Indices, - columnConditionalDropdown: any, - columnStaticDropdown: any, - dropdown_properties: any + conditionalDropdowns: ConditionalDropdowns, + staticDropdowns: StaticDropdowns, + dataDropdowns: DataDropdowns ) => mapData((datum, rowIndex) => R.map(column => { - const applicable = this.applicable.get(column.id, rowIndex)( - column, - indices[rowIndex], - columnConditionalDropdown, - columnStaticDropdown, - dropdown_properties - ); + const realIndex = indices[rowIndex]; + + const appliedStaticDropdown = ( + dataDropdowns && + dataDropdowns.length > realIndex && + dataDropdowns[realIndex] && + dataDropdowns[realIndex][column.id] + ) || staticDropdowns[column.id]; return this.dropdown.get(column.id, rowIndex)( - applicable, + appliedStaticDropdown, + conditionalDropdowns, column, datum ); }, columns), data)); - /** - * Returns the list of applicable dropdowns for a cell. - */ - private readonly applicable = memoizerCache<[ColumnId, number]>()(( - column: IBaseVisibleColumn, - realIndex: number, - columnConditionalDropdown: any, - columnStaticDropdown: any, - dropdown_properties: any - ): [any, any] => { - let legacyDropdown = ( - ( - dropdown_properties && - dropdown_properties[column.id] && - ( - dropdown_properties[column.id].length > realIndex ? - dropdown_properties[column.id][realIndex] : - null - ) - ) || column - ).options; - - const conditional = columnConditionalDropdown.find((cs: any) => cs.id === column.id); - const base = columnStaticDropdown.find((ss: any) => ss.id === column.id); - - return [ - legacyDropdown || (base && base.dropdown), - (conditional && conditional.dropdowns) || [] - ]; - }); - /** * Returns the highest priority dropdown from the * applicable dropdowns. */ private readonly dropdown = memoizerCache<[ColumnId, number]>()(( - applicableDropdowns: [any, any], + base: IDropdown | undefined, + conditionals: ConditionalDropdowns, column: IVisibleColumn, datum: Datum ) => { - const [staticDropdown, conditionalDropdowns] = applicableDropdowns; - - const matches = [ - ...(staticDropdown ? [staticDropdown] : []), - ...R.map( - ([cd]) => cd.dropdown, - R.filter<[IConditionalDropdown, number]>( - ([cd, i]) => this.evaluation.get(column.id, i)( - this.ast.get(column.id, i)(cd.condition), + const conditional = R.findLast( + ([cd, i]) => + ifColumnId(cd.if, column.id) && + ( + R.isNil(cd.if) || + R.isNil(cd.if.filter_query) || + this.evaluation.get(column.id, i)( + this.ast.get(column.id, i)(cd.if.filter_query), datum - ), - R.addIndex(R.map)( - (cd, i) => [cd, i], - conditionalDropdowns - )) + ) + ), + R.addIndex(R.map)( + (cd, i) => [cd, i], + conditionals ) - ]; + ); - return matches.length ? matches.slice(-1)[0] : undefined; + return (conditional && conditional[0]) || base || undefined; }); /** diff --git a/src/dash-table/derived/data/viewport.ts b/src/dash-table/derived/data/viewport.ts index 6257d12d0..c60cf05f4 100644 --- a/src/dash-table/derived/data/viewport.ts +++ b/src/dash-table/derived/data/viewport.ts @@ -4,21 +4,20 @@ import { lastPage } from 'dash-table/derived/paginator'; import { Data, Indices, - IPaginationSettings, - PaginationMode, - IDerivedData + IDerivedData, + TableAction } from 'dash-table/components/Table/props'; function getNoPagination(data: Data, indices: Indices): IDerivedData { return { data, indices }; } -function getFrontEndPagination(settings: IPaginationSettings, data: Data, indices: Indices): IDerivedData { - let currentPage = Math.min(settings.current_page, lastPage(data, settings)); +function getFrontEndPagination(page_current: number, page_size: number, data: Data, indices: Indices): IDerivedData { + let currentPage = Math.min(page_current, lastPage(data, page_size)); - const firstIndex = settings.page_size * currentPage; + const firstIndex = page_size * currentPage; const lastIndex = Math.min( - firstIndex + settings.page_size, + firstIndex + page_size, data.length ); @@ -33,21 +32,21 @@ function getBackEndPagination(data: Data, indices: Indices): IDerivedData { } const getter = ( - pagination_mode: PaginationMode, - pagination_settings: IPaginationSettings, + page_action: TableAction, + page_current: number, + page_size: number, data: Data, indices: Indices ): IDerivedData => { - switch (pagination_mode) { - case false: + switch (page_action) { + case TableAction.None: return getNoPagination(data, indices); - case true: - case 'fe': - return getFrontEndPagination(pagination_settings, data, indices); - case 'be': + case TableAction.Native: + return getFrontEndPagination(page_current, page_size, data, indices); + case TableAction.Custom: return getBackEndPagination(data, indices); default: - throw new Error(`Unknown pagination mode: '${pagination_mode}'`); + throw new Error(`Unknown pagination mode: '${page_action}'`); } }; diff --git a/src/dash-table/derived/data/virtual.ts b/src/dash-table/derived/data/virtual.ts index ca14879d0..186057541 100644 --- a/src/dash-table/derived/data/virtual.ts +++ b/src/dash-table/derived/data/virtual.ts @@ -1,42 +1,53 @@ import * as R from 'ramda'; import { memoizeOneFactory } from 'core/memoizer'; -import sort, { defaultIsNully, SortSettings } from 'core/sorting'; +import sort, { SortBy } from 'core/sorting'; import { + ColumnId, Data, Datum, - Filtering, IDerivedData, - Sorting + SortAsNull, + VisibleColumns, + TableAction } from 'dash-table/components/Table/props'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; const getter = ( + columns: VisibleColumns, data: Data, - filtering: Filtering, - filter: string, - sorting: Sorting, - sort_by: SortSettings = [], - sorting_treat_empty_string_as_none: boolean + filter_action: TableAction, + filter_query: string, + sort_action: TableAction, + sort_by: SortBy = [] ): IDerivedData => { const map = new Map(); R.addIndex(R.forEach)((datum, index) => { map.set(datum, index); }, data); - if (filtering === 'fe' || filtering === true) { - const tree = new QuerySyntaxTree(filter); + if (filter_action === TableAction.Native) { + const tree = new QuerySyntaxTree(filter_query); data = tree.isValid ? tree.filter(data) : data; } - const isNully = sorting_treat_empty_string_as_none ? - (value: any) => value === '' || defaultIsNully(value) : - undefined; + const getNullyCases = ( + columnId: ColumnId + ): SortAsNull => { + const column = R.find(c => c.id === columnId, columns); - if (sorting === 'fe' || sorting === true) { + return (column && column.sort_as_null) || []; + }; + + const isNully = ( + value: any, + columnId: ColumnId + ) => R.isNil(value) || R.contains(value, getNullyCases(columnId)); + + if (sort_action === TableAction.Native) { data = sort(data, sort_by, isNully); } diff --git a/src/dash-table/derived/edges/operationOfFilters.ts b/src/dash-table/derived/edges/operationOfFilters.ts index 5098262b1..12964d3d0 100644 --- a/src/dash-table/derived/edges/operationOfFilters.ts +++ b/src/dash-table/derived/edges/operationOfFilters.ts @@ -30,11 +30,11 @@ const getWeightedStyle = ( export default memoizeOneFactory(( columns: number, - filtering: boolean, + filter_action: boolean, borderStyles: IConvertedStyle[], listViewStyle: boolean ) => { - if (!filtering || columns === 0) { + if (!filter_action || columns === 0) { return; } diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index fef77afed..bae228d49 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -2,19 +2,18 @@ import * as R from 'ramda'; import React from 'react'; import { memoizeOneFactory } from 'core/memoizer'; -import { SortDirection, SortSettings } from 'core/sorting'; -import multiUpdateSettings from 'core/sorting/multi'; -import singleUpdateSettings from 'core/sorting/single'; +import { SortDirection, SortBy } from 'core/sorting'; +import multiUpdate from 'core/sorting/multi'; +import singleUpdate from 'core/sorting/single'; import { ColumnId, - PaginationMode, - SortingType, + SortMode, VisibleColumns, IVisibleColumn, SetProps, ControlledTableProps, - Sorting + TableAction } from 'dash-table/components/Table/props'; import * as actions from 'dash-table/utils/actions'; @@ -24,10 +23,10 @@ function deleteColumn(column: IVisibleColumn, columns: VisibleColumns, columnRow }; } -function doSort(columnId: ColumnId, sortSettings: SortSettings, sortType: SortingType, setProps: SetProps) { +function doSort(columnId: ColumnId, sortBy: SortBy, mode: SortMode, setProps: SetProps) { return () => { let direction: SortDirection; - switch (getSorting(columnId, sortSettings)) { + switch (getSorting(columnId, sortBy)) { case SortDirection.Descending: direction = SortDirection.None; break; @@ -42,13 +41,13 @@ function doSort(columnId: ColumnId, sortSettings: SortSettings, sortType: Sortin break; } - const sortingStrategy = sortType === 'single' ? - singleUpdateSettings : - multiUpdateSettings; + const sortingStrategy = mode === SortMode.Single ? + singleUpdate : + multiUpdate; setProps({ sort_by: sortingStrategy( - sortSettings, + sortBy, { column_id: columnId, direction } ), ...actions.clearSelection @@ -62,14 +61,14 @@ function editColumnName(column: IVisibleColumn, columns: VisibleColumns, columnR }; } -function getSorting(columnId: ColumnId, settings: SortSettings): SortDirection { - const setting = R.find(s => s.column_id === columnId, settings); +function getSorting(columnId: ColumnId, sortBy: SortBy): SortDirection { + const sort = R.find(s => s.column_id === columnId, sortBy); - return setting ? setting.direction : SortDirection.None; + return sort ? sort.direction : SortDirection.None; } -function getSortingIcon(columnId: ColumnId, sortSettings: SortSettings) { - switch (getSorting(columnId, sortSettings)) { +function getSortingIcon(columnId: ColumnId, sortBy: SortBy) { + switch (getSorting(columnId, sortBy)) { case SortDirection.Descending: return '↓'; case SortDirection.Ascending: @@ -83,10 +82,10 @@ function getSortingIcon(columnId: ColumnId, sortSettings: SortSettings) { function getter( columns: VisibleColumns, labelsAndIndices: R.KeyValuePair[], - sorting: Sorting, - sortType: SortingType, - sortSettings: SortSettings, - paginationMode: PaginationMode, + sort_action: TableAction, + mode: SortMode, + sortBy: SortBy, + paginationMode: TableAction, setProps: SetProps, options: ControlledTableProps ): JSX.Element[][] { @@ -98,27 +97,28 @@ function getter( columnIndex => { const column = columns[columnIndex]; - const editable = (column.editable_name && R.type(column.editable_name) === 'Boolean') || - (R.type(column.editable_name) === 'Number' && column.editable_name === headerRowIndex); + const renamable: boolean = typeof column.renamable === 'boolean' ? + column.renamable : + !!column.renamable && column.renamable[headerRowIndex]; - const deletable = paginationMode !== 'be' && - ( - (column.deletable && R.type(column.deletable) === 'Boolean') || - (R.type(column.deletable) === 'Number' && column.deletable === headerRowIndex) - ); + const deletable = paginationMode !== TableAction.Custom && ( + typeof column.deletable === 'boolean' ? + column.deletable : + !!column.deletable && column.deletable[headerRowIndex] + ); return (
- {sorting && isLastRow ? + {sort_action !== TableAction.None && isLastRow ? ( - {getSortingIcon(column.id, sortSettings)} + {getSortingIcon(column.id, sortBy)} ) : '' } - {editable ? + {renamable ? ( (Array.isArray(c.name) ? c.name.length : 1); +const getColLength = (c: IColumn) => c.hidden ? + 0 : + Array.isArray(c.name) ? + c.name.length : + 1; export default ( - columns: VisibleColumns + columns: IColumn[] ): number => Math.max(...columns.map(getColLength)); \ No newline at end of file diff --git a/src/dash-table/derived/paginator.ts b/src/dash-table/derived/paginator.ts index da0a8a343..8f3671eb6 100644 --- a/src/dash-table/derived/paginator.ts +++ b/src/dash-table/derived/paginator.ts @@ -1,14 +1,11 @@ -import { merge } from 'ramda'; - import { memoizeOneFactory } from 'core/memoizer'; import { clearSelection } from 'dash-table/utils/actions'; import { Data, - PaginationMode, SetProps, - IPaginationSettings + TableAction } from 'dash-table/components/Table/props'; export interface IPaginator { @@ -16,59 +13,54 @@ export interface IPaginator { loadPrevious(): void; } -export function lastPage(data: Data, settings: IPaginationSettings) { - return Math.max(Math.ceil(data.length / settings.page_size) - 1, 0); +export function lastPage(data: Data, page_size: number) { + return Math.max(Math.ceil(data.length / page_size) - 1, 0); } function getBackEndPagination( - pagination_settings: IPaginationSettings, + page_current: number, setProps: SetProps ): IPaginator { return { loadNext: () => { - pagination_settings.current_page++; - setProps({ pagination_settings, ...clearSelection }); + page_current++; + setProps({ page_current, ...clearSelection }); }, loadPrevious: () => { - if (pagination_settings.current_page <= 0) { + if (page_current <= 0) { return; } - pagination_settings.current_page--; - setProps({ pagination_settings, ...clearSelection }); + page_current--; + setProps({ page_current, ...clearSelection }); } }; } function getFrontEndPagination( - pagination_settings: IPaginationSettings, + page_current: number, + page_size: number, setProps: SetProps, data: Data ) { return { loadNext: () => { - const maxPageIndex = lastPage(data, pagination_settings); + const maxPageIndex = lastPage(data, page_size); - if (pagination_settings.current_page >= maxPageIndex) { + if (page_current >= maxPageIndex) { return; } - pagination_settings = merge(pagination_settings, { - current_page: pagination_settings.current_page + 1 - }); - - setProps({ pagination_settings, ...clearSelection }); + page_current++; + setProps({ page_current, ...clearSelection }); }, loadPrevious: () => { - if (pagination_settings.current_page <= 0) { + if (page_current <= 0) { return; } - pagination_settings = merge(pagination_settings, { - current_page: pagination_settings.current_page - 1 - }); - - setProps({ pagination_settings, ...clearSelection }); + page_current--; + setProps({ page_current, ...clearSelection }); } }; } @@ -81,21 +73,21 @@ function getNoPagination() { } const getter = ( - pagination_mode: PaginationMode, - pagination_settings: IPaginationSettings, + page_action: TableAction, + page_current: number, + page_size: number, setProps: SetProps, data: Data ): IPaginator => { - switch (pagination_mode) { - case false: + switch (page_action) { + case TableAction.None: return getNoPagination(); - case true: - case 'fe': - return getFrontEndPagination(pagination_settings, setProps, data); - case 'be': - return getBackEndPagination(pagination_settings, setProps); + case TableAction.Native: + return getFrontEndPagination(page_current, page_size, setProps, data); + case TableAction.Custom: + return getBackEndPagination(page_current, setProps); default: - throw new Error(`Unknown pagination mode: '${pagination_mode}'`); + throw new Error(`Unknown pagination mode: '${page_action}'`); } }; diff --git a/src/dash-table/derived/style/index.ts b/src/dash-table/derived/style/index.ts index 61d6e6930..fc90ea4b9 100644 --- a/src/dash-table/derived/style/index.ts +++ b/src/dash-table/derived/style/index.ts @@ -49,7 +49,7 @@ function convertElement(style: GenericStyle): IConvertedStyle { !R.isNil(style.if.column_type) ), checksRow: () => !R.isNil(indexFilter), - checksFilter: () => !R.isNil(style.if) && !R.isNil(style.if.filter), + checksFilter: () => !R.isNil(style.if) && !R.isNil(style.if.filter_query), matchesColumn: (column: IVisibleColumn | undefined) => !style.if || ( @@ -65,8 +65,8 @@ function convertElement(style: GenericStyle): IConvertedStyle { !R.isNil(index) && (indexFilter === 'odd' ? index % 2 === 1 : index % 2 === 0), matchesFilter: (datum: Datum) => !style.if || - style.if.filter === undefined || - (ast = ast || new QuerySyntaxTree(style.if.filter)).evaluate(datum), + style.if.filter_query === undefined || + (ast = ast || new QuerySyntaxTree(style.if.filter_query)).evaluate(datum), style: convertStyle(style) }; } diff --git a/src/dash-table/derived/table/index.tsx b/src/dash-table/derived/table/index.tsx index 14417c49e..de8500994 100644 --- a/src/dash-table/derived/table/index.tsx +++ b/src/dash-table/derived/table/index.tsx @@ -14,12 +14,12 @@ import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; const handleSetFilter = ( setProps: SetProps, setState: SetState, - filter: string, + filter_query: string, rawFilterQuery: string, map: Map ) => { - setProps({ filter, ...clearSelection }); - setState({ workFilter: { map, value: filter }, rawFilterQuery }); + setProps({ filter_query, ...clearSelection }); + setState({ workFilter: { map, value: filter_query }, rawFilterQuery }); }; function filterPropsFn(propsFn: () => ControlledTableProps, setFilter: any) { diff --git a/src/dash-table/derived/table/tooltip.ts b/src/dash-table/derived/table/tooltip.ts index 9d5a3b11d..7fc44d17a 100644 --- a/src/dash-table/derived/table/tooltip.ts +++ b/src/dash-table/derived/table/tooltip.ts @@ -1,6 +1,6 @@ import * as R from 'ramda'; -import { IUSerInterfaceTooltip, ITableTooltips, ITableStaticTooltips, IVirtualizedDerivedData } from 'dash-table/components/Table/props'; +import { IUSerInterfaceTooltip, ITableStaticTooltips, IVirtualizedDerivedData, DataTooltips } from 'dash-table/components/Table/props'; import { ifColumnId, ifRowIndex, ifFilter } from 'dash-table/conditional'; import { ConditionalTooltip, TooltipSyntax } from 'dash-table/tooltips/props'; import { memoizeOne } from 'core/memoizer'; @@ -9,31 +9,28 @@ import { memoizeOne } from 'core/memoizer'; export const MAX_32BITS = 2147483647; function getSelectedTooltip( - tooltip: IUSerInterfaceTooltip, - tooltips: ITableTooltips | undefined, - column_conditional_tooltips: ConditionalTooltip[], - column_static_tooltip: ITableStaticTooltips, + currentTooltip: IUSerInterfaceTooltip, + tooltip_data: DataTooltips, + tooltip_conditional: ConditionalTooltip[], + tooltip_static: ITableStaticTooltips, virtualized: IVirtualizedDerivedData ) { - if (!tooltip) { + if (!currentTooltip) { return undefined; } - const { id, row } = tooltip; + const { id, row } = currentTooltip; if (id === undefined || row === undefined) { return undefined; } - const legacyTooltip = tooltips && - tooltips[id] && - ( - tooltips[id].length > row ? - tooltips[id][row] : - null - ); - - const staticTooltip = column_static_tooltip[id]; + const appliedStaticTooltip = ( + tooltip_data && + tooltip_data.length > row && + tooltip_data[row] && + tooltip_data[row][id] + ) || tooltip_static[id]; const conditionalTooltips = R.filter(tt => { return !tt.if || @@ -42,11 +39,11 @@ function getSelectedTooltip( ifRowIndex(tt.if, row) && ifFilter(tt.if, virtualized.data[row - virtualized.offset.rows]) ); - }, column_conditional_tooltips); + }, tooltip_conditional); return conditionalTooltips.length ? conditionalTooltips.slice(-1)[0] : - legacyTooltip || staticTooltip; + appliedStaticTooltip; } function convertDelay(delay: number | null) { @@ -74,19 +71,19 @@ function getDuration(duration: number | null | undefined, defaultTo: number) { } export default memoizeOne(( - tooltip: IUSerInterfaceTooltip, - tooltips: ITableTooltips | undefined, - column_conditional_tooltips: ConditionalTooltip[], - column_static_tooltip: ITableStaticTooltips, + currentTooltip: IUSerInterfaceTooltip, + tooltip_data: DataTooltips, + tooltip_conditional: ConditionalTooltip[], + tooltip_static: ITableStaticTooltips, virtualized: IVirtualizedDerivedData, defaultDelay: number | null, defaultDuration: number | null ) => { const selectedTooltip = getSelectedTooltip( - tooltip, - tooltips, - column_conditional_tooltips, - column_static_tooltip, + currentTooltip, + tooltip_data, + tooltip_conditional, + tooltip_static, virtualized ); diff --git a/src/dash-table/handlers/cellEvents.ts b/src/dash-table/handlers/cellEvents.ts index c61f4a03b..a50092bf3 100644 --- a/src/dash-table/handlers/cellEvents.ts +++ b/src/dash-table/handlers/cellEvents.ts @@ -155,7 +155,7 @@ export const handleEnter = (propsFn: () => ICellFactoryProps, idx: number, i: nu const realIdx = virtualized.indices[idx]; setState({ - tooltip: { + currentTooltip: { id: c.id, row: realIdx } @@ -167,26 +167,26 @@ export const handleLeave = (propsFn: () => ICellFactoryProps, _idx: number, _i: setState } = propsFn(); - setState({ tooltip: undefined }); + setState({ currentTooltip: undefined }); }; export const handleMove = (propsFn: () => ICellFactoryProps, idx: number, i: number) => { const { columns, + currentTooltip, virtualized, - setState, - tooltip + setState } = propsFn(); const c = columns[i]; const realIdx = virtualized.indices[idx]; - if (tooltip && tooltip.id === c.id && tooltip.row === realIdx) { + if (currentTooltip && currentTooltip.id === c.id && currentTooltip.row === realIdx) { return; } setState({ - tooltip: { + currentTooltip: { id: c.id, row: realIdx } diff --git a/src/dash-table/utils/applyClipboardToData.ts b/src/dash-table/utils/applyClipboardToData.ts index b1bbb9df1..e0e731a83 100644 --- a/src/dash-table/utils/applyClipboardToData.ts +++ b/src/dash-table/utils/applyClipboardToData.ts @@ -36,7 +36,8 @@ export default ( newColumns.push({ id: `Column ${i + 1}`, name: `Column ${i + 1}`, - type: ColumnType.Any + type: ColumnType.Any, + sort_as_null: [] }); newData.forEach(row => (row[`Column ${i}`] = '')); } diff --git a/tests/cypress/dash/v_be_page.py b/tests/cypress/dash/v_be_page.py index c5296d929..20775f7f6 100644 --- a/tests/cypress/dash/v_be_page.py +++ b/tests/cypress/dash/v_be_page.py @@ -28,12 +28,9 @@ dash_table.DataTable( id="table", data=[], - pagination_mode="be", - pagination_settings={ - "current_page": 0, - "page_size": 250, - }, - navigation="page", + page_action="custom", + page_current=0, + page_size=250, columns=[ {"id": 0, "name": "Complaint ID"}, {"id": 1, "name": "Product"}, @@ -50,12 +47,12 @@ {"id": 12, "name": "Timely response?"}, {"id": 13, "name": "Consumer disputed?"}, ], - n_fixed_columns=2, - n_fixed_rows=1, + fixed_columns={ 'headers': True, 'data': -1 }, + fixed_rows={ 'headers': True, 'data': -1 }, row_selectable=True, row_deletable=True, - sorting="be", - filtering=False, + sort_action="custom", + filter_action='none', editable=True, ), ] @@ -63,15 +60,11 @@ @app.callback(Output("table", "data"), [ - Input("table", "pagination_settings"), + Input("table", "page_current"), + Input("table", "page_size"), Input("table", "sort_by") ]) -def updateData(pagination_settings, sort_by): - print(pagination_settings) - - current_page = pagination_settings["current_page"] - page_size = pagination_settings["page_size"] - +def updateData(current_page, page_size, sort_by): start_index = current_page * page_size end_index = start_index + page_size print(str(start_index) + "," + str(end_index)) diff --git a/tests/cypress/dash/v_copy_paste.py b/tests/cypress/dash/v_copy_paste.py index 2330994cc..052bcfe30 100644 --- a/tests/cypress/dash/v_copy_paste.py +++ b/tests/cypress/dash/v_copy_paste.py @@ -29,7 +29,6 @@ dash_table.DataTable( id="table", data=df[0:250], - navigation="page", columns=[ {"id": 0, "name": "Complaint ID"}, {"id": 1, "name": "Product"}, @@ -47,7 +46,7 @@ {"id": 13, "name": "Consumer disputed?"}, ], editable=True, - sorting=True, + sort_action='native', ), ] ) diff --git a/tests/cypress/dash/v_fe_page.py b/tests/cypress/dash/v_fe_page.py index 57fd34d8b..0f6b97cc0 100644 --- a/tests/cypress/dash/v_fe_page.py +++ b/tests/cypress/dash/v_fe_page.py @@ -36,12 +36,9 @@ dash_table.DataTable( id="table", data=data, - pagination_mode="fe", - pagination_settings={ - "current_page": 0, - "page_size": 250, - }, - navigation="page", + page_action="native", + page_current=0, + page_size=250, columns=[ {"id": 0, "name": "Complaint ID"}, {"id": 1, "name": "Product"}, @@ -58,12 +55,12 @@ {"id": 12, "name": "Timely response?"}, {"id": 13, "name": "Consumer disputed?"}, ], - n_fixed_columns=2, - n_fixed_rows=1, + fixed_columns={ 'headers': True }, + fixed_rows={ 'headers': True }, row_selectable=True, row_deletable=True, - sorting="fe", - filtering=True, + sort_action="native", + filter_action='native', editable=True, ), html.Div(id="props_container") diff --git a/tests/cypress/tests/unit/derivedViewport_test.ts b/tests/cypress/tests/unit/derivedViewport_test.ts index 2e7e18397..b93818e0a 100644 --- a/tests/cypress/tests/unit/derivedViewport_test.ts +++ b/tests/cypress/tests/unit/derivedViewport_test.ts @@ -1,6 +1,7 @@ import * as R from 'ramda'; import derivedViewportData from 'dash-table/derived/data/viewport'; +import { TableAction } from 'dash-table/components/Table/props'; describe('derived viewport', () => { const viewportData = derivedViewportData(); @@ -9,8 +10,9 @@ describe('derived viewport', () => { describe('with no pagination', () => { it('returns entire data', () => { const result = viewportData( - false, - { current_page: 0, page_size: 250 }, + TableAction.None, + 0, + 250, R.map(() => { }, R.range(0, 5)), R.range(0, 5) ); @@ -23,8 +25,9 @@ describe('derived viewport', () => { describe('with fe pagination', () => { it('returns entire data', () => { const result = viewportData( - 'fe', - { current_page: 0, page_size: 250 }, + TableAction.Native, + 0, + 250, R.map(() => { }, R.range(0, 5)), R.range(0, 5) ); @@ -37,8 +40,9 @@ describe('derived viewport', () => { describe('with be pagination', () => { it('returns entire data', () => { const result = viewportData( - 'be', - { current_page: 0, page_size: 250 }, + TableAction.Custom, + 0, + 250, R.map(() => { }, R.range(0, 5)), R.range(0, 5) ); @@ -53,8 +57,9 @@ describe('derived viewport', () => { describe('with fe pagination', () => { it('returns slice of data', () => { const result = viewportData( - 'fe', - { current_page: 0, page_size: 250 }, + TableAction.Native, + 0, + 250, R.map(idx => ({ idx }), R.range(0, 500)), R.range(0, 500) ); diff --git a/tests/cypress/tests/unit/sorting_test.ts b/tests/cypress/tests/unit/sorting_test.ts index 8b0a981d7..f2c557c30 100644 --- a/tests/cypress/tests/unit/sorting_test.ts +++ b/tests/cypress/tests/unit/sorting_test.ts @@ -1,200 +1,226 @@ -import sort, { SortDirection, SortSettings } from 'core/sorting'; -import multiUpdateSettings from 'core/sorting/multi'; -import singleUpdateSettings from 'core/sorting/single'; +import * as R from 'ramda'; + +import sort, { SortDirection, SortBy } from 'core/sorting'; +import multiUpdate from 'core/sorting/multi'; +import singleUpdate from 'core/sorting/single'; describe('sort', () => { it('sorts', () => { - const data = [[1], [3], [4], [2]]; + const data = [1, 3, 4, 2].map(v => ({ a: v })); const sorted = sort( data, - [{ column_id: 0, direction: SortDirection.Descending }] + [{ column_id: 'a', direction: SortDirection.Descending }] ); expect(sorted.length).to.equal(data.length); - expect(sorted[0][0]).to.equal(4); - expect(sorted[1][0]).to.equal(3); - expect(sorted[2][0]).to.equal(2); - expect(sorted[3][0]).to.equal(1); + expect(sorted[0].a).to.equal(4); + expect(sorted[1].a).to.equal(3); + expect(sorted[2].a).to.equal(2); + expect(sorted[3].a).to.equal(1); }); it('sorts undefined after when descending', () => { - const data = [[1], [undefined], [3], [undefined], [4], [2], [undefined]]; - + const data = [1, undefined, 3, undefined, 4, 2, undefined].map(v => ({ a: v })); const sorted = sort( data, - [{ column_id: 0, direction: SortDirection.Descending }] + [{ column_id: 'a', direction: SortDirection.Descending }] ); expect(sorted.length).to.equal(data.length); - expect(sorted[0][0]).to.equal(4); - expect(sorted[1][0]).to.equal(3); - expect(sorted[2][0]).to.equal(2); - expect(sorted[3][0]).to.equal(1); - expect(sorted[4][0]).to.equal(undefined); - expect(sorted[5][0]).to.equal(undefined); - expect(sorted[6][0]).to.equal(undefined); + expect(sorted[0].a).to.equal(4); + expect(sorted[1].a).to.equal(3); + expect(sorted[2].a).to.equal(2); + expect(sorted[3].a).to.equal(1); + expect(sorted[4].a).to.equal(undefined); + expect(sorted[5].a).to.equal(undefined); + expect(sorted[6].a).to.equal(undefined); }); it('sorts undefined after when ascending', () => { - const data = [[1], [undefined], [3], [undefined], [4], [2], [undefined]]; - + const data = [1, undefined, 3, undefined, 4, 2, undefined].map(v => ({ a: v })); const sorted = sort( data, - [{ column_id: 0, direction: SortDirection.Ascending }] + [{ column_id: 'a', direction: SortDirection.Ascending }] ); expect(sorted.length).to.equal(data.length); - expect(sorted[0][0]).to.equal(1); - expect(sorted[1][0]).to.equal(2); - expect(sorted[2][0]).to.equal(3); - expect(sorted[3][0]).to.equal(4); - expect(sorted[4][0]).to.equal(undefined); - expect(sorted[5][0]).to.equal(undefined); - expect(sorted[6][0]).to.equal(undefined); + expect(sorted[0].a).to.equal(1); + expect(sorted[1].a).to.equal(2); + expect(sorted[2].a).to.equal(3); + expect(sorted[3].a).to.equal(4); + expect(sorted[4].a).to.equal(undefined); + expect(sorted[5].a).to.equal(undefined); + expect(sorted[6].a).to.equal(undefined); }); it('sorts null after when descending', () => { - const data = [[1], [null], [3], [null], [4], [2], [null]]; - + const data = [1, null, 3, null, 4, 2, null].map(v => ({ a: v })); const sorted = sort( data, - [{ column_id: 0, direction: SortDirection.Descending }] + [{ column_id: 'a', direction: SortDirection.Descending }] ); expect(sorted.length).to.equal(data.length); - expect(sorted[0][0]).to.equal(4); - expect(sorted[1][0]).to.equal(3); - expect(sorted[2][0]).to.equal(2); - expect(sorted[3][0]).to.equal(1); - expect(sorted[4][0]).to.equal(null); - expect(sorted[5][0]).to.equal(null); - expect(sorted[6][0]).to.equal(null); + expect(sorted[0].a).to.equal(4); + expect(sorted[1].a).to.equal(3); + expect(sorted[2].a).to.equal(2); + expect(sorted[3].a).to.equal(1); + expect(sorted[4].a).to.equal(null); + expect(sorted[5].a).to.equal(null); + expect(sorted[6].a).to.equal(null); }); it('sorts null after when ascending', () => { - const data = [[1], [null], [3], [null], [4], [2], [null]]; + const data = [1, null, 3, null, 4, 2, null].map(v => ({ a: v })); + const sorted = sort( + data, + [{ column_id: 'a', direction: SortDirection.Ascending }] + ); + + expect(sorted.length).to.equal(data.length); + expect(sorted[0].a).to.equal(1); + expect(sorted[1].a).to.equal(2); + expect(sorted[2].a).to.equal(3); + expect(sorted[3].a).to.equal(4); + expect(sorted[4].a).to.equal(null); + expect(sorted[5].a).to.equal(null); + expect(sorted[6].a).to.equal(null); + }); + + it('sorts nully (undefined, null, 0, 1) after when descending', () => { + const data = [1, 0, 3, undefined, 4, 2, null].map(v => ({ a: v })); + const sorted = sort( + data, + [{ column_id: 'a', direction: SortDirection.Ascending }], + value => R.isNil(value) || value === 0 || value === 1 + ); + + expect(sorted.length).to.equal(data.length); + expect(sorted[0].a).to.equal(2); + expect(sorted[1].a).to.equal(3); + expect(sorted[2].a).to.equal(4); + }); + it('sorts nully (undefined, null, 0, 1) after when ascending', () => { + const data = [1, 0, 3, undefined, 4, 2, null].map(v => ({ a: v })); const sorted = sort( data, - [{ column_id: 0, direction: SortDirection.Ascending }] + [{ column_id: 'a', direction: SortDirection.Descending }], + value => R.isNil(value) || value === 0 || value === 1 ); expect(sorted.length).to.equal(data.length); - expect(sorted[0][0]).to.equal(1); - expect(sorted[1][0]).to.equal(2); - expect(sorted[2][0]).to.equal(3); - expect(sorted[3][0]).to.equal(4); - expect(sorted[4][0]).to.equal(null); - expect(sorted[5][0]).to.equal(null); - expect(sorted[6][0]).to.equal(null); + expect(sorted[0].a).to.equal(4); + expect(sorted[1].a).to.equal(3); + expect(sorted[2].a).to.equal(2); }); it('respects sort order - 1', () => { const data = [ - [1, 3], - [2, 3], - [0, 0], - [0, 3], - [0, 1], - [2, 1], - [1, 0], - [1, 1], - [2, 0] + { a: 1, b: 3 }, + { a: 2, b: 3 }, + { a: 0, b: 0 }, + { a: 0, b: 3 }, + { a: 0, b: 1 }, + { a: 2, b: 1 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 2, b: 0 } ]; const sorted = sort( data, [ - { column_id: 0, direction: SortDirection.Descending }, - { column_id: 1, direction: SortDirection.Descending } + { column_id: 'a', direction: SortDirection.Descending }, + { column_id: 'b', direction: SortDirection.Descending } ] ); expect(sorted.length).to.equal(data.length); - expect(sorted[0][0]).to.equal(2); - expect(sorted[0][1]).to.equal(3); - expect(sorted[1][0]).to.equal(2); - expect(sorted[1][1]).to.equal(1); - expect(sorted[2][0]).to.equal(2); - expect(sorted[2][1]).to.equal(0); + expect(sorted[0].a).to.equal(2); + expect(sorted[0].b).to.equal(3); + expect(sorted[1].a).to.equal(2); + expect(sorted[1].b).to.equal(1); + expect(sorted[2].a).to.equal(2); + expect(sorted[2].b).to.equal(0); }); it('respects sort order - 2', () => { const data = [ - [1, 3], - [2, 3], - [0, 0], - [0, 3], - [0, 1], - [2, 1], - [1, 0], - [1, 1], - [2, 0] + { a: 1, b: 3 }, + { a: 2, b: 3 }, + { a: 0, b: 0 }, + { a: 0, b: 3 }, + { a: 0, b: 1 }, + { a: 2, b: 1 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 2, b: 0 } ]; const sorted = sort( data, [ - { column_id: 1, direction: SortDirection.Descending }, - { column_id: 0, direction: SortDirection.Ascending } + { column_id: 'b', direction: SortDirection.Descending }, + { column_id: 'a', direction: SortDirection.Ascending } ] ); expect(sorted.length).to.equal(data.length); - expect(sorted[0][0]).to.equal(0); - expect(sorted[0][1]).to.equal(3); - expect(sorted[1][0]).to.equal(1); - expect(sorted[1][1]).to.equal(3); - expect(sorted[2][0]).to.equal(2); - expect(sorted[2][1]).to.equal(3); + expect(sorted[0].a).to.equal(0); + expect(sorted[0].b).to.equal(3); + expect(sorted[1].a).to.equal(1); + expect(sorted[1].b).to.equal(3); + expect(sorted[2].a).to.equal(2); + expect(sorted[2].b).to.equal(3); }); }); describe('sorting settings', () => { describe('single column sorting', () => { it('new descending', () => { - const settings = singleUpdateSettings([], { column_id: 0, direction: SortDirection.Descending }); + const settings = singleUpdate([], { column_id: 'a', direction: SortDirection.Descending }); expect(settings.length).to.equal(1); - expect(settings[0].column_id).to.equal(0); + expect(settings[0].column_id).to.equal('a'); expect(settings[0].direction).to.equal(SortDirection.Descending); }); it('update to descending', () => { - const settings = singleUpdateSettings( - [{ column_id: 0, direction: SortDirection.Ascending }], - { column_id: 0, direction: SortDirection.Descending } + const settings = singleUpdate( + [{ column_id: 'a', direction: SortDirection.Ascending }], + { column_id: 'a', direction: SortDirection.Descending } ); expect(settings.length).to.equal(1); - expect(settings[0].column_id).to.equal(0); + expect(settings[0].column_id).to.equal('a'); expect(settings[0].direction).to.equal(SortDirection.Descending); }); it('remove by setting to None', () => { - const settings = singleUpdateSettings( - [{ column_id: 0, direction: SortDirection.Ascending }], - { column_id: 0, direction: SortDirection.None } + const settings = singleUpdate( + [{ column_id: 'a', direction: SortDirection.Ascending }], + { column_id: 'a', direction: SortDirection.None } ); expect(settings.length).to.equal(0); }); it('replace with other', () => { - const settings = singleUpdateSettings( - [{ column_id: 0, direction: SortDirection.Ascending }], - { column_id: 1, direction: SortDirection.Ascending } + const settings = singleUpdate( + [{ column_id: 'a', direction: SortDirection.Ascending }], + { column_id: 'b', direction: SortDirection.Ascending } ); expect(settings.length).to.equal(1); - expect(settings[0].column_id).to.equal(1); + expect(settings[0].column_id).to.equal('b'); expect(settings[0].direction).to.equal(SortDirection.Ascending); }); it('replace with None', () => { - const settings = singleUpdateSettings( - [{ column_id: 0, direction: SortDirection.Ascending }], - { column_id: 1, direction: SortDirection.None } + const settings = singleUpdate( + [{ column_id: 'a', direction: SortDirection.Ascending }], + { column_id: 'b', direction: SortDirection.None } ); expect(settings.length).to.equal(0); @@ -203,65 +229,65 @@ describe('sorting settings', () => { describe('multi columns sorting', () => { it('new descending', () => { - const settings = multiUpdateSettings([], { column_id: 0, direction: SortDirection.Descending }); + const settings = multiUpdate([], { column_id: 'a', direction: SortDirection.Descending }); expect(settings.length).to.equal(1); - expect(settings[0].column_id).to.equal(0); + expect(settings[0].column_id).to.equal('a'); expect(settings[0].direction).to.equal(SortDirection.Descending); }); it('update to descending', () => { - const settings = multiUpdateSettings( - [{ column_id: 0, direction: SortDirection.Ascending }], - { column_id: 0, direction: SortDirection.Descending } + const settings = multiUpdate( + [{ column_id: 'a', direction: SortDirection.Ascending }], + { column_id: 'a', direction: SortDirection.Descending } ); expect(settings.length).to.equal(1); - expect(settings[0].column_id).to.equal(0); + expect(settings[0].column_id).to.equal('a'); expect(settings[0].direction).to.equal(SortDirection.Descending); }); it('remove by setting to None', () => { - const settings = multiUpdateSettings( - [{ column_id: 0, direction: SortDirection.Ascending }], - { column_id: 0, direction: SortDirection.None } + const settings = multiUpdate( + [{ column_id: 'a', direction: SortDirection.Ascending }], + { column_id: 'a', direction: SortDirection.None } ); expect(settings.length).to.equal(0); }); it('respects order', () => { - const settings = multiUpdateSettings( - [{ column_id: 0, direction: SortDirection.Ascending }], - { column_id: 1, direction: SortDirection.Ascending } + const settings = multiUpdate( + [{ column_id: 'a', direction: SortDirection.Ascending }], + { column_id: 'b', direction: SortDirection.Ascending } ); expect(settings.length).to.equal(2); - expect(settings[0].column_id).to.equal(0); - expect(settings[1].column_id).to.equal(1); + expect(settings[0].column_id).to.equal('a'); + expect(settings[1].column_id).to.equal('b'); }); it('respects order when removed and added back', () => { - let settings: SortSettings = [{ column_id: 0, direction: SortDirection.Ascending }]; + let settings: SortBy = [{ column_id: 'a', direction: SortDirection.Ascending }]; - settings = multiUpdateSettings( + settings = multiUpdate( settings, - { column_id: 1, direction: SortDirection.Ascending } + { column_id: 'b', direction: SortDirection.Ascending } ); - settings = multiUpdateSettings( + settings = multiUpdate( settings, - { column_id: 0, direction: SortDirection.None } + { column_id: 'a', direction: SortDirection.None } ); - settings = multiUpdateSettings( + settings = multiUpdate( settings, - { column_id: 0, direction: SortDirection.Ascending } + { column_id: 'a', direction: SortDirection.Ascending } ); expect(settings.length).to.equal(2); - expect(settings[0].column_id).to.equal(1); - expect(settings[1].column_id).to.equal(0); + expect(settings[0].column_id).to.equal('b'); + expect(settings[1].column_id).to.equal('a'); }); }); }); \ No newline at end of file diff --git a/tests/dash/app_dataframe_backend_paging.py b/tests/dash/app_dataframe_backend_paging.py index 6fe7dcc28..adcf83f77 100644 --- a/tests/dash/app_dataframe_backend_paging.py +++ b/tests/dash/app_dataframe_backend_paging.py @@ -40,11 +40,9 @@ def layout(): columns=[ {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], - pagination_settings={ - 'current_page': 0, - 'page_size': PAGE_SIZE - }, - pagination_mode='be' + page_current=0, + page_size=PAGE_SIZE, + page_action='custom' ), html.Hr(), @@ -58,7 +56,7 @@ def layout(): with large pages, might not be aware that this is only occuring on the current page. - Instead, we recommend implmenting sorting and filtering on the + Instead, we recommend implementing sorting and filtering on the backend as well. That is, on the entire underlying dataset. ''')), @@ -69,14 +67,12 @@ def layout(): columns=[ {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], - pagination_settings={ - 'current_page': 0, - 'page_size': PAGE_SIZE - }, - pagination_mode='be', + page_current=0, + page_size=PAGE_SIZE, + page_action='custom', - sorting='be', - sorting_type='single', + sort_action='custom', + sort_mode='single', sort_by=[] ), @@ -96,14 +92,12 @@ def layout(): columns=[ {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], - pagination_settings={ - 'current_page': 0, - 'page_size': PAGE_SIZE - }, - pagination_mode='be', + page_current=0, + page_size=PAGE_SIZE, + page_action='custom', - sorting='be', - sorting_type='multi', + sort_action='custom', + sort_mode='multi', sort_by=[] ), @@ -134,14 +128,12 @@ def layout(): columns=[ {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], - pagination_settings={ - 'current_page': 0, - 'page_size': PAGE_SIZE - }, - pagination_mode='be', + page_current=0, + page_size=PAGE_SIZE, + page_action='custom', - filtering='be', - filter='' + filter_action='custom', + filter_query='' ), section_title('Backend Paging with Filtering and Multi-Column Sorting'), @@ -151,17 +143,15 @@ def layout(): columns=[ {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], - pagination_settings={ - 'current_page': 0, - 'page_size': PAGE_SIZE - }, - pagination_mode='be', + page_current=0, + page_size=PAGE_SIZE, + page_action='custom', - filtering='be', - filter='', + filter_action='custom', + filter_query='', - sorting='be', - sorting_type='multi', + sort_action='custom', + sort_mode='multi', sort_by=[] ), @@ -181,17 +171,15 @@ def layout(): columns=[ {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], - pagination_settings={ - 'current_page': 0, - 'page_size': 20 - }, - pagination_mode='be', + page_current=0, + page_size=20, + page_action='custom', - filtering='be', - filter='', + filter_action='custom', + filter_query='', - sorting='be', - sorting_type='multi', + sort_action='custom', + sort_mode='multi', sort_by=[] ), style={'height': 750, 'overflowY': 'scroll'}, @@ -210,19 +198,20 @@ def layout(): @app.callback( Output(IDS["table"], "data"), - [Input(IDS["table"], "pagination_settings")]) -def update_graph(pagination_settings): + [Input(IDS["table"], "page_current"), Input(IDS["table"], "page_size")]) +def update_graph(page_current, page_size): return df.iloc[ - pagination_settings['current_page']*pagination_settings['page_size']: - (pagination_settings['current_page'] + 1)*pagination_settings['page_size'] + page_current * page_size: + (page_current + 1) * page_size ].to_dict('rows') @app.callback( Output(IDS["table-sorting"], "data"), - [Input(IDS["table-sorting"], "pagination_settings"), + [Input(IDS["table-sorting"], "page_current"), + Input(IDS["table-sorting"], "page_size"), Input(IDS["table-sorting"], "sort_by")]) -def update_graph(pagination_settings, sort_by): +def update_graph(page_current, page_size, sort_by): print(sort_by) if len(sort_by): dff = df.sort_values( @@ -235,17 +224,18 @@ def update_graph(pagination_settings, sort_by): dff = df return dff.iloc[ - pagination_settings['current_page']*pagination_settings['page_size']: - (pagination_settings['current_page'] + 1)*pagination_settings['page_size'] + page_current * page_size: + (page_current + 1) * page_size ].to_dict('rows') @app.callback( Output(IDS["table-multi-sorting"], "data"), - [Input(IDS["table-multi-sorting"], "pagination_settings"), + [Input(IDS["table-multi-sorting"], "page_current"), + Input(IDS["table-multi-sorting"], "page_size"), Input(IDS["table-multi-sorting"], "sort_by")]) -def update_graph(pagination_settings, sort_by): +def update_graph(page_current, page_size, sort_by): print(sort_by) if len(sort_by): dff = df.sort_values( @@ -261,59 +251,61 @@ def update_graph(pagination_settings, sort_by): dff = df return dff.iloc[ - pagination_settings['current_page']*pagination_settings['page_size']: - (pagination_settings['current_page'] + 1)*pagination_settings['page_size'] + page_current * page_size: + (page_current + 1) * page_size ].to_dict('rows') @app.callback( Output(IDS["table-filtering"], "data"), - [Input(IDS["table-filtering"], "pagination_settings"), - Input(IDS["table-filtering"], "filter")]) -def update_graph(pagination_settings, filter): - print(filter) - filtering_expressions = filter.split(' && ') + [Input(IDS["table-filtering"], "page_current"), + Input(IDS["table-filtering"], "page_size"), + Input(IDS["table-filtering"], "filter_query")]) +def update_graph(page_current, page_size, filter_query): + print(filter_query) + filtering_expressions = filter_query.split(' && ') dff = df - for filter in filtering_expressions: - if ' eq ' in filter: - col_name = filter.split(' eq ')[0] - filter_value = filter.split(' eq ')[1] + for filter_query in filtering_expressions: + if ' eq ' in filter_query: + col_name = filter_query.split(' eq ')[0] + filter_value = filter_query.split(' eq ')[1] dff = dff.loc[dff[col_name] == filter_value] - if ' > ' in filter: - col_name = filter.split(' > ')[0] - filter_value = float(filter.split(' > ')[1]) + if ' > ' in filter_query: + col_name = filter_query.split(' > ')[0] + filter_value = float(filter_query.split(' > ')[1]) dff = dff.loc[dff[col_name] > filter_value] - if ' < ' in filter: - col_name = filter.split(' < ')[0] - filter_value = float(filter.split(' < ')[1]) + if ' < ' in filter_query: + col_name = filter_query.split(' < ')[0] + filter_value = float(filter_query.split(' < ')[1]) dff = dff.loc[dff[col_name] < filter_value] return dff.iloc[ - pagination_settings['current_page']*pagination_settings['page_size']: - (pagination_settings['current_page'] + 1)*pagination_settings['page_size'] + page_current * page_size: + (page_current + 1) * page_size ].to_dict('rows') @app.callback( Output(IDS["table-sorting-filtering"], "data"), - [Input(IDS["table-sorting-filtering"], "pagination_settings"), + [Input(IDS["table-sorting-filtering"], "page_current"), + Input(IDS["table-sorting-filtering"], "page_size"), Input(IDS["table-sorting-filtering"], "sort_by"), - Input(IDS["table-sorting-filtering"], "filter")]) -def update_graph(pagination_settings, sort_by, filter): - filtering_expressions = filter.split(' && ') + Input(IDS["table-sorting-filtering"], "filter_query")]) +def update_graph(page_current, page_size, sort_by, filter_query): + filtering_expressions = filter_query.split(' && ') dff = df - for filter in filtering_expressions: - if ' eq ' in filter: - col_name = filter.split(' eq ')[0] - filter_value = filter.split(' eq ')[1] + for filter_query in filtering_expressions: + if ' eq ' in filter_query: + col_name = filter_query.split(' eq ')[0] + filter_value = filter_query.split(' eq ')[1] dff = dff.loc[dff[col_name] == filter_value] - if ' > ' in filter: - col_name = filter.split(' > ')[0] - filter_value = float(filter.split(' > ')[1]) + if ' > ' in filter_query: + col_name = filter_query.split(' > ')[0] + filter_value = float(filter_query.split(' > ')[1]) dff = dff.loc[dff[col_name] > filter_value] - if ' < ' in filter: - col_name = filter.split(' < ')[0] - filter_value = float(filter.split(' < ')[1]) + if ' < ' in filter_query: + col_name = filter_query.split(' < ')[0] + filter_value = float(filter_query.split(' < ')[1]) dff = dff.loc[dff[col_name] < filter_value] if len(sort_by): @@ -327,31 +319,32 @@ def update_graph(pagination_settings, sort_by, filter): ) return dff.iloc[ - pagination_settings['current_page']*pagination_settings['page_size']: - (pagination_settings['current_page'] + 1)*pagination_settings['page_size'] + page_current * page_size: + (page_current + 1) * page_size ].to_dict('rows') @app.callback( Output(IDS["table-paging-with-graph"], "data"), - [Input(IDS["table-paging-with-graph"], "pagination_settings"), + [Input(IDS["table-paging-with-graph"], "page_current"), + Input(IDS["table-paging-with-graph"], "page_size"), Input(IDS["table-paging-with-graph"], "sort_by"), - Input(IDS["table-paging-with-graph"], "filter")]) -def update_table(pagination_settings, sort_by, filter): - filtering_expressions = filter.split(' && ') + Input(IDS["table-paging-with-graph"], "filter_query")]) +def update_table(page_current, page_size, sort_by, filter_query): + filtering_expressions = filter_query.split(' && ') dff = df - for filter in filtering_expressions: - if ' eq ' in filter: - col_name = filter.split(' eq ')[0] - filter_value = filter.split(' eq ')[1] + for filter_query in filtering_expressions: + if ' eq ' in filter_query: + col_name = filter_query.split(' eq ')[0] + filter_value = filter_query.split(' eq ')[1] dff = dff.loc[dff[col_name] == filter_value] - if ' > ' in filter: - col_name = filter.split(' > ')[0] - filter_value = float(filter.split(' > ')[1]) + if ' > ' in filter_query: + col_name = filter_query.split(' > ')[0] + filter_value = float(filter_query.split(' > ')[1]) dff = dff.loc[dff[col_name] > filter_value] - if ' < ' in filter: - col_name = filter.split(' < ')[0] - filter_value = float(filter.split(' < ')[1]) + if ' < ' in filter_query: + col_name = filter_query.split(' < ')[0] + filter_value = float(filter_query.split(' < ')[1]) dff = dff.loc[dff[col_name] < filter_value] if len(sort_by): @@ -365,8 +358,8 @@ def update_table(pagination_settings, sort_by, filter): ) return dff.iloc[ - pagination_settings['current_page']*pagination_settings['page_size']: - (pagination_settings['current_page'] + 1)*pagination_settings['page_size'] + page_current * page_size: + (page_current + 1) * page_size ].to_dict('rows') diff --git a/tests/dash/app_dataframe_updating_graph_fe.py b/tests/dash/app_dataframe_updating_graph_fe.py index beb8de26c..26e4e2fad 100644 --- a/tests/dash/app_dataframe_updating_graph_fe.py +++ b/tests/dash/app_dataframe_updating_graph_fe.py @@ -24,13 +24,13 @@ def layout(): ], data=df.to_dict("rows"), editable=True, - filtering=True, - sorting=True, - sorting_type="multi", + filter_action='native', + sort_action='native', + sort_mode="multi", row_selectable="multi", row_deletable=True, selected_rows=[], - n_fixed_rows=1, + fixed_rows={ 'headers': True }, ), style={"height": 300, "overflowY": "scroll"}, ), @@ -43,8 +43,8 @@ def layout(): `Table` includes several features for modifying and transforming the view of the data. These include: - - Sorting by column (`sorting=True`) - - Filtering by column (`filtering=True`) + - Sorting by column (`sort_action='native'`) + - Filtering by column (`filter_action='native'`) - Editing the cells (`editable=True`) - Deleting rows (`row_deletable=True`) - Deleting columns (`columns[i].deletable=True`) diff --git a/tests/dash/app_editor_with_configurable_options.py b/tests/dash/app_editor_with_configurable_options.py deleted file mode 100644 index a3e0faede..000000000 --- a/tests/dash/app_editor_with_configurable_options.py +++ /dev/null @@ -1,152 +0,0 @@ -import dash -import dash_core_components as dcc -import dash_html_components as html -from dash.dependencies import Input, Output -import pandas as pd -import pprint - -import dash_table -from index import app - -sanitized_name = __name__.replace(".", "-") - -df = pd.read_csv("./datasets/gapminder.csv") -df_small = pd.read_csv("./datasets/gapminder-small.csv") - - -CONTROLS = [ - html.Label("Dataset"), - dcc.RadioItems( - id="dataset", - options=[{"label": i, "value": i} for i in ["1700 Rows", "13 Rows"]], - value="13 Rows", - ), - html.Label("Editable"), - dcc.RadioItems( - id="editable", - options=[{"label": str(i), "value": i} for i in [True, False]], - value=True, - ), - html.Label("Sorting"), - dcc.RadioItems( - id="sorting", - options=[{"label": str(i), "value": i} for i in ["fe", True, False]], - value=True, - ), - html.Label("Sorting Type"), - dcc.RadioItems( - id="sorting_type", - options=[{"label": str(i), "value": i} for i in ["single", "multi"]], - value="single", - ), - html.Label("Sorting Treat Empty String As None"), - dcc.RadioItems( - id="sorting_treat_empty_string_as_none", - options=[{"label": str(i), "value": i} for i in [True, False]], - value=True, - ), - html.Label("Row Selectable"), - dcc.RadioItems( - id="row_selectable", - options=[{"label": str(i), "value": i} for i in ["single", "multi", False]], - value="single", - ), - html.Label("Pagination Mode"), - dcc.RadioItems( - id="pagination_mode", - options=[{"label": str(i), "value": i} for i in ["fe", True, False]], - value="fe", - ), - html.Label("Number of Fixed Rows"), - html.Div(dcc.Input(id="n_fixed_rows", type="number", value="0")), - html.Label("Number of Fixed Columns"), - html.Div(dcc.Input(id="n_fixed_columns", type="number", value="0")), -] - - -def layout(): - return html.Div( - className="row", - children=[ - html.Div(className="four columns", children=html.Div(id="table-container")), - html.Div( - className="four columns", - children=html.Div( - [ - html.Div( - CONTROLS, - style={"maxHeight": "100vh", "overflowY": "scroll"}, - ) - ] - ), - ), - html.Div( - className="four columns", - children=html.Div( - id="output", style={"maxHeight": "100vh", "overflowY": "scroll"} - ), - ), - ], - ) - - -@app.callback( - Output("table-container", "children"), - [ - Input(prop, "value") - for prop in [ - "editable", # 0 - "sorting", # 1 - "sorting_type", # 2 - "sorting_treat_empty_string_as_none", # 3 - "row_selectable", # 4 - "pagination_mode", # 5 - "n_fixed_rows", # 6 - "n_fixed_columns", # 7 - "dataset", - ] - ], -) -def update_table(*args): - if args[8] == "1700 Rows": - rows = df.to_dict("rows") - else: - rows = df_small.to_dict("rows") - return dash_table.DataTable( - id=sanitized_name, - columns=[{"name": i, "id": i} for i in df.columns], - data=rows, - editable=args[0], - sorting=args[1], - sorting_type=args[2], - sorting_treat_empty_string_as_none=args[3], - row_selectable=args[4], - pagination_mode=args[5], - n_fixed_rows=args[6], - n_fixed_columns=args[7], - ) - - -AVAILABLE_PROPERTIES = dash_table.DataTable(id="req").available_properties - - -@app.callback( - Output("output", "children"), - [Input(sanitized_name, prop) for prop in AVAILABLE_PROPERTIES], -) -def display_propeties(*args): - return html.Div( - [ - html.Details( - open=True, - children=[ - html.Summary(html.Code(prop)), - html.Pre( - str(pprint.pformat(args[i])), - style={"maxHeight": 200, "overflowY": "scroll"}, - ), - ], - ) - for (i, prop) in enumerate(AVAILABLE_PROPERTIES) - ] - ) diff --git a/tests/visual/percy-storybook/Border.defaults.percy.tsx b/tests/visual/percy-storybook/Border.defaults.percy.tsx index 55fdf3c4f..7ce3688ab 100644 --- a/tests/visual/percy-storybook/Border.defaults.percy.tsx +++ b/tests/visual/percy-storybook/Border.defaults.percy.tsx @@ -72,16 +72,16 @@ storiesOf('DashTable/Border (available space not filled)', module) />)) .add('with frozen rows and no frozen columns', () => ()) .add('with no frozen rows and frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); storiesOf('DashTable/Border (available space filled)', module) @@ -90,16 +90,16 @@ storiesOf('DashTable/Border (available space filled)', module) />)) .add('with frozen rows and no frozen columns', () => ()) .add('with no frozen rows and frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); let props3 = Object.assign({}, BORDER_PROPS_DEFAULTS, { @@ -116,16 +116,16 @@ storiesOf('DashTable/ListView style, Border (available space not filled)', modul />)) .add('with frozen rows and no frozen columns', () => ()) .add('with no frozen rows and frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); storiesOf('DashTable/ListView style, Border (available space filled)', module) @@ -134,14 +134,14 @@ storiesOf('DashTable/ListView style, Border (available space filled)', module) />)) .add('with frozen rows and no frozen columns', () => ()) .add('with no frozen rows and frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Border.style.percy.tsx b/tests/visual/percy-storybook/Border.style.percy.tsx index 81ebf3f26..65687918d 100644 --- a/tests/visual/percy-storybook/Border.style.percy.tsx +++ b/tests/visual/percy-storybook/Border.style.percy.tsx @@ -3,13 +3,14 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import DataTable from 'dash-table/dash/DataTable'; import { BORDER_PROPS_DEFAULTS } from './Border.defaults.percy'; +import { TableAction, SortMode } from 'dash-table/components/Table/props'; const OPS_VARIANTS: ITest[] = [ - { name: 'with ops', props: { row_deletable: true, row_selectable: 'single' } }, - { name: 'fixed columns', props: { n_fixed_columns: 2, row_deletable: true, row_selectable: 'single' } }, - { name: 'fixed rows', props: { n_fixed_rows: 1, row_deletable: true, row_selectable: 'single' } }, - { name: 'fixed columns & rows', props: { n_fixed_columns: 2, n_fixed_rows: 1, row_deletable: true, row_selectable: 'single' } }, - { name: 'fixed columns & rows inside fragments', props: { n_fixed_columns: 3, n_fixed_rows: 2, row_deletable: true, row_selectable: 'single' } } + { name: 'with ops', props: { row_deletable: true, row_selectable: SortMode.Single } }, + { name: 'fixed columns', props: { fixed_columns: { headers: true }, row_deletable: true, row_selectable: SortMode.Single } }, + { name: 'fixed rows', props: { fixed_rows: { headers: true }, row_deletable: true, row_selectable: SortMode.Single } }, + { name: 'fixed columns & rows', props: { fixed_columns: { headers: true }, fixed_rows: { headers: true }, row_deletable: true, row_selectable: SortMode.Single } }, + { name: 'fixed columns & rows inside fragments', props: { fixed_columns: { headers: true, data: 1 }, fixed_rows: { headers: true, data: 1 }, row_deletable: true, row_selectable: SortMode.Single } } ]; interface ITest { @@ -69,7 +70,7 @@ const scenarios: ITest[] = [ }, { name: 'with filter style', props: { - filtering: true, + filter_action: TableAction.Native, style_filter: { border: '1px solid hotpink' } @@ -97,7 +98,7 @@ const scenarios: ITest[] = [ }, { name: 'with header / filter / cell (data) style - filter wins on header, filter wins on cell (data)', props: { - filtering: true, + filter_action: TableAction.Native, style_cell: { border: '1px solid teal' }, @@ -111,7 +112,7 @@ const scenarios: ITest[] = [ }, { name: 'with header / data / cell (filter) style - header wins on cell (filter), data wins on cell (filter)', props: { - filtering: true, + filter_action: TableAction.Native, style_data: { border: '1px solid teal' }, @@ -125,7 +126,7 @@ const scenarios: ITest[] = [ }, { name: 'with cell (header) / filter / data style - filter wins on cell (header), data wins on filter', props: { - filtering: true, + filter_action: TableAction.Native, style_data: { border: '1px solid teal' }, @@ -139,7 +140,7 @@ const scenarios: ITest[] = [ }, { name: 'with data / cell (header, filter) style - data wins on filter', props: { - filtering: true, + filter_action: TableAction.Native, style_data: { border: '1px solid teal' }, @@ -150,7 +151,7 @@ const scenarios: ITest[] = [ }, { name: 'with header / filter / data style - data wins on filter, filter wins on header', props: { - filtering: true, + filter_action: TableAction.Native, style_data: { border: '1px solid teal' }, @@ -164,7 +165,7 @@ const scenarios: ITest[] = [ }, { name: 'style as list view', props: { - filtering: true, + filter_action: TableAction.Native, style_data: { border: '1px solid teal' }, @@ -177,7 +178,7 @@ const scenarios: ITest[] = [ style_as_list_view: true } }, { - name: 'hoizontal border between header and first row should be blue', + name: 'horizontal border between header and first row should be blue', props: { css: [{selector: 'th', rule: 'border: 1px solid pink'}], style_data: {border: '1px solid blue'} @@ -185,7 +186,7 @@ const scenarios: ITest[] = [ }, { name: 'horizontal border between header and filter should be purple', props: { - filtering: true, + filter_action: TableAction.Native, css: [{selector: 'th', rule: 'border: 1px solid pink'}], style_filter: {border: '1px solid purple'} } @@ -247,7 +248,7 @@ const ops_scenarios: ITest[] = [ border: '1px solid black' }, style_data_conditional: [{ - if: { filter: '{a} eq 85' }, + if: { filter_query: '{a} eq 85' }, backgroundColor: 'pink', border: '1px solid red' }] @@ -291,7 +292,7 @@ const ops_scenarios: ITest[] = [ }, { name: 'filter ops do not get styled on conditional column_id', props: { - filtering: true, + filter_action: TableAction.Native, style_filter: { border: '1px solid black' }, @@ -304,7 +305,7 @@ const ops_scenarios: ITest[] = [ }, { name: 'filter ops do not get styled on conditional column_type', props: { - filtering: true, + filter_action: TableAction.Native, style_filter: { border: '1px solid black' }, diff --git a/tests/visual/percy-storybook/DashTable.percy.tsx b/tests/visual/percy-storybook/DashTable.percy.tsx index 70fb9477a..64916f4cc 100644 --- a/tests/visual/percy-storybook/DashTable.percy.tsx +++ b/tests/visual/percy-storybook/DashTable.percy.tsx @@ -46,6 +46,7 @@ fixtures.forEach(fixture => { }); import dataset from './../../../datasets/gapminder.csv'; +import { TableAction } from 'dash-table/components/Table/props'; storiesOf('DashTable/Without Data', module) .add('with 1 column', () => ()) .add('with 2 fixed rows, 3 fixed columns, hidden columns and merged cells', () => { @@ -182,8 +183,8 @@ storiesOf('DashTable/Fixed Rows & Columns', module) data={dataA2J} columns={mergedColumns} merge_duplicate_headers={true} - n_fixed_columns={3} - n_fixed_rows={2} + fixed_columns={{ headers: true, data: 3 }} + fixed_rows={{ headers: true, data: 1 }} style_data_conditional={style_data_conditional} />); }); @@ -240,7 +241,7 @@ storiesOf('DashTable/Sorting', module) id='table' data={sparseData} columns={mergedColumns} - sorting={true} + sort_action={TableAction.Native} sort_by={[{ column_id: 'a', direction: 'asc' }]} style_data_conditional={style_data_conditional} />)) @@ -249,7 +250,7 @@ storiesOf('DashTable/Sorting', module) id='table' data={sparseData} columns={mergedColumns} - sorting={true} + sort_action={TableAction.Native} sort_by={[{ column_id: 'a', direction: 'desc' }]} style_data_conditional={style_data_conditional} />)) @@ -258,9 +259,9 @@ storiesOf('DashTable/Sorting', module) id='table' data={sparseData} columns={mergedColumns} - sorting={true} + sort_action={TableAction.Native} sort_by={[{ column_id: 'a', direction: 'asc' }]} - sorting_treat_empty_string_as_none={true} + sort_as_null={['']} style_data_conditional={style_data_conditional} />)) .add('"a" descending -- empty string override', () => ()) + .add(`"a" descending -- '' & 426 override`, () => ()) + .add(`"a" ascending -- '' and 426 override`, () => ()); storiesOf('DashTable/Without id', module) @@ -278,8 +299,8 @@ storiesOf('DashTable/Without id', module) setProps={setProps} data={dataA2J} columns={columnsA2J} - n_fixed_columns={2} - n_fixed_rows={1} + fixed_columns={{ headers: true }} + fixed_rows={{ headers: true }} row_deletable={true} row_selectable={true} style_data_conditional={style_data_conditional} @@ -288,8 +309,8 @@ storiesOf('DashTable/Without id', module) setProps={setProps} data={dataA2J} columns={columnsA2J} - n_fixed_columns={2} - n_fixed_rows={1} + fixed_columns={{ headers: true }} + fixed_rows={{ headers: true }} row_deletable={true} row_selectable={true} style_table={{height: 500, width: 200}} @@ -299,8 +320,8 @@ storiesOf('DashTable/Without id', module) setProps={setProps} data={dataA2J} columns={columnsA2J} - n_fixed_columns={2} - n_fixed_rows={1} + fixed_columns={{ headers: true }} + fixed_rows={{ headers: true }} row_deletable={true} row_selectable={true} style_table={{height: 500, width: 200}} @@ -314,8 +335,8 @@ storiesOf('DashTable/Without id', module) setProps={setProps} data={dataA2J} columns={columnsA2J} - n_fixed_columns={2} - n_fixed_rows={1} + fixed_columns={{ headers: true }} + fixed_rows={{ headers: true }} row_deletable={true} row_selectable={true} style_table={{height: 500, width: 400}} @@ -328,8 +349,8 @@ storiesOf('DashTable/Without id', module) setProps={setProps} data={dataA2J} columns={columnsA2J} - n_fixed_columns={2} - n_fixed_rows={1} + fixed_columns={{ headers: true }} + fixed_rows={{ headers: true }} row_deletable={true} row_selectable={true} style_table={{height: 500, width: 400}} diff --git a/tests/visual/percy-storybook/Dropdown.percy.tsx b/tests/visual/percy-storybook/Dropdown.percy.tsx index 4770e5ada..a5d7864d7 100644 --- a/tests/visual/percy-storybook/Dropdown.percy.tsx +++ b/tests/visual/percy-storybook/Dropdown.percy.tsx @@ -36,19 +36,20 @@ storiesOf('DashTable/Dropdown', module) data={data} columns={columns} editable={true} - column_static_dropdown={[{ - id: 'climate', - dropdown: R.map( - i => ({ label: i, value: i }), - ['Sunny', 'Snowy', 'Rainy'] - ) - }, { - id: 'city', - dropdown: R.map( - i => ({ label: i, value: i }), - ['NYC', 'Montreal', 'Miami'] - ) - }]} + dropdown={{ + climate: { + options: R.map( + i => ({ label: i, value: i }), + ['Sunny', 'Snowy', 'Rainy'] + ) + }, + city: { + options: R.map( + i => ({ label: i, value: i }), + ['NYC', 'Montreal', 'Miami'] + ) + } + }} />)) .add('dropdown by filtering', () => ( ({ label: i, value: i }), - ['Brooklyn', 'Queens', 'Staten Island'] - ) - }, { - condition: '{City} eq "Montreal"', - dropdown: R.map( - i => ({ label: i, value: i }), - ['Mile End', 'Plateau', 'Hochelaga'] - ) - }, { - condition: '{City} eq "Los Angeles"', - dropdown: R.map( - i => ({ label: i, value: i }), - ['Venice', 'Hollywood', 'Los Feliz'] - ) - }] + dropdown_conditional={[{ + if: { + column_id: 'Neighborhood', + filter_query: '{City} eq "NYC"' + }, + options: R.map( + i => ({ label: i, value: i }), + ['Brooklyn', 'Queens', 'Staten Island'] + ) + }, { + if: { + column_id: 'Neighborhood', + filter_query: '{City} eq "Montreal"' + }, + options: R.map( + i => ({ label: i, value: i }), + ['Mile End', 'Plateau', 'Hochelaga'] + ) + }, + { + if: { + column_id: 'Neighborhood', + filter_query: '{City} eq "Los Angeles"' + }, + options: R.map( + i => ({ label: i, value: i }), + ['Venice', 'Hollywood', 'Los Feliz'] + ) }]} />)).add('dropdown by cell (deprecated)', () => ( ({ label: i, value: i }), - ['Brooklyn', 'Queens', 'Staten Island'] - ) - }, { - options: R.map( - i => ({ label: i, value: i }), - ['Mile End', 'Plateau', 'Hochelaga'] - ) - }, { - options: R.map( - i => ({ label: i, value: i }), - ['Venice', 'Hollywood', 'Los Feliz'] - ) - }] - }} + dropdown_data={[ + { + Neighborhood: { + options: R.map( + i => ({ label: i, value: i }), + ['Brooklyn', 'Queens', 'Staten Island'] + ) + } + }, + { + Neighborhood: { + options: R.map( + i => ({ label: i, value: i }), + ['Mile End', 'Plateau', 'Hochelaga'] + ) + } + }, + { + Neighborhood: { + options: R.map( + i => ({ label: i, value: i }), + ['Venice', 'Hollywood', 'Los Feliz'] + ) + } + } + ]} />)); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Filters.percy.tsx b/tests/visual/percy-storybook/Filters.percy.tsx index c08197cf9..c551959bc 100644 --- a/tests/visual/percy-storybook/Filters.percy.tsx +++ b/tests/visual/percy-storybook/Filters.percy.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; import DataTable from 'dash-table/dash/DataTable'; +import { TableAction } from 'dash-table/components/Table/props'; const setProps = () => { }; @@ -21,7 +22,7 @@ let props = { setProps, id: 'table', data: data, - filtering: true, + filter_action: TableAction.Native, style_cell: { width: 100, min_width: 100, diff --git a/tests/visual/percy-storybook/Sizing.percy.tsx b/tests/visual/percy-storybook/Sizing.percy.tsx index 338ced890..c7dd959f6 100644 --- a/tests/visual/percy-storybook/Sizing.percy.tsx +++ b/tests/visual/percy-storybook/Sizing.percy.tsx @@ -29,7 +29,6 @@ storiesOf('DashTable/Sizing', module) />)) .add('padding', () => ()) .add('single column width by percentage', () => ( ({ name: i, id: i }), R.keysIn(data[0])) } - content_style='grow' style_table={{ width: '100%' }} @@ -102,7 +101,6 @@ storiesOf('DashTable/Style type condition', module) i => ({ name: i, id: i }), R.keysIn(data[0])) } - content_style='grow' style_table={{ width: '100%' }} @@ -123,15 +121,14 @@ storiesOf('DashTable/Style type condition', module) i => ({ name: i, id: i }), R.keysIn(data[0])) } - content_style='grow' style_table={{ width: '100%' }} style_data_conditional={[{ - if: { column_id: 'Region', filter: '{Region} eq Montreal' }, + if: { column_id: 'Region', filter_query: '{Region} eq Montreal' }, background_color: 'yellow' }, { - if: { column_id: 'Humidity', filter: '{Humidity} eq 20' }, + if: { column_id: 'Humidity', filter_query: '{Humidity} eq 20' }, background_color: 'yellow' }]} />)) diff --git a/tests/visual/percy-storybook/TriadValidation.percy.tsx b/tests/visual/percy-storybook/TriadValidation.percy.tsx index 4fbfb3e07..d51b5e08d 100644 --- a/tests/visual/percy-storybook/TriadValidation.percy.tsx +++ b/tests/visual/percy-storybook/TriadValidation.percy.tsx @@ -2,22 +2,21 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import DataTable from 'dash-table/dash/DataTable'; +import { TableAction } from 'dash-table/components/Table/props'; -const filteringValues = ['fe', 'be']; -const sortingValues = ['fe', 'be']; -const paginationValues = ['fe', 'be']; +const actions = [TableAction.Native, TableAction.Custom]; const setProps = () => { }; let stories = storiesOf('DashTable/Props Validation', module); -filteringValues.forEach(filter => { - sortingValues.forEach(sort => { - paginationValues.forEach(page => { +actions.forEach(filter => { + actions.forEach(sort => { + actions.forEach(page => { stories = stories.add(`filter=${filter}, sorting=${sort}, pagination=${page}`, () => ()); }); diff --git a/tests/visual/percy-storybook/Virtualization.percy.tsx b/tests/visual/percy-storybook/Virtualization.percy.tsx index d197da826..e0d4ea70d 100644 --- a/tests/visual/percy-storybook/Virtualization.percy.tsx +++ b/tests/visual/percy-storybook/Virtualization.percy.tsx @@ -6,6 +6,7 @@ import { storiesOf } from '@storybook/react'; import dataset from './../../../datasets/16zpallagi-25cols-100klines.csv'; import DataTable from 'dash-table/dash/DataTable'; +import { TableAction } from 'dash-table/components/Table/props'; const setProps = () => { }; @@ -18,10 +19,10 @@ storiesOf('DashTable/Virtualization', module) id='table' data={data} columns={columns} - pagination_mode={false} + page_action={TableAction.None} virtualization={true} editable={true} - n_fixed_rows={1} + fixed_rows={{ headers: true }} style_table={{ height: 800, max_height: 800, diff --git a/tests/visual/percy-storybook/Width.all.percy.tsx b/tests/visual/percy-storybook/Width.all.percy.tsx index bc90d6c6b..f9a524108 100644 --- a/tests/visual/percy-storybook/Width.all.percy.tsx +++ b/tests/visual/percy-storybook/Width.all.percy.tsx @@ -23,7 +23,6 @@ const baseProps = { setProps, id: 'table', data, - content_style: 'fit', style_data_conditional: [ { width: '20px', min_width: '20px', max_width: '20px' } ] @@ -39,14 +38,14 @@ storiesOf('DashTable/Width width, minWidth, maxWidth', module) />)) .add('with frozen rows', () => ()) .add('with frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Width.defaults.percy.tsx b/tests/visual/percy-storybook/Width.defaults.percy.tsx index fe050ddcd..dea879c54 100644 --- a/tests/visual/percy-storybook/Width.defaults.percy.tsx +++ b/tests/visual/percy-storybook/Width.defaults.percy.tsx @@ -22,7 +22,6 @@ const data = (() => { const baseProps = { setProps, id: 'table', - content_style: 'fit', data }; @@ -36,14 +35,14 @@ storiesOf('DashTable/Width defaults', module) />)) .add('with frozen rows', () => ()) .add('with frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Width.empty.percy.tsx b/tests/visual/percy-storybook/Width.empty.percy.tsx index 2b59a5fc5..f9ef27bd3 100644 --- a/tests/visual/percy-storybook/Width.empty.percy.tsx +++ b/tests/visual/percy-storybook/Width.empty.percy.tsx @@ -4,7 +4,7 @@ import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; import DataTable from 'dash-table/dash/DataTable'; -import { FilteringType } from 'dash-table/components/Table/props'; +import { TableAction } from 'dash-table/components/Table/props'; const setProps = () => { }; @@ -24,10 +24,8 @@ const data = (() => { const baseProps = { setProps, id: 'table', - content_style: 'fit', data, - filtering: 'fe', - filtering_type: FilteringType.Basic, + filter_action: TableAction.Native, style_cell: { width: 100, max_width: 100, min_width: 100 } }; @@ -41,21 +39,21 @@ storiesOf('DashTable/Empty', module) />)) .add('with column filters -- invalid query', () => ()) .add('with column filters -- single query', () => ()) .add('with column filters -- multi query', () => ()) .add('with column filters -- multi query, no data', () => ()); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Width.max.percy.tsx b/tests/visual/percy-storybook/Width.max.percy.tsx index de4157f70..bf75fdf34 100644 --- a/tests/visual/percy-storybook/Width.max.percy.tsx +++ b/tests/visual/percy-storybook/Width.max.percy.tsx @@ -22,7 +22,6 @@ const data = (() => { const baseProps = { setProps, id: 'table', - content_style: 'fit', data, style_data_conditional: [{ max_width: 10 }] }; @@ -37,14 +36,14 @@ storiesOf('DashTable/Width maxWidth only', module) />)) .add('with frozen rows', () => ()) .add('with frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Width.min.percy.tsx b/tests/visual/percy-storybook/Width.min.percy.tsx index ad9525100..ba229e20d 100644 --- a/tests/visual/percy-storybook/Width.min.percy.tsx +++ b/tests/visual/percy-storybook/Width.min.percy.tsx @@ -22,7 +22,6 @@ const data = (() => { const baseProps = { setProps, id: 'table', - content_style: 'fit', data }; @@ -37,14 +36,14 @@ storiesOf('DashTable/Width minWidth only', module) />)) .add('with frozen rows', () => ()) .add('with frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Width.percentages.percy.tsx b/tests/visual/percy-storybook/Width.percentages.percy.tsx index 0bdea6f0f..609a4989c 100644 --- a/tests/visual/percy-storybook/Width.percentages.percy.tsx +++ b/tests/visual/percy-storybook/Width.percentages.percy.tsx @@ -21,12 +21,10 @@ const data = (() => { const baseProps = { setProps, id: 'table', - content_style: 'grow', data }; const props = Object.assign({}, baseProps, { - content_style: 'grow', columns: columns.map((id => ({ id: id, name: id.toUpperCase() }))), style_cell: { width: '33%' @@ -48,14 +46,14 @@ storiesOf('DashTable/Width percentages', module) />)) .add('with frozen rows', () => ()) .add('with frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Width.width.percy.tsx b/tests/visual/percy-storybook/Width.width.percy.tsx index 2957f66cd..b67cfc643 100644 --- a/tests/visual/percy-storybook/Width.width.percy.tsx +++ b/tests/visual/percy-storybook/Width.width.percy.tsx @@ -22,7 +22,6 @@ const data = (() => { const baseProps = { setProps, id: 'table', - content_style: 'fit', data }; @@ -36,14 +35,14 @@ storiesOf('DashTable/Width width only', module) />)) .add('with frozen rows', () => ()) .add('with frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); \ No newline at end of file diff --git a/tests/visual/percy-storybook/fixtures.ts b/tests/visual/percy-storybook/fixtures.ts index 5072a1366..18b80be77 100644 --- a/tests/visual/percy-storybook/fixtures.ts +++ b/tests/visual/percy-storybook/fixtures.ts @@ -10,26 +10,24 @@ export default [ name: 'Column 1', id: 'column-1', type: ColumnType.Text, - presentation: Presentation.Dropdown, + presentation: Presentation.Dropdown + } + ], + dropdown: { + 'column-1': { options: [ - { - label: 'Montréal', - value: 'mtl' - }, - { - label: 'San Francisco', - value: 'sf' - } + { label: 'Montréal', value: 'mtl' }, + { label: 'San Francisco', value: 'sf' } ] } - ], + }, data: [ {'column-1': 'mtl'}, {'column-1': 'sf'}, {'column-1': 'mtl'}, {'column-1': 'boston'} ], - n_fixed_rows: 1, + fixed_rows: { headers: true }, editable: true, css: [{ selector: '.dash-spreadsheet.dash-freeze-top', @@ -136,19 +134,17 @@ export default [ name: 'Column 1', id: 'column-1', type: ColumnType.Text, - presentation: Presentation.Dropdown, + presentation: Presentation.Dropdown + } + ], + dropdown: { + 'column-1': { options: [ - { - label: 'Montréal', - value: 'mtl' - }, - { - label: 'San Francisco', - value: 'sf' - } + { label: 'Montréal', value: 'mtl' }, + { label: 'San Francisco', value: 'sf' } ] } - ], + }, data: [ {'column-1': 'mtl'}, {'column-1': 'sf'}, @@ -163,7 +159,6 @@ export default [ { name: 'dropdown with column widths', props: { - content_style: 'fit', style_data_conditional: [ { if: { column_id: 'column-2' }, width: 400 }, { if: { column_id: 'column-3' }, width: 80 } @@ -173,51 +168,41 @@ export default [ name: 'Column 1', id: 'column-1', type: ColumnType.Text, - presentation: Presentation.Dropdown, - options: [ - { - label: 'Montréal', - value: 'mtl' - }, - { - label: 'San Francisco', - value: 'sf' - } - ] + presentation: Presentation.Dropdown }, { name: 'Column 2', id: 'column-2', type: ColumnType.Text, - presentation: Presentation.Dropdown, - options: [ - { - label: 'Montréal', - value: 'mtl' - }, - { - label: 'San Francisco', - value: 'sf' - } - ] + presentation: Presentation.Dropdown }, { name: 'Column 3', id: 'column-3', type: ColumnType.Text, - presentation: Presentation.Dropdown, + presentation: Presentation.Dropdown + } + ], + dropdown: { + 'column-1': { + options: [ + { label: 'Montréal', value: 'mtl' }, + { label: 'San Francisco', value: 'sf' } + ] + }, + 'column-2': { + options: [ + { label: 'Montréal', value: 'mtl' }, + { label: 'San Francisco', value: 'sf' } + ] + }, + 'column-3': { options: [ - { - label: 'Montréal', - value: 'mtl' - }, - { - label: 'San Francisco', - value: 'sf' - } + { label: 'Montréal', value: 'mtl' }, + { label: 'San Francisco', value: 'sf' } ] } - ], + }, data: [ {'column-1': 'mtl', 'column-2': 'mtl', 'column-3': 'mtl'}, {'column-1': 'mtl', 'column-2': 'mtl', 'column-3': 'mtl'}, @@ -256,19 +241,17 @@ export default [ name: ['Region', ''], id: 'region', type: ColumnType.Text, - presentation: Presentation.Dropdown, + presentation: Presentation.Dropdown + } + ], + dropdown: { + region: { options: [ - { - label: 'Hawaii', - value: 'hawaii' - }, - { - label: 'Costa Rica', - value: 'costa-rica' - } + { label: 'Hawaii', value: 'hawaii' }, + { label: 'Costa Rica', value: 'costa-rica' } ] } - ], + }, merge_duplicate_headers: true, data: [ { @@ -297,7 +280,6 @@ export default [ name: 'mixed percentage and pixel column widths', props: { id: 'table', - content_style: 'fit', style_data_conditional: [ { if: { column_id: 'column-2' }, width: 400 }, { if: { column_id: 'column-3' }, width: '30%' } @@ -534,7 +516,7 @@ export default [ name: ['City', 'NYC'], id: 'city-nyc', deletable: true, - editable_name: true + renamable: true }, { name: ['City', 'SF'], @@ -544,7 +526,7 @@ export default [ { name: ['Weather', 'Rainy'], id: 'weather-rainy', - editable_name: true + renamable: true }, { name: ['Weather', 'Sunny'], @@ -554,7 +536,7 @@ export default [ name: ['Village', 'NYC'], id: 'village-nyc', deletable: true, - editable_name: 0 + renamable: [true, false] }, { name: ['Village', 'SF'], @@ -564,7 +546,7 @@ export default [ { name: ['Climate', 'Rainy'], id: 'climate-rainy', - editable_name: 1 + renamable: [false, true] }, { name: ['Climate', 'Sunny'], diff --git a/tslint.json b/tslint.json index 84bd4fd2e..74acc28ba 100644 --- a/tslint.json +++ b/tslint.json @@ -5,9 +5,12 @@ ], "linterOptions": { "exclude": [ - "node_modules/**", + ".config/**", "cypress/**", - "@Types/**" + "inst/**", + "node_modules/**", + "@Types/**", + "venv/**" ] }, "jsRules": {