From 50daad3a71291348a18af2c311cfe57b447a5e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 11 Jul 2019 16:24:21 -0400 Subject: [PATCH 01/12] Initial working implementation of clearable --- src/dash-table/components/FilterFactory.tsx | 27 +------ src/dash-table/components/HeaderFactory.tsx | 14 +++- src/dash-table/components/Table/Table.less | 44 ++++++------ src/dash-table/components/Table/props.ts | 23 ++++++ src/dash-table/dash/DataTable.js | 19 +++++ src/dash-table/derived/header/content.tsx | 78 ++++++++++++++++----- src/dash-table/derived/table/index.tsx | 13 ++-- src/dash-table/utils/actions.js | 24 +++++-- 8 files changed, 166 insertions(+), 76 deletions(-) diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 173ceec6e..f72e0e445 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -7,38 +7,15 @@ import memoizerCache from 'core/cache/memoizer'; import { memoizeOne } from 'core/memoizer'; import ColumnFilter from 'dash-table/components/Filter/Column'; -import { ColumnId, IVisibleColumn, VisibleColumns, RowSelection, TableAction } from 'dash-table/components/Table/props'; +import { ColumnId, IVisibleColumn, TableAction, IFilterFactoryProps, SetFilter } 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'; -import { BasicFilters, Cells, Style } from 'dash-table/derived/style/props'; import { SingleColumnSyntaxTree, getMultiColumnQueryString } from 'dash-table/syntax-tree'; import { IEdgesMatrices } from 'dash-table/derived/edges/type'; import { updateMap } from 'dash-table/derived/filter/map'; -type SetFilter = ( - filter_query: string, - rawFilter: string, - map: Map -) => void; - -export interface IFilterOptions { - columns: VisibleColumns; - filter_query: string; - filter_action: TableAction; - id: string; - map: Map; - rawFilterQuery: string; - row_deletable: boolean; - row_selectable: RowSelection; - setFilter: SetFilter; - style_cell: Style; - style_cell_conditional: Cells; - style_filter: Style; - style_filter_conditional: BasicFilters; -} - const NO_FILTERS: JSX.Element[][] = []; export default class FilterFactory { @@ -51,7 +28,7 @@ export default class FilterFactory { return this.propsFn(); } - constructor(private readonly propsFn: () => IFilterOptions) { + constructor(private readonly propsFn: () => IFilterFactoryProps) { } diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index b11df1182..f955f2b89 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -4,7 +4,7 @@ import React, { CSSProperties } from 'react'; import { arrayMap2 } from 'core/math/arrayZipMap'; import { matrixMap2, matrixMap3 } from 'core/math/matrixZipMap'; -import { ControlledTableProps, VisibleColumns } from 'dash-table/components/Table/props'; +import { ControlledTableProps, VisibleColumns, SetFilter } from 'dash-table/components/Table/props'; import derivedHeaderContent from 'dash-table/derived/header/content'; import getHeaderRows from 'dash-table/derived/header/headerRows'; import getIndices from 'dash-table/derived/header/indices'; @@ -16,6 +16,12 @@ import derivedHeaderStyles, { derivedHeaderOpStyles } from 'dash-table/derived/h import { IEdgesMatrices } from 'dash-table/derived/edges/type'; import { memoizeOne } from 'core/memoizer'; +import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; + +type Props = ControlledTableProps & { + map: Map; + setFilter: SetFilter; +}; export default class HeaderFactory { private readonly headerContent = derivedHeaderContent(); @@ -29,7 +35,7 @@ export default class HeaderFactory { return this.propsFn(); } - constructor(private readonly propsFn: () => ControlledTableProps) { + constructor(private readonly propsFn: () => Props) { } @@ -39,10 +45,12 @@ export default class HeaderFactory { const { columns, data, + map, merge_duplicate_headers, page_action, row_deletable, row_selectable, + setFilter, setProps, sort_action, sort_by, @@ -92,10 +100,12 @@ export default class HeaderFactory { columns, data, labelsAndIndices, + map, sort_action, sort_mode, sort_by, page_action, + setFilter, setProps ); diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index f0cbe2c7c..21f66a928 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -321,8 +321,9 @@ } th { - .column-header--edit, + .column-header--clear, .column-header--delete, + .column-header--edit, .sort { .not-selectable(); cursor: pointer; @@ -472,29 +473,24 @@ color: var(--accent); } - .dash-spreadsheet-inner .column-header--edit { - float: left; - opacity: 0.1; - padding-left: 2px; - padding-right: 2px; - cursor: pointer; - } - - .dash-spreadsheet-inner th:hover .column-header--edit { - color: var(--accent); - opacity: 1; - } - - .dash-spreadsheet-inner .column-header--delete { - float: left; - opacity: 0.1; - padding-left: 2px; - padding-right: 2px; - cursor: pointer; - } + .dash-spreadsheet-inner { + .column-header--clear, + .column-header--delete, + .column-header--edit { + float: left; + opacity: 0.1; + padding-left: 2px; + padding-right: 2px; + cursor: pointer; + } - .dash-spreadsheet-inner th:hover .column-header--delete { - color: var(--accent); - opacity: 1; + th:hover { + .column-header--clear, + .column-header--delete, + .column-header--edit { + color: var(--accent); + opacity: 1; + } + } } } diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index c0e941bcd..e57437d69 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -154,6 +154,7 @@ export interface IDatetimeColumn extends ITypeColumn { } export interface IBaseVisibleColumn { + clearable?: boolean | boolean[]; deletable?: boolean | boolean[]; editable: boolean; renamable?: boolean | boolean[]; @@ -391,6 +392,28 @@ export type ControlledTableProps = SanitizedProps & IState & { virtualized: IVirtualizedDerivedData; }; +export type SetFilter = ( + filter_query: string, + rawFilter: string, + map: Map +) => void; + +export interface IFilterFactoryProps { + columns: VisibleColumns; + filter_query: string; + filter_action: TableAction; + id: string; + map: Map; + rawFilterQuery: string; + row_deletable: boolean; + row_selectable: RowSelection; + setFilter: SetFilter; + style_cell: Style; + style_cell_conditional: Cells; + style_filter: Style; + style_filter_conditional: BasicFilters; +} + export interface ICellFactoryProps { active_cell: ICellCoordinates; columns: VisibleColumns; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index ce7177cc4..ccc2360bf 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -116,6 +116,25 @@ export const propTypes = { */ columns: PropTypes.arrayOf(PropTypes.exact({ + /** + * If True, the user can clear the column by clicking on a little `Ø` + * button on the column. + * If there are merged, multi-header columns then you can choose + * which column header row to display the "Ø" in by + * supplying an array of booleans. + * For example, `[true, false]` will display the "Ø" on the first row, + * but not the second row. + * If the "Ø" appears on a merged column, then clicking on that button + * will clear *all* of the merged columns associated with it. + * + * Unlike `column.deletable`, this action does not remove the column(s) + * from the table. It only removed the associated entries from `data`. + */ + clearable: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.arrayOf(PropTypes.bool) + ]), + /** * If True, the user can delete the column by clicking on a little `x` * button on the column. diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 11fce31b5..38fdac3d2 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -13,15 +13,49 @@ import { VisibleColumns, IVisibleColumn, SetProps, - TableAction + TableAction, + SetFilter } from 'dash-table/components/Table/props'; import * as actions from 'dash-table/utils/actions'; +import { SingleColumnSyntaxTree, getMultiColumnQueryString } from 'dash-table/syntax-tree'; +import { updateMap } from '../filter/map'; + +const doAction = ( + action: ( + column: IVisibleColumn, + columns: VisibleColumns, + columnRowIndex: any, + data: Data + ) => any, + column: IVisibleColumn, + columns: VisibleColumns, + columnRowIndex: any, + setFilter: SetFilter, + setProps: SetProps, + map: Map, + data: Data +) => () => { + setProps(action(column, columns, columnRowIndex, data)); + + const affectedColumnIds: string[] = actions.getAffectedColumns(column, columns, columnRowIndex); + + R.forEach(id => { + const affectedColumn = columns.find(c => c.id === id); + if (affectedColumn) { + map = updateMap(map, affectedColumn, ''); + } + }, affectedColumnIds); -function deleteColumn(column: IVisibleColumn, columns: VisibleColumns, columnRowIndex: any, setProps: SetProps, data: Data) { - return () => { - setProps(actions.deleteColumn(column, columns, columnRowIndex, data)); - }; -} + const asts = Array.from(map.values()); + const globalFilter = getMultiColumnQueryString(asts); + + const rawGlobalFilter = R.map( + ast => ast.query || '', + R.filter(ast => Boolean(ast), asts) + ).join(' && '); + + setFilter(globalFilter, rawGlobalFilter, map); +}; function doSort(columnId: ColumnId, sortBy: SortBy, mode: SortMode, setProps: SetProps) { return () => { @@ -79,14 +113,22 @@ function getSortingIcon(columnId: ColumnId, sortBy: SortBy) { } } +function getColumnFlag(i: number, flag?: boolean | boolean[]): boolean { + return typeof flag === 'boolean' ? + flag : + !!flag && flag[i]; +} + function getter( columns: VisibleColumns, data: Data, labelsAndIndices: R.KeyValuePair[], + map: Map, sort_action: TableAction, mode: SortMode, sortBy: SortBy, paginationMode: TableAction, + setFilter: SetFilter, setProps: SetProps ): JSX.Element[][] { return R.addIndex, JSX.Element[]>(R.map)( @@ -97,15 +139,9 @@ function getter( columnIndex => { const column = columns[columnIndex]; - const renamable: boolean = typeof column.renamable === 'boolean' ? - column.renamable : - !!column.renamable && column.renamable[headerRowIndex]; - - const deletable = paginationMode !== TableAction.Custom && ( - typeof column.deletable === 'boolean' ? - column.deletable : - !!column.deletable && column.deletable[headerRowIndex] - ); + const renamable = getColumnFlag(headerRowIndex, column.renamable); + const clearable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, column.clearable); + const deletable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, column.deletable); return (
{sort_action !== TableAction.None && isLastRow ? @@ -128,10 +164,20 @@ function getter( '' } + {clearable ? + ( + {'Ø'} + ) : + '' + } + {deletable ? ( {'×'} ) : diff --git a/src/dash-table/derived/table/index.tsx b/src/dash-table/derived/table/index.tsx index f7e0e87d0..7d3ed773e 100644 --- a/src/dash-table/derived/table/index.tsx +++ b/src/dash-table/derived/table/index.tsx @@ -22,7 +22,7 @@ const handleSetFilter = ( setState({ workFilter: { map, value: filter_query }, rawFilterQuery }); }; -function filterPropsFn(propsFn: () => ControlledTableProps, setFilter: any) { +function propsAndMapFn(propsFn: () => ControlledTableProps, setFilter: any) { const props = propsFn(); return R.merge(props, { map: props.workFilter.map, setFilter }); @@ -35,12 +35,15 @@ export default (propsFn: () => ControlledTableProps) => { ) => handleSetFilter.bind(undefined, setProps, setState)); const cellFactory = new CellFactory(propsFn); - const filterFactory = new FilterFactory(() => { + + const augmentedPropsFn = () => { const props = propsFn(); - return filterPropsFn(propsFn, setFilter(props.setProps, props.setState)); - }); - const headerFactory = new HeaderFactory(propsFn); + return propsAndMapFn(propsFn, setFilter(props.setProps, props.setState)); + }; + + const filterFactory = new FilterFactory(augmentedPropsFn); + const headerFactory = new HeaderFactory(augmentedPropsFn); const edgeFactory = new EdgeFactory(propsFn); const merge = memoizeOne((data: JSX.Element[][], filters: JSX.Element[][], headers: JSX.Element[][]) => { diff --git a/src/dash-table/utils/actions.js b/src/dash-table/utils/actions.js index 3e1241d50..7a4194bc7 100644 --- a/src/dash-table/utils/actions.js +++ b/src/dash-table/utils/actions.js @@ -22,22 +22,38 @@ function getGroupedColumnIndices(column, columns, headerRowIndex) { return { groupIndexFirst: columnIndex, groupIndexLast: lastColumnIndex }; } -export function deleteColumn(column, columns, headerRowIndex, data) { - const {groupIndexFirst, groupIndexLast} = getGroupedColumnIndices( +export function getAffectedColumns(column, columns, headerRowIndex) { + const { groupIndexFirst, groupIndexLast } = getGroupedColumnIndices( column, columns, headerRowIndex ); - const rejectedColumnIds = R.slice( + + return R.slice( groupIndexFirst, groupIndexLast + 1, R.pluck('id', columns) ); +} + +export function clearColumn(column, columns, headerRowIndex, data) { + const rejectedColumnIds = getAffectedColumns(column, columns, headerRowIndex); + + return { + data: R.map(R.omit(rejectedColumnIds), data) + }; +} + +export function deleteColumn(column, columns, headerRowIndex, data) { + const {groupIndexFirst, groupIndexLast} = getGroupedColumnIndices( + column, columns, headerRowIndex + ); + return { columns: R.remove( groupIndexFirst, 1 + groupIndexLast - groupIndexFirst, columns ), - data: R.map(R.omit(rejectedColumnIds), data), + ...clearColumn(column, columns, headerRowIndex, data), // NOTE - We're just clearing these so that there aren't any // inconsistencies. In an ideal world, we would probably only // update them if they contained one of the columns that we're From 933506c1d29a3b0e612fb42731353ebbe24d6524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Jul 2019 12:32:28 -0400 Subject: [PATCH 02/12] refactor map --- src/dash-table/components/FilterFactory.tsx | 16 ++----- src/dash-table/derived/filter/map.ts | 53 ++++++++++++++++----- src/dash-table/derived/header/content.tsx | 21 +++----- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index f72e0e445..dd6235769 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -11,10 +11,10 @@ import { ColumnId, IVisibleColumn, TableAction, IFilterFactoryProps, SetFilter } import derivedFilterStyles, { derivedFilterOpStyles } from 'dash-table/derived/filter/wrapperStyles'; import derivedHeaderOperations from 'dash-table/derived/header/operations'; import { derivedRelevantFilterStyles } from 'dash-table/derived/style'; -import { SingleColumnSyntaxTree, getMultiColumnQueryString } from 'dash-table/syntax-tree'; +import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; import { IEdgesMatrices } from 'dash-table/derived/edges/type'; -import { updateMap } from 'dash-table/derived/filter/map'; +import { updateColumnFilter } from 'dash-table/derived/filter/map'; const NO_FILTERS: JSX.Element[][] = []; @@ -37,17 +37,7 @@ export default class FilterFactory { const value = ev.target.value.trim(); - map = updateMap(map, column, value); - - const asts = Array.from(map.values()); - const globalFilter = getMultiColumnQueryString(asts); - - const rawGlobalFilter = R.map( - ast => ast.query || '', - R.filter(ast => Boolean(ast), asts) - ).join(' && '); - - setFilter(globalFilter, rawGlobalFilter, map); + updateColumnFilter(map, column, value, setFilter); } private filter = memoizerCache<[ColumnId, number]>()(( diff --git a/src/dash-table/derived/filter/map.ts b/src/dash-table/derived/filter/map.ts index 88787701b..aaf6d8640 100644 --- a/src/dash-table/derived/filter/map.ts +++ b/src/dash-table/derived/filter/map.ts @@ -2,8 +2,8 @@ import * as R from 'ramda'; import { memoizeOneFactory } from 'core/memoizer'; -import { VisibleColumns, IVisibleColumn } from 'dash-table/components/Table/props'; -import { SingleColumnSyntaxTree, MultiColumnsSyntaxTree, getSingleColumnMap } from 'dash-table/syntax-tree'; +import { VisibleColumns, IVisibleColumn, SetFilter } from 'dash-table/components/Table/props'; +import { SingleColumnSyntaxTree, MultiColumnsSyntaxTree, getMultiColumnQueryString, getSingleColumnMap } from 'dash-table/syntax-tree'; const cloneIf = ( current: Map, @@ -61,20 +61,49 @@ export default memoizeOneFactory(( return newMap; }); -export const updateMap = ( - map: Map, - column: IVisibleColumn, - value: any -): Map => { - const safeColumnId = column.id.toString(); - +function updateMap(map: Map, column: IVisibleColumn, value: any) { + const id = column.id.toString(); const newMap = new Map(map); if (value && value.length) { - newMap.set(safeColumnId, new SingleColumnSyntaxTree(value, column)); + newMap.set(id, new SingleColumnSyntaxTree(value, column)); } else { - newMap.delete(safeColumnId); + newMap.delete(id); } return newMap; -}; \ No newline at end of file +} + +function updateState(map: Map, setFilter: SetFilter) { + const asts = Array.from(map.values()); + const globalFilter = getMultiColumnQueryString(asts); + + const rawGlobalFilter = R.map( + ast => ast.query || '', + R.filter(ast => Boolean(ast), asts) + ).join(' && '); + + setFilter(globalFilter, rawGlobalFilter, map); +} + +export const updateColumnFilter = ( + map: Map, + column: IVisibleColumn, + value: any, + setFilter: SetFilter +) => { + map = updateMap(map, column, value); + updateState(map, setFilter); +}; + +export const clearColumnsFilter = ( + map: Map, + columns: VisibleColumns, + setFilter: SetFilter +) => { + R.forEach(column => { + map = updateMap(map, column, ''); + }, columns); + + updateState(map, setFilter); +} \ No newline at end of file diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 38fdac3d2..64c347853 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -17,8 +17,8 @@ import { SetFilter } from 'dash-table/components/Table/props'; import * as actions from 'dash-table/utils/actions'; -import { SingleColumnSyntaxTree, getMultiColumnQueryString } from 'dash-table/syntax-tree'; -import { updateMap } from '../filter/map'; +import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; +import { clearColumnsFilter } from '../filter/map'; const doAction = ( action: ( @@ -37,24 +37,15 @@ const doAction = ( ) => () => { setProps(action(column, columns, columnRowIndex, data)); - const affectedColumnIds: string[] = actions.getAffectedColumns(column, columns, columnRowIndex); - + const affectedColumns: VisibleColumns = []; R.forEach(id => { const affectedColumn = columns.find(c => c.id === id); if (affectedColumn) { - map = updateMap(map, affectedColumn, ''); + affectedColumns.push(affectedColumn); } - }, affectedColumnIds); - - const asts = Array.from(map.values()); - const globalFilter = getMultiColumnQueryString(asts); - - const rawGlobalFilter = R.map( - ast => ast.query || '', - R.filter(ast => Boolean(ast), asts) - ).join(' && '); + }, actions.getAffectedColumns(column, columns, columnRowIndex)); - setFilter(globalFilter, rawGlobalFilter, map); + clearColumnsFilter(map, affectedColumns, setFilter); }; function doSort(columnId: ColumnId, sortBy: SortBy, mode: SortMode, setProps: SetProps) { From 7713b2240fc44edc3ed2524a4cd084a99fb3c254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Jul 2019 13:43:06 -0400 Subject: [PATCH 03/12] clean up types + lint --- src/dash-table/components/HeaderFactory.tsx | 10 ++-------- src/dash-table/components/Table/props.ts | 5 +++++ src/dash-table/derived/filter/map.ts | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index f955f2b89..ebb435104 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -4,7 +4,7 @@ import React, { CSSProperties } from 'react'; import { arrayMap2 } from 'core/math/arrayZipMap'; import { matrixMap2, matrixMap3 } from 'core/math/matrixZipMap'; -import { ControlledTableProps, VisibleColumns, SetFilter } from 'dash-table/components/Table/props'; +import { VisibleColumns, HeaderFactoryProps } from 'dash-table/components/Table/props'; import derivedHeaderContent from 'dash-table/derived/header/content'; import getHeaderRows from 'dash-table/derived/header/headerRows'; import getIndices from 'dash-table/derived/header/indices'; @@ -16,12 +16,6 @@ import derivedHeaderStyles, { derivedHeaderOpStyles } from 'dash-table/derived/h import { IEdgesMatrices } from 'dash-table/derived/edges/type'; import { memoizeOne } from 'core/memoizer'; -import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; - -type Props = ControlledTableProps & { - map: Map; - setFilter: SetFilter; -}; export default class HeaderFactory { private readonly headerContent = derivedHeaderContent(); @@ -35,7 +29,7 @@ export default class HeaderFactory { return this.propsFn(); } - constructor(private readonly propsFn: () => Props) { + constructor(private readonly propsFn: () => HeaderFactoryProps) { } diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index e57437d69..d425d5f26 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -414,6 +414,11 @@ export interface IFilterFactoryProps { style_filter_conditional: BasicFilters; } +export type HeaderFactoryProps = ControlledTableProps & { + map: Map; + setFilter: SetFilter; +}; + export interface ICellFactoryProps { active_cell: ICellCoordinates; columns: VisibleColumns; diff --git a/src/dash-table/derived/filter/map.ts b/src/dash-table/derived/filter/map.ts index aaf6d8640..1c7219fdc 100644 --- a/src/dash-table/derived/filter/map.ts +++ b/src/dash-table/derived/filter/map.ts @@ -106,4 +106,4 @@ export const clearColumnsFilter = ( }, columns); updateState(map, setFilter); -} \ No newline at end of file +}; \ No newline at end of file From 86cf59b9cf5fdad72a623f8ca1c211ce890dbb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Jul 2019 14:32:32 -0400 Subject: [PATCH 04/12] sanity test for clear and delete columns --- demo/AppMode.ts | 21 +++++- src/dash-table/derived/header/content.tsx | 2 +- tests/cypress/src/DashTable.ts | 34 +++++++--- tests/cypress/tests/standalone/column_test.ts | 64 +++++++++++++++++++ 4 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 tests/cypress/tests/standalone/column_test.ts diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 06f3fa470..8e64e942c 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -14,6 +14,7 @@ import { import { TooltipSyntax } from 'dash-table/tooltips/props'; export enum AppMode { + Clearable = 'clearable', Date = 'date', Default = 'default', Filtering = 'filtering', @@ -48,6 +49,7 @@ function getBaseTableProps(mock: IDataMock) { action: ChangeAction.None }, renamable: true, + clearable: true, deletable: true })), dropdown: { @@ -59,6 +61,8 @@ function getBaseTableProps(mock: IDataMock) { })) } }, + merge_duplicate_headers: true, + filter_action: 'native', page_action: TableAction.None, style_table: { max_height: '800px', @@ -91,7 +95,7 @@ function getDefaultState( sort_action: TableAction.Native, fixed_rows: { headers: true }, fixed_columns: { headers: true }, - merge_duplicate_headers: false, + // merge_duplicate_headers: false, row_deletable: true, row_selectable: 'single', page_action: TableAction.Native @@ -183,6 +187,19 @@ function getTypedState() { return state; } +function getClearableState() { + const state = getDefaultState(); + + R.forEach(c => { + c.clearable = true; + }, state.tableProps.columns || []); + + state.tableProps.merge_duplicate_headers = true; + state.tableProps.filter_action = TableAction.Native; + + return state; +} + function getDateState() { const state = getTypedState(); @@ -316,6 +333,8 @@ function getState() { const mode = Environment.searchParams.get('mode'); switch (mode) { + case AppMode.Clearable: + return getClearableState(); case AppMode.Date: return getDateState(); case AppMode.Filtering: diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 64c347853..890f676ae 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -175,7 +175,7 @@ function getter( '' } - {labels[columnIndex]} + {labels[columnIndex]}
); }, indices diff --git a/tests/cypress/src/DashTable.ts b/tests/cypress/src/DashTable.ts index 6a713a46c..790957e98 100644 --- a/tests/cypress/src/DashTable.ts +++ b/tests/cypress/src/DashTable.ts @@ -3,26 +3,42 @@ export default class DashTable { return cy.get(`#table tbody tr td.column-${column}`).eq(row); } + static getCellById(row: number, column: string) { + return cy.get(`#table tbody tr td[data-dash-column="${column}"]`).eq(row); + } + + static getFilter(column: number) { + return cy.get(`#table tbody tr th.dash-filter.column-${column}`); + } + + static getFilterById(column: string) { + return cy.get(`#table tbody tr th.dash-filter[data-dash-column="${column}"]`); + } + + static getHeader(row: number, column: number) { + return cy.get(`#table tbody tr th.dash-header.column-${column}`).eq(row); + } + + static getHeaderById(row: number, column: string) { + return cy.get(`#table tbody tr th.dash-header[data-dash-column="${column}"]`).eq(row); + } + static focusCell(row: number, column: number) { // somehow we need to scrollIntoView AFTER click, or it doesn't // work right. Why? return this.getCell(row, column).click().scrollIntoView(); } - static getCellById(row: number, column: number | string) { - return cy.get(`#table tbody tr td[data-dash-column="${column}"]`).eq(row); - } - - static focusCellById(row: number, column: number | string) { + static focusCellById(row: number, column: string) { return this.getCellById(row, column).click().scrollIntoView(); } - static getFilter(column: number) { - return cy.get(`#table tbody tr th.dash-filter.column-${column}`); + static clearColumnById(row: number, column: string) { + return cy.get(`#table tbody tr th.dash-header[data-dash-column="${column}"] .column-header--clear`).eq(row).click(); } - static getFilterById(column: number | string) { - return cy.get(`#table tbody tr th.dash-filter[data-dash-column="${column}"]`); + static deleteColumnById(row: number, column: string) { + return cy.get(`#table tbody tr th.dash-header[data-dash-column="${column}"] .column-header--delete`).eq(row).click(); } static getDelete(row: number) { diff --git a/tests/cypress/tests/standalone/column_test.ts b/tests/cypress/tests/standalone/column_test.ts new file mode 100644 index 000000000..7732f9efe --- /dev/null +++ b/tests/cypress/tests/standalone/column_test.ts @@ -0,0 +1,64 @@ +import DashTable from 'cypress/DashTable'; +import DOM from 'cypress/DOM'; + +import { AppMode } from 'demo/AppMode'; + +describe(`column, mode=${AppMode.Clearable}`, () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.Clearable}`); + DashTable.toggleScroll(false); + }); + + it('can delete column', () => { + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'rows')); + DashTable.deleteColumnById(0, 'rows'); + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'City')); + DashTable.getHeader(1, 0).within(() => cy.get('span.column-header-name').should('have.html', 'Canada')); + DashTable.getHeader(2, 0).within(() => cy.get('span.column-header-name').should('have.html', 'Toronto')); + DashTable.deleteColumnById(1, 'ccc'); // Canada + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'City')); + DashTable.getHeader(1, 0).within(() => cy.get('span.column-header-name').should('have.html', 'America')); + DashTable.getHeader(2, 0).within(() => cy.get('span.column-header-name').should('have.html', 'New York City')); + DashTable.deleteColumnById(0, 'fff'); // Boston + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'City')); + DashTable.getHeader(1, 0).within(() => cy.get('span.column-header-name').should('have.html', 'America')); + DashTable.getHeader(2, 0).within(() => cy.get('span.column-header-name').should('have.html', 'New York City')); + DashTable.getHeader(0, 1).within(() => cy.get('span.column-header-name').should('have.html', 'France')); + DashTable.getHeader(1, 1).within(() => cy.get('span.column-header-name').should('have.html', 'Paris')); + }); + + it('can clear column', () => { + DashTable.getFilter(0).click(); + DOM.focused.type(`is num`); + DashTable.getFilter(1).click(); + DOM.focused.type(`is num`); + DashTable.getFilter(2).click(); + DOM.focused.type(`is num`); + DashTable.getFilter(3).click(); + DOM.focused.type(`is num`); + DashTable.getFilter(4).click(); + DOM.focused.type(`is num`); + + DashTable.clearColumnById(0, 'rows'); + DashTable.clearColumnById(1, 'ccc'); // Canada + DashTable.clearColumnById(0, 'fff'); // Boston + + DashTable.getHeader(2, 0).within(() => cy.get('span.column-header-name').should('have.html', 'rows')); + DashTable.getHeader(2, 1).within(() => cy.get('span.column-header-name').should('have.html', 'Toronto')); + DashTable.getHeader(0, 2).within(() => cy.get('span.column-header-name').should('have.html', 'Montréal')); + DashTable.getHeader(1, 3).within(() => cy.get('span.column-header-name').should('have.html', 'New York City')); + DashTable.getHeader(0, 4).within(() => cy.get('span.column-header-name').should('have.html', 'Boston')); + + DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '')); + DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('have.html', '')); + DashTable.getCell(0, 2).within(() => cy.get('.dash-cell-value').should('have.html', '')); + DashTable.getCell(0, 3).within(() => cy.get('.dash-cell-value').should('have.html', '1')); + DashTable.getCell(0, 4).within(() => cy.get('.dash-cell-value').should('have.html', '')); + + DashTable.getFilter(0).within(() => cy.get('input').should('have.value', '')); + DashTable.getFilter(1).within(() => cy.get('input').should('have.value', '')); + DashTable.getFilter(2).within(() => cy.get('input').should('have.value', '')); + DashTable.getFilter(3).within(() => cy.get('input').should('have.value', 'is num')); + DashTable.getFilter(4).within(() => cy.get('input').should('have.value', '')); + }); +}); \ No newline at end of file From f94dfca3475c0e1a556e106625e5f18e50e6bdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Jul 2019 15:10:17 -0400 Subject: [PATCH 05/12] - fix behavior for merged / not merged headers --- demo/AppMode.ts | 12 ++++++++++-- src/dash-table/components/HeaderFactory.tsx | 1 + src/dash-table/derived/header/content.tsx | 11 +++++++---- src/dash-table/utils/actions.js | 18 +++++++++--------- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 8e64e942c..dbf4cb7f8 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -15,6 +15,7 @@ import { TooltipSyntax } from 'dash-table/tooltips/props'; export enum AppMode { Clearable = 'clearable', + ClearableMerged = 'clearableMerged', Date = 'date', Default = 'default', Filtering = 'filtering', @@ -95,7 +96,7 @@ function getDefaultState( sort_action: TableAction.Native, fixed_rows: { headers: true }, fixed_columns: { headers: true }, - // merge_duplicate_headers: false, + merge_duplicate_headers: false, row_deletable: true, row_selectable: 'single', page_action: TableAction.Native @@ -189,13 +190,18 @@ function getTypedState() { function getClearableState() { const state = getDefaultState(); + state.tableProps.filter_action = TableAction.Native; R.forEach(c => { c.clearable = true; }, state.tableProps.columns || []); + return state; +} + +function getClearableMergedState() { + const state = getClearableState(); state.tableProps.merge_duplicate_headers = true; - state.tableProps.filter_action = TableAction.Native; return state; } @@ -335,6 +341,8 @@ function getState() { switch (mode) { case AppMode.Clearable: return getClearableState(); + case AppMode.ClearableMerged: + return getClearableMergedState(); case AppMode.Date: return getDateState(); case AppMode.Filtering: diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index ebb435104..d40301e5b 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -92,6 +92,7 @@ export default class HeaderFactory { const contents = this.headerContent( columns, + merge_duplicate_headers, data, labelsAndIndices, map, diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 890f676ae..af9cf4e3a 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -25,17 +25,19 @@ const doAction = ( column: IVisibleColumn, columns: VisibleColumns, columnRowIndex: any, + mergeDuplicateHeaders: boolean, data: Data ) => any, column: IVisibleColumn, columns: VisibleColumns, columnRowIndex: any, + mergeDuplicateHeaders: boolean, setFilter: SetFilter, setProps: SetProps, map: Map, data: Data ) => () => { - setProps(action(column, columns, columnRowIndex, data)); + setProps(action(column, columns, columnRowIndex, mergeDuplicateHeaders, data)); const affectedColumns: VisibleColumns = []; R.forEach(id => { @@ -43,7 +45,7 @@ const doAction = ( if (affectedColumn) { affectedColumns.push(affectedColumn); } - }, actions.getAffectedColumns(column, columns, columnRowIndex)); + }, actions.getAffectedColumns(column, columns, columnRowIndex, mergeDuplicateHeaders)); clearColumnsFilter(map, affectedColumns, setFilter); }; @@ -112,6 +114,7 @@ function getColumnFlag(i: number, flag?: boolean | boolean[]): boolean { function getter( columns: VisibleColumns, + mergeDuplicateHeaders: boolean, data: Data, labelsAndIndices: R.KeyValuePair[], map: Map, @@ -158,7 +161,7 @@ function getter( {clearable ? ( {'Ø'} ) : @@ -168,7 +171,7 @@ function getter( {deletable ? ( {'×'} ) : diff --git a/src/dash-table/utils/actions.js b/src/dash-table/utils/actions.js index 7a4194bc7..ea2de85f9 100644 --- a/src/dash-table/utils/actions.js +++ b/src/dash-table/utils/actions.js @@ -1,9 +1,9 @@ import * as R from 'ramda'; -function getGroupedColumnIndices(column, columns, headerRowIndex) { +function getGroupedColumnIndices(column, columns, headerRowIndex, mergeDuplicateHeaders) { const columnIndex = columns.indexOf(column); - if (!column.name || (Array.isArray(column.name) && column.name.length < headerRowIndex)) { + if (!column.name || (Array.isArray(column.name) && column.name.length < headerRowIndex) || !mergeDuplicateHeaders) { return { groupIndexFirst: columnIndex, groupIndexLast: columnIndex }; } @@ -22,9 +22,9 @@ function getGroupedColumnIndices(column, columns, headerRowIndex) { return { groupIndexFirst: columnIndex, groupIndexLast: lastColumnIndex }; } -export function getAffectedColumns(column, columns, headerRowIndex) { +export function getAffectedColumns(column, columns, headerRowIndex, mergeDuplicateHeaders) { const { groupIndexFirst, groupIndexLast } = getGroupedColumnIndices( - column, columns, headerRowIndex + column, columns, headerRowIndex, mergeDuplicateHeaders ); return R.slice( @@ -34,17 +34,17 @@ export function getAffectedColumns(column, columns, headerRowIndex) { ); } -export function clearColumn(column, columns, headerRowIndex, data) { - const rejectedColumnIds = getAffectedColumns(column, columns, headerRowIndex); +export function clearColumn(column, columns, headerRowIndex, mergeDuplicateHeaders, data) { + const rejectedColumnIds = getAffectedColumns(column, columns, headerRowIndex, mergeDuplicateHeaders); return { data: R.map(R.omit(rejectedColumnIds), data) }; } -export function deleteColumn(column, columns, headerRowIndex, data) { +export function deleteColumn(column, columns, headerRowIndex, mergeDuplicateHeaders, data) { const {groupIndexFirst, groupIndexLast} = getGroupedColumnIndices( - column, columns, headerRowIndex + column, columns, headerRowIndex, mergeDuplicateHeaders ); return { @@ -53,7 +53,7 @@ export function deleteColumn(column, columns, headerRowIndex, data) { 1 + groupIndexLast - groupIndexFirst, columns ), - ...clearColumn(column, columns, headerRowIndex, data), + ...clearColumn(column, columns, headerRowIndex, mergeDuplicateHeaders, data), // NOTE - We're just clearing these so that there aren't any // inconsistencies. In an ideal world, we would probably only // update them if they contained one of the columns that we're From 0b9cb9cbd19b8f8a207c5153228d58e8f6bded1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Jul 2019 15:11:18 -0400 Subject: [PATCH 06/12] - update clear/delete tests - update changelog --- CHANGELOG.md | 16 +++++ tests/cypress/tests/standalone/column_test.ts | 65 ++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a16c4452..000da1803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] +### Added +[#497](https://github.com/plotly/dash-table/pull/497) +- New `column.clearable` flag that displays a `Ø` action in the column + Accepts a boolean or array of booleans for multi-line headers. + Clicking a merged column's `Ø` will clear all related columns. + + - Clearing column(s) will remove the appropriate data props from each datum + row of `data`. + - Additionally clearing the column will reset the filter for the affected column(s) + +### Changed +[#497](https://github.com/plotly/dash-table/pull/497) +- Like for clearing above, deleting through the `x` action will also +reset the filter for the affected column(s) + ## [4.0.1] - 2019-07-09 ### Changed [#488](https://github.com/plotly/dash-table/pull/488) diff --git a/tests/cypress/tests/standalone/column_test.ts b/tests/cypress/tests/standalone/column_test.ts index 7732f9efe..c1a8d1522 100644 --- a/tests/cypress/tests/standalone/column_test.ts +++ b/tests/cypress/tests/standalone/column_test.ts @@ -3,9 +3,9 @@ import DOM from 'cypress/DOM'; import { AppMode } from 'demo/AppMode'; -describe(`column, mode=${AppMode.Clearable}`, () => { +describe(`column, mode=${AppMode.ClearableMerged}`, () => { beforeEach(() => { - cy.visit(`http://localhost:8080?mode=${AppMode.Clearable}`); + cy.visit(`http://localhost:8080?mode=${AppMode.ClearableMerged}`); DashTable.toggleScroll(false); }); @@ -61,4 +61,65 @@ describe(`column, mode=${AppMode.Clearable}`, () => { DashTable.getFilter(3).within(() => cy.get('input').should('have.value', 'is num')); DashTable.getFilter(4).within(() => cy.get('input').should('have.value', '')); }); +}); + +describe(`column, mode=${AppMode.Clearable}`, () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.Clearable}`); + DashTable.toggleScroll(false); + }); + + it('can delete column', () => { + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'rows')); + DashTable.deleteColumnById(0, 'rows'); + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'City')); + DashTable.getHeader(1, 0).within(() => cy.get('span.column-header-name').should('have.html', 'Canada')); + DashTable.getHeader(2, 0).within(() => cy.get('span.column-header-name').should('have.html', 'Toronto')); + DashTable.deleteColumnById(1, 'ccc'); // Canada + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'City')); + DashTable.getHeader(1, 0).within(() => cy.get('span.column-header-name').should('have.html', 'Canada')); + DashTable.getHeader(2, 0).within(() => cy.get('span.column-header-name').should('have.html', 'Montréal')); + DashTable.deleteColumnById(0, 'fff'); // Boston + DashTable.getHeader(0, 1).within(() => cy.get('span.column-header-name').should('have.html', 'City')); + DashTable.getHeader(1, 1).within(() => cy.get('span.column-header-name').should('have.html', 'America')); + DashTable.getHeader(2, 1).within(() => cy.get('span.column-header-name').should('have.html', 'New York City')); + DashTable.getHeader(0, 2).within(() => cy.get('span.column-header-name').should('have.html', 'City')); + DashTable.getHeader(1, 2).within(() => cy.get('span.column-header-name').should('have.html', 'France')); + DashTable.getHeader(2, 2).within(() => cy.get('span.column-header-name').should('have.html', 'Paris')); + }); + + it('can clear column', () => { + DashTable.getFilter(0).click(); + DOM.focused.type(`is num`); + DashTable.getFilter(1).click(); + DOM.focused.type(`is num`); + DashTable.getFilter(2).click(); + DOM.focused.type(`is num`); + DashTable.getFilter(3).click(); + DOM.focused.type(`is num`); + DashTable.getFilter(4).click(); + DOM.focused.type(`is num`); + + DashTable.clearColumnById(0, 'rows'); + DashTable.clearColumnById(1, 'ccc'); // Canada + DashTable.clearColumnById(0, 'fff'); // Boston + + DashTable.getHeader(2, 0).within(() => cy.get('span.column-header-name').should('have.html', 'rows')); + DashTable.getHeader(2, 1).within(() => cy.get('span.column-header-name').should('have.html', 'Toronto')); + DashTable.getHeader(2, 2).within(() => cy.get('span.column-header-name').should('have.html', 'Montréal')); + DashTable.getHeader(2, 3).within(() => cy.get('span.column-header-name').should('have.html', 'New York City')); + DashTable.getHeader(2, 4).within(() => cy.get('span.column-header-name').should('have.html', 'Boston')); + + DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '')); + DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('have.html', '')); + DashTable.getCell(0, 2).within(() => cy.get('.dash-cell-value').should('have.html', '100')); + DashTable.getCell(0, 3).within(() => cy.get('.dash-cell-value').should('have.html', '1')); + DashTable.getCell(0, 4).within(() => cy.get('.dash-cell-value').should('have.html', '')); + + DashTable.getFilter(0).within(() => cy.get('input').should('have.value', '')); + DashTable.getFilter(1).within(() => cy.get('input').should('have.value', '')); + DashTable.getFilter(2).within(() => cy.get('input').should('have.value', 'is num')); + DashTable.getFilter(3).within(() => cy.get('input').should('have.value', 'is num')); + DashTable.getFilter(4).within(() => cy.get('input').should('have.value', '')); + }); }); \ No newline at end of file From 4fabe4908917a49caddde5dcc980621b055b3850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Jul 2019 15:18:47 -0400 Subject: [PATCH 07/12] revert appmode default state changes --- demo/AppMode.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/demo/AppMode.ts b/demo/AppMode.ts index dbf4cb7f8..097e00039 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -50,7 +50,6 @@ function getBaseTableProps(mock: IDataMock) { action: ChangeAction.None }, renamable: true, - clearable: true, deletable: true })), dropdown: { @@ -62,8 +61,6 @@ function getBaseTableProps(mock: IDataMock) { })) } }, - merge_duplicate_headers: true, - filter_action: 'native', page_action: TableAction.None, style_table: { max_height: '800px', From efc45e8e8252ba341e53567f4da74d4e385e1d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 16 Jul 2019 17:17:42 -0400 Subject: [PATCH 08/12] fix merge issues --- src/dash-table/utils/actions.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/dash-table/utils/actions.js b/src/dash-table/utils/actions.js index 0d4b6ab32..10064c897 100644 --- a/src/dash-table/utils/actions.js +++ b/src/dash-table/utils/actions.js @@ -1,8 +1,7 @@ import * as R from 'ramda'; +import getHeaderRows from 'dash-table/derived/header/headerRows'; -function getGroupedColumnIndices(column, columns, headerRowIndex, mergeDuplicateHeaders) { - const columnIndex = columns.indexOf(column); - +function getGroupedColumnIndices(column, columns, headerRowIndex, mergeDuplicateHeaders, columnIndex) { if (!column.name || (Array.isArray(column.name) && column.name.length < headerRowIndex) || !mergeDuplicateHeaders) { return { groupIndexFirst: columnIndex, groupIndexLast: columnIndex }; } @@ -24,7 +23,7 @@ function getGroupedColumnIndices(column, columns, headerRowIndex, mergeDuplicate export function getAffectedColumns(column, columns, headerRowIndex, mergeDuplicateHeaders) { const { groupIndexFirst, groupIndexLast } = getGroupedColumnIndices( - column, columns, headerRowIndex, mergeDuplicateHeaders + column, columns, headerRowIndex, mergeDuplicateHeaders, columns.indexOf(column) ); return R.slice( @@ -44,7 +43,7 @@ export function clearColumn(column, columns, headerRowIndex, mergeDuplicateHeade export function deleteColumn(column, columns, headerRowIndex, mergeDuplicateHeaders, data) { const {groupIndexFirst, groupIndexLast} = getGroupedColumnIndices( - column, columns, headerRowIndex, mergeDuplicateHeaders + column, columns, headerRowIndex, mergeDuplicateHeaders, columns.indexOf(column) ); return { @@ -82,7 +81,7 @@ export function changeColumnHeader(column, columns, headerRowIndex, mergeDuplica } const { groupIndexFirst, groupIndexLast } = getGroupedColumnIndices( - column, newColumns, headerRowIndex, mergeDuplicateHeaders + column, newColumns, headerRowIndex, mergeDuplicateHeaders, columnIndex ); R.range(groupIndexFirst, groupIndexLast + 1).map(i => { From 207c5b208614502d52b05252b6b2f209209858a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 17 Jul 2019 09:37:18 -0400 Subject: [PATCH 09/12] add header actions visual tests with variants --- .../percy-storybook/Header.actions.percy.tsx | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/visual/percy-storybook/Header.actions.percy.tsx diff --git a/tests/visual/percy-storybook/Header.actions.percy.tsx b/tests/visual/percy-storybook/Header.actions.percy.tsx new file mode 100644 index 000000000..f67bbed53 --- /dev/null +++ b/tests/visual/percy-storybook/Header.actions.percy.tsx @@ -0,0 +1,129 @@ +import * as R from 'ramda'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import DataTable from 'dash-table/dash/DataTable'; + +const setProps = () => { }; + +const DATA_BASE = R.map(i => ({ + year: i, + montreal: i * 10, + toronto: i * 100, + ottawa: i * -1, + vancouver: i * -10, + temp: i * -100, + humidity: i * 0.1 +}), R.range(0, 100)); + +const COLUMNS_BASE = [ + { name: ['Year', ''], id: 'year' }, + { name: ['City', 'Montreal'], id: 'montreal' }, + { name: ['City', 'Toronto'], id: 'toronto' }, + { name: ['City', 'Ottawa'], id: 'ottawa' }, + { name: ['City', 'Vancouver'], id: 'vancouver' }, + { name: ['Climate', 'Temperature'], id: 'temp' }, + { name: ['Climate', 'Humidity'], id: 'humidity' } +]; + +interface ITest { + name: string; + props: any; +} + +const DEFAULT_PROPS = { + id: 'clear-header', + data: DATA_BASE, + setProps +}; + +const variants: ITest[] = [ + { + name: 'base', + props: {} + }, { + name: 'merged', + props: { + merge_duplicate_headers: true + } + } +]; + +const scenarios: ITest[] = [ + { + name: 'clearable', + props: { + columns: R.map(c => R.mergeRight(c, { + clearable: true + }), COLUMNS_BASE) + } + }, { + name: 'clearable (top-city, bottom-climate)', + props: { + columns: R.map((c: any) => { + const firstName = c.name[0]; + + if (firstName === 'City') { + return R.mergeRight(c, { + clearable: [true, false] + }); + } else if (firstName === 'Climate') { + return R.mergeRight(c, { + clearable: [false, true] + }); + + } else { + return c; + } + }, COLUMNS_BASE) + } + }, { + name: 'deletable', + props: { + columns: R.map(c => R.mergeRight(c, { + deletable: true + }), COLUMNS_BASE) + } + }, { + name: 'deletable (top-city, bottom-climate)', + props: { + columns: R.map((c: any) => { + const firstName = c.name[0]; + + if (firstName === 'City') { + return R.mergeRight(c, { + deletable: [true, false] + }); + } else if (firstName === 'Climate') { + return R.mergeRight(c, { + deletable: [false, true] + }); + + } else { + return c; + } + }, COLUMNS_BASE) + } + }, { + name: 'clearable+deletable', + props: { + columns: R.map(c => R.mergeRight(c, { + clearable: true, + deletable: true + }), COLUMNS_BASE) + } + } +]; + +const tests = R.xprod(scenarios, variants); + +R.reduce( + (chain, [scenario, variant]) => chain.add(`${scenario.name} (${variant.name})`, () => ()), + storiesOf(`DashTable/Headers, actions`, module), + tests +); \ No newline at end of file From d50f7a17be5757f3de97da93f42cd2de9d9839c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 17 Jul 2019 10:54:50 -0400 Subject: [PATCH 10/12] action icons through ::before pseudo --- src/dash-table/components/Table/Table.less | 12 ++++++++++++ src/dash-table/derived/header/content.tsx | 12 +++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index c1e57df5a..2d964126e 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -485,6 +485,18 @@ } .dash-spreadsheet-inner { + .column-header--clear::before { + content: 'ø'; + } + + .column-header--delete::before { + content: 'x' + } + + .column-header--edit::before { + content: '✎'; + } + .column-header--clear, .column-header--delete, .column-header--edit { diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 73d5a37b5..3190c3178 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -153,9 +153,7 @@ function getter( ( - {`✎`} - ) : + />) : '' } @@ -163,9 +161,7 @@ function getter( ( - {'Ø'} - ) : + />) : '' } @@ -173,9 +169,7 @@ function getter( ( - {'×'} - ) : + />) : '' } From 39aa50528369360e84184939d30f40d763fbceb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 17 Jul 2019 13:25:40 -0400 Subject: [PATCH 11/12] less rows in header action visual tests --- tests/visual/percy-storybook/Header.actions.percy.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/visual/percy-storybook/Header.actions.percy.tsx b/tests/visual/percy-storybook/Header.actions.percy.tsx index f67bbed53..497992ff9 100644 --- a/tests/visual/percy-storybook/Header.actions.percy.tsx +++ b/tests/visual/percy-storybook/Header.actions.percy.tsx @@ -13,7 +13,7 @@ const DATA_BASE = R.map(i => ({ vancouver: i * -10, temp: i * -100, humidity: i * 0.1 -}), R.range(0, 100)); +}), R.range(0, 10)); const COLUMNS_BASE = [ { name: ['Year', ''], id: 'year' }, From ab899e0457e0fa1ea9e178ebbb3f86f36b32e103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 17 Jul 2019 14:20:35 -0400 Subject: [PATCH 12/12] =?UTF-8?q?revert=20'=C3=97'=20->=20'x'=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dash-table/components/Table/Table.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index 2d964126e..bff78b995 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -490,7 +490,7 @@ } .column-header--delete::before { - content: 'x' + content: '×' } .column-header--edit::before {