@cdx-ui/data-grid 0.0.1-beta.185

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.
Files changed (54) hide show
  1. package/README.md +11 -0
  2. package/lib/commonjs/DataGrid.js +478 -0
  3. package/lib/commonjs/DataGrid.js.map +1 -0
  4. package/lib/commonjs/adapter/columns.js +38 -0
  5. package/lib/commonjs/adapter/columns.js.map +1 -0
  6. package/lib/commonjs/adapter/selectColumn.js +18 -0
  7. package/lib/commonjs/adapter/selectColumn.js.map +1 -0
  8. package/lib/commonjs/index.js +13 -0
  9. package/lib/commonjs/index.js.map +1 -0
  10. package/lib/commonjs/package.json +1 -0
  11. package/lib/commonjs/styles.js +61 -0
  12. package/lib/commonjs/styles.js.map +1 -0
  13. package/lib/commonjs/types.js +17 -0
  14. package/lib/commonjs/types.js.map +1 -0
  15. package/lib/commonjs/useDataGridTable.js +253 -0
  16. package/lib/commonjs/useDataGridTable.js.map +1 -0
  17. package/lib/module/DataGrid.js +474 -0
  18. package/lib/module/DataGrid.js.map +1 -0
  19. package/lib/module/adapter/columns.js +32 -0
  20. package/lib/module/adapter/columns.js.map +1 -0
  21. package/lib/module/adapter/selectColumn.js +14 -0
  22. package/lib/module/adapter/selectColumn.js.map +1 -0
  23. package/lib/module/index.js +4 -0
  24. package/lib/module/index.js.map +1 -0
  25. package/lib/module/styles.js +57 -0
  26. package/lib/module/styles.js.map +1 -0
  27. package/lib/module/types.js +13 -0
  28. package/lib/module/types.js.map +1 -0
  29. package/lib/module/useDataGridTable.js +240 -0
  30. package/lib/module/useDataGridTable.js.map +1 -0
  31. package/lib/typescript/DataGrid.d.ts +7 -0
  32. package/lib/typescript/DataGrid.d.ts.map +1 -0
  33. package/lib/typescript/adapter/columns.d.ts +17 -0
  34. package/lib/typescript/adapter/columns.d.ts.map +1 -0
  35. package/lib/typescript/adapter/selectColumn.d.ts +9 -0
  36. package/lib/typescript/adapter/selectColumn.d.ts.map +1 -0
  37. package/lib/typescript/index.d.ts +3 -0
  38. package/lib/typescript/index.d.ts.map +1 -0
  39. package/lib/typescript/styles.d.ts +22 -0
  40. package/lib/typescript/styles.d.ts.map +1 -0
  41. package/lib/typescript/types.d.ts +112 -0
  42. package/lib/typescript/types.d.ts.map +1 -0
  43. package/lib/typescript/useDataGridTable.d.ts +54 -0
  44. package/lib/typescript/useDataGridTable.d.ts.map +1 -0
  45. package/package.json +87 -0
  46. package/src/DataGrid.tsx +622 -0
  47. package/src/adapter/columns.test.ts +47 -0
  48. package/src/adapter/columns.ts +32 -0
  49. package/src/adapter/selectColumn.ts +12 -0
  50. package/src/index.ts +17 -0
  51. package/src/styles.ts +104 -0
  52. package/src/types.ts +133 -0
  53. package/src/useDataGridTable.test.ts +125 -0
  54. package/src/useDataGridTable.ts +295 -0
