@cdx-ui/data-grid 0.3.11 → 0.3.12

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 (82) hide show
  1. package/README.md +3 -1
  2. package/coverage.svg +1 -1
  3. package/lib/commonjs/DataGrid.js +232 -62
  4. package/lib/commonjs/DataGrid.js.map +1 -1
  5. package/lib/commonjs/DataGridFilterPanel.js +420 -0
  6. package/lib/commonjs/DataGridFilterPanel.js.map +1 -0
  7. package/lib/commonjs/adapter/columnTypes.js +256 -0
  8. package/lib/commonjs/adapter/columnTypes.js.map +1 -0
  9. package/lib/commonjs/adapter/columns.js +132 -4
  10. package/lib/commonjs/adapter/columns.js.map +1 -1
  11. package/lib/commonjs/adapter/filterOperators.js +422 -0
  12. package/lib/commonjs/adapter/filterOperators.js.map +1 -0
  13. package/lib/commonjs/adapter/pinning.js +101 -0
  14. package/lib/commonjs/adapter/pinning.js.map +1 -0
  15. package/lib/commonjs/index.js +26 -0
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/numberCells.js +280 -0
  18. package/lib/commonjs/numberCells.js.map +1 -0
  19. package/lib/commonjs/styles.js +61 -1
  20. package/lib/commonjs/styles.js.map +1 -1
  21. package/lib/commonjs/types.js +29 -1
  22. package/lib/commonjs/types.js.map +1 -1
  23. package/lib/commonjs/useDataGridTable.js +98 -13
  24. package/lib/commonjs/useDataGridTable.js.map +1 -1
  25. package/lib/module/DataGrid.js +237 -67
  26. package/lib/module/DataGrid.js.map +1 -1
  27. package/lib/module/DataGridFilterPanel.js +414 -0
  28. package/lib/module/DataGridFilterPanel.js.map +1 -0
  29. package/lib/module/adapter/columnTypes.js +240 -0
  30. package/lib/module/adapter/columnTypes.js.map +1 -0
  31. package/lib/module/adapter/columns.js +129 -4
  32. package/lib/module/adapter/columns.js.map +1 -1
  33. package/lib/module/adapter/filterOperators.js +406 -0
  34. package/lib/module/adapter/filterOperators.js.map +1 -0
  35. package/lib/module/adapter/pinning.js +90 -0
  36. package/lib/module/adapter/pinning.js.map +1 -0
  37. package/lib/module/index.js +2 -0
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/numberCells.js +275 -0
  40. package/lib/module/numberCells.js.map +1 -0
  41. package/lib/module/styles.js +60 -0
  42. package/lib/module/styles.js.map +1 -1
  43. package/lib/module/types.js +28 -0
  44. package/lib/module/types.js.map +1 -1
  45. package/lib/module/useDataGridTable.js +99 -14
  46. package/lib/module/useDataGridTable.js.map +1 -1
  47. package/lib/typescript/DataGrid.d.ts.map +1 -1
  48. package/lib/typescript/DataGridFilterPanel.d.ts +17 -0
  49. package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -0
  50. package/lib/typescript/adapter/columnTypes.d.ts +32 -0
  51. package/lib/typescript/adapter/columnTypes.d.ts.map +1 -0
  52. package/lib/typescript/adapter/columns.d.ts +14 -5
  53. package/lib/typescript/adapter/columns.d.ts.map +1 -1
  54. package/lib/typescript/adapter/filterOperators.d.ts +43 -0
  55. package/lib/typescript/adapter/filterOperators.d.ts.map +1 -0
  56. package/lib/typescript/adapter/pinning.d.ts +25 -0
  57. package/lib/typescript/adapter/pinning.d.ts.map +1 -0
  58. package/lib/typescript/index.d.ts +4 -1
  59. package/lib/typescript/index.d.ts.map +1 -1
  60. package/lib/typescript/numberCells.d.ts +46 -0
  61. package/lib/typescript/numberCells.d.ts.map +1 -0
  62. package/lib/typescript/styles.d.ts +21 -0
  63. package/lib/typescript/styles.d.ts.map +1 -1
  64. package/lib/typescript/types.d.ts +86 -7
  65. package/lib/typescript/types.d.ts.map +1 -1
  66. package/lib/typescript/useDataGridTable.d.ts +18 -4
  67. package/lib/typescript/useDataGridTable.d.ts.map +1 -1
  68. package/package.json +5 -5
  69. package/src/DataGrid.tsx +377 -67
  70. package/src/DataGridFilterPanel.tsx +477 -0
  71. package/src/adapter/columnTypes.ts +327 -0
  72. package/src/adapter/columns.test.ts +75 -1
  73. package/src/adapter/columns.ts +178 -3
  74. package/src/adapter/filterOperators.test.ts +292 -0
  75. package/src/adapter/filterOperators.ts +524 -0
  76. package/src/adapter/pinning.ts +125 -0
  77. package/src/index.ts +15 -0
  78. package/src/numberCells.tsx +379 -0
  79. package/src/styles.ts +75 -0
  80. package/src/types.ts +114 -7
  81. package/src/useDataGridTable.test.ts +69 -0
  82. package/src/useDataGridTable.ts +127 -8
