From 306528072538ff8b00d0546102847cfc35f3ef51 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 21 Apr 2019 08:47:16 -0400 Subject: [PATCH 01/21] lint Format.py --- dash_table/Format.py | 69 +++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index 011d50728..4ae06ace3 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -1,6 +1,4 @@ import collections -import inspect -import sys def get_named_tuple(name, dict): @@ -26,23 +24,23 @@ def get_named_tuple(name, dict): }) Prefix = get_named_tuple('prefix', { - 'yocto': 10**-24, - 'zepto': 10**-21, - 'atto': 10**-18, - 'femto': 10**-15, - 'pico': 10**-12, - 'nano': 10**-9, - 'micro': 10**-6, - 'milli': 10**-3, + 'yocto': 10 ** -24, + 'zepto': 10 ** -21, + 'atto': 10 ** -18, + 'femto': 10 ** -15, + 'pico': 10 ** -12, + 'nano': 10 ** -9, + 'micro': 10 ** -6, + 'milli': 10 ** -3, 'none': None, - 'kilo': 10**3, - 'mega': 10**6, - 'giga': 10**9, - 'tera': 10**12, - 'peta': 10**15, - 'exa': 10**18, - 'zetta': 10**21, - 'yotta': 10**24 + 'kilo': 10 ** 3, + 'mega': 10 ** 6, + 'giga': 10 ** 9, + 'tera': 10 ** 12, + 'peta': 10 ** 15, + 'exa': 10 ** 18, + 'zetta': 10 ** 21, + 'yotta': 10 ** 24 }) Scheme = get_named_tuple('scheme', { @@ -102,11 +100,17 @@ def __init__(self, **kwargs): 'type': Scheme.default } - valid_methods = [m for m in dir(self.__class__) if m[0] != '_' and m != 'to_plotly_json'] + valid_methods = [ + m for m in dir(self.__class__) + if m[0] != '_' and m != 'to_plotly_json' + ] for kw, val in kwargs.items(): if kw not in valid_methods: - raise TypeError('{0} is not a format method. Expected one of'.format(kw), str(list(valid_methods))) + raise TypeError( + '{0} is not a format method. Expected one of'.format(kw), + str(list(valid_methods)) + ) getattr(self, kw)(val) @@ -128,7 +132,8 @@ def _validate_non_negative_integer_or_none(self, value): def _validate_named(self, value, named_values): if value not in named_values: - raise TypeError('expected value to be one of', str(list(named_values))) + raise TypeError('expected value to be one of', + str(list(named_values))) def _validate_string(self, value): if not isinstance(value, (str, u''.__class__)): @@ -174,7 +179,9 @@ def padding_width(self, value): def precision(self, value): self._validate_non_negative_integer_or_none(value) - self._specifier['precision'] = '.{0}'.format(value) if value is not None else '' + self._specifier['precision'] = ( + '.{0}'.format(value) if value is not None else '' + ) return self def scheme(self, value): @@ -238,13 +245,20 @@ def group_delimiter(self, value): return self def groups(self, groups): - groups = groups if isinstance(groups, list) else [groups] if isinstance(groups, int) else None + groups = ( + groups if isinstance(groups, list) else + [groups] if isinstance(groups, int) else None + ) if not isinstance(groups, list): - raise TypeError('expected groups to be an integer or a list of integers') - + raise TypeError( + 'expected groups to be an integer or a list of integers' + ) if len(groups) == 0: - raise ValueError('expected groups to be an integer or a list of one or more integers') + raise ValueError( + 'expected groups to be an integer or a list of ' + 'one or more integers' + ) for group in groups: if not isinstance(group, int): @@ -273,8 +287,9 @@ def to_plotly_json(self): f['locale'] = self._locale.copy() f['nully'] = self._nully f['prefix'] = self._prefix + aligned = self._specifier['align'] != Align.default f['specifier'] = '{}{}{}{}{}{}{}{}{}{}'.format( - self._specifier['fill'] if self._specifier['align'] != Align.default else '', + self._specifier['fill'] if aligned else '', self._specifier['align'], self._specifier['sign'], self._specifier['symbol'], From 773d0ddacd4e72341516cb853caabe5a1287bccc Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 12 Apr 2019 07:00:00 -0400 Subject: [PATCH 02/21] remove obsolete comment about #177 and clarify --- src/dash-table/dash/DataTable.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index b0ab6b87b..e4f84678f 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -485,15 +485,12 @@ export const propTypes = { row_selectable: PropTypes.oneOf(['single', 'multi', false]), /** - * `selected_cells` represents the set of cells that are selected. + * `selected_cells` represents the set of cells that are selected, + * as a nested array of `[row, column]` indices. * This is similar to `active_cell` except that it contains multiple * cells. Multiple cells can be selected by holding down shift and * clicking on a different cell or holding down shift and navigating * with the arrow keys. - * - * NOTE - This property may change in the future, subscribe to - * [https://github.com/plotly/dash-table/issues/177](https://github.com/plotly/dash-table/issues/177) - * for more details. */ selected_cells: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)), From 572cdf13ae78eac83fdc79ef5db5bc67468d8cc7 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 21 Apr 2019 19:02:00 -0400 Subject: [PATCH 03/21] row IDs --- src/dash-table/components/CellFactory.tsx | 3 +- .../components/ControlledTable/index.tsx | 179 +++++++++--------- src/dash-table/components/Table/index.tsx | 19 +- src/dash-table/components/Table/props.ts | 38 ++-- src/dash-table/dash/DataTable.js | 85 +++++++-- src/dash-table/derived/cell/cellProps.ts | 39 ++++ src/dash-table/derived/cell/contents.tsx | 6 +- src/dash-table/derived/cell/isActive.ts | 10 +- src/dash-table/derived/cell/isSelected.ts | 2 +- src/dash-table/derived/cell/operations.tsx | 57 +++--- src/dash-table/derived/cell/wrappers.tsx | 4 +- src/dash-table/derived/paginator.ts | 12 +- src/dash-table/handlers/cellEvents.ts | 78 ++++---- src/dash-table/utils/TableClipboardHelper.ts | 10 +- src/dash-table/utils/actions.js | 12 +- src/dash-table/utils/applyClipboardToData.ts | 16 +- src/dash-table/utils/navigation.js | 20 +- tests/cypress/tests/unit/clipboard_test.ts | 18 +- 18 files changed, 384 insertions(+), 224 deletions(-) create mode 100644 src/dash-table/derived/cell/cellProps.ts diff --git a/src/dash-table/components/CellFactory.tsx b/src/dash-table/components/CellFactory.tsx index 217606cf0..1f47e04e0 100644 --- a/src/dash-table/components/CellFactory.tsx +++ b/src/dash-table/components/CellFactory.tsx @@ -50,7 +50,6 @@ export default class CellFactory { } = this.props; const operations = this.cellOperations( - active_cell, data, virtualized.data, virtualized.indices, @@ -114,4 +113,4 @@ export default class CellFactory { (o, c) => Array.prototype.concat(o, c) ); } -} \ No newline at end of file +} diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index a9d8dc57e..f05f2a170 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -8,7 +8,8 @@ import { isCtrlDown, isNavKey } from 'dash-table/utils/unicode'; -import { selectionCycle } from 'dash-table/utils/navigation'; +import { selectionBounds, selectionCycle } from 'dash-table/utils/navigation'; +import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps'; import getScrollbarWidth from 'core/browser/scrollbarWidth'; import Logger from 'core/Logger'; @@ -17,7 +18,7 @@ import { memoizeOne } from 'core/memoizer'; import lexer from 'core/syntax-tree/lexer'; import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper'; -import { ControlledTableProps } from 'dash-table/components/Table/props'; +import { ControlledTableProps, ICellFactoryProps } from 'dash-table/components/Table/props'; import dropdownHelper from 'dash-table/components/dropdownHelper'; import derivedTable from 'dash-table/derived/table'; @@ -31,8 +32,6 @@ import TableTooltip from './fragments/TableTooltip'; import queryLexicon from 'dash-table/syntax-tree/lexicon/query'; -const sortNumerical = R.sort((a, b) => a - b); - const DEFAULT_STYLE = { width: '100%' }; @@ -108,7 +107,7 @@ export default class ControlledTable extends PureComponent } = this.props; if (selected_cells.length && - active_cell.length && + R.has('row', active_cell) && !R.contains(active_cell, selected_cells) ) { setProps({ active_cell: selected_cells[0] }); @@ -320,6 +319,8 @@ export default class ControlledTable extends PureComponent active_cell, columns, selected_cells, + start_cell, + end_cell, setProps, viewport } = this.props; @@ -363,69 +364,88 @@ export default class ControlledTable extends PureComponent setProps({ is_focused: false, selected_cells: [nextCell], - active_cell: nextCell + active_cell: nextCell, + start_cell: nextCell, + end_cell: nextCell }); return; } // else we are navigating with arrow keys and extending selection // with shift. - let targetCells: any[] = []; - let removeCells: any[] = []; - const selectedRows = sortNumerical(R.uniq(R.pluck(0, selected_cells))); - const selectedCols = sortNumerical(R.uniq(R.pluck(1, selected_cells))); - - const minRow = selectedRows[0]; - const minCol = selectedCols[0]; - const maxRow = selectedRows[selectedRows.length - 1]; - const maxCol = selectedCols[selectedCols.length - 1]; + let {minRow, minCol, maxRow, maxCol} = selectionBounds(selected_cells); const selectingDown = e.keyCode === KEY_CODES.ARROW_DOWN || e.keyCode === KEY_CODES.ENTER; const selectingUp = e.keyCode === KEY_CODES.ARROW_UP; const selectingRight = e.keyCode === KEY_CODES.ARROW_RIGHT || e.keyCode === KEY_CODES.TAB; const selectingLeft = e.keyCode === KEY_CODES.ARROW_LEFT; - - // If there are selections above the active cell and we are - // selecting down then pull down the top selection towards - // the active cell. - if (selectingDown && (active_cell as any)[0] > minRow) { - removeCells = selectedCols.map(col => [minRow, col]); - } else if (selectingDown && maxRow !== viewport.data.length - 1) { - // Otherwise if we are selecting down select the next row if possible. - targetCells = selectedCols.map(col => [maxRow + 1, col]); - } else if (selectingUp && (active_cell as any)[0] < maxRow) { - // If there are selections below the active cell and we are selecting - // up remove lower row. - removeCells = selectedCols.map(col => [maxRow, col]); - } else if (selectingUp && minRow > 0) { - // Otherwise if we are selecting up select next row if possible. - targetCells = selectedCols.map(col => [minRow - 1, col]); - } else if (selectingLeft && (active_cell as any)[1] < maxCol) { - // If there are selections to the right of the active cell and - // we are selecting left, move the right side closer to active_cell - removeCells = selectedRows.map(row => [row, maxCol]); - } else if (selectingLeft && minCol > 0) { - // Otherwise increase the selection left if possible - targetCells = selectedRows.map(row => [row, minCol - 1]); - } else if (selectingRight && (active_cell as any)[1] > minCol) { - // If there are selections to the left of the active cell and - // we are selecting right, move the left side closer to active_cell - removeCells = selectedRows.map(row => [row, minCol]); - } else if (selectingRight && maxCol + 1 <= columns.length - 1) { - // Otherwise move selection right if possible - targetCells = selectedRows.map(row => [row, maxCol + 1]); + let startRow = start_cell && start_cell.row; + let startCol = start_cell && start_cell.column; + let endRow = end_cell && end_cell.row; + let endCol = end_cell && end_cell.column; + + if (selectingDown) { + if (active_cell.row > minRow) { + minRow++; + endRow = minRow; + } else if (maxRow < viewport.data.length - 1) { + maxRow++; + endRow = maxRow; + } + } else if (selectingUp) { + if (active_cell.row < maxRow) { + maxRow--; + endRow = maxRow; + } else if (minRow > 0) { + minRow--; + endRow = minRow; + } + } else if (selectingRight) { + if (active_cell.column > minCol) { + minCol++; + endCol = minCol; + } else if (maxCol < columns.length - 1) { + maxCol++; + endCol = maxCol; + } + } else if (selectingLeft) { + if (active_cell.column < maxCol) { + maxCol--; + endCol = maxCol; + } else if (minCol > 0) { + minCol--; + endCol = minCol; + } + } else { + return; } - const newSelectedCell = R.without( - removeCells, - R.uniq(R.concat(targetCells, selected_cells)) + const finalSelected = makeSelection( + {minRow, maxRow, minCol, maxCol}, + columns, viewport ); - setProps({ + + const newProps: Partial = { is_focused: false, - selected_cells: newSelectedCell - }); + end_cell: makeCell(endRow, endCol, columns, viewport), + selected_cells: finalSelected + }; + + const newStartRow = endRow === minRow ? maxRow : minRow; + const newStartCol = endCol === minCol ? maxCol : minCol; + + if (startRow !== newStartRow || startCol !== newStartCol) { + newProps.start_cell = makeCell( + newStartRow, + newStartCol, + columns, + viewport + ); + } + + setProps(newProps); } deleteCell = (event: any) => { @@ -443,7 +463,7 @@ export default class ControlledTable extends PureComponent let newData = data; const realCells: [number, number][] = R.map( - cell => [viewport.indices[cell[0]], cell[1]] as [number, number], + cell => [viewport.indices[cell.row], cell.column] as [number, number], selected_cells ); @@ -467,55 +487,42 @@ export default class ControlledTable extends PureComponent const e = event; + const {row, column} = currentCell; + let nextCoords; + switch (e.keyCode) { case KEY_CODES.ARROW_LEFT: - return restrictToSelection - ? selectionCycle( - [currentCell[0], currentCell[1] - 1], - selected_cells - ) - : [ - currentCell[0], - R.max(0, currentCell[1] - 1) - ]; + nextCoords = restrictToSelection + ? selectionCycle([row, column - 1], selected_cells) + : [row, R.max(0, column - 1)]; + break; case KEY_CODES.ARROW_RIGHT: case KEY_CODES.TAB: - return restrictToSelection - ? selectionCycle( - [currentCell[0], currentCell[1] + 1], - selected_cells - ) - : [ - currentCell[0], - R.min(columns.length - 1, currentCell[1] + 1) - ]; + nextCoords = restrictToSelection + ? selectionCycle([row, column + 1], selected_cells) + : [row, R.min(columns.length - 1, column + 1)]; + break; case KEY_CODES.ARROW_UP: - return restrictToSelection - ? selectionCycle( - [currentCell[0] - 1, currentCell[1]], - selected_cells - ) - : [R.max(0, currentCell[0] - 1), currentCell[1]]; + nextCoords = restrictToSelection + ? selectionCycle([row - 1, column], selected_cells) + : [R.max(0, row - 1), column]; + break; case KEY_CODES.ARROW_DOWN: case KEY_CODES.ENTER: - return restrictToSelection - ? selectionCycle( - [currentCell[0] + 1, currentCell[1]], - selected_cells - ) - : [ - R.min(viewport.data.length - 1, currentCell[0] + 1), - currentCell[1] - ]; + nextCoords = restrictToSelection + ? selectionCycle([row + 1, column], selected_cells) + : [R.min(viewport.data.length - 1, row + 1), column]; + break; default: throw new Error( `Table.getNextCell: unknown navigation keycode ${e.keyCode}` ); } + return makeCell(nextCoords[0], nextCoords[1], columns, viewport); } onCopy = (e: any) => { @@ -541,7 +548,7 @@ export default class ControlledTable extends PureComponent viewport } = this.props; - if (!editable) { + if (!editable || !active_cell) { return; } diff --git a/src/dash-table/components/Table/index.tsx b/src/dash-table/components/Table/index.tsx index 1ce9b95a2..491ecf017 100644 --- a/src/dash-table/components/Table/index.tsx +++ b/src/dash-table/components/Table/index.tsx @@ -185,25 +185,38 @@ export default class Table extends Component virtual.data[i].id, + virtual_selected_rows + ); } if (!viewportSelectedRowsCached) { newProps.derived_viewport_selected_rows = viewport_selected_rows; + newProps.derived_viewport_selected_row_ids = R.map( + i => viewport.data[i].id, + viewport_selected_rows + ); } if (invalidateSelection) { newProps.active_cell = undefined; - newProps.selected_cells = undefined; - newProps.selected_rows = undefined; + newProps.selected_cells = []; + newProps.start_cell = undefined; + newProps.end_cell = undefined; + newProps.selected_rows = []; + newProps.selected_row_ids = []; } if (!R.keys(newProps).length) { @@ -272,4 +285,4 @@ export default class Table extends Component new QuerySyntaxTree(query).toStructure() ); -} \ No newline at end of file +} diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 1a8f1e64d..084354001 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -52,18 +52,24 @@ export enum ContentStyle { Grow = 'grow' } -export type ActiveCell = CellCoordinates | []; -export type CellCoordinates = [number, number]; +export interface ICellCoordinates { + row: number; + column: number; + row_id?: RowId; + column_id: ColumnId; +} + export type ColumnId = string | number; export type Columns = IColumn[]; export type Data = Datum[]; export type Datum = IDatumObject | any; export type Filtering = 'fe' | 'be' | boolean; export type Indices = number[]; +export type RowId = string | number; export type Navigation = 'page'; export type PaginationMode = 'fe' | 'be' | boolean; export type RowSelection = 'single' | 'multi' | false; -export type SelectedCells = CellCoordinates[]; +export type SelectedCells = ICellCoordinates[]; export type SetProps = (...args: any[]) => void; export type SetState = (state: Partial) => void; export type Sorting = 'fe' | 'be' | boolean; @@ -242,12 +248,12 @@ export interface IState { export type StandaloneState = IState & Partial; -interface IProps { +export interface IProps { data_previous?: any[]; data_timestamp?: number; - end_cell?: [number, number]; + end_cell?: ICellCoordinates; is_focused?: boolean; - start_cell?: [number, number]; + start_cell?: ICellCoordinates; id: string; @@ -257,7 +263,7 @@ interface IProps { column_static_tooltip: ITableStaticTooltips; column_conditional_tooltips: ConditionalTooltip[]; - active_cell?: ActiveCell; + active_cell?: ICellCoordinates; columns?: Columns; column_conditional_dropdowns?: IConditionalColumnDropdown[]; column_static_dropdown?: IColumnDropdown[]; @@ -279,6 +285,7 @@ interface IProps { row_selectable?: RowSelection; selected_cells?: SelectedCells; selected_rows?: Indices; + selected_row_ids?: RowId[]; setProps?: SetProps; sorting?: Sorting; sorting_settings?: SortSettings; @@ -302,7 +309,7 @@ interface IProps { } interface IDefaultProps { - active_cell: ActiveCell; + active_cell: ICellCoordinates; columns: Columns; column_conditional_dropdowns: IConditionalColumnDropdown[]; column_static_dropdown: IColumnDropdown[]; @@ -320,7 +327,10 @@ interface IDefaultProps { row_deletable: boolean; row_selectable: RowSelection; selected_cells: SelectedCells; - selected_rows: number[]; + start_cell: ICellCoordinates; + end_cell: ICellCoordinates; + selected_rows: Indices; + selected_row_ids: RowId[]; sorting: Sorting; sorting_settings: SortSettings; sorting_type: SortingType; @@ -348,10 +358,14 @@ interface IDerivedProps { derived_filter_structure: object | null; derived_viewport_data: Data; derived_viewport_indices: Indices; + derived_viewport_row_ids: RowId[]; derived_viewport_selected_rows: Indices; + derived_viewport_selected_row_ids: RowId[]; derived_virtual_data: Data; derived_virtual_indices: Indices; + derived_virtual_row_ids: RowId[]; derived_virtual_selected_rows: Indices; + derived_virtual_selected_row_ids: RowId[]; } export type PropsWithDefaults = IProps & IDefaultProps; @@ -372,7 +386,7 @@ export type ControlledTableProps = PropsWithDefaults & IState & { }; export interface ICellFactoryProps { - active_cell: ActiveCell; + active_cell: ICellCoordinates; columns: VisibleColumns; column_conditional_dropdowns: IConditionalColumnDropdown[]; column_conditional_tooltips: ConditionalTooltip[]; @@ -389,7 +403,9 @@ export interface ICellFactoryProps { row_deletable: boolean; row_selectable: RowSelection; selected_cells: SelectedCells; - selected_rows: number[]; + start_cell: ICellCoordinates; + end_cell: ICellCoordinates; + selected_rows: Indices; setProps: SetProps; setState: SetState; style_cell: Style; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index e4f84678f..82538758e 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -51,10 +51,14 @@ export const defaultProps = { derived_viewport_data: [], derived_viewport_indices: [], + derived_viewport_row_ids: [], derived_viewport_selected_rows: [], + derived_viewport_selected_row_ids: [], derived_virtual_data: [], derived_virtual_indices: [], + derived_virtual_row_ids: [], derived_virtual_selected_rows: [], + derived_virtual_selected_row_ids: [], column_conditional_dropdowns: [], column_static_dropdown: [], @@ -67,9 +71,9 @@ export const defaultProps = { data: [], columns: [], editable: false, - active_cell: [], - selected_cells: [[]], + selected_cells: [], selected_rows: [], + selected_row_ids: [], row_selectable: false, style_table: {}, @@ -79,12 +83,18 @@ export const defaultProps = { virtualization: false }; +const cellCoordsIDsProp = PropTypes.exact({ + row: PropTypes.number, + column: PropTypes.number, + row_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) +}); + export const propTypes = { /** - * The [row, column] index of which cell is currently - * active. + * The row and column indices and IDs of the currently active cell. */ - active_cell: PropTypes.array, + active_cell: cellCoordsIDsProp, /** * Columns describes various aspects about each individual column. @@ -367,6 +377,10 @@ export const propTypes = { /** * The contents of the table. * The keys of each item in data should match the column IDs. + * Each item can also have an 'id' key, whose value is its row ID. If there + * is a column with ID='id' this will display the row ID, otherwise it is + * just used to reference the row for selections, filtering, etc. + * * Example: * * [ @@ -411,11 +425,11 @@ export const propTypes = { /** * When selecting multiple cells * (via clicking on a cell and then shift-clicking on another cell), - * `end_cell` represents the [row, column] coordinates of the cell + * `end_cell` represents the row / column coordinates and IDs of the cell * in one of the corners of the region. * `start_cell` represents the coordinates of the other corner. */ - end_cell: PropTypes.arrayOf(PropTypes.number), + end_cell: cellCoordsIDsProp, /** * The ID of the table. @@ -486,21 +500,27 @@ export const propTypes = { /** * `selected_cells` represents the set of cells that are selected, - * as a nested array of `[row, column]` indices. - * This is similar to `active_cell` except that it contains multiple - * cells. Multiple cells can be selected by holding down shift and + * as an array of objects, each item similar to `active_cell`. + * Multiple cells can be selected by holding down shift and * clicking on a different cell or holding down shift and navigating * with the arrow keys. */ - selected_cells: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)), + selected_cells: PropTypes.arrayOf(cellCoordsIDsProp), /** - * `selected_rows` contains the indices of the rows that + * `selected_rows` contains the indices of rows that * are selected via the UI elements that appear when * `row_selectable` is `'single'` or `'multi'`. */ selected_rows: PropTypes.arrayOf(PropTypes.number), + /** + * `selected_row_ids` contains the ids of rows that + * are selected via the UI elements that appear when + * `row_selectable` is `'single'` or `'multi'`. + */ + selected_row_ids: PropTypes.arrayOf(PropTypes.string), + setProps: PropTypes.func, /** @@ -510,7 +530,7 @@ export const propTypes = { * in one of the corners of the region. * `end_cell` represents the coordinates of the other corner. */ - start_cell: PropTypes.arrayOf(PropTypes.number), + start_cell: cellCoordsIDsProp, /** * If True, then the table will be styled like a list view @@ -1022,16 +1042,30 @@ export const propTypes = { * `derived_viewport_indices` indicates the order in which the original * rows appear after being filtered, sorted, and/or paged. * `derived_viewport_indices` contains indices for the current page, - * while `derived_virtual_indices` contains indices for across all pages. + * while `derived_virtual_indices` contains indices across all pages. */ derived_viewport_indices: PropTypes.arrayOf(PropTypes.number), + /** + * `derived_viewport_row_ids` lists row IDs in the order they appear + * after being filtered, sorted, and/or paged. + * `derived_viewport_row_ids` contains IDs for the current page, + * while `derived_virtual_row_ids` contains IDs across all pages. + */ + derived_viewport_row_ids: PropTypes.arrayOf(PropTypes.string), + /** * `derived_viewport_selected_rows` represents the indices of the - * `selected_rows` from the perspective of the `derived_viewport_indices`. + * `selected_rows` from the perspective of the `derived_viewport_indices`. */ derived_viewport_selected_rows: PropTypes.arrayOf(PropTypes.number), + /** + * `derived_viewport_selected_row_ids` represents the IDs of the + * `selected_rows` on the currently visible page. + */ + derived_viewport_selected_row_ids: PropTypes.arrayOf(PropTypes.string), + /** * This property represents the visible state of `data` * across all pages after the front-end sorting and filtering @@ -1040,19 +1074,34 @@ export const propTypes = { derived_virtual_data: PropTypes.arrayOf(PropTypes.object), /** - * `derived_viewport_indices` indicates the order in which the original - * rows appear after being filtered, sorted, and/or paged. + * `derived_virtual_indices` indicates the order in which the original + * rows appear after being filtered and sorted. * `derived_viewport_indices` contains indices for the current page, - * while `derived_virtual_indices` contains indices for across all pages. + * while `derived_virtual_indices` contains indices across all pages. */ derived_virtual_indices: PropTypes.arrayOf(PropTypes.number), + /** + * `derived_virtual_row_ids` indicates the row IDs in the order in which + * they appear after being filtered and sorted. + * `derived_viewport_row_ids` contains IDs for the current page, + * while `derived_virtual_row_ids` contains IDs across all pages. + */ + derived_virtual_row_ids: PropTypes.arrayOf(PropTypes.number), + /** * `derived_virtual_selected_rows` represents the indices of the * `selected_rows` from the perspective of the `derived_virtual_indices`. */ derived_virtual_selected_rows: PropTypes.arrayOf(PropTypes.number), + /** + * `derived_virtual_selected_row_ids` represents the IDs of the + * `selected_rows` as they appear after filtering and sorting, + * across all pages. + */ + derived_virtual_selected_row_ids: PropTypes.arrayOf(PropTypes.string), + /** * DEPRECATED * Subscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) diff --git a/src/dash-table/derived/cell/cellProps.ts b/src/dash-table/derived/cell/cellProps.ts new file mode 100644 index 000000000..30da66c6c --- /dev/null +++ b/src/dash-table/derived/cell/cellProps.ts @@ -0,0 +1,39 @@ +import { map, range, xprod } from 'ramda'; +import { ICellCoordinates, Columns, IDerivedData } from 'dash-table/components/Table/props'; + +export function makeCell ( + row: number, + column: number, + columns: Columns, + viewport: IDerivedData +) { + const cell: ICellCoordinates = { + row, + column, + column_id: columns[column].id + }; + const rowId = viewport.data[row].id; + if (rowId !== undefined) { + cell.row_id = rowId; + } + return cell; +} + +interface ISelectionBounds { + minRow: number; + maxRow: number; + minCol: number; + maxCol: number; +} + +export function makeSelection ( + bounds: ISelectionBounds, + columns: Columns, + viewport: IDerivedData +) { + const {minRow, maxRow, minCol, maxCol} = bounds; + return map( + rc => makeCell(rc[0], rc[1], columns, viewport), + xprod(range(minRow, maxRow + 1), range(minCol, maxCol + 1)) + ); +} diff --git a/src/dash-table/derived/cell/contents.tsx b/src/dash-table/derived/cell/contents.tsx index 4a7a282ee..dfc1ebac5 100644 --- a/src/dash-table/derived/cell/contents.tsx +++ b/src/dash-table/derived/cell/contents.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { - ActiveCell, + ICellCoordinates, Data, Datum, DropdownValues, @@ -57,7 +57,7 @@ class Contents { } get = memoizeOne(( - activeCell: ActiveCell, + activeCell: ICellCoordinates | undefined, columns: VisibleColumns, data: Data, offset: IViewportOffset, @@ -122,4 +122,4 @@ class Contents { data ); }); -} \ No newline at end of file +} diff --git a/src/dash-table/derived/cell/isActive.ts b/src/dash-table/derived/cell/isActive.ts index 78b705788..4cfd31d41 100644 --- a/src/dash-table/derived/cell/isActive.ts +++ b/src/dash-table/derived/cell/isActive.ts @@ -1,7 +1,11 @@ -import { ActiveCell } from 'dash-table/components/Table/props'; +import { ICellCoordinates } from 'dash-table/components/Table/props'; export default ( - activeCell: ActiveCell, + activeCell: ICellCoordinates | undefined, row: number, column: number -) => activeCell[0] === row && activeCell[1] === column; \ No newline at end of file +) => ( + !!activeCell && + activeCell.row === row && + activeCell.column === column +); diff --git a/src/dash-table/derived/cell/isSelected.ts b/src/dash-table/derived/cell/isSelected.ts index 8e701b864..3c12569d9 100644 --- a/src/dash-table/derived/cell/isSelected.ts +++ b/src/dash-table/derived/cell/isSelected.ts @@ -6,4 +6,4 @@ export default ( selectedCells: SelectedCells, row: number, column: number -) => R.contains([row, column], selectedCells); \ No newline at end of file +) => R.any(cell => cell.row === row && cell.column === column, selectedCells); diff --git a/src/dash-table/derived/cell/operations.tsx b/src/dash-table/derived/cell/operations.tsx index c160fd8c2..24625c3dd 100644 --- a/src/dash-table/derived/cell/operations.tsx +++ b/src/dash-table/derived/cell/operations.tsx @@ -2,31 +2,38 @@ import * as R from 'ramda'; import React from 'react'; import { memoizeOneFactory } from 'core/memoizer'; +import { clearSelection } from 'dash-table/utils/actions'; import { Data, Datum, SetProps, RowSelection, - ActiveCell, Indices } from 'dash-table/components/Table/props'; -function deleteRow(idx: number, activeCell: ActiveCell, data: Data, selectedRows: number[]) { +function deleteRow(idx: number, data: Data, selectedRows: number[]) { const newProps: any = { - data: R.remove(idx, 1, data) + data: R.remove(idx, 1, data), + // We could try to adjust selection, but there are lots of edge cases + ...clearSelection }; - if (R.is(Array, activeCell) && activeCell[0] === idx) { - newProps.active_cell = []; - } - if (R.is(Array, selectedRows) && R.contains(idx, selectedRows)) { - newProps.selected_rows = R.without([idx], selectedRows); + if (R.is(Array, selectedRows) && R.any(i => i >= idx, selectedRows)) { + newProps.selected_rows = R.map( + // all rows past idx have now lost one from their index + i => i > idx ? i - 1 : i, + R.without([idx], selectedRows) + ); + newProps.selected_row_ids = R.map( + i => newProps.data[i].id, + newProps.selected_rows + ); } return newProps; } -function rowSelectCell(idx: number, rowSelectable: RowSelection, selectedRows: number[], setProps: SetProps) { +function rowSelectCell(idx: number, rowSelectable: RowSelection, selectedRows: number[], setProps: SetProps, data: Data) { return ( setProps({ - selected_rows: - rowSelectable === 'single' ? - [idx] : - R.ifElse( - R.contains(idx), - R.without([idx]), - R.append(idx) - )(selectedRows) - })} + onChange={() => { + const newSelectedRows = rowSelectable === 'single' ? + [idx] : + R.ifElse( + R.contains(idx), + R.without([idx]), + R.append(idx) + )(selectedRows); + setProps({ + selected_rows: newSelectedRows, + selected_row_ids: R.map(i => data[i].id, newSelectedRows) + }); + }} /> ); } -function rowDeleteCell(setProps: SetProps, deleteFn: () => any) { +function rowDeleteCell(doDelete: () => any) { return ( setProps(deleteFn())} + onClick={() => doDelete()} style={{ width: `30px`, maxWidth: `30px`, minWidth: `30px` }} > {'×'} @@ -62,7 +72,6 @@ function rowDeleteCell(setProps: SetProps, deleteFn: () => any) { } const getter = ( - activeCell: ActiveCell, data: Data, viewportData: Data, viewportIndices: Indices, @@ -72,8 +81,8 @@ const getter = ( setProps: SetProps ): JSX.Element[][] => R.addIndex(R.map)( (_, rowIndex) => [ - ...(rowDeletable ? [rowDeleteCell(setProps, deleteRow.bind(undefined, viewportIndices[rowIndex], activeCell, data, selectedRows))] : []), - ...(rowSelectable ? [rowSelectCell(viewportIndices[rowIndex], rowSelectable, selectedRows, setProps)] : []) + ...(rowDeletable ? [rowDeleteCell(() => setProps(deleteRow(viewportIndices[rowIndex], data, selectedRows)))] : []), + ...(rowSelectable ? [rowSelectCell(viewportIndices[rowIndex], rowSelectable, selectedRows, setProps, data)] : []) ], viewportData ); diff --git a/src/dash-table/derived/cell/wrappers.tsx b/src/dash-table/derived/cell/wrappers.tsx index 6c86d9ea8..f0c813578 100644 --- a/src/dash-table/derived/cell/wrappers.tsx +++ b/src/dash-table/derived/cell/wrappers.tsx @@ -3,7 +3,7 @@ import React, { MouseEvent } from 'react'; import { memoizeOne } from 'core/memoizer'; import memoizerCache from 'core/cache/memoizer'; -import { Data, IVisibleColumn, VisibleColumns, ActiveCell, SelectedCells, Datum, ColumnId, IViewportOffset, Presentation, ICellFactoryProps } from 'dash-table/components/Table/props'; +import { Data, IVisibleColumn, VisibleColumns, ICellCoordinates, SelectedCells, Datum, ColumnId, IViewportOffset, Presentation, ICellFactoryProps } from 'dash-table/components/Table/props'; import Cell from 'dash-table/components/Cell'; import derivedCellEventHandlerProps, { Handler } from 'dash-table/derived/cell/eventHandlerProps'; import isActiveCell from 'dash-table/derived/cell/isActive'; @@ -23,7 +23,7 @@ class Wrappers { * Returns the wrapper for each cell in the table. */ get = memoizeOne(( - activeCell: ActiveCell, + activeCell: ICellCoordinates | undefined, columns: VisibleColumns, data: Data, offset: IViewportOffset, diff --git a/src/dash-table/derived/paginator.ts b/src/dash-table/derived/paginator.ts index f384bb4f7..a989de96e 100644 --- a/src/dash-table/derived/paginator.ts +++ b/src/dash-table/derived/paginator.ts @@ -2,6 +2,8 @@ import * as R from 'ramda'; import { memoizeOneFactory } from 'core/memoizer'; +import { clearSelection } from 'dash-table/utils/actions'; + import { Data, PaginationMode, @@ -21,7 +23,7 @@ function getBackEndPagination( return { loadNext: () => { pagination_settings.current_page++; - setProps({ pagination_settings }); + setProps({ pagination_settings, ...clearSelection }); }, loadPrevious: () => { if (pagination_settings.current_page <= 0) { @@ -29,7 +31,7 @@ function getBackEndPagination( } pagination_settings.current_page--; - setProps({ pagination_settings }); + setProps({ pagination_settings, ...clearSelection }); } }; } @@ -51,7 +53,7 @@ function getFrontEndPagination( current_page: pagination_settings.current_page + 1 }); - setProps({ pagination_settings }); + setProps({ pagination_settings, ...clearSelection }); }, loadPrevious: () => { if (pagination_settings.current_page <= 0) { @@ -62,7 +64,7 @@ function getFrontEndPagination( current_page: pagination_settings.current_page - 1 }); - setProps({ pagination_settings }); + setProps({ pagination_settings, ...clearSelection }); } }; } @@ -93,4 +95,4 @@ const getter = ( } }; -export default memoizeOneFactory(getter); \ No newline at end of file +export default memoizeOneFactory(getter); diff --git a/src/dash-table/handlers/cellEvents.ts b/src/dash-table/handlers/cellEvents.ts index 8560fef7c..aaf9cb177 100644 --- a/src/dash-table/handlers/cellEvents.ts +++ b/src/dash-table/handlers/cellEvents.ts @@ -1,20 +1,21 @@ -import * as R from 'ramda'; -import { SelectedCells, ICellFactoryProps } from 'dash-table/components/Table/props'; +import { min, max, set, lensPath } from 'ramda'; +import { ICellFactoryProps } from 'dash-table/components/Table/props'; import isActive from 'dash-table/derived/cell/isActive'; +import isSelected from 'dash-table/derived/cell/isSelected'; +import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps'; import reconcile from 'dash-table/type/reconcile'; -function isCellSelected(selectedCells: SelectedCells, idx: number, i: number) { - return selectedCells && R.contains([idx, i], selectedCells); -} - export const handleClick = (propsFn: () => ICellFactoryProps, idx: number, i: number, e: any) => { const { selected_cells, + start_cell, setProps, - virtualized + virtualized, + columns, + viewport } = propsFn(); - const selected = isCellSelected(selected_cells, idx, i); + const selected = isSelected(selected_cells, idx, i); // don't update if already selected if (selected) { @@ -22,36 +23,47 @@ export const handleClick = (propsFn: () => ICellFactoryProps, idx: number, i: nu } e.preventDefault(); - const cellLocation: [number, number] = [ - idx + virtualized.offset.rows, - i + virtualized.offset.columns - ]; + + const row = idx + virtualized.offset.rows; + const col = i + virtualized.offset.columns; + + const activeCell = makeCell(row, col, columns, viewport); const newProps: Partial = { is_focused: false, - active_cell: cellLocation + active_cell: activeCell, + start_cell: activeCell, + end_cell: activeCell }; - const selectedRows = R.uniq(R.pluck(0, selected_cells)).sort((a, b) => a - b); - const selectedCols = R.uniq(R.pluck(1, selected_cells)).sort((a, b) => a - b); - const minRow = selectedRows[0]; - const minCol = selectedCols[0]; - - if (e.shiftKey) { - newProps.selected_cells = R.xprod( - R.range( - R.min(minRow, cellLocation[0]), - R.max(minRow, cellLocation[0]) + 1 - ), - R.range( - R.min(minCol, cellLocation[1]), - R.max(minCol, cellLocation[1]) + 1 - ) - ) as any; + if (e.shiftKey && start_cell && start_cell.row !== undefined) { + newProps.selected_cells = makeSelection( + { + minRow: min(row, start_cell.row), + maxRow: max(row, start_cell.row), + minCol: min(col, start_cell.column), + maxCol: max(col, start_cell.column) + }, + columns, + viewport + ); + delete newProps.start_cell; + delete newProps.active_cell; + } else { - newProps.selected_cells = [cellLocation]; + newProps.selected_cells = [activeCell]; } + /* + * In some cases this will initiate browser text selection. + * We've hijacked copying, so while it might be nice to allow copying part + * of a cell, currently you'll always get the whole cell regardless of what + * the browser thinks is selected. + * And when you've selected multiple cells the browser selection is + * completely wrong. + */ + window.getSelection().removeAllRanges(); + setProps(newProps); }; @@ -105,8 +117,8 @@ export const handleChange = (propsFn: () => ICellFactoryProps, idx: number, i: n return; } - const newData = R.set( - R.lensPath([realIdx, c.id]), + const newData = set( + lensPath([realIdx, c.id]), result.value, data ); @@ -184,4 +196,4 @@ export const handleOnMouseUp = (propsFn: () => ICellFactoryProps, idx: number, i export const handlePaste = (_propsFn: () => ICellFactoryProps, _idx: number, _i: number, e: any) => { e.preventDefault(); -}; \ No newline at end of file +}; diff --git a/src/dash-table/utils/TableClipboardHelper.ts b/src/dash-table/utils/TableClipboardHelper.ts index 7d7698209..54b19765d 100644 --- a/src/dash-table/utils/TableClipboardHelper.ts +++ b/src/dash-table/utils/TableClipboardHelper.ts @@ -4,15 +4,15 @@ import SheetClip from 'sheetclip'; import Clipboard from 'core/Clipboard'; import Logger from 'core/Logger'; -import { ActiveCell, Columns, Data, SelectedCells } from 'dash-table/components/Table/props'; +import { ICellCoordinates, Columns, Data, SelectedCells } from 'dash-table/components/Table/props'; import applyClipboardToData from './applyClipboardToData'; export default class TableClipboardHelper { private static lastLocalCopy: any[][] = [[]]; public static toClipboard(e: any, selectedCells: SelectedCells, columns: Columns, data: Data) { - const selectedRows = R.uniq(R.pluck(0, selectedCells).sort((a, b) => a - b)); - const selectedCols: any = R.uniq(R.pluck(1, selectedCells).sort((a, b) => a - b)); + const selectedRows = R.uniq(R.pluck('row', selectedCells).sort((a, b) => a - b)); + const selectedCols: any = R.uniq(R.pluck('column', selectedCells).sort((a, b) => a - b)); const df = R.slice( R.head(selectedRows) as any, @@ -32,7 +32,7 @@ export default class TableClipboardHelper { public static fromClipboard( ev: ClipboardEvent, - activeCell: ActiveCell, + activeCell: ICellCoordinates, derived_viewport_indices: number[], columns: Columns, data: Data, @@ -62,4 +62,4 @@ export default class TableClipboardHelper { overflowRows ); } -} \ No newline at end of file +} diff --git a/src/dash-table/utils/actions.js b/src/dash-table/utils/actions.js index e135fac9c..931b25559 100644 --- a/src/dash-table/utils/actions.js +++ b/src/dash-table/utils/actions.js @@ -43,13 +43,17 @@ export function deleteColumn(column, columns, headerRowIndex, props) { // inconsistencies. In an ideal world, we would probably only // update them if they contained one of the columns that we're // trying to delete - active_cell: [], - end_cell: [], - selected_cells: [], - start_cell: [0] + ...clearSelection }; } +export const clearSelection = { + active_cell: undefined, + start_cell: undefined, + end_cell: undefined, + selected_cells: [] +}; + export function editColumnName(column, columns, headerRowIndex, props) { const { groupIndexFirst, groupIndexLast } = getGroupedColumnIndices( column, columns, headerRowIndex, props diff --git a/src/dash-table/utils/applyClipboardToData.ts b/src/dash-table/utils/applyClipboardToData.ts index 7973a891d..b1bbb9df1 100644 --- a/src/dash-table/utils/applyClipboardToData.ts +++ b/src/dash-table/utils/applyClipboardToData.ts @@ -2,13 +2,13 @@ import * as R from 'ramda'; import Logger from 'core/Logger'; -import { ActiveCell, Columns, Data, ColumnType } from 'dash-table/components/Table/props'; +import { ICellCoordinates, Columns, Data, ColumnType } from 'dash-table/components/Table/props'; import reconcile from 'dash-table/type/reconcile'; import isEditable from 'dash-table/derived/cell/isEditable'; export default ( values: any[][], - activeCell: ActiveCell, + activeCell: ICellCoordinates, derived_viewport_indices: number[], columns: Columns, data: Data, @@ -27,10 +27,10 @@ export default ( let newData = R.clone(data); const newColumns = R.clone(columns); - if (overflowColumns && values[0].length + (activeCell as any)[1] >= columns.length) { + if (overflowColumns && values[0].length + (activeCell as any).column >= columns.length) { for ( let i = columns.length; - i < values[0].length + (activeCell as any)[1]; + i < values[0].length + (activeCell as any).column; i++ ) { newColumns.push({ @@ -42,7 +42,7 @@ export default ( } } - const realActiveRow = derived_viewport_indices[(activeCell as any)[0]]; + const realActiveRow = derived_viewport_indices[(activeCell as any).row]; if (overflowRows && values.length + realActiveRow >= data.length) { const emptyRow: any = {}; columns.forEach(c => (emptyRow[c.id] = '')); @@ -60,7 +60,7 @@ export default ( for (let [i, row] of values.entries()) { for (let [j, value] of row.entries()) { - const viewportIndex = (activeCell as any)[0] + i; + const viewportIndex = (activeCell as any).row + i; let iRealCell: number | undefined = viewportSize > viewportIndex ? derived_viewport_indices[viewportIndex] : @@ -72,7 +72,7 @@ export default ( continue; } - const jOffset = (activeCell as any)[1] + j; + const jOffset = (activeCell as any).column + j; const col = newColumns[jOffset]; if (!col || !isEditable(true, col.editable)) { continue; @@ -93,4 +93,4 @@ export default ( } return { data: newData, columns: newColumns }; -}; \ No newline at end of file +}; diff --git a/src/dash-table/utils/navigation.js b/src/dash-table/utils/navigation.js index 3df332a9d..052d3d8c7 100644 --- a/src/dash-table/utils/navigation.js +++ b/src/dash-table/utils/navigation.js @@ -1,13 +1,19 @@ import * as R from 'ramda'; -export function selectionCycle(nextCell, selected_cells) { - const selectedRows = R.uniq(R.pluck(0, selected_cells)).sort((a, b) => a - b); - const selectedCols = R.uniq(R.pluck(1, selected_cells)).sort((a, b) => a - b); +export function selectionBounds(selected_cells) { + const selectedRows = R.pluck('row', selected_cells); + const selectedCols = R.pluck('column', selected_cells); + + return { + minRow: R.reduce(R.min, Infinity, selectedRows), + minCol: R.reduce(R.min, Infinity, selectedCols), + maxRow: R.reduce(R.max, 0, selectedRows), + maxCol: R.reduce(R.max, 0, selectedCols) + }; +} - const minRow = selectedRows[0]; - const minCol = selectedCols[0]; - const maxRow = selectedRows[selectedRows.length - 1]; - const maxCol = selectedCols[selectedCols.length - 1]; +export function selectionCycle(nextCell, selected_cells) { + const {minRow, minCol, maxRow, maxCol} = selectionBounds(selected_cells); const [nextRow, nextCol] = nextCell; const adjustedCell = [nextRow, nextCol]; diff --git a/tests/cypress/tests/unit/clipboard_test.ts b/tests/cypress/tests/unit/clipboard_test.ts index 285f35148..6d362de43 100644 --- a/tests/cypress/tests/unit/clipboard_test.ts +++ b/tests/cypress/tests/unit/clipboard_test.ts @@ -7,7 +7,7 @@ describe('clipboard', () => { it('pastes one line at [0, 0] in one line df', () => { const res = applyClipboardToData( R.range(0, 1).map(value => [`${value}`]), - [0, 0], + {row: 0, column: 0, column_id: ''}, R.range(0, 1), ['c1'].map(id => ({ id: id, name: id })), R.range(0, 1).map(() => ({ c1: 'c1' })), @@ -26,7 +26,7 @@ describe('clipboard', () => { it('pastes two lines at [0, 0] in one line df', () => { const res = applyClipboardToData( R.range(0, 2).map(value => [`${value}`]), - [0, 0], + {row: 0, column: 0, column_id: ''}, R.range(0, 1), ['c1'].map(id => ({ id: id, name: id })), R.range(0, 1).map(() => ({ c1: 'c1' })), @@ -46,7 +46,7 @@ describe('clipboard', () => { it('pastes ten lines at [0, 0] in three line df', () => { const res = applyClipboardToData( R.range(0, 10).map(value => [`${value}`]), - [0, 0], + {row: 0, column: 0, column_id: ''}, R.range(0, 3), ['c1'].map(id => ({ id: id, name: id })), R.range(0, 3).map(() => ({ c1: 'c1' })), @@ -67,7 +67,7 @@ describe('clipboard', () => { it('pastes ten lines at [1, 0] in three line df', () => { const res = applyClipboardToData( R.range(0, 10).map(value => [`${value}`]), - [1, 0], + {row: 1, column: 0, column_id: ''}, R.range(0, 3), ['c1'].map(id => ({ id: id, name: id })), R.range(0, 3).map(() => ({ c1: 'c1' })), @@ -91,7 +91,7 @@ describe('clipboard', () => { it('pastes one line at [0, 0] in one line df', () => { const res = applyClipboardToData( R.range(0, 1).map(value => [`${value}`]), - [0, 0], + {row: 0, column: 0, column_id: ''}, R.range(0, 1), ['c1'].map(id => ({ id: id, name: id })), R.range(0, 1).map(() => ({ c1: 'c1' })), @@ -110,7 +110,7 @@ describe('clipboard', () => { it('pastes two lines at [0, 0] in one line df', () => { const res = applyClipboardToData( R.range(0, 2).map(value => [`${value}`]), - [0, 0], + {row: 0, column: 0, column_id: ''}, R.range(0, 1), ['c1'].map(id => ({ id: id, name: id })), R.range(0, 1).map(() => ({ c1: 'c1' })), @@ -129,7 +129,7 @@ describe('clipboard', () => { it('pastes ten lines at [0, 0] in three line df', () => { const res = applyClipboardToData( R.range(0, 10).map(value => [`${value}`]), - [0, 0], + {row: 0, column: 0, column_id: ''}, R.range(0, 3), ['c1'].map(id => ({ id: id, name: id })), R.range(0, 3).map(() => ({ c1: 'c1' })), @@ -150,7 +150,7 @@ describe('clipboard', () => { it('pastes ten lines at [1, 0] in three line df', () => { const res = applyClipboardToData( R.range(0, 10).map(value => [`${value}`]), - [1, 0], + {row: 1, column: 0, column_id: ''}, R.range(0, 3), ['c1'].map(id => ({ id: id, name: id })), R.range(0, 3).map(() => ({ c1: 'c1' })), @@ -169,4 +169,4 @@ describe('clipboard', () => { } }); }); -}); \ No newline at end of file +}); From edd86e96c7fda178cbbfbb0bbee40222d102371a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 21 Apr 2019 20:06:02 -0400 Subject: [PATCH 04/21] changelog for row IDs --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf9c7646..267c3fdb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,24 @@ This project adheres to [Semantic Versioning](http://semver.org/). - new `contains` relational operator for strings - new readonly `derived_filter_structure` prop exposing the query structure in a programmatically friendlier way +[#412](https://github.com/plotly/dash-table/pull/412) +- Add support for row IDs, based on the `'id'` attribute of each row of `data` + - IDs will not be displayed unless there is a column with `id='id'` + - `active_cell`, `start_cell`, `end_cell`, and items in `selected_cells` contain row and column IDs: All are now dicts `{'row', 'column', 'row_id' and 'column_id'}` rather than arrays `[row, column]`. + - Added new props mirroring all existing row indices props: + - `selected_row_ids` mirrors `selected_rows` + - `derived_viewport_row_ids` mirrors `derived_viewport_indices` + - `derived_virtual_row_ids` mirrors `derived_virtual_indices` + - `derived_viewport_selected_row_ids` mirrors `derived_viewport_selected_rows` + - `derived_virtual_selected_row_ids` mirrors `derived_virtual_selected_rows` + ### Changed [#397](https://github.com/plotly/dash-table/pull/397) - Rename `filtering_settings` to `filter` +[#412](https://github.com/plotly/dash-table/pull/412) +- `active_cell` and `selected_cells` items are dicts `{'row', 'column', 'row_id' and 'column_id'}` instead of arrays `[row, column]` + ## [3.6.0] - 2019-03-04 ### Fixed [#189](https://github.com/plotly/dash-table/issues/189) From 6bb24ad7c44a49aee9793145c5204ab3c02599c8 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 21 Apr 2019 20:21:11 -0400 Subject: [PATCH 05/21] fix doubleclick with row IDs --- src/dash-table/handlers/cellEvents.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/dash-table/handlers/cellEvents.ts b/src/dash-table/handlers/cellEvents.ts index aaf9cb177..c21e812de 100644 --- a/src/dash-table/handlers/cellEvents.ts +++ b/src/dash-table/handlers/cellEvents.ts @@ -72,23 +72,28 @@ export const handleDoubleClick = (propsFn: () => ICellFactoryProps, idx: number, editable, is_focused, setProps, - virtualized + virtualized, + columns, + viewport } = propsFn(); if (!editable) { return; } - const cellLocation: [number, number] = [ + const newCell = makeCell( idx + virtualized.offset.rows, - i + virtualized.offset.columns - ]; + i + virtualized.offset.columns, + columns, viewport + ); if (!is_focused) { e.preventDefault(); const newProps = { - selected_cells: [cellLocation], - active_cell: cellLocation, + selected_cells: [newCell], + active_cell: newCell, + start_cell: newCell, + end_cell: newCell, is_focused: true }; setProps(newProps); From c48ea4404ff5b0253b78cca2ade725c53b77a0b2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 21 Apr 2019 21:03:01 -0400 Subject: [PATCH 06/21] for realz: row/col IDs can be string or number! I was a bit schizophrenic about this before... --- src/dash-table/dash/DataTable.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 82538758e..ca1e79317 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -519,7 +519,9 @@ export const propTypes = { * are selected via the UI elements that appear when * `row_selectable` is `'single'` or `'multi'`. */ - selected_row_ids: PropTypes.arrayOf(PropTypes.string), + selected_row_ids: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + ), setProps: PropTypes.func, @@ -1052,7 +1054,9 @@ export const propTypes = { * `derived_viewport_row_ids` contains IDs for the current page, * while `derived_virtual_row_ids` contains IDs across all pages. */ - derived_viewport_row_ids: PropTypes.arrayOf(PropTypes.string), + derived_viewport_row_ids: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + ), /** * `derived_viewport_selected_rows` represents the indices of the @@ -1064,7 +1068,9 @@ export const propTypes = { * `derived_viewport_selected_row_ids` represents the IDs of the * `selected_rows` on the currently visible page. */ - derived_viewport_selected_row_ids: PropTypes.arrayOf(PropTypes.string), + derived_viewport_selected_row_ids: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + ), /** * This property represents the visible state of `data` @@ -1087,7 +1093,9 @@ export const propTypes = { * `derived_viewport_row_ids` contains IDs for the current page, * while `derived_virtual_row_ids` contains IDs across all pages. */ - derived_virtual_row_ids: PropTypes.arrayOf(PropTypes.number), + derived_virtual_row_ids: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + ), /** * `derived_virtual_selected_rows` represents the indices of the @@ -1100,7 +1108,9 @@ export const propTypes = { * `selected_rows` as they appear after filtering and sorting, * across all pages. */ - derived_virtual_selected_row_ids: PropTypes.arrayOf(PropTypes.string), + derived_virtual_selected_row_ids: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + ), /** * DEPRECATED From b1f2a390c4afb7d5c8874e1c12bf7b60f1941ea4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 21 Apr 2019 21:28:31 -0400 Subject: [PATCH 07/21] update percy storybook tests for row IDs --- .../percy-storybook/DashTable.percy.tsx | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/visual/percy-storybook/DashTable.percy.tsx b/tests/visual/percy-storybook/DashTable.percy.tsx index a2ac626c1..fbb561817 100644 --- a/tests/visual/percy-storybook/DashTable.percy.tsx +++ b/tests/visual/percy-storybook/DashTable.percy.tsx @@ -9,9 +9,41 @@ import fixtures from './fixtures'; const setProps = () => { }; +function makeCell(row: number, column: number, data: any[], columns: any[]) { + const cell: any = { + row, + column, + column_id: columns[column].id + }; + const rowID = data[row].id; + if(rowID !== undefined) { + cell.row_id = rowID; + } + return cell; +} + +function makeSelection(coords: any[], data: any[], columns: any[]) { + return coords.map(pair => makeCell(pair[0], pair[1], data, columns)); +} + // Legacy: Tests previously run in Python const fixtureStories = storiesOf('DashTable/Fixtures', module); -fixtures.forEach(fixture => fixtureStories.add(fixture.name, () => ())); +fixtures.forEach(fixture => { + // update active and selected cells for the new cell object format + const {data, columns, active_cell, selected_cells} = fixture.props; + if(Array.isArray(active_cell)) { + fixture.props.active_cell = makeCell( + active_cell[0], active_cell[1], data as any[], columns as any[] + ); + } + if(Array.isArray(selected_cells) && Array.isArray(selected_cells[0])) { + fixture.props.selected_cells = makeSelection( + selected_cells, data as any[], columns as any[] + ); + } + + fixtureStories.add(fixture.name, () => ()) +}); import dataset from './../../../datasets/gapminder.csv'; @@ -189,7 +221,7 @@ storiesOf('DashTable/Hidden Columns', module) id='table' data={data} columns={hiddenColumns} - active_cell={[1, 1]} + active_cell={makeCell(1, 1, data, hiddenColumns)} style_data_conditional={style_data_conditional} />)) .add('selected cells', () => (