@reactorui/datagrid 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/components/DataGrid/DataGrid.d.ts +1 -1
- package/dist/components/DataGrid/DataGrid.d.ts.map +1 -1
- package/dist/components/DataGrid/DataGrid.js +23 -13
- package/dist/components/Table/TableBody.d.ts +2 -3
- package/dist/components/Table/TableBody.d.ts.map +1 -1
- package/dist/components/Table/TableBody.js +24 -6
- package/dist/hooks/useDataGrid.d.ts +1 -0
- package/dist/hooks/useDataGrid.d.ts.map +1 -1
- package/dist/hooks/useDataGrid.js +29 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# @reactorui/datagrid
|
|
2
2
|
|
|
3
|
-
A high-performance, feature-rich React data grid component with TypeScript support, server-side integration, and advanced filtering capabilities.
|
|
3
|
+
A high-performance, feature-rich React data grid component with TypeScript support, server-side integration, pagination and advanced filtering capabilities.
|
|
4
|
+
|
|
5
|
+
## 🖼️ Screenshots
|
|
6
|
+
|
|
7
|
+
### Light Mode
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
### Dark Mode
|
|
12
|
+
|
|
13
|
+

|
|
4
14
|
|
|
5
15
|
## ✨ Features
|
|
6
16
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DataGridProps } from '../../types';
|
|
2
2
|
export declare const DataGrid: <T extends {
|
|
3
|
-
|
|
3
|
+
[key: string]: any;
|
|
4
4
|
} = any>({ data, endpoint, columns: columnsProp, enableSearch, enableSorting, enableFilters, enableSelection, pageSize, serverPageSize, pageSizeOptions, httpConfig, variant, size, className, enableRefresh, onDataLoad, onDataError, onLoadingStateChange, onPageChange, onPageSizeChange, onSortChange, onFilterChange, onSearchChange, onTableRefresh, onTableRowClick, onTableRowDoubleClick, onRowSelect, onSelectionChange, onTableRowHover, onCellClick, ...rest }: DataGridProps<T>) => import("react/jsx-runtime").JSX.Element;
|
|
5
5
|
//# sourceMappingURL=DataGrid.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataGrid.d.ts","sourceRoot":"","sources":["../../../src/components/DataGrid/DataGrid.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAU,MAAM,aAAa,CAAC;AAOpD,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS;IAAE,
|
|
1
|
+
{"version":3,"file":"DataGrid.d.ts","sourceRoot":"","sources":["../../../src/components/DataGrid/DataGrid.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAU,MAAM,aAAa,CAAC;AAOpD,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,GAAG,GAAG,EAAE,mcAmC9D,aAAa,CAAC,CAAC,CAAC,4CA4PlB,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useMemo } from 'react';
|
|
2
|
+
import React, { useMemo, useCallback } from 'react';
|
|
3
3
|
import { useDataGrid } from '../../hooks';
|
|
4
4
|
import { SearchInput } from '../Search';
|
|
5
5
|
import { FilterControls } from '../Filter';
|
|
@@ -10,7 +10,8 @@ export const DataGrid = ({ data, endpoint, columns: columnsProp = [], enableSear
|
|
|
10
10
|
onDataLoad, onDataError, onLoadingStateChange, onPageChange, onPageSizeChange, onSortChange, onFilterChange, onSearchChange, onTableRefresh, onTableRowClick, onTableRowDoubleClick, onRowSelect, onSelectionChange, onTableRowHover, onCellClick, ...rest }) => {
|
|
11
11
|
const theme = getTheme(variant);
|
|
12
12
|
// Use the data grid hook
|
|
13
|
-
const { data: sourceData, processedData, paginatedData, loading, error, searchTerm, activeFilters, sortConfig, selectedRows, currentPage, currentPageSize, setSearchTerm, setSort, setCurrentPageSize, navigateNext, navigatePrevious, addFilter, removeFilter, clearFilters, selectRow, selectAll, paginationInfo, selectedData, refresh,
|
|
13
|
+
const { data: sourceData, processedData, paginatedData, loading, error, searchTerm, activeFilters, sortConfig, selectedRows, currentPage, currentPageSize, setSearchTerm, setSort, setCurrentPageSize, navigateNext, navigatePrevious, addFilter, removeFilter, clearFilters, selectRow, selectAll, paginationInfo, selectedData, refresh, getRowId, // Get the stable ID generator
|
|
14
|
+
} = useDataGrid({
|
|
14
15
|
data,
|
|
15
16
|
endpoint,
|
|
16
17
|
httpConfig,
|
|
@@ -41,31 +42,40 @@ onDataLoad, onDataError, onLoadingStateChange, onPageChange, onPageSizeChange, o
|
|
|
41
42
|
}
|
|
42
43
|
return [];
|
|
43
44
|
}, [columnsProp, sourceData]);
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// Memoize the selection change handler
|
|
46
|
+
const handleSelectionChange = useCallback((newSelectedData) => {
|
|
47
|
+
onSelectionChange?.(newSelectedData);
|
|
48
|
+
}, [onSelectionChange]);
|
|
49
|
+
// Use a ref to track previous selectedData to avoid unnecessary calls
|
|
50
|
+
const previousSelectedData = React.useRef([]);
|
|
51
|
+
// Only call onSelectionChange when selectedData actually changes
|
|
52
|
+
React.useLayoutEffect(() => {
|
|
53
|
+
const hasChanged = selectedData.length !== previousSelectedData.current.length ||
|
|
54
|
+
selectedData.some((item, index) => item !== previousSelectedData.current[index]);
|
|
55
|
+
if (hasChanged && onSelectionChange) {
|
|
56
|
+
previousSelectedData.current = selectedData;
|
|
57
|
+
handleSelectionChange(selectedData);
|
|
48
58
|
}
|
|
49
|
-
}, [selectedData,
|
|
59
|
+
}, [selectedData, handleSelectionChange]);
|
|
50
60
|
// Handle row selection with callback
|
|
51
|
-
const handleRowSelect = (rowId, selected) => {
|
|
61
|
+
const handleRowSelect = useCallback((rowId, selected) => {
|
|
52
62
|
selectRow(rowId, selected);
|
|
53
63
|
if (onRowSelect) {
|
|
54
|
-
const row = sourceData.find((r) =>
|
|
64
|
+
const row = sourceData.find((r) => getRowId(r) === rowId);
|
|
55
65
|
if (row) {
|
|
56
66
|
onRowSelect(row, selected);
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
|
-
};
|
|
69
|
+
}, [selectRow, onRowSelect, sourceData, getRowId]);
|
|
60
70
|
// Handle refresh
|
|
61
|
-
const handleRefresh = () => {
|
|
71
|
+
const handleRefresh = useCallback(() => {
|
|
62
72
|
refresh();
|
|
63
73
|
onTableRefresh?.();
|
|
64
|
-
};
|
|
74
|
+
}, [refresh, onTableRefresh]);
|
|
65
75
|
if (error) {
|
|
66
76
|
return (_jsx("div", { className: `${theme.container} ${className}`, ...rest, children: _jsxs("div", { className: "px-4 py-8 text-center", children: [_jsx("div", { className: "text-red-600 dark:text-red-400 mb-2", children: "Error loading data" }), _jsx("div", { className: "text-sm text-gray-600 dark:text-gray-400 mb-4", children: error }), _jsx("button", { onClick: handleRefresh, className: theme.button, children: "Try Again" })] }) }));
|
|
67
77
|
}
|
|
68
|
-
return (_jsxs("div", { className: `${theme.container} ${className}`, ...rest, children: [enableFilters && (_jsx("div", { className: "p-4 pb-2", children: _jsxs("div", { className: "flex justify-between items-start gap-4", children: [_jsx("div", { className: "flex-1", children: _jsx(FilterControls, { columns: columns, activeFilters: activeFilters, onAddFilter: addFilter, onRemoveFilter: removeFilter, onClearFilters: clearFilters }) }), enableRefresh && (_jsx("div", { className: "flex-shrink-0", children: _jsx("button", { onClick: handleRefresh, disabled: loading, className: "px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-colors duration-150", children: loading ? 'Loading...' : 'Refresh' }) }))] }) })), _jsx("div", { className: "px-4 pb-4", children: _jsxs("div", { className: "flex justify-between items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [_jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: "Show" }), _jsx("select", { value: currentPageSize, onChange: (e) => setCurrentPageSize(parseInt(e.target.value)), className: "px-2 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400", children: pageSizeOptions.map((size) => (_jsx("option", { value: size, children: size }, size))) }), _jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: "entries" })] }), enableSearch && (_jsx("div", { className: "w-64 flex-shrink-0", children: _jsx(SearchInput, { value: searchTerm, onChange: setSearchTerm, placeholder: "Search...", disabled: loading, className: theme.searchInput }) }))] }) }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: theme.table, children: [_jsx(TableHeader, { columns: columns, sortConfig: sortConfig, onSort: enableSorting ? setSort : undefined, enableSelection: enableSelection, selectedCount: selectedRows.size, totalCount: paginatedData.length, onSelectAll: enableSelection ? selectAll : undefined, theme: theme }), _jsx(TableBody, { columns: columns, data: paginatedData, selectedRows: selectedRows, onSelectRow: enableSelection ? handleRowSelect : undefined, onRowClick: onTableRowClick, onRowDoubleClick: onTableRowDoubleClick, onRowHover: onTableRowHover, onCellClick: onCellClick, enableSelection: enableSelection, loading: loading, theme: theme })] }) }), _jsxs("div", { className: theme.pagination, children: [_jsxs("div", { className: "text-sm text-gray-700 dark:text-gray-300 flex-shrink-0", children: ["Showing ", paginationInfo.start, "-", paginationInfo.end, " of", ' ', paginationInfo.totalRecords.toLocaleString(), " records"] }), _jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [_jsx("button", { onClick: navigatePrevious, disabled: !paginationInfo.hasPrevious, className: "px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150", children: "Previous" }), _jsxs("span", { className: "text-sm text-gray-700 dark:text-gray-300 px-2", children: ["Page ", currentPage, " ", paginationInfo.totalPages > 0 && `of ${paginationInfo.totalPages}`] }), _jsx("button", { onClick: navigateNext, disabled: !paginationInfo.hasNext, className: "px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150", children: "Next" })] })] })] }));
|
|
78
|
+
return (_jsxs("div", { className: `${theme.container} ${className}`, ...rest, children: [enableFilters && (_jsx("div", { className: "p-4 pb-2", children: _jsxs("div", { className: "flex justify-between items-start gap-4", children: [_jsx("div", { className: "flex-1", children: _jsx(FilterControls, { columns: columns, activeFilters: activeFilters, onAddFilter: addFilter, onRemoveFilter: removeFilter, onClearFilters: clearFilters }) }), enableRefresh && (_jsx("div", { className: "flex-shrink-0", children: _jsx("button", { onClick: handleRefresh, disabled: loading, className: "px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-colors duration-150", children: loading ? 'Loading...' : 'Refresh' }) }))] }) })), _jsx("div", { className: "px-4 pb-4", children: _jsxs("div", { className: "flex justify-between items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [_jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: "Show" }), _jsx("select", { value: currentPageSize, onChange: (e) => setCurrentPageSize(parseInt(e.target.value)), className: "px-2 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400", children: pageSizeOptions.map((size) => (_jsx("option", { value: size, children: size }, size))) }), _jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: "entries" })] }), enableSearch && (_jsx("div", { className: "w-64 flex-shrink-0", children: _jsx(SearchInput, { value: searchTerm, onChange: setSearchTerm, placeholder: "Search...", disabled: loading, className: theme.searchInput }) }))] }) }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: theme.table, children: [_jsx(TableHeader, { columns: columns, sortConfig: sortConfig, onSort: enableSorting ? setSort : undefined, enableSelection: enableSelection, selectedCount: selectedRows.size, totalCount: paginatedData.length, onSelectAll: enableSelection ? selectAll : undefined, theme: theme }), _jsx(TableBody, { columns: columns, data: paginatedData, selectedRows: selectedRows, onSelectRow: enableSelection ? handleRowSelect : undefined, onRowClick: onTableRowClick, onRowDoubleClick: onTableRowDoubleClick, onRowHover: onTableRowHover, onCellClick: onCellClick, enableSelection: enableSelection, loading: loading, theme: theme, getRowId: getRowId })] }) }), _jsxs("div", { className: theme.pagination, children: [_jsxs("div", { className: "text-sm text-gray-700 dark:text-gray-300 flex-shrink-0", children: ["Showing ", paginationInfo.start, "-", paginationInfo.end, " of", ' ', paginationInfo.totalRecords.toLocaleString(), " records"] }), _jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [_jsx("button", { onClick: navigatePrevious, disabled: !paginationInfo.hasPrevious, className: "px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150", children: "Previous" }), _jsxs("span", { className: "text-sm text-gray-700 dark:text-gray-300 px-2", children: ["Page ", currentPage, " ", paginationInfo.totalPages > 0 && `of ${paginationInfo.totalPages}`] }), _jsx("button", { onClick: navigateNext, disabled: !paginationInfo.hasNext, className: "px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150", children: "Next" })] })] })] }));
|
|
69
79
|
};
|
|
70
80
|
// Helper function to infer data type
|
|
71
81
|
function inferDataType(value) {
|
|
@@ -14,9 +14,8 @@ interface TableBodyProps<T> {
|
|
|
14
14
|
loading: boolean;
|
|
15
15
|
emptyMessage?: string;
|
|
16
16
|
theme: Theme;
|
|
17
|
+
getRowId?: (row: T) => string;
|
|
17
18
|
}
|
|
18
|
-
export declare const TableBody: <T extends {
|
|
19
|
-
id?: string | number;
|
|
20
|
-
}>({ columns, data, selectedRows, onSelectRow, onRowClick, onRowDoubleClick, onRowHover, onCellClick, enableSelection, loading, emptyMessage, theme, }: TableBodyProps<T>) => import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare const TableBody: <T extends Record<string, any>>({ columns, data, selectedRows, onSelectRow, onRowClick, onRowDoubleClick, onRowHover, onCellClick, enableSelection, loading, emptyMessage, theme, getRowId, }: TableBodyProps<T>) => import("react/jsx-runtime").JSX.Element;
|
|
21
20
|
export {};
|
|
22
21
|
//# sourceMappingURL=TableBody.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TableBody.d.ts","sourceRoot":"","sources":["../../../src/components/Table/TableBody.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC,UAAU,cAAc,CAAC,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrB,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACzD,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACvD,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC7D,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC9D,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACvF,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"TableBody.d.ts","sourceRoot":"","sources":["../../../src/components/Table/TableBody.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC,UAAU,cAAc,CAAC,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrB,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACzD,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACvD,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC7D,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC9D,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACvF,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;CAC/B;AAED,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,+JActD,cAAc,CAAC,CAAC,CAAC,4CAsJnB,CAAC"}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export const TableBody = ({ columns, data, selectedRows, onSelectRow, onRowClick, onRowDoubleClick, onRowHover, onCellClick, enableSelection, loading, emptyMessage = 'No data available', theme, getRowId, }) => {
|
|
4
|
+
// Generate stable row IDs - always generate, never use existing fields
|
|
5
|
+
const rowIdMap = React.useMemo(() => {
|
|
6
|
+
const map = new Map();
|
|
7
|
+
data.forEach((row, index) => {
|
|
8
|
+
// Always generate based on index and content hash
|
|
9
|
+
const contentHash = JSON.stringify(row)
|
|
10
|
+
.slice(0, 50)
|
|
11
|
+
.replace(/[^a-zA-Z0-9]/g, '');
|
|
12
|
+
const stableId = `row-${index}-${contentHash}`;
|
|
13
|
+
map.set(row, stableId);
|
|
14
|
+
});
|
|
15
|
+
return map;
|
|
16
|
+
}, [data]);
|
|
17
|
+
// Use provided getRowId or fallback to generated IDs
|
|
18
|
+
const getStableRowId = React.useCallback((row) => {
|
|
19
|
+
if (getRowId) {
|
|
20
|
+
return getRowId(row);
|
|
21
|
+
}
|
|
22
|
+
return rowIdMap.get(row) || `fallback-${Math.random().toString(36).substr(2, 9)}`;
|
|
23
|
+
}, [getRowId, rowIdMap]);
|
|
3
24
|
const renderCell = (row, column) => {
|
|
4
25
|
const value = row[column.key];
|
|
5
26
|
if (column.render) {
|
|
@@ -7,9 +28,6 @@ export const TableBody = ({ columns, data, selectedRows, onSelectRow, onRowClick
|
|
|
7
28
|
}
|
|
8
29
|
return value?.toString() || '';
|
|
9
30
|
};
|
|
10
|
-
const getRowId = (row) => {
|
|
11
|
-
return String(row.id || Math.random());
|
|
12
|
-
};
|
|
13
31
|
const handleRowClick = (row, event) => {
|
|
14
32
|
// Don't trigger if clicking on interactive elements
|
|
15
33
|
if (event.target instanceof HTMLInputElement ||
|
|
@@ -20,7 +38,7 @@ export const TableBody = ({ columns, data, selectedRows, onSelectRow, onRowClick
|
|
|
20
38
|
onRowClick?.(row, event);
|
|
21
39
|
// Also handle selection if enabled
|
|
22
40
|
if (enableSelection && onSelectRow) {
|
|
23
|
-
const rowId =
|
|
41
|
+
const rowId = getStableRowId(row);
|
|
24
42
|
const isSelected = selectedRows.has(rowId);
|
|
25
43
|
onSelectRow(rowId, !isSelected);
|
|
26
44
|
}
|
|
@@ -38,7 +56,7 @@ export const TableBody = ({ columns, data, selectedRows, onSelectRow, onRowClick
|
|
|
38
56
|
return (_jsx("tbody", { className: "bg-white dark:bg-gray-800", children: _jsx("tr", { children: _jsx("td", { colSpan: columns.length + (enableSelection ? 1 : 0), className: "px-4 py-8 text-center text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800", children: emptyMessage }) }) }));
|
|
39
57
|
}
|
|
40
58
|
return (_jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-600", children: data.map((row, index) => {
|
|
41
|
-
const rowId =
|
|
59
|
+
const rowId = getStableRowId(row);
|
|
42
60
|
const isSelected = selectedRows.has(rowId);
|
|
43
61
|
return (_jsxs("tr", { className: `cursor-pointer ${isSelected ? theme.selectedRow : theme.row}`, onClick: (e) => handleRowClick(row, e), onDoubleClick: (e) => onRowDoubleClick?.(row, e), onMouseEnter: (e) => onRowHover?.(row, e), onMouseLeave: (e) => onRowHover?.(null, e), children: [enableSelection && (_jsx("td", { className: theme.cell, children: _jsx("input", { type: "checkbox", checked: isSelected, onChange: (e) => {
|
|
44
62
|
e.stopPropagation();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDataGrid.d.ts","sourceRoot":"","sources":["../../src/hooks/useDataGrid.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EAEX,cAAc,EACd,YAAY,EACZ,UAAU,EACV,UAAU,EACV,cAAc,EACf,MAAM,UAAU,CAAC;AAGlB,UAAU,gBAAgB,CAAC,CAAC;IAC1B,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC/C,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,KAAK,IAAI,CAAC;IACtE,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9E,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IACnD,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAED,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,WAAW,EAAE,oMAchD,gBAAgB,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"useDataGrid.d.ts","sourceRoot":"","sources":["../../src/hooks/useDataGrid.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EAEX,cAAc,EACd,YAAY,EACZ,UAAU,EACV,UAAU,EACV,cAAc,EACf,MAAM,UAAU,CAAC;AAGlB,UAAU,gBAAgB,CAAC,CAAC;IAC1B,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC/C,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,KAAK,IAAI,CAAC;IACtE,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9E,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IACnD,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAED,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,WAAW,EAAE,oMAchD,gBAAgB,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;0BAoUX,MAAM;sBArFJ,MAAM;2BAkBR,MAAM;sCA2CC,MAAM;;;wBAuCX,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC;0BAqB5B,MAAM;;uBA2BsB,MAAM,YAAY,OAAO;0BAalD,OAAO;;;;;oBA1WZ,CAAC,KAAG,MAAM;CAwanB,CAAC"}
|
|
@@ -11,13 +11,30 @@ export const useDataGrid = ({ data: staticData, endpoint, httpConfig, pageSize =
|
|
|
11
11
|
const [activeFilters, setActiveFilters] = useState([]);
|
|
12
12
|
const [sortConfig, setSortConfig] = useState({ column: '', direction: 'asc' });
|
|
13
13
|
const [selectedRows, setSelectedRows] = useState(new Set());
|
|
14
|
-
// Pagination state
|
|
14
|
+
// Pagination state
|
|
15
15
|
const [currentPage, setCurrentPage] = useState(1);
|
|
16
16
|
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
|
17
17
|
const [continuationToken, setContinuationToken] = useState();
|
|
18
18
|
const [tokenHistory, setTokenHistory] = useState([]);
|
|
19
19
|
// Determine data source
|
|
20
20
|
const sourceData = staticData || serverData;
|
|
21
|
+
// Generate stable unique IDs for rows - always generate, never use existing fields
|
|
22
|
+
const rowIdMap = useMemo(() => {
|
|
23
|
+
const map = new Map();
|
|
24
|
+
sourceData.forEach((row, index) => {
|
|
25
|
+
// Always generate a stable ID based on index and content hash
|
|
26
|
+
const contentHash = JSON.stringify(row)
|
|
27
|
+
.slice(0, 50)
|
|
28
|
+
.replace(/[^a-zA-Z0-9]/g, '');
|
|
29
|
+
const stableId = `row-${index}-${contentHash}`;
|
|
30
|
+
map.set(row, stableId);
|
|
31
|
+
});
|
|
32
|
+
return map;
|
|
33
|
+
}, [sourceData]);
|
|
34
|
+
// Helper function to get row ID - always uses generated IDs
|
|
35
|
+
const getRowId = useCallback((row) => {
|
|
36
|
+
return rowIdMap.get(row) || `fallback-${Math.random().toString(36).substr(2, 9)}`;
|
|
37
|
+
}, [rowIdMap]);
|
|
21
38
|
// Internal loading state handler
|
|
22
39
|
const handleLoadingChange = useCallback((newLoading, context) => {
|
|
23
40
|
setLoading(newLoading);
|
|
@@ -59,6 +76,13 @@ export const useDataGrid = ({ data: staticData, endpoint, httpConfig, pageSize =
|
|
|
59
76
|
const end = start + currentPageSize;
|
|
60
77
|
return processedData.slice(start, end);
|
|
61
78
|
}, [processedData, currentPage, currentPageSize, staticData, sourceData]);
|
|
79
|
+
// Memoize selectedData with proper dependencies and stable ID handling
|
|
80
|
+
const selectedData = useMemo(() => {
|
|
81
|
+
return sourceData.filter((row) => {
|
|
82
|
+
const rowId = getRowId(row);
|
|
83
|
+
return selectedRows.has(rowId);
|
|
84
|
+
});
|
|
85
|
+
}, [sourceData, selectedRows, getRowId]);
|
|
62
86
|
// Pagination info
|
|
63
87
|
const paginationInfo = useMemo(() => {
|
|
64
88
|
if (!staticData && lastServerResponse) {
|
|
@@ -300,13 +324,13 @@ export const useDataGrid = ({ data: staticData, endpoint, httpConfig, pageSize =
|
|
|
300
324
|
const selectAll = useCallback((selected) => {
|
|
301
325
|
if (selected) {
|
|
302
326
|
const currentPageData = staticData ? paginatedData : sourceData;
|
|
303
|
-
const allIds = currentPageData.map(
|
|
327
|
+
const allIds = currentPageData.map(getRowId);
|
|
304
328
|
setSelectedRows(new Set(allIds));
|
|
305
329
|
}
|
|
306
330
|
else {
|
|
307
331
|
setSelectedRows(new Set());
|
|
308
332
|
}
|
|
309
|
-
}, [staticData, paginatedData, sourceData]);
|
|
333
|
+
}, [staticData, paginatedData, sourceData, getRowId]);
|
|
310
334
|
const refresh = useCallback(() => {
|
|
311
335
|
if (staticData) {
|
|
312
336
|
setSearchTerm('');
|
|
@@ -350,7 +374,8 @@ export const useDataGrid = ({ data: staticData, endpoint, httpConfig, pageSize =
|
|
|
350
374
|
refresh,
|
|
351
375
|
// Computed
|
|
352
376
|
paginationInfo,
|
|
353
|
-
selectedData
|
|
377
|
+
selectedData,
|
|
354
378
|
hasSelection: selectedRows.size > 0,
|
|
379
|
+
getRowId, // Export the helper function
|
|
355
380
|
};
|
|
356
381
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactorui/datagrid",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "High-performance React data grid with TypeScript support, server-side integration, and continuation token pagination",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|