@@ -0,0 +1,327 @@
1
+ import type {
2
+ GridAlignment,
3
+ GridColDef,
4
+ GridColType,
5
+ GridSingleSelectColDef,
6
+ GridValidRowModel,
7
+ GridValueOption,
8
+ } from '../types';
9
+
10
+ const localeNumberFormat = new Intl.NumberFormat();
11
+ const localeDateFormat = new Intl.DateTimeFormat();
12
+ const localeDateTimeFormat = new Intl.DateTimeFormat(undefined, {
13
+ year: 'numeric',
14
+ month: 'numeric',
15
+ day: 'numeric',
16
+ hour: 'numeric',
17
+ minute: 'numeric',
18
+ second: 'numeric',
19
+ });
20
+
21
+ export interface DataGridColumnMeta<R extends GridValidRowModel = GridValidRowModel> {
22
+ colDef: GridColDef<R>;
23
+ getFilterText: (value: unknown, row: R) => string;
24
+ }
25
+
26
+ interface ValueRow {
27
+ getValue: (columnId: string) => unknown;
28
+ original: unknown;
29
+ getAllCells: () => readonly {
30
+ column: { id: string; columnDef: { meta?: unknown } };
31
+ }[];
32
+ }
33
+
34
+ function isDataGridColumnMeta(meta: unknown): meta is DataGridColumnMeta {
35
+ return (
36
+ typeof meta === 'object' &&
37
+ meta !== null &&
38
+ 'colDef' in meta &&
39
+ 'getFilterText' in meta &&
40
+ typeof (meta as { getFilterText: unknown }).getFilterText === 'function'
41
+ );
42
+ }
43
+
44
+ const DISPLAY_FILTER_TYPES: ReadonlySet<GridColType> = new Set([
45
+ 'number',
46
+ 'date',
47
+ 'dateTime',
48
+ 'boolean',
49
+ 'singleSelect',
50
+ ]);
51
+
52
+ export function usesDisplayFilter<R extends GridValidRowModel>(col: GridColDef<R>): boolean {
53
+ return col.type != null && DISPLAY_FILTER_TYPES.has(col.type);
54
+ }
55
+
56
+ export function metaUsesDisplayFilter(meta: unknown): boolean {
57
+ return isDataGridColumnMeta(meta) && usesDisplayFilter(meta.colDef);
58
+ }
59
+
60
+ export function columnSortFnName<R extends GridValidRowModel>(
61
+ col: GridColDef<R>,
62
+ ): 'text' | 'number' | 'datetime' | 'boolean' | 'selectLabel' | undefined {
63
+ switch (col.type) {
64
+ case 'string':
65
+ return 'text';
66
+ case 'number':
67
+ return 'number';
68
+ case 'date':
69
+ case 'dateTime':
70
+ return 'datetime';
71
+ case 'boolean':
72
+ return 'boolean';
73
+ case 'singleSelect':
74
+ return 'selectLabel';
75
+ default:
76
+ return undefined;
77
+ }
78
+ }
79
+
80
+ export function typeDefaultAlign(type: GridColType | undefined): GridAlignment | undefined {
81
+ if (type === 'number') {
82
+ return 'end';
83
+ }
84
+ if (type === 'boolean') {
85
+ return 'center';
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ function asValidDate(value: unknown): Date | null {
91
+ return value instanceof Date && Number.isFinite(value.getTime()) ? value : null;
92
+ }
93
+
94
+ function formatNumberCell(value: unknown): string {
95
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
96
+ return '';
97
+ }
98
+ return localeNumberFormat.format(value);
99
+ }
100
+
101
+ function formatDateCell(value: unknown): string {
102
+ const date = asValidDate(value);
103
+ return date ? localeDateFormat.format(date) : '';
104
+ }
105
+
106
+ function formatDateTimeCell(value: unknown): string {
107
+ const date = asValidDate(value);
108
+ return date ? localeDateTimeFormat.format(date) : '';
109
+ }
110
+
111
+ function stringifyCellValue(value: unknown): string {
112
+ if (typeof value === 'string') {
113
+ return value;
114
+ }
115
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
116
+ return value.toString();
117
+ }
118
+ return '';
119
+ }
120
+
121
+ function optionValue(option: GridValueOption): string | number {
122
+ return typeof option === 'object' ? option.value : option;
123
+ }
124
+
125
+ function optionLabel(option: GridValueOption): string {
126
+ if (typeof option === 'object') {
127
+ return option.label;
128
+ }
129
+ return typeof option === 'number' ? option.toString() : option;
130
+ }
131
+
132
+ function resolveValueOptions<R extends GridValidRowModel>(
133
+ col: GridSingleSelectColDef<R>,
134
+ row: R,
135
+ ): readonly GridValueOption[] {
136
+ const options = col.valueOptions;
137
+ return typeof options === 'function' ? options({ row }) : options;
138
+ }
139
+
140
+ function resolveSelectLabel<R extends GridValidRowModel>(
141
+ col: GridSingleSelectColDef<R>,
142
+ value: unknown,
143
+ row: R,
144
+ ): string {
145
+ const match = resolveValueOptions(col, row).find((option) => optionValue(option) === value);
146
+ if (match) {
147
+ return optionLabel(match);
148
+ }
149
+ return stringifyCellValue(value);
150
+ }
151
+
152
+ export function getColumnFilterText<R extends GridValidRowModel>(
153
+ col: GridColDef<R>,
154
+ value: unknown,
155
+ row: R,
156
+ ): string {
157
+ if (value == null) {
158
+ return '';
159
+ }
160
+ switch (col.type) {
161
+ case 'number':
162
+ return formatNumberCell(value);
163
+ case 'date':
164
+ return formatDateCell(value);
165
+ case 'dateTime':
166
+ return formatDateTimeCell(value);
167
+ case 'boolean':
168
+ return typeof value === 'boolean' ? (value ? 'Yes' : 'No') : stringifyCellValue(value);
169
+ case 'singleSelect':
170
+ return resolveSelectLabel(col, value, row);
171
+ case 'string':
172
+ return stringifyCellValue(value);
173
+ default:
174
+ return stringifyCellValue(value);
175
+ }
176
+ }
177
+
178
+ export function getTypedCellText<R extends GridValidRowModel>(
179
+ col: GridColDef<R> | undefined,
180
+ value: unknown,
181
+ row: R,
182
+ ): string | undefined {
183
+ if (!col?.type || col.type === 'boolean') {
184
+ return undefined;
185
+ }
186
+ if (value == null) {
187
+ return '';
188
+ }
189
+ return getColumnFilterText(col, value, row);
190
+ }
191
+
192
+ function compareNullish(a: unknown, b: unknown): number | undefined {
193
+ const aNull = a == null;
194
+ const bNull = b == null;
195
+ if (aNull && bNull) {
196
+ return 0;
197
+ }
198
+ if (aNull) {
199
+ return 1;
200
+ }
201
+ if (bNull) {
202
+ return -1;
203
+ }
204
+ return undefined;
205
+ }
206
+
207
+ function getColumnMeta(row: ValueRow, columnId: string): DataGridColumnMeta | undefined {
208
+ for (const cell of row.getAllCells()) {
209
+ if (cell.column.id === columnId) {
210
+ const meta = cell.column.columnDef.meta;
211
+ return isDataGridColumnMeta(meta) ? meta : undefined;
212
+ }
213
+ }
214
+ return undefined;
215
+ }
216
+
217
+ function filterTextForRow(row: ValueRow, columnId: string): string {
218
+ const value = row.getValue(columnId);
219
+ const meta = getColumnMeta(row, columnId);
220
+ if (meta) {
221
+ return meta.getFilterText(value, row.original as GridValidRowModel);
222
+ }
223
+ return stringifyCellValue(value);
224
+ }
225
+
226
+ export function normalizeFilterQuery(filterValue: unknown): string {
227
+ if (typeof filterValue !== 'string' && typeof filterValue !== 'number') {
228
+ return '';
229
+ }
230
+ return filterValue.toString().trim().toLowerCase();
231
+ }
232
+
233
+ function stringifyForIncludes(value: unknown): string {
234
+ if (typeof value === 'string') {
235
+ return value.toLowerCase();
236
+ }
237
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
238
+ return value.toString().toLowerCase();
239
+ }
240
+ if (value instanceof Date) {
241
+ return value.toString().toLowerCase();
242
+ }
243
+ if (Array.isArray(value)) {
244
+ return value.join(',').toLowerCase();
245
+ }
246
+ if (typeof value === 'object' && value !== null) {
247
+ return '[object object]';
248
+ }
249
+ if (typeof value === 'symbol' || typeof value === 'function') {
250
+ return value.toString().toLowerCase();
251
+ }
252
+ return '';
253
+ }
254
+
255
+ export function untypedCellIncludesQuery(value: unknown, query: string): boolean {
256
+ if (!query) {
257
+ return true;
258
+ }
259
+ if (value == null) {
260
+ return false;
261
+ }
262
+ return stringifyForIncludes(value).includes(query);
263
+ }
264
+
265
+ export function filterFn_displayText(
266
+ row: ValueRow,
267
+ columnId: string,
268
+ filterValue: unknown,
269
+ ): boolean {
270
+ const query = normalizeFilterQuery(filterValue);
271
+ if (!query) {
272
+ return true;
273
+ }
274
+ return filterTextForRow(row, columnId).toLowerCase().includes(query);
275
+ }
276
+
277
+ function toFiniteNumber(value: unknown): number | null {
278
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
279
+ }
280
+
281
+ function toTimestamp(value: unknown): number | null {
282
+ const date = asValidDate(value);
283
+ return date ? date.getTime() : null;
284
+ }
285
+
286
+ function compareFinite(a: number | null, b: number | null): number {
287
+ if (a == null && b == null) {
288
+ return 0;
289
+ }
290
+ if (a == null) {
291
+ return 1;
292
+ }
293
+ if (b == null) {
294
+ return -1;
295
+ }
296
+ return a - b;
297
+ }
298
+
299
+ export function sortFn_number(rowA: ValueRow, rowB: ValueRow, columnId: string): number {
300
+ return compareFinite(
301
+ toFiniteNumber(rowA.getValue(columnId)),
302
+ toFiniteNumber(rowB.getValue(columnId)),
303
+ );
304
+ }
305
+
306
+ export function sortFn_datetime(rowA: ValueRow, rowB: ValueRow, columnId: string): number {
307
+ return compareFinite(toTimestamp(rowA.getValue(columnId)), toTimestamp(rowB.getValue(columnId)));
308
+ }
309
+
310
+ export function sortFn_boolean(rowA: ValueRow, rowB: ValueRow, columnId: string): number {
311
+ const a = rowA.getValue(columnId);
312
+ const b = rowB.getValue(columnId);
313
+ return compareFinite(
314
+ typeof a === 'boolean' ? Number(a) : null,
315
+ typeof b === 'boolean' ? Number(b) : null,
316
+ );
317
+ }
318
+
319
+ export function sortFn_selectLabel(rowA: ValueRow, rowB: ValueRow, columnId: string): number {
320
+ const a = rowA.getValue(columnId);
321
+ const b = rowB.getValue(columnId);
322
+ const nullish = compareNullish(a, b);
323
+ if (nullish !== undefined) {
324
+ return nullish;
325
+ }
326
+ return filterTextForRow(rowA, columnId).localeCompare(filterTextForRow(rowB, columnId));
327
+ }
@@ -1,4 +1,12 @@
1
- import { applyUpdater, gridColDefToColumnDef, normalizeGridAlignment } from './columns';
1
+ import { SELECT_COLUMN_ID, type GridColDef } from '../types';
2
+ import {
3
+ applyUpdater,
4
+ gridColDefToColumnDef,
5
+ normalizeGridAlignment,
6
+ normalizePinnedColumns,
7
+ resolveColumnAlign,
8
+ resolveColumnHeaderAlign,
9
+ } from './columns';
2
10
 
