@object-ui/plugin-grid 3.0.3 → 3.1.0
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/.turbo/turbo-build.log +12 -6
- package/dist/index.js +2169 -922
- package/dist/index.umd.cjs +9 -3
- package/dist/plugin-grid/src/FormulaBar.d.ts +29 -0
- package/dist/plugin-grid/src/GroupRow.d.ts +23 -0
- package/dist/plugin-grid/src/ImportWizard.d.ts +29 -0
- package/dist/plugin-grid/src/ObjectGrid.d.ts +1 -0
- package/dist/plugin-grid/src/SplitPaneGrid.d.ts +22 -0
- package/dist/plugin-grid/src/components/BulkActionBar.d.ts +12 -0
- package/dist/plugin-grid/src/components/RowActionMenu.d.ts +23 -0
- package/dist/plugin-grid/src/index.d.ts +22 -2
- package/dist/plugin-grid/src/useCellClipboard.d.ts +47 -0
- package/dist/plugin-grid/src/useColumnSummary.d.ts +25 -0
- package/dist/plugin-grid/src/useGradientColor.d.ts +37 -0
- package/dist/plugin-grid/src/useGroupReorder.d.ts +34 -0
- package/dist/plugin-grid/src/useGroupedData.d.ts +24 -3
- package/package.json +10 -10
- package/src/FormulaBar.tsx +151 -0
- package/src/GroupRow.tsx +69 -0
- package/src/ImportWizard.tsx +412 -0
- package/src/ListColumnExtensions.test.tsx +4 -5
- package/src/ObjectGrid.tsx +994 -139
- package/src/SplitPaneGrid.tsx +120 -0
- package/src/VirtualGrid.tsx +2 -2
- package/src/__tests__/GroupRow.test.tsx +206 -0
- package/src/__tests__/ImportPreview.test.tsx +171 -0
- package/src/__tests__/accessorKey-inference.test.tsx +132 -0
- package/src/__tests__/airtable-style.test.tsx +508 -0
- package/src/__tests__/column-features.test.tsx +490 -0
- package/src/__tests__/grid-export.test.tsx +121 -0
- package/src/__tests__/mobile-card-view.test.tsx +355 -0
- package/src/__tests__/objectdef-enrichment.test.tsx +566 -0
- package/src/__tests__/phase11-features.test.tsx +418 -0
- package/src/__tests__/row-bulk-actions.test.tsx +413 -0
- package/src/__tests__/row-height.test.tsx +160 -0
- package/src/__tests__/useGroupedData.test.ts +165 -0
- package/src/components/BulkActionBar.tsx +66 -0
- package/src/components/RowActionMenu.tsx +91 -0
- package/src/index.tsx +46 -2
- package/src/useCellClipboard.ts +136 -0
- package/src/useColumnSummary.ts +128 -0
- package/src/useGradientColor.ts +103 -0
- package/src/useGroupReorder.ts +123 -0
- package/src/useGroupedData.ts +69 -4
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Airtable-Style Grid Optimizations Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for auto-type inference (date, select, boolean, user),
|
|
5
|
+
* row number column, compact density, and frozen first column defaults.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { ObjectGrid } from '../ObjectGrid';
|
|
12
|
+
import { registerAllFields } from '@object-ui/fields';
|
|
13
|
+
import { ActionProvider } from '@object-ui/react';
|
|
14
|
+
import type { ListColumn } from '@object-ui/types';
|
|
15
|
+
|
|
16
|
+
registerAllFields();
|
|
17
|
+
|
|
18
|
+
// --- Mock Data with various field types ---
|
|
19
|
+
const mockData = [
|
|
20
|
+
{
|
|
21
|
+
_id: '1',
|
|
22
|
+
subject: 'Task Alpha',
|
|
23
|
+
status: 'In Progress',
|
|
24
|
+
priority: 'High',
|
|
25
|
+
category: 'Engineering',
|
|
26
|
+
due_date: '2026-02-20T03:46:37.982Z',
|
|
27
|
+
created_at: '2026-01-15T10:00:00.000Z',
|
|
28
|
+
is_completed: true,
|
|
29
|
+
assignee: 'Alice Smith',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
_id: '2',
|
|
33
|
+
subject: 'Task Beta',
|
|
34
|
+
status: 'Done',
|
|
35
|
+
priority: 'Low',
|
|
36
|
+
category: 'Design',
|
|
37
|
+
due_date: '2026-03-01T00:00:00.000Z',
|
|
38
|
+
created_at: '2026-01-20T10:00:00.000Z',
|
|
39
|
+
is_completed: false,
|
|
40
|
+
assignee: 'Bob Jones',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
_id: '3',
|
|
44
|
+
subject: 'Task Gamma',
|
|
45
|
+
status: 'To Do',
|
|
46
|
+
priority: 'Medium',
|
|
47
|
+
category: 'Engineering',
|
|
48
|
+
due_date: '2026-04-15T00:00:00.000Z',
|
|
49
|
+
created_at: '2026-02-01T10:00:00.000Z',
|
|
50
|
+
is_completed: false,
|
|
51
|
+
assignee: 'Charlie Brown',
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function renderGrid(columns: ListColumn[], opts?: Record<string, any>) {
|
|
56
|
+
const schema: any = {
|
|
57
|
+
type: 'object-grid' as const,
|
|
58
|
+
objectName: 'test_object',
|
|
59
|
+
columns,
|
|
60
|
+
data: { provider: 'value', items: mockData },
|
|
61
|
+
...opts,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return render(
|
|
65
|
+
<ActionProvider>
|
|
66
|
+
<ObjectGrid schema={schema} />
|
|
67
|
+
</ActionProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =========================================================================
|
|
72
|
+
// 1. Auto-type inference: Date fields
|
|
73
|
+
// =========================================================================
|
|
74
|
+
describe('Auto-type inference: Date fields', () => {
|
|
75
|
+
it('should format date fields containing "date" in name as human-readable dates', async () => {
|
|
76
|
+
renderGrid([
|
|
77
|
+
{ field: 'subject', label: 'Subject' },
|
|
78
|
+
{ field: 'due_date', label: 'Due Date' },
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Should NOT show raw ISO format
|
|
86
|
+
expect(screen.queryByText('2026-02-20T03:46:37.982Z')).not.toBeInTheDocument();
|
|
87
|
+
// Should show human-readable format (relative or absolute depending on distance from today)
|
|
88
|
+
// Date values are rendered in relative format by default
|
|
89
|
+
const cells = screen.getAllByRole('cell');
|
|
90
|
+
const dateCell = cells.find(cell => cell.querySelector('span.tabular-nums'));
|
|
91
|
+
expect(dateCell).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should format created_at fields as datetime', async () => {
|
|
95
|
+
renderGrid([
|
|
96
|
+
{ field: 'subject', label: 'Subject' },
|
|
97
|
+
{ field: 'created_at', label: 'Created At' },
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
await waitFor(() => {
|
|
101
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Should show human-readable datetime, not ISO string
|
|
105
|
+
expect(screen.queryByText('2026-01-15T10:00:00.000Z')).not.toBeInTheDocument();
|
|
106
|
+
// created_at now renders as datetime with split date/time display
|
|
107
|
+
expect(screen.getByText('1/15/2026')).toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should not override explicit type for date-like fields', async () => {
|
|
111
|
+
renderGrid([
|
|
112
|
+
{ field: 'subject', label: 'Subject' },
|
|
113
|
+
{ field: 'due_date', label: 'Due Date', type: 'text' },
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// With explicit type: 'text', should render as text
|
|
121
|
+
expect(screen.getByText('2026-02-20T03:46:37.982Z')).toBeInTheDocument();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// =========================================================================
|
|
126
|
+
// 2. Auto-type inference: Select/Badge fields
|
|
127
|
+
// =========================================================================
|
|
128
|
+
describe('Auto-type inference: Select/Badge fields', () => {
|
|
129
|
+
it('should render status field as badge when values are enumerable', async () => {
|
|
130
|
+
renderGrid([
|
|
131
|
+
{ field: 'subject', label: 'Subject' },
|
|
132
|
+
{ field: 'status', label: 'Status' },
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Status values should be present
|
|
140
|
+
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
|
141
|
+
expect(screen.getByText('Done')).toBeInTheDocument();
|
|
142
|
+
expect(screen.getByText('To Do')).toBeInTheDocument();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should render priority field as badge', async () => {
|
|
146
|
+
renderGrid([
|
|
147
|
+
{ field: 'subject', label: 'Subject' },
|
|
148
|
+
{ field: 'priority', label: 'Priority' },
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(screen.getByText('High')).toBeInTheDocument();
|
|
156
|
+
expect(screen.getByText('Low')).toBeInTheDocument();
|
|
157
|
+
expect(screen.getByText('Medium')).toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// =========================================================================
|
|
162
|
+
// 3. Auto-type inference: Boolean/Checkbox fields
|
|
163
|
+
// =========================================================================
|
|
164
|
+
describe('Auto-type inference: Boolean/Checkbox fields', () => {
|
|
165
|
+
it('should render is_completed as semantic completion indicator', async () => {
|
|
166
|
+
renderGrid([
|
|
167
|
+
{ field: 'subject', label: 'Subject' },
|
|
168
|
+
{ field: 'is_completed', label: 'Is Completed' },
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Should render green circle completion indicators (not checkboxes)
|
|
176
|
+
const indicators = screen.getAllByTestId('completion-indicator');
|
|
177
|
+
expect(indicators.length).toBeGreaterThan(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// =========================================================================
|
|
182
|
+
// 4. Row number column
|
|
183
|
+
// =========================================================================
|
|
184
|
+
describe('Row number column', () => {
|
|
185
|
+
it('should show row numbers by default in ObjectGrid', async () => {
|
|
186
|
+
renderGrid([
|
|
187
|
+
{ field: 'subject', label: 'Subject' },
|
|
188
|
+
{ field: 'status', label: 'Status' },
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Row numbers should be rendered (1, 2, 3)
|
|
196
|
+
expect(screen.getByText('1')).toBeInTheDocument();
|
|
197
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
198
|
+
expect(screen.getByText('3')).toBeInTheDocument();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// =========================================================================
|
|
203
|
+
// 5. Compact row density (default medium)
|
|
204
|
+
// =========================================================================
|
|
205
|
+
describe('Row density', () => {
|
|
206
|
+
it('should apply default medium density classes', async () => {
|
|
207
|
+
renderGrid([
|
|
208
|
+
{ field: 'subject', label: 'Subject' },
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
await waitFor(() => {
|
|
212
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Verify data-table is rendered (implicitly has the cellClassName applied)
|
|
216
|
+
expect(screen.getByText('Task Alpha')).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// =========================================================================
|
|
221
|
+
// 6. Frozen first column default
|
|
222
|
+
// =========================================================================
|
|
223
|
+
describe('Frozen first column', () => {
|
|
224
|
+
it('should default frozenColumns to 1', async () => {
|
|
225
|
+
renderGrid([
|
|
226
|
+
{ field: 'subject', label: 'Subject' },
|
|
227
|
+
{ field: 'status', label: 'Status' },
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
await waitFor(() => {
|
|
231
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// The first column header should have sticky class (indicating it's frozen)
|
|
235
|
+
const subjectHeader = screen.getByText('Subject').closest('th');
|
|
236
|
+
expect(subjectHeader).toHaveClass('sticky');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should not freeze columns when frozenColumns is explicitly 0', async () => {
|
|
240
|
+
renderGrid(
|
|
241
|
+
[
|
|
242
|
+
{ field: 'subject', label: 'Subject' },
|
|
243
|
+
{ field: 'status', label: 'Status' },
|
|
244
|
+
],
|
|
245
|
+
{ frozenColumns: 0 }
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const subjectHeader = screen.getByText('Subject').closest('th');
|
|
253
|
+
expect(subjectHeader).not.toHaveClass('sticky');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// =========================================================================
|
|
258
|
+
// 7. Column header type icons
|
|
259
|
+
// =========================================================================
|
|
260
|
+
describe('Column header type icons', () => {
|
|
261
|
+
it('should render type icons in column headers', async () => {
|
|
262
|
+
renderGrid([
|
|
263
|
+
{ field: 'subject', label: 'Subject' },
|
|
264
|
+
{ field: 'status', label: 'Status' },
|
|
265
|
+
{ field: 'due_date', label: 'Due Date' },
|
|
266
|
+
]);
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
269
|
+
});
|
|
270
|
+
// Each column header should have an SVG icon
|
|
271
|
+
const headers = screen.getAllByRole('columnheader');
|
|
272
|
+
// Filter out utility headers (row numbers, checkboxes, actions)
|
|
273
|
+
const dataHeaders = headers.filter(h => h.querySelector('svg'));
|
|
274
|
+
expect(dataHeaders.length).toBeGreaterThanOrEqual(3);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// =========================================================================
|
|
279
|
+
// 8. Datetime type inference
|
|
280
|
+
// =========================================================================
|
|
281
|
+
describe('Datetime type inference', () => {
|
|
282
|
+
it('should infer datetime type for created_at fields', async () => {
|
|
283
|
+
renderGrid([
|
|
284
|
+
{ field: 'subject', label: 'Subject' },
|
|
285
|
+
{ field: 'created_at', label: 'Created At' },
|
|
286
|
+
]);
|
|
287
|
+
await waitFor(() => {
|
|
288
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
289
|
+
});
|
|
290
|
+
// Should show split datetime format (date and time separately)
|
|
291
|
+
// The time part should be rendered in a separate muted span
|
|
292
|
+
const dateEl = screen.getByText('1/15/2026');
|
|
293
|
+
expect(dateEl).toBeInTheDocument();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// =========================================================================
|
|
298
|
+
// 9. Compound cell with prefix badge
|
|
299
|
+
// =========================================================================
|
|
300
|
+
describe('Compound cell with prefix badge', () => {
|
|
301
|
+
it('should render prefix badge before the main value', async () => {
|
|
302
|
+
renderGrid([
|
|
303
|
+
{ field: 'subject', label: 'Subject', prefix: { field: 'category', type: 'badge' } },
|
|
304
|
+
{ field: 'status', label: 'Status' },
|
|
305
|
+
]);
|
|
306
|
+
await waitFor(() => {
|
|
307
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
308
|
+
});
|
|
309
|
+
// The category values should appear as badges alongside subject
|
|
310
|
+
const engineeringBadges = screen.getAllByText('Engineering');
|
|
311
|
+
expect(engineeringBadges.length).toBeGreaterThanOrEqual(1);
|
|
312
|
+
expect(screen.getByText('Task Alpha')).toBeInTheDocument();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// =========================================================================
|
|
317
|
+
// 10. Add record row
|
|
318
|
+
// =========================================================================
|
|
319
|
+
describe('Add record row', () => {
|
|
320
|
+
it('should show add record row when operations.create is true', async () => {
|
|
321
|
+
renderGrid(
|
|
322
|
+
[
|
|
323
|
+
{ field: 'subject', label: 'Subject' },
|
|
324
|
+
{ field: 'status', label: 'Status' },
|
|
325
|
+
],
|
|
326
|
+
{ operations: { create: true } }
|
|
327
|
+
);
|
|
328
|
+
await waitFor(() => {
|
|
329
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
330
|
+
});
|
|
331
|
+
expect(screen.getByTestId('add-record-row')).toBeInTheDocument();
|
|
332
|
+
expect(screen.getByText('Add record')).toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should not show add record row by default', async () => {
|
|
336
|
+
renderGrid([
|
|
337
|
+
{ field: 'subject', label: 'Subject' },
|
|
338
|
+
{ field: 'status', label: 'Status' },
|
|
339
|
+
]);
|
|
340
|
+
await waitFor(() => {
|
|
341
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
342
|
+
});
|
|
343
|
+
expect(screen.queryByTestId('add-record-row')).not.toBeInTheDocument();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// =========================================================================
|
|
348
|
+
// 11. Primary field auto-link
|
|
349
|
+
// =========================================================================
|
|
350
|
+
describe('Primary field auto-link', () => {
|
|
351
|
+
it('should render first column cells as clickable links (primary field)', async () => {
|
|
352
|
+
renderGrid([
|
|
353
|
+
{ field: 'subject', label: 'Subject' },
|
|
354
|
+
{ field: 'status', label: 'Status' },
|
|
355
|
+
]);
|
|
356
|
+
await waitFor(() => {
|
|
357
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// First column cells should be rendered as buttons (links) with primary-field-link testid
|
|
361
|
+
const primaryLinks = screen.getAllByTestId('primary-field-link');
|
|
362
|
+
expect(primaryLinks.length).toBe(3); // 3 data rows
|
|
363
|
+
expect(primaryLinks[0]).toHaveTextContent('Task Alpha');
|
|
364
|
+
expect(primaryLinks[1]).toHaveTextContent('Task Beta');
|
|
365
|
+
expect(primaryLinks[2]).toHaveTextContent('Task Gamma');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should style primary field cells with font-medium and text-primary', async () => {
|
|
369
|
+
renderGrid([
|
|
370
|
+
{ field: 'subject', label: 'Subject' },
|
|
371
|
+
{ field: 'status', label: 'Status' },
|
|
372
|
+
]);
|
|
373
|
+
await waitFor(() => {
|
|
374
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const primaryLinks = screen.getAllByTestId('primary-field-link');
|
|
378
|
+
// Primary field links should have text-primary and font-medium classes
|
|
379
|
+
expect(primaryLinks[0]).toHaveClass('text-primary');
|
|
380
|
+
expect(primaryLinks[0]).toHaveClass('font-medium');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should not auto-link first column when it already has explicit link=true', async () => {
|
|
384
|
+
renderGrid([
|
|
385
|
+
{ field: 'subject', label: 'Subject', link: true },
|
|
386
|
+
{ field: 'status', label: 'Status' },
|
|
387
|
+
]);
|
|
388
|
+
await waitFor(() => {
|
|
389
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Still should have clickable buttons (via explicit link), but no primary-field-link testid
|
|
393
|
+
// because isPrimaryField is only true when !col.link && !col.action
|
|
394
|
+
const primaryLinks = screen.queryAllByTestId('primary-field-link');
|
|
395
|
+
expect(primaryLinks.length).toBe(0);
|
|
396
|
+
|
|
397
|
+
// But the cells should still be buttons (from col.link path)
|
|
398
|
+
const subjectCell = screen.getByText('Task Alpha');
|
|
399
|
+
expect(subjectCell.closest('button')).toBeTruthy();
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// =========================================================================
|
|
404
|
+
// 12. Empty value display
|
|
405
|
+
// =========================================================================
|
|
406
|
+
describe('Empty value display', () => {
|
|
407
|
+
it('should show styled empty indicator for null/empty values', async () => {
|
|
408
|
+
const dataWithEmpty = [
|
|
409
|
+
{ _id: '1', subject: 'Task Alpha', company: null },
|
|
410
|
+
{ _id: '2', subject: 'Task Beta', company: '' },
|
|
411
|
+
{ _id: '3', subject: 'Task Gamma', company: 'Acme' },
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
const schema: any = {
|
|
415
|
+
type: 'object-grid' as const,
|
|
416
|
+
objectName: 'test_object',
|
|
417
|
+
columns: [
|
|
418
|
+
{ field: 'subject', label: 'Subject' },
|
|
419
|
+
{ field: 'company', label: 'Company' },
|
|
420
|
+
],
|
|
421
|
+
data: { provider: 'value', items: dataWithEmpty },
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
render(
|
|
425
|
+
<ActionProvider>
|
|
426
|
+
<ObjectGrid schema={schema} />
|
|
427
|
+
</ActionProvider>
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
await waitFor(() => {
|
|
431
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Non-empty value should be displayed normally
|
|
435
|
+
expect(screen.getByText('Acme')).toBeInTheDocument();
|
|
436
|
+
|
|
437
|
+
// Empty values should show muted dash indicators
|
|
438
|
+
const emptyIndicators = screen.getAllByText('—');
|
|
439
|
+
expect(emptyIndicators.length).toBeGreaterThanOrEqual(2);
|
|
440
|
+
// Verify they have the styled classes
|
|
441
|
+
emptyIndicators.forEach(el => {
|
|
442
|
+
expect(el.className).toContain('italic');
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// =========================================================================
|
|
448
|
+
// 13. Record detail panel (form-based)
|
|
449
|
+
// =========================================================================
|
|
450
|
+
describe('Record detail panel', () => {
|
|
451
|
+
it('should render form-based record detail with renderRecordDetail', async () => {
|
|
452
|
+
const onRowClick = vi.fn();
|
|
453
|
+
renderGrid(
|
|
454
|
+
[
|
|
455
|
+
{ field: 'subject', label: 'Subject' },
|
|
456
|
+
{ field: 'status', label: 'Status' },
|
|
457
|
+
],
|
|
458
|
+
{
|
|
459
|
+
navigation: { mode: 'drawer' },
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
await waitFor(() => {
|
|
464
|
+
expect(screen.getByText('Subject')).toBeInTheDocument();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// The grid should render successfully with navigation configured
|
|
468
|
+
expect(screen.getByText('Task Alpha')).toBeInTheDocument();
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// =========================================================================
|
|
473
|
+
// 13. Auto-type inference: Currency/Amount fields
|
|
474
|
+
// =========================================================================
|
|
475
|
+
describe('Auto-type inference: Currency/Amount fields', () => {
|
|
476
|
+
const currencyData = [
|
|
477
|
+
{ _id: '1', name: 'Order 1', total_amount: 15459.99 },
|
|
478
|
+
{ _id: '2', name: 'Order 2', total_amount: 289.50 },
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
function renderCurrencyGrid(columns: ListColumn[]) {
|
|
482
|
+
const schema: any = {
|
|
483
|
+
type: 'object-grid' as const,
|
|
484
|
+
objectName: 'test_object',
|
|
485
|
+
columns,
|
|
486
|
+
data: { provider: 'value', items: currencyData },
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
return render(
|
|
490
|
+
<ActionProvider>
|
|
491
|
+
<ObjectGrid schema={schema} />
|
|
492
|
+
</ActionProvider>
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
it('should auto-infer currency type for amount fields and format values', async () => {
|
|
497
|
+
renderCurrencyGrid([
|
|
498
|
+
{ field: 'name', label: 'Name' },
|
|
499
|
+
{ field: 'total_amount', label: 'Amount' },
|
|
500
|
+
]);
|
|
501
|
+
await waitFor(() => {
|
|
502
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
503
|
+
});
|
|
504
|
+
// Should show formatted currency (e.g. "$15,459.99") instead of raw "15459.99"
|
|
505
|
+
expect(screen.queryByText('15459.99')).not.toBeInTheDocument();
|
|
506
|
+
expect(screen.getByText(/15,459\.99/)).toBeInTheDocument();
|
|
507
|
+
});
|
|
508
|
+
});
|