From b01101819ae4422dc704f5aaec99c82a1346ce68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 17 Jul 2019 15:23:24 -0400 Subject: [PATCH 01/32] - react 15 to 16 - hidden vs. visible columns props at sanitation --- demo/AppMode.ts | 3 +- demo/index.html | 6 +- src/core/storage/Cookie.ts | 4 +- src/dash-table/components/CellFactory.tsx | 14 +- .../components/ControlledTable/index.tsx | 44 ++-- src/dash-table/components/EdgeFactory.tsx | 10 +- src/dash-table/components/FilterFactory.tsx | 18 +- src/dash-table/components/HeaderFactory.tsx | 18 +- .../components/Table/controlledPropsHelper.ts | 106 +++++++++ .../components/Table/derivedPropsHelper.ts | 107 +++++++++ src/dash-table/components/Table/index.tsx | 217 ++---------------- src/dash-table/components/Table/props.ts | 36 ++- src/dash-table/conditional/index.ts | 8 +- src/dash-table/dash/DataTable.js | 2 + src/dash-table/dash/Sanitizer.ts | 14 +- src/dash-table/derived/cell/contents.tsx | 12 +- src/dash-table/derived/cell/dropdowns.ts | 8 +- src/dash-table/derived/cell/wrapperStyles.ts | 4 +- src/dash-table/derived/cell/wrappers.tsx | 8 +- src/dash-table/derived/column/visible.ts | 8 - src/dash-table/derived/data/virtual.ts | 4 +- src/dash-table/derived/edges/data.ts | 4 +- src/dash-table/derived/edges/filter.ts | 4 +- src/dash-table/derived/edges/header.ts | 4 +- src/dash-table/derived/edges/index.ts | 8 +- src/dash-table/derived/filter/map.ts | 6 +- .../derived/filter/wrapperStyles.ts | 4 +- src/dash-table/derived/header/content.tsx | 10 +- src/dash-table/derived/header/headerRows.ts | 8 +- src/dash-table/derived/header/indices.ts | 4 +- src/dash-table/derived/header/labels.ts | 4 +- .../derived/header/wrapperStyles.ts | 4 +- src/dash-table/derived/header/wrappers.tsx | 4 +- src/dash-table/derived/style/index.ts | 12 +- src/dash-table/handlers/cellEvents.ts | 34 +-- .../syntax-tree/SingleColumnSyntaxTree.ts | 4 +- src/dash-table/syntax-tree/index.ts | 4 +- src/dash-table/utils/actions.js | 6 +- 38 files changed, 400 insertions(+), 375 deletions(-) create mode 100644 src/dash-table/components/Table/controlledPropsHelper.ts create mode 100644 src/dash-table/components/Table/derivedPropsHelper.ts delete mode 100644 src/dash-table/derived/column/visible.ts diff --git a/demo/AppMode.ts b/demo/AppMode.ts index e7e62dbb2..f036248bb 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -7,7 +7,6 @@ import { PropsWithDefaults, ChangeAction, ChangeFailure, - IVisibleColumn, ColumnType, TableAction } from 'dash-table/components/Table/props'; @@ -176,7 +175,7 @@ function getTypedState() { const state = getDefaultState(); R.forEach(column => { - (column as IVisibleColumn).on_change = { + column.on_change = { action: ChangeAction.Coerce, failure: ChangeFailure.Reject }; diff --git a/demo/index.html b/demo/index.html index 5fb27b747..613c17016 100644 --- a/demo/index.html +++ b/demo/index.html @@ -5,11 +5,9 @@
- - + + - diff --git a/src/core/storage/Cookie.ts b/src/core/storage/Cookie.ts index 71f665767..b06db4ce3 100644 --- a/src/core/storage/Cookie.ts +++ b/src/core/storage/Cookie.ts @@ -3,7 +3,7 @@ const __20years = 86400 * 1000 * 365 * 20; export default class CookieStorage { public static delete(id: string, domain: string = '', path: string = '/') { - let expires = new Date((new Date().getTime() - __1day)).toUTCString(); + let expires = new Date(Date.now() - __1day).toUTCString(); document.cookie = `${id}=;expires=${expires};domain=${domain};path=${path}`; } @@ -28,7 +28,7 @@ export default class CookieStorage { } public static set(id: string, value: string, domain: string = '', path: string = '/') { - let expires = new Date((new Date().getTime() + __20years)).toUTCString(); + let expires = new Date(Date.now() + __20years).toUTCString(); let entry = `${id}=${value};expires=${expires};domain=${domain};path=${path}`; diff --git a/src/dash-table/components/CellFactory.tsx b/src/dash-table/components/CellFactory.tsx index e295158e3..144713cc0 100644 --- a/src/dash-table/components/CellFactory.tsx +++ b/src/dash-table/components/CellFactory.tsx @@ -36,7 +36,6 @@ export default class CellFactory { public createCells(dataEdges: IEdgesMatrices | undefined, dataOpEdges: IEdgesMatrices | undefined) { const { active_cell, - columns, dropdown_conditional, dropdown, data, @@ -51,7 +50,8 @@ export default class CellFactory { style_cell_conditional, style_data, style_data_conditional, - virtualized + virtualized, + visibleColumns } = this.props; const relevantStyles = this.relevantStyles( @@ -62,7 +62,7 @@ export default class CellFactory { ); const partialCellStyles = this.dataPartialStyles( - columns, + visibleColumns, relevantStyles, virtualized.data, virtualized.offset @@ -82,7 +82,7 @@ export default class CellFactory { ); const dropdowns = this.cellDropdowns( - columns, + visibleColumns, virtualized.data, virtualized.indices, dropdown_conditional, @@ -101,7 +101,7 @@ export default class CellFactory { ); const partialCellWrappers = this.cellWrappers.partialGet( - columns, + visibleColumns, virtualized.data, virtualized.offset ); @@ -114,7 +114,7 @@ export default class CellFactory { ); const partialCellContents = this.cellContents.partialGet( - columns, + visibleColumns, virtualized.data, virtualized.offset, !!is_focused, @@ -124,7 +124,7 @@ export default class CellFactory { const cellContents = this.cellContents.get( partialCellContents, active_cell, - columns, + visibleColumns, virtualized.data, virtualized.offset, !!is_focused, diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index c2124ef39..33e760bea 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -317,12 +317,12 @@ export default class ControlledTable extends PureComponent const e = event; const { active_cell, - columns, selected_cells, start_cell, end_cell, setProps, - viewport + viewport, + visibleColumns } = this.props; // This is mostly to prevent TABing also triggering native HTML tab @@ -414,7 +414,7 @@ export default class ControlledTable extends PureComponent if (active_cell.column > minCol) { minCol++; endCol = minCol; - } else if (maxCol < columns.length - 1) { + } else if (maxCol < visibleColumns.length - 1) { maxCol++; endCol = maxCol; } @@ -432,12 +432,12 @@ export default class ControlledTable extends PureComponent const finalSelected = makeSelection( {minRow, maxRow, minCol, maxCol}, - columns, viewport + visibleColumns, viewport ); const newProps: Partial = { is_focused: false, - end_cell: makeCell(endRow, endCol, columns, viewport), + end_cell: makeCell(endRow, endCol, visibleColumns, viewport), selected_cells: finalSelected }; @@ -448,7 +448,7 @@ export default class ControlledTable extends PureComponent newProps.start_cell = makeCell( newStartRow, newStartCol, - columns, + visibleColumns, viewport ); } @@ -458,11 +458,11 @@ export default class ControlledTable extends PureComponent deleteCell = (event: any) => { const { - columns, data, selected_cells, setProps, - viewport + viewport, + visibleColumns } = this.props; event.preventDefault(); @@ -475,9 +475,9 @@ export default class ControlledTable extends PureComponent ); realCells.forEach(cell => { - if (columns[cell[1]].editable) { + if (visibleColumns[cell[1]].editable) { newData = R.set( - R.lensPath([cell[0], columns[cell[1]].id]), + R.lensPath([cell[0], visibleColumns[cell[1]].id]), '', newData ); @@ -490,7 +490,7 @@ export default class ControlledTable extends PureComponent } getNextCell = (event: any, { restrictToSelection, currentCell }: any) => { - const { columns, selected_cells, viewport } = this.props; + const { selected_cells, viewport, visibleColumns } = this.props; const e = event; @@ -508,7 +508,7 @@ export default class ControlledTable extends PureComponent case KEY_CODES.TAB: nextCoords = restrictToSelection ? selectionCycle([row, column + 1], selected_cells) - : [row, R.min(columns.length - 1, column + 1)]; + : [row, R.min(visibleColumns.length - 1, column + 1)]; break; case KEY_CODES.ARROW_UP: @@ -529,30 +529,30 @@ export default class ControlledTable extends PureComponent `Table.getNextCell: unknown navigation keycode ${e.keyCode}` ); } - return makeCell(nextCoords[0], nextCoords[1], columns, viewport); + return makeCell(nextCoords[0], nextCoords[1], visibleColumns, viewport); } onCopy = (e: any) => { const { - columns, selected_cells, - viewport + viewport, + visibleColumns } = this.props; - TableClipboardHelper.toClipboard(e, selected_cells, columns, viewport.data); + TableClipboardHelper.toClipboard(e, selected_cells, visibleColumns, viewport.data); this.$el.focus(); } onPaste = (e: any) => { const { active_cell, - columns, data, editable, filter_query, setProps, sort_by, - viewport + viewport, + visibleColumns } = this.props; if (!editable || !active_cell) { @@ -563,7 +563,7 @@ export default class ControlledTable extends PureComponent e, active_cell, viewport.indices, - columns, + visibleColumns, data, true, !sort_by.length || !filter_query.length @@ -680,7 +680,6 @@ export default class ControlledTable extends PureComponent render() { const { id, - columns, tooltip_conditional, tooltip, currentTooltip, @@ -699,7 +698,8 @@ export default class ControlledTable extends PureComponent uiViewport, viewport, virtualized, - virtualization + virtualization, + visibleColumns } = this.props; const fragmentClasses = [ @@ -729,7 +729,7 @@ export default class ControlledTable extends PureComponent ...(style_as_list_view ? ['dash-list-view'] : []), ...(empty[0][1] ? ['dash-empty-01'] : []), ...(empty[1][1] ? ['dash-empty-11'] : []), - ...(columns.length ? [] : ['dash-no-columns']), + ...(visibleColumns.length ? [] : ['dash-no-columns']), ...(virtualized.data.length ? [] : ['dash-no-data']), ...(filter_action !== TableAction.None ? [] : ['dash-no-filter']), ...(fill_width ? ['dash-fill-width'] : []) diff --git a/src/dash-table/components/EdgeFactory.tsx b/src/dash-table/components/EdgeFactory.tsx index 08da253d4..4478998f2 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, TableAction } from './Table/props'; +import { ControlledTableProps, Columns, IViewportOffset, Data, ICellCoordinates, TableAction } from './Table/props'; import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; type EdgesMatricesOp = EdgesMatrices | undefined; @@ -139,7 +139,6 @@ export default class EdgeFactory { public createEdges() { const { active_cell, - columns, filter_action, workFilter, fixed_columns, @@ -155,12 +154,13 @@ export default class EdgeFactory { style_filter_conditional, style_header, style_header_conditional, - virtualized + virtualized, + visibleColumns } = this.props; return this.memoizedCreateEdges( active_cell, - columns, + visibleColumns, (row_deletable ? 1 : 0) + (row_selectable ? 1 : 0), filter_action !== TableAction.None, workFilter.map, @@ -182,7 +182,7 @@ export default class EdgeFactory { private memoizedCreateEdges = memoizeOne(( active_cell: ICellCoordinates, - columns: VisibleColumns, + columns: Columns, operations: number, filter_action: boolean, filterMap: Map, diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 173ceec6e..bbe91798b 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, IVisibleColumn, VisibleColumns, RowSelection, TableAction } from 'dash-table/components/Table/props'; +import { ColumnId, IColumn, Columns, 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'; @@ -24,7 +24,6 @@ type SetFilter = ( ) => void; export interface IFilterOptions { - columns: VisibleColumns; filter_query: string; filter_action: TableAction; id: string; @@ -37,6 +36,7 @@ export interface IFilterOptions { style_cell_conditional: Cells; style_filter: Style; style_filter_conditional: BasicFilters; + visibleColumns: Columns; } const NO_FILTERS: JSX.Element[][] = []; @@ -55,7 +55,7 @@ export default class FilterFactory { } - private onChange = (column: IVisibleColumn, map: Map, setFilter: SetFilter, ev: any) => { + private onChange = (column: IColumn, map: Map, setFilter: SetFilter, ev: any) => { Logger.debug('Filter -- onChange', column.id, ev.target.value && ev.target.value.trim()); const value = ev.target.value.trim(); @@ -74,7 +74,7 @@ export default class FilterFactory { } private filter = memoizerCache<[ColumnId, number]>()(( - column: IVisibleColumn, + column: IColumn, index: number, map: Map, setFilter: SetFilter @@ -107,7 +107,6 @@ export default class FilterFactory { filterOpEdges: IEdgesMatrices | undefined ) { const { - columns, filter_action, map, row_deletable, @@ -116,7 +115,8 @@ export default class FilterFactory { style_cell, style_cell_conditional, style_filter, - style_filter_conditional + style_filter_conditional, + visibleColumns } = this.props; if (filter_action === TableAction.None) { @@ -131,7 +131,7 @@ export default class FilterFactory { ); const wrapperStyles = this.wrapperStyles( - this.filterStyles(columns, relevantStyles), + this.filterStyles(visibleColumns, relevantStyles), filterEdges ); @@ -141,14 +141,14 @@ export default class FilterFactory { relevantStyles )[0]; - const filters = R.addIndex(R.map)((column, index) => { + const filters = R.addIndex(R.map)((column, index) => { return this.filter.get(column.id, index)( column, index, map, setFilter ); - }, columns); + }, visibleColumns); const styledFilters = this.getFilterCells( filters, diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index c9ebfd26f..382ba9c31 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, Columns } 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'; @@ -37,7 +37,6 @@ export default class HeaderFactory { const props = this.props; const { - columns, data, merge_duplicate_headers, page_action, @@ -50,12 +49,13 @@ export default class HeaderFactory { style_cell, style_cell_conditional, style_header, - style_header_conditional + style_header_conditional, + visibleColumns } = props; - const headerRows = getHeaderRows(columns); + const headerRows = getHeaderRows(visibleColumns); - const labelsAndIndices = this.getLabelsAndIndices(columns, headerRows, merge_duplicate_headers); + const labelsAndIndices = this.getLabelsAndIndices(visibleColumns, headerRows, merge_duplicate_headers); const relevantStyles = this.relevantStyles( style_cell, @@ -71,7 +71,7 @@ export default class HeaderFactory { ); const wrapperStyles = this.headerStyles( - columns, + visibleColumns, headerRows, relevantStyles ); @@ -83,13 +83,13 @@ export default class HeaderFactory { ); const wrappers = this.headerWrappers( - columns, + visibleColumns, labelsAndIndices, merge_duplicate_headers ); const contents = this.headerContent( - columns, + visibleColumns, data, labelsAndIndices, sort_action, @@ -117,7 +117,7 @@ export default class HeaderFactory { } getLabelsAndIndices = memoizeOne(( - columns: VisibleColumns, + columns: Columns, headerRows: number, merge_duplicate_headers: boolean ) => { diff --git a/src/dash-table/components/Table/controlledPropsHelper.ts b/src/dash-table/components/Table/controlledPropsHelper.ts new file mode 100644 index 000000000..dbf5849b6 --- /dev/null +++ b/src/dash-table/components/Table/controlledPropsHelper.ts @@ -0,0 +1,106 @@ +import * as R from 'ramda'; + +import derivedPaginator from 'dash-table/derived/paginator'; +import derivedSelectedRows from 'dash-table/derived/selectedRows'; +import derivedViewportData from 'dash-table/derived/data/viewport'; +import derivedVirtualData from 'dash-table/derived/data/virtual'; +import derivedVirtualizedData from 'dash-table/derived/data/virtualized'; + +import { + ControlledTableProps, + SanitizedAndDerivedProps, + SetProps, + SetState, + StandaloneState +} from './props'; + +export default () => { + const getPaginator = derivedPaginator(); + const getViewport = derivedViewportData(); + const getViewportSelectedRows = derivedSelectedRows(); + const getVirtual = derivedVirtualData(); + const getVirtualSelectedRows = derivedSelectedRows(); + const getVirtualized = derivedVirtualizedData(); + + return ( + setProps: SetProps, + setState: SetState, + props: SanitizedAndDerivedProps, + state: StandaloneState + ) => { + const { + data, + filter_query, + filter_action, + page_action, + page_current, + page_size, + selected_rows, + sort_action, + sort_by, + uiCell, + uiHeaders, + uiViewport, + virtualization, + visibleColumns + } = R.merge(props, state) as (SanitizedAndDerivedProps & StandaloneState); + + const virtual = getVirtual( + visibleColumns, + data, + filter_action, + filter_query, + sort_action, + sort_by + ); + + const viewport = getViewport( + page_action, + page_current, + page_size, + virtual.data, + virtual.indices + ); + + const virtualized = getVirtualized( + virtualization, + uiCell, + uiHeaders, + uiViewport, + viewport + ); + + const virtual_selected_rows = getVirtualSelectedRows( + virtual.indices, + selected_rows + ); + + const viewport_selected_rows = getViewportSelectedRows( + viewport.indices, + selected_rows + ); + + const paginator = getPaginator( + page_action, + page_current, + page_size, + setProps, + virtual.data + ); + + return R.mergeAll([ + props, + state, + { + paginator, + setProps, + setState, + viewport, + viewport_selected_rows, + virtual, + virtual_selected_rows, + virtualized + } + ]) as ControlledTableProps; + }; +}; \ No newline at end of file diff --git a/src/dash-table/components/Table/derivedPropsHelper.ts b/src/dash-table/components/Table/derivedPropsHelper.ts new file mode 100644 index 000000000..f25ff422b --- /dev/null +++ b/src/dash-table/components/Table/derivedPropsHelper.ts @@ -0,0 +1,107 @@ +import * as R from 'ramda'; + +import { memoizeOneWithFlag } from 'core/memoizer'; + +import QuerySyntaxTree from 'dash-table/syntax-tree/QuerySyntaxTree'; + +import { + ControlledTableProps, + SanitizedAndDerivedProps, + SetProps, + TableAction +} from './props'; + +export default () => { + const filterCache = memoizeOneWithFlag(filter_query => filter_query); + const paginationCache = memoizeOneWithFlag((page_current, page_size) => [page_current, page_size]); + const sortCache = memoizeOneWithFlag(sort => sort); + const viewportCache = memoizeOneWithFlag(viewport => viewport); + const viewportSelectedRowsCache = memoizeOneWithFlag(viewport => viewport); + const virtualCache = memoizeOneWithFlag(virtual => virtual); + const virtualSelectedRowsCache = memoizeOneWithFlag(virtual => virtual); + const structuredQueryCache = memoizeOneWithFlag( + (query: string) => new QuerySyntaxTree(query).toStructure() + ); + + return (props: ControlledTableProps, setProps: SetProps) => { + const { + filter_query, + filter_action, + page_action, + page_current, + page_size, + sort_action, + sort_by, + viewport, + viewport_selected_rows, + virtual, + virtual_selected_rows + } = props; + + const derivedStructureCache = structuredQueryCache(filter_query); + + const viewportCached = viewportCache(viewport).cached; + const virtualCached = virtualCache(virtual).cached; + + const viewportSelectedRowsCached = viewportSelectedRowsCache(viewport_selected_rows).cached; + const virtualSelectedRowsCached = virtualSelectedRowsCache(virtual_selected_rows).cached; + + const invalidatedFilter = filterCache(filter_query); + const invalidatedPagination = paginationCache(page_current, page_size); + const invalidatedSort = sortCache(sort_by); + + const invalidateSelection = + (!invalidatedFilter.cached && !invalidatedFilter.first && filter_action === TableAction.Custom) || + (!invalidatedPagination.cached && !invalidatedPagination.first && page_action === TableAction.Custom) || + (!invalidatedSort.cached && !invalidatedSort.first && sort_action === TableAction.Custom); + + let newProps: Partial = {}; + + if (!derivedStructureCache.cached) { + newProps.derived_filter_structure = derivedStructureCache.result; + } + + if (!virtualCached) { + newProps.derived_virtual_data = virtual.data; + newProps.derived_virtual_indices = virtual.indices; + newProps.derived_virtual_row_ids = R.pluck('id', virtual.data); + } + + if (!viewportCached) { + newProps.derived_viewport_data = viewport.data; + newProps.derived_viewport_indices = viewport.indices; + newProps.derived_viewport_row_ids = R.pluck('id', viewport.data); + } + + if (!virtualSelectedRowsCached) { + newProps.derived_virtual_selected_rows = virtual_selected_rows; + newProps.derived_virtual_selected_row_ids = R.map( + i => virtual.data[i].id, + virtual_selected_rows + ); + } + + if (!viewportSelectedRowsCached) { + newProps.derived_viewport_selected_rows = viewport_selected_rows; + newProps.derived_viewport_selected_row_ids = R.map( + i => viewport.data[i].id, + viewport_selected_rows + ); + } + + if (invalidateSelection) { + newProps.active_cell = undefined; + newProps.selected_cells = []; + newProps.start_cell = undefined; + newProps.end_cell = undefined; + newProps.selected_rows = []; + newProps.selected_row_ids = []; + } + + if (!R.keys(newProps).length) { + return; + } + + setTimeout(() => setProps(newProps), 0); + }; +}; \ No newline at end of file diff --git a/src/dash-table/components/Table/index.tsx b/src/dash-table/components/Table/index.tsx index 18bbf1916..070c927ea 100644 --- a/src/dash-table/components/Table/index.tsx +++ b/src/dash-table/components/Table/index.tsx @@ -5,26 +5,15 @@ import * as R from 'ramda'; import Logger from 'core/Logger'; /*#endif*/ -import { memoizeOne, memoizeOneWithFlag } from 'core/memoizer'; +import { memoizeOne } from 'core/memoizer'; import ControlledTable from 'dash-table/components/ControlledTable'; -import derivedPaginator from 'dash-table/derived/paginator'; -import derivedSelectedRows from 'dash-table/derived/selectedRows'; -import derivedViewportData from 'dash-table/derived/data/viewport'; -import derivedVirtualData from 'dash-table/derived/data/virtual'; -import derivedVirtualizedData from 'dash-table/derived/data/virtualized'; -import derivedVisibleColumns from 'dash-table/derived/column/visible'; - -import QuerySyntaxTree from 'dash-table/syntax-tree/QuerySyntaxTree'; - import { - ControlledTableProps, SetProps, IState, StandaloneState, - SanitizedAndDerivedProps, - TableAction + SanitizedAndDerivedProps } from './props'; import 'react-select/dist/react-select.css'; @@ -34,6 +23,9 @@ import { isEqual } from 'core/comparer'; import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; import derivedFilterMap from 'dash-table/derived/filter/map'; +import controlledPropsHelper from './controlledPropsHelper'; +import derivedPropsHelper from './derivedPropsHelper'; + const DERIVED_REGEX = /^derived_/; export default class Table extends Component { @@ -47,7 +39,7 @@ export default class Table extends Component(), props.filter_query, - props.columns + props.visibleColumns ) }, rawFilterQuery: '', @@ -67,7 +59,7 @@ export default class Table extends Component); - } - - private getControlledProps(): ControlledTableProps { - const { - controlledSetProps: setProps, - controlledSetState: setState - } = this; - - const { - columns, - data, - filter_query, - filter_action, - page_action, - page_current, - page_size, - selected_rows, - sort_action, - sort_by, - uiCell, - uiHeaders, - uiViewport, - virtualization - } = R.merge(this.props, this.state) as (SanitizedAndDerivedProps & StandaloneState); - - const virtual = this.virtual( - columns, - data, - filter_action, - filter_query, - sort_action, - sort_by - ); - - const viewport = this.viewport( - page_action, - page_current, - page_size, - virtual.data, - virtual.indices - ); - - const virtualized = this.virtualized( - virtualization, - uiCell, - uiHeaders, - uiViewport, - viewport - ); - - const virtual_selected_rows = this.virtualSelectedRows( - virtual.indices, - selected_rows - ); - - const viewport_selected_rows = this.viewportSelectedRows( - viewport.indices, - selected_rows - ); - - const paginator = this.paginator( - page_action, - page_current, - page_size, - setProps, - virtual.data - ); - - const visibleColumns = this.visibleColumns(columns); - - return R.mergeAll([ + let controlled = this.controlledPropsHelper( + this.controlledSetProps, + this.controlledSetState, this.props, - this.state, - { - columns: visibleColumns, - paginator, - setProps, - setState, - viewport, - viewport_selected_rows, - virtual, - virtual_selected_rows, - virtualized - } - ]) as ControlledTableProps; - } - - private updateDerivedProps(controlled: ControlledTableProps) { - const { - filter_query, - filter_action, - page_action, - page_current, - page_size, - sort_action, - sort_by, - viewport, - viewport_selected_rows, - virtual, - virtual_selected_rows - } = controlled; - - const derivedStructureCache = this.structuredQueryCache(filter_query); - - const viewportCached = this.viewportCache(viewport).cached; - const virtualCached = this.virtualCache(virtual).cached; - - const viewportSelectedRowsCached = this.viewportSelectedRowsCache(viewport_selected_rows).cached; - const virtualSelectedRowsCached = this.virtualSelectedRowsCache(virtual_selected_rows).cached; - - const invalidatedFilter = this.filterCache(filter_query); - const invalidatedPagination = this.paginationCache(page_current, page_size); - const invalidatedSort = this.sortCache(sort_by); - - const invalidateSelection = - (!invalidatedFilter.cached && !invalidatedFilter.first && filter_action === TableAction.Custom) || - (!invalidatedPagination.cached && !invalidatedPagination.first && page_action === TableAction.Custom) || - (!invalidatedSort.cached && !invalidatedSort.first && sort_action === TableAction.Custom); - - const { controlledSetProps } = this; - let newProps: Partial = {}; - - if (!derivedStructureCache.cached) { - newProps.derived_filter_structure = derivedStructureCache.result; - } - - if (!virtualCached) { - newProps.derived_virtual_data = virtual.data; - newProps.derived_virtual_indices = virtual.indices; - newProps.derived_virtual_row_ids = R.pluck('id', virtual.data); - } - - if (!viewportCached) { - newProps.derived_viewport_data = viewport.data; - newProps.derived_viewport_indices = viewport.indices; - newProps.derived_viewport_row_ids = R.pluck('id', viewport.data); - } - - if (!virtualSelectedRowsCached) { - newProps.derived_virtual_selected_rows = virtual_selected_rows; - newProps.derived_virtual_selected_row_ids = R.map( - i => virtual.data[i].id, - virtual_selected_rows - ); - } - - if (!viewportSelectedRowsCached) { - newProps.derived_viewport_selected_rows = viewport_selected_rows; - newProps.derived_viewport_selected_row_ids = R.map( - i => viewport.data[i].id, - viewport_selected_rows - ); - } - - if (invalidateSelection) { - newProps.active_cell = undefined; - newProps.selected_cells = []; - newProps.start_cell = undefined; - newProps.end_cell = undefined; - newProps.selected_rows = []; - newProps.selected_row_ids = []; - } + this.state + ); - if (!R.keys(newProps).length) { - return; - } + this.updateDerivedProps(controlled, this.controlledSetProps); - setTimeout(() => controlledSetProps(newProps), 0); + return (); } private get controlledSetProps() { @@ -304,22 +134,7 @@ export default class Table extends Component (state: Partial) => this.setState(state as IState)); private readonly filterMap = derivedFilterMap(); - private readonly paginator = derivedPaginator(); - private readonly viewport = derivedViewportData(); - private readonly viewportSelectedRows = derivedSelectedRows(); - private readonly virtual = derivedVirtualData(); - private readonly virtualSelectedRows = derivedSelectedRows(); - private readonly virtualized = derivedVirtualizedData(); - private readonly visibleColumns = derivedVisibleColumns(); - 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); - private readonly virtualCache = memoizeOneWithFlag(virtual => virtual); - private readonly virtualSelectedRowsCache = memoizeOneWithFlag(virtual => virtual); - private readonly structuredQueryCache = memoizeOneWithFlag( - (query: string) => new QuerySyntaxTree(query).toStructure() - ); + private readonly controlledPropsHelper = controlledPropsHelper(); + private readonly updateDerivedProps = derivedPropsHelper(); } diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 3cd22622e..7a94f225e 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -74,7 +74,6 @@ export type SelectedCells = ICellCoordinates[]; export type SetProps = (...args: any[]) => void; export type SetState = (state: Partial) => void; export type SortAsNull = (string | number | boolean)[]; -export type VisibleColumns = IVisibleColumn[]; export enum ChangeAction { Coerce = 'coerce', @@ -153,7 +152,7 @@ export interface IDatetimeColumn extends ITypeColumn { validation?: IDateValidation; } -export interface IBaseVisibleColumn { +export interface IBaseColumn { deletable?: boolean | boolean[]; editable: boolean; renamable?: boolean | boolean[]; @@ -169,11 +168,7 @@ 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; - -export type IColumn = IVisibleColumn & { - hidden?: boolean; -}; +export type IColumn = IBaseColumn & IColumnType; interface IDatumObject { [key: string]: any; @@ -274,6 +269,7 @@ export interface IProps { fill_width?: boolean; filter_query?: string; filter_action?: TableAction; + hidden_columns?: string[]; locale_format: INumberLocale; merge_duplicate_headers?: boolean; fixed_columns?: Fixed; @@ -369,13 +365,14 @@ interface IDerivedProps { export type PropsWithDefaults = IProps & IDefaultProps; -export type SanitizedProps = Omit< - Omit< - Merge, - 'locale_format' - >, - 'sort_as_null' ->; +export type SanitizedProps = Omit, + 'locale_format'>, + 'sort_as_null'>; export type SanitizedAndDerivedProps = SanitizedProps & IDerivedProps; @@ -383,7 +380,6 @@ export type ControlledTableProps = SanitizedProps & IState & { setProps: SetProps; setState: SetState; - columns: VisibleColumns; currentTooltip: IUSerInterfaceTooltip; paginator: IPaginator; viewport: IDerivedData; @@ -395,7 +391,6 @@ export type ControlledTableProps = SanitizedProps & IState & { export interface ICellFactoryProps { active_cell: ICellCoordinates; - columns: VisibleColumns; dropdown: StaticDropdowns; dropdown_conditional: ConditionalDropdowns; dropdown_data: DataDropdowns; @@ -403,19 +398,19 @@ export interface ICellFactoryProps { currentTooltip: IUSerInterfaceTooltip; data: Data; editable: boolean; - id: string; - is_focused?: boolean; + end_cell: ICellCoordinates; fixed_columns: number; fixed_rows: number; + id: string; + is_focused?: boolean; paginator: IPaginator; row_deletable: boolean; row_selectable: RowSelection; selected_cells: SelectedCells; - start_cell: ICellCoordinates; - end_cell: ICellCoordinates; selected_rows: Indices; setProps: SetProps; setState: SetState; + start_cell: ICellCoordinates; style_cell: Style; style_data: Style; style_filter: Style; @@ -431,4 +426,5 @@ export interface ICellFactoryProps { viewport: IDerivedData; virtualization: boolean; virtualized: IVirtualizedDerivedData; + visibleColumns: Columns; } diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index a0326c7b4..92f2f5691 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -1,6 +1,6 @@ import * as R from 'ramda'; -import { ColumnId, Datum, ColumnType, IVisibleColumn } from 'dash-table/components/Table/props'; +import { ColumnId, Datum, ColumnType, IColumn } from 'dash-table/components/Table/props'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; import { IConvertedStyle } from 'dash-table/derived/style'; @@ -89,17 +89,17 @@ export function ifEditable(condition: IEditableElement | undefined, isEditable: export type Filter = (s: T[]) => T[]; -export const matchesDataCell = (datum: Datum, i: number, column: IVisibleColumn): Filter => R.filter((style => +export const matchesDataCell = (datum: Datum, i: number, column: IColumn): Filter => R.filter((style => style.matchesRow(i) && style.matchesColumn(column) && style.matchesFilter(datum) )); -export const matchesFilterCell = (column: IVisibleColumn): Filter => R.filter((style => +export const matchesFilterCell = (column: IColumn): Filter => R.filter((style => style.matchesColumn(column) )); -export const matchesHeaderCell = (i: number, column: IVisibleColumn): Filter => R.filter((style => +export const matchesHeaderCell = (i: number, column: IColumn): Filter => R.filter((style => style.matchesRow(i) && style.matchesColumn(column) )); diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index e796286d5..f91879be3 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -423,6 +423,8 @@ export const propTypes = { */ fill_width: PropTypes.bool, + hidden_columns: PropTypes.arrayOf(PropTypes.string), + /** * The ID of the table. */ diff --git a/src/dash-table/dash/Sanitizer.ts b/src/dash-table/dash/Sanitizer.ts index ea35b471d..a7771a48c 100644 --- a/src/dash-table/dash/Sanitizer.ts +++ b/src/dash-table/dash/Sanitizer.ts @@ -62,21 +62,31 @@ const applyDefaultsToColumns = (defaultLocale: INumberLocale, defaultSort: SortA const applyDefaultToLocale = (locale: INumberLocale) => getLocale(locale); +const getVisibleColumns = ( + columns: Columns, + hiddenColumns: string[] | undefined +) => R.filter(column => !hiddenColumns || hiddenColumns.indexOf(column.id) < 0, columns); + export default class Sanitizer { sanitize(props: PropsWithDefaults): SanitizedProps { const locale_format = this.applyDefaultToLocale(props.locale_format); + const columns = this.applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable); + const visibleColumns = this.getVisibleColumns(columns, props.hidden_columns); return R.merge(props, { - columns: this.applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable), + 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 + locale_format, + visibleColumns }); } private readonly applyDefaultToLocale = memoizeOne(applyDefaultToLocale); private readonly applyDefaultsToColumns = memoizeOne(applyDefaultsToColumns); + + private readonly getVisibleColumns = memoizeOne(getVisibleColumns); } export const getLocale = (...locales: Partial[]): INumberLocale => diff --git a/src/dash-table/derived/cell/contents.tsx b/src/dash-table/derived/cell/contents.tsx index f4a18335f..3539546f2 100644 --- a/src/dash-table/derived/cell/contents.tsx +++ b/src/dash-table/derived/cell/contents.tsx @@ -9,9 +9,9 @@ import { IDropdown, IDropdownValue, IViewportOffset, - IVisibleColumn, + IColumn, Presentation, - VisibleColumns + Columns } from 'dash-table/components/Table/props'; import CellInput from 'dash-table/components/CellInput'; import derivedCellEventHandlerProps, { Handler } from 'dash-table/derived/cell/eventHandlerProps'; @@ -22,7 +22,7 @@ import getFormatter from 'dash-table/type/formatter'; import { shallowClone } from 'core/math/matrixZipMap'; const mapData = R.addIndex(R.map); -const mapRow = R.addIndex(R.map); +const mapRow = R.addIndex(R.map); enum CellType { Dropdown, @@ -57,7 +57,7 @@ class Contents { } partialGet = memoizeOne(( - columns: VisibleColumns, + columns: Columns, data: Data, _offset: IViewportOffset, isFocused: boolean, @@ -82,7 +82,7 @@ class Contents { get = memoizeOne(( contents: JSX.Element[][], activeCell: ICellCoordinates | undefined, - columns: VisibleColumns, + columns: Columns, data: Data, offset: IViewportOffset, isFocused: boolean, @@ -117,7 +117,7 @@ class Contents { return contents; }); - private getContent(active: boolean, isFocused: boolean, column: IVisibleColumn, dropdown: IDropdown | undefined, columnIndex: number, rowIndex: number, datum: any, formatters: ((value: any) => any)[]) { + private getContent(active: boolean, isFocused: boolean, column: IColumn, dropdown: IDropdown | undefined, columnIndex: number, rowIndex: number, datum: any, formatters: ((value: any) => any)[]) { const className = [ ...(active ? ['input-active'] : []), isFocused ? 'focused' : 'unfocused', diff --git a/src/dash-table/derived/cell/dropdowns.ts b/src/dash-table/derived/cell/dropdowns.ts index fa6cfb364..00b1cc5c4 100644 --- a/src/dash-table/derived/cell/dropdowns.ts +++ b/src/dash-table/derived/cell/dropdowns.ts @@ -12,9 +12,9 @@ import { IConditionalDropdown, IDropdown, Indices, - IVisibleColumn, + IColumn, StaticDropdowns, - VisibleColumns + Columns } from 'dash-table/components/Table/props'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; import { ifColumnId } from 'dash-table/conditional'; @@ -28,7 +28,7 @@ class Dropdowns { * Return the dropdown for each cell in the table. */ get = memoizeOne(( - columns: VisibleColumns, + columns: Columns, data: Data, indices: Indices, conditionalDropdowns: ConditionalDropdowns, @@ -59,7 +59,7 @@ class Dropdowns { private readonly dropdown = memoizerCache<[ColumnId, number]>()(( base: IDropdown | undefined, conditionals: ConditionalDropdowns, - column: IVisibleColumn, + column: IColumn, datum: Datum ) => { const conditional = R.findLast( diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 7acfe6983..7ebc7ff22 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -2,14 +2,14 @@ import * as R from 'ramda'; import { CSSProperties } from 'react'; import { memoizeOneFactory } from 'core/memoizer'; -import { Data, VisibleColumns, IViewportOffset, SelectedCells } from 'dash-table/components/Table/props'; +import { Data, Columns, IViewportOffset, SelectedCells } from 'dash-table/components/Table/props'; import { IConvertedStyle, getDataCellStyle, getDataOpCellStyle } from '../style'; import { traverseMap2, shallowClone } from 'core/math/matrixZipMap'; const SELECTED_CELL_STYLE = { backgroundColor: 'var(--selected-background)' }; const partialGetter = ( - columns: VisibleColumns, + columns: Columns, styles: IConvertedStyle[], data: Data, offset: IViewportOffset diff --git a/src/dash-table/derived/cell/wrappers.tsx b/src/dash-table/derived/cell/wrappers.tsx index 9533d6314..715a7bdd0 100644 --- a/src/dash-table/derived/cell/wrappers.tsx +++ b/src/dash-table/derived/cell/wrappers.tsx @@ -3,7 +3,7 @@ import React, { MouseEvent } from 'react'; import { memoizeOne } from 'core/memoizer'; import memoizerCache from 'core/cache/memoizer'; -import { Data, IVisibleColumn, VisibleColumns, ICellCoordinates, SelectedCells, Datum, ColumnId, IViewportOffset, Presentation, ICellFactoryProps } from 'dash-table/components/Table/props'; +import { Data, IColumn, Columns, ICellCoordinates, SelectedCells, Datum, ColumnId, IViewportOffset, Presentation, ICellFactoryProps } from 'dash-table/components/Table/props'; import Cell from 'dash-table/components/Cell'; import derivedCellEventHandlerProps, { Handler } from 'dash-table/derived/cell/eventHandlerProps'; import isActiveCell from 'dash-table/derived/cell/isActive'; @@ -20,11 +20,11 @@ class Wrappers { } partialGet = memoizeOne(( - columns: VisibleColumns, + columns: Columns, data: Data, _offset: IViewportOffset ) => R.addIndex(R.map)( - (_, rowIndex) => R.addIndex(R.map)( + (_, rowIndex) => R.addIndex(R.map)( (column, columnIndex) => this.getWrapper( false, false, @@ -69,7 +69,7 @@ class Wrappers { selected: boolean, rowIndex: number, columnIndex: number, - column: IVisibleColumn + column: IColumn ) { const isDropdown = column.presentation === Presentation.Dropdown; const classes = 'dash-cell' + diff --git a/src/dash-table/derived/column/visible.ts b/src/dash-table/derived/column/visible.ts deleted file mode 100644 index 9d5d38d18..000000000 --- a/src/dash-table/derived/column/visible.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as R from 'ramda'; - -import { memoizeOneFactory } from 'core/memoizer'; -import { Columns, VisibleColumns } from 'dash-table/components/Table/props'; - -const getter = (columns: Columns): VisibleColumns => R.filter(column => !column.hidden, columns); - -export default memoizeOneFactory(getter); diff --git a/src/dash-table/derived/data/virtual.ts b/src/dash-table/derived/data/virtual.ts index 186057541..6a388ba38 100644 --- a/src/dash-table/derived/data/virtual.ts +++ b/src/dash-table/derived/data/virtual.ts @@ -8,13 +8,13 @@ import { Datum, IDerivedData, SortAsNull, - VisibleColumns, + Columns, TableAction } from 'dash-table/components/Table/props'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; const getter = ( - columns: VisibleColumns, + columns: Columns, data: Data, filter_action: TableAction, filter_query: string, diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index 44497e05c..d97169f74 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -3,7 +3,7 @@ import { memoizeOneFactory } from 'core/memoizer'; import { IViewportOffset, - VisibleColumns, + Columns, Data, ICellCoordinates } from 'dash-table/components/Table/props'; @@ -14,7 +14,7 @@ import { getDataCellEdges } from '.'; import { traverse2 } from 'core/math/matrixZipMap'; export default memoizeOneFactory(( - columns: VisibleColumns, + columns: Columns, styles: IConvertedStyle[], data: Data, offset: IViewportOffset, diff --git a/src/dash-table/derived/edges/filter.ts b/src/dash-table/derived/edges/filter.ts index 5ff35fcac..8f1f64212 100644 --- a/src/dash-table/derived/edges/filter.ts +++ b/src/dash-table/derived/edges/filter.ts @@ -4,7 +4,7 @@ import Environment from 'core/environment'; import { memoizeOneFactory } from 'core/memoizer'; import { - VisibleColumns + Columns } from 'dash-table/components/Table/props'; import { IConvertedStyle } from '../style'; @@ -14,7 +14,7 @@ import { getFilterCellEdges } from '.'; import { traverse2 } from 'core/math/matrixZipMap'; export default memoizeOneFactory(( - columns: VisibleColumns, + columns: Columns, showFilters: boolean, map: Map, styles: IConvertedStyle[], diff --git a/src/dash-table/derived/edges/header.ts b/src/dash-table/derived/edges/header.ts index 757109615..09f53f64c 100644 --- a/src/dash-table/derived/edges/header.ts +++ b/src/dash-table/derived/edges/header.ts @@ -4,7 +4,7 @@ import Environment from 'core/environment'; import { memoizeOneFactory } from 'core/memoizer'; import { - VisibleColumns + Columns } from 'dash-table/components/Table/props'; import { IConvertedStyle } from '../style'; @@ -13,7 +13,7 @@ import { getHeaderCellEdges } from '.'; import { traverse2 } from 'core/math/matrixZipMap'; export default memoizeOneFactory(( - columns: VisibleColumns, + columns: Columns, headerRows: number, styles: IConvertedStyle[], listViewStyle: boolean diff --git a/src/dash-table/derived/edges/index.ts b/src/dash-table/derived/edges/index.ts index 893b3a3ce..c323c4d15 100644 --- a/src/dash-table/derived/edges/index.ts +++ b/src/dash-table/derived/edges/index.ts @@ -1,6 +1,6 @@ import { BorderStyle, BORDER_PROPERTIES } from './type'; import { IConvertedStyle } from '../style'; -import { Datum, IVisibleColumn } from 'dash-table/components/Table/props'; +import { Datum, IColumn } from 'dash-table/components/Table/props'; import { matchesDataCell, matchesDataOpCell, matchesFilterCell, getFilterOpStyles, matchesHeaderCell, getHeaderOpStyles } from 'dash-table/conditional'; import { traverse2 } from 'core/math/matrixZipMap'; @@ -22,9 +22,9 @@ function resolveEdges(styles: IConvertedStyle[]): BorderStyle { return res; } -export const getDataCellEdges = (datum: Datum, i: number, column: IVisibleColumn) => (styles: IConvertedStyle[]) => resolveEdges(matchesDataCell(datum, i, column)(styles)); +export const getDataCellEdges = (datum: Datum, i: number, column: IColumn) => (styles: IConvertedStyle[]) => resolveEdges(matchesDataCell(datum, i, column)(styles)); export const getDataOpCellEdges = (datum: Datum, i: number) => (styles: IConvertedStyle[]) => resolveEdges(matchesDataOpCell(datum, i)(styles)); -export const getFilterCellEdges = (column: IVisibleColumn) => (styles: IConvertedStyle[]) => resolveEdges(matchesFilterCell(column)(styles)); +export const getFilterCellEdges = (column: IColumn) => (styles: IConvertedStyle[]) => resolveEdges(matchesFilterCell(column)(styles)); export const getFilterOpCellEdges = () => (styles: IConvertedStyle[]) => resolveEdges(getFilterOpStyles(styles)); -export const getHeaderCellEdges = (i: number, column: IVisibleColumn) => (styles: IConvertedStyle[]) => resolveEdges(matchesHeaderCell(i, column)(styles)); +export const getHeaderCellEdges = (i: number, column: IColumn) => (styles: IConvertedStyle[]) => resolveEdges(matchesHeaderCell(i, column)(styles)); export const getHeaderOpCellEdges = (i: number) => (styles: IConvertedStyle[]) => resolveEdges(getHeaderOpStyles(i)(styles)); \ No newline at end of file diff --git a/src/dash-table/derived/filter/map.ts b/src/dash-table/derived/filter/map.ts index 88787701b..6f86ee112 100644 --- a/src/dash-table/derived/filter/map.ts +++ b/src/dash-table/derived/filter/map.ts @@ -2,7 +2,7 @@ import * as R from 'ramda'; import { memoizeOneFactory } from 'core/memoizer'; -import { VisibleColumns, IVisibleColumn } from 'dash-table/components/Table/props'; +import { Columns, IColumn } from 'dash-table/components/Table/props'; import { SingleColumnSyntaxTree, MultiColumnsSyntaxTree, getSingleColumnMap } from 'dash-table/syntax-tree'; const cloneIf = ( @@ -13,7 +13,7 @@ const cloneIf = ( export default memoizeOneFactory(( map: Map, query: string, - columns: VisibleColumns + columns: Columns ): Map => { const multiQuery = new MultiColumnsSyntaxTree(query); const reversedMap = getSingleColumnMap(multiQuery, columns); @@ -63,7 +63,7 @@ export default memoizeOneFactory(( export const updateMap = ( map: Map, - column: IVisibleColumn, + column: IColumn, value: any ): Map => { const safeColumnId = column.id.toString(); diff --git a/src/dash-table/derived/filter/wrapperStyles.ts b/src/dash-table/derived/filter/wrapperStyles.ts index b09955367..ce55600f1 100644 --- a/src/dash-table/derived/filter/wrapperStyles.ts +++ b/src/dash-table/derived/filter/wrapperStyles.ts @@ -2,13 +2,13 @@ import * as R from 'ramda'; import { memoizeOneFactory } from 'core/memoizer'; -import { VisibleColumns } from 'dash-table/components/Table/props'; +import { Columns } from 'dash-table/components/Table/props'; import { IConvertedStyle, getFilterCellStyle, getFilterOpCellStyle } from '../style'; import { traverseMap2 } from 'core/math/matrixZipMap'; const getter = ( - columns: VisibleColumns, + columns: Columns, filterStyles: IConvertedStyle[] ) => R.map( column => getFilterCellStyle(column)(filterStyles), diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 947a4348f..dab551062 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -10,14 +10,14 @@ import { ColumnId, Data, SortMode, - VisibleColumns, - IVisibleColumn, + Columns, + IColumn, SetProps, TableAction } from 'dash-table/components/Table/props'; import * as actions from 'dash-table/utils/actions'; -function deleteColumn(column: IVisibleColumn, columns: VisibleColumns, columnRowIndex: any, setProps: SetProps, data: Data) { +function deleteColumn(column: IColumn, columns: Columns, columnRowIndex: any, setProps: SetProps, data: Data) { return () => { setProps(actions.deleteColumn(column, columns, columnRowIndex, data)); }; @@ -55,7 +55,7 @@ function doSort(columnId: ColumnId, sortBy: SortBy, mode: SortMode, setProps: Se }; } -function editColumnName(column: IVisibleColumn, columns: VisibleColumns, columnRowIndex: any, setProps: SetProps, mergeDuplicateHeaders: boolean) { +function editColumnName(column: IColumn, columns: Columns, columnRowIndex: any, setProps: SetProps, mergeDuplicateHeaders: boolean) { return () => { setProps(actions.editColumnName(column, columns, columnRowIndex, mergeDuplicateHeaders)); }; @@ -80,7 +80,7 @@ function getSortingIcon(columnId: ColumnId, sortBy: SortBy) { } function getter( - columns: VisibleColumns, + columns: Columns, data: Data, labelsAndIndices: R.KeyValuePair[], sort_action: TableAction, diff --git a/src/dash-table/derived/header/headerRows.ts b/src/dash-table/derived/header/headerRows.ts index 45e881085..d48cd67ea 100644 --- a/src/dash-table/derived/header/headerRows.ts +++ b/src/dash-table/derived/header/headerRows.ts @@ -1,10 +1,8 @@ import { IColumn } from 'dash-table/components/Table/props'; -const getColLength = (c: IColumn) => c.hidden ? - 0 : - Array.isArray(c.name) ? - c.name.length : - 1; +const getColLength = (c: IColumn) => Array.isArray(c.name) ? + c.name.length : + 1; export default ( columns: IColumn[] diff --git a/src/dash-table/derived/header/indices.ts b/src/dash-table/derived/header/indices.ts index e3e2f3212..ebf009c0f 100644 --- a/src/dash-table/derived/header/indices.ts +++ b/src/dash-table/derived/header/indices.ts @@ -1,9 +1,9 @@ import * as R from 'ramda'; -import { VisibleColumns } from 'dash-table/components/Table/props'; +import { Columns } from 'dash-table/components/Table/props'; export default ( - columns: VisibleColumns, + columns: Columns, labels: any[][], mergeHeaders: boolean ): number[][] => { diff --git a/src/dash-table/derived/header/labels.ts b/src/dash-table/derived/header/labels.ts index 5ca394d95..c1334deb4 100644 --- a/src/dash-table/derived/header/labels.ts +++ b/src/dash-table/derived/header/labels.ts @@ -1,11 +1,11 @@ import * as R from 'ramda'; -import { VisibleColumns } from 'dash-table/components/Table/props'; +import { Columns } from 'dash-table/components/Table/props'; const getColNameAt = (c: any, i: number) => (Array.isArray(c.name) ? c.name[i] : c.name); export default ( - columns: VisibleColumns, + columns: Columns, headerRows: number ): any[][] => { return R.map( diff --git a/src/dash-table/derived/header/wrapperStyles.ts b/src/dash-table/derived/header/wrapperStyles.ts index 2293e1835..b91a700aa 100644 --- a/src/dash-table/derived/header/wrapperStyles.ts +++ b/src/dash-table/derived/header/wrapperStyles.ts @@ -2,13 +2,13 @@ import * as R from 'ramda'; import { memoizeOneFactory } from 'core/memoizer'; -import { VisibleColumns } from 'dash-table/components/Table/props'; +import { Columns } from 'dash-table/components/Table/props'; import { IConvertedStyle, getHeaderCellStyle, getHeaderOpCellStyle } from '../style'; import { traverseMap2 } from 'core/math/matrixZipMap'; const getter = ( - columns: VisibleColumns, + columns: Columns, headerRows: number, headerStyles: IConvertedStyle[] ) => traverseMap2( diff --git a/src/dash-table/derived/header/wrappers.tsx b/src/dash-table/derived/header/wrappers.tsx index c18c34a5e..06d0c60ec 100644 --- a/src/dash-table/derived/header/wrappers.tsx +++ b/src/dash-table/derived/header/wrappers.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { memoizeOneFactory } from 'core/memoizer'; -import { VisibleColumns } from 'dash-table/components/Table/props'; +import { Columns } from 'dash-table/components/Table/props'; function getter( - columns: VisibleColumns, + columns: Columns, labelsAndIndices: R.KeyValuePair[], mergeHeaders: boolean ): JSX.Element[][] { diff --git a/src/dash-table/derived/style/index.ts b/src/dash-table/derived/style/index.ts index ed3df3993..b885234d9 100644 --- a/src/dash-table/derived/style/index.ts +++ b/src/dash-table/derived/style/index.ts @@ -3,7 +3,7 @@ import { CSSProperties } from 'react'; import { memoizeOneFactory } from 'core/memoizer'; -import { Datum, IVisibleColumn } from 'dash-table/components/Table/props'; +import { Datum, IColumn } from 'dash-table/components/Table/props'; import { Cells, @@ -35,7 +35,7 @@ export interface IConvertedStyle { checksColumn: () => boolean; checksRow: () => boolean; checksFilter: () => boolean; - matchesColumn: (column: IVisibleColumn | undefined) => boolean; + matchesColumn: (column: IColumn | undefined) => boolean; matchesRow: (index: number | undefined) => boolean; matchesFilter: (datum: Datum) => boolean; } @@ -56,7 +56,7 @@ function convertElement(style: GenericStyle): IConvertedStyle { checksRow: () => !R.isNil(indexFilter), checksFilter: () => !R.isNil(style.if) && !R.isNil(style.if.filter_query), - matchesColumn: (column: IVisibleColumn | undefined) => + matchesColumn: (column: IColumn | undefined) => !style.if || ( !R.isNil(column) && ifColumnId(style.if, column && column.id) && @@ -139,9 +139,9 @@ export function resolveStyle(styles: IConvertedStyle[]): CSSProperties { return R.omit(BORDER_PROPERTIES_AND_FRAGMENTS, res); } -export const getDataCellStyle = (datum: Datum, i: number, column: IVisibleColumn) => (styles: IConvertedStyle[]) => resolveStyle(matchesDataCell(datum, i, column)(styles)); +export const getDataCellStyle = (datum: Datum, i: number, column: IColumn) => (styles: IConvertedStyle[]) => resolveStyle(matchesDataCell(datum, i, column)(styles)); export const getDataOpCellStyle = (datum: Datum, i: number) => (styles: IConvertedStyle[]) => resolveStyle(matchesDataOpCell(datum, i)(styles)); -export const getFilterCellStyle = (column: IVisibleColumn) => (styles: IConvertedStyle[]) => resolveStyle(matchesFilterCell(column)(styles)); +export const getFilterCellStyle = (column: IColumn) => (styles: IConvertedStyle[]) => resolveStyle(matchesFilterCell(column)(styles)); export const getFilterOpCellStyle = () => (styles: IConvertedStyle[]) => resolveStyle(getFilterOpStyles(styles)); -export const getHeaderCellStyle = (i: number, column: IVisibleColumn) => (styles: IConvertedStyle[]) => resolveStyle(matchesHeaderCell(i, column)(styles)); +export const getHeaderCellStyle = (i: number, column: IColumn) => (styles: IConvertedStyle[]) => resolveStyle(matchesHeaderCell(i, column)(styles)); export const getHeaderOpCellStyle = (i: number) => (styles: IConvertedStyle[]) => resolveStyle(getHeaderOpStyles(i)(styles)); diff --git a/src/dash-table/handlers/cellEvents.ts b/src/dash-table/handlers/cellEvents.ts index bf5e89b2a..e3f3e9875 100644 --- a/src/dash-table/handlers/cellEvents.ts +++ b/src/dash-table/handlers/cellEvents.ts @@ -10,15 +10,15 @@ export const handleClick = (propsFn: () => ICellFactoryProps, idx: number, i: nu selected_cells, active_cell, setProps, + viewport, virtualized, - columns, - viewport + visibleColumns } = propsFn(); const row = idx + virtualized.offset.rows; const col = i + virtualized.offset.columns; - const clickedCell = makeCell(row, col, columns, viewport); + const clickedCell = makeCell(row, col, visibleColumns, viewport); // clicking again on the already-active cell: ignore if (active_cell && row === active_cell.row && col === active_cell.column) { @@ -66,7 +66,7 @@ export const handleClick = (propsFn: () => ICellFactoryProps, idx: number, i: nu minCol: min(col, active_cell.column), maxCol: max(col, active_cell.column) }, - columns, + visibleColumns, viewport ); } else { @@ -82,12 +82,12 @@ export const handleDoubleClick = (propsFn: () => ICellFactoryProps, idx: number, const { is_focused, setProps, + viewport, virtualized, - columns, - viewport + visibleColumns } = propsFn(); - const c = columns[i]; + const c = visibleColumns[i]; if (!c.editable) { return; @@ -96,7 +96,7 @@ export const handleDoubleClick = (propsFn: () => ICellFactoryProps, idx: number, const newCell = makeCell( idx + virtualized.offset.rows, i + virtualized.offset.columns, - columns, viewport + visibleColumns, viewport ); if (!is_focused) { @@ -114,13 +114,13 @@ export const handleDoubleClick = (propsFn: () => ICellFactoryProps, idx: number, export const handleChange = (propsFn: () => ICellFactoryProps, idx: number, i: number, value: any) => { const { - columns, data, setProps, - virtualized + virtualized, + visibleColumns } = propsFn(); - const c = columns[i]; + const c = visibleColumns[i]; const realIdx = virtualized.indices[idx]; if (!c.editable) { @@ -146,12 +146,12 @@ export const handleChange = (propsFn: () => ICellFactoryProps, idx: number, i: n export const handleEnter = (propsFn: () => ICellFactoryProps, idx: number, i: number) => { const { - columns, + setState, virtualized, - setState + visibleColumns } = propsFn(); - const c = columns[i]; + const c = visibleColumns[i]; const realIdx = virtualized.indices[idx]; setState({ @@ -172,13 +172,13 @@ export const handleLeave = (propsFn: () => ICellFactoryProps, _idx: number, _i: export const handleMove = (propsFn: () => ICellFactoryProps, idx: number, i: number) => { const { - columns, currentTooltip, + setState, virtualized, - setState + visibleColumns } = propsFn(); - const c = columns[i]; + const c = visibleColumns[i]; const realIdx = virtualized.indices[idx]; if (currentTooltip && currentTooltip.id === c.id && currentTooltip.row === realIdx) { diff --git a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts index 04482da3f..f5ecd81c6 100644 --- a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts +++ b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts @@ -3,7 +3,7 @@ import SyntaxTree from 'core/syntax-tree'; import { ILexemeResult, ILexerResult } from 'core/syntax-tree/lexer'; import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon'; -import { ColumnType, IVisibleColumn } from 'dash-table/components/Table/props'; +import { ColumnType, IColumn } from 'dash-table/components/Table/props'; import { fieldExpression } from './lexeme/expression'; import { equal, RelationalOperator } from './lexeme/relational'; @@ -60,7 +60,7 @@ function modifyLex(config: SingleColumnConfig, res: ILexerResult) { return res; } -export type SingleColumnConfig = RequiredPluck & OptionalPluck; +export type SingleColumnConfig = RequiredPluck & OptionalPluck; export default class SingleColumnSyntaxTree extends SyntaxTree { constructor(query: string, config: SingleColumnConfig) { diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index 46d307751..e4d03607c 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -6,7 +6,7 @@ import MultiColumnsSyntaxTree from './MultiColumnsSyntaxTree'; import QuerySyntaxTree from './QuerySyntaxTree'; import SingleColumnSyntaxTree from './SingleColumnSyntaxTree'; import { RelationalOperator } from './lexeme/relational'; -import { IVisibleColumn } from 'dash-table/components/Table/props'; +import { IColumn } from 'dash-table/components/Table/props'; export const getMultiColumnQueryString = ( asts: SingleColumnSyntaxTree[] @@ -17,7 +17,7 @@ export const getMultiColumnQueryString = ( export const getSingleColumnMap = ( ast: MultiColumnsSyntaxTree, - columns: IVisibleColumn[] + columns: IColumn[] ) => { if (!ast.isValid) { return; diff --git a/src/dash-table/utils/actions.js b/src/dash-table/utils/actions.js index 76affd6d0..2b0f5d8fc 100644 --- a/src/dash-table/utils/actions.js +++ b/src/dash-table/utils/actions.js @@ -5,6 +5,7 @@ function getGroupedColumnIndices(column, columns, headerRowIndex, mergeDuplicate if (!column.name || (Array.isArray(column.name) && column.name.length < headerRowIndex) || !mergeDuplicateHeaders) { return { groupIndexFirst: columnIndex, groupIndexLast: columnIndex }; } + let lastColumnIndex = columnIndex; for (let i = columnIndex; i < columns.length; ++i) { @@ -16,6 +17,7 @@ function getGroupedColumnIndices(column, columns, headerRowIndex, mergeDuplicate break; } } + return { groupIndexFirst: columnIndex, groupIndexLast: lastColumnIndex }; } @@ -57,7 +59,7 @@ export function changeColumnHeader(column, columns, headerRowIndex, mergeDuplica if (typeof column.name === 'string' && maxLength > 1) { const newColumnNames = Array(maxLength).fill(column.name); - const cloneColumn = R.mergeRight(column, {name: newColumnNames}); + const cloneColumn = R.mergeRight(column, { name: newColumnNames }); newColumns = newColumns.slice(0); newColumns[columnIndex] = cloneColumn; } @@ -74,7 +76,7 @@ export function changeColumnHeader(column, columns, headerRowIndex, mergeDuplica newColumns = R.set(R.lensPath(namePath), newColumnName, newColumns); }); - return { columns: newColumns} ; + return { columns: newColumns }; } export function editColumnName(column, columns, headerRowIndex, mergeDuplicateHeaders) { From 5fb388aaa87887e8bb730ae22e5870003f9d2fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 18 Jul 2019 08:45:02 -0400 Subject: [PATCH 02/32] react 16 in index.html --- dash_table/index.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dash_table/index.html b/dash_table/index.html index 5fb27b747..613c17016 100644 --- a/dash_table/index.html +++ b/dash_table/index.html @@ -5,11 +5,9 @@
- - + + - From 6c68d4421eb49422f421a3a249f4e3cd621e571a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 18 Jul 2019 10:29:13 -0400 Subject: [PATCH 03/32] update hidden columns tests --- .../percy-storybook/DashTable.percy.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/visual/percy-storybook/DashTable.percy.tsx b/tests/visual/percy-storybook/DashTable.percy.tsx index 64916f4cc..5924ff438 100644 --- a/tests/visual/percy-storybook/DashTable.percy.tsx +++ b/tests/visual/percy-storybook/DashTable.percy.tsx @@ -200,38 +200,34 @@ const sparseData = (() => { )); })(); -const hiddenColumns = R.addIndex(R.map)((column, index) => - R.mergeAll([ - {}, - column, - { hidden: index % 2 === 0 } - ]), - columnsA2J -); +const hiddenColumns = columnsA2J.map(c => c.id).filter((_, index) => index % 2 === 0); storiesOf('DashTable/Hidden Columns', module) .add('hides', () => ()) .add('active cell', () => ()) .add('selected cells', () => ()); From 80c33a603d5ced9bcd432ee9aa11637b482dcadb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 18 Jul 2019 12:46:03 -0400 Subject: [PATCH 04/32] add icons --- package.json | 3 ++ src/dash-table/components/Table/Table.less | 31 +++++++++--- .../Table/font-awesome-polyfills.less | 47 +++++++++++++++++++ src/dash-table/components/Table/index.tsx | 1 + src/dash-table/components/Table/style.ts | 17 +++++++ src/dash-table/derived/header/content.tsx | 12 ++--- 6 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 src/dash-table/components/Table/font-awesome-polyfills.less create mode 100644 src/dash-table/components/Table/style.ts diff --git a/package.json b/package.json index 0f7311bcc..3fcfbfff9 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,9 @@ "@babel/preset-env": "^7.5.4", "@babel/preset-react": "^7.0.0", "@cypress/webpack-preprocessor": "^4.1.0", + "@fortawesome/fontawesome-svg-core": "^1.2.19", + "@fortawesome/free-regular-svg-icons": "^5.9.0", + "@fortawesome/free-solid-svg-icons": "^5.9.0", "@percy-io/percy-storybook": "^2.1.0", "@storybook/cli": "^5.1.9", "@storybook/react": "^5.1.9", diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index bff78b995..34ce12970 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -1,4 +1,5 @@ @import (reference) '~dash-table/style/reset.less'; +@import (reference) '~./font-awesome-polyfills.less'; .fit-content-polyfill() { width: auto; // MS Edge, IE @@ -406,8 +407,20 @@ cursor: pointer; float: left; color: var(--faded-text-header); - font-size: 20px; + + &.sort--asc { + .fa-sort-up(); + } + + &.sort--desc { + .fa-sort-down(); + } + + &.sort--none { + .fa-sort(); + } } + th:hover .sort { color: var(--accent); } @@ -485,16 +498,20 @@ } .dash-spreadsheet-inner { - .column-header--clear::before { - content: 'ø'; + .column-header--clear { + .fa-eraser(); } - .column-header--delete::before { - content: '×' + .column-header--delete { + .fa-trash(); + } + + .column-header--edit { + .fa-pencil(); } - .column-header--edit::before { - content: '✎'; + .column-header--hide { + .fa-eye-slash(); } .column-header--clear, diff --git a/src/dash-table/components/Table/font-awesome-polyfills.less b/src/dash-table/components/Table/font-awesome-polyfills.less new file mode 100644 index 000000000..af6d5488e --- /dev/null +++ b/src/dash-table/components/Table/font-awesome-polyfills.less @@ -0,0 +1,47 @@ +.fa(@code) { + display: none; + font-family: "Font Awesome 5 Free"; + content: @code; +} + +.fa-regular(@code) { + &::before { + .fa(@code); + font-weight: 400; + } +} + +.fa-solid(@code) { + &::before { + .fa(@code); + font-weight: 900; + } +} + +.fa-eraser() { + .fa-solid('\f12d'); +} + +.fa-eye-slash() { + .fa-regular('\f070'); +} + +.fa-pencil() { + .fa-solid('\f303') +} + +.fa-sort() { + .fa-solid('\f0dc'); +} + +.fa-sort-down() { + .fa-solid('\f0dd'); +} + +.fa-sort-up() { + .fa-solid('\f0de'); +} + +.fa-trash() { + .fa-regular('\f2ed'); +} \ No newline at end of file diff --git a/src/dash-table/components/Table/index.tsx b/src/dash-table/components/Table/index.tsx index 070c927ea..f20893e7a 100644 --- a/src/dash-table/components/Table/index.tsx +++ b/src/dash-table/components/Table/index.tsx @@ -18,6 +18,7 @@ import { import 'react-select/dist/react-select.css'; import './Table.less'; +import './style'; import './Dropdown.css'; import { isEqual } from 'core/comparer'; import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; diff --git a/src/dash-table/components/Table/style.ts b/src/dash-table/components/Table/style.ts new file mode 100644 index 000000000..42dcd673f --- /dev/null +++ b/src/dash-table/components/Table/style.ts @@ -0,0 +1,17 @@ +import { config, library, dom } from '@fortawesome/fontawesome-svg-core'; +import { faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import { faEraser, faPencilAlt, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons' + +config.searchPseudoElements = true; + +library.add( + faEraser, + faEyeSlash, + faPencilAlt, + faSort, + faSortDown, + faSortUp, + faTrashAlt +); + +dom.watch(); diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 2cb9bbfa6..21429444a 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -97,12 +97,12 @@ function getSorting(columnId: ColumnId, sortBy: SortBy): SortDirection { function getSortingIcon(columnId: ColumnId, sortBy: SortBy) { switch (getSorting(columnId, sortBy)) { case SortDirection.Descending: - return '↓'; + return 'sort--desc'; case SortDirection.Ascending: - return '↑'; + return 'sort--asc'; case SortDirection.None: default: - return '↕'; + return 'sort--none'; } } @@ -140,11 +140,9 @@ function getter( return (
{sort_action !== TableAction.None && isLastRow ? ( - {getSortingIcon(column.id, sortBy)} - ) : + />) : '' } From 0948ccafc83b0148a3d812448caff70866bdb192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 18 Jul 2019 12:52:23 -0400 Subject: [PATCH 05/32] lint --- src/dash-table/components/Table/style.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dash-table/components/Table/style.ts b/src/dash-table/components/Table/style.ts index 42dcd673f..89702b068 100644 --- a/src/dash-table/components/Table/style.ts +++ b/src/dash-table/components/Table/style.ts @@ -1,6 +1,6 @@ import { config, library, dom } from '@fortawesome/fontawesome-svg-core'; import { faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import { faEraser, faPencilAlt, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons' +import { faEraser, faPencilAlt, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; config.searchPseudoElements = true; From 1fb1e2632c850641efb5691dcb20d0c827bb028f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 18 Jul 2019 14:09:19 -0400 Subject: [PATCH 06/32] hideable columns + hide icon / action --- src/dash-table/components/HeaderFactory.tsx | 2 ++ src/dash-table/components/Table/Table.less | 7 ++-- src/dash-table/components/Table/props.ts | 1 + src/dash-table/dash/DataTable.js | 5 +++ src/dash-table/derived/header/content.tsx | 20 +++++++++++- src/dash-table/utils/actions.js | 11 +++++++ .../percy-storybook/Header.actions.percy.tsx | 32 +++++++++++++++++-- 7 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index 574eda688..a57855692 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -38,6 +38,7 @@ export default class HeaderFactory { const { data, + hidden_columns, map, merge_duplicate_headers, page_action, @@ -92,6 +93,7 @@ export default class HeaderFactory { const contents = this.headerContent( visibleColumns, + hidden_columns, data, labelsAndIndices, map, diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index 34ce12970..43f4722fc 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -336,6 +336,7 @@ .column-header--clear, .column-header--delete, .column-header--edit, + .column-header--hide .sort { .not-selectable(); cursor: pointer; @@ -516,7 +517,8 @@ .column-header--clear, .column-header--delete, - .column-header--edit { + .column-header--edit, + .column-header--hide { float: left; opacity: 0.1; padding-left: 2px; @@ -527,7 +529,8 @@ th:hover { .column-header--clear, .column-header--delete, - .column-header--edit { + .column-header--edit, + .column-header--hide { color: var(--accent); opacity: 1; } diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 448652ef2..50bc4029a 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -156,6 +156,7 @@ export interface IBaseColumn { clearable?: boolean | boolean[]; deletable?: boolean | boolean[]; editable: boolean; + hideable?: boolean | boolean[]; renamable?: boolean | boolean[]; sort_as_null: SortAsNull; id: ColumnId; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 3f511383e..2930bb70d 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -165,6 +165,11 @@ export const propTypes = { */ editable: PropTypes.bool, + hideable: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.arrayOf(PropTypes.bool) + ]), + /** * If True, then the name of this column is editable. * If there are multiple column headers (if `name` is a list of strings), diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 21429444a..b04b4ab35 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -114,6 +114,7 @@ function getColumnFlag(i: number, flag?: boolean | boolean[]): boolean { function getter( columns: Columns, + hiddenColumns: string[] | undefined, data: Data, labelsAndIndices: R.KeyValuePair[], map: Map, @@ -133,9 +134,10 @@ function getter( columnIndex => { const column = columns[columnIndex]; - const renamable = getColumnFlag(headerRowIndex, column.renamable); const clearable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, column.clearable); const deletable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, column.deletable); + const hideable = getColumnFlag(headerRowIndex, column.hideable); + const renamable = getColumnFlag(headerRowIndex, column.renamable); return (
{sort_action !== TableAction.None && isLastRow ? @@ -170,6 +172,22 @@ function getter( '' } + {hideable ? + ( { + const ids = actions.getColumnIds(column, columns, headerRowIndex, mergeDuplicateHeaders); + + const hidden_columns = hiddenColumns ? + R.union(hiddenColumns, ids) : + ids; + + setProps({ hidden_columns }); + }} + />) : + '' + } + {labels[columnIndex]}
); }, diff --git a/src/dash-table/utils/actions.js b/src/dash-table/utils/actions.js index 19c5c1a4c..9d24e8997 100644 --- a/src/dash-table/utils/actions.js +++ b/src/dash-table/utils/actions.js @@ -61,6 +61,17 @@ export function deleteColumn(column, columns, headerRowIndex, mergeDuplicateHead }; } +export function getColumnIds(column, columns, headerRowIndex, mergeDuplicateHeaders) { + const { groupIndexFirst, groupIndexLast } = getGroupedColumnIndices( + column, columns, headerRowIndex, mergeDuplicateHeaders, columns.indexOf(column) + ); + + return R.map( + c => c.id, + columns.slice(groupIndexFirst, groupIndexLast + 1) + ); +} + export const clearSelection = { active_cell: undefined, start_cell: undefined, diff --git a/tests/visual/percy-storybook/Header.actions.percy.tsx b/tests/visual/percy-storybook/Header.actions.percy.tsx index 497992ff9..0f61eaa25 100644 --- a/tests/visual/percy-storybook/Header.actions.percy.tsx +++ b/tests/visual/percy-storybook/Header.actions.percy.tsx @@ -104,11 +104,39 @@ const scenarios: ITest[] = [ }, COLUMNS_BASE) } }, { - name: 'clearable+deletable', + name: 'hideable', + props: { + columns: R.map(c => R.mergeRight(c, { + hideable: true + }), COLUMNS_BASE) + } + }, { + name: 'hideable (top-city, bottom-climate)', + props: { + columns: R.map((c: any) => { + const firstName = c.name[0]; + + if (firstName === 'City') { + return R.mergeRight(c, { + hideable: [true, false] + }); + } else if (firstName === 'Climate') { + return R.mergeRight(c, { + hideable: [false, true] + }); + + } else { + return c; + } + }, COLUMNS_BASE) + } + }, { + name: 'clearable+deletable+hideable', props: { columns: R.map(c => R.mergeRight(c, { clearable: true, - deletable: true + deletable: true, + hideable: true }), COLUMNS_BASE) } } From 0e516dc65634c76893027e86b006fa165faaabf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 22 Jul 2019 17:37:16 -0400 Subject: [PATCH 07/32] toggle column menu + states --- .../components/ControlledTable/index.tsx | 126 +++++++++++++++++- src/dash-table/components/Table/Table.less | 18 +++ src/dash-table/components/Table/props.ts | 1 + 3 files changed, 139 insertions(+), 6 deletions(-) diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 33e760bea..405f4b003 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -18,9 +18,10 @@ import { memoizeOne } from 'core/memoizer'; import lexer from 'core/syntax-tree/lexer'; import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper'; -import { ControlledTableProps, ICellFactoryProps, TableAction } from 'dash-table/components/Table/props'; +import { ControlledTableProps, ICellFactoryProps, TableAction, IColumn } from 'dash-table/components/Table/props'; import dropdownHelper from 'dash-table/components/dropdownHelper'; +import getHeaderRows from 'dash-table/derived/header/headerRows'; import derivedTable from 'dash-table/derived/table'; import derivedTableFragments from 'dash-table/derived/table/fragments'; import derivedTableFragmentStyles from 'dash-table/derived/table/fragmentStyles'; @@ -36,6 +37,7 @@ const DEFAULT_STYLE = { }; export default class ControlledTable extends PureComponent { + private readonly menuRef = React.createRef(); private readonly stylesheet: Stylesheet = new Stylesheet(`#${this.props.id}`); private readonly tableFn = derivedTable(() => this.props); private readonly tableFragments = derivedTableFragments(); @@ -121,13 +123,13 @@ export default class ControlledTable extends PureComponent // Fallback method for paste handling in Chrome // when no input element has focused inside the table window.addEventListener('resize', this.forceHandleResize); + document.addEventListener('mousedown', this.handleClick); document.addEventListener('paste', this.handlePaste); - document.addEventListener('mousedown', this.handleClickOutside); } componentWillUnmount() { window.removeEventListener('resize', this.forceHandleResize); - document.removeEventListener('mousedown', this.handleClickOutside); + document.removeEventListener('mousedown', this.handleClick); document.removeEventListener('paste', this.handlePaste); } @@ -172,7 +174,7 @@ export default class ControlledTable extends PureComponent }); } - handleClickOutside = (event: any) => { + handleClick = (event: any) => { const $el = this.$el; if ($el && @@ -182,8 +184,23 @@ export default class ControlledTable extends PureComponent * so, only call when the table isn't already focussed, otherwise * the app will excessively re-render on _every click on the page_ */ - this.props.is_focused) { - this.props.setProps({ is_focused: false }); + this.props.is_focused + ) { + this.props.setProps({ + is_focused: false + }); + } + + const menu = this.menuRef; + + if (this.props.activeMenu && + menu && + menu.current && + !menu.current.contains(event.target as Node) + ) { + this.props.setState({ + activeMenu: undefined + }); } } @@ -679,7 +696,9 @@ export default class ControlledTable extends PureComponent render() { const { + columns, id, + merge_duplicate_headers, tooltip_conditional, tooltip, currentTooltip, @@ -760,6 +779,8 @@ export default class ControlledTable extends PureComponent tooltip_duration ); + const headerRows = getHeaderRows(columns); + return (
className='dash-table-tooltip' tooltip={tableTooltip} /> + {!this.showToggleColumns ? null : ( +
+ + {this.props.activeMenu !== 'show/hide' ? + null : +
+ {this.props.columns.map(column => { + const checked = !this.props.hidden_columns || this.props.hidden_columns.indexOf(column.id) < 0; + const disabled = checked && ( + typeof column.hideable === 'undefined' ? + true : + typeof column.hideable === 'boolean' ? + !column.hideable : + ( + (merge_duplicate_headers && (column.hideable.length !== headerRows || !column.hideable.slice(-1)[0])) || + (!merge_duplicate_headers && R.all(c => !c, column.hideable)) + ) + ); + console.log(column.id, column.hideable, headerRows, checked, disabled); + return (
+ + +
); + })} +
+ } +
+ )}
(this.refs.tooltip as TableTooltip).updateBounds(cell); } } + + private get showToggleColumns(): boolean { + const { + columns, + hidden_columns, + merge_duplicate_headers + } = this.props; + + const headerRows = getHeaderRows(columns); + + return ( + hidden_columns && hidden_columns.length > 0) || + R.any( + column => column.hideable === true || ( + Array.isArray(column.hideable) && ( + (merge_duplicate_headers && column.hideable.length === headerRows && column.hideable.slice(-1)[0]) || + (!merge_duplicate_headers && R.any(c => c, column.hideable)) + ) + + ), + columns + ); + } + + private toggleColumn = (column: IColumn) => { + const { + hidden_columns: base, + setProps + } = this.props; + + const hidden_columns = base ? base.slice(0) : []; + const cIndex = hidden_columns.indexOf(column.id); + + if (cIndex >= 0) { + hidden_columns.splice(cIndex, 1); + } else { + hidden_columns.push(column.id); + } + + setProps({ hidden_columns }); + } } diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index 43f4722fc..d634914df 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -104,6 +104,24 @@ } } } + +.dash-spreadsheet-menu-item { + position: relative; + + .show-hide-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 500; + background-color: #fafafa; + border: 1px solid #d3d3d3; + + .show-hide-menu-item { + padding: 5px; + } + } +} + .dash-spreadsheet-container { .reset-css(); display: flex; diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 50bc4029a..47c8e4a77 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -230,6 +230,7 @@ export interface IUSerInterfaceTooltip { } export interface IState { + activeMenu?: 'show/hide'; currentTooltip?: IUSerInterfaceTooltip; forcedResizeOnly: boolean; rawFilterQuery: string; From 090ca2ba7c5983e43e9c599c4e6637e2ab7bb152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 23 Jul 2019 12:15:58 -0400 Subject: [PATCH 08/32] fix font-awesome performance issues --- .../components/ControlledTable/index.tsx | 34 ++------------ src/dash-table/components/Table/Table.less | 29 ------------ .../Table/font-awesome-polyfills.less | 47 ------------------- src/dash-table/components/Table/props.ts | 2 +- src/dash-table/components/Table/style.ts | 6 +-- src/dash-table/dash/DataTable.js | 5 +- src/dash-table/derived/header/content.tsx | 33 ++++++++----- 7 files changed, 28 insertions(+), 128 deletions(-) delete mode 100644 src/dash-table/components/Table/font-awesome-polyfills.less diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 405f4b003..96670792d 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -21,7 +21,6 @@ import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper'; import { ControlledTableProps, ICellFactoryProps, TableAction, IColumn } from 'dash-table/components/Table/props'; import dropdownHelper from 'dash-table/components/dropdownHelper'; -import getHeaderRows from 'dash-table/derived/header/headerRows'; import derivedTable from 'dash-table/derived/table'; import derivedTableFragments from 'dash-table/derived/table/fragments'; import derivedTableFragmentStyles from 'dash-table/derived/table/fragmentStyles'; @@ -696,9 +695,7 @@ export default class ControlledTable extends PureComponent render() { const { - columns, id, - merge_duplicate_headers, tooltip_conditional, tooltip, currentTooltip, @@ -779,8 +776,6 @@ export default class ControlledTable extends PureComponent tooltip_duration ); - const headerRows = getHeaderRows(columns); - return (
> {this.props.columns.map(column => { const checked = !this.props.hidden_columns || this.props.hidden_columns.indexOf(column.id) < 0; - const disabled = checked && ( - typeof column.hideable === 'undefined' ? - true : - typeof column.hideable === 'boolean' ? - !column.hideable : - ( - (merge_duplicate_headers && (column.hideable.length !== headerRows || !column.hideable.slice(-1)[0])) || - (!merge_duplicate_headers && R.all(c => !c, column.hideable)) - ) - ); - console.log(column.id, column.hideable, headerRows, checked, disabled); + const disabled = !column.hideable && checked; + return (
private get showToggleColumns(): boolean { const { columns, - hidden_columns, - merge_duplicate_headers + hidden_columns } = this.props; - const headerRows = getHeaderRows(columns); - return ( hidden_columns && hidden_columns.length > 0) || - R.any( - column => column.hideable === true || ( - Array.isArray(column.hideable) && ( - (merge_duplicate_headers && column.hideable.length === headerRows && column.hideable.slice(-1)[0]) || - (!merge_duplicate_headers && R.any(c => c, column.hideable)) - ) - - ), - columns - ); + R.any(column => !!column.hideable, columns); } private toggleColumn = (column: IColumn) => { diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index d634914df..dd1953dcb 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -1,5 +1,4 @@ @import (reference) '~dash-table/style/reset.less'; -@import (reference) '~./font-awesome-polyfills.less'; .fit-content-polyfill() { width: auto; // MS Edge, IE @@ -426,18 +425,6 @@ cursor: pointer; float: left; color: var(--faded-text-header); - - &.sort--asc { - .fa-sort-up(); - } - - &.sort--desc { - .fa-sort-down(); - } - - &.sort--none { - .fa-sort(); - } } th:hover .sort { @@ -517,22 +504,6 @@ } .dash-spreadsheet-inner { - .column-header--clear { - .fa-eraser(); - } - - .column-header--delete { - .fa-trash(); - } - - .column-header--edit { - .fa-pencil(); - } - - .column-header--hide { - .fa-eye-slash(); - } - .column-header--clear, .column-header--delete, .column-header--edit, diff --git a/src/dash-table/components/Table/font-awesome-polyfills.less b/src/dash-table/components/Table/font-awesome-polyfills.less deleted file mode 100644 index af6d5488e..000000000 --- a/src/dash-table/components/Table/font-awesome-polyfills.less +++ /dev/null @@ -1,47 +0,0 @@ -.fa(@code) { - display: none; - font-family: "Font Awesome 5 Free"; - content: @code; -} - -.fa-regular(@code) { - &::before { - .fa(@code); - font-weight: 400; - } -} - -.fa-solid(@code) { - &::before { - .fa(@code); - font-weight: 900; - } -} - -.fa-eraser() { - .fa-solid('\f12d'); -} - -.fa-eye-slash() { - .fa-regular('\f070'); -} - -.fa-pencil() { - .fa-solid('\f303') -} - -.fa-sort() { - .fa-solid('\f0dc'); -} - -.fa-sort-down() { - .fa-solid('\f0dd'); -} - -.fa-sort-up() { - .fa-solid('\f0de'); -} - -.fa-trash() { - .fa-regular('\f2ed'); -} \ No newline at end of file diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 47c8e4a77..af2d42f54 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -156,7 +156,7 @@ export interface IBaseColumn { clearable?: boolean | boolean[]; deletable?: boolean | boolean[]; editable: boolean; - hideable?: boolean | boolean[]; + hideable?: boolean; renamable?: boolean | boolean[]; sort_as_null: SortAsNull; id: ColumnId; diff --git a/src/dash-table/components/Table/style.ts b/src/dash-table/components/Table/style.ts index 89702b068..1b5eca4e1 100644 --- a/src/dash-table/components/Table/style.ts +++ b/src/dash-table/components/Table/style.ts @@ -1,9 +1,7 @@ -import { config, library, dom } from '@fortawesome/fontawesome-svg-core'; +import { library } from '@fortawesome/fontawesome-svg-core'; import { faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; import { faEraser, faPencilAlt, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; -config.searchPseudoElements = true; - library.add( faEraser, faEyeSlash, @@ -13,5 +11,3 @@ library.add( faSortUp, faTrashAlt ); - -dom.watch(); diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 2930bb70d..3e85b0ee5 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -165,10 +165,7 @@ export const propTypes = { */ editable: PropTypes.bool, - hideable: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.arrayOf(PropTypes.bool) - ]), + hideable: PropTypes.bool, /** * If True, then the name of this column is editable. diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index b04b4ab35..56c94c3a0 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -5,7 +5,7 @@ import { memoizeOneFactory } from 'core/memoizer'; import { SortDirection, SortBy } from 'core/sorting'; import multiUpdate from 'core/sorting/multi'; import singleUpdate from 'core/sorting/single'; - +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ColumnId, Data, @@ -97,12 +97,12 @@ function getSorting(columnId: ColumnId, sortBy: SortBy): SortDirection { function getSortingIcon(columnId: ColumnId, sortBy: SortBy) { switch (getSorting(columnId, sortBy)) { case SortDirection.Descending: - return 'sort--desc'; + return 'sort-down'; case SortDirection.Ascending: - return 'sort--asc'; + return 'sort-up'; case SortDirection.None: default: - return 'sort--none'; + return 'sort'; } } @@ -142,9 +142,11 @@ function getter( return (
{sort_action !== TableAction.None && isLastRow ? () : + > + + ) : '' } @@ -152,7 +154,9 @@ function getter( () : + > + + ) : '' } @@ -160,7 +164,9 @@ function getter( () : + > + + ) : '' } @@ -168,11 +174,13 @@ function getter( () : + > + + ) : '' } - {hideable ? + {(hideable && isLastRow) ? ( { @@ -183,8 +191,9 @@ function getter( ids; setProps({ hidden_columns }); - }} - />) : + }}> + + ) : '' } From 66f8d1d36abf57751facefe617bf3d44653d4925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 23 Jul 2019 12:24:17 -0400 Subject: [PATCH 09/32] dev dep --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3fcfbfff9..30dc44632 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.19", "@fortawesome/free-regular-svg-icons": "^5.9.0", "@fortawesome/free-solid-svg-icons": "^5.9.0", + "@fortawesome/react-fontawesome": "^0.1.4", "@percy-io/percy-storybook": "^2.1.0", "@storybook/cli": "^5.1.9", "@storybook/react": "^5.1.9", From 9f0dce9601fe7d3f8f604c62dd0b32ddffa29158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 23 Jul 2019 15:20:30 -0400 Subject: [PATCH 10/32] Update changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd8effd5..b0eb640d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +[#314](https://github.com/plotly/dash-table/issues/314) +- New `column.hideable` flag that displays a "eye" action icon in the column + Accepts a boolean. Clicking on the "eye" will add the column to the `hidden_columns` prop. + `hidden_columns` can be added back through the Columns toggle menu whether they are hideable or not. + [#313](https://github.com/plotly/dash-table/issues/313) - Ability to export table as csv or xlsx file. @@ -23,7 +28,7 @@ reset the filter for the affected column(s) ### Fixed [#491](https://github.com/plotly/dash-table/issues/491) -- Fixed unconsistent behaviors when editing cell headers +- Fixed inconsistent behaviors when editing cell headers ## [4.0.2] - 2019-07-15 ### Fixed From f91821664848669fdf8979a8b43d489c9eac3f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 23 Jul 2019 15:38:47 -0400 Subject: [PATCH 11/32] refactor / isolate menu rendering --- .../components/ControlledTable/index.tsx | 98 +++++++++++-------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index bc41a6d8c..4c446cfd3 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -798,49 +798,7 @@ export default class ControlledTable extends PureComponent className='dash-table-tooltip' tooltip={tableTooltip} /> - {!this.showToggleColumns ? null : ( -
- - {this.props.activeMenu !== 'show/hide' ? - null : -
- {this.props.columns.map(column => { - const checked = !this.props.hidden_columns || this.props.hidden_columns.indexOf(column.id) < 0; - const disabled = !column.hideable && checked; - - return (
- - -
); - })} -
- } -
- )} + {this.renderMenu()}
); } + renderMenu() { + if (!this.showToggleColumns) { + return null; + } + + const { + activeMenu, + columns, + hidden_columns, + setState + } = this.props; + + return (
+ + {activeMenu !== 'show/hide' ? + null : +
+ {columns.map(column => { + const checked = !hidden_columns || hidden_columns.indexOf(column.id) < 0; + const disabled = !column.hideable && checked; + + return (
+ + +
); + })} +
+ } +
); + } + private adjustTooltipPosition() { const { currentTooltip, virtualized } = this.props; From 952b2abcbfd09ccd7065d8dfc923643f1cd961d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 23 Jul 2019 16:45:11 -0400 Subject: [PATCH 12/32] standalone tests --- demo/AppMode.ts | 19 ++++---- tests/cypress/src/DashTable.ts | 4 ++ tests/cypress/tests/standalone/column_test.ts | 47 +++++++++++++++++-- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 54bbd151f..178eb197d 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -13,8 +13,8 @@ import { import { TooltipSyntax } from 'dash-table/tooltips/props'; export enum AppMode { - Clearable = 'clearable', - ClearableMerged = 'clearableMerged', + Actionable = 'actionable', + ActionableMerged = 'actionableMerged', Date = 'date', Default = 'default', Filtering = 'filtering', @@ -186,19 +186,20 @@ function getTypedState() { return state; } -function getClearableState() { +function getActionableState() { const state = getDefaultState(); state.tableProps.filter_action = TableAction.Native; R.forEach(c => { c.clearable = true; + c.hideable = true; }, state.tableProps.columns || []); return state; } -function getClearableMergedState() { - const state = getClearableState(); +function getActionableMergedState() { + const state = getActionableState(); state.tableProps.merge_duplicate_headers = true; return state; @@ -344,10 +345,10 @@ function getState() { const mode = Environment.searchParams.get('mode'); switch (mode) { - case AppMode.Clearable: - return getClearableState(); - case AppMode.ClearableMerged: - return getClearableMergedState(); + case AppMode.Actionable: + return getActionableState(); + case AppMode.ActionableMerged: + return getActionableMergedState(); case AppMode.Date: return getDateState(); case AppMode.Filtering: diff --git a/tests/cypress/src/DashTable.ts b/tests/cypress/src/DashTable.ts index 790957e98..c3d33fd49 100644 --- a/tests/cypress/src/DashTable.ts +++ b/tests/cypress/src/DashTable.ts @@ -41,6 +41,10 @@ export default class DashTable { return cy.get(`#table tbody tr th.dash-header[data-dash-column="${column}"] .column-header--delete`).eq(row).click(); } + static hideColumnById(row: number, column: string) { + return cy.get(`#table tbody tr th.dash-header[data-dash-column="${column}"] .column-header--hide`).eq(row).click(); + } + static getDelete(row: number) { return cy.get(`#table tbody tr td.dash-delete-cell`).eq(row); } diff --git a/tests/cypress/tests/standalone/column_test.ts b/tests/cypress/tests/standalone/column_test.ts index c1a8d1522..3cca137fa 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.ClearableMerged}`, () => { +describe(`column, mode=${AppMode.ActionableMerged}`, () => { beforeEach(() => { - cy.visit(`http://localhost:8080?mode=${AppMode.ClearableMerged}`); + cy.visit(`http://localhost:8080?mode=${AppMode.ActionableMerged}`); DashTable.toggleScroll(false); }); @@ -61,11 +61,31 @@ describe(`column, mode=${AppMode.ClearableMerged}`, () => { DashTable.getFilter(3).within(() => cy.get('input').should('have.value', 'is num')); DashTable.getFilter(4).within(() => cy.get('input').should('have.value', '')); }); + + it('can hide column', () => { + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'rows')); + DashTable.hideColumnById(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.hideColumnById(0, '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.hideColumnById(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', 'Canada')); + DashTable.getHeader(0, 1).within(() => cy.get('span.column-header-name').should('have.html', 'America')); + DashTable.getHeader(1, 1).within(() => cy.get('span.column-header-name').should('have.html', 'New York City')); + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'City')); + DashTable.getHeader(0, 2).within(() => cy.get('span.column-header-name').should('have.html', 'France')); + DashTable.getHeader(1, 2).within(() => cy.get('span.column-header-name').should('have.html', 'Paris')); + }); }); -describe(`column, mode=${AppMode.Clearable}`, () => { +describe(`column, mode=${AppMode.Actionable}`, () => { beforeEach(() => { - cy.visit(`http://localhost:8080?mode=${AppMode.Clearable}`); + cy.visit(`http://localhost:8080?mode=${AppMode.Actionable}`); DashTable.toggleScroll(false); }); @@ -122,4 +142,23 @@ 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', '')); }); + + it('can hide column', () => { + DashTable.getHeader(0, 0).within(() => cy.get('span.column-header-name').should('have.html', 'rows')); + DashTable.hideColumnById(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.hideColumnById(0, '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.hideColumnById(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')); + }); }); \ No newline at end of file From 7055d3c57c06489c8233e2f970ab8fae4af80171 Mon Sep 17 00:00:00 2001 From: Marc-Andre-Rivet Date: Tue, 23 Jul 2019 20:22:19 -0400 Subject: [PATCH 13/32] reorg controlled table menu --- .../components/ControlledTable/index.tsx | 16 ++++----- src/dash-table/components/Table/Table.less | 35 +++++++++++++------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 4c446cfd3..d168934e4 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -798,7 +798,10 @@ export default class ControlledTable extends PureComponent className='dash-table-tooltip' tooltip={tableTooltip} /> - {this.renderMenu()} +
+ {this.renderMenu()} + +
)} -
); } @@ -855,18 +857,12 @@ export default class ControlledTable extends PureComponent >Toggle Columns {activeMenu !== 'show/hide' ? null : -
+
{columns.map(column => { const checked = !hidden_columns || hidden_columns.indexOf(column.id) < 0; const disabled = !column.hideable && checked; - return (
+ return (
* { + padding-right: 5px; + } + + .dash-spreadsheet-menu-item { + position: relative; - .show-hide-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 500; - background-color: #fafafa; - border: 1px solid #d3d3d3; + .show-hide-menu { + background-color: #fafafa; + border: 1px solid #d3d3d3; + display: flex; + flex-direction: column; + position: absolute; + top: 100%; + left: 0; + z-index: 500; - .show-hide-menu-item { - padding: 5px; + .show-hide-menu-item { + display: flex; + flex-direction: row; + padding: 5px; + } } } } From 4e70f9277f3ab9b817b563a17660df252d33c1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 25 Jul 2019 14:43:52 -0400 Subject: [PATCH 14/32] prop description --- src/dash-table/dash/DataTable.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index af971ae87..363b7a5ce 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -166,6 +166,16 @@ export const propTypes = { */ editable: PropTypes.bool, + /** + * If True, then the column is hideable through the UI. + * If there are multiple column headers, only the last row of + * the column headers will dispay the `hide` action button. + * + * Hidden columns are listed by id in the `hidden_columns` prop. + * + * If `hidden_columns` is not empty, a toggle button is displayed + * above the table that allows toggling columns visibility. + */ hideable: PropTypes.bool, /** @@ -462,6 +472,11 @@ export const propTypes = { */ fill_width: PropTypes.bool, + /** + * List of columns ids of the columns that are currently hidden. + * + * See the associated nested prop `columns.hideable`. + */ hidden_columns: PropTypes.arrayOf(PropTypes.string), /** From 1b7784a17e0c52bab9ba63bc42040e06d11681cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Rivet?= Date: Tue, 30 Jul 2019 19:43:20 -0400 Subject: [PATCH 15/32] Update CHANGELOG.md Co-Authored-By: alexcjohnson --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0eb640d9..df570be69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added [#314](https://github.com/plotly/dash-table/issues/314) -- New `column.hideable` flag that displays a "eye" action icon in the column +- New `column.hideable` flag that displays an "eye" action icon in the column Accepts a boolean. Clicking on the "eye" will add the column to the `hidden_columns` prop. `hidden_columns` can be added back through the Columns toggle menu whether they are hideable or not. From 342849fe92c6a5bbbf3867bf617f49b331b9d3f7 Mon Sep 17 00:00:00 2001 From: Marc-Andre-Rivet Date: Tue, 30 Jul 2019 19:55:37 -0400 Subject: [PATCH 16/32] improve column toggle menu styling --- src/dash-table/components/Table/Table.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index 155088df2..7fd9b6ea8 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -120,6 +120,8 @@ border: 1px solid #d3d3d3; display: flex; flex-direction: column; + max-height: 300px; + overflow: auto; position: absolute; top: 100%; left: 0; @@ -129,6 +131,10 @@ display: flex; flex-direction: row; padding: 5px; + + label { + white-space: nowrap; + } } } } From 52568bc0a38332b1e87293fd87a78eb4fd59e695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 31 Jul 2019 09:12:15 -0400 Subject: [PATCH 17/32] - prevent hiding last column - styling for hiding - hideable table-level prop --- .../components/ControlledTable/index.tsx | 7 +++-- src/dash-table/components/Table/Table.less | 15 +++++++-- src/dash-table/components/Table/props.ts | 2 ++ src/dash-table/dash/DataTable.js | 19 ++++++++++++ src/dash-table/dash/Sanitizer.ts | 15 ++++++--- src/dash-table/derived/cell/isEditable.ts | 6 ---- src/dash-table/derived/cell/resolveFlag.ts | 6 ++++ src/dash-table/derived/header/content.tsx | 31 ++++++++++++------- 8 files changed, 75 insertions(+), 26 deletions(-) delete mode 100644 src/dash-table/derived/cell/isEditable.ts create mode 100644 src/dash-table/derived/cell/resolveFlag.ts diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index d168934e4..6d50254fd 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -843,9 +843,12 @@ export default class ControlledTable extends PureComponent activeMenu, columns, hidden_columns, - setState + setState, + visibleColumns } = this.props; + const singleColumn = visibleColumns.length === 1; + return (
{columns.map(column => { const checked = !hidden_columns || hidden_columns.indexOf(column.id) < 0; - const disabled = !column.hideable && checked; + const disabled = singleColumn || (!column.hideable && checked); return (
R.map(column => { +const applyDefaultsToColumns = ( + defaultLocale: INumberLocale, + defaultSort: SortAsNull, + columns: Columns, + editable: boolean, + hideable: boolean +) => R.map(column => { const c = R.clone(column); - c.editable = isEditable(editable, column.editable); + c.editable = resolveFlag(editable, column.editable); + c.hideable = resolveFlag(hideable, column.hideable); c.sort_as_null = c.sort_as_null || defaultSort; if (c.type === ColumnType.Numeric && c.format) { @@ -70,7 +77,7 @@ const getVisibleColumns = ( export default class Sanitizer { sanitize(props: PropsWithDefaults): SanitizedProps { const locale_format = this.applyDefaultToLocale(props.locale_format); - const columns = this.applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable); + const columns = this.applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable, props.hideable); const visibleColumns = this.getVisibleColumns(columns, props.hidden_columns); let headerFormat = props.export_headers; diff --git a/src/dash-table/derived/cell/isEditable.ts b/src/dash-table/derived/cell/isEditable.ts deleted file mode 100644 index 31612b0f8..000000000 --- a/src/dash-table/derived/cell/isEditable.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default ( - isEditableTable: boolean, - isEditableColumn: boolean | undefined -): boolean => isEditableColumn === undefined ? - isEditableTable : - isEditableColumn; \ No newline at end of file diff --git a/src/dash-table/derived/cell/resolveFlag.ts b/src/dash-table/derived/cell/resolveFlag.ts new file mode 100644 index 000000000..68e1f4598 --- /dev/null +++ b/src/dash-table/derived/cell/resolveFlag.ts @@ -0,0 +1,6 @@ +export default ( + tableFlag: boolean, + columnFlag: boolean | undefined +): boolean => columnFlag === undefined ? + tableFlag : + columnFlag; \ 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 56c94c3a0..9a235bdd1 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -139,6 +139,8 @@ function getter( const hideable = getColumnFlag(headerRowIndex, column.hideable); const renamable = getColumnFlag(headerRowIndex, column.renamable); + const singleColumn = columns.length === 1; + return (
{sort_action !== TableAction.None && isLastRow ? ( ) : @@ -182,16 +187,18 @@ function getter( {(hideable && isLastRow) ? ( { - const ids = actions.getColumnIds(column, columns, headerRowIndex, mergeDuplicateHeaders); - - const hidden_columns = hiddenColumns ? - R.union(hiddenColumns, ids) : - ids; - - setProps({ hidden_columns }); - }}> + className={'column-header--hide' + (singleColumn ? ' disabled' : '')} + onClick={singleColumn ? + undefined : + () => { + const ids = actions.getColumnIds(column, columns, headerRowIndex, mergeDuplicateHeaders); + + const hidden_columns = hiddenColumns ? + R.union(hiddenColumns, ids) : + ids; + + setProps({ hidden_columns }); + }}> ) : '' From 0b3a0c5cf7c908f8fb7982755378abd3de8485af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 31 Jul 2019 09:20:31 -0400 Subject: [PATCH 18/32] update unit tests --- tests/cypress/tests/unit/isEditable_test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/cypress/tests/unit/isEditable_test.ts b/tests/cypress/tests/unit/isEditable_test.ts index 91185b28b..75d26fe3b 100644 --- a/tests/cypress/tests/unit/isEditable_test.ts +++ b/tests/cypress/tests/unit/isEditable_test.ts @@ -1,27 +1,27 @@ -import isEditable from 'dash-table/derived/cell/isEditable'; +import resolveFlag from 'dash-table/derived/cell/resolveFlag'; describe('isEditable', () => { it('returns false if table=false, column=false', () => - expect(isEditable(false, false)).to.equal(false) + expect(resolveFlag(false, false)).to.equal(false) ); it('returns false if table=false, column=undefined', () => - expect(isEditable(false, undefined)).to.equal(false) + expect(resolveFlag(false, undefined)).to.equal(false) ); it('returns true if table=false, column=true', () => - expect(isEditable(false, true)).to.equal(true) + expect(resolveFlag(false, true)).to.equal(true) ); it('returns false if table=true, column=false', () => - expect(isEditable(true, false)).to.equal(false) + expect(resolveFlag(true, false)).to.equal(false) ); it('returns true if table=true, column=undefined', () => - expect(isEditable(true, undefined)).to.equal(true) + expect(resolveFlag(true, undefined)).to.equal(true) ); it('returns true if table=true, column=true', () => - expect(isEditable(true, true)).to.equal(true) + expect(resolveFlag(true, true)).to.equal(true) ); }); \ No newline at end of file From 8b91da6f57bb828de9c4d2eaacb952746e3af6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 31 Jul 2019 09:52:34 -0400 Subject: [PATCH 19/32] - display `hide` action on desired header row --- src/dash-table/components/HeaderFactory.tsx | 2 ++ src/dash-table/components/Table/props.ts | 1 + src/dash-table/dash/DataTable.js | 8 ++++++++ src/dash-table/derived/header/content.tsx | 21 ++++++++++++++++++--- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index a57855692..7080907ee 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -38,6 +38,7 @@ export default class HeaderFactory { const { data, + hideable_row, hidden_columns, map, merge_duplicate_headers, @@ -94,6 +95,7 @@ export default class HeaderFactory { const contents = this.headerContent( visibleColumns, hidden_columns, + hideable_row, data, labelsAndIndices, map, diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index a5217b793..4561bc113 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -273,6 +273,7 @@ export interface IProps { filter_query?: string; filter_action?: TableAction; hideable?: boolean; + hideable_row: number; hidden_columns?: string[]; locale_format: INumberLocale; merge_duplicate_headers?: boolean; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 8d20eb2b3..fca3bc959 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -453,6 +453,14 @@ export const propTypes = { */ hideable: PropTypes.bool, + /** + * 'beta'. Define the header row on which the hideable action will be + * visible. + * + * Defaults to the last header row. + */ + hideable_row: PropTypes.number, + /** * When selecting multiple cells * (via clicking on a cell and then shift-clicking on another cell), diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 9a235bdd1..c195d091c 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -115,6 +115,7 @@ function getColumnFlag(i: number, flag?: boolean | boolean[]): boolean { function getter( columns: Columns, hiddenColumns: string[] | undefined, + hideable_row: number | undefined, data: Data, labelsAndIndices: R.KeyValuePair[], map: Map, @@ -129,17 +130,31 @@ function getter( return R.addIndex, JSX.Element[]>(R.map)( ([labels, indices], headerRowIndex) => { const isLastRow = headerRowIndex === labelsAndIndices.length - 1; + const isHideableRow = R.isNil(hideable_row) ? + isLastRow : + headerRowIndex === hideable_row; return R.addIndex(R.map)( - columnIndex => { + (columnIndex, index) => { const column = columns[columnIndex]; + let colSpan: number; + if (!mergeDuplicateHeaders) { + colSpan = 1; + } else { + if (columnIndex === R.last(indices)) { + colSpan = labels.length - columnIndex; + } else { + colSpan = indices[index + 1] - columnIndex; + } + } + const clearable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, column.clearable); const deletable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, column.deletable); const hideable = getColumnFlag(headerRowIndex, column.hideable); const renamable = getColumnFlag(headerRowIndex, column.renamable); - const singleColumn = columns.length === 1; + const singleColumn = columns.length === colSpan; return (
{sort_action !== TableAction.None && isLastRow ? @@ -185,7 +200,7 @@ function getter( '' } - {(hideable && isLastRow) ? + {(hideable && isHideableRow) ? ( Date: Wed, 31 Jul 2019 11:08:14 -0400 Subject: [PATCH 20/32] hideable merged columns / header row index --- .../components/ControlledTable/index.tsx | 48 ++++++++++++++----- src/dash-table/components/HeaderFactory.tsx | 20 ++------ src/dash-table/components/Table/Table.less | 9 ---- .../derived/header/labelsAndIndices.ts | 21 ++++++++ 4 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 src/dash-table/derived/header/labelsAndIndices.ts diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 6d50254fd..6751d4a1f 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -8,6 +8,7 @@ import { isCtrlDown, isNavKey } from 'dash-table/utils/unicode'; +import * as actions from 'dash-table/utils/actions'; import ExportButton from 'dash-table/components/Export'; import { selectionBounds, selectionCycle } from 'dash-table/utils/navigation'; import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps'; @@ -22,6 +23,8 @@ import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper'; import { ControlledTableProps, ICellFactoryProps, TableAction, IColumn } from 'dash-table/components/Table/props'; import dropdownHelper from 'dash-table/components/dropdownHelper'; +import getHeaderRows from 'dash-table/derived/header/headerRows'; +import derivedLabelsAndIndices from 'dash-table/derived/header/labelsAndIndices'; import derivedTable from 'dash-table/derived/table'; import derivedTableFragments from 'dash-table/derived/table/fragments'; import derivedTableFragmentStyles from 'dash-table/derived/table/fragmentStyles'; @@ -42,6 +45,7 @@ export default class ControlledTable extends PureComponent private readonly tableFn = derivedTable(() => this.props); private readonly tableFragments = derivedTableFragments(); private readonly tableStyle = derivedTableStyle(); + private readonly labelsAndIndices = derivedLabelsAndIndices(); private calculateTableStyle = memoizeOne((style: Partial) => R.mergeAll( this.tableStyle(DEFAULT_STYLE, style) @@ -842,12 +846,25 @@ export default class ControlledTable extends PureComponent const { activeMenu, columns, + hideable_row, hidden_columns, + merge_duplicate_headers, setState, visibleColumns } = this.props; - const singleColumn = visibleColumns.length === 1; + const headerRows = getHeaderRows(columns); + const hideableRow = R.isNil(hideable_row) ? + headerRows : + hideable_row; + + const labelsAndIndices = this.labelsAndIndices(columns, merge_duplicate_headers); + const [, indices] = labelsAndIndices[hideableRow]; + + const visibleLabelsAndIndices = this.labelsAndIndices(visibleColumns, merge_duplicate_headers); + const [, visibleIndices] = visibleLabelsAndIndices[hideableRow]; + + const singleColumn = visibleIndices.length === 1; return (
{activeMenu !== 'show/hide' ? null :
- {columns.map(column => { + {indices.map(index => { + const column = columns[index]; + const checked = !hidden_columns || hidden_columns.indexOf(column.id) < 0; - const disabled = singleColumn || (!column.hideable && checked); + const disabled = (singleColumn && checked) || (!column.hideable && checked); return (
); })} @@ -915,20 +934,25 @@ export default class ControlledTable extends PureComponent R.any(column => !!column.hideable, columns); } - private toggleColumn = (column: IColumn) => { + private toggleColumn = (column: IColumn, headerRowIndex: number, mergeDuplicateHeaders: boolean) => { const { + columns, hidden_columns: base, setProps } = this.props; + const ids: string[] = actions.getColumnIds(column, columns, headerRowIndex, mergeDuplicateHeaders); + const hidden_columns = base ? base.slice(0) : []; - const cIndex = hidden_columns.indexOf(column.id); + R.forEach(id => { + const cIndex = hidden_columns.indexOf(id); - if (cIndex >= 0) { - hidden_columns.splice(cIndex, 1); - } else { - hidden_columns.push(column.id); - } + if (cIndex >= 0) { + hidden_columns.splice(cIndex, 1); + } else { + hidden_columns.push(id); + } + }, ids); setProps({ hidden_columns }); } diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index 7080907ee..dacaf55db 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -6,8 +6,7 @@ import { matrixMap2, matrixMap3 } from 'core/math/matrixZipMap'; import derivedHeaderContent from 'dash-table/derived/header/content'; import getHeaderRows from 'dash-table/derived/header/headerRows'; -import getIndices from 'dash-table/derived/header/indices'; -import getLabels from 'dash-table/derived/header/labels'; +import derivedLabelsAndIndices from 'dash-table/derived/header/labelsAndIndices'; import derivedHeaderOperations from 'dash-table/derived/header/operations'; import derivedHeaderWrappers from 'dash-table/derived/header/wrappers'; import { derivedRelevantHeaderStyles } from 'dash-table/derived/style'; @@ -15,7 +14,7 @@ import derivedHeaderStyles, { derivedHeaderOpStyles } from 'dash-table/derived/h import { IEdgesMatrices } from 'dash-table/derived/edges/type'; import { memoizeOne } from 'core/memoizer'; -import { HeaderFactoryProps, Columns } from './Table/props'; +import { HeaderFactoryProps } from './Table/props'; export default class HeaderFactory { private readonly headerContent = derivedHeaderContent(); @@ -24,6 +23,7 @@ export default class HeaderFactory { private readonly headerOpStyles = derivedHeaderOpStyles(); private readonly headerWrappers = derivedHeaderWrappers(); private readonly relevantStyles = derivedRelevantHeaderStyles(); + private readonly labelsAndIndices = derivedLabelsAndIndices(); private get props() { return this.propsFn(); @@ -59,7 +59,7 @@ export default class HeaderFactory { const headerRows = getHeaderRows(visibleColumns); - const labelsAndIndices = this.getLabelsAndIndices(visibleColumns, headerRows, merge_duplicate_headers); + const labelsAndIndices = this.labelsAndIndices(visibleColumns, merge_duplicate_headers); const relevantStyles = this.relevantStyles( style_cell, @@ -124,18 +124,6 @@ export default class HeaderFactory { return this.getCells(ops, headers); } - getLabelsAndIndices = memoizeOne(( - columns: Columns, - headerRows: number, - merge_duplicate_headers: boolean - ) => { - const labels = getLabels(columns, headerRows); - const indices = getIndices(columns, labels, merge_duplicate_headers); - - return R.zip(labels, indices); - - }); - getCells = memoizeOne(( opCells: JSX.Element[][], dataCells: JSX.Element[][] diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index 9212aa58a..5f0566694 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -530,15 +530,6 @@ cursor: pointer; } - .column-header--clear, - .column-header--delete, - .column-header--edit, - .column-header--hide { - &.disabled { - display: none; - } - } - th:hover { .column-header--clear, .column-header--delete, diff --git a/src/dash-table/derived/header/labelsAndIndices.ts b/src/dash-table/derived/header/labelsAndIndices.ts new file mode 100644 index 000000000..8f965f10d --- /dev/null +++ b/src/dash-table/derived/header/labelsAndIndices.ts @@ -0,0 +1,21 @@ +import * as R from 'ramda'; + +import { memoizeOneFactory } from 'core/memoizer'; + +import getHeaderRows from 'dash-table/derived/header/headerRows'; +import getIndices from 'dash-table/derived/header/indices'; +import getLabels from 'dash-table/derived/header/labels'; + +import { Columns } from 'dash-table/components/Table/props'; + +export default memoizeOneFactory(( + columns: Columns, + merge_duplicate_headers: boolean +) => { + const headerRows = getHeaderRows(columns); + + const labels = getLabels(columns, headerRows); + const indices = getIndices(columns, labels, merge_duplicate_headers); + + return R.zip(labels, indices); +}); \ No newline at end of file From bc4b57114be2c1ba9da7e208d4cb18448b63998a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 31 Jul 2019 14:26:56 -0400 Subject: [PATCH 21/32] add new visual tests for hideable columns --- .../components/ControlledTable/index.tsx | 2 +- .../visual/percy-storybook/Hideable.percy.tsx | 168 ++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 tests/visual/percy-storybook/Hideable.percy.tsx diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 6751d4a1f..919915e8a 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -855,7 +855,7 @@ export default class ControlledTable extends PureComponent const headerRows = getHeaderRows(columns); const hideableRow = R.isNil(hideable_row) ? - headerRows : + headerRows - 1 : hideable_row; const labelsAndIndices = this.labelsAndIndices(columns, merge_duplicate_headers); diff --git a/tests/visual/percy-storybook/Hideable.percy.tsx b/tests/visual/percy-storybook/Hideable.percy.tsx new file mode 100644 index 000000000..fb78fa00c --- /dev/null +++ b/tests/visual/percy-storybook/Hideable.percy.tsx @@ -0,0 +1,168 @@ +import parser from 'papaparse'; +import * as R from 'ramda'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import DataTable from 'dash-table/dash/DataTable'; + +import dataset from './../../assets/gapminder.csv'; +const result = parser.parse(dataset, { delimiter: ',', header: true }); + +const getColumns = () => R.addIndex(R.map)( + (id, index) => ({ + name: [ + Math.floor(index / 4).toString(), + Math.floor(index / 2).toString(), + id + ], + id + }), + result.meta.fields +); + +interface ITest { + name: string; + props: any; +} + +const DEFAULT_PROPS = { + id: 'table', + data: result.data.slice(0, 10) +}; + +const variants: ITest[] = [ + { name: 'merged', props: { merge_duplicate_headers: true } }, + { name: 'unmerged', props: { merge_duplicate_headers: false } } +]; + +const scenarios: ITest[] = [ + { + name: 'default (bottom row)', + props: { + columns: getColumns(), + hideable: true + } + }, + { + name: 'explicit bottom row', + props: { + columns: getColumns(), + hideable: true, + hideable_row: 2 + } + }, + { + name: 'explicit middle row', + props: { + columns: getColumns(), + hideable: true, + hideable_row: 1 + } + }, + { + name: 'explicit top row', + props: { + columns: getColumns(), + hideable: true, + hideable_row: 0 + } + }, + { + name: 'some non-hideable top rows', + props: { + columns: getColumns().map((c: any, i) => { + if (i % 8 === 0) { + c.hideable = false; + } + + return c; + }), + hideable: true, + hideable_row: 0 + } + }, + { + name: 'some non-hideable middle rows', + props: { + columns: getColumns().map((c: any, i) => { + if (i % 4 === 0) { + c.hideable = false; + } + + return c; + }), + hideable: true, + hideable_row: 1 + } + }, + { + name: 'some non-hideable bottom rows', + props: { + columns: getColumns().map((c: any, i) => { + if (i % 2 === 0) { + c.hideable = false; + } + + return c; + }), + hideable: true, + hideable_row: 2 + } + }, + { + name: 'some hideable top rows', + props: { + columns: getColumns().map((c: any, i) => { + if (i % 8 === 0) { + c.hideable = true; + } + + return c; + }), + hideable: false, + hideable_row: 0 + } + }, + { + name: 'some hideable middle rows', + props: { + columns: getColumns().map((c: any, i) => { + if (i % 4 === 0) { + c.hideable = true; + } + + return c; + }), + hideable: false, + hideable_row: 1 + } + }, + { + name: 'some hideable bottom rows', + props: { + columns: getColumns().map((c: any, i) => { + if (i % 2 === 0) { + c.hideable = true; + } + + return c; + }), + hideable: false, + hideable_row: 2 + } + } +]; + +const tests = R.xprod(scenarios, variants); + +R.reduce( + (chain, [scenario, variant]) => chain.add(`${scenario.name} (${variant.name})`, () => ()), + storiesOf(`DashTable/Hideable Columns`, module), + tests +); \ No newline at end of file From b01468db717e5d0aa529974469f16280ec89058b Mon Sep 17 00:00:00 2001 From: Marc-Andre-Rivet Date: Wed, 31 Jul 2019 23:33:33 -0400 Subject: [PATCH 22/32] - support hideable on all headers at the same time - update visual tests --- CHANGELOG.md | 2 +- .../components/ControlledTable/index.tsx | 61 ++++++++-------- src/dash-table/components/HeaderFactory.tsx | 5 +- src/dash-table/components/Table/props.ts | 7 +- src/dash-table/dash/DataTable.js | 41 ++++------- src/dash-table/dash/Sanitizer.ts | 6 +- src/dash-table/derived/header/columnFlag.ts | 6 ++ src/dash-table/derived/header/content.tsx | 23 ++----- .../derived/header/labelsAndIndices.ts | 5 +- .../visual/percy-storybook/Hideable.percy.tsx | 69 ++++++++++--------- 10 files changed, 101 insertions(+), 124 deletions(-) create mode 100644 src/dash-table/derived/header/columnFlag.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 129ca76a2..66a917322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added [#314](https://github.com/plotly/dash-table/issues/314) - New `column.hideable` flag that displays an "eye" action icon in the column - Accepts a boolean. Clicking on the "eye" will add the column to the `hidden_columns` prop. + Accepts a boolean or array of booleans. Clicking on the "eye" will add the column to the `hidden_columns` prop. `hidden_columns` can be added back through the Columns toggle menu whether they are hideable or not. [#313](https://github.com/plotly/dash-table/issues/313) diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 919915e8a..285179ca4 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -23,7 +23,7 @@ import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper'; import { ControlledTableProps, ICellFactoryProps, TableAction, IColumn } from 'dash-table/components/Table/props'; import dropdownHelper from 'dash-table/components/dropdownHelper'; -import getHeaderRows from 'dash-table/derived/header/headerRows'; +import getColumnFlag from 'dash-table/derived/header/columnFlag'; import derivedLabelsAndIndices from 'dash-table/derived/header/labelsAndIndices'; import derivedTable from 'dash-table/derived/table'; import derivedTableFragments from 'dash-table/derived/table/fragments'; @@ -824,7 +824,7 @@ export default class ControlledTable extends PureComponent ref={`r${rowIndex}c${columnIndex}`} className={`cell cell-${rowIndex}-${columnIndex} ${c}`} > - {g ? React.cloneElement(g, { style: s.cell }) : g} + {g ? React.cloneElement(g, { style: s.cell }) : g}
))}
))}
@@ -846,25 +846,13 @@ export default class ControlledTable extends PureComponent const { activeMenu, columns, - hideable_row, hidden_columns, merge_duplicate_headers, setState, visibleColumns } = this.props; - const headerRows = getHeaderRows(columns); - const hideableRow = R.isNil(hideable_row) ? - headerRows - 1 : - hideable_row; - - const labelsAndIndices = this.labelsAndIndices(columns, merge_duplicate_headers); - const [, indices] = labelsAndIndices[hideableRow]; - - const visibleLabelsAndIndices = this.labelsAndIndices(visibleColumns, merge_duplicate_headers); - const [, visibleIndices] = visibleLabelsAndIndices[hideableRow]; - - const singleColumn = visibleIndices.length === 1; + const labelsAndIndices = this.labelsAndIndices(columns, visibleColumns, merge_duplicate_headers); return (
{activeMenu !== 'show/hide' ? null :
- {indices.map(index => { + {R.flatten(labelsAndIndices.map(([, indices], i) => indices.map((index, j) => { + const spansAllColumns = indices.length === 1; const column = columns[index]; const checked = !hidden_columns || hidden_columns.indexOf(column.id) < 0; - const disabled = (singleColumn && checked) || (!column.hideable && checked); - - return (
- - -
); - })} + const hideable = getColumnFlag(i, column.hideable); + + const disabled = (spansAllColumns && checked) || (!hideable && checked); + + return { + i: index, + j, + component: (
+ + +
) + }; + }))).sort((a, b) => a.j - b.j).sort((a, b) => a.i - b.i).map(a => a.component)}
}
); diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index dacaf55db..cbcb6313a 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -37,8 +37,8 @@ export default class HeaderFactory { const props = this.props; const { + columns, data, - hideable_row, hidden_columns, map, merge_duplicate_headers, @@ -59,7 +59,7 @@ export default class HeaderFactory { const headerRows = getHeaderRows(visibleColumns); - const labelsAndIndices = this.labelsAndIndices(visibleColumns, merge_duplicate_headers); + const labelsAndIndices = this.labelsAndIndices(columns, visibleColumns, merge_duplicate_headers); const relevantStyles = this.relevantStyles( style_cell, @@ -95,7 +95,6 @@ export default class HeaderFactory { const contents = this.headerContent( visibleColumns, hidden_columns, - hideable_row, data, labelsAndIndices, map, diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 4561bc113..e887282ce 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -156,7 +156,7 @@ export interface IBaseColumn { clearable?: boolean | boolean[]; deletable?: boolean | boolean[]; editable: boolean; - hideable?: boolean; + hideable?: boolean | boolean[]; renamable?: boolean | boolean[]; sort_as_null: SortAsNull; id: ColumnId; @@ -272,8 +272,7 @@ export interface IProps { fill_width?: boolean; filter_query?: string; filter_action?: TableAction; - hideable?: boolean; - hideable_row: number; + hideable?: boolean | boolean[]; hidden_columns?: string[]; locale_format: INumberLocale; merge_duplicate_headers?: boolean; @@ -321,7 +320,7 @@ interface IDefaultProps { fill_width: boolean; filter_query: string; filter_action: TableAction; - hideable: boolean; + hideable: boolean | boolean[]; merge_duplicate_headers: boolean; fixed_columns: Fixed; fixed_rows: Fixed; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index fca3bc959..e39e67d02 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -168,22 +168,27 @@ export const propTypes = { editable: PropTypes.bool, /** - * There are two `hideable` flags in the table. - * This is the column-level hideable flag and there is - * also the table-level `hideable` flag. + * If True, then the column is hideable through the UI. * - * These flags determine if a column is hideable or not. + * If there are merged, multi-header columns then you can choose + * which column header row to display the `hide` action button in by + * supplying an array of booleans. + * For example, `[true, false]` will display the `hide` action button + * on the first row, but not the second row. * - * If True, then the column is hideable through the UI. - * If there are multiple column headers, only the last row of - * the column headers will dispay the `hide` action button. + * If the `hide` action button appears on a merged column, then + * clicking on that button will hide *all* of the merged columns + * associated with it. * * Hidden columns are listed by id in the `hidden_columns` prop. * * If `hidden_columns` is not empty, a toggle button is displayed * above the table that allows toggling columns visibility. */ - hideable: PropTypes.bool, + hideable: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.arrayOf(PropTypes.bool) + ]), /** * If True, then the name of this column is editable. @@ -441,26 +446,6 @@ export const propTypes = { */ editable: PropTypes.bool, - /** - * If True, then the columns will be hideable through the UI. - * When `hideable` is True, particular columns can be made - * un-hideable by setting `hideable` to `False` in the `columns` - * property. - * - * When `hideable` is False, particular columns can be made - * hideable by setting `hideable` to `True` in the `columns` - * property. - */ - hideable: PropTypes.bool, - - /** - * 'beta'. Define the header row on which the hideable action will be - * visible. - * - * Defaults to the last header row. - */ - hideable_row: PropTypes.number, - /** * When selecting multiple cells * (via clicking on a cell and then shift-clicking on another cell), diff --git a/src/dash-table/dash/Sanitizer.ts b/src/dash-table/dash/Sanitizer.ts index e3f0a614d..dc2282630 100644 --- a/src/dash-table/dash/Sanitizer.ts +++ b/src/dash-table/dash/Sanitizer.ts @@ -51,12 +51,10 @@ const applyDefaultsToColumns = ( defaultLocale: INumberLocale, defaultSort: SortAsNull, columns: Columns, - editable: boolean, - hideable: boolean + editable: boolean ) => R.map(column => { const c = R.clone(column); c.editable = resolveFlag(editable, column.editable); - c.hideable = resolveFlag(hideable, column.hideable); c.sort_as_null = c.sort_as_null || defaultSort; if (c.type === ColumnType.Numeric && c.format) { @@ -77,7 +75,7 @@ const getVisibleColumns = ( export default class Sanitizer { sanitize(props: PropsWithDefaults): SanitizedProps { const locale_format = this.applyDefaultToLocale(props.locale_format); - const columns = this.applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable, props.hideable); + const columns = this.applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable); const visibleColumns = this.getVisibleColumns(columns, props.hidden_columns); let headerFormat = props.export_headers; diff --git a/src/dash-table/derived/header/columnFlag.ts b/src/dash-table/derived/header/columnFlag.ts new file mode 100644 index 000000000..ac7b356e7 --- /dev/null +++ b/src/dash-table/derived/header/columnFlag.ts @@ -0,0 +1,6 @@ +export default ( + i: number, + flag?: boolean | boolean[] +): boolean => typeof flag === 'boolean' ? + flag : + !!flag && flag[i]; \ 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 c195d091c..569d280b0 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -16,6 +16,7 @@ import { TableAction, SetFilter } from 'dash-table/components/Table/props'; +import getColumnFlag from 'dash-table/derived/header/columnFlag'; import * as actions from 'dash-table/utils/actions'; import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; import { clearColumnsFilter } from '../filter/map'; @@ -106,16 +107,9 @@ 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: Columns, hiddenColumns: string[] | undefined, - hideable_row: number | undefined, data: Data, labelsAndIndices: R.KeyValuePair[], map: Map, @@ -130,9 +124,6 @@ function getter( return R.addIndex, JSX.Element[]>(R.map)( ([labels, indices], headerRowIndex) => { const isLastRow = headerRowIndex === labelsAndIndices.length - 1; - const isHideableRow = R.isNil(hideable_row) ? - isLastRow : - headerRowIndex === hideable_row; return R.addIndex(R.map)( (columnIndex, index) => { @@ -154,7 +145,7 @@ function getter( const hideable = getColumnFlag(headerRowIndex, column.hideable); const renamable = getColumnFlag(headerRowIndex, column.renamable); - const singleColumn = columns.length === colSpan; + const spansAllColumns = columns.length === colSpan; return (
{sort_action !== TableAction.None && isLastRow ? @@ -189,8 +180,8 @@ function getter( {deletable ? ( { const ids = actions.getColumnIds(column, columns, headerRowIndex, mergeDuplicateHeaders); diff --git a/src/dash-table/derived/header/labelsAndIndices.ts b/src/dash-table/derived/header/labelsAndIndices.ts index 8f965f10d..8c4223b05 100644 --- a/src/dash-table/derived/header/labelsAndIndices.ts +++ b/src/dash-table/derived/header/labelsAndIndices.ts @@ -10,12 +10,13 @@ import { Columns } from 'dash-table/components/Table/props'; export default memoizeOneFactory(( columns: Columns, + usedColumns: Columns, merge_duplicate_headers: boolean ) => { const headerRows = getHeaderRows(columns); - const labels = getLabels(columns, headerRows); - const indices = getIndices(columns, labels, merge_duplicate_headers); + const labels = getLabels(usedColumns, headerRows); + const indices = getIndices(usedColumns, labels, merge_duplicate_headers); return R.zip(labels, indices); }); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Hideable.percy.tsx b/tests/visual/percy-storybook/Hideable.percy.tsx index fb78fa00c..e93fd417e 100644 --- a/tests/visual/percy-storybook/Hideable.percy.tsx +++ b/tests/visual/percy-storybook/Hideable.percy.tsx @@ -37,118 +37,121 @@ const variants: ITest[] = [ const scenarios: ITest[] = [ { - name: 'default (bottom row)', + name: 'default (all)', props: { - columns: getColumns(), - hideable: true + columns: getColumns().map((c: any) => { + c.hideable = true; + + return c; + }) } }, { name: 'explicit bottom row', props: { - columns: getColumns(), - hideable: true, - hideable_row: 2 + columns: getColumns().map((c: any) => { + c.hideable = [false, false, true]; + + return c; + }) } }, { name: 'explicit middle row', props: { - columns: getColumns(), - hideable: true, - hideable_row: 1 + columns: getColumns().map((c: any) => { + c.hideable = [false, true, false]; + + return c; + }) } }, { name: 'explicit top row', props: { - columns: getColumns(), - hideable: true, - hideable_row: 0 + columns: getColumns().map((c: any) => { + c.hideable = [true, false, false]; + + return c; + }) } }, { name: 'some non-hideable top rows', props: { columns: getColumns().map((c: any, i) => { + c.hideable = [true, false, false]; if (i % 8 === 0) { c.hideable = false; } return c; - }), - hideable: true, - hideable_row: 0 + }) } }, { name: 'some non-hideable middle rows', props: { columns: getColumns().map((c: any, i) => { + c.hideable = [false, true, false]; if (i % 4 === 0) { c.hideable = false; } return c; - }), - hideable: true, - hideable_row: 1 + }) } }, { name: 'some non-hideable bottom rows', props: { columns: getColumns().map((c: any, i) => { + c.hideable = [false, false, true]; if (i % 2 === 0) { c.hideable = false; } return c; - }), - hideable: true, - hideable_row: 2 + }) } }, { name: 'some hideable top rows', props: { columns: getColumns().map((c: any, i) => { + c.hideable = false; if (i % 8 === 0) { - c.hideable = true; + c.hideable = [true, false, false]; } return c; - }), - hideable: false, - hideable_row: 0 + }) } }, { name: 'some hideable middle rows', props: { columns: getColumns().map((c: any, i) => { + c.hideable = false; if (i % 4 === 0) { - c.hideable = true; + c.hideable = [false, true, false]; } return c; - }), - hideable: false, - hideable_row: 1 + }) } }, { name: 'some hideable bottom rows', props: { columns: getColumns().map((c: any, i) => { + c.hideable = false; if (i % 2 === 0) { - c.hideable = true; + c.hideable = [false, false, true]; } return c; - }), - hideable: false, - hideable_row: 2 + }) } } ]; From 53555592f836d46cfaa0968a46726974b58b97d0 Mon Sep 17 00:00:00 2001 From: Marc-Andre-Rivet Date: Wed, 31 Jul 2019 23:45:37 -0400 Subject: [PATCH 23/32] - update standalone test to use only last column --- demo/AppMode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 178eb197d..788823a74 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -192,7 +192,7 @@ function getActionableState() { R.forEach(c => { c.clearable = true; - c.hideable = true; + c.hideable = [false, false, true]; }, state.tableProps.columns || []); return state; From 1a5aba168726b2b32d4afcd0bf70c37c687a1ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 10:23:27 -0400 Subject: [PATCH 24/32] - add support for 'last' in column.hideable --- .../components/ControlledTable/index.tsx | 18 +++++++------ src/dash-table/components/EdgeFactory.tsx | 9 ++++--- src/dash-table/components/Export/index.tsx | 11 ++++---- src/dash-table/components/HeaderFactory.tsx | 26 ++++++++++++------- src/dash-table/components/Table/props.ts | 2 +- src/dash-table/dash/DataTable.js | 1 + src/dash-table/derived/header/columnFlag.ts | 11 +++++--- src/dash-table/derived/header/content.tsx | 11 ++++---- 8 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 285179ca4..fdc7c887c 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -700,6 +700,7 @@ export default class ControlledTable extends PureComponent render() { const { + columns, id, tooltip_conditional, tooltip, @@ -785,7 +786,8 @@ export default class ControlledTable extends PureComponent const buttonProps = { export_format, virtual_data: virtual, - columns: visibleColumns, + columns, + visibleColumns, export_headers }; @@ -848,11 +850,11 @@ export default class ControlledTable extends PureComponent columns, hidden_columns, merge_duplicate_headers, - setState, - visibleColumns + setState } = this.props; - const labelsAndIndices = this.labelsAndIndices(columns, visibleColumns, merge_duplicate_headers); + const labelsAndIndices = this.labelsAndIndices(columns, columns, merge_duplicate_headers); + const lastRow = labelsAndIndices.length - 1; return (
{activeMenu !== 'show/hide' ? null :
- {R.flatten(labelsAndIndices.map(([, indices], i) => indices.map((index, j) => { + {R.unnest(labelsAndIndices.map(([, indices], i) => indices.map((index, j) => { const spansAllColumns = indices.length === 1; const column = columns[index]; const checked = !hidden_columns || hidden_columns.indexOf(column.id) < 0; - const hideable = getColumnFlag(i, column.hideable); + const hideable = getColumnFlag(i, lastRow, column.hideable); const disabled = (spansAllColumns && checked) || (!hideable && checked); return { i: index, j, - component: (
+ component: !hideable ? null : (
}
) }; - }))).sort((a, b) => a.j - b.j).sort((a, b) => a.i - b.i).map(a => a.component)} + }))).filter(i => !R.isNil(i)).sort((a, b) => a.j - b.j).sort((a, b) => a.i - b.i).map(a => a.component)}
}
); diff --git a/src/dash-table/components/EdgeFactory.tsx b/src/dash-table/components/EdgeFactory.tsx index 4478998f2..6cf20fd5a 100644 --- a/src/dash-table/components/EdgeFactory.tsx +++ b/src/dash-table/components/EdgeFactory.tsx @@ -139,6 +139,7 @@ export default class EdgeFactory { public createEdges() { const { active_cell, + columns, filter_action, workFilter, fixed_columns, @@ -160,6 +161,7 @@ export default class EdgeFactory { return this.memoizedCreateEdges( active_cell, + columns, visibleColumns, (row_deletable ? 1 : 0) + (row_selectable ? 1 : 0), filter_action !== TableAction.None, @@ -183,6 +185,7 @@ export default class EdgeFactory { private memoizedCreateEdges = memoizeOne(( active_cell: ICellCoordinates, columns: Columns, + visibleColumns: Columns, operations: number, filter_action: boolean, filterMap: Map, @@ -224,7 +227,7 @@ export default class EdgeFactory { const headerRows = getHeaderRows(columns); let dataEdges = this.getDataEdges( - columns, + visibleColumns, dataStyles, data, offset, @@ -241,7 +244,7 @@ export default class EdgeFactory { ); let filterEdges = this.getFilterEdges( - columns, + visibleColumns, filter_action, filterMap, filterStyles, @@ -256,7 +259,7 @@ export default class EdgeFactory { ); let headerEdges = this.getHeaderEdges( - columns, + visibleColumns, headerRows, headerStyles, style_as_list_view diff --git a/src/dash-table/components/Export/index.tsx b/src/dash-table/components/Export/index.tsx index 245272b5a..46b112076 100644 --- a/src/dash-table/components/Export/index.tsx +++ b/src/dash-table/components/Export/index.tsx @@ -1,24 +1,25 @@ import XLSX from 'xlsx'; import React from 'react'; -import { IDerivedData, IColumn } from 'dash-table/components/Table/props'; +import { IDerivedData, Columns } from 'dash-table/components/Table/props'; import { createWorkbook, createHeadings, createWorksheet } from './utils'; import getHeaderRows from 'dash-table/derived/header/headerRows'; interface IExportButtonProps { + columns: Columns; export_format: string; virtual_data: IDerivedData; - columns: IColumn[]; + visibleColumns: Columns; export_headers: string; } export default React.memo((props: IExportButtonProps) => { - const { columns, export_format, virtual_data, export_headers } = props; + const { columns, export_format, virtual_data, export_headers, visibleColumns } = props; const isFormatSupported = export_format === 'csv' || export_format === 'xlsx'; const handleExport = () => { - const columnID = columns.map(column => column.id); - const columnHeaders = columns.map(column => column.name); + const columnID = visibleColumns.map(column => column.id); + const columnHeaders = visibleColumns.map(column => column.name); const maxLength = getHeaderRows(columns); const heading = (export_headers !== 'none') ? createHeadings(columnHeaders, maxLength) : []; const ws = createWorksheet(heading, virtual_data.data, columnID, export_headers); diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index cbcb6313a..f196de7e1 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -5,7 +5,6 @@ import { arrayMap2 } from 'core/math/arrayZipMap'; import { matrixMap2, matrixMap3 } from 'core/math/matrixZipMap'; import derivedHeaderContent from 'dash-table/derived/header/content'; -import getHeaderRows from 'dash-table/derived/header/headerRows'; import derivedLabelsAndIndices from 'dash-table/derived/header/labelsAndIndices'; import derivedHeaderOperations from 'dash-table/derived/header/operations'; import derivedHeaderWrappers from 'dash-table/derived/header/wrappers'; @@ -57,9 +56,8 @@ export default class HeaderFactory { visibleColumns } = props; - const headerRows = getHeaderRows(visibleColumns); - const labelsAndIndices = this.labelsAndIndices(columns, visibleColumns, merge_duplicate_headers); + const headerRows = labelsAndIndices.length; const relevantStyles = this.relevantStyles( style_cell, @@ -113,6 +111,11 @@ export default class HeaderFactory { headerOpEdges ); + console.log('getHeaderCells, wrappers', wrappers); + console.log('getHeaderCells, contents', contents); + console.log('getHeaderCells, wrapperStyles', wrapperStyles); + console.log('getHeaderCells, headerEdges', headerEdges); + const headers = this.getHeaderCells( wrappers, contents, @@ -157,12 +160,15 @@ export default class HeaderFactory { wrappers, styles, contents, - (w, s, c, i, j) => React.cloneElement(w, { - children: [c], - style: R.mergeAll([ - s, - edges && edges.getStyle(i, j) - ]) - }) + (w, s, c, i, j) => { + console.log('it', w, s, c, i, j); + return React.cloneElement(w, { + children: [c], + style: R.mergeAll([ + s, + edges && edges.getStyle(i, j) + ]) + }); + } )); } diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index e887282ce..7968bb4a1 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -156,7 +156,7 @@ export interface IBaseColumn { clearable?: boolean | boolean[]; deletable?: boolean | boolean[]; editable: boolean; - hideable?: boolean | boolean[]; + hideable?: boolean | boolean[] | 'last'; renamable?: boolean | boolean[]; sort_as_null: SortAsNull; id: ColumnId; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index e39e67d02..16bf3a8d1 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -186,6 +186,7 @@ export const propTypes = { * above the table that allows toggling columns visibility. */ hideable: PropTypes.oneOfType([ + PropTypes.oneOf(['last']), PropTypes.bool, PropTypes.arrayOf(PropTypes.bool) ]), diff --git a/src/dash-table/derived/header/columnFlag.ts b/src/dash-table/derived/header/columnFlag.ts index ac7b356e7..ec188e02e 100644 --- a/src/dash-table/derived/header/columnFlag.ts +++ b/src/dash-table/derived/header/columnFlag.ts @@ -1,6 +1,9 @@ export default ( i: number, - flag?: boolean | boolean[] -): boolean => typeof flag === 'boolean' ? - flag : - !!flag && flag[i]; \ No newline at end of file + last: number, + flag?: boolean | boolean[] | 'last' +): boolean => typeof flag === 'string' ? + i === last : + typeof flag === 'boolean' ? + flag : + !!flag && flag[i]; \ 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 569d280b0..6c606ba1e 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -123,7 +123,8 @@ function getter( ): JSX.Element[][] { return R.addIndex, JSX.Element[]>(R.map)( ([labels, indices], headerRowIndex) => { - const isLastRow = headerRowIndex === labelsAndIndices.length - 1; + const lastRow = labelsAndIndices.length - 1; + const isLastRow = headerRowIndex === lastRow; return R.addIndex(R.map)( (columnIndex, index) => { @@ -140,10 +141,10 @@ function getter( } } - const clearable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, column.clearable); - const deletable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, column.deletable); - const hideable = getColumnFlag(headerRowIndex, column.hideable); - const renamable = getColumnFlag(headerRowIndex, column.renamable); + const clearable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, lastRow, column.clearable); + const deletable = paginationMode !== TableAction.Custom && getColumnFlag(headerRowIndex, lastRow, column.deletable); + const hideable = getColumnFlag(headerRowIndex, lastRow, column.hideable); + const renamable = getColumnFlag(headerRowIndex, lastRow, column.renamable); const spansAllColumns = columns.length === colSpan; From e5e7e7fc6194a63bebf6c859c06566f5e4c2feee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 10:24:22 -0400 Subject: [PATCH 25/32] fix lint --- src/dash-table/components/HeaderFactory.tsx | 22 +++++++-------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index f196de7e1..35f82fb52 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -111,11 +111,6 @@ export default class HeaderFactory { headerOpEdges ); - console.log('getHeaderCells, wrappers', wrappers); - console.log('getHeaderCells, contents', contents); - console.log('getHeaderCells, wrapperStyles', wrapperStyles); - console.log('getHeaderCells, headerEdges', headerEdges); - const headers = this.getHeaderCells( wrappers, contents, @@ -160,15 +155,12 @@ export default class HeaderFactory { wrappers, styles, contents, - (w, s, c, i, j) => { - console.log('it', w, s, c, i, j); - return React.cloneElement(w, { - children: [c], - style: R.mergeAll([ - s, - edges && edges.getStyle(i, j) - ]) - }); - } + (w, s, c, i, j) => React.cloneElement(w, { + children: [c], + style: R.mergeAll([ + s, + edges && edges.getStyle(i, j) + ]) + }) )); } From c178da8df161823406fd82d9da2946d18c8ef987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 10:50:14 -0400 Subject: [PATCH 26/32] - add 'last' to clearable, deletable, renamable --- src/dash-table/components/Table/props.ts | 6 +++--- src/dash-table/dash/DataTable.js | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 7968bb4a1..87fc22c7c 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -153,11 +153,11 @@ export interface IDatetimeColumn extends ITypeColumn { } export interface IBaseColumn { - clearable?: boolean | boolean[]; - deletable?: boolean | boolean[]; + clearable?: boolean | boolean[] | 'last'; + deletable?: boolean | boolean[] | 'last'; editable: boolean; hideable?: boolean | boolean[] | 'last'; - renamable?: boolean | boolean[]; + renamable?: boolean | boolean[] | 'last'; sort_as_null: SortAsNull; id: ColumnId; name: string | string[]; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 16bf3a8d1..07d67794b 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -134,6 +134,7 @@ export const propTypes = { * from the table. It only removed the associated entries from `data`. */ clearable: PropTypes.oneOfType([ + PropTypes.oneOf(['last']), PropTypes.bool, PropTypes.arrayOf(PropTypes.bool) ]), @@ -150,6 +151,7 @@ export const propTypes = { * will delete *all* of the merged columns associated with it. */ deletable: PropTypes.oneOfType([ + PropTypes.oneOf(['last']), PropTypes.bool, PropTypes.arrayOf(PropTypes.bool) ]), @@ -202,6 +204,7 @@ export const propTypes = { * update the name of each column. */ renamable: PropTypes.oneOfType([ + PropTypes.oneOf(['last']), PropTypes.bool, PropTypes.arrayOf(PropTypes.bool) ]), From 4fcd8e3bc0d603a36ffe955df8dbd2c4eec8bd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 11:01:40 -0400 Subject: [PATCH 27/32] - add support for 'first' too --- src/dash-table/components/Table/props.ts | 8 ++++---- src/dash-table/dash/DataTable.js | 8 ++++---- src/dash-table/derived/header/columnFlag.ts | 12 +++++++----- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 87fc22c7c..72fdd8060 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -153,11 +153,11 @@ export interface IDatetimeColumn extends ITypeColumn { } export interface IBaseColumn { - clearable?: boolean | boolean[] | 'last'; - deletable?: boolean | boolean[] | 'last'; + clearable?: boolean | boolean[] | 'first' | 'last'; + deletable?: boolean | boolean[] | 'first' |'last'; editable: boolean; - hideable?: boolean | boolean[] | 'last'; - renamable?: boolean | boolean[] | 'last'; + hideable?: boolean | boolean[] | 'first' | 'last'; + renamable?: boolean | boolean[] | 'first' | 'last'; sort_as_null: SortAsNull; id: ColumnId; name: string | string[]; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 07d67794b..10bf9f11a 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -134,7 +134,7 @@ export const propTypes = { * from the table. It only removed the associated entries from `data`. */ clearable: PropTypes.oneOfType([ - PropTypes.oneOf(['last']), + PropTypes.oneOf(['first', 'last']), PropTypes.bool, PropTypes.arrayOf(PropTypes.bool) ]), @@ -151,7 +151,7 @@ export const propTypes = { * will delete *all* of the merged columns associated with it. */ deletable: PropTypes.oneOfType([ - PropTypes.oneOf(['last']), + PropTypes.oneOf(['first', 'last']), PropTypes.bool, PropTypes.arrayOf(PropTypes.bool) ]), @@ -188,7 +188,7 @@ export const propTypes = { * above the table that allows toggling columns visibility. */ hideable: PropTypes.oneOfType([ - PropTypes.oneOf(['last']), + PropTypes.oneOf(['first', 'last']), PropTypes.bool, PropTypes.arrayOf(PropTypes.bool) ]), @@ -204,7 +204,7 @@ export const propTypes = { * update the name of each column. */ renamable: PropTypes.oneOfType([ - PropTypes.oneOf(['last']), + PropTypes.oneOf(['first', 'last']), PropTypes.bool, PropTypes.arrayOf(PropTypes.bool) ]), diff --git a/src/dash-table/derived/header/columnFlag.ts b/src/dash-table/derived/header/columnFlag.ts index ec188e02e..ff34cb3ae 100644 --- a/src/dash-table/derived/header/columnFlag.ts +++ b/src/dash-table/derived/header/columnFlag.ts @@ -1,9 +1,11 @@ export default ( i: number, last: number, - flag?: boolean | boolean[] | 'last' -): boolean => typeof flag === 'string' ? + flag?: boolean | boolean[] | 'first' | 'last' +): boolean => flag === 'last' ? i === last : - typeof flag === 'boolean' ? - flag : - !!flag && flag[i]; \ No newline at end of file + flag === 'first' ? + i === 0 : + typeof flag === 'boolean' ? + flag : + !!flag && flag[i]; \ No newline at end of file From 80e513774ab080fd54a2565a4ef3492edb326aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 11:05:57 -0400 Subject: [PATCH 28/32] extra visual tests for first and last --- .../percy-storybook/Header.actions.percy.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/visual/percy-storybook/Header.actions.percy.tsx b/tests/visual/percy-storybook/Header.actions.percy.tsx index 0f61eaa25..7db9ab188 100644 --- a/tests/visual/percy-storybook/Header.actions.percy.tsx +++ b/tests/visual/percy-storybook/Header.actions.percy.tsx @@ -77,6 +77,26 @@ const scenarios: ITest[] = [ }, COLUMNS_BASE) } }, { + name: 'clearable (first-city, last-climate)', + props: { + columns: R.map((c: any) => { + const firstName = c.name[0]; + + if (firstName === 'City') { + return R.mergeRight(c, { + clearable: 'first' + }); + } else if (firstName === 'Climate') { + return R.mergeRight(c, { + clearable: 'last' + }); + + } else { + return c; + } + }, COLUMNS_BASE) + } + }, { name: 'deletable', props: { columns: R.map(c => R.mergeRight(c, { @@ -104,6 +124,26 @@ const scenarios: ITest[] = [ }, COLUMNS_BASE) } }, { + name: 'deletable (first-city, last-climate)', + props: { + columns: R.map((c: any) => { + const firstName = c.name[0]; + + if (firstName === 'City') { + return R.mergeRight(c, { + deletable: 'first' + }); + } else if (firstName === 'Climate') { + return R.mergeRight(c, { + deletable: 'last' + }); + + } else { + return c; + } + }, COLUMNS_BASE) + } + }, { name: 'hideable', props: { columns: R.map(c => R.mergeRight(c, { @@ -131,6 +171,26 @@ const scenarios: ITest[] = [ }, COLUMNS_BASE) } }, { + name: 'hideable (first-city, last-climate)', + props: { + columns: R.map((c: any) => { + const firstName = c.name[0]; + + if (firstName === 'City') { + return R.mergeRight(c, { + hideable: 'first' + }); + } else if (firstName === 'Climate') { + return R.mergeRight(c, { + hideable: 'last' + }); + + } else { + return c; + } + }, COLUMNS_BASE) + } + }, { name: 'clearable+deletable+hideable', props: { columns: R.map(c => R.mergeRight(c, { From d01777c8462ceb1179662154171d589351028f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 11:19:25 -0400 Subject: [PATCH 29/32] - update changelog - update docstrings --- CHANGELOG.md | 6 ++- src/dash-table/dash/DataTable.js | 88 +++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a917322..59870ec86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added [#314](https://github.com/plotly/dash-table/issues/314) - New `column.hideable` flag that displays an "eye" action icon in the column - Accepts a boolean or array of booleans. Clicking on the "eye" will add the column to the `hidden_columns` prop. + Accepts a boolean, array of booleans, 'last' or 'first'. Clicking on the "eye" will add the column to the `hidden_columns` prop. `hidden_columns` can be added back through the Columns toggle menu whether they are hideable or not. +- New accepted values for `column.clearable`, `column.deletable` and `column.renamable` + These props now also accept 'last' and 'first'. + - 'last' will display the action only on the last row of the headers + - 'first' will display the action only on the first row of the headers [#313](https://github.com/plotly/dash-table/issues/313) - Ability to export table as csv or xlsx file. diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 10bf9f11a..4b648d230 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -120,15 +120,23 @@ 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 True, the user can clear the column by clicking on the `clear` + * action button on the column. If there are multiple header rows, True + * will display the action button on each row. + * + * If `last`, the `clear` action button will only appear on the last header + * row. If `first` it will only appear on the first header row. These + * are respectively shortcut equivalents to `[False, ..., True]` and + * `[True, ..., False]`. + * * If there are merged, multi-header columns then you can choose - * which column header row to display the "Ø" in by + * which column header row to display the `clear` action button 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. + * For example, `[true, false]` will display the `clear` action button + * on the first row, but not the second row. + * + * If the `clear` action button 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`. @@ -140,15 +148,23 @@ export const propTypes = { ]), /** - * If True, the user can delete the column by clicking on a little `x` - * button on the column. + * If True, the user can remove the column by clicking on the `delete` + * action button on the column. If there are multiple header rows, True + * will display the action button on each row. + * + * If `last`, the `delete` action button will only appear on the last header + * row. If `first` it will only appear on the first header row. These + * are respectively shortcut equivalents to `[False, ..., True]` and + * `[True, ..., False]`. + * * If there are merged, multi-header columns then you can choose - * which column header row to display the "x" in by + * which column header row to display the `delete` action button in by * 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. + * For example, `[true, false]` will display the `delete` action button + * on the first row, but not the second row. + * + * If the `delete` action button appears on a merged column, then clicking + * on that button will remove *all* of the merged columns associated with it. */ deletable: PropTypes.oneOfType([ PropTypes.oneOf(['first', 'last']), @@ -170,7 +186,14 @@ export const propTypes = { editable: PropTypes.bool, /** - * If True, then the column is hideable through the UI. + * If True, the user can hide the column by clicking on the `hide` + * action button on the column. If there are multiple header rows, True + * will display the action button on each row. + * + * If `last`, the `hide` action button will only appear on the last header + * row. If `first` it will only appear on the first header row. These + * are respectively shortcut equivalents to `[False, ..., True]` and + * `[True, ..., False]`. * * If there are merged, multi-header columns then you can choose * which column header row to display the `hide` action button in by @@ -178,14 +201,8 @@ export const propTypes = { * For example, `[true, false]` will display the `hide` action button * on the first row, but not the second row. * - * If the `hide` action button appears on a merged column, then - * clicking on that button will hide *all* of the merged columns - * associated with it. - * - * Hidden columns are listed by id in the `hidden_columns` prop. - * - * If `hidden_columns` is not empty, a toggle button is displayed - * above the table that allows toggling columns visibility. + * If the `hide` action button appears on a merged column, then clicking + * on that button will hide *all* of the merged columns associated with it. */ hideable: PropTypes.oneOfType([ PropTypes.oneOf(['first', 'last']), @@ -194,14 +211,23 @@ 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 `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. + * If True, the user can rename the column by clicking on the `rename` + * action button on the column. If there are multiple header rows, True + * will display the action button on each row. + * + * If `last`, the `rename` action button will only appear on the last header + * row. If `first` it will only appear on the first header row. These + * are respectively shortcut equivalents to `[False, ..., True]` and + * `[True, ..., False]`. + * + * If there are merged, multi-header columns then you can choose + * which column header row to display the `rename` action button in by + * supplying an array of booleans. + * For example, `[true, false]` will display the `rename` action button + * on the first row, but not the second row. + * + * If the `rename` action button appears on a merged column, then clicking + * on that button will rename *all* of the merged columns associated with it. */ renamable: PropTypes.oneOfType([ PropTypes.oneOf(['first', 'last']), From 630345a9b614e046baaa739b7c8175c1164a4f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 11:31:06 -0400 Subject: [PATCH 30/32] remove still remaining table-level hideable prop code --- src/dash-table/components/Table/props.ts | 2 -- src/dash-table/dash/DataTable.js | 1 - 2 files changed, 3 deletions(-) diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 72fdd8060..3905fb2e2 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -272,7 +272,6 @@ export interface IProps { fill_width?: boolean; filter_query?: string; filter_action?: TableAction; - hideable?: boolean | boolean[]; hidden_columns?: string[]; locale_format: INumberLocale; merge_duplicate_headers?: boolean; @@ -320,7 +319,6 @@ interface IDefaultProps { fill_width: boolean; filter_query: string; filter_action: TableAction; - hideable: boolean | boolean[]; merge_duplicate_headers: boolean; fixed_columns: Fixed; fixed_rows: Fixed; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 4b648d230..f20d40695 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -88,7 +88,6 @@ export const defaultProps = { columns: [], editable: false, export_format: 'none', - hideable: false, selected_cells: [], selected_rows: [], selected_row_ids: [], From b9e235f4497ced79efe24fc6dc013c6717a61a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 12:14:06 -0400 Subject: [PATCH 31/32] simplify toggle menu sorting --- src/dash-table/components/ControlledTable/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index fdc7c887c..6d80f0228 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -895,7 +895,7 @@ export default class ControlledTable extends PureComponent }
) }; - }))).filter(i => !R.isNil(i)).sort((a, b) => a.j - b.j).sort((a, b) => a.i - b.i).map(a => a.component)} + }))).filter(i => !R.isNil(i)).sort((a, b) => (a.i - b.i) || (a.j - b.j)).map(a => a.component)}
}
); From b3ff548f44e9d1d55bbbfc576235965ebbec6eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 1 Aug 2019 14:37:58 -0400 Subject: [PATCH 32/32] - switch to js true/false from python True/False - improve boolean array usage examples --- src/dash-table/dash/DataTable.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index f20d40695..21f70b1b8 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -119,14 +119,14 @@ export const propTypes = { columns: PropTypes.arrayOf(PropTypes.exact({ /** - * If True, the user can clear the column by clicking on the `clear` - * action button on the column. If there are multiple header rows, True + * If true, the user can clear the column by clicking on the `clear` + * action button on the column. If there are multiple header rows, true * will display the action button on each row. * * If `last`, the `clear` action button will only appear on the last header * row. If `first` it will only appear on the first header row. These - * are respectively shortcut equivalents to `[False, ..., True]` and - * `[True, ..., False]`. + * are respectively shortcut equivalents to `[false, ..., false, true]` and + * `[true, ..., true, false]`. * * If there are merged, multi-header columns then you can choose * which column header row to display the `clear` action button in by @@ -147,14 +147,14 @@ export const propTypes = { ]), /** - * If True, the user can remove the column by clicking on the `delete` - * action button on the column. If there are multiple header rows, True + * If true, the user can remove the column by clicking on the `delete` + * action button on the column. If there are multiple header rows, true * will display the action button on each row. * * If `last`, the `delete` action button will only appear on the last header * row. If `first` it will only appear on the first header row. These - * are respectively shortcut equivalents to `[False, ..., True]` and - * `[True, ..., False]`. + * are respectively shortcut equivalents to `[false, ..., false, true]` and + * `[true, ..., true, false]`. * * If there are merged, multi-header columns then you can choose * which column header row to display the `delete` action button in by @@ -185,14 +185,14 @@ export const propTypes = { editable: PropTypes.bool, /** - * If True, the user can hide the column by clicking on the `hide` - * action button on the column. If there are multiple header rows, True + * If true, the user can hide the column by clicking on the `hide` + * action button on the column. If there are multiple header rows, true * will display the action button on each row. * * If `last`, the `hide` action button will only appear on the last header * row. If `first` it will only appear on the first header row. These - * are respectively shortcut equivalents to `[False, ..., True]` and - * `[True, ..., False]`. + * are respectively shortcut equivalents to `[false, ..., false, true]` and + * `[true, ..., true, false]`. * * If there are merged, multi-header columns then you can choose * which column header row to display the `hide` action button in by @@ -210,14 +210,14 @@ export const propTypes = { ]), /** - * If True, the user can rename the column by clicking on the `rename` - * action button on the column. If there are multiple header rows, True + * If true, the user can rename the column by clicking on the `rename` + * action button on the column. If there are multiple header rows, true * will display the action button on each row. * * If `last`, the `rename` action button will only appear on the last header * row. If `first` it will only appear on the first header row. These - * are respectively shortcut equivalents to `[False, ..., True]` and - * `[True, ..., False]`. + * are respectively shortcut equivalents to `[false, ..., false, true]` and + * `[true, ..., true, false]`. * * If there are merged, multi-header columns then you can choose * which column header row to display the `rename` action button in by