3
11
  describe('gridColDefToColumnDef', () => {
4
12
  it('maps field, header, and sort/filter flags', () => {
@@ -23,6 +31,12 @@ describe('gridColDefToColumnDef', () => {
23
31
 
24
32
  expect(def.accessorFn?.({ name: 'operating' }, 0)).toBe('OPERATING');
25
33
  });
34
+
35
+ it('accepts the untyped column shape from the current release', () => {
36
+ const columns: GridColDef[] = [{ field: 'name' }, { field: 'id', type: 'string' }];
37
+
38
+ expect(columns.map((col) => col.field)).toEqual(['name', 'id']);
39
+ });
26
40
  });
27
41
 
28
42
  describe('applyUpdater', () => {
@@ -45,3 +59,63 @@ describe('normalizeGridAlignment', () => {
45
59
  expect(normalizeGridAlignment('right')).toBe('end');
46
60
  });
47
61
  });
62
+
63
+ describe('normalizePinnedColumns', () => {
64
+ const known = new Set(['id', 'name', 'actions']);
65
+
66
+ it('maps left/right aliases and merges with logical keys first', () => {
67
+ expect(
68
+ normalizePinnedColumns(
69
+ { start: ['id'], left: ['name', 'id'], end: ['actions'], right: ['name'] },
70
+ known,
71
+ false,
72
+ ),
73
+ ).toEqual({ start: ['id', 'name'], end: ['actions'] });
74
+ });
75
+
76
+ it('pins conflicts to start and ignores unknown fields', () => {
77
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
78
+ expect(
79
+ normalizePinnedColumns({ start: ['name'], end: ['name', 'missing'] }, known, false),
80
+ ).toEqual({ start: ['name'], end: [] });
81
+ expect(warn).toHaveBeenCalled();
82
+ warn.mockRestore();
83
+ });
84
+
85
+ it('maps __check__ and auto-pins the selection column when start is pinned', () => {
86
+ expect(normalizePinnedColumns({ left: ['__check__', 'name'] }, known, true)).toEqual({
87
+ start: [SELECT_COLUMN_ID, 'name'],
88
+ end: [],
89
+ });
90
+ expect(normalizePinnedColumns({ start: ['name'] }, known, true)).toEqual({
91
+ start: [SELECT_COLUMN_ID, 'name'],
92
+ end: [],
93
+ });
94
+ });
95
+ });
96
+
97
+ describe('resolveColumnAlign', () => {
98
+ it('uses type defaults only when align is omitted', () => {
99
+ expect(resolveColumnAlign({ field: 'name' })).toBe('start');
100
+ expect(resolveColumnAlign({ field: 'amount', type: 'number' })).toBe('end');
101
+ expect(resolveColumnAlign({ field: 'flag', type: 'boolean' })).toBe('center');
102
+ expect(resolveColumnAlign({ field: 'amount', type: 'number', align: 'start' })).toBe('start');
103
+ });
104
+ });
105
+
106
+ describe('resolveColumnHeaderAlign', () => {
107
+ it('prefers headerAlign, then align, then the type default', () => {
108
+ expect(resolveColumnHeaderAlign({ field: 'amount', type: 'number' })).toBe('end');
109
+ expect(resolveColumnHeaderAlign({ field: 'amount', type: 'number', align: 'start' })).toBe(
110
+ 'start',
111
+ );
112
+ expect(
113
+ resolveColumnHeaderAlign({
114
+ field: 'amount',
115
+ type: 'number',
116
+ align: 'start',
117
+ headerAlign: 'center',
118
+ }),
119
+ ).toBe('center');
120
+ });
121
+ });
@@ -1,7 +1,36 @@
1
1
  import type { TableAlignment } from '@cdx-ui/components';
2
- import type { GridAlignment, GridColDef, GridValidRowModel } from '../types';
2
+ import type { ColumnPinningState } from '@tanstack/react-table';
3
+ import {
4
+ MUI_CHECKBOX_FIELD,
5
+ SELECT_COLUMN_ID,
6
+ type GridAlignment,
7
+ type GridColDef,
8
+ type GridPinnedColumnFields,
9
+ type GridValidRowModel,
10
+ } from '../types';
11
+ import {
12
+ columnSortFnName,
13
+ filterFn_displayText,
14
+ getColumnFilterText,
15
+ normalizeFilterQuery,
16
+ untypedCellIncludesQuery,
17
+ usesDisplayFilter,
18
+ type DataGridColumnMeta,
19
+ typeDefaultAlign,
20
+ } from './columnTypes';
21
+ import {
22
+ isFilterValueGroup,
23
+ isOperatorFilterValue,
24
+ matchesOperatorFilter,
25
+ } from './filterOperators';
3
26
 
4
27
  export function gridColDefToColumnDef<R extends GridValidRowModel>(col: GridColDef<R>) {
28
+ const sortFn = columnSortFnName(col);
29
+ const meta: DataGridColumnMeta<R> = {
30
+ colDef: col,
31
+ getFilterText: (value, row) => getColumnFilterText(col, value, row),
32
+ };
33
+
5
34
  return {
6
35
  id: col.field,
7
36
  accessorFn: (row: R) => {
@@ -12,8 +41,31 @@ export function gridColDefToColumnDef<R extends GridValidRowModel>(col: GridColD
12
41
  enableSorting: col.sortable !== false,
13
42
  enableColumnFilter: col.filterable !== false,
14
43
  enableGlobalFilter: col.filterable !== false,
15
- filterFn: 'includesString' as const,
16
- meta: { colDef: col },
44
+ filterFn: (
45
+ row: Parameters<typeof filterFn_displayText>[0],
46
+ columnId: string,
47
+ filterValue: unknown,
48
+ ): boolean => {
49
+ const matches = (value: unknown): boolean => {
50
+ if (isOperatorFilterValue(value)) {
51
+ return matchesOperatorFilter(col, row.getValue(columnId), row.original as R, value);
52
+ }
53
+ if (usesDisplayFilter(col)) {
54
+ return filterFn_displayText(row, columnId, value);
55
+ }
56
+ return untypedCellIncludesQuery(row.getValue(columnId), normalizeFilterQuery(value));
57
+ };
58
+ return isFilterValueGroup(filterValue)
59
+ ? filterValue.items.every(matches)
60
+ : matches(filterValue);
61
+ },
62
+ ...(sortFn
63
+ ? {
64
+ sortFn,
65
+ sortUndefined: 'last' as const,
66
+ }
67
+ : {}),
68
+ meta,
17
69
  };
18
70
  }
19
71
 
@@ -30,3 +82,126 @@ export function normalizeGridAlignment(align?: GridAlignment): TableAlignment {
30
82
  }
31
83
  return align ?? 'start';
32
84
  }
85
+
86
+ function resolvePinnedFieldId(field: string): string {
87
+ return field === MUI_CHECKBOX_FIELD ? SELECT_COLUMN_ID : field;
88
+ }
89
+
90
+ const warnedUnknownPinnedFields = new Set<string>();
91
+ const warnedConflictPinnedFields = new Set<string>();
92
+
93
+ function warnUnknownPinnedField(raw: string): void {
94
+ if (!__DEV__ || warnedUnknownPinnedFields.has(raw)) {
95
+ return;
96
+ }
97
+ warnedUnknownPinnedFields.add(raw);
98
+ console.warn(`[DataGrid] Unknown pinned column field "${raw}" was ignored.`);
99
+ }
100
+
101
+ function warnConflictPinnedField(field: string): void {
102
+ if (!__DEV__ || warnedConflictPinnedFields.has(field)) {
103
+ return;
104
+ }
105
+ warnedConflictPinnedFields.add(field);
106
+ console.warn(
107
+ `[DataGrid] Column "${field}" is listed in both start and end pinned columns; pinning to start.`,
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Maps `pinnedColumns` (including deprecated `left` / `right` and MUI `__check__`)
113
+ * to TanStack `ColumnPinningState`. Logical keys win over aliases; start wins over end.
114
+ */
115
+ export function normalizePinnedColumns(
116
+ pinnedColumns: GridPinnedColumnFields | undefined,
117
+ knownFields: ReadonlySet<string>,
118
+ checkboxSelection: boolean,
119
+ ): ColumnPinningState {
120
+ if (pinnedColumns == null) {
121
+ return { start: [], end: [] };
122
+ }
123
+
124
+ const appendUnique = (
125
+ target: string[],
126
+ seen: Set<string>,
127
+ fields: readonly string[] | undefined,
128
+ ) => {
129
+ if (fields == null) {
130
+ return;
131
+ }
132
+ for (const raw of fields) {
133
+ const field = resolvePinnedFieldId(raw);
134
+ if (seen.has(field)) {
135
+ continue;
136
+ }
137
+ const isSelect = field === SELECT_COLUMN_ID;
138
+ if (isSelect ? !checkboxSelection : !knownFields.has(field)) {
139
+ warnUnknownPinnedField(raw);
140
+ continue;
141
+ }
142
+ seen.add(field);
143
+ target.push(field);
144
+ }
145
+ };
146
+
147
+ const startSeen = new Set<string>();
148
+ const start: string[] = [];
149
+ appendUnique(start, startSeen, pinnedColumns.start);
150
+ // Intentionally accept deprecated MUI X `left` / `right` aliases.
151
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- alias merge
152
+ appendUnique(start, startSeen, pinnedColumns.left);
153
+
154
+ const endSeen = new Set<string>();
155
+ const end: string[] = [];
156
+ const appendEnd = (fields: readonly string[] | undefined) => {
157
+ if (fields == null) {
158
+ return;
159
+ }
160
+ for (const raw of fields) {
161
+ const field = resolvePinnedFieldId(raw);
162
+ if (startSeen.has(field)) {
163
+ warnConflictPinnedField(field);
164
+ continue;
165
+ }
166
+ if (endSeen.has(field)) {
167
+ continue;
168
+ }
169
+ const isSelect = field === SELECT_COLUMN_ID;
170
+ if (isSelect ? !checkboxSelection : !knownFields.has(field)) {
171
+ warnUnknownPinnedField(raw);
172
+ continue;
173
+ }
174
+ endSeen.add(field);
175
+ end.push(field);
176
+ }
177
+ };
178
+ appendEnd(pinnedColumns.end);
179
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- alias merge
180
+ appendEnd(pinnedColumns.right);
181
+
182
+ if (checkboxSelection && start.length > 0) {
183
+ const withoutSelect = start.filter((field) => field !== SELECT_COLUMN_ID);
184
+ start.length = 0;
185
+ start.push(SELECT_COLUMN_ID, ...withoutSelect);
186
+ }
187
+
188
+ return { start, end };
189
+ }
190
+
191
+ export function resolveColumnAlign<R extends GridValidRowModel>(
192
+ col: GridColDef<R> | undefined,
193
+ ): TableAlignment {
194
+ if (!col) {
195
+ return 'start';
196
+ }
197
+ return normalizeGridAlignment(col.align ?? typeDefaultAlign(col.type));
198
+ }
199
+
200
+ export function resolveColumnHeaderAlign<R extends GridValidRowModel>(
201
+ col: GridColDef<R> | undefined,
202
+ ): TableAlignment {
203
+ if (!col) {
204
+ return 'start';
205
+ }
206
+ return normalizeGridAlignment(col.headerAlign ?? col.align ?? typeDefaultAlign(col.type));
207
+ }