@@ -0,0 +1,12 @@
1
+ import { SELECT_COLUMN_ID } from '../types';
2
+
3
+ export function createSelectColumn() {
4
+ return {
5
+ id: SELECT_COLUMN_ID,
6
+ header: SELECT_COLUMN_ID,
7
+ enableSorting: false,
8
+ enableColumnFilter: false,
9
+ enableGlobalFilter: false,
10
+ size: 48,
11
+ } as const;
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { DataGrid } from './DataGrid';
2
+ export type {
3
+ DataGridProps,
4
+ GridAlignment,
5
+ GridColDef,
6
+ GridFilterItem,
7
+ GridFilterModel,
8
+ GridPaginationModel,
9
+ GridRenderCellParams,
10
+ GridRowId,
11
+ GridRowSelectionModel,
12
+ GridRowsProp,
13
+ GridSortDirection,
14
+ GridSortItem,
15
+ GridSortModel,
16
+ GridValidRowModel,
17
+ } from './types';
package/src/styles.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { Platform } from 'react-native';
2
+ import { cva } from '@cdx-ui/utils';
3
+
4
+ export const dataGridRootVariants = cva(
5
+ 'h-[300px] w-full min-w-0 max-w-full flex-col overflow-hidden rounded-2xl border border-stroke-secondary bg-surface-primary',
6
+ );
7
+
8
+ export const dataGridMainVariants = cva(
9
+ 'relative min-h-0 w-full min-w-0 max-w-full flex-1 overflow-auto',
10
+ );
11
+
12
+ export const dataGridToolbarVariants = cva(
13
+ 'shrink-0 flex-row items-center gap-3 border-b border-stroke-secondary px-4 py-4',
14
+ );
15
+
16
+ export const dataGridToolbarSearchVariants = cva('max-w-80 min-w-0 flex-1');
17
+
18
+ export const dataGridToolbarActionsVariants = cva('ms-auto flex-row items-center gap-3');
19
+
20
+ export const dataGridFooterVariants = cva(
21
+ 'min-h-[52px] shrink-0 flex-row items-center justify-end gap-2 border-t border-stroke-secondary py-0 pe-0.5 ps-6',
22
+ );
23
+
24
+ export const dataGridOverlayVariants = cva(
25
+ 'absolute inset-x-0 bottom-0 items-center justify-center bg-surface-primary',
26
+ {
27
+ variants: {
28
+ headerFilters: {
29
+ true: 'top-24',
30
+ false: 'top-12',
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ headerFilters: false,
35
+ },
36
+ },
37
+ );
38
+
39
+ export const dataGridEmptyOverlayVariants = cva(
40
+ 'pointer-events-none absolute inset-0 items-center justify-center',
41
+ );
42
+
43
+ export const dataGridHeaderCellInnerVariants = cva('flex-row items-center gap-3');
44
+
45
+ // Collapsed table borders scroll away from sticky headers; separated cell borders stick.
46
+ export const dataGridTableVariants = cva(
47
+ Platform.select({
48
+ web: 'border-separate border-spacing-0',
49
+ default: '',
50
+ }),
51
+ );
52
+
53
+ export const dataGridHeaderVariants = cva([
54
+ 'bg-neutral-50',
55
+ Platform.select({
56
+ web: 'sticky top-0 z-20',
57
+ default: '',
58
+ }),
59
+ ]);
60
+
61
+ export const dataGridBodyVariants = cva(
62
+ Platform.select({
63
+ web: '[&_tr:last-child>*]:border-b-0',
64
+ default: '',
65
+ }),
66
+ );
67
+
68
+ export const dataGridFilterInputVariants = cva('min-w-24');
69
+
70
+ export const dataGridHeadVariants = cva([
71
+ 'h-12 bg-neutral-50 px-4',
72
+ Platform.select({
73
+ web: 'border-b border-stroke-secondary',
74
+ default: '',
75
+ }),
76
+ ]);
77
+
78
+ export const dataGridSelectHeadVariants = cva([
79
+ 'w-14 bg-neutral-50 px-4',
80
+ Platform.select({
81
+ web: 'border-b border-stroke-secondary',
82
+ default: '',
83
+ }),
84
+ ]);
85
+
86
+ export const dataGridCellVariants = cva([
87
+ 'h-12 px-4',
88
+ Platform.select({
89
+ web: 'border-b border-stroke-secondary',
90
+ default: '',
91
+ }),
92
+ ]);
93
+
94
+ export const dataGridSelectCellVariants = cva([
95
+ 'w-14 px-4',
96
+ Platform.select({
97
+ web: 'border-b border-stroke-secondary',
98
+ default: '',
99
+ }),
100
+ ]);
101
+
102
+ export const dataGridPaginationVariants = cva('flex-row flex-wrap items-center justify-end gap-0');
103
+
104
+ export const dataGridPaginationRangeVariants = cva('mx-5 shrink-0');
package/src/types.ts ADDED
@@ -0,0 +1,133 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ViewStyle } from 'react-native';
3
+
4
+ export type GridRowId = string | number;
5
+
6
+ export type GridValidRowModel = Record<string, unknown>;
7
+
8
+ export type GridRowsProp<R extends GridValidRowModel = GridValidRowModel> = readonly R[];
9
+
10
+ export type GridSortDirection = 'asc' | 'desc';
11
+
12
+ export type GridAlignment = 'start' | 'center' | 'end' | 'left' | 'right';
13
+
14
+ export interface GridSortItem {
15
+ field: string;
16
+ sort: GridSortDirection;
17
+ }
18
+
19
+ export type GridSortModel = GridSortItem[];
20
+
21
+ export interface GridPaginationModel {
22
+ page: number;
23
+ pageSize: number;
24
+ }
25
+
26
+ export interface GridRowSelectionModel {
27
+ type: 'include' | 'exclude';
28
+ ids: Set<GridRowId>;
29
+ }
30
+
31
+ export interface GridFilterItem {
32
+ field: string;
33
+ value: string;
34
+ }
35
+
36
+ export type GridFilterModel = GridFilterItem[];
37
+
38
+ export interface GridRenderCellParams<R extends GridValidRowModel = GridValidRowModel> {
39
+ value: unknown;
40
+ row: R;
41
+ field: string;
42
+ id: GridRowId;
43
+ }
44
+
45
+ export interface GridColDef<R extends GridValidRowModel = GridValidRowModel> {
46
+ field: string;
47
+ headerName?: string;
48
+ width?: number;
49
+ minWidth?: number;
50
+ flex?: number;
51
+ /**
52
+ * Logical cell-content alignment.
53
+ *
54
+ * `left` and `right` are deprecated compatibility aliases for `start` and `end`.
55
+ * @default 'start'
56
+ */
57
+ align?: GridAlignment;
58
+ /**
59
+ * Logical header-content alignment. Defaults to `align`.
60
+ *
61
+ * `left` and `right` are deprecated compatibility aliases for `start` and `end`.
62
+ */
63
+ headerAlign?: GridAlignment;
64
+ sortable?: boolean;
65
+ filterable?: boolean;
66
+ type?: 'string';
67
+ valueGetter?: (value: unknown, row: R, col: GridColDef<R>) => unknown;
68
+ valueFormatter?: (value: unknown, row: R, col: GridColDef<R>) => ReactNode | string;
69
+ renderCell?: (params: GridRenderCellParams<R>) => ReactNode;
70
+ renderHeader?: () => ReactNode;
71
+ }
72
+
73
+ export type GridPaginationMode = 'client' | 'server';
74
+
75
+ export interface DataGridProps<R extends GridValidRowModel = GridValidRowModel> {
76
+ rows: GridRowsProp<R>;
77
+ columns: readonly GridColDef<R>[];
78
+ getRowId?: (row: R) => GridRowId;
79
+ /** Shows the loading overlay, disables pagination, and suppresses the empty state. */
80
+ loading?: boolean;
81
+ checkboxSelection?: boolean;
82
+ hideFooter?: boolean;
83
+ hideFooterPagination?: boolean;
84
+ disableColumnSorting?: boolean;
85
+ paginationMode?: GridPaginationMode;
86
+ sortingMode?: GridPaginationMode;
87
+ rowCount?: number;
88
+ sortModel?: GridSortModel;
89
+ defaultSortModel?: GridSortModel;
90
+ onSortModelChange?: (model: GridSortModel) => void;
91
+ paginationModel?: GridPaginationModel;
92
+ defaultPaginationModel?: GridPaginationModel;
93
+ onPaginationModelChange?: (model: GridPaginationModel) => void;
94
+ /** Options for the footer rows-per-page control. Defaults to 25, 50, 100. */
95
+ pageSizeOptions?: readonly (number | { value: number; label: string })[];
96
+ rowSelectionModel?: GridRowSelectionModel;
97
+ defaultRowSelectionModel?: GridRowSelectionModel;
98
+ onRowSelectionModelChange?: (model: GridRowSelectionModel) => void;
99
+ filterModel?: GridFilterModel;
100
+ defaultFilterModel?: GridFilterModel;
101
+ onFilterModelChange?: (model: GridFilterModel) => void;
102
+ quickFilter?: string;
103
+ defaultQuickFilter?: string;
104
+ onQuickFilterChange?: (value: string) => void;
105
+ /** Shows the toolbar search field. Off by default, same as MUI Community. */
106
+ showToolbar?: boolean;
107
+ /** Consumer-owned toolbar controls beside search (export, density, toggles, etc.). Wire actions to app state that drives grid props — DataGrid does not ship toolbar buttons. */
108
+ toolbarActions?: ReactNode;
109
+ headerFilters?: boolean;
110
+ /** Replaces the default centered "No rows" message. Shown when `rows` is empty and not loading. */
111
+ noRowsOverlay?: ReactNode;
112
+ /** Replaces the default skeleton rows shown when `loading` is true. */
113
+ loadingOverlay?: ReactNode;
114
+ className?: string;
115
+ style?: ViewStyle;
116
+ label?: string;
117
+ 'aria-label'?: string;
118
+ 'aria-labelledby'?: string;
119
+ }
120
+
121
+ export const SELECT_COLUMN_ID = '__select__';
122
+
123
+ export const DEFAULT_PAGE_SIZE_OPTIONS = [25, 50, 100] as const;
124
+
125
+ export const DEFAULT_PAGINATION_MODEL: GridPaginationModel = {
126
+ page: 0,
127
+ pageSize: 25,
128
+ };
129
+
130
+ export const EMPTY_SELECTION: GridRowSelectionModel = {
131
+ type: 'include',
132
+ ids: new Set(),
133
+ };
@@ -0,0 +1,125 @@
1
+ import type { RowSelectionState } from '@tanstack/react-table';
2
+ import type { GridRowId } from './types';
3
+ import {
4
+ columnFiltersToFilterModel,
5
+ rowSelectionToModel,
6
+ selectionModelToRowSelection,
7
+ } from './useDataGridTable';
8
+
9
+ describe('columnFiltersToFilterModel', () => {
10
+ it('keeps string filter values', () => {
11
+ expect(columnFiltersToFilterModel([{ id: 'name', value: 'alice' }])).toEqual([
12
+ { field: 'name', value: 'alice' },
13
+ ]);
14
+ });
15
+
16
+ it('drops non-primitive filter values instead of stringifying objects', () => {
17
+ expect(columnFiltersToFilterModel([{ id: 'name', value: { nested: true } }])).toEqual([
18
+ { field: 'name', value: '' },
19
+ ]);
20
+ });
21
+
22
+ it('omits the select column', () => {
23
+ expect(
24
+ columnFiltersToFilterModel([
25
+ { id: '__select__', value: 'x' },
26
+ { id: 'status', value: 'active' },
27
+ ]),
28
+ ).toEqual([{ field: 'status', value: 'active' }]);
29
+ });
30
+ });
31
+
32
+ describe('selectionModelToRowSelection', () => {
33
+ const allIds: GridRowId[] = [1, 2, 3];
34
+
35
+ it('maps include models to TanStack row selection keys', () => {
36
+ const model = { type: 'include' as const, ids: new Set<GridRowId>([1, 2]) };
37
+ expect(selectionModelToRowSelection(model, allIds)).toEqual({
38
+ 'number:1': true,
39
+ 'number:2': true,
40
+ });
41
+ });
42
+
43
+ it('does not conflate string and numeric ids in exclude models', () => {
44
+ const model = { type: 'exclude' as const, ids: new Set<GridRowId>(['1']) };
45
+ expect(selectionModelToRowSelection(model, allIds)).toEqual({
46
+ 'number:1': true,
47
+ 'number:2': true,
48
+ 'number:3': true,
49
+ });
50
+ });
51
+
52
+ it('maps exclude models when excluded ids are numeric', () => {
53
+ const model = { type: 'exclude' as const, ids: new Set<GridRowId>([2]) };
54
+ expect(selectionModelToRowSelection(model, allIds)).toEqual({
55
+ 'number:1': true,
56
+ 'number:3': true,
57
+ });
58
+ });
59
+
60
+ it('creates distinct table keys for string and numeric ids', () => {
61
+ const model = { type: 'include' as const, ids: new Set<GridRowId>([1, '1']) };
62
+ expect(selectionModelToRowSelection(model, [1, '1'])).toEqual({
63
+ 'number:1': true,
64
+ 'string:1': true,
65
+ });
66
+ });
67
+ });
68
+
69
+ describe('rowSelectionToModel', () => {
70
+ const allIds: GridRowId[] = [1, 2, 3];
71
+
72
+ it('round-trips include models with canonical numeric ids', () => {
73
+ const model = { type: 'include' as const, ids: new Set<GridRowId>([2, 3]) };
74
+ const state = selectionModelToRowSelection(model, allIds);
75
+ expect(rowSelectionToModel(state, model, allIds)).toEqual({
76
+ type: 'include',
77
+ ids: new Set([2, 3]),
78
+ });
79
+ });
80
+
81
+ it('round-trips exclude models preserving type and numeric ids', () => {
82
+ const model = { type: 'exclude' as const, ids: new Set<GridRowId>([1]) };
83
+ const state = selectionModelToRowSelection(model, allIds);
84
+ expect(rowSelectionToModel(state, model, allIds)).toEqual({
85
+ type: 'exclude',
86
+ ids: new Set([1]),
87
+ });
88
+ });
89
+
90
+ it('keeps exclude type after a user deselects a row', () => {
91
+ const model = { type: 'exclude' as const, ids: new Set<GridRowId>([1]) };
92
+ const next: RowSelectionState = { 'number:3': true };
93
+ expect(rowSelectionToModel(next, model, allIds)).toEqual({
94
+ type: 'exclude',
95
+ ids: new Set([1, 2]),
96
+ });
97
+ });
98
+
99
+ it('restores numeric ids after include selection changes', () => {
100
+ const model = { type: 'include' as const, ids: new Set<GridRowId>() };
101
+ const next: RowSelectionState = { 'number:1': true };
102
+ expect(rowSelectionToModel(next, model, allIds)).toEqual({
103
+ type: 'include',
104
+ ids: new Set([1]),
105
+ });
106
+ });
107
+
108
+ it('preserves included selections from unloaded server pages', () => {
109
+ const model = { type: 'include' as const, ids: new Set<GridRowId>([1]) };
110
+ const next: RowSelectionState = { 'number:2': true };
111
+ expect(rowSelectionToModel(next, model, [2])).toEqual({
112
+ type: 'include',
113
+ ids: new Set([1, 2]),
114
+ });
115
+ });
116
+
117
+ it('preserves excluded selections from unloaded server pages', () => {
118
+ const model = { type: 'exclude' as const, ids: new Set<GridRowId>([1]) };
119
+ const next: RowSelectionState = { 'number:2': true };
120
+ expect(rowSelectionToModel(next, model, [2])).toEqual({
121
+ type: 'exclude',
122
+ ids: new Set([1]),
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,295 @@
1
+ import { useMemo } from 'react';
2
+ import {
3
+ columnFilteringFeature,
4
+ createFilteredRowModel,
5
+ createPaginatedRowModel,
6
+ createSortedRowModel,
7
+ filterFn_includesString,
8
+ globalFilteringFeature,
9
+ rowPaginationFeature,
10
+ rowSelectionFeature,
11
+ rowSortingFeature,
12
+ sortFn_alphanumeric,
13
+ sortFn_text,
14
+ tableFeatures,
15
+ useTable,
16
+ type ColumnFiltersState,
17
+ type PaginationState,
18
+ type RowSelectionState,
19
+ type SortingState,
20
+ } from '@tanstack/react-table';
21
+ import { createSelectColumn } from './adapter/selectColumn';
22
+ import { applyUpdater, gridColDefToColumnDef } from './adapter/columns';
23
+ import type {
24
+ GridColDef,
25
+ GridFilterModel,
26
+ GridPaginationModel,
27
+ GridRowId,
28
+ GridRowSelectionModel,
29
+ GridRowsProp,
30
+ GridSortModel,
31
+ GridValidRowModel,
32
+ } from './types';
33
+ import { SELECT_COLUMN_ID } from './types';
34
+
35
+ const features = tableFeatures({
36
+ columnFilteringFeature,
37
+ globalFilteringFeature,
38
+ rowSortingFeature,
39
+ rowPaginationFeature,
40
+ rowSelectionFeature,
41
+ filteredRowModel: createFilteredRowModel(),
42
+ sortedRowModel: createSortedRowModel(),
43
+ paginatedRowModel: createPaginatedRowModel(),
44
+ filterFns: { includesString: filterFn_includesString },
45
+ sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
46
+ });
47
+
48
+ export function sortModelToSortingState(model: GridSortModel): SortingState {
49
+ return model.map((item) => ({ id: item.field, desc: item.sort === 'desc' }));
50
+ }
51
+
52
+ export function sortingStateToSortModel(state: SortingState): GridSortModel {
53
+ return state
54
+ .filter((item) => item.id !== SELECT_COLUMN_ID)
55
+ .slice(0, 1)
56
+ .map((item) => ({
57
+ field: item.id,
58
+ sort: item.desc ? 'desc' : 'asc',
59
+ }));
60
+ }
61
+
62
+ export function paginationModelToState(model: GridPaginationModel): PaginationState {
63
+ return { pageIndex: model.page, pageSize: model.pageSize };
64
+ }
65
+
66
+ export function paginationStateToModel(state: PaginationState): GridPaginationModel {
67
+ return { page: state.pageIndex, pageSize: state.pageSize };
68
+ }
69
+
70
+ function columnFilterValueToString(value: unknown): string {
71
+ if (typeof value === 'string') {
72
+ return value;
73
+ }
74
+ if (value == null) {
75
+ return '';
76
+ }
77
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
78
+ return String(value);
79
+ }
80
+ return '';
81
+ }
82
+
83
+ export function filterModelToColumnFilters(model: GridFilterModel): ColumnFiltersState {
84
+ return model
85
+ .filter((item) => item.value.trim().length > 0)
86
+ .map((item) => ({ id: item.field, value: item.value }));
87
+ }
88
+
89
+ export function columnFiltersToFilterModel(state: ColumnFiltersState): GridFilterModel {
90
+ return state
91
+ .filter((item) => item.id !== SELECT_COLUMN_ID)
92
+ .map((item) => ({ field: item.id, value: columnFilterValueToString(item.value) }));
93
+ }
94
+
95
+ function selectionIdKey(id: GridRowId): string {
96
+ return `${typeof id}:${String(id)}`;
97
+ }
98
+
99
+ export function selectionModelToRowSelection(
100
+ model: GridRowSelectionModel,
101
+ allIds: GridRowId[],
102
+ ): RowSelectionState {
103
+ const selected = new Set<string>();
104
+ if (model.type === 'include') {
105
+ for (const id of model.ids) {
106
+ selected.add(selectionIdKey(id));
107
+ }
108
+ } else {
109
+ for (const id of allIds) {
110
+ if (!model.ids.has(id)) {
111
+ selected.add(selectionIdKey(id));
112
+ }
113
+ }
114
+ }
115
+ return Object.fromEntries([...selected].map((id) => [id, true]));
116
+ }
117
+
118
+ export function rowSelectionToModel(
119
+ state: RowSelectionState,
120
+ previousModel: GridRowSelectionModel,
121
+ allIds: GridRowId[],
122
+ ): GridRowSelectionModel {
123
+ const currentIdsByKey = new Map<string, GridRowId>();
124
+ for (const id of allIds) {
125
+ currentIdsByKey.set(selectionIdKey(id), id);
126
+ }
127
+
128
+ const selectedKeys = new Set(Object.keys(state).filter((key) => state[key]));
129
+ const ids = new Set(previousModel.ids);
130
+
131
+ if (previousModel.type === 'exclude') {
132
+ for (const [key, id] of currentIdsByKey) {
133
+ if (selectedKeys.has(key)) {
134
+ ids.delete(id);
135
+ } else {
136
+ ids.add(id);
137
+ }
138
+ }
139
+ return { type: 'exclude', ids };
140
+ }
141
+
142
+ for (const [key, id] of currentIdsByKey) {
143
+ if (selectedKeys.has(key)) {
144
+ ids.add(id);
145
+ } else {
146
+ ids.delete(id);
147
+ }
148
+ }
149
+ return { type: 'include', ids };
150
+ }
151
+
152
+ export function resolveRowId<R extends GridValidRowModel>(
153
+ row: R,
154
+ getRowId?: (row: R) => GridRowId,
155
+ ): GridRowId {
156
+ if (getRowId) {
157
+ return getRowId(row);
158
+ }
159
+ const id = row.id;
160
+ if (typeof id === 'string' || typeof id === 'number') {
161
+ return id;
162
+ }
163
+ if (__DEV__) {
164
+ console.warn(
165
+ '[DataGrid] Each row needs an `id` or a `getRowId` function. Missing ids break selection.',
166
+ );
167
+ }
168
+ return '';
169
+ }
170
+
171
+ interface UseDataGridTableParams<R extends GridValidRowModel> {
172
+ rows: GridRowsProp<R>;
173
+ columns: readonly GridColDef<R>[];
174
+ getRowId?: (row: R) => GridRowId;
175
+ checkboxSelection: boolean;
176
+ disableColumnSorting: boolean;
177
+ paginationMode: 'client' | 'server';
178
+ sortingMode: 'client' | 'server';
179
+ rowCount?: number;
180
+ sortModel: GridSortModel;
181
+ onSortModelChange: (model: GridSortModel) => void;
182
+ paginationModel: GridPaginationModel;
183
+ onPaginationModelChange: (model: GridPaginationModel) => void;
184
+ filterModel: GridFilterModel;
185
+ onFilterModelChange: (model: GridFilterModel) => void;
186
+ quickFilter: string;
187
+ onQuickFilterChange: (value: string) => void;
188
+ rowSelectionModel: GridRowSelectionModel;
189
+ onRowSelectionModelChange: (model: GridRowSelectionModel) => void;
190
+ }
191
+
192
+ export function useDataGridTable<R extends GridValidRowModel>({
193
+ rows,
194
+ columns,
195
+ getRowId,
196
+ checkboxSelection,
197
+ disableColumnSorting,
198
+ paginationMode,
199
+ sortingMode,
200
+ rowCount,
201
+ sortModel,
202
+ onSortModelChange,
203
+ paginationModel,
204
+ onPaginationModelChange,
205
+ filterModel,
206
+ onFilterModelChange,
207
+ quickFilter,
208
+ onQuickFilterChange,
209
+ rowSelectionModel,
210
+ onRowSelectionModelChange,
211
+ }: UseDataGridTableParams<R>) {
212
+ const data = useMemo(() => [...rows], [rows]);
213
+ const allIds = useMemo(() => data.map((row) => resolveRowId(row, getRowId)), [data, getRowId]);
214
+
215
+ const columnDefs = useMemo(() => {
216
+ const mapped = columns.map((col) => {
217
+ const def = gridColDefToColumnDef(col);
218
+ if (disableColumnSorting) {
219
+ def.enableSorting = false;
220
+ }
221
+ return def;
222
+ });
223
+ return checkboxSelection ? [createSelectColumn(), ...mapped] : mapped;
224
+ }, [checkboxSelection, columns, disableColumnSorting]);
225
+
226
+ const sortingState = useMemo(() => sortModelToSortingState(sortModel), [sortModel]);
227
+ const paginationState = useMemo(() => paginationModelToState(paginationModel), [paginationModel]);
228
+ const columnFilterState = useMemo(() => filterModelToColumnFilters(filterModel), [filterModel]);
229
+ const rowSelectionState = useMemo(
230
+ () => selectionModelToRowSelection(rowSelectionModel, allIds),
231
+ [rowSelectionModel, allIds],
232
+ );
233
+
234
+ const resetPaginationPage = () => {
235
+ onPaginationModelChange({ ...paginationModel, page: 0 });
236
+ };
237
+
238
+ const setQuickFilter = (value: string) => {
239
+ onQuickFilterChange(value);
240
+ resetPaginationPage();
241
+ };
242
+
243
+ const setColumnFilter = (field: string, value: string) => {
244
+ const next = filterModel.filter((item) => item.field !== field);
245
+ if (value.trim().length > 0) {
246
+ next.push({ field, value });
247
+ }
248
+ onFilterModelChange(next);
249
+ resetPaginationPage();
250
+ };
251
+
252
+ const table = useTable({
253
+ features,
254
+ columns: columnDefs,
255
+ data,
256
+ getRowId: (row) => selectionIdKey(resolveRowId(row, getRowId)),
257
+ enableMultiSort: false,
258
+ enableMultiRowSelection: true,
259
+ manualPagination: paginationMode === 'server',
260
+ manualSorting: sortingMode === 'server',
261
+ autoResetPageIndex: false,
262
+ rowCount: paginationMode === 'server' ? rowCount : undefined,
263
+ state: {
264
+ sorting: sortingState,
265
+ pagination: paginationState,
266
+ columnFilters: columnFilterState,
267
+ globalFilter: quickFilter,
268
+ rowSelection: rowSelectionState,
269
+ },
270
+ onSortingChange: (updater) => {
271
+ const next = applyUpdater(updater, sortingState);
272
+ onSortModelChange(sortingStateToSortModel(next));
273
+ resetPaginationPage();
274
+ },
275
+ onPaginationChange: (updater) => {
276
+ const next = applyUpdater(updater, paginationState);
277
+ onPaginationModelChange(paginationStateToModel(next));
278
+ },
279
+ onColumnFiltersChange: (updater) => {
280
+ const next = applyUpdater(updater, columnFilterState);
281
+ onFilterModelChange(columnFiltersToFilterModel(next));
282
+ resetPaginationPage();
283
+ },
284
+ onGlobalFilterChange: (updater) => {
285
+ const next = applyUpdater(updater, quickFilter);
286
+ setQuickFilter(typeof next === 'string' ? next : '');
287
+ },
288
+ onRowSelectionChange: (updater) => {
289
+ const next = applyUpdater(updater, rowSelectionState);
290
+ onRowSelectionModelChange(rowSelectionToModel(next, rowSelectionModel, allIds));
291
+ },
292
+ });
293
+
294
+ return { table, setQuickFilter, setColumnFilter };
295
+ }