diff --git a/packages/dash-table/CHANGELOG.md b/packages/dash-table/CHANGELOG.md index 7a318398c5..8cad4e7527 100644 --- a/packages/dash-table/CHANGELOG.md +++ b/packages/dash-table/CHANGELOG.md @@ -29,6 +29,26 @@ This project adheres to [Semantic Versioning](http://semver.org/). - `derived_viewport_selected_row_ids` mirrors `derived_viewport_selected_rows` - `derived_virtual_selected_row_ids` mirrors `derived_virtual_selected_rows` +[#424](https://github.com/plotly/dash-table/pull/424) +- Customizable cell borders through `style_**` props + - cell borders now no longer use `box-shadow` and use `border` instead + - Supports CSS shorthands: + border, border_bottom, border_left, border_right, border_top + - style_** props will ignore the following CSS rules: + border_bottom_color, border_bottom_left_radius, border_bottom_right_radius, border_bottom_style, border_bottom_width, border_collapse, border_color, border_corner_shape, border_image_source, border_image_width, border_left_color, border_left_style, border_left_width, border_right_color, border_right_style, border_right_width, border_spacing, border_style, border_top_color, border_top_left_radius, border_top_right_radius, border_top_style, border_top_width, border_width + - Styles priority: + 1. Props priority in decreasing order + style_data_conditional + style_data + style_filter_conditional + style_filter + style_header_conditional + style_header + style_cell_conditional + style_cell + 2. Within each props, higher index rules win over lower index rules + 3. Previously applied styles of equal priority win over later ones (applied top to bottom, left to right) + ### Changed [#397](https://github.com/plotly/dash-table/pull/397) - Rename `filtering_settings` to `filter` diff --git a/packages/dash-table/src/core/environment/index.ts b/packages/dash-table/src/core/environment/index.ts index dfd399ad66..8343129f97 100644 --- a/packages/dash-table/src/core/environment/index.ts +++ b/packages/dash-table/src/core/environment/index.ts @@ -1,9 +1,14 @@ -import { DebugLevel, LogLevel } from 'core/Logger'; import CookieStorage from 'core/storage/Cookie'; +import { DebugLevel, LogLevel } from 'core/Logger'; + +import { Edge } from 'dash-table/derived/edges/type'; const DASH_DEBUG = 'dash_debug'; const DASH_LOG = 'dash_log'; +const DEFAULT_EDGE: Edge = '1px solid #d3d3d3'; +const ACTIVE_EDGE: Edge = '1px solid var(--accent)'; + interface ISearchParams { get: (key: string) => string | null; } @@ -33,4 +38,12 @@ export default class Environment { (LogLevel as any)[log] || LogLevel.ERROR : LogLevel.ERROR; } + + public static get defaultEdge(): Edge { + return DEFAULT_EDGE; + } + + public static get activeEdge(): Edge { + return ACTIVE_EDGE; + } } \ No newline at end of file diff --git a/packages/dash-table/src/core/math/arrayZipMap.ts b/packages/dash-table/src/core/math/arrayZipMap.ts index 2c3909399f..36276b3700 100644 --- a/packages/dash-table/src/core/math/arrayZipMap.ts +++ b/packages/dash-table/src/core/math/arrayZipMap.ts @@ -2,7 +2,16 @@ import * as R from 'ramda'; type Array = T[]; -export function arrayMap( +export function arrayMap( + a1: Array, + cb: (d1: T1, i: number) => TR +) { + const mapArray = R.addIndex(R.map); + + return mapArray((iValue, i) => cb(iValue, i), a1); +} + +export function arrayMap2( a1: Array, a2: Array, cb: (d1: T1, d2: T2, i: number) => TR diff --git a/packages/dash-table/src/core/math/matrixZipMap.ts b/packages/dash-table/src/core/math/matrixZipMap.ts index 874da4af66..876079ad48 100644 --- a/packages/dash-table/src/core/math/matrixZipMap.ts +++ b/packages/dash-table/src/core/math/matrixZipMap.ts @@ -2,17 +2,37 @@ import * as R from 'ramda'; type Matrix = T[][]; -export function matrixMap( +export function matrixMap( m1: Matrix, - m2: Matrix, - cb: (d1: T1, d2: T2, i: number, j: number) => TR + cb: (d1: T1, i: number, j: number) => TR ) { const mapMatrix = R.addIndex(R.map); const mapRow = R.addIndex(R.map); return mapMatrix((iRow, i) => mapRow( - (ijValue, j) => cb(ijValue, m2[i][j], i, j), + (ijValue, j) => cb(ijValue, i, j), + iRow + ), m1 + ); +} + +export function matrixMap2( + m1: Matrix, + m2: Matrix | undefined, + cb: (d1: T1, d2: T2 | undefined, i: number, j: number) => TR +) { + const mapMatrix = R.addIndex(R.map); + const mapRow = R.addIndex(R.map); + + return mapMatrix((iRow, i) => + mapRow( + (ijValue, j) => cb( + ijValue, + m2 ? m2[i][j] : undefined, + i, + j + ), iRow ), m1 ); @@ -20,16 +40,22 @@ export function matrixMap( export function matrixMap3( m1: Matrix, - m2: Matrix, - m3: Matrix, - cb: (d1: T1, d2: T2, d3: T3, i: number, j: number) => TR + m2: Matrix | undefined, + m3: Matrix | undefined, + cb: (d1: T1, d2: T2 | undefined, d3: T3 | undefined, i: number, j: number) => TR ) { const mapMatrix = R.addIndex(R.map); const mapRow = R.addIndex(R.map); return mapMatrix((iRow, i) => mapRow( - (ijValue, j) => cb(ijValue, m2[i][j], m3[i][j], i, j), + (ijValue, j) => cb( + ijValue, + m2 ? m2[i][j] : undefined, + m3 ? m3[i][j] : undefined, + i, + j + ), iRow ), m1 ); @@ -37,17 +63,24 @@ export function matrixMap3( export function matrixMap4( m1: Matrix, - m2: Matrix, - m3: Matrix, - m4: Matrix, - cb: (d1: T1, d2: T2, d3: T3, d4: T4, i: number, j: number) => TR + m2: Matrix | undefined, + m3: Matrix | undefined, + m4: Matrix | undefined, + cb: (d1: T1, d2: T2 | undefined, d3: T3 | undefined, d4: T4 | undefined, i: number, j: number) => TR ) { const mapMatrix = R.addIndex(R.map); const mapRow = R.addIndex(R.map); return mapMatrix((iRow, i) => mapRow( - (ijValue, j) => cb(ijValue, m2[i][j], m3[i][j], m4[i][j], i, j), + (ijValue, j) => cb( + ijValue, + m2 ? m2[i][j] : undefined, + m3 ? m3[i][j] : undefined, + m4 ? m4[i][j] : undefined, + i, + j + ), iRow ), m1 ); @@ -55,17 +88,23 @@ export function matrixMap4( export function matrixMapN( cb: (i: number, j: number, ...args: any[]) => TR, - ...matrices: (any[][])[] + m1: Matrix, + ...matrices: (any[][] | undefined)[] ) { - const m1 = matrices.slice(0, 1); - const ms = matrices.slice(1); - const mapMatrix = R.addIndex(R.map); const mapRow = R.addIndex(R.map); return mapMatrix((iRow, i) => mapRow( - (ijValue, j) => cb(i, j, [ijValue, ...ms.map(m => m[i][j])]), + (ijValue, j) => cb( + i, + j, + [ + ijValue, + m1[i][j], + ...matrices.map(m => m ? m[i][j] : undefined) + ] + ), iRow ), m1 ); diff --git a/packages/dash-table/src/core/type/index.ts b/packages/dash-table/src/core/type/index.ts index f509ef023e..b9e54c7744 100644 --- a/packages/dash-table/src/core/type/index.ts +++ b/packages/dash-table/src/core/type/index.ts @@ -1,2 +1,10 @@ +export type OptionalMap = { + [tr in TR]?: M +}; export type RequiredPluck = { [r in R]: T[r] }; export type OptionalPluck = { [r in R]?: T[r] }; + +export type RequiredProp = T[R]; +export type OptionalProp = T[R] | undefined; + +export type PropOf = R; \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/components/CellFactory.tsx b/packages/dash-table/src/dash-table/components/CellFactory.tsx index 1f47e04e00..6e3821e23b 100644 --- a/packages/dash-table/src/dash-table/components/CellFactory.tsx +++ b/packages/dash-table/src/dash-table/components/CellFactory.tsx @@ -1,15 +1,17 @@ +import * as R from 'ramda'; import React from 'react'; +import { matrixMap2, matrixMap3 } from 'core/math/matrixZipMap'; +import { arrayMap2 } from 'core/math/arrayZipMap'; + import { ICellFactoryProps } from 'dash-table/components/Table/props'; import derivedCellWrappers from 'dash-table/derived/cell/wrappers'; import derivedCellContents from 'dash-table/derived/cell/contents'; import derivedCellOperations from 'dash-table/derived/cell/operations'; -import derivedCellStyles from 'dash-table/derived/cell/wrapperStyles'; +import derivedCellStyles, { derivedDataOpStyles } from 'dash-table/derived/cell/wrapperStyles'; import derivedDropdowns from 'dash-table/derived/cell/dropdowns'; import { derivedRelevantCellStyles } from 'dash-table/derived/style'; - -import { matrixMap3 } from 'core/math/matrixZipMap'; -import { arrayMap } from 'core/math/arrayZipMap'; +import { IEdgesMatrices } from 'dash-table/derived/edges/type'; export default class CellFactory { @@ -23,11 +25,12 @@ export default class CellFactory { private readonly cellDropdowns = derivedDropdowns(), private readonly cellOperations = derivedCellOperations(), private readonly cellStyles = derivedCellStyles(), + private readonly dataOpStyles = derivedDataOpStyles(), private readonly cellWrappers = derivedCellWrappers(propsFn), private readonly relevantStyles = derivedRelevantCellStyles() ) { } - public createCells() { + public createCells(dataEdges: IEdgesMatrices | undefined, dataOpEdges: IEdgesMatrices | undefined) { const { active_cell, columns, @@ -49,16 +52,6 @@ export default class CellFactory { virtualized } = this.props; - const operations = this.cellOperations( - data, - virtualized.data, - virtualized.indices, - row_selectable, - row_deletable, - selected_rows, - setProps - ); - const relevantStyles = this.relevantStyles( style_cell, style_data, @@ -66,13 +59,20 @@ export default class CellFactory { style_data_conditional ); - const wrapperStyles = this.cellStyles( + const cellStyles = this.cellStyles( columns, relevantStyles, virtualized.data, virtualized.offset ); + const dataOpStyles = this.dataOpStyles( + (row_selectable ? 1 : 0) + (row_deletable ? 1 : 0), + relevantStyles, + virtualized.data, + virtualized.offset + ); + const dropdowns = this.cellDropdowns( columns, virtualized.data, @@ -82,7 +82,17 @@ export default class CellFactory { dropdown_properties ); - const wrappers = this.cellWrappers( + const operations = this.cellOperations( + data, + virtualized.data, + virtualized.indices, + row_selectable, + row_deletable, + selected_rows, + setProps + ); + + const cellWrappers = this.cellWrappers( active_cell, columns, virtualized.data, @@ -90,7 +100,7 @@ export default class CellFactory { selected_cells ); - const contents = this.cellContents( + const cellContents = this.cellContents( active_cell, columns, virtualized.data, @@ -100,15 +110,33 @@ export default class CellFactory { dropdowns ); + const ops = matrixMap2( + operations, + dataOpStyles, + (o, s, i, j) => React.cloneElement(o, { + style: R.mergeAll([ + dataOpEdges && dataOpEdges.getStyle(i, j), + s, + o.props.style + ]) + }) + ); + const cells = matrixMap3( - wrappers, - wrapperStyles, - contents, - (w, s, c) => React.cloneElement(w, { children: [c], style: s }) + cellWrappers, + cellStyles, + cellContents, + (w, s, c, i, j) => React.cloneElement(w, { + children: [c], + style: R.mergeAll([ + s, + dataEdges && dataEdges.getStyle(i, j) + ]) + }) ); - return arrayMap( - operations, + return arrayMap2( + ops, cells, (o, c) => Array.prototype.concat(o, c) ); diff --git a/packages/dash-table/src/dash-table/components/ControlledTable/index.tsx b/packages/dash-table/src/dash-table/components/ControlledTable/index.tsx index 3870006366..80d233dc79 100644 --- a/packages/dash-table/src/dash-table/components/ControlledTable/index.tsx +++ b/packages/dash-table/src/dash-table/components/ControlledTable/index.tsx @@ -644,7 +644,7 @@ export default class ControlledTable extends PureComponent this.stylesheet.setRule( `.dash-fixed-row:not(.dash-fixed-column) th:nth-of-type(${index + 1})`, - `width: ${width}; min-width: ${width}; max-width: ${width};` + `width: ${width} !important; min-width: ${width} !important; max-width: ${width} !important;` ); }); } @@ -657,7 +657,7 @@ export default class ControlledTable extends PureComponent this.stylesheet.setRule( `.dash-fixed-column.dash-fixed-row th:nth-of-type(${index + 1})`, - `width: ${width}; min-width: ${width}; max-width: ${width};` + `width: ${width} !important; min-width: ${width} !important; max-width: ${width} !important;` ); }); } @@ -683,9 +683,11 @@ export default class ControlledTable extends PureComponent render() { const { id, + columns, column_conditional_tooltips, column_static_tooltip, content_style, + filtering, n_fixed_columns, n_fixed_rows, scrollbarWidth, @@ -703,26 +705,6 @@ export default class ControlledTable extends PureComponent virtualization } = this.props; - const containerClasses = [ - 'dash-spreadsheet', - 'dash-spreadsheet-container', - ...(virtualization ? ['dash-virtualized'] : []), - ...(n_fixed_rows ? ['dash-freeze-top'] : []), - ...(n_fixed_columns ? ['dash-freeze-left'] : []), - ...(style_as_list_view ? ['dash-list-view'] : []), - [`dash-${content_style}`] - ]; - - const classes = [ - 'dash-spreadsheet', - 'dash-spreadsheet-inner', - ...(virtualization ? ['dash-virtualized'] : []), - ...(n_fixed_rows ? ['dash-freeze-top'] : []), - ...(n_fixed_columns ? ['dash-freeze-left'] : []), - ...(style_as_list_view ? ['dash-list-view'] : []), - [`dash-${content_style}`] - ]; - const fragmentClasses = [ [ n_fixed_rows && n_fixed_columns ? 'dash-fixed-row dash-fixed-column' : '', @@ -735,13 +717,30 @@ export default class ControlledTable extends PureComponent ]; const rawTable = this.tableFn(); - const grid = derivedTableFragments( + const { grid, empty } = derivedTableFragments( n_fixed_columns, n_fixed_rows, rawTable, virtualized.offset.rows ); + const classes = [ + 'dash-spreadsheet', + ...(virtualization ? ['dash-virtualized'] : []), + ...(n_fixed_rows ? ['dash-freeze-top'] : []), + ...(n_fixed_columns ? ['dash-freeze-left'] : []), + ...(style_as_list_view ? ['dash-list-view'] : []), + ...(empty[0][1] ? ['dash-empty-01'] : []), + ...(empty[1][1] ? ['dash-empty-11'] : []), + ...(columns.length ? [] : ['dash-no-columns']), + ...(virtualized.data.length ? [] : ['dash-no-data']), + ...(filtering ? [] : ['dash-no-filter']), + [`dash-${content_style}`] + ]; + + const containerClasses = ['dash-spreadsheet-container', ...classes]; + const innerClasses = ['dash-spreadsheet-inner', ...classes]; + const tableStyle = this.calculateTableStyle(style_table); const gridStyle = derivedTableFragmentStyles( virtualization, @@ -780,7 +779,7 @@ export default class ControlledTable extends PureComponent
{grid.map((row, rowIndex) => (
{ + if (EdgeFactory.hasPrecedence( + hPrevious.getWeight(iPrevious, j), + hTarget.getWeight(iTarget, j), + cutoffWeight + )) { + hTarget.setEdge(iTarget, j, hPrevious.getEdge(iPrevious, j), Infinity, true); + } + hPrevious.setEdge(iPrevious, j, undefined, -Infinity, true); + }, R.range(0, hPrevious.columns)); + } + + private vOverride(previous: EdgesMatricesOp, target: EdgesMatricesOp, cutoffWeight: number) { + if (!previous || !target) { + return; + } + + const hPrevious = previous.getMatrices().vertical; + const hTarget = target.getMatrices().vertical; + + const jPrevious = hPrevious.columns - 1; + const jTarget = 0; + + R.forEach(i => { + if (EdgeFactory.hasPrecedence( + hPrevious.getWeight(i, jPrevious), + hTarget.getWeight(i, jTarget), + cutoffWeight + )) { + hTarget.setEdge(i, jTarget, hPrevious.getEdge(i, jPrevious), Infinity, true); + } + hPrevious.setEdge(i, jPrevious, undefined, -Infinity, true); + }, R.range(0, hPrevious.rows)); + } + + private hReconcile(target: EdgesMatrices | undefined, next: EdgesMatrices | undefined, cutoffWeight: number) { + if (!target || !next) { + return; + } + + const hNext = next.getMatrices().horizontal; + const hTarget = target.getMatrices().horizontal; + + const iNext = 0; + const iTarget = hTarget.rows - 1; + + R.forEach(j => + !EdgeFactory.hasPrecedence( + hTarget.getWeight(iTarget, j), + hNext.getWeight(iNext, j), + cutoffWeight + ) && hTarget.setEdge(iTarget, j, undefined, -Infinity, true), + R.range(0, hTarget.columns) + ); + } + + private vReconcile(target: EdgesMatrices | undefined, next: EdgesMatrices | undefined, cutoffWeight: number) { + if (!target || !next) { + return; + } + + const vNext = next.getMatrices().vertical; + const vTarget = target.getMatrices().vertical; + + const jNext = 0; + const jTarget = vTarget.columns - 1; + + R.forEach(i => + !EdgeFactory.hasPrecedence( + vTarget.getWeight(i, jTarget), + vNext.getWeight(i, jNext), + cutoffWeight + ) && vTarget.setEdge(i, jTarget, undefined, -Infinity, true), + R.range(0, vTarget.rows) + ); + } + + private get props() { + return this.propsFn(); + } + + constructor(private readonly propsFn: () => ControlledTableProps) { + + } + + public createEdges() { + const { + active_cell, + columns, + filtering, + workFilter, + n_fixed_columns, + n_fixed_rows, + row_deletable, + row_selectable, + style_as_list_view, + style_cell, + style_cell_conditional, + style_data, + style_data_conditional, + style_filter, + style_filter_conditional, + style_header, + style_header_conditional, + virtualized + } = this.props; + + return this.memoizedCreateEdges( + active_cell, + columns, + (row_deletable ? 1 : 0) + (row_selectable ? 1 : 0), + !!filtering, + workFilter.map, + n_fixed_columns, + n_fixed_rows, + style_as_list_view, + style_cell, + style_cell_conditional, + style_data, + style_data_conditional, + style_filter, + style_filter_conditional, + style_header, + style_header_conditional, + virtualized.data, + virtualized.offset + ); + } + + private memoizedCreateEdges = memoizeOne(( + active_cell: ICellCoordinates, + columns: VisibleColumns, + operations: number, + filtering: boolean, + filterMap: Map, + _n_fixed_columns: number, + n_fixed_rows: number, + style_as_list_view: boolean, + style_cell: Style, + style_cell_conditional: Cells, + style_data: Style, + style_data_conditional: DataCells, + style_filter: Style, + style_filter_conditional: BasicFilters, + style_header: Style, + style_header_conditional: Headers, + data: Data, + offset: IViewportOffset + ) => { + const dataStyles = this.dataStyles( + style_cell, + style_data, + style_cell_conditional, + style_data_conditional + ); + + const filterStyles = this.filterStyles( + style_cell, + style_filter, + style_cell_conditional, + style_filter_conditional + ); + + const headerStyles = this.headerStyles( + style_cell, + style_header, + style_cell_conditional, + style_header_conditional + ); + + const headerRows = getHeaderRows(columns); + + let dataEdges = this.getDataEdges( + columns, + dataStyles, + data, + offset, + active_cell, + style_as_list_view + ); + + let dataOpEdges = this.getDataOpEdges( + operations, + dataStyles, + data, + offset, + style_as_list_view + ); + + let filterEdges = this.getFilterEdges( + columns, + filtering, + filterMap, + filterStyles, + style_as_list_view + ); + + let filterOpEdges = this.getFilterOpEdges( + operations, + filtering, + filterStyles, + style_as_list_view + ); + + let headerEdges = this.getHeaderEdges( + columns, + headerRows, + headerStyles, + style_as_list_view + ); + + let headerOpEdges = this.getHeaderOpEdges( + operations, + headerRows, + headerStyles, + style_as_list_view + ); + + const cutoffWeight = (style_cell ? 1 : 0) + style_cell_conditional.length - 1; + + headerEdges = EdgeFactory.clone(headerEdges); + headerOpEdges = EdgeFactory.clone(headerOpEdges); + filterEdges = EdgeFactory.clone(filterEdges); + filterOpEdges = EdgeFactory.clone(filterOpEdges); + dataEdges = EdgeFactory.clone(dataEdges); + dataOpEdges = EdgeFactory.clone(dataOpEdges); + + this.hReconcile(headerEdges, filterEdges || dataEdges, cutoffWeight); + this.hReconcile(headerOpEdges, filterOpEdges || dataOpEdges, cutoffWeight); + this.hReconcile(filterEdges, dataEdges, cutoffWeight); + this.hReconcile(filterOpEdges, dataOpEdges, cutoffWeight); + + this.vReconcile(headerOpEdges, headerEdges, cutoffWeight); + this.vReconcile(filterOpEdges, filterEdges, cutoffWeight); + this.vReconcile(dataOpEdges, dataEdges, cutoffWeight); + + if (n_fixed_rows === headerRows) { + if (filtering) { + this.hOverride(headerEdges, filterEdges, cutoffWeight); + this.hOverride(headerOpEdges, filterOpEdges, cutoffWeight); + } else { + this.hOverride(headerEdges, dataEdges, cutoffWeight); + this.hOverride(headerOpEdges, dataOpEdges, cutoffWeight); + } + } else if (filtering && n_fixed_rows === headerRows + 1) { + this.hOverride(filterEdges, dataEdges, cutoffWeight); + this.hOverride(filterOpEdges, dataOpEdges, cutoffWeight); + } + + if (_n_fixed_columns === operations) { + this.vOverride(headerOpEdges, headerEdges, cutoffWeight); + this.vOverride(filterOpEdges, filterEdges, cutoffWeight); + this.vOverride(dataOpEdges, dataEdges, cutoffWeight); + } + + return { + dataEdges: dataEdges as (IEdgesMatrices | undefined), + dataOpEdges: dataOpEdges as (IEdgesMatrices | undefined), + filterEdges: filterEdges as (IEdgesMatrices | undefined), + filterOpEdges: filterOpEdges as (IEdgesMatrices | undefined), + headerEdges: headerEdges as (IEdgesMatrices | undefined), + headerOpEdges: headerOpEdges as (IEdgesMatrices | undefined) + }; + }); +} \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/components/Filter/Column.tsx b/packages/dash-table/src/dash-table/components/Filter/Column.tsx index 1c32da7467..b6e80f061c 100644 --- a/packages/dash-table/src/dash-table/components/Filter/Column.tsx +++ b/packages/dash-table/src/dash-table/components/Filter/Column.tsx @@ -15,7 +15,11 @@ interface IColumnFilterProps { value?: string; } -export default class ColumnFilter extends PureComponent { +interface IState { + value?: string; +} + +export default class ColumnFilter extends PureComponent { constructor(props: IColumnFilterProps) { super(props); diff --git a/packages/dash-table/src/dash-table/components/FilterFactory.tsx b/packages/dash-table/src/dash-table/components/FilterFactory.tsx index 6ebbdf80b9..4c774f4f85 100644 --- a/packages/dash-table/src/dash-table/components/FilterFactory.tsx +++ b/packages/dash-table/src/dash-table/components/FilterFactory.tsx @@ -2,19 +2,26 @@ import * as R from 'ramda'; import React from 'react'; import Logger from 'core/Logger'; -import { arrayMap } from 'core/math/arrayZipMap'; +import { arrayMap, arrayMap2 } from 'core/math/arrayZipMap'; import memoizerCache from 'core/cache/memoizer'; import { memoizeOne } from 'core/memoizer'; import ColumnFilter from 'dash-table/components/Filter/Column'; import { ColumnId, Filtering, FilteringType, IVisibleColumn, VisibleColumns, RowSelection } from 'dash-table/components/Table/props'; -import derivedFilterStyles from 'dash-table/derived/filter/wrapperStyles'; +import derivedFilterStyles, { derivedFilterOpStyles } from 'dash-table/derived/filter/wrapperStyles'; import derivedHeaderOperations from 'dash-table/derived/header/operations'; import { derivedRelevantFilterStyles } from 'dash-table/derived/style'; import { BasicFilters, Cells, Style } from 'dash-table/derived/style/props'; -import { MultiColumnsSyntaxTree, SingleColumnSyntaxTree, getMultiColumnQueryString, getSingleColumnMap } from 'dash-table/syntax-tree'; +import { SingleColumnSyntaxTree, getMultiColumnQueryString } from 'dash-table/syntax-tree'; -type SetFilter = (filter: string, rawFilter: string) => void; +import { IEdgesMatrices } from 'dash-table/derived/edges/type'; +import { updateMap } from 'dash-table/derived/filter/map'; + +type SetFilter = ( + filter: string, + rawFilter: string, + map: Map +) => void; export interface IFilterOptions { columns: VisibleColumns; @@ -22,6 +29,7 @@ export interface IFilterOptions { filtering: Filtering; filtering_type: FilteringType; id: string; + map: Map; rawFilterQuery: string; row_deletable: boolean; row_selectable: RowSelection; @@ -33,13 +41,11 @@ export interface IFilterOptions { } export default class FilterFactory { - private readonly handlers = new Map(); private readonly filterStyles = derivedFilterStyles(); + private readonly filterOpStyles = derivedFilterOpStyles(); private readonly relevantStyles = derivedRelevantFilterStyles(); private readonly headerOperations = derivedHeaderOperations(); - private ops = new Map(); - private get props() { return this.propsFn(); } @@ -48,19 +54,14 @@ export default class FilterFactory { } - private onChange = (column: IVisibleColumn, setFilter: SetFilter, ev: any) => { + private onChange = (column: IVisibleColumn, 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(); - const safeColumnId = column.id.toString(); - if (value && value.length) { - this.ops.set(safeColumnId, new SingleColumnSyntaxTree(value, column)); - } else { - this.ops.delete(safeColumnId); - } + map = updateMap(map, column, value); - const asts = Array.from(this.ops.values()); + const asts = Array.from(map.values()); const globalFilter = getMultiColumnQueryString(asts); const rawGlobalFilter = R.map( @@ -68,68 +69,47 @@ export default class FilterFactory { R.filter(ast => Boolean(ast), asts) ).join(' && '); - setFilter(globalFilter, rawGlobalFilter); - } - - private getEventHandler = (fn: Function, column: IVisibleColumn, setFilter: SetFilter): any => { - const fnHandler = (this.handlers.get(fn) || this.handlers.set(fn, new Map()).get(fn)); - const columnIdHandler = (fnHandler.get(column.id) || fnHandler.set(column.id, new Map()).get(column.id)); - - return ( - columnIdHandler.get(setFilter) || - (columnIdHandler.set(setFilter, fn.bind(this, column, setFilter)).get(setFilter)) - ); + setFilter(globalFilter, rawGlobalFilter, map); } - private updateOps = memoizeOne((query: string, columns: IVisibleColumn[]) => { - const multiQuery = new MultiColumnsSyntaxTree(query); - - const newOps = getSingleColumnMap(multiQuery, columns); - if (!newOps) { - return; - } - - /* Mapping multi-column to single column queries will expand - * compressed forms. If the new ast query is equal to the - * old one, keep the old one instead. - * - * If the value was changed by the user, the current ast will - * have been modified already and the UI experience will also - * be consistent in that case. - */ - R.forEach(([key, ast]) => { - const newAst = newOps.get(key); - - if (newAst && newAst.toQueryString() === ast.toQueryString()) { - newOps.set(key, ast); - } - }, Array.from(this.ops.entries())); - - this.ops = newOps; - }); - private filter = memoizerCache<[ColumnId, number]>()(( column: IVisibleColumn, index: number, - ast: SingleColumnSyntaxTree | undefined, + map: Map, setFilter: SetFilter ) => { + const ast = map.get(column.id.toString()); + return (); }); - public createFilters() { + private wrapperStyles = memoizeOne(( + styles: any[], + edges: IEdgesMatrices | undefined + ) => arrayMap( + styles, + (s, j) => R.merge( + s, + edges && edges.getStyle(0, j) + ) + )); + + public createFilters( + filterEdges: IEdgesMatrices | undefined, + filterOpEdges: IEdgesMatrices | undefined + ) { const { columns, - filter, filtering, filtering_type, + map, row_deletable, row_selectable, setFilter, @@ -143,34 +123,41 @@ export default class FilterFactory { return []; } - this.updateOps(filter, columns); - if (filtering_type === FilteringType.Basic) { - const filterStyles = this.relevantStyles( + const relevantStyles = this.relevantStyles( style_cell, style_filter, style_cell_conditional, style_filter_conditional ); - const wrapperStyles = this.filterStyles( - columns, - filterStyles + const wrapperStyles = this.wrapperStyles( + this.filterStyles(columns, relevantStyles), + filterEdges ); + const opStyles = this.filterOpStyles( + 1, + (row_selectable ? 1 : 0) + (row_deletable ? 1 : 0), + relevantStyles + )[0]; + const filters = R.addIndex(R.map)((column, index) => { return this.filter.get(column.id, index)( column, index, - this.ops.get(column.id.toString()), + map, setFilter ); }, columns); - const styledFilters = arrayMap( + const styledFilters = arrayMap2( filters, wrapperStyles, - (f, s) => React.cloneElement(f, { style: s })); + (f, s) => React.cloneElement(f, { + style: s + }) + ); const operations = this.headerOperations( 1, @@ -178,7 +165,19 @@ export default class FilterFactory { row_deletable )[0]; - return [operations.concat(styledFilters)]; + const operators = arrayMap2( + operations, + opStyles, + (o, s, j) => React.cloneElement(o, { + style: R.mergeAll([ + filterOpEdges && filterOpEdges.getStyle(0, j), + s, + o.props.style + ]) + }) + ); + + return [operators.concat(styledFilters)]; } else { return [[]]; } diff --git a/packages/dash-table/src/dash-table/components/HeaderFactory.tsx b/packages/dash-table/src/dash-table/components/HeaderFactory.tsx index 185b0ab1ca..59db68ddf6 100644 --- a/packages/dash-table/src/dash-table/components/HeaderFactory.tsx +++ b/packages/dash-table/src/dash-table/components/HeaderFactory.tsx @@ -1,8 +1,8 @@ import * as R from 'ramda'; import React from 'react'; -import { arrayMap } from 'core/math/arrayZipMap'; -import { matrixMap3 } from 'core/math/matrixZipMap'; +import { arrayMap2 } from 'core/math/arrayZipMap'; +import { matrixMap2, matrixMap3 } from 'core/math/matrixZipMap'; import { ControlledTableProps } from 'dash-table/components/Table/props'; import derivedHeaderContent from 'dash-table/derived/header/content'; @@ -12,12 +12,15 @@ import getLabels from 'dash-table/derived/header/labels'; import derivedHeaderOperations from 'dash-table/derived/header/operations'; import derivedHeaderWrappers from 'dash-table/derived/header/wrappers'; import { derivedRelevantHeaderStyles } from 'dash-table/derived/style'; -import derivedHeaderStyles from 'dash-table/derived/header/wrapperStyles'; +import derivedHeaderStyles, { derivedHeaderOpStyles } from 'dash-table/derived/header/wrapperStyles'; + +import { IEdgesMatrices } from 'dash-table/derived/edges/type'; export default class HeaderFactory { private readonly headerContent = derivedHeaderContent(); private readonly headerOperations = derivedHeaderOperations(); private readonly headerStyles = derivedHeaderStyles(); + private readonly headerOpStyles = derivedHeaderOpStyles(); private readonly headerWrappers = derivedHeaderWrappers(); private readonly relevantStyles = derivedRelevantHeaderStyles(); @@ -29,7 +32,7 @@ export default class HeaderFactory { } - public createHeaders() { + public createHeaders(headerEdges: IEdgesMatrices | undefined, headerOpEdges: IEdgesMatrices | undefined) { const props = this.props; const { @@ -55,12 +58,6 @@ export default class HeaderFactory { const labelsAndIndices = R.zip(labels, indices); - const operations = this.headerOperations( - headerRows, - row_selectable, - row_deletable - ); - const relevantStyles = this.relevantStyles( style_cell, style_header, @@ -68,12 +65,24 @@ export default class HeaderFactory { style_header_conditional ); + const operations = this.headerOperations( + headerRows, + row_selectable, + row_deletable + ); + const wrapperStyles = this.headerStyles( columns, headerRows, relevantStyles ); + const opStyles = this.headerOpStyles( + headerRows, + (row_selectable ? 1 : 0) + (row_deletable ? 1 : 0), + relevantStyles + ); + const wrappers = this.headerWrappers( columns, labelsAndIndices, @@ -91,12 +100,30 @@ export default class HeaderFactory { props ); + const ops = matrixMap2( + operations, + opStyles, + (o, s, i, j) => React.cloneElement(o, { + style: R.mergeAll([ + headerOpEdges && headerOpEdges.getStyle(i, j), + s, + o.props.style + ]) + }) + ); + const headers = matrixMap3( wrappers, wrapperStyles, content, - (w, s, c) => React.cloneElement(w, { children: [c], style: s })); - - return arrayMap(operations, headers, (o, h) => Array.prototype.concat(o, h)); + (w, s, c, i, j) => React.cloneElement(w, { + children: [c], + style: R.mergeAll([ + s, + headerEdges && headerEdges.getStyle(i, j) + ]) + })); + + return arrayMap2(ops, headers, (o, h) => Array.prototype.concat(o, h)); } } diff --git a/packages/dash-table/src/dash-table/components/Table/Table.less b/packages/dash-table/src/dash-table/components/Table/Table.less index 3c733d52b7..4d919b91e6 100644 --- a/packages/dash-table/src/dash-table/components/Table/Table.less +++ b/packages/dash-table/src/dash-table/components/Table/Table.less @@ -1,183 +1,5 @@ @import (reference) '~dash-table/style/reset.less'; -.inset-shadow(@color, @left, @top, @right, @bottom) { - box-shadow: inset @left 0px 0px 0px @color, - inset 0px @top 0px 0px @color, - inset @right 0px 0px @color, - inset 0px @bottom 0px 0px @color; -} - -.outline-shadow(@color, @left, @top, @right, @bottom) { - box-shadow: @left 0px 0px 0px @color, - 0px @top 0px 0px @color, - @right 0px 0px @color, - 0px @bottom 0px 0px @color; -} - -.top-left-cells(@isListView) { - .left-cells(@isListView); - .top-cells(@isListView); - - tr:first-of-type { - td:first-of-type, - th:first-of-type { - & when (@isListView = True) { - .inset-shadow(var(--border), 0px, 1px, 0px, -1px); - - &.focused { - .inset-shadow(var(--accent), 0px, 0px, 0px, -1px); - } - } - - & when (@isListView = False) { - .inset-shadow(var(--border), 1px, 1px, -1px, -1px); - - &.focused { - box-shadow: inset 0 0 0 1px var(--accent) - } - } - } - } -} - -.top-cells(@isListView) { - tr:first-of-type { - td, th { - & when (@isListView = True) { - .inset-shadow(var(--border), 0px, 1px, 0px, -1px); - - &.focused { - .inset-shadow(var(--accent), 0px, 0px, 0px, -1px); - } - } - - & when (@isListView = False) { - .inset-shadow(var(--border), 0px, 1px, -1px, -1px); - - &.focused { - box-shadow: inset 0 0 0 1px var(--accent) - } - } - } - } -} - -.left-cells(@isListView) { - tr { - td:first-of-type, - th:first-of-type { - & when (@isListView = True) { - .inset-shadow(var(--border), 0px, 0px, 0px, -1px); - - &.focused { - .inset-shadow(var(--accent), 0px, 0px, 0px, -1px); - } - } - - & when (@isListView = False) { - .inset-shadow(var(--border), 1px, 0px, -1px, -1px); - - &.focused { - box-shadow: inset 0 0 0 1px var(--accent) - } - } - - } - } -} - -.dash-borders(@isListView) { - &:not(.dash-freeze-top):not(.dash-freeze-left) { - .cell-1-1 { - .top-left-cells(@isListView); - } - } - - &:not(.dash-freeze-top).dash-freeze-left { - .cell-1-0 { - .top-left-cells(@isListView); - } - - .cell-1-1 { - .top-cells(@isListView); - } - } - - &.dash-freeze-top:not(.dash-freeze-left) { - .cell-0-1 { - .top-left-cells(@isListView); - } - - .cell-1-1 { - .left-cells(@isListView); - } - } - - &.dash-freeze-top.dash-freeze-left { - .cell-0-0 { - .top-left-cells(@isListView); - } - - .cell-0-1 { - .top-cells(@isListView); - } - - .cell-1-0 { - .left-cells(@isListView); - } - } - - td, th { - & when (@isListView = True) { - .inset-shadow(var(--border), 0px, 0px, 0px, -1px); - } - - & when (@isListView = False) { - .inset-shadow(var(--border), 0px, 0px, -1px, -1px); - } - - &:focus { - outline: none; - } - } - - td.focused { - & when (@isListView = True) { - .inset-shadow(var(--accent), 0px, 0px, 0px, -1px); - } - - & when (@isListView = False) { - box-shadow: inset 0 0 0 1px var(--accent) - } - } - - .dash-filter { - input::placeholder { - color: inherit; - font-size: 0.8em; - padding-right: 5px; - } - - & + .dash-filter { - &:not(:hover):not(:focus-within) { - input::placeholder { - color: transparent; - } - } - } - - &.invalid { - & when (@isListView = True) { - .inset-shadow(red, 0px, 0px, 0px, -1px); - } - - & when (@isListView = False) { - .inset-shadow(red, 1px, 1px, -1px, -1px); - } - } - } -} - .fit-content-polyfill() { width: auto; // MS Edge, IE width: fit-content; // Chrome @@ -334,6 +156,48 @@ } } + .dash-filter { + input::placeholder { + color: inherit; + font-size: 0.8em; + padding-right: 5px; + } + + & + .dash-filter { + &:not(:hover):not(:focus-within) { + input::placeholder { + color: transparent; + } + } + } + + &.invalid { + background-color: pink; + } + } + + &:not(.dash-empty-11) { + .row-0 { + tr:last-of-type { + td, th { + border-bottom: none !important; + } + } + } + } + + &:not(.dash-empty-01) { + .cell-0-0, + .cell-1-0 { + tr { + td:last-of-type, + th:last-of-type { + border-right: none !important; + } + } + } + } + &.dash-freeze-left, &.dash-freeze-top, &.dash-virtualized { @@ -348,7 +212,7 @@ .row-1 { display: flex; flex-direction: row; - overflow: scroll; + overflow: auto; } .cell-0-0, @@ -380,18 +244,6 @@ } } - &:not(.dash-list-view) { - .dash-borders(False); - } - - &.dash-list-view { - .dash-borders(True); - } - - .dash-filter.invalid { - background-color: pink; - } - .selected-row { td, th { background-color: var(--selected-row); @@ -550,11 +402,6 @@ overflow-x: visible; } - .dash-spreadsheet-inner th { - box-shadow: inset 1px 0px 0px 0px var(--border), - inset 0px 1px 0px 0px var(--border); - } - .dash-spreadsheet-inner :not(.cell--selected) tr:hover, tr:hover input :not(.cell--selected) { background-color: var(--hover); @@ -578,7 +425,6 @@ } .expanded-row--empty-cell { - box-shadow: none; background-color: transparent; } @@ -633,12 +479,6 @@ color: var(--accent); } - .expanded-row { - box-shadow: inset 2px 0px 0px 0px var(--accent), - inset -1px 0px 0px 0px var(--border); - /* inset 0px 1px 0px 0px var(--border); */ - } - .dash-spreadsheet-inner .dash-delete-cell, .dash-spreadsheet-inner .dash-delete-header { .not-selectable(); diff --git a/packages/dash-table/src/dash-table/components/Table/index.tsx b/packages/dash-table/src/dash-table/components/Table/index.tsx index 84c7dbcfb9..aa105baf36 100644 --- a/packages/dash-table/src/dash-table/components/Table/index.tsx +++ b/packages/dash-table/src/dash-table/components/Table/index.tsx @@ -1,6 +1,10 @@ import React, { Component } from 'react'; import * as R from 'ramda'; +/*#if DEV*/ +import Logger from 'core/Logger'; +/*#endif*/ + import { memoizeOne, memoizeOneWithFlag } from 'core/memoizer'; import ControlledTable from 'dash-table/components/ControlledTable'; @@ -27,9 +31,8 @@ import 'react-select/dist/react-select.css'; import './Table.less'; import './Dropdown.css'; import { isEqual } from 'core/comparer'; -/*#if DEV*/ -import Logger from 'core/Logger'; -/*#endif*/ +import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; +import derivedFilterMap from 'dash-table/derived/filter/map'; const DERIVED_REGEX = /^derived_/; @@ -39,11 +42,41 @@ export default class Table extends Component(), + props.filter, + props.columns + ) + }, rawFilterQuery: '', scrollbarWidth: 0 }; } + componentWillReceiveProps(nextProps: PropsWithDefaultsAndDerived) { + if (nextProps.filter === this.props.filter) { + return; + } + + this.setState(state => { + const { workFilter: { map: currentMap, value } } = state; + + if (value !== nextProps.filter) { + const map = this.filterMap( + currentMap, + nextProps.filter, + nextProps.columns + ); + + return map !== currentMap ? { workFilter: { map, value} } : null; + } else { + return null; + } + }); + } + shouldComponentUpdate(nextProps: any, nextState: any) { const props: any = this.props; const state: any = this.state; @@ -267,6 +300,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(); diff --git a/packages/dash-table/src/dash-table/components/Table/props.ts b/packages/dash-table/src/dash-table/components/Table/props.ts index 4625f139e0..3b3fc3e153 100644 --- a/packages/dash-table/src/dash-table/components/Table/props.ts +++ b/packages/dash-table/src/dash-table/components/Table/props.ts @@ -12,6 +12,7 @@ import { ConditionalTooltip, Tooltip } from 'dash-table/tooltips/props'; +import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; export enum ColumnType { Any = 'any', @@ -238,6 +239,10 @@ export interface IUSerInterfaceTooltip { export interface IState { forcedResizeOnly: boolean; + workFilter: { + value: string, + map: Map + }; rawFilterQuery: string; scrollbarWidth: number; tooltip?: IUSerInterfaceTooltip; diff --git a/packages/dash-table/src/dash-table/dash/DataTable.js b/packages/dash-table/src/dash-table/dash/DataTable.js index 5357183a6d..ac731aef36 100644 --- a/packages/dash-table/src/dash-table/dash/DataTable.js +++ b/packages/dash-table/src/dash-table/dash/DataTable.js @@ -77,8 +77,9 @@ export const defaultProps = { row_selectable: false, style_table: {}, - style_data_conditional: [], style_cell_conditional: [], + style_data_conditional: [], + style_filter_conditional: [], style_header_conditional: [], virtualization: false }; diff --git a/packages/dash-table/src/dash-table/derived/cell/wrapperStyles.ts b/packages/dash-table/src/dash-table/derived/cell/wrapperStyles.ts index f53d78d98c..764acf72fb 100644 --- a/packages/dash-table/src/dash-table/derived/cell/wrapperStyles.ts +++ b/packages/dash-table/src/dash-table/derived/cell/wrapperStyles.ts @@ -4,6 +4,7 @@ import { CSSProperties } from 'react'; import { memoizeOneFactory } from 'core/memoizer'; import { Data, VisibleColumns, IViewportOffset } from 'dash-table/components/Table/props'; import { IConvertedStyle } from '../style'; +import { BORDER_PROPERTIES_AND_FRAGMENTS } from '../edges/type'; type Style = CSSProperties | undefined; @@ -25,8 +26,41 @@ function getter( ) ); - return relevantStyles.length ? R.mergeAll(relevantStyles) : undefined; + return relevantStyles.length ? + R.omit( + BORDER_PROPERTIES_AND_FRAGMENTS, + R.mergeAll(relevantStyles) + ) : + undefined; }, columns), data); } +function opGetter( + columns: number, + columnStyles: IConvertedStyle[], + data: Data, + offset: IViewportOffset +) { + return R.addIndex(R.map)((datum, index) => R.map(_ => { + const relevantStyles = R.map( + s => s.style, + R.filter( + style => + !style.checksColumn() && + style.matchesRow(index + offset.rows) && + style.matchesFilter(datum), + columnStyles + ) + ); + + return relevantStyles.length ? + R.omit( + BORDER_PROPERTIES_AND_FRAGMENTS, + R.mergeAll(relevantStyles) + ) : + undefined; + }, R.range(0, columns)), data); +} + export default memoizeOneFactory(getter); +export const derivedDataOpStyles = memoizeOneFactory(opGetter); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/edges/data.ts b/packages/dash-table/src/dash-table/derived/edges/data.ts new file mode 100644 index 0000000000..94d4a9ad63 --- /dev/null +++ b/packages/dash-table/src/dash-table/derived/edges/data.ts @@ -0,0 +1,88 @@ +import * as R from 'ramda'; + +import Environment from 'core/environment'; +import { memoizeOneFactory } from 'core/memoizer'; + +import { + IViewportOffset, + IVisibleColumn, + VisibleColumns, + Data, + ICellCoordinates +} from 'dash-table/components/Table/props'; + +import { IConvertedStyle } from '../style'; +import { BorderStyle, BORDER_PROPERTIES, EdgesMatrices } from './type'; + +const getWeightedStyle = ( + borderStyles: IConvertedStyle[], + column: IVisibleColumn, + index: number, + offset: IViewportOffset, + datum: any +): BorderStyle => { + const res: BorderStyle = {}; + + R.addIndex(R.forEach)((rs, i) => { + if (!rs.matchesColumn(column) || + !rs.matchesRow(index + offset.rows) || + !rs.matchesFilter(datum) + ) { + return; + } + + R.forEach(p => { + const s = rs.style[p] || rs.style.border; + + if (!R.isNil(s)) { + res[p] = [s, i]; + } + }, BORDER_PROPERTIES); + }, borderStyles); + + return res; +}; + +export default memoizeOneFactory(( + columns: VisibleColumns, + borderStyles: IConvertedStyle[], + data: Data, + offset: IViewportOffset, + active_cell: ICellCoordinates | undefined, + listViewStyle: boolean +) => { + if (data.length === 0 || columns.length === 0) { + return; + } + + const edges = new EdgesMatrices(data.length, columns.length, Environment.defaultEdge, true, !listViewStyle); + + R.addIndex(R.forEach)((datum, i) => + R.addIndex(R.forEach)( + (column, j) => { + const cellStyle = getWeightedStyle( + borderStyles, + column, + i, + offset, + datum + ); + + edges.setEdges(i, j, cellStyle); + }, + columns + ), + data + ); + + if (active_cell) { + edges.setEdges(active_cell.row, active_cell.column, { + borderBottom: [Environment.activeEdge, Infinity], + borderLeft: [Environment.activeEdge, Infinity], + borderRight: [Environment.activeEdge, Infinity], + borderTop: [Environment.activeEdge, Infinity] + }); + } + + return edges; +}); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/edges/filter.ts b/packages/dash-table/src/dash-table/derived/edges/filter.ts new file mode 100644 index 0000000000..229a486ff0 --- /dev/null +++ b/packages/dash-table/src/dash-table/derived/edges/filter.ts @@ -0,0 +1,77 @@ +import * as R from 'ramda'; + +import Environment from 'core/environment'; +import { memoizeOneFactory } from 'core/memoizer'; + +import { + IVisibleColumn, + VisibleColumns +} from 'dash-table/components/Table/props'; + +import { IConvertedStyle } from '../style'; +import { BorderStyle, BORDER_PROPERTIES, EdgesMatrices } from './type'; +import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; + +const getWeightedStyle = ( + borderStyles: IConvertedStyle[], + column: IVisibleColumn +): BorderStyle => { + const res: BorderStyle = {}; + + R.addIndex(R.forEach)((rs, i) => { + if (!rs.matchesColumn(column)) { + return; + } + + R.forEach(p => { + const s = rs.style[p] || rs.style.border; + + if (!R.isNil(s)) { + res[p] = [s, i]; + } + }, BORDER_PROPERTIES); + }, borderStyles); + + return res; +}; + +export default memoizeOneFactory(( + columns: VisibleColumns, + showFilters: boolean, + map: Map, + borderStyles: IConvertedStyle[], + listViewStyle: boolean +) => { + if (!showFilters || columns.length === 0) { + return; + } + + const edges = new EdgesMatrices(1, columns.length, Environment.defaultEdge, true, !listViewStyle); + + R.forEach(i => + R.addIndex(R.forEach)( + (column, j) => { + const cellStyle = getWeightedStyle( + borderStyles, + column + ); + + edges.setEdges(i, j, cellStyle); + + const ast = map.get(column.id.toString()); + if (ast && !ast.isValid) { + edges.setEdges(i, j, { + borderBottom: [Environment.activeEdge, Infinity], + borderLeft: [Environment.activeEdge, Infinity], + borderRight: [Environment.activeEdge, Infinity], + borderTop: [Environment.activeEdge, Infinity] + }); + } + }, + columns + ), + R.range(0, 1) + ); + + return edges; +}); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/edges/header.ts b/packages/dash-table/src/dash-table/derived/edges/header.ts new file mode 100644 index 0000000000..9631f1b9ce --- /dev/null +++ b/packages/dash-table/src/dash-table/derived/edges/header.ts @@ -0,0 +1,69 @@ +import * as R from 'ramda'; + +import Environment from 'core/environment'; +import { memoizeOneFactory } from 'core/memoizer'; + +import { + IVisibleColumn, + VisibleColumns +} from 'dash-table/components/Table/props'; + +import { IConvertedStyle } from '../style'; +import { BorderStyle, BORDER_PROPERTIES, EdgesMatrices } from './type'; + +const getWeightedStyle = ( + borderStyles: IConvertedStyle[], + column: IVisibleColumn, + index: number +): BorderStyle => { + const res: BorderStyle = {}; + + R.addIndex(R.forEach)((rs, i) => { + if (!rs.matchesColumn(column) || + !rs.matchesRow(index) + ) { + return; + } + + R.forEach(p => { + const s = rs.style[p] || rs.style.border; + + if (!R.isNil(s)) { + res[p] = [s, i]; + } + }, BORDER_PROPERTIES); + }, borderStyles); + + return res; +}; + +export default memoizeOneFactory(( + columns: VisibleColumns, + headerRows: number, + borderStyles: IConvertedStyle[], + listViewStyle: boolean +) => { + if (headerRows === 0 || columns.length === 0) { + return; + } + + const edges = new EdgesMatrices(headerRows, columns.length, Environment.defaultEdge, true, !listViewStyle); + + R.forEach(i => + R.addIndex(R.forEach)( + (column, j) => { + const cellStyle = getWeightedStyle( + borderStyles, + column, + i + ); + + edges.setEdges(i, j, cellStyle); + }, + columns + ), + R.range(0, headerRows) + ); + + return edges; +}); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/edges/operationOfData.ts b/packages/dash-table/src/dash-table/derived/edges/operationOfData.ts new file mode 100644 index 0000000000..d4bbd906aa --- /dev/null +++ b/packages/dash-table/src/dash-table/derived/edges/operationOfData.ts @@ -0,0 +1,72 @@ +import * as R from 'ramda'; + +import Environment from 'core/environment'; +import { memoizeOneFactory } from 'core/memoizer'; + +import { + Data, + IViewportOffset +} from 'dash-table/components/Table/props'; + +import { IConvertedStyle } from '../style'; +import { BorderStyle, BORDER_PROPERTIES, EdgesMatrices } from './type'; + +const getWeightedStyle = ( + borderStyles: IConvertedStyle[], + index: number, + offset: IViewportOffset, + datum: any +): BorderStyle => { + const res: BorderStyle = {}; + + R.addIndex(R.forEach)((rs, i) => { + if (rs.checksColumn() || + !rs.matchesRow(index + offset.rows) || + !rs.matchesFilter(datum) + ) { + return; + } + + R.forEach(p => { + const s = rs.style[p] || rs.style.border; + + if (!R.isNil(s)) { + res[p] = [s, i]; + } + }, BORDER_PROPERTIES); + }, borderStyles); + + return res; +}; + +export default memoizeOneFactory(( + columns: number, + borderStyles: IConvertedStyle[], + data: Data, + offset: IViewportOffset, + listViewStyle: boolean +) => { + if (data.length === 0 || columns === 0) { + return; + } + + const edges = new EdgesMatrices(data.length, columns, Environment.defaultEdge, true, !listViewStyle); + + R.addIndex(R.forEach)((datum, i) => + R.forEach(j => { + const cellStyle = getWeightedStyle( + borderStyles, + i, + offset, + datum + ); + + edges.setEdges(i, j, cellStyle); + }, + R.range(0, columns) + ), + data + ); + + return edges; +}); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/edges/operationOfFilters.ts b/packages/dash-table/src/dash-table/derived/edges/operationOfFilters.ts new file mode 100644 index 0000000000..5098262b10 --- /dev/null +++ b/packages/dash-table/src/dash-table/derived/edges/operationOfFilters.ts @@ -0,0 +1,55 @@ +import * as R from 'ramda'; + +import Environment from 'core/environment'; +import { memoizeOneFactory } from 'core/memoizer'; + +import { IConvertedStyle } from '../style'; +import { BorderStyle, BORDER_PROPERTIES, EdgesMatrices } from './type'; + +const getWeightedStyle = ( + borderStyles: IConvertedStyle[] +): BorderStyle => { + const res: BorderStyle = {}; + + R.addIndex(R.forEach)((rs, i) => { + if (rs.checksColumn()) { + return; + } + + R.forEach(p => { + const s = rs.style[p] || rs.style.border; + + if (!R.isNil(s)) { + res[p] = [s, i]; + } + }, BORDER_PROPERTIES); + }, borderStyles); + + return res; +}; + +export default memoizeOneFactory(( + columns: number, + filtering: boolean, + borderStyles: IConvertedStyle[], + listViewStyle: boolean +) => { + if (!filtering || columns === 0) { + return; + } + + const edges = new EdgesMatrices(1, columns, Environment.defaultEdge, true, !listViewStyle); + + R.forEach(i => + R.forEach(j => { + const cellStyle = getWeightedStyle(borderStyles); + + edges.setEdges(i, j, cellStyle); + }, + R.range(0, columns) + ), + R.range(0, 1) + ); + + return edges; +}); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/edges/operationOfHeaders.ts b/packages/dash-table/src/dash-table/derived/edges/operationOfHeaders.ts new file mode 100644 index 0000000000..60c83e3366 --- /dev/null +++ b/packages/dash-table/src/dash-table/derived/edges/operationOfHeaders.ts @@ -0,0 +1,61 @@ +import * as R from 'ramda'; + +import Environment from 'core/environment'; +import { memoizeOneFactory } from 'core/memoizer'; + +import { IConvertedStyle } from '../style'; +import { BorderStyle, BORDER_PROPERTIES, EdgesMatrices } from './type'; + +const getWeightedStyle = ( + borderStyles: IConvertedStyle[], + index: number +): BorderStyle => { + const res: BorderStyle = {}; + + R.addIndex(R.forEach)((rs, i) => { + if (rs.checksColumn() || + !rs.matchesRow(index) + ) { + return; + } + + R.forEach(p => { + const s = rs.style[p] || rs.style.border; + + if (!R.isNil(s)) { + res[p] = [s, i]; + } + }, BORDER_PROPERTIES); + }, borderStyles); + + return res; +}; + +export default memoizeOneFactory(( + columns: number, + headerRows: number, + borderStyles: IConvertedStyle[], + listViewStyle: boolean +) => { + if (headerRows === 0 || columns === 0) { + return; + } + + const edges = new EdgesMatrices(headerRows, columns, Environment.defaultEdge, true, !listViewStyle); + + R.forEach(i => + R.forEach(j => { + const cellStyle = getWeightedStyle( + borderStyles, + i + ); + + edges.setEdges(i, j, cellStyle); + }, + R.range(0, columns) + ), + R.range(0, headerRows) + ); + + return edges; +}); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/edges/type.ts b/packages/dash-table/src/dash-table/derived/edges/type.ts new file mode 100644 index 0000000000..34b5d6c2c1 --- /dev/null +++ b/packages/dash-table/src/dash-table/derived/edges/type.ts @@ -0,0 +1,214 @@ +import * as R from 'ramda'; +import { CSSProperties } from 'react'; + +import { OptionalMap, OptionalProp, PropOf } from 'core/type'; +import py2jsCssProperties from '../style/py2jsCssProperties'; + +export type Edge = any; + +type BorderProp = + PropOf | + PropOf | + PropOf | + PropOf; + +export type BorderStyle = + OptionalMap, number]> & + OptionalMap, number]> & + OptionalMap, number]> & + OptionalMap, number]>; + +export const BORDER_PROPERTIES: BorderProp[] = [ + 'borderBottom', + 'borderLeft', + 'borderRight', + 'borderTop' +]; + +export const BORDER_PROPERTIES_AND_FRAGMENTS: string[] = R.uniq( + R.filter( + p => p.indexOf('border') === 0, + Array.from(py2jsCssProperties.values()) + ) +); + +export interface IEdgesMatrix { + getEdge(i: number, j: number): Edge; + getEdges(): Edge[][]; + getWeight(i: number, j: number): number; + isDefault(i: number, j: number): boolean; +} + +export interface IEdgesMatrices { + getEdges(): { + horizontal: Edge[][], + vertical: Edge[][] + }; + getMatrices(): { + horizontal: EdgesMatrix, + vertical: EdgesMatrix + }; + getStyle(i: number, j: number): CSSProperties; +} + +export class EdgesMatrix implements IEdgesMatrix { + private weights: number[][]; + private edges: Edge[][]; + + public readonly rows: number; + public readonly columns: number; + public readonly defaultEdge: Edge | undefined; + + constructor(m: EdgesMatrix); + constructor( + rows: number, + columns: number, + defaultEdge?: Edge + ); + constructor( + rowsOrMatrix: number | EdgesMatrix, + columns?: number, + defaultEdge?: Edge + ) { + if (typeof rowsOrMatrix === 'number' && typeof columns !== 'undefined') { + const rows = rowsOrMatrix; + + this.rows = rows; + this.columns = columns; + this.defaultEdge = defaultEdge; + + this.weights = R.map( + () => new Array(columns).fill(-Infinity), + R.range(0, rows) + ); + + this.edges = R.map( + () => new Array(columns).fill(defaultEdge), + R.range(0, rows) + ); + } else { + const source = rowsOrMatrix as EdgesMatrix; + + this.rows = source.rows; + this.columns = source.columns; + this.defaultEdge = source.defaultEdge; + + this.weights = R.clone(source.weights); + this.edges = R.clone(source.edges); + } + } + + setEdge(i: number, j: number, edge: Edge, weight: number, force: boolean = false) { + if (!force && (R.isNil(edge) || weight <= this.weights[i][j])) { + return; + } + + this.weights[i][j] = weight; + this.edges[i][j] = edge; + } + + getEdge = (i: number, j: number) => this.edges[i][j]; + + getEdges = () => this.edges; + + getWeight = (i: number, j: number) => this.weights[i][j]; + + isDefault = (i: number, j: number) => this.weights[i][j] === -Infinity; + + clone = () => new EdgesMatrix(this); +} + +export class EdgesMatrices implements IEdgesMatrices { + private readonly horizontal: EdgesMatrix; + private readonly vertical: EdgesMatrix; + + private readonly horizontalEdges: boolean; + private readonly verticalEdges: boolean; + + private readonly rows: number; + private readonly columns: number; + private readonly defaultEdge: Edge | undefined; + + constructor(m: EdgesMatrices); + constructor( + rows: number, + columns: number, + defaultEdge: Edge | undefined, + horizontalEdges?: boolean, + verticalEdges?: boolean + ); + constructor( + rowsOrMatrix: number | EdgesMatrices, + columns?: number, + defaultEdge?: Edge, + horizontalEdges?: boolean, + verticalEdges?: boolean + ) { + if (typeof rowsOrMatrix === 'number' && typeof columns !== 'undefined') { + const rows = rowsOrMatrix; + + this.rows = rows; + this.columns = columns; + this.defaultEdge = defaultEdge; + + this.horizontalEdges = R.isNil(horizontalEdges) || horizontalEdges; + this.verticalEdges = R.isNil(verticalEdges) || verticalEdges; + + this.horizontal = new EdgesMatrix(rows + 1, columns, this.horizontalEdges ? defaultEdge : undefined); + this.vertical = new EdgesMatrix(rows, columns + 1, this.verticalEdges ? defaultEdge : undefined); + } else { + const source = rowsOrMatrix as EdgesMatrices; + + this.rows = source.rows; + this.columns = source.columns; + this.defaultEdge = source.defaultEdge; + + this.horizontal = source.horizontal.clone(); + this.vertical = source.vertical.clone(); + + this.horizontalEdges = source.horizontalEdges; + this.verticalEdges = source.verticalEdges; + } + } + + setEdges(i: number, j: number, style: BorderStyle) { + if (this.horizontalEdges) { + if (style.borderTop) { + this.horizontal.setEdge(i, j, style.borderTop[0], style.borderTop[1]); + } + + if (style.borderBottom) { + this.horizontal.setEdge(i + 1, j, style.borderBottom[0], style.borderBottom[1]); + } + } + + if (this.verticalEdges) { + if (style.borderLeft) { + this.vertical.setEdge(i, j, style.borderLeft[0], style.borderLeft[1]); + } + + if (style.borderRight) { + this.vertical.setEdge(i, j + 1, style.borderRight[0], style.borderRight[1]); + } + } + } + + getEdges = () => ({ + horizontal: this.horizontal.getEdges(), + vertical: this.vertical.getEdges() + }) + + getMatrices = () => ({ + horizontal: this.horizontal, + vertical: this.vertical + }) + + getStyle = (i: number, j: number): CSSProperties => ({ + borderBottom: this.horizontal.getEdge(i + 1, j) || null, + borderTop: this.horizontal.getEdge(i, j) || null, + borderLeft: this.vertical.getEdge(i, j) || null, + borderRight: this.vertical.getEdge(i, j + 1) || null + }) + + clone = () => new EdgesMatrices(this); +} \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/filter/map.ts b/packages/dash-table/src/dash-table/derived/filter/map.ts new file mode 100644 index 0000000000..88787701b9 --- /dev/null +++ b/packages/dash-table/src/dash-table/derived/filter/map.ts @@ -0,0 +1,80 @@ +import * as R from 'ramda'; + +import { memoizeOneFactory } from 'core/memoizer'; + +import { VisibleColumns, IVisibleColumn } from 'dash-table/components/Table/props'; +import { SingleColumnSyntaxTree, MultiColumnsSyntaxTree, getSingleColumnMap } from 'dash-table/syntax-tree'; + +const cloneIf = ( + current: Map, + base: Map +) => current === base ? new Map(base) : current; + +export default memoizeOneFactory(( + map: Map, + query: string, + columns: VisibleColumns +): Map => { + const multiQuery = new MultiColumnsSyntaxTree(query); + const reversedMap = getSingleColumnMap(multiQuery, columns); + + /* + * Couldn't process the query, just use the previous value. + */ + if (!reversedMap) { + return map; + } + + /* Mapping multi-column to single column queries will expand + * compressed forms. If the new ast query is equal to the + * old one, keep the old one instead. + * + * If the value was changed by the user, the current ast will + * have been modified already and the UI experience will also + * be consistent in that case. + */ + let newMap = map; + + const keys = R.uniq( + R.concat( + Array.from(map.keys()), + Array.from(reversedMap.keys()) + ) + ); + + R.forEach(key => { + const ast = map.get(key); + const reversedAst = reversedMap.get(key); + + if (R.isNil(reversedAst)) { + newMap = cloneIf(newMap, map); + newMap.delete(key); + } else if ( + R.isNil(ast) || + reversedAst.toQueryString() !== ast.toQueryString() + ) { + newMap = cloneIf(newMap, map); + newMap.set(key, reversedAst); + } + }, keys); + + return newMap; +}); + +export const updateMap = ( + map: Map, + column: IVisibleColumn, + value: any +): Map => { + const safeColumnId = column.id.toString(); + + const newMap = new Map(map); + + if (value && value.length) { + newMap.set(safeColumnId, new SingleColumnSyntaxTree(value, column)); + } else { + newMap.delete(safeColumnId); + } + + return newMap; +}; \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/filter/wrapperStyles.ts b/packages/dash-table/src/dash-table/derived/filter/wrapperStyles.ts index 81fa97ce26..1e85e0d55d 100644 --- a/packages/dash-table/src/dash-table/derived/filter/wrapperStyles.ts +++ b/packages/dash-table/src/dash-table/derived/filter/wrapperStyles.ts @@ -6,6 +6,7 @@ import { memoizeOneFactory } from 'core/memoizer'; import { VisibleColumns } from 'dash-table/components/Table/props'; import { IConvertedStyle } from '../style'; +import { BORDER_PROPERTIES_AND_FRAGMENTS } from '../edges/type'; type Style = CSSProperties | undefined; @@ -22,8 +23,37 @@ function getter( ) ); - return relevantStyles.length ? R.mergeAll(relevantStyles) : undefined; + return relevantStyles.length ? + R.omit( + BORDER_PROPERTIES_AND_FRAGMENTS, + R.mergeAll(relevantStyles) + ) : + undefined; }, columns); } +function opGetter( + rows: number, + columns: number, + columnStyles: IConvertedStyle[] +) { + return R.map(() => R.map(() => { + const relevantStyles = R.map( + s => s.style, + R.filter( + style => !style.checksColumn(), + columnStyles + ) + ); + + return relevantStyles.length ? + R.omit( + BORDER_PROPERTIES_AND_FRAGMENTS, + R.mergeAll(relevantStyles) + ) : + undefined; + }, R.range(0, columns)), R.range(0, rows)); +} + export default memoizeOneFactory(getter); +export const derivedFilterOpStyles = memoizeOneFactory(opGetter); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/header/wrapperStyles.ts b/packages/dash-table/src/dash-table/derived/header/wrapperStyles.ts index d887da1f6b..607567cfc9 100644 --- a/packages/dash-table/src/dash-table/derived/header/wrapperStyles.ts +++ b/packages/dash-table/src/dash-table/derived/header/wrapperStyles.ts @@ -6,6 +6,7 @@ import { memoizeOneFactory } from 'core/memoizer'; import { VisibleColumns } from 'dash-table/components/Table/props'; import { IConvertedStyle } from '../style'; +import { BORDER_PROPERTIES_AND_FRAGMENTS } from '../edges/type'; type Style = CSSProperties | undefined; @@ -25,8 +26,37 @@ function getter( ) ); - return relevantStyles.length ? R.mergeAll(relevantStyles) : undefined; + return relevantStyles.length ? + R.omit( + BORDER_PROPERTIES_AND_FRAGMENTS, + R.mergeAll(relevantStyles) + ) : + undefined; }, columns), R.range(0, headerRows)); } +function opGetter( + rows: number, + columns: number, + columnStyles: IConvertedStyle[] +) { + return R.map(() => R.map(() => { + const relevantStyles = R.map( + s => s.style, + R.filter( + style => !style.checksColumn(), + columnStyles + ) + ); + + return relevantStyles.length ? + R.omit( + BORDER_PROPERTIES_AND_FRAGMENTS, + R.mergeAll(relevantStyles) + ) : + undefined; + }, R.range(0, columns)), R.range(0, rows)); +} + export default memoizeOneFactory(getter); +export const derivedHeaderOpStyles = memoizeOneFactory(opGetter); \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/style/index.ts b/packages/dash-table/src/dash-table/derived/style/index.ts index 641671bea5..61d6e69307 100644 --- a/packages/dash-table/src/dash-table/derived/style/index.ts +++ b/packages/dash-table/src/dash-table/derived/style/index.ts @@ -28,30 +28,41 @@ import { QuerySyntaxTree } from 'dash-table/syntax-tree'; export interface IConvertedStyle { style: CSSProperties; - matchesColumn: (column: IVisibleColumn) => boolean; - matchesRow: (index: number) => boolean; + checksColumn: () => boolean; + checksRow: () => boolean; + checksFilter: () => boolean; + matchesColumn: (column: IVisibleColumn | undefined) => boolean; + matchesRow: (index: number | undefined) => boolean; matchesFilter: (datum: Datum) => boolean; } type GenericIf = Partial; type GenericStyle = Style & Partial<{ if: GenericIf }>; -function convertElement(style: GenericStyle) { +function convertElement(style: GenericStyle): IConvertedStyle { const indexFilter = style.if && (style.if.header_index || style.if.row_index); let ast: QuerySyntaxTree; return { - matchesColumn: (column: IVisibleColumn) => + checksColumn: () => !R.isNil(style.if) && ( + !R.isNil(style.if.column_id) || + !R.isNil(style.if.column_type) + ), + checksRow: () => !R.isNil(indexFilter), + checksFilter: () => !R.isNil(style.if) && !R.isNil(style.if.filter), + + matchesColumn: (column: IVisibleColumn | undefined) => !style.if || ( - ifColumnId(style.if, column.id) && - ifColumnType(style.if, column.type) + !R.isNil(column) && + ifColumnId(style.if, column && column.id) && + ifColumnType(style.if, column && column.type) ), - matchesRow: (index: number) => + matchesRow: (index: number | undefined) => indexFilter === undefined ? true : typeof indexFilter === 'number' ? index === indexFilter : - indexFilter === 'odd' ? index % 2 === 1 : index % 2 === 0, + !R.isNil(index) && (indexFilter === 'odd' ? index % 2 === 1 : index % 2 === 0), matchesFilter: (datum: Datum) => !style.if || style.if.filter === undefined || @@ -74,48 +85,36 @@ export const derivedRelevantCellStyles = memoizeOneFactory(( dataCell: Style, cells: Cells, dataCells: DataCells -) => R.concat( - R.concat( - cell ? [convertElement(cell)] : [], - R.map(convertElement, cells || []) - ), - R.concat( - dataCell ? [convertElement(dataCell)] : [], - R.map(convertElement, dataCells || []) - ) -)); +) => R.unnest([ + cell ? [convertElement(cell)] : [], + R.map(convertElement, cells || []), + dataCell ? [convertElement(dataCell)] : [], + R.map(convertElement, dataCells || []) +])); export const derivedRelevantFilterStyles = memoizeOneFactory(( cell: Style, filter: Style, cells: Cells, filters: BasicFilters -) => R.concat( - R.concat( - cell ? [convertElement(cell)] : [], - R.map(convertElement, cells || []) - ), - R.concat( - filter ? [convertElement(filter)] : [], - R.map(convertElement, filters || []) - ) -)); +) => R.unnest([ + cell ? [convertElement(cell)] : [], + R.map(convertElement, cells || []), + filter ? [convertElement(filter)] : [], + R.map(convertElement, filters || []) +])); export const derivedRelevantHeaderStyles = memoizeOneFactory(( cell: Style, header: Style, cells: Cells, headers: Headers -) => R.concat( - R.concat( - cell ? [convertElement(cell)] : [], - R.map(convertElement, cells || []) - ), - R.concat( - header ? [convertElement(header)] : [], - R.map(convertElement, headers || []) - ) -)); +) => R.unnest([ + cell ? [convertElement(cell)] : [], + R.map(convertElement, cells || []), + header ? [convertElement(header)] : [], + R.map(convertElement, headers || []) +])); export const derivedTableStyle = memoizeOneFactory( (defaultTable: Table, table: Table) => [ diff --git a/packages/dash-table/src/dash-table/derived/table/fragments.tsx b/packages/dash-table/src/dash-table/derived/table/fragments.tsx index db5ce3204e..1274003b1b 100644 --- a/packages/dash-table/src/dash-table/derived/table/fragments.tsx +++ b/packages/dash-table/src/dash-table/derived/table/fragments.tsx @@ -18,12 +18,17 @@ function renderFragment(cells: any[][] | null, offset: number = 0) { null; } +const isEmpty = (cells: JSX.Element[][] | null) => + !cells || + cells.length === 0 || + cells[0].length === 0; + export default ( fixedColumns: number, fixedRows: number, cells: JSX.Element[][], offset: number -): (JSX.Element | null)[][] => { +): { grid: (JSX.Element | null)[][], empty: boolean[][] } => { // slice out fixed columns const fixedColumnCells = fixedColumns ? R.map(row => @@ -50,8 +55,14 @@ export default ( fixedColumnCells.splice(0, fixedRows) : null; - return [ - [renderFragment(fixedRowAndColumnCells), renderFragment(fixedRowCells)], - [renderFragment(fixedColumnCells), renderFragment(cells, offset)] - ]; + return { + grid: [ + [renderFragment(fixedRowAndColumnCells), renderFragment(fixedRowCells)], + [renderFragment(fixedColumnCells), renderFragment(cells, offset)] + ], + empty: [ + [isEmpty(fixedRowAndColumnCells), isEmpty(fixedRowCells)], + [isEmpty(fixedColumnCells), isEmpty(cells)] + ] + }; }; \ No newline at end of file diff --git a/packages/dash-table/src/dash-table/derived/table/index.tsx b/packages/dash-table/src/dash-table/derived/table/index.tsx index b4a59ad920..14417c49ee 100644 --- a/packages/dash-table/src/dash-table/derived/table/index.tsx +++ b/packages/dash-table/src/dash-table/derived/table/index.tsx @@ -3,32 +3,44 @@ import * as R from 'ramda'; import { memoizeOne } from 'core/memoizer'; import CellFactory from 'dash-table/components/CellFactory'; +import EdgeFactory from 'dash-table/components/EdgeFactory'; import FilterFactory from 'dash-table/components/FilterFactory'; import HeaderFactory from 'dash-table/components/HeaderFactory'; import { clearSelection } from 'dash-table/utils/actions'; import { ControlledTableProps, SetProps, SetState } from 'dash-table/components/Table/props'; -const handleSetFilter = (setProps: SetProps, setState: SetState, filter: string, rawFilterQuery: string) => { +import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; + +const handleSetFilter = ( + setProps: SetProps, + setState: SetState, + filter: string, + rawFilterQuery: string, + map: Map +) => { setProps({ filter, ...clearSelection }); - setState({ rawFilterQuery }); + setState({ workFilter: { map, value: filter }, rawFilterQuery }); }; function filterPropsFn(propsFn: () => ControlledTableProps, setFilter: any) { const props = propsFn(); - return R.merge(props, { setFilter }); + return R.merge(props, { map: props.workFilter.map, setFilter }); } function getter( cellFactory: CellFactory, filterFactory: FilterFactory, - headerFactory: HeaderFactory + headerFactory: HeaderFactory, + edgeFactory: EdgeFactory ): JSX.Element[][] { const cells: JSX.Element[][] = []; - const dataCells = cellFactory.createCells(); - const filters = filterFactory.createFilters(); - const headers = headerFactory.createHeaders(); + const edges = edgeFactory.createEdges(); + + const dataCells = cellFactory.createCells(edges.dataEdges, edges.dataOpEdges); + const filters = filterFactory.createFilters(edges.filterEdges, edges.filterOpEdges); + const headers = headerFactory.createHeaders(edges.headerEdges, edges.headerOpEdges); cells.push(...headers); cells.push(...filters); @@ -50,6 +62,7 @@ export default (propsFn: () => ControlledTableProps) => { return filterPropsFn(propsFn, setFilter(props.setProps, props.setState)); }); const headerFactory = new HeaderFactory(propsFn); + const edgeFactory = new EdgeFactory(propsFn); - return getter.bind(undefined, cellFactory, filterFactory, headerFactory); + return getter.bind(undefined, cellFactory, filterFactory, headerFactory, edgeFactory); }; diff --git a/packages/dash-table/tests/cypress/tests/standalone/filtering_test.ts b/packages/dash-table/tests/cypress/tests/standalone/filtering_test.ts index 7417860da7..a6bbb60537 100644 --- a/packages/dash-table/tests/cypress/tests/standalone/filtering_test.ts +++ b/packages/dash-table/tests/cypress/tests/standalone/filtering_test.ts @@ -113,6 +113,17 @@ describe('filter', () => { DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '100')); }); + it('typing invalid followed by valid query fragment does not reset invalid', () => { + DashTable.getFilterById('ccc').click(); + DOM.focused.type(`gt`); + DashTable.getFilterById('ddd').click(); + DOM.focused.type('lt 20000'); + DashTable.getFilterById('eee').click(); + + DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt')); + DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'lt 20000')); + }); + it('reset updates results and filter fields', () => { let cell_0; let cell_1; diff --git a/packages/dash-table/tests/cypress/tests/unit/edges_test.ts b/packages/dash-table/tests/cypress/tests/unit/edges_test.ts new file mode 100644 index 0000000000..67a043969e --- /dev/null +++ b/packages/dash-table/tests/cypress/tests/unit/edges_test.ts @@ -0,0 +1,400 @@ +import dataEdges from 'dash-table/derived/edges/data'; +import Environment from 'core/environment'; + +describe('data edges', () => { + const edgesFn = dataEdges(); + + it('without data has no edges', () => { + const res = edgesFn( + [{ id: 'id', name: 'id' }], + [], + [], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res).to.equal(undefined); + }); + + it('without one data row', () => { + const res = edgesFn( + [], + [], + [{ id: 1 }], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res).to.equal(undefined); + }); + + it('uses `undefined` default style', () => { + const res = edgesFn( + [{ id: 'id', name: 'id' }], + [], + [{ id: 1 }], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res).to.not.equal(undefined); + if (res) { + const { horizontal, vertical } = res.getEdges(); + + expect(horizontal.length).to.equal(2); + expect(horizontal[0].length).to.equal(1); + expect(horizontal[1].length).to.equal(1); + expect(horizontal[0][0]).to.equal(Environment.defaultEdge); + expect(horizontal[1][0]).to.equal(Environment.defaultEdge); + + expect(vertical.length).to.equal(1); + expect(vertical[0].length).to.equal(2); + expect(vertical[0][0]).to.equal(Environment.defaultEdge); + expect(vertical[0][1]).to.equal(Environment.defaultEdge); + } + }); + + it('uses default style', () => { + const res = edgesFn( + [{ id: 'id', name: 'id' }], + [], + [{ id: 1 }], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res).to.not.equal(undefined); + if (res) { + const { horizontal, vertical } = res.getEdges(); + + expect(horizontal.length).to.equal(2); + expect(horizontal[0].length).to.equal(1); + expect(horizontal[1].length).to.equal(1); + expect(horizontal[0][0]).to.equal(Environment.defaultEdge); + expect(horizontal[1][0]).to.equal(Environment.defaultEdge); + + expect(vertical.length).to.equal(1); + expect(vertical[0].length).to.equal(2); + expect(vertical[0][0]).to.equal(Environment.defaultEdge); + expect(vertical[0][1]).to.equal(Environment.defaultEdge); + } + }); + + it('uses default style on multiple rows & columns', () => { + const res = edgesFn( + [ + { id: 'id', name: 'id' }, + { id: 'name', name: 'name' } + ], + [], + [ + { id: 1, name: 'a' }, + { id: 1, name: 'b' }, + { id: 2, name: 'a' }, + { id: 2, name: 'b' } + ], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res).to.not.equal(undefined); + if (res) { + const { horizontal, vertical } = res.getEdges(); + + expect(horizontal.length).to.equal(5); + horizontal.forEach(edges => { + expect(edges.length).to.equal(2); + + edges.forEach(edge => { + expect(edge).to.equal(Environment.defaultEdge); + }); + }); + + expect(vertical.length).to.equal(4); + vertical.forEach(edges => { + expect(edges.length).to.equal(3); + + edges.forEach(edge => { + expect(edge).to.equal(Environment.defaultEdge); + }); + }); + } + }); + + it('applies `border`', () => { + const res = edgesFn( + [ + { id: 'id', name: 'id' }, + { id: 'name', name: 'name' } + ], + [{ + style: { border: '1px solid green' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }], + [ + { id: 1, name: 'a' }, + { id: 1, name: 'b' }, + { id: 2, name: 'a' }, + { id: 2, name: 'b' } + ], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res).to.not.equal(undefined); + if (res) { + const { horizontal, vertical } = res.getEdges(); + + expect(horizontal.length).to.equal(5); + horizontal.forEach(edges => { + expect(edges.length).to.equal(2); + + edges.forEach(edge => { + expect(edge).to.equal('1px solid green'); + }); + }); + + expect(vertical.length).to.equal(4); + vertical.forEach(edges => { + expect(edges.length).to.equal(3); + + edges.forEach(edge => { + expect(edge).to.equal('1px solid green'); + }); + }); + } + }); + + it('applies `borderLeft` and `borderTop`', () => { + const res = edgesFn( + [ + { id: 'id', name: 'id' }, + { id: 'name', name: 'name' } + ], + [{ + style: { borderLeft: '1px solid green', borderTop: '1px solid darkgreen' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }], + [ + { id: 1, name: 'a' }, + { id: 1, name: 'b' }, + { id: 2, name: 'a' }, + { id: 2, name: 'b' } + ], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res).to.not.equal(undefined); + if (res) { + const { horizontal, vertical } = res.getEdges(); + + expect(horizontal.length).to.equal(5); + horizontal.forEach((edges, rowIndex) => { + expect(edges.length).to.equal(2); + + edges.forEach(edge => { + expect(edge).to.equal(rowIndex === horizontal.length - 1 ? + Environment.defaultEdge : + '1px solid darkgreen' + ); + }); + }); + + expect(vertical.length).to.equal(4); + vertical.forEach(edges => { + expect(edges.length).to.equal(3); + + edges.forEach((edge, index) => { + expect(edge).to.equal(index === edges.length - 1 ? + Environment.defaultEdge : + '1px solid green' + ); + }); + }); + } + }); + + it('applies `borderLeft` overridden by higher precedence `borderRight`', () => { + const res = edgesFn( + [ + { id: 'id', name: 'id' }, + { id: 'name', name: 'name' } + ], + [{ + style: { borderLeft: '1px solid green' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }, { + style: { borderRight: '1px solid darkgreen' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }], + [ + { id: 1, name: 'a' } + ], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res).to.not.equal(undefined); + if (res) { + const { vertical } = res.getEdges(); + + expect(vertical.length).to.equal(1); + expect(vertical[0].length).to.equal(3); + expect(vertical[0][0]).to.equal('1px solid green'); + expect(vertical[0][1]).to.equal('1px solid darkgreen'); + expect(vertical[0][2]).to.equal('1px solid darkgreen'); + } + }); + + it('applies `borderLeft` not overridden by lower precedence `borderRight`', () => { + const res = edgesFn( + [ + { id: 'id', name: 'id' }, + { id: 'name', name: 'name' } + ], + [{ + style: { borderRight: '1px solid darkgreen' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }, { + style: { borderLeft: '1px solid green' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }], + [ + { id: 1, name: 'a' } + ], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res === undefined).to.equal(false); + if (res) { + const { vertical } = res.getEdges(); + + expect(vertical.length).to.equal(1); + expect(vertical[0].length).to.equal(3); + expect(vertical[0][0]).to.equal('1px solid green'); + expect(vertical[0][1]).to.equal('1px solid green'); + expect(vertical[0][2]).to.equal('1px solid darkgreen'); + } + }); + + it('applies `borderLeft` overridden by higher precedence `border`', () => { + const res = edgesFn( + [ + { id: 'id', name: 'id' }, + { id: 'name', name: 'name' } + ], + [{ + style: { borderLeft: '1px solid darkgreen' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }, { + style: { border: '1px solid green' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }], + [ + { id: 1, name: 'a' } + ], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res !== undefined).to.equal(true); + if (res) { + const { horizontal, vertical } = res.getEdges(); + + expect(horizontal.length).to.equal(2); + horizontal.forEach(edges => { + expect(edges.length).to.equal(2); + + edges.forEach(edge => { + expect(edge).to.equal('1px solid green'); + }); + }); + + expect(vertical.length).to.equal(1); + vertical.forEach(edges => { + expect(edges.length).to.equal(3); + + edges.forEach(edge => { + expect(edge).to.equal('1px solid green'); + }); + }); + } + }); + + it('applies `border` overridden by higher precedence `borderLeft`', () => { + const res = edgesFn( + [ + { id: 'id', name: 'id' }, + { id: 'name', name: 'name' } + ], + [{ + style: { border: '1px solid green' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }, { + style: { borderLeft: '1px solid darkgreen' }, + matchesColumn: () => true, + matchesFilter: () => true, + matchesRow: () => true + }], + [ + { id: 1, name: 'a' } + ], + { columns: 0, rows: 0 }, + undefined, + false + ); + + expect(res !== undefined).to.equal(true); + if (res) { + const { horizontal, vertical } = res.getEdges(); + + expect(horizontal.length).to.equal(2); + horizontal.forEach(edges => { + expect(edges.length).to.equal(2); + + edges.forEach(edge => { + expect(edge).to.equal('1px solid green'); + }); + }); + + expect(vertical.length).to.equal(1); + vertical.forEach(edges => { + expect(edges.length).to.equal(3); + + edges.forEach((edge, j) => { + expect(edge).to.equal(j + 1 === edges.length ? '1px solid green' : '1px solid darkgreen'); + }); + }); + } + }); +}); \ No newline at end of file diff --git a/packages/dash-table/tests/visual/percy-storybook/Border.percy.tsx b/packages/dash-table/tests/visual/percy-storybook/Border.defaults.percy.tsx similarity index 91% rename from packages/dash-table/tests/visual/percy-storybook/Border.percy.tsx rename to packages/dash-table/tests/visual/percy-storybook/Border.defaults.percy.tsx index 0f60e97320..55fdf3c4fb 100644 --- a/packages/dash-table/tests/visual/percy-storybook/Border.percy.tsx +++ b/packages/dash-table/tests/visual/percy-storybook/Border.defaults.percy.tsx @@ -40,10 +40,15 @@ const style_table = { }; const style_data_conditional = [ - { width: 100 } + { + if: {}, + width: 100, + minWidth: 100, + maxWidth: 100 + } ]; -let props = { +export const BORDER_PROPS_DEFAULTS = { setProps, id: 'table', data: data, @@ -63,18 +68,18 @@ let props2 = { storiesOf('DashTable/Border (available space not filled)', module) .add('with no frozen rows and no frozen columns', () => ()) .add('with frozen rows and no frozen columns', () => ()) .add('with no frozen rows and frozen columns', () => ()) .add('with frozen rows and frozen columns', () => ()); @@ -97,7 +102,7 @@ storiesOf('DashTable/Border (available space filled)', module) n_fixed_rows={1} />)); -let props3 = Object.assign({}, props, { +let props3 = Object.assign({}, BORDER_PROPS_DEFAULTS, { style_as_list_view: true }); diff --git a/packages/dash-table/tests/visual/percy-storybook/Border.style.percy.tsx b/packages/dash-table/tests/visual/percy-storybook/Border.style.percy.tsx new file mode 100644 index 0000000000..43ad5c224b --- /dev/null +++ b/packages/dash-table/tests/visual/percy-storybook/Border.style.percy.tsx @@ -0,0 +1,311 @@ +import * as R from 'ramda'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import DataTable from 'dash-table/dash/DataTable'; +import { BORDER_PROPS_DEFAULTS } from './Border.defaults.percy'; + +const OPS_VARIANTS: ITest[] = [ + { name: 'with ops', props: { row_deletable: true, row_selectable: 'single' } }, + { name: 'fixed columns', props: { n_fixed_columns: 2, row_deletable: true, row_selectable: 'single' } }, + { name: 'fixed rows', props: { n_fixed_rows: 1, row_deletable: true, row_selectable: 'single' } }, + { name: 'fixed columns & rows', props: { n_fixed_columns: 2, n_fixed_rows: 1, row_deletable: true, row_selectable: 'single' } }, + { name: 'fixed columns & rows inside fragments', props: { n_fixed_columns: 3, n_fixed_rows: 2, row_deletable: true, row_selectable: 'single' } } +]; + +interface ITest { + name: string; + props: any; +} + +const ALL_VARIANTS: ITest[] = [ + { name: 'base', props: {} }, + ...OPS_VARIANTS +]; + +const scenarios: ITest[] = [ + { + name: 'with defaults', + props: {} + }, { + name: 'with defaults & active cell (1,1)', props: { + active_cell: { + column: 1, + column_id: 'b', + row: 1, + row_id: null + } + } + }, { + name: 'with defaults & active cell (0, 0)', + props: { + active_cell: { + column: 0, + column_id: 'a', + row: 0, + row_id: null + } + } + }, { + name: 'with cell style', + props: { + style_cell: { + border: '1px solid hotpink' + } + } + }, { + name: 'with data style', + props: { + style_data: { + border: '1px solid hotpink' + } + } + }, { + name: 'with header style', + props: { + style_header: { + border: '1px solid hotpink' + } + } + }, { + name: 'with filter style', + props: { + filtering: true, + style_filter: { + border: '1px solid hotpink' + } + } + }, { + name: 'with header / cell (data) style - header wins on cell (data)', + props: { + style_cell: { + border: '1px solid teal' + }, + style_header: { + border: '1px solid hotpink' + } + } + }, { + name: 'with header / data style - data wins on header', + props: { + style_data: { + border: '1px solid teal' + }, + style_header: { + border: '1px solid hotpink' + } + } + }, { + name: 'with header / filter / cell (data) style - filter wins on header, filter wins on cell (data)', + props: { + filtering: true, + style_cell: { + border: '1px solid teal' + }, + style_filter: { + border: '1px solid burlywood' + }, + style_header: { + border: '1px solid hotpink' + } + } + }, { + name: 'with header / data / cell (filter) style - header wins on cell (filter), data wins on cell (filter)', + props: { + filtering: true, + style_data: { + border: '1px solid teal' + }, + style_cell: { + border: '1px solid burlywood' + }, + style_header: { + border: '1px solid hotpink' + } + } + }, { + name: 'with cell (header) / filter / data style - filter wins on cell (header), data wins on filter', + props: { + filtering: true, + style_data: { + border: '1px solid teal' + }, + style_filter: { + border: '1px solid burlywood' + }, + style_cell: { + border: '1px solid hotpink' + } + } + }, { + name: 'with data / cell (header, filter) style - data wins on filter', + props: { + filtering: true, + style_data: { + border: '1px solid teal' + }, + style_cell: { + border: '1px solid hotpink' + } + } + }, { + name: 'with header / filter / data style - data wins on filter, filter wins on header', + props: { + filtering: true, + style_data: { + border: '1px solid teal' + }, + style_filter: { + border: '1px solid burlywood' + }, + style_header: { + border: '1px solid hotpink' + } + } + }, { + name: 'style as list view', + props: { + filtering: true, + style_data: { + border: '1px solid teal' + }, + style_filter: { + border: '1px solid burlywood' + }, + style_header: { + border: '1px solid hotpink' + }, + style_as_list_view: true + } + } +]; + +const ops_scenarios: ITest[] = [ + { + name: 'data ops do not get styled on conditional column_id', + props: { + style_data: { + border: '1px solid black' + }, + style_data_conditional: [{ + if: { column_id: 'a' }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + }, { + name: 'data ops do not get styled on conditional column_type', + props: { + style_data: { + border: '1px solid black' + }, + style_data_conditional: [{ + if: { column_type: 'any' }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + }, { + name: 'data ops get styled on conditional row_index', + props: { + style_data: { + border: '1px solid black' + }, + style_data_conditional: [{ + if: { row_index: 1 }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + }, { + name: 'data ops get styled on conditional filter', + props: { + style_data: { + border: '1px solid black' + }, + style_data_conditional: [{ + if: { filter: '{a} eq 85' }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + }, { + name: 'header ops do not get styled on conditional column_id', + props: { + style_header: { + border: '1px solid black' + }, + style_header_conditional: [{ + if: { column_id: 'a' }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + }, { + name: 'header ops do not get styled on conditional column_type', + props: { + style_header: { + border: '1px solid black' + }, + style_header_conditional: [{ + if: { column_type: 'any' }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + }, { + name: 'header ops get styled on conditional header_index', + props: { + style_header: { + border: '1px solid black' + }, + style_header_conditional: [{ + if: { header_index: 0 }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + }, { + name: 'filter ops do not get styled on conditional column_id', + props: { + filtering: true, + style_filter: { + border: '1px solid black' + }, + style_filter_conditional: [{ + if: { column_id: 'a' }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + }, { + name: 'filter ops do not get styled on conditional column_type', + props: { + filtering: true, + style_filter: { + border: '1px solid black' + }, + style_filter_conditional: [{ + if: { column_type: 'any' }, + backgroundColor: 'pink', + border: '1px solid red' + }] + } + } +]; + +const tests = R.concat( + R.xprod(scenarios, ALL_VARIANTS), + R.xprod(ops_scenarios, OPS_VARIANTS) +); + +R.reduce( + (chain, [scenario, variant]) => chain.add(`${scenario.name} (${variant.name})`, () => ()), + storiesOf(`DashTable/Border, custom styles`, module), + tests +); \ No newline at end of file diff --git a/packages/dash-table/tests/visual/percy-storybook/fixtures.ts b/packages/dash-table/tests/visual/percy-storybook/fixtures.ts index 8ec610969c..19dc6eac5c 100644 --- a/packages/dash-table/tests/visual/percy-storybook/fixtures.ts +++ b/packages/dash-table/tests/visual/percy-storybook/fixtures.ts @@ -33,7 +33,7 @@ export default [ editable: true, css: [{ selector: '.dash-spreadsheet.dash-freeze-top', - rule: 'height: 100px;' + rule: 'height: 110px;' }] } }, diff --git a/packages/dash-table/webpack.test.config.js b/packages/dash-table/webpack.test.config.js index 1928de87bb..a16598f417 100644 --- a/packages/dash-table/webpack.test.config.js +++ b/packages/dash-table/webpack.test.config.js @@ -1,6 +1,6 @@ const config = require('./.config/webpack/base.js')( { - definitions: ['TEST_COPY_PASTE'] + definitions: ['TEST', 'TEST_COPY_PASTE'] }, 'development' );