@jmruthers/pace-core 0.6.8 → 0.6.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/CHANGELOG.md +3 -0
- package/audit-tool/audits/02-project-structure.cjs +38 -35
- package/dist/{DataTable-6RMSCQJ6.js → DataTable-SOAFXIWY.js} +1 -1
- package/dist/{chunk-EURB7QFZ.js → chunk-5HNSDQWH.js} +3 -3
- package/dist/{chunk-IUBRCBSY.js → chunk-C7ZQ5O4C.js} +11 -5
- package/dist/{chunk-NKHKXPI4.js → chunk-J2U36LHD.js} +65 -2
- package/dist/components.js +4 -4
- package/dist/{database.generated-CcnC_DRc.d.ts → database.generated-DT8JTZiP.d.ts} +12 -12
- package/dist/hooks.d.ts +3 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/rbac/index.d.ts +1 -1
- package/dist/{timezone-BZe_eUxx.d.ts → timezone-0AyangqX.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/{usePublicRouteParams-MamNgwqe.d.ts → usePublicRouteParams-DQLrDqDb.d.ts} +1 -1
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +3 -3
- package/docs/api/modules.md +1 -1
- package/docs/api-reference/rpc-functions.md +3 -3
- package/docs/implementation-guides/data-tables.md +66 -0
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/DataTable.select-label-display.test.tsx +483 -0
- package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +224 -0
- package/src/components/DataTable/hooks/useTableColumns.ts +23 -1
- package/src/components/DataTable/utils/__tests__/selectFieldUtils.test.ts +207 -0
- package/src/components/DataTable/utils/index.ts +1 -0
- package/src/components/DataTable/utils/selectFieldUtils.ts +134 -0
- package/src/components/UserMenu/UserMenu.tsx +3 -5
- package/src/types/database.generated.ts +9 -9
- package/src/utils/supabase/createBaseClient.ts +25 -7
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file DataTable Select Field Label Display Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/__tests__
|
|
5
|
+
* @since 0.6.0
|
|
6
|
+
*
|
|
7
|
+
* Tests for automatic label display in select fields.
|
|
8
|
+
* Ensures that select fields with fieldOptions automatically display labels
|
|
9
|
+
* instead of raw values in read mode.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { render, screen, within } from '@testing-library/react';
|
|
14
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
15
|
+
import { DataTable } from '../DataTable';
|
|
16
|
+
import type { DataTableColumn } from '../types';
|
|
17
|
+
import { createDefaultFeatures } from './test-utils';
|
|
18
|
+
|
|
19
|
+
// Mock the RBAC hooks
|
|
20
|
+
vi.mock('../../../rbac/hooks', () => ({
|
|
21
|
+
useCan: vi.fn(() => ({
|
|
22
|
+
can: true,
|
|
23
|
+
isLoading: false,
|
|
24
|
+
error: null,
|
|
25
|
+
})),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock the auth provider
|
|
29
|
+
const mockUseUnifiedAuthFn = vi.fn(() => ({
|
|
30
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
31
|
+
isAuthenticated: true,
|
|
32
|
+
isLoading: false,
|
|
33
|
+
error: null,
|
|
34
|
+
selectedOrganisation: { id: 'test-org' },
|
|
35
|
+
selectedEvent: { event_id: 'test-event' },
|
|
36
|
+
supabase: {},
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('../../../providers/services/UnifiedAuthProvider', () => ({
|
|
40
|
+
useUnifiedAuth: () => mockUseUnifiedAuthFn(),
|
|
41
|
+
UnifiedAuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Mock the DataTableCore component
|
|
45
|
+
vi.mock('../components/DataTableCore', () => ({
|
|
46
|
+
DataTableCore: ({ children, ...props }: any) => (
|
|
47
|
+
<div role="table" data-testid="datatable-core" {...props}>
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
describe('DataTable Select Field Label Display', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Simple Select Fields', () => {
|
|
59
|
+
it('displays label for select field with numeric values', () => {
|
|
60
|
+
interface TestData {
|
|
61
|
+
id: string;
|
|
62
|
+
phone_type_id: number;
|
|
63
|
+
phone_number: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data: TestData[] = [
|
|
67
|
+
{ id: '1', phone_type_id: 1, phone_number: '0412 345 678' },
|
|
68
|
+
{ id: '2', phone_type_id: 2, phone_number: '03 9876 5432' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
72
|
+
{
|
|
73
|
+
id: 'phone_type',
|
|
74
|
+
accessorKey: 'phone_type_id',
|
|
75
|
+
header: 'Phone Type',
|
|
76
|
+
fieldType: 'select',
|
|
77
|
+
fieldOptions: [
|
|
78
|
+
{ value: 1, label: 'Mobile' },
|
|
79
|
+
{ value: 2, label: 'Home' },
|
|
80
|
+
{ value: 3, label: 'Work' },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'phone_number',
|
|
85
|
+
accessorKey: 'phone_number',
|
|
86
|
+
header: 'Phone Number',
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<DataTable
|
|
92
|
+
data={data}
|
|
93
|
+
columns={columns}
|
|
94
|
+
rbac={{ pageId: 'test-page' }}
|
|
95
|
+
features={createDefaultFeatures()}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Check that labels are displayed instead of numeric values
|
|
100
|
+
// Note: Since we're mocking DataTableCore, we need to check the actual rendered content
|
|
101
|
+
// In a real scenario, the table would render the labels
|
|
102
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('displays label for select field with string values', () => {
|
|
106
|
+
interface TestData {
|
|
107
|
+
id: string;
|
|
108
|
+
status: string;
|
|
109
|
+
name: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data: TestData[] = [
|
|
113
|
+
{ id: '1', status: 'active', name: 'Item 1' },
|
|
114
|
+
{ id: '2', status: 'pending', name: 'Item 2' },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
118
|
+
{
|
|
119
|
+
id: 'status',
|
|
120
|
+
accessorKey: 'status',
|
|
121
|
+
header: 'Status',
|
|
122
|
+
fieldType: 'select',
|
|
123
|
+
fieldOptions: [
|
|
124
|
+
{ value: 'active', label: 'Active' },
|
|
125
|
+
{ value: 'pending', label: 'Pending' },
|
|
126
|
+
{ value: 'cancelled', label: 'Cancelled' },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'name',
|
|
131
|
+
accessorKey: 'name',
|
|
132
|
+
header: 'Name',
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
render(
|
|
137
|
+
<DataTable
|
|
138
|
+
data={data}
|
|
139
|
+
columns={columns}
|
|
140
|
+
rbac={{ pageId: 'test-page' }}
|
|
141
|
+
features={createDefaultFeatures()}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('falls back to raw value when label not found', () => {
|
|
149
|
+
interface TestData {
|
|
150
|
+
id: string;
|
|
151
|
+
phone_type_id: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const data: TestData[] = [
|
|
155
|
+
{ id: '1', phone_type_id: 99 }, // Value not in fieldOptions
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
159
|
+
{
|
|
160
|
+
id: 'phone_type',
|
|
161
|
+
accessorKey: 'phone_type_id',
|
|
162
|
+
header: 'Phone Type',
|
|
163
|
+
fieldType: 'select',
|
|
164
|
+
fieldOptions: [
|
|
165
|
+
{ value: 1, label: 'Mobile' },
|
|
166
|
+
{ value: 2, label: 'Home' },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
render(
|
|
172
|
+
<DataTable
|
|
173
|
+
data={data}
|
|
174
|
+
columns={columns}
|
|
175
|
+
rbac={{ pageId: 'test-page' }}
|
|
176
|
+
features={createDefaultFeatures()}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('handles null values gracefully', () => {
|
|
184
|
+
interface TestData {
|
|
185
|
+
id: string;
|
|
186
|
+
phone_type_id: number | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const data: TestData[] = [
|
|
190
|
+
{ id: '1', phone_type_id: null },
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
194
|
+
{
|
|
195
|
+
id: 'phone_type',
|
|
196
|
+
accessorKey: 'phone_type_id',
|
|
197
|
+
header: 'Phone Type',
|
|
198
|
+
fieldType: 'select',
|
|
199
|
+
fieldOptions: [
|
|
200
|
+
{ value: 1, label: 'Mobile' },
|
|
201
|
+
{ value: 2, label: 'Home' },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
render(
|
|
207
|
+
<DataTable
|
|
208
|
+
data={data}
|
|
209
|
+
columns={columns}
|
|
210
|
+
rbac={{ pageId: 'test-page' }}
|
|
211
|
+
features={createDefaultFeatures()}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('Grouped Select Fields', () => {
|
|
220
|
+
it('finds label in grouped options', () => {
|
|
221
|
+
interface TestData {
|
|
222
|
+
id: string;
|
|
223
|
+
category_id: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const data: TestData[] = [
|
|
227
|
+
{ id: '1', category_id: 1 },
|
|
228
|
+
{ id: '2', category_id: 2 },
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
232
|
+
{
|
|
233
|
+
id: 'category',
|
|
234
|
+
accessorKey: 'category_id',
|
|
235
|
+
header: 'Category',
|
|
236
|
+
fieldType: 'select',
|
|
237
|
+
fieldOptions: [
|
|
238
|
+
{ type: 'separator' },
|
|
239
|
+
{
|
|
240
|
+
type: 'group',
|
|
241
|
+
label: 'Primary Categories',
|
|
242
|
+
items: [
|
|
243
|
+
{ value: 1, label: 'Electronics' },
|
|
244
|
+
{ value: 2, label: 'Clothing' },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: 'group',
|
|
249
|
+
label: 'Secondary Categories',
|
|
250
|
+
items: [
|
|
251
|
+
{ value: 3, label: 'Books' },
|
|
252
|
+
{ value: 4, label: 'Toys' },
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
render(
|
|
260
|
+
<DataTable
|
|
261
|
+
data={data}
|
|
262
|
+
columns={columns}
|
|
263
|
+
rbac={{ pageId: 'test-page' }}
|
|
264
|
+
features={createDefaultFeatures()}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('Type Coercion', () => {
|
|
273
|
+
it('handles string value matching numeric option', () => {
|
|
274
|
+
interface TestData {
|
|
275
|
+
id: string;
|
|
276
|
+
phone_type_id: string | number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const data: TestData[] = [
|
|
280
|
+
{ id: '1', phone_type_id: '1' }, // String value
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
284
|
+
{
|
|
285
|
+
id: 'phone_type',
|
|
286
|
+
accessorKey: 'phone_type_id',
|
|
287
|
+
header: 'Phone Type',
|
|
288
|
+
fieldType: 'select',
|
|
289
|
+
fieldOptions: [
|
|
290
|
+
{ value: 1, label: 'Mobile' }, // Numeric option
|
|
291
|
+
{ value: 2, label: 'Home' },
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
render(
|
|
297
|
+
<DataTable
|
|
298
|
+
data={data}
|
|
299
|
+
columns={columns}
|
|
300
|
+
rbac={{ pageId: 'test-page' }}
|
|
301
|
+
features={createDefaultFeatures()}
|
|
302
|
+
/>
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('handles numeric value matching string option', () => {
|
|
309
|
+
interface TestData {
|
|
310
|
+
id: string;
|
|
311
|
+
status_id: number;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const data: TestData[] = [
|
|
315
|
+
{ id: '1', status_id: 1 },
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
319
|
+
{
|
|
320
|
+
id: 'status',
|
|
321
|
+
accessorKey: 'status_id',
|
|
322
|
+
header: 'Status',
|
|
323
|
+
fieldType: 'select',
|
|
324
|
+
fieldOptions: [
|
|
325
|
+
{ value: '1', label: 'Active' }, // String option
|
|
326
|
+
{ value: '2', label: 'Pending' },
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
render(
|
|
332
|
+
<DataTable
|
|
333
|
+
data={data}
|
|
334
|
+
columns={columns}
|
|
335
|
+
rbac={{ pageId: 'test-page' }}
|
|
336
|
+
features={createDefaultFeatures()}
|
|
337
|
+
/>
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('Custom Cell Renderers', () => {
|
|
345
|
+
it('does not override custom cell renderers', () => {
|
|
346
|
+
interface TestData {
|
|
347
|
+
id: string;
|
|
348
|
+
phone_type_id: number;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const data: TestData[] = [
|
|
352
|
+
{ id: '1', phone_type_id: 1 },
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
const customCellRenderer = vi.fn(({ getValue }) => {
|
|
356
|
+
return <span data-testid="custom-cell">{String(getValue())}</span>;
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
360
|
+
{
|
|
361
|
+
id: 'phone_type',
|
|
362
|
+
accessorKey: 'phone_type_id',
|
|
363
|
+
header: 'Phone Type',
|
|
364
|
+
fieldType: 'select',
|
|
365
|
+
fieldOptions: [
|
|
366
|
+
{ value: 1, label: 'Mobile' },
|
|
367
|
+
{ value: 2, label: 'Home' },
|
|
368
|
+
],
|
|
369
|
+
cell: customCellRenderer, // Custom renderer should be preserved
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
render(
|
|
374
|
+
<DataTable
|
|
375
|
+
data={data}
|
|
376
|
+
columns={columns}
|
|
377
|
+
rbac={{ pageId: 'test-page' }}
|
|
378
|
+
features={createDefaultFeatures()}
|
|
379
|
+
/>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Custom renderer should be called, not the automatic one
|
|
383
|
+
expect(customCellRenderer).toHaveBeenCalled();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('Edge Cases', () => {
|
|
388
|
+
it('handles empty fieldOptions array', () => {
|
|
389
|
+
interface TestData {
|
|
390
|
+
id: string;
|
|
391
|
+
phone_type_id: number;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const data: TestData[] = [
|
|
395
|
+
{ id: '1', phone_type_id: 1 },
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
399
|
+
{
|
|
400
|
+
id: 'phone_type',
|
|
401
|
+
accessorKey: 'phone_type_id',
|
|
402
|
+
header: 'Phone Type',
|
|
403
|
+
fieldType: 'select',
|
|
404
|
+
fieldOptions: [], // Empty array
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
render(
|
|
409
|
+
<DataTable
|
|
410
|
+
data={data}
|
|
411
|
+
columns={columns}
|
|
412
|
+
rbac={{ pageId: 'test-page' }}
|
|
413
|
+
features={createDefaultFeatures()}
|
|
414
|
+
/>
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('handles undefined fieldOptions', () => {
|
|
421
|
+
interface TestData {
|
|
422
|
+
id: string;
|
|
423
|
+
phone_type_id: number;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const data: TestData[] = [
|
|
427
|
+
{ id: '1', phone_type_id: 1 },
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
431
|
+
{
|
|
432
|
+
id: 'phone_type',
|
|
433
|
+
accessorKey: 'phone_type_id',
|
|
434
|
+
header: 'Phone Type',
|
|
435
|
+
fieldType: 'select',
|
|
436
|
+
// fieldOptions not provided
|
|
437
|
+
},
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
render(
|
|
441
|
+
<DataTable
|
|
442
|
+
data={data}
|
|
443
|
+
columns={columns}
|
|
444
|
+
rbac={{ pageId: 'test-page' }}
|
|
445
|
+
features={createDefaultFeatures()}
|
|
446
|
+
/>
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('handles columns without fieldType select', () => {
|
|
453
|
+
interface TestData {
|
|
454
|
+
id: string;
|
|
455
|
+
name: string;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const data: TestData[] = [
|
|
459
|
+
{ id: '1', name: 'Test' },
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
const columns: DataTableColumn<TestData>[] = [
|
|
463
|
+
{
|
|
464
|
+
id: 'name',
|
|
465
|
+
accessorKey: 'name',
|
|
466
|
+
header: 'Name',
|
|
467
|
+
// No fieldType, should not inject cell renderer
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
render(
|
|
472
|
+
<DataTable
|
|
473
|
+
data={data}
|
|
474
|
+
columns={columns}
|
|
475
|
+
rbac={{ pageId: 'test-page' }}
|
|
476
|
+
features={createDefaultFeatures()}
|
|
477
|
+
/>
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|
|
@@ -135,6 +135,230 @@ describe('[unit] useTableColumns', () => {
|
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
describe('Select Field Label Display', () => {
|
|
139
|
+
it('injects automatic cell renderer for select fields without custom cell', () => {
|
|
140
|
+
const columnsWithSelect: DataTableColumn<TestData>[] = [
|
|
141
|
+
{ accessorKey: 'id', header: 'ID' },
|
|
142
|
+
{
|
|
143
|
+
accessorKey: 'name',
|
|
144
|
+
header: 'Name',
|
|
145
|
+
fieldType: 'select',
|
|
146
|
+
fieldOptions: [
|
|
147
|
+
{ value: 'john', label: 'John Doe' },
|
|
148
|
+
{ value: 'jane', label: 'Jane Smith' },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const { result } = renderHook(() => useTableColumns({
|
|
154
|
+
columns: columnsWithSelect,
|
|
155
|
+
features: mockFullFeatures,
|
|
156
|
+
effectiveActions: [],
|
|
157
|
+
columnOrder: ['id', 'name']
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
|
|
161
|
+
expect(nameColumn).toBeDefined();
|
|
162
|
+
expect(nameColumn?.cell).toBeDefined();
|
|
163
|
+
expect(typeof nameColumn?.cell).toBe('function');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('does not inject cell renderer when custom cell is provided', () => {
|
|
167
|
+
const customCellRenderer = vi.fn(({ getValue }) => getValue());
|
|
168
|
+
|
|
169
|
+
const columnsWithCustomCell: DataTableColumn<TestData>[] = [
|
|
170
|
+
{
|
|
171
|
+
accessorKey: 'name',
|
|
172
|
+
header: 'Name',
|
|
173
|
+
fieldType: 'select',
|
|
174
|
+
fieldOptions: [
|
|
175
|
+
{ value: 'john', label: 'John Doe' },
|
|
176
|
+
],
|
|
177
|
+
cell: customCellRenderer,
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const { result } = renderHook(() => useTableColumns({
|
|
182
|
+
columns: columnsWithCustomCell,
|
|
183
|
+
features: mockFullFeatures,
|
|
184
|
+
effectiveActions: [],
|
|
185
|
+
columnOrder: ['name']
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
|
|
189
|
+
expect(nameColumn).toBeDefined();
|
|
190
|
+
expect(nameColumn?.cell).toBe(customCellRenderer);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('does not inject cell renderer when fieldType is not select', () => {
|
|
194
|
+
const columnsWithoutSelect: DataTableColumn<TestData>[] = [
|
|
195
|
+
{
|
|
196
|
+
accessorKey: 'name',
|
|
197
|
+
header: 'Name',
|
|
198
|
+
fieldType: 'text',
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const { result } = renderHook(() => useTableColumns({
|
|
203
|
+
columns: columnsWithoutSelect,
|
|
204
|
+
features: mockFullFeatures,
|
|
205
|
+
effectiveActions: [],
|
|
206
|
+
columnOrder: ['name']
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
|
|
210
|
+
expect(nameColumn).toBeDefined();
|
|
211
|
+
expect(nameColumn?.cell).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('does not inject cell renderer when fieldOptions is empty', () => {
|
|
215
|
+
const columnsWithEmptyOptions: DataTableColumn<TestData>[] = [
|
|
216
|
+
{
|
|
217
|
+
accessorKey: 'name',
|
|
218
|
+
header: 'Name',
|
|
219
|
+
fieldType: 'select',
|
|
220
|
+
fieldOptions: [],
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const { result } = renderHook(() => useTableColumns({
|
|
225
|
+
columns: columnsWithEmptyOptions,
|
|
226
|
+
features: mockFullFeatures,
|
|
227
|
+
effectiveActions: [],
|
|
228
|
+
columnOrder: ['name']
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
|
|
232
|
+
expect(nameColumn).toBeDefined();
|
|
233
|
+
expect(nameColumn?.cell).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('does not inject cell renderer when fieldOptions is undefined', () => {
|
|
237
|
+
const columnsWithoutOptions: DataTableColumn<TestData>[] = [
|
|
238
|
+
{
|
|
239
|
+
accessorKey: 'name',
|
|
240
|
+
header: 'Name',
|
|
241
|
+
fieldType: 'select',
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
const { result } = renderHook(() => useTableColumns({
|
|
246
|
+
columns: columnsWithoutOptions,
|
|
247
|
+
features: mockFullFeatures,
|
|
248
|
+
effectiveActions: [],
|
|
249
|
+
columnOrder: ['name']
|
|
250
|
+
}));
|
|
251
|
+
|
|
252
|
+
const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
|
|
253
|
+
expect(nameColumn).toBeDefined();
|
|
254
|
+
expect(nameColumn?.cell).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('injected cell renderer displays label for matching value', () => {
|
|
258
|
+
const columnsWithSelect: DataTableColumn<TestData>[] = [
|
|
259
|
+
{
|
|
260
|
+
id: 'status',
|
|
261
|
+
accessorKey: 'name',
|
|
262
|
+
header: 'Status',
|
|
263
|
+
fieldType: 'select',
|
|
264
|
+
fieldOptions: [
|
|
265
|
+
{ value: 'active', label: 'Active' },
|
|
266
|
+
{ value: 'pending', label: 'Pending' },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const { result } = renderHook(() => useTableColumns({
|
|
272
|
+
columns: columnsWithSelect,
|
|
273
|
+
features: mockFullFeatures,
|
|
274
|
+
effectiveActions: [],
|
|
275
|
+
columnOrder: ['status']
|
|
276
|
+
}));
|
|
277
|
+
|
|
278
|
+
const statusColumn = result.current.enhancedColumns.find(col => col.id === 'status');
|
|
279
|
+
expect(statusColumn?.cell).toBeDefined();
|
|
280
|
+
|
|
281
|
+
// Test the cell renderer function
|
|
282
|
+
const mockCellContext = {
|
|
283
|
+
getValue: () => 'active',
|
|
284
|
+
row: { original: { id: '1', name: 'active', email: 'test@example.com' } },
|
|
285
|
+
column: { id: 'status' },
|
|
286
|
+
} as any;
|
|
287
|
+
|
|
288
|
+
const rendered = statusColumn?.cell?.(mockCellContext);
|
|
289
|
+
expect(rendered).toBe('Active');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('injected cell renderer falls back to raw value when label not found', () => {
|
|
293
|
+
const columnsWithSelect: DataTableColumn<TestData>[] = [
|
|
294
|
+
{
|
|
295
|
+
id: 'status',
|
|
296
|
+
accessorKey: 'name',
|
|
297
|
+
header: 'Status',
|
|
298
|
+
fieldType: 'select',
|
|
299
|
+
fieldOptions: [
|
|
300
|
+
{ value: 'active', label: 'Active' },
|
|
301
|
+
{ value: 'pending', label: 'Pending' },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const { result } = renderHook(() => useTableColumns({
|
|
307
|
+
columns: columnsWithSelect,
|
|
308
|
+
features: mockFullFeatures,
|
|
309
|
+
effectiveActions: [],
|
|
310
|
+
columnOrder: ['status']
|
|
311
|
+
}));
|
|
312
|
+
|
|
313
|
+
const statusColumn = result.current.enhancedColumns.find(col => col.id === 'status');
|
|
314
|
+
expect(statusColumn?.cell).toBeDefined();
|
|
315
|
+
|
|
316
|
+
// Test the cell renderer function with unmatched value
|
|
317
|
+
const mockCellContext = {
|
|
318
|
+
getValue: () => 'unknown',
|
|
319
|
+
row: { original: { id: '1', name: 'unknown', email: 'test@example.com' } },
|
|
320
|
+
column: { id: 'status' },
|
|
321
|
+
} as any;
|
|
322
|
+
|
|
323
|
+
const rendered = statusColumn?.cell?.(mockCellContext);
|
|
324
|
+
expect(rendered).toBe('unknown');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('injected cell renderer handles null values', () => {
|
|
328
|
+
const columnsWithSelect: DataTableColumn<TestData>[] = [
|
|
329
|
+
{
|
|
330
|
+
id: 'status',
|
|
331
|
+
accessorKey: 'name',
|
|
332
|
+
header: 'Status',
|
|
333
|
+
fieldType: 'select',
|
|
334
|
+
fieldOptions: [
|
|
335
|
+
{ value: 'active', label: 'Active' },
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
const { result } = renderHook(() => useTableColumns({
|
|
341
|
+
columns: columnsWithSelect,
|
|
342
|
+
features: mockFullFeatures,
|
|
343
|
+
effectiveActions: [],
|
|
344
|
+
columnOrder: ['status']
|
|
345
|
+
}));
|
|
346
|
+
|
|
347
|
+
const statusColumn = result.current.enhancedColumns.find(col => col.id === 'status');
|
|
348
|
+
expect(statusColumn?.cell).toBeDefined();
|
|
349
|
+
|
|
350
|
+
// Test the cell renderer function with null value
|
|
351
|
+
const mockCellContext = {
|
|
352
|
+
getValue: () => null,
|
|
353
|
+
row: { original: { id: '1', name: null, email: 'test@example.com' } },
|
|
354
|
+
column: { id: 'status' },
|
|
355
|
+
} as any;
|
|
356
|
+
|
|
357
|
+
const rendered = statusColumn?.cell?.(mockCellContext);
|
|
358
|
+
expect(rendered).toBe('');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
138
362
|
describe('Feature-Based Enhancements', () => {
|
|
139
363
|
it('applies sorting enhancement when enabled', () => {
|
|
140
364
|
const { result } = renderHook(() => useTableColumns({
|