Skip to content
This repository was archived by the owner on Jun 4, 2024. It is now read-only.

Issue 315: Clearable columns #497

Merged
merged 14 commits into from
Jul 17, 2019
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

#[Unreleased]
## [Unreleased]
### Added
[#497](https://github.com/plotly/dash-table/pull/497)
- New `column.clearable` flag that displays a `Ø` action in the column
Accepts a boolean or array of booleans for multi-line headers.
Clicking a merged column's `Ø` will clear all related columns.

- Clearing column(s) will remove the appropriate data props from each datum
row of `data`.
- Additionally clearing the column will reset the filter for the affected column(s)

### Changed
[#497](https://github.com/plotly/dash-table/pull/497)
- Like for clearing above, deleting through the `x` action will also
reset the filter for the affected column(s)

### Fixed
[#491](https://github.com/plotly/dash-table/issues/491)
- Fixed unconsistent behaviors when editing cell headers
Expand Down
1 change: 0 additions & 1 deletion dash-main
Submodule dash-main deleted from 9819e5
24 changes: 24 additions & 0 deletions demo/AppMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
import { TooltipSyntax } from 'dash-table/tooltips/props';

export enum AppMode {
Clearable = 'clearable',
ClearableMerged = 'clearableMerged',
Date = 'date',
Default = 'default',
Filtering = 'filtering',
Expand Down Expand Up @@ -185,6 +187,24 @@ function getTypedState() {
return state;
}

function getClearableState() {
const state = getDefaultState();
state.tableProps.filter_action = TableAction.Native;

R.forEach(c => {
c.clearable = true;
}, state.tableProps.columns || []);

return state;
}

function getClearableMergedState() {
const state = getClearableState();
state.tableProps.merge_duplicate_headers = true;

return state;
}

function getDateState() {
const state = getTypedState();

Expand Down Expand Up @@ -325,6 +345,10 @@ function getState() {
const mode = Environment.searchParams.get('mode');

switch (mode) {
case AppMode.Clearable:
return getClearableState();
case AppMode.ClearableMerged:
return getClearableMergedState();
case AppMode.Date:
return getDateState();
case AppMode.Filtering:
Expand Down
43 changes: 5 additions & 38 deletions src/dash-table/components/FilterFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,14 @@ import memoizerCache from 'core/cache/memoizer';
import { memoizeOne } from 'core/memoizer';

import ColumnFilter from 'dash-table/components/Filter/Column';
import { ColumnId, IVisibleColumn, VisibleColumns, RowSelection, TableAction } from 'dash-table/components/Table/props';
import { ColumnId, IVisibleColumn, TableAction, IFilterFactoryProps, SetFilter } from 'dash-table/components/Table/props';
import derivedFilterStyles, { derivedFilterOpStyles } from 'dash-table/derived/filter/wrapperStyles';
import derivedHeaderOperations from 'dash-table/derived/header/operations';
import { derivedRelevantFilterStyles } from 'dash-table/derived/style';
import { BasicFilters, Cells, Style } from 'dash-table/derived/style/props';
import { SingleColumnSyntaxTree, getMultiColumnQueryString } from 'dash-table/syntax-tree';
import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree';

import { IEdgesMatrices } from 'dash-table/derived/edges/type';
import { updateMap } from 'dash-table/derived/filter/map';

type SetFilter = (
filter_query: string,
rawFilter: string,
map: Map<string, SingleColumnSyntaxTree>
) => void;

export interface IFilterOptions {
columns: VisibleColumns;
filter_query: string;
filter_action: TableAction;
id: string;
map: Map<string, SingleColumnSyntaxTree>;
rawFilterQuery: string;
row_deletable: boolean;
row_selectable: RowSelection;
setFilter: SetFilter;
style_cell: Style;
style_cell_conditional: Cells;
style_filter: Style;
style_filter_conditional: BasicFilters;
}
import { updateColumnFilter } from 'dash-table/derived/filter/map';

const NO_FILTERS: JSX.Element[][] = [];

Expand All @@ -51,7 +28,7 @@ export default class FilterFactory {
return this.propsFn();
}

constructor(private readonly propsFn: () => IFilterOptions) {
constructor(private readonly propsFn: () => IFilterFactoryProps) {

}

Expand All @@ -60,17 +37,7 @@ export default class FilterFactory {

const value = ev.target.value.trim();

map = updateMap(map, column, value);

const asts = Array.from(map.values());
const globalFilter = getMultiColumnQueryString(asts);

const rawGlobalFilter = R.map(
ast => ast.query || '',
R.filter<SingleColumnSyntaxTree>(ast => Boolean(ast), asts)
).join(' && ');

setFilter(globalFilter, rawGlobalFilter, map);
updateColumnFilter(map, column, value, setFilter);
}

private filter = memoizerCache<[ColumnId, number]>()((
Expand Down
9 changes: 7 additions & 2 deletions src/dash-table/components/HeaderFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { VisibleColumns, HeaderFactoryProps } from 'dash-table/components/Table/props';
import derivedHeaderContent from 'dash-table/derived/header/content';
import getHeaderRows from 'dash-table/derived/header/headerRows';
import getIndices from 'dash-table/derived/header/indices';
Expand All @@ -29,7 +29,7 @@ export default class HeaderFactory {
return this.propsFn();
}

constructor(private readonly propsFn: () => ControlledTableProps) {
constructor(private readonly propsFn: () => HeaderFactoryProps) {

}

Expand All @@ -39,10 +39,12 @@ export default class HeaderFactory {
const {
columns,
data,
map,
merge_duplicate_headers,
page_action,
row_deletable,
row_selectable,
setFilter,
setProps,
sort_action,
sort_by,
Expand Down Expand Up @@ -90,12 +92,15 @@ export default class HeaderFactory {

const contents = this.headerContent(
columns,
merge_duplicate_headers,
data,
labelsAndIndices,
map,
sort_action,
sort_mode,
sort_by,
page_action,
setFilter,
setProps,
merge_duplicate_headers
);
Expand Down
52 changes: 30 additions & 22 deletions src/dash-table/components/Table/Table.less
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,9 @@
}

th {
.column-header--edit,
.column-header--clear,
.column-header--delete,
.column-header--edit,
.sort {
.not-selectable();
cursor: pointer;
Expand Down Expand Up @@ -483,29 +484,36 @@
color: var(--accent);
}

.dash-spreadsheet-inner .column-header--edit {
float: left;
opacity: 0.1;
padding-left: 2px;
padding-right: 2px;
cursor: pointer;
}
.dash-spreadsheet-inner {
.column-header--clear::before {
content: 'ø';
}

.dash-spreadsheet-inner th:hover .column-header--edit {
color: var(--accent);
opacity: 1;
}
.column-header--delete::before {
content: '×'
}

.dash-spreadsheet-inner .column-header--delete {
float: left;
opacity: 0.1;
padding-left: 2px;
padding-right: 2px;
cursor: pointer;
}
.column-header--edit::before {
content: '✎';
}

.dash-spreadsheet-inner th:hover .column-header--delete {
color: var(--accent);
opacity: 1;
.column-header--clear,
.column-header--delete,
.column-header--edit {
float: left;
opacity: 0.1;
padding-left: 2px;
padding-right: 2px;
cursor: pointer;
}

th:hover {
.column-header--clear,
.column-header--delete,
.column-header--edit {
color: var(--accent);
opacity: 1;
}
}
}
}
28 changes: 28 additions & 0 deletions src/dash-table/components/Table/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export interface IDatetimeColumn extends ITypeColumn {
}

export interface IBaseVisibleColumn {
clearable?: boolean | boolean[];
deletable?: boolean | boolean[];
editable: boolean;
renamable?: boolean | boolean[];
Expand Down Expand Up @@ -393,6 +394,33 @@ export type ControlledTableProps = SanitizedProps & IState & {
virtualized: IVirtualizedDerivedData;
};

export type SetFilter = (
filter_query: string,
rawFilter: string,
map: Map<string, SingleColumnSyntaxTree>
) => void;

export interface IFilterFactoryProps {
columns: VisibleColumns;
filter_query: string;
filter_action: TableAction;
id: string;
map: Map<string, SingleColumnSyntaxTree>;
rawFilterQuery: string;
row_deletable: boolean;
row_selectable: RowSelection;
setFilter: SetFilter;
style_cell: Style;
style_cell_conditional: Cells;
style_filter: Style;
style_filter_conditional: BasicFilters;
}

export type HeaderFactoryProps = ControlledTableProps & {
map: Map<string, SingleColumnSyntaxTree>;
setFilter: SetFilter;
};

export interface ICellFactoryProps {
active_cell: ICellCoordinates;
columns: VisibleColumns;
Expand Down
19 changes: 19 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,25 @@ export const propTypes = {
*/
columns: PropTypes.arrayOf(PropTypes.exact({

/**
* If True, the user can clear the column by clicking on a little `Ø`
* button on the column.
* If there are merged, multi-header columns then you can choose
* which column header row to display the "Ø" in by
* supplying an array of booleans.
* For example, `[true, false]` will display the "Ø" on the first row,
* but not the second row.
* If the "Ø" appears on a merged column, then clicking on that button
* will clear *all* of the merged columns associated with it.
*
* Unlike `column.deletable`, this action does not remove the column(s)
* from the table. It only removed the associated entries from `data`.
*/
clearable: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.arrayOf(PropTypes.bool)
]),

/**
* If True, the user can delete the column by clicking on a little `x`
* button on the column.
Expand Down
51 changes: 40 additions & 11 deletions src/dash-table/derived/filter/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as R from 'ramda';

import { memoizeOneFactory } from 'core/memoizer';

import { VisibleColumns, IVisibleColumn } from 'dash-table/components/Table/props';
import { SingleColumnSyntaxTree, MultiColumnsSyntaxTree, getSingleColumnMap } from 'dash-table/syntax-tree';
import { VisibleColumns, IVisibleColumn, SetFilter } from 'dash-table/components/Table/props';
import { SingleColumnSyntaxTree, MultiColumnsSyntaxTree, getMultiColumnQueryString, getSingleColumnMap } from 'dash-table/syntax-tree';

const cloneIf = (
current: Map<string, SingleColumnSyntaxTree>,
Expand Down Expand Up @@ -61,20 +61,49 @@ export default memoizeOneFactory((
return newMap;
});

export const updateMap = (
map: Map<string, SingleColumnSyntaxTree>,
column: IVisibleColumn,
value: any
): Map<string, SingleColumnSyntaxTree> => {
const safeColumnId = column.id.toString();

function updateMap(map: Map<string, SingleColumnSyntaxTree>, column: IVisibleColumn, value: any) {
const id = column.id.toString();
const newMap = new Map<string, SingleColumnSyntaxTree>(map);

if (value && value.length) {
newMap.set(safeColumnId, new SingleColumnSyntaxTree(value, column));
newMap.set(id, new SingleColumnSyntaxTree(value, column));
} else {
newMap.delete(safeColumnId);
newMap.delete(id);
}

return newMap;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isolating the map update logic - can be called for a single column update (user edits filter) or through header clear and delete column actions (which may impact multiple columns at the same time)


function updateState(map: Map<string, SingleColumnSyntaxTree>, setFilter: SetFilter) {
const asts = Array.from(map.values());
const globalFilter = getMultiColumnQueryString(asts);

const rawGlobalFilter = R.map(
ast => ast.query || '',
R.filter<SingleColumnSyntaxTree>(ast => Boolean(ast), asts)
).join(' && ');

setFilter(globalFilter, rawGlobalFilter, map);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same idea here, isolating the state update logic for both scenarios.


export const updateColumnFilter = (
map: Map<string, SingleColumnSyntaxTree>,
column: IVisibleColumn,
value: any,
setFilter: SetFilter
) => {
map = updateMap(map, column, value);
updateState(map, setFilter);
};

export const clearColumnsFilter = (
map: Map<string, SingleColumnSyntaxTree>,
columns: VisibleColumns,
setFilter: SetFilter
) => {
R.forEach(column => {
map = updateMap(map, column, '');
}, columns);

updateState(map, setFilter);
};
Loading