From 07c20f448467cb0b4d39e446374394fadad1d5f0 Mon Sep 17 00:00:00 2001 From: AlekseyManetov Date: Thu, 12 Dec 2024 16:31:56 +0100 Subject: [PATCH] [DataTable]: fixed error while using lazy table with copy cell feature --- .../_examples/tables/LazyTable.example.tsx | 35 ++++++++++++++----- app/src/documents/structureComponents.ts | 2 +- .../DataTableSelectionProvider.tsx | 10 +++++- .../__tests__/helpers.test.tsx | 20 +++++------ .../__tests__/useSelectionManager.test.tsx | 32 ++++++++--------- .../tableCellsSelection/hooks/helpers.ts | 14 +++++--- .../hooks/useSelectionManager.tsx | 21 +++++------ .../tableCellsSelection/mocks/tables.tsx | 21 ++++++++--- .../src/table/tableCellsSelection/types.ts | 2 +- 9 files changed, 101 insertions(+), 56 deletions(-) diff --git a/app/src/docs/_examples/tables/LazyTable.example.tsx b/app/src/docs/_examples/tables/LazyTable.example.tsx index 49122d9725..9b25fad479 100644 --- a/app/src/docs/_examples/tables/LazyTable.example.tsx +++ b/app/src/docs/_examples/tables/LazyTable.example.tsx @@ -1,6 +1,16 @@ import React, { ReactNode, useCallback, useMemo, useState } from 'react'; import { DataSourceState, DataColumnProps, useUuiContext, useLazyDataSource, DropdownBodyProps } from '@epam/uui-core'; -import { Dropdown, DropdownMenuButton, DropdownMenuSplitter, DropdownMenuBody, Text, DataTable, Panel, IconButton } from '@epam/uui'; +import { + Dropdown, + DropdownMenuButton, + DropdownMenuSplitter, + DropdownMenuBody, + Text, + DataTable, + Panel, + IconButton, + DataTableCell, TextInput, +} from '@epam/uui'; import { City } from '@epam/uui-docs'; import { ReactComponent as MoreIcon } from '@epam/assets/icons/common/navigation-more_vert-18.svg'; import { ReactComponent as PencilIcon } from '@epam/assets/icons/common/content-edit-18.svg'; @@ -39,11 +49,14 @@ export default function CitiesTable() { { key: 'name', caption: 'Name', - render: (city) => ( - - {city.name} - + renderCell: (props) => ( + } + { ...props } + /> ), + canCopy: () => true, isSortable: true, width: 162, grow: 1, @@ -51,11 +64,14 @@ export default function CitiesTable() { { key: 'countryName', caption: 'Country', - render: (city) => ( - - {city.countryName} - + renderCell: (props) => ( + } + { ...props } + /> ), + canAcceptCopy: () => true, isSortable: true, width: 128, isFilterActive: (filter) => filter.country && filter.country.$in && !!filter.country.$in.length, @@ -123,6 +139,7 @@ export default function CitiesTable() { // Spread ListProps and provide getRows function from view to DataTable component. // getRows function will be called every time when table will need more rows. { ...view.getListProps() } + onCopy={ () => {} } getRows={ view.getVisibleRows } showColumnsConfig={ false } headerTextCase="upper" diff --git a/app/src/documents/structureComponents.ts b/app/src/documents/structureComponents.ts index 21193fd951..c666614e31 100644 --- a/app/src/documents/structureComponents.ts +++ b/app/src/documents/structureComponents.ts @@ -116,7 +116,7 @@ export const componentsStructure = orderBy( { id: 'tables', name: 'Data Tables', parentId: 'components', tags: ['table'] }, { id: 'tablesOverview', name: 'Overview', component: TablesOverviewDoc, parentId: 'tables', order: 1, tags: ['tables', 'dataTable'] }, { id: 'tree', name: 'Tree', component: TreeDoc, parentId: 'components', tags: ['tree', 'virtualList', 'dataSources'] }, - { id: 'editableTables', name: 'Editable Tables', component: EditableTablesDoc, parentId: 'tables', order: 2, tags: ['tables', 'dataTable'] }, + { id: 'editableTables', name: 'Editable', component: EditableTablesDoc, parentId: 'tables', order: 2, tags: ['tables', 'dataTable'] }, { id: 'advancedTables', name: 'Advanced', component: AdvancedTablesDoc, parentId: 'tables', order: 3, tags: ['tables', 'dataTable'] }, { id: 'useTableState', name: 'useTableState', component: useTableStateDoc, parentId: 'tables', order: 4, tags: ['tables', 'dataTable'] }, { id: 'filtersPanel', name: 'Filters Panel', component: FiltersPanelDoc, parentId: 'tables', order: 5, tags: ['tables', 'dataTable'] }, diff --git a/uui-components/src/table/tableCellsSelection/DataTableSelectionProvider.tsx b/uui-components/src/table/tableCellsSelection/DataTableSelectionProvider.tsx index 37a17a81b3..ebeca8c575 100644 --- a/uui-components/src/table/tableCellsSelection/DataTableSelectionProvider.tsx +++ b/uui-components/src/table/tableCellsSelection/DataTableSelectionProvider.tsx @@ -12,9 +12,17 @@ interface DataTableSelectionProviderProps extends React.Pro export function DataTableSelectionProvider({ onCopy, rows, columns, children, }: DataTableSelectionProviderProps) { + const rowsByIndex = useMemo(() => { + const rowsMap = new Map>(); + rows.forEach((row) => { + rowsMap.set(row.index, row); + }); + return rowsMap; + }, [rows]); + const { selectionRange, setSelectionRange, getSelectedCells, startCell, getCellSelectionInfo, - } = useSelectionManager({ rows, columns }); + } = useSelectionManager({ rowsByIndex, columns }); useEffect(() => { if (!selectionRange || !onCopy) return; diff --git a/uui-components/src/table/tableCellsSelection/__tests__/helpers.test.tsx b/uui-components/src/table/tableCellsSelection/__tests__/helpers.test.tsx index 86288daf2c..289646696f 100644 --- a/uui-components/src/table/tableCellsSelection/__tests__/helpers.test.tsx +++ b/uui-components/src/table/tableCellsSelection/__tests__/helpers.test.tsx @@ -1,7 +1,7 @@ import { getCell, getCellPosition, getStartCell, getNormalizedLimits, } from '../hooks/helpers'; -import { rowsMock, columnsMock } from '../mocks'; +import { rowsByIndexMock, columnsMock } from '../mocks'; describe('getNormalizedLimits', () => { it('should return normalized limits', () => { @@ -13,37 +13,37 @@ describe('getNormalizedLimits', () => { describe('getCell', () => { it('should get cell by coordinates', () => { - const { row, column } = getCell(1, 1, rowsMock, columnsMock); + const { row, column } = getCell(1, 1, rowsByIndexMock, columnsMock); const expectedColumn = columnsMock[1]; - const expectedRow = rowsMock[1]; + const expectedRow = rowsByIndexMock.get(1); expect(column).toEqual(expectedColumn); expect(row).toEqual(expectedRow); }); it('should return null if out of range', () => { - expect(getCell(rowsMock.length, 1, rowsMock, columnsMock)).toBeNull(); - expect(getCell(1, columnsMock.length, rowsMock, columnsMock)).toBeNull(); + expect(getCell(rowsByIndexMock.size, 1, rowsByIndexMock, columnsMock)).toBeNull(); + expect(getCell(1, columnsMock.length, rowsByIndexMock, columnsMock)).toBeNull(); }); }); describe('getStartCell', () => { it('should find a cell to copy from by coordinates', () => { const copyCellColumn = 0; - const copyCellRow = 1; + const copyCellRowIndex = 1; const expectedColumn = columnsMock[copyCellColumn]; - const expectedRow = rowsMock[copyCellRow]; + const expectedRow = rowsByIndexMock.get(copyCellRowIndex); const selectionRange = { - startColumnIndex: copyCellColumn, startRowIndex: copyCellRow, endColumnIndex: 1, endRowIndex: 2, + startColumnIndex: copyCellColumn, startRowIndex: copyCellRowIndex, endColumnIndex: 1, endRowIndex: 2, }; - const { column, row } = getStartCell(selectionRange, rowsMock, columnsMock); + const { column, row } = getStartCell(selectionRange, rowsByIndexMock, columnsMock); expect(column).toEqual(expectedColumn); expect(row).toEqual(expectedRow); }); it('should return null if no cell was selected', () => { - expect(getStartCell(null, rowsMock, columnsMock)).toBeNull(); + expect(getStartCell(null, rowsByIndexMock, columnsMock)).toBeNull(); }); }); diff --git a/uui-components/src/table/tableCellsSelection/__tests__/useSelectionManager.test.tsx b/uui-components/src/table/tableCellsSelection/__tests__/useSelectionManager.test.tsx index 3aa4b0aa67..2db06ed806 100644 --- a/uui-components/src/table/tableCellsSelection/__tests__/useSelectionManager.test.tsx +++ b/uui-components/src/table/tableCellsSelection/__tests__/useSelectionManager.test.tsx @@ -1,12 +1,12 @@ import { act } from 'react-dom/test-utils'; import { renderHook } from '@epam/uui-test-utils'; import { useSelectionManager } from '../hooks'; -import { columnsMock, rowsMock } from '../mocks'; +import { columnsMock, rowsByIndexMock } from '../mocks'; describe('useSelectioManager', () => { describe('selectRange', () => { it('should select some range', async () => { - const { result } = renderHook(() => useSelectionManager({ rows: rowsMock, columns: columnsMock })); + const { result } = renderHook(() => useSelectionManager({ rowsByIndex: rowsByIndexMock, columns: columnsMock })); const newSelectionRange = { startColumnIndex: 0, startRowIndex: 0, endColumnIndex: 1, endRowIndex: 1, isCopying: true, }; @@ -26,7 +26,7 @@ describe('useSelectioManager', () => { describe('startCell', () => { it('should return cell to copy from', async () => { - const { result } = renderHook(() => useSelectionManager({ rows: rowsMock, columns: columnsMock })); + const { result } = renderHook(() => useSelectionManager({ rowsByIndex: rowsByIndexMock, columns: columnsMock })); const newSelectionRange = { startColumnIndex: 1, startRowIndex: 1, endColumnIndex: 1, endRowIndex: 5, isCopying: true, }; @@ -35,7 +35,7 @@ describe('useSelectioManager', () => { }); const expectedColumn = columnsMock[newSelectionRange.startColumnIndex]; - const expectedRow = rowsMock[newSelectionRange.startRowIndex]; + const expectedRow = rowsByIndexMock.get(newSelectionRange.startRowIndex); expect(result.current.selectionRange).toEqual(newSelectionRange); expect(result.current.startCell).toBeDefined(); @@ -45,7 +45,7 @@ describe('useSelectioManager', () => { expect(row).toEqual(expectedRow); }); it('should null if selection range was not set', () => { - const { result } = renderHook(() => useSelectionManager({ rows: rowsMock, columns: columnsMock })); + const { result } = renderHook(() => useSelectionManager({ rowsByIndex: rowsByIndexMock, columns: columnsMock })); expect(result.current.selectionRange).toBeNull(); expect(result.current.startCell).toBeNull(); @@ -54,7 +54,7 @@ describe('useSelectioManager', () => { describe('getSelectedCells', () => { it('should return selected range', async () => { - const { result } = renderHook(() => useSelectionManager({ rows: rowsMock, columns: columnsMock })); + const { result } = renderHook(() => useSelectionManager({ rowsByIndex: rowsByIndexMock, columns: columnsMock })); const newSelectionRange = { startColumnIndex: 0, startRowIndex: 0, endColumnIndex: 1, endRowIndex: 3, isCopying: true, }; @@ -63,17 +63,17 @@ describe('useSelectioManager', () => { }); expect(result.current.getSelectedCells()).toEqual([ - { column: columnsMock[0], row: rowsMock[0] }, - { column: columnsMock[1], row: rowsMock[0] }, - { column: columnsMock[1], row: rowsMock[1] }, - { column: columnsMock[0], row: rowsMock[2] }, - { column: columnsMock[1], row: rowsMock[2] }, - { column: columnsMock[1], row: rowsMock[3] }, + { column: columnsMock[0], row: rowsByIndexMock.get(0) }, + { column: columnsMock[1], row: rowsByIndexMock.get(0) }, + { column: columnsMock[1], row: rowsByIndexMock.get(1) }, + { column: columnsMock[0], row: rowsByIndexMock.get(2) }, + { column: columnsMock[1], row: rowsByIndexMock.get(2) }, + { column: columnsMock[1], row: rowsByIndexMock.get(3) }, ]); }); it('should return null if selected range', () => { - const { result } = renderHook(() => useSelectionManager({ rows: rowsMock, columns: columnsMock })); + const { result } = renderHook(() => useSelectionManager({ rowsByIndex: rowsByIndexMock, columns: columnsMock })); expect(result.current.getSelectedCells()).toEqual([]); }); }); @@ -83,7 +83,7 @@ describe('useSelectioManager', () => { startColumnIndex: 0, startRowIndex: 0, endColumnIndex: 2, endRowIndex: 3, isCopying: true, }; it('should render borders for start cell', async () => { - const { result } = renderHook(() => useSelectionManager({ rows: rowsMock, columns: columnsMock })); + const { result } = renderHook(() => useSelectionManager({ rowsByIndex: rowsByIndexMock, columns: columnsMock })); act(() => { result.current.setSelectionRange(selectionRange); }); @@ -101,7 +101,7 @@ describe('useSelectioManager', () => { }); it('should render border for cell near border', async () => { - const { result } = renderHook(() => useSelectionManager({ rows: rowsMock, columns: columnsMock })); + const { result } = renderHook(() => useSelectionManager({ rowsByIndex: rowsByIndexMock, columns: columnsMock })); act(() => { result.current.setSelectionRange(selectionRange); }); @@ -130,7 +130,7 @@ describe('useSelectioManager', () => { }); it('should not render borders for cell inside the area of selection', async () => { - const { result } = renderHook(() => useSelectionManager({ rows: rowsMock, columns: columnsMock })); + const { result } = renderHook(() => useSelectionManager({ rowsByIndex: rowsByIndexMock, columns: columnsMock })); act(() => { result.current.setSelectionRange(selectionRange); }); diff --git a/uui-components/src/table/tableCellsSelection/hooks/helpers.ts b/uui-components/src/table/tableCellsSelection/hooks/helpers.ts index e01e001e4b..764e7de928 100644 --- a/uui-components/src/table/tableCellsSelection/hooks/helpers.ts +++ b/uui-components/src/table/tableCellsSelection/hooks/helpers.ts @@ -1,8 +1,14 @@ import { DataColumnProps, DataRowProps, DataTableSelectedCellData } from '@epam/uui-core'; import { DataTableSelectionRange } from '../types'; -export const getCell = (rowIndex: number, columnIndex: number, rows: DataRowProps[], columns: DataColumnProps[]) => { - const row = rows[rowIndex]; +export const getCell = ( + rowIndex: number, + columnIndex: number, + rowsByIndex: Map>, + columns: DataColumnProps[], +) => { + const row = rowsByIndex.get(rowIndex); const column = columns[columnIndex]; if (!row || !column) { @@ -13,7 +19,7 @@ export const getCell = (rowIndex: number, columnIndex: number, rows: export const getStartCell = ( selectionRange: DataTableSelectionRange | null, - rows: DataRowProps[], + rowsByIndex: Map>, columns: DataColumnProps[], ): DataTableSelectedCellData | null => { if (selectionRange === null) { @@ -21,7 +27,7 @@ export const getStartCell = ( } const { startRowIndex, startColumnIndex } = selectionRange; - return getCell(startRowIndex, startColumnIndex, rows, columns); + return getCell(startRowIndex, startColumnIndex, rowsByIndex, columns); }; export const getNormalizedLimits = (startIndex: number, endIndex: number) => (startIndex < endIndex ? [startIndex, endIndex] : [endIndex, startIndex]); diff --git a/uui-components/src/table/tableCellsSelection/hooks/useSelectionManager.tsx b/uui-components/src/table/tableCellsSelection/hooks/useSelectionManager.tsx index 70dd18576b..b5f840d9db 100644 --- a/uui-components/src/table/tableCellsSelection/hooks/useSelectionManager.tsx +++ b/uui-components/src/table/tableCellsSelection/hooks/useSelectionManager.tsx @@ -7,27 +7,28 @@ import { getCell, getCellPosition, getStartCell, getNormalizedLimits, } from './helpers'; -export const useSelectionManager = ({ rows, columns }: SelectionManagerProps): SelectionManager => { +export const useSelectionManager = ({ rowsByIndex, columns }: SelectionManagerProps): SelectionManager => { const [selectionRange, setSelectionRange] = useState(null); const startCell = useMemo( - () => getStartCell(selectionRange, rows, columns), + () => getStartCell(selectionRange, rowsByIndex, columns), [ - selectionRange?.startColumnIndex, selectionRange?.startRowIndex, rows, columns, + selectionRange?.startColumnIndex, selectionRange?.startRowIndex, rowsByIndex, columns, ], ); const canBeSelected = useCallback( (rowIndex: number, columnIndex: number, { copyFrom, copyTo }: CopyOptions) => { - const cell = getCell(rowIndex, columnIndex, rows, columns); + const cell = getCell(rowIndex, columnIndex, rowsByIndex, columns); + if (!startCell && copyTo) return false; - if (copyFrom) return !!cell.column.canCopy?.(cell); + if (copyFrom) { + return !!cell.column.canCopy?.(cell); + } return !!cell.column.canAcceptCopy?.(startCell, cell); }, - [ - startCell, columns, rows, - ], + [startCell, columns, rowsByIndex], ); const shouldSelectCell = useCallback( @@ -53,7 +54,7 @@ export const useSelectionManager = ({ rows, columns }: Sele for (let rowIndex = startRow; rowIndex <= endRow; rowIndex++) { for (let columnIndex = startColumn; columnIndex <= endColumn; columnIndex++) { if (shouldSelectCell(rowIndex, columnIndex)) { - const cell = getCell(rowIndex, columnIndex, rows, columns); + const cell = getCell(rowIndex, columnIndex, rowsByIndex, columns); selectedCells.push(cell); } } @@ -61,7 +62,7 @@ export const useSelectionManager = ({ rows, columns }: Sele return selectedCells; }, [ - selectionRange, columns, shouldSelectCell, rows, + selectionRange, columns, shouldSelectCell, rowsByIndex, ]); const getCellSelectionInfo = useCallback( diff --git a/uui-components/src/table/tableCellsSelection/mocks/tables.tsx b/uui-components/src/table/tableCellsSelection/mocks/tables.tsx index 7ad4c8aae0..cca20555e0 100644 --- a/uui-components/src/table/tableCellsSelection/mocks/tables.tsx +++ b/uui-components/src/table/tableCellsSelection/mocks/tables.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { DataColumnProps, DataRowProps } from '@epam/uui-core'; type Row = { salary: number; age: number; name: string; phone: string }; -export const rowsMock: DataRowProps[] = [ +const rowsMock: DataRowProps[] = [ { id: 1, rowKey: '1', @@ -10,21 +10,24 @@ export const rowsMock: DataRowProps[] = [ value: { age: 10, salary: 1000, name: 'first', phone: 'some phone 1', }, - }, { + }, + { id: 2, rowKey: '2', index: 1, value: { age: 20, salary: 2000, name: 'second', phone: 'some phone 2', }, - }, { + }, + { id: 3, rowKey: '3', index: 2, value: { age: 30, salary: 3000, name: 'third', phone: 'some phone 3', }, - }, { + }, + { id: 4, rowKey: '4', index: 3, @@ -34,6 +37,16 @@ export const rowsMock: DataRowProps[] = [ }, ]; +const getRowsByIndex = (rows: DataRowProps[]) => { + const rowsMap = new Map>(); + rows.forEach((row) => { + rowsMap.set(row.index, row); + }); + return rowsMap; +}; + +export const rowsByIndexMock = getRowsByIndex(rowsMock); + export const columnsMock: DataColumnProps[] = [ { key: 'age', diff --git a/uui-components/src/table/tableCellsSelection/types.ts b/uui-components/src/table/tableCellsSelection/types.ts index 577ae075a0..c3236baa7c 100644 --- a/uui-components/src/table/tableCellsSelection/types.ts +++ b/uui-components/src/table/tableCellsSelection/types.ts @@ -10,7 +10,7 @@ export interface DataTableSelectionRange { } export interface SelectionManagerProps { - rows: DataRowProps[]; + rowsByIndex: Map>; columns: DataColumnProps[]; }