Skip to content

Commit

Permalink
[DataTable]: fixed error while using lazy table with copy cell feature
Browse files Browse the repository at this point in the history
  • Loading branch information
AlekseyManetov committed Dec 12, 2024
1 parent 1fc5731 commit b9330b0
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 55 deletions.
35 changes: 26 additions & 9 deletions app/src/docs/_examples/tables/LazyTable.example.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import React, { ReactNode, useCallback, useEffect, 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';
Expand Down Expand Up @@ -37,22 +47,28 @@ export default function CitiesTable() {
}, {
key: 'name',
caption: 'Name',
render: (city) => (
<Text color="primary" fontSize="14">
{city.name}
</Text>
renderCell: (props) => (
<DataTableCell
{ ...props.rowLens.prop('name').toProps() }
renderEditor={ (props) => <TextInput { ...props } /> }
{ ...props }
/>
),
canCopy: () => true,
isSortable: true,
width: 162,
grow: 1,
}, {
key: 'countryName',
caption: 'Country',
render: (city) => (
<Text color="primary" fontSize="14">
{city.countryName}
</Text>
renderCell: (props) => (
<DataTableCell
{ ...props.rowLens.prop('countryName').toProps() }
renderEditor={ (props) => <TextInput { ...props } /> }
{ ...props }
/>
),
canAcceptCopy: () => true,
isSortable: true,
width: 128,
isFilterActive: (filter) => filter.country && filter.country.$in && !!filter.country.$in.length,
Expand Down Expand Up @@ -122,6 +138,7 @@ export default function CitiesTable() {
// Spread ListProps and provide getVisibleRows 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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@ export interface DataTableSelectionProviderProps<TItem, TId, TFilter> extends Re
export function DataTableSelectionProvider<TItem, TId, TFilter>({
onCopy, rows, columns, children,
}: DataTableSelectionProviderProps<TItem, TId, TFilter>) {
const rowsByIndex = useMemo(() => {
const rowsMap = new Map<number, DataRowProps<TItem, TId>>();
rows.forEach((row) => {
rowsMap.set(row.index, row);
});
return rowsMap;
}, [rows]);

const {
selectionRange, setSelectionRange, getSelectedCells, startCell, getCellSelectionInfo,
} = useSelectionManager<TItem, TId, TFilter>({ rows, columns });
} = useSelectionManager<TItem, TId, TFilter>({ rowsByIndex, columns });

useEffect(() => {
if (!selectionRange || !onCopy) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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();
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -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,
};
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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,
};
Expand All @@ -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([]);
});
});
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand Down Expand Up @@ -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);
});
Expand Down
14 changes: 10 additions & 4 deletions uui-components/src/table/tableCellsSelection/hooks/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { DataColumnProps, DataRowProps, DataTableSelectedCellData } from '@epam/uui-core';
import { DataTableSelectionRange } from '../types';

export const getCell = <TItem, TId>(rowIndex: number, columnIndex: number, rows: DataRowProps<TItem, TId>[], columns: DataColumnProps<TItem, TId>[]) => {
const row = rows[rowIndex];
export const getCell = <TItem, TId>(
rowIndex: number,
columnIndex: number,
rowsByIndex: Map<number,
DataRowProps<TItem, TId>>,
columns: DataColumnProps<TItem, TId>[],
) => {
const row = rowsByIndex.get(rowIndex);
const column = columns[columnIndex];

if (!row || !column) {
Expand All @@ -13,15 +19,15 @@ export const getCell = <TItem, TId>(rowIndex: number, columnIndex: number, rows:

export const getStartCell = <TItem, TId, TFilter>(
selectionRange: DataTableSelectionRange | null,
rows: DataRowProps<TItem, TId>[],
rowsByIndex: Map<number, DataRowProps<TItem, TId>>,
columns: DataColumnProps<TItem, TId>[],
): DataTableSelectedCellData<TItem, TId, TFilter> | null => {
if (selectionRange === null) {
return null;
}

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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,28 @@ import {
getCell, getCellPosition, getStartCell, getNormalizedLimits,
} from './helpers';

export const useSelectionManager = <TItem, TId, TFilter>({ rows, columns }: SelectionManagerProps<TItem, TId>): SelectionManager<TItem> => {
export const useSelectionManager = <TItem, TId, TFilter>({ rowsByIndex, columns }: SelectionManagerProps<TItem, TId>): SelectionManager<TItem> => {
const [selectionRange, setSelectionRange] = useState<DataTableSelectionRange>(null);

const startCell = useMemo(
() => getStartCell<TItem, TId, TFilter>(selectionRange, rows, columns),
() => getStartCell<TItem, TId, TFilter>(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(
Expand All @@ -53,15 +54,15 @@ export const useSelectionManager = <TItem, TId, TFilter>({ 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);
}
}
}

return selectedCells;
}, [
selectionRange, columns, shouldSelectCell, rows,
selectionRange, columns, shouldSelectCell, rowsByIndex,
]);

const getCellSelectionInfo = useCallback(
Expand Down
21 changes: 17 additions & 4 deletions uui-components/src/table/tableCellsSelection/mocks/tables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,32 @@ 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<Row, number>[] = [
const rowsMock: DataRowProps<Row, number>[] = [
{
id: 1,
rowKey: '1',
index: 0,
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,
Expand All @@ -34,6 +37,16 @@ export const rowsMock: DataRowProps<Row, number>[] = [
},
];

const getRowsByIndex = (rows: DataRowProps<Row, number>[]) => {
const rowsMap = new Map<number, DataRowProps<Row, number>>();
rows.forEach((row) => {
rowsMap.set(row.index, row);
});
return rowsMap;
};

export const rowsByIndexMock = getRowsByIndex(rowsMock);

export const columnsMock: DataColumnProps<Row, number>[] = [
{
key: 'age',
Expand Down
2 changes: 1 addition & 1 deletion uui-components/src/table/tableCellsSelection/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface DataTableSelectionRange {
}

export interface SelectionManagerProps<TItem, TId> {
rows: DataRowProps<TItem, TId>[];
rowsByIndex: Map<number, DataRowProps<TItem, TId>>;
columns: DataColumnProps<TItem, TId>[];
}

Expand Down

0 comments on commit b9330b0

Please sign in to comment.