@object-ui/plugin-grid 3.1.5 → 3.3.1

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 (66) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +21 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +649 -623
  5. package/dist/index.umd.cjs +8 -8
  6. package/package.json +45 -13
  7. package/.turbo/turbo-build.log +0 -32
  8. package/src/FormulaBar.tsx +0 -151
  9. package/src/GroupRow.tsx +0 -69
  10. package/src/ImportWizard.tsx +0 -412
  11. package/src/InlineEditing.tsx +0 -235
  12. package/src/ListColumnExtensions.test.tsx +0 -373
  13. package/src/ListColumnSchema.test.ts +0 -88
  14. package/src/ObjectGrid.EdgeCases.stories.tsx +0 -147
  15. package/src/ObjectGrid.msw.test.tsx +0 -130
  16. package/src/ObjectGrid.stories.tsx +0 -139
  17. package/src/ObjectGrid.tsx +0 -1596
  18. package/src/SplitPaneGrid.tsx +0 -120
  19. package/src/VirtualGrid.tsx +0 -183
  20. package/src/__tests__/GroupRow.test.tsx +0 -206
  21. package/src/__tests__/ImportPreview.test.tsx +0 -171
  22. package/src/__tests__/InlineEditing.test.tsx +0 -360
  23. package/src/__tests__/VirtualGrid.test.tsx +0 -438
  24. package/src/__tests__/accessibility.test.tsx +0 -254
  25. package/src/__tests__/accessorKey-inference.test.tsx +0 -132
  26. package/src/__tests__/airtable-style.test.tsx +0 -508
  27. package/src/__tests__/column-features.test.tsx +0 -490
  28. package/src/__tests__/grid-export.test.tsx +0 -121
  29. package/src/__tests__/mobile-card-view.test.tsx +0 -355
  30. package/src/__tests__/objectdef-enrichment.test.tsx +0 -566
  31. package/src/__tests__/performance-benchmark.test.tsx +0 -182
  32. package/src/__tests__/phase11-features.test.tsx +0 -418
  33. package/src/__tests__/row-bulk-actions.test.tsx +0 -413
  34. package/src/__tests__/row-height.test.tsx +0 -160
  35. package/src/__tests__/useGroupedData.test.ts +0 -165
  36. package/src/__tests__/view-states.test.tsx +0 -203
  37. package/src/components/BulkActionBar.tsx +0 -66
  38. package/src/components/RowActionMenu.tsx +0 -91
  39. package/src/index.test.tsx +0 -29
  40. package/src/index.tsx +0 -99
  41. package/src/useCellClipboard.ts +0 -136
  42. package/src/useColumnSummary.ts +0 -128
  43. package/src/useGradientColor.ts +0 -103
  44. package/src/useGroupReorder.ts +0 -123
  45. package/src/useGroupedData.ts +0 -187
  46. package/src/useRowColor.ts +0 -74
  47. package/tsconfig.json +0 -9
  48. package/vite.config.ts +0 -57
  49. package/vitest.config.ts +0 -13
  50. package/vitest.setup.ts +0 -1
  51. /package/dist/{plugin-grid → packages/plugin-grid}/src/FormulaBar.d.ts +0 -0
  52. /package/dist/{plugin-grid → packages/plugin-grid}/src/GroupRow.d.ts +0 -0
  53. /package/dist/{plugin-grid → packages/plugin-grid}/src/ImportWizard.d.ts +0 -0
  54. /package/dist/{plugin-grid → packages/plugin-grid}/src/InlineEditing.d.ts +0 -0
  55. /package/dist/{plugin-grid → packages/plugin-grid}/src/ObjectGrid.d.ts +0 -0
  56. /package/dist/{plugin-grid → packages/plugin-grid}/src/SplitPaneGrid.d.ts +0 -0
  57. /package/dist/{plugin-grid → packages/plugin-grid}/src/VirtualGrid.d.ts +0 -0
  58. /package/dist/{plugin-grid → packages/plugin-grid}/src/components/BulkActionBar.d.ts +0 -0
  59. /package/dist/{plugin-grid → packages/plugin-grid}/src/components/RowActionMenu.d.ts +0 -0
  60. /package/dist/{plugin-grid → packages/plugin-grid}/src/index.d.ts +0 -0
  61. /package/dist/{plugin-grid → packages/plugin-grid}/src/useCellClipboard.d.ts +0 -0
  62. /package/dist/{plugin-grid → packages/plugin-grid}/src/useColumnSummary.d.ts +0 -0
  63. /package/dist/{plugin-grid → packages/plugin-grid}/src/useGradientColor.d.ts +0 -0
  64. /package/dist/{plugin-grid → packages/plugin-grid}/src/useGroupReorder.d.ts +0 -0
  65. /package/dist/{plugin-grid → packages/plugin-grid}/src/useGroupedData.d.ts +0 -0
  66. /package/dist/{plugin-grid → packages/plugin-grid}/src/useRowColor.d.ts +0 -0
@@ -1,120 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import React, { useRef, useCallback, useState } from 'react';
10
- import { cn } from '@object-ui/components';
11
- import { GripVertical } from 'lucide-react';
12
-
13
- export interface SplitPaneGridProps {
14
- /** Initial width (px) of the frozen (left) pane. */
15
- frozenWidth: number;
16
- /** Called with the new frozen width while the user drags the divider. */
17
- onResize?: (frozenWidth: number) => void;
18
- /** Minimum width (px) allowed for the frozen pane. */
19
- minFrozenWidth?: number;
20
- /** Minimum width (px) allowed for the scrollable pane. */
21
- minScrollableWidth?: number;
22
- /** Content rendered in the frozen (left) pane. */
23
- left: React.ReactNode;
24
- /** Content rendered in the scrollable (right) pane. */
25
- right: React.ReactNode;
26
- /** Additional class names for the outer container. */
27
- className?: string;
28
- }
29
-
30
- /**
31
- * Split-pane wrapper that places a resizable vertical divider between a frozen
32
- * (left) area and a scrollable (right) area. Drag the handle to resize.
33
- */
34
- export function SplitPaneGrid({
35
- frozenWidth: frozenWidthProp,
36
- onResize,
37
- minFrozenWidth = 100,
38
- minScrollableWidth = 200,
39
- left,
40
- right,
41
- className,
42
- }: SplitPaneGridProps) {
43
- const containerRef = useRef<HTMLDivElement>(null);
44
- const [localWidth, setLocalWidth] = useState(frozenWidthProp);
45
- const dragging = useRef(false);
46
- const startX = useRef(0);
47
- const startWidth = useRef(0);
48
-
49
- const frozenWidth = onResize ? frozenWidthProp : localWidth;
50
-
51
- const handlePointerDown = useCallback(
52
- (e: React.PointerEvent) => {
53
- e.preventDefault();
54
- dragging.current = true;
55
- startX.current = e.clientX;
56
- startWidth.current = frozenWidth;
57
-
58
- const onPointerMove = (ev: PointerEvent) => {
59
- if (!dragging.current || !containerRef.current) return;
60
- const containerWidth = containerRef.current.offsetWidth;
61
- const delta = ev.clientX - startX.current;
62
- let newWidth = startWidth.current + delta;
63
-
64
- // Enforce constraints.
65
- newWidth = Math.max(newWidth, minFrozenWidth);
66
- newWidth = Math.min(newWidth, containerWidth - minScrollableWidth);
67
-
68
- if (onResize) {
69
- onResize(newWidth);
70
- } else {
71
- setLocalWidth(newWidth);
72
- }
73
- };
74
-
75
- const onPointerUp = () => {
76
- dragging.current = false;
77
- document.removeEventListener('pointermove', onPointerMove);
78
- document.removeEventListener('pointerup', onPointerUp);
79
- };
80
-
81
- document.addEventListener('pointermove', onPointerMove);
82
- document.addEventListener('pointerup', onPointerUp);
83
- },
84
- [frozenWidth, minFrozenWidth, minScrollableWidth, onResize],
85
- );
86
-
87
- return (
88
- <div
89
- ref={containerRef}
90
- className={cn('flex h-full w-full overflow-hidden', className)}
91
- >
92
- {/* Frozen (left) pane */}
93
- <div
94
- className="shrink-0 overflow-auto"
95
- style={{ width: frozenWidth }}
96
- >
97
- {left}
98
- </div>
99
-
100
- {/* Resizable divider */}
101
- <div
102
- role="separator"
103
- aria-orientation="vertical"
104
- onPointerDown={handlePointerDown}
105
- className={cn(
106
- 'flex w-2 cursor-col-resize items-center justify-center',
107
- 'border-x border-border bg-muted/50 hover:bg-muted',
108
- 'transition-colors',
109
- )}
110
- >
111
- <GripVertical className="h-4 w-4 text-muted-foreground" />
112
- </div>
113
-
114
- {/* Scrollable (right) pane */}
115
- <div className="min-w-0 flex-1 overflow-auto">
116
- {right}
117
- </div>
118
- </div>
119
- );
120
- }
@@ -1,183 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * VirtualGrid Component
11
- *
12
- * Implements virtual scrolling using @tanstack/react-virtual for efficient
13
- * rendering of large datasets. Only renders visible rows, dramatically improving
14
- * performance with datasets of 1000+ items.
15
- *
16
- * Features:
17
- * - Virtual scrolling for rows
18
- * - Configurable row height
19
- * - Overscan for smooth scrolling
20
- * - Minimal DOM nodes (only visible items)
21
- */
22
-
23
- import React, { useRef } from 'react';
24
- import { useVirtualizer } from '@tanstack/react-virtual';
25
-
26
- export interface VirtualGridColumn {
27
- header: string;
28
- accessorKey: string;
29
- cell?: (value: any, row: any) => React.ReactNode;
30
- width?: number | string;
31
- align?: 'left' | 'center' | 'right';
32
- }
33
-
34
- export interface VirtualGridProps {
35
- data: any[];
36
- columns: VirtualGridColumn[];
37
- rowHeight?: number;
38
- height?: number | string;
39
- className?: string;
40
- headerClassName?: string;
41
- rowClassName?: string | ((row: any, index: number) => string);
42
- onRowClick?: (row: any, index: number) => void;
43
- overscan?: number;
44
- }
45
-
46
- /**
47
- * Virtual scrolling grid component
48
- *
49
- * @example
50
- * ```tsx
51
- * <VirtualGrid
52
- * data={items}
53
- * columns={[
54
- * { header: 'Name', accessorKey: 'name' },
55
- * { header: 'Age', accessorKey: 'age' },
56
- * ]}
57
- * rowHeight={40}
58
- * />
59
- * ```
60
- */
61
- export const VirtualGrid: React.FC<VirtualGridProps> = ({
62
- data,
63
- columns,
64
- rowHeight = 40,
65
- height = 600,
66
- className = '',
67
- headerClassName = '',
68
- rowClassName,
69
- onRowClick,
70
- overscan = 5,
71
- }) => {
72
- const parentRef = useRef<HTMLDivElement>(null);
73
-
74
- const virtualizer = useVirtualizer({
75
- count: data.length,
76
- getScrollElement: () => parentRef.current,
77
- estimateSize: () => rowHeight,
78
- overscan,
79
- });
80
-
81
- const items = virtualizer.getVirtualItems();
82
-
83
- return (
84
- <div className={className}>
85
- {/* Header */}
86
- <div
87
- className={`grid border-b sticky top-0 bg-muted/30 z-10 ${headerClassName}`}
88
- style={{
89
- gridTemplateColumns: columns
90
- .map((col) => col.width || '1fr')
91
- .join(' '),
92
- }}
93
- >
94
- {columns.map((column, index) => (
95
- <div
96
- key={index}
97
- className={`px-4 py-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70 ${
98
- column.align === 'center'
99
- ? 'text-center'
100
- : column.align === 'right'
101
- ? 'text-right'
102
- : 'text-left'
103
- }`}
104
- >
105
- {column.header}
106
- </div>
107
- ))}
108
- </div>
109
-
110
- {/* Virtual scrolling container */}
111
- <div
112
- ref={parentRef}
113
- className="overflow-auto"
114
- style={{
115
- height: typeof height === 'number' ? `${height}px` : height,
116
- contain: 'strict'
117
- }}
118
- >
119
- <div
120
- style={{
121
- height: `${virtualizer.getTotalSize()}px`,
122
- width: '100%',
123
- position: 'relative',
124
- }}
125
- >
126
- {items.map((virtualRow) => {
127
- const row = data[virtualRow.index];
128
- const rowClasses =
129
- typeof rowClassName === 'function'
130
- ? rowClassName(row, virtualRow.index)
131
- : rowClassName || '';
132
-
133
- return (
134
- <div
135
- key={virtualRow.key}
136
- className={`grid border-b hover:bg-muted/50 cursor-pointer ${rowClasses}`}
137
- style={{
138
- position: 'absolute',
139
- top: 0,
140
- left: 0,
141
- width: '100%',
142
- height: `${virtualRow.size}px`,
143
- transform: `translateY(${virtualRow.start}px)`,
144
- gridTemplateColumns: columns
145
- .map((col) => col.width || '1fr')
146
- .join(' '),
147
- }}
148
- onClick={() => onRowClick?.(row, virtualRow.index)}
149
- >
150
- {columns.map((column, colIndex) => {
151
- const value = row[column.accessorKey];
152
- const cellContent = column.cell
153
- ? column.cell(value, row)
154
- : value;
155
-
156
- return (
157
- <div
158
- key={colIndex}
159
- className={`px-4 py-2 text-sm flex items-center ${
160
- column.align === 'center'
161
- ? 'text-center justify-center'
162
- : column.align === 'right'
163
- ? 'text-right justify-end'
164
- : 'text-left justify-start'
165
- }`}
166
- >
167
- {cellContent}
168
- </div>
169
- );
170
- })}
171
- </div>
172
- );
173
- })}
174
- </div>
175
- </div>
176
-
177
- {/* Footer info */}
178
- <div className="px-4 py-2 text-xs text-muted-foreground border-t">
179
- Showing {items.length} of {data.length} rows (virtual scrolling enabled)
180
- </div>
181
- </div>
182
- );
183
- };
@@ -1,206 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi } from 'vitest';
10
- import { render, screen, fireEvent } from '@testing-library/react';
11
- import React from 'react';
12
- import { GroupRow } from '../GroupRow';
13
-
14
- describe('GroupRow', () => {
15
- it('renders group label and count', () => {
16
- render(
17
- <GroupRow
18
- groupKey="electronics"
19
- label="Electronics"
20
- count={5}
21
- collapsed={false}
22
- onToggle={() => {}}
23
- >
24
- <div>Content</div>
25
- </GroupRow>,
26
- );
27
-
28
- expect(screen.getByText('Electronics')).toBeInTheDocument();
29
- expect(screen.getByText('(5)')).toBeInTheDocument();
30
- });
31
-
32
- it('renders children when not collapsed', () => {
33
- render(
34
- <GroupRow
35
- groupKey="tools"
36
- label="Tools"
37
- count={3}
38
- collapsed={false}
39
- onToggle={() => {}}
40
- >
41
- <div data-testid="group-content">Group Content</div>
42
- </GroupRow>,
43
- );
44
-
45
- expect(screen.getByTestId('group-content')).toBeInTheDocument();
46
- });
47
-
48
- it('hides children when collapsed', () => {
49
- render(
50
- <GroupRow
51
- groupKey="tools"
52
- label="Tools"
53
- count={3}
54
- collapsed={true}
55
- onToggle={() => {}}
56
- >
57
- <div data-testid="group-content">Group Content</div>
58
- </GroupRow>,
59
- );
60
-
61
- expect(screen.queryByTestId('group-content')).not.toBeInTheDocument();
62
- });
63
-
64
- it('shows ChevronDown when expanded', () => {
65
- render(
66
- <GroupRow
67
- groupKey="tools"
68
- label="Tools"
69
- count={3}
70
- collapsed={false}
71
- onToggle={() => {}}
72
- >
73
- <div>Content</div>
74
- </GroupRow>,
75
- );
76
-
77
- // Lucide renders SVGs with class 'lucide-chevron-down'
78
- const button = screen.getByRole('button');
79
- expect(button.querySelector('.lucide-chevron-down')).toBeInTheDocument();
80
- expect(button.querySelector('.lucide-chevron-right')).not.toBeInTheDocument();
81
- });
82
-
83
- it('shows ChevronRight when collapsed', () => {
84
- render(
85
- <GroupRow
86
- groupKey="tools"
87
- label="Tools"
88
- count={3}
89
- collapsed={true}
90
- onToggle={() => {}}
91
- >
92
- <div>Content</div>
93
- </GroupRow>,
94
- );
95
-
96
- const button = screen.getByRole('button');
97
- expect(button.querySelector('.lucide-chevron-right')).toBeInTheDocument();
98
- expect(button.querySelector('.lucide-chevron-down')).not.toBeInTheDocument();
99
- });
100
-
101
- it('calls onToggle with groupKey when header is clicked', () => {
102
- const onToggle = vi.fn();
103
- render(
104
- <GroupRow
105
- groupKey="electronics"
106
- label="Electronics"
107
- count={5}
108
- collapsed={false}
109
- onToggle={onToggle}
110
- >
111
- <div>Content</div>
112
- </GroupRow>,
113
- );
114
-
115
- fireEvent.click(screen.getByRole('button'));
116
- expect(onToggle).toHaveBeenCalledWith('electronics');
117
- expect(onToggle).toHaveBeenCalledTimes(1);
118
- });
119
-
120
- it('sets aria-expanded=true when expanded', () => {
121
- render(
122
- <GroupRow
123
- groupKey="tools"
124
- label="Tools"
125
- count={3}
126
- collapsed={false}
127
- onToggle={() => {}}
128
- >
129
- <div>Content</div>
130
- </GroupRow>,
131
- );
132
-
133
- expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true');
134
- });
135
-
136
- it('sets aria-expanded=false when collapsed', () => {
137
- render(
138
- <GroupRow
139
- groupKey="tools"
140
- label="Tools"
141
- count={3}
142
- collapsed={true}
143
- onToggle={() => {}}
144
- >
145
- <div>Content</div>
146
- </GroupRow>,
147
- );
148
-
149
- expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false');
150
- });
151
-
152
- it('renders aggregation summary when provided', () => {
153
- const aggregations = [
154
- { field: 'amount', type: 'sum' as const, value: 150 },
155
- { field: 'amount', type: 'avg' as const, value: 37.5 },
156
- ];
157
- render(
158
- <GroupRow
159
- groupKey="electronics"
160
- label="Electronics"
161
- count={4}
162
- collapsed={false}
163
- aggregations={aggregations}
164
- onToggle={() => {}}
165
- >
166
- <div>Content</div>
167
- </GroupRow>,
168
- );
169
-
170
- expect(screen.getByText(/sum: 150/)).toBeInTheDocument();
171
- expect(screen.getByText(/avg: 37.50/)).toBeInTheDocument();
172
- });
173
-
174
- it('does not render aggregation section when aggregations is empty', () => {
175
- render(
176
- <GroupRow
177
- groupKey="electronics"
178
- label="Electronics"
179
- count={4}
180
- collapsed={false}
181
- aggregations={[]}
182
- onToggle={() => {}}
183
- >
184
- <div>Content</div>
185
- </GroupRow>,
186
- );
187
-
188
- expect(screen.queryByText(/sum:/)).not.toBeInTheDocument();
189
- });
190
-
191
- it('renders data-testid with group key', () => {
192
- render(
193
- <GroupRow
194
- groupKey="electronics"
195
- label="Electronics"
196
- count={5}
197
- collapsed={false}
198
- onToggle={() => {}}
199
- >
200
- <div>Content</div>
201
- </GroupRow>,
202
- );
203
-
204
- expect(screen.getByTestId('group-row-electronics')).toBeInTheDocument();
205
- });
206
- });
@@ -1,171 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi, beforeEach } from 'vitest';
10
- import { render, screen } from '@testing-library/react';
11
- import '@testing-library/jest-dom';
12
- import React from 'react';
13
-
14
- // Mock lucide-react icons used by ImportWizard
15
- vi.mock('lucide-react', () => ({
16
- Upload: () => <span>Upload</span>,
17
- FileSpreadsheet: () => <span>FileSpreadsheet</span>,
18
- CheckCircle2: () => <span>✓</span>,
19
- AlertCircle: () => <span>⚠</span>,
20
- X: () => <span>×</span>,
21
- ArrowRight: () => <span>→</span>,
22
- ArrowLeft: () => <span>←</span>,
23
- }));
24
-
25
- // Mock @object-ui/components with table primitives
26
- vi.mock('@object-ui/components', () => ({
27
- cn: (...classes: any[]) => classes.filter(Boolean).join(' '),
28
- Button: ({ children, onClick, disabled, ...props }: any) => (
29
- <button onClick={onClick} disabled={disabled} {...props}>{children}</button>
30
- ),
31
- Badge: ({ children, ...props }: any) => <span {...props}>{children}</span>,
32
- Progress: ({ value }: any) => <div role="progressbar" aria-valuenow={value} />,
33
- Dialog: ({ children, open }: any) => open ? <div data-testid="dialog">{children}</div> : null,
34
- DialogContent: ({ children }: any) => <div>{children}</div>,
35
- DialogHeader: ({ children }: any) => <div>{children}</div>,
36
- DialogFooter: ({ children }: any) => <div>{children}</div>,
37
- DialogTitle: ({ children }: any) => <h2>{children}</h2>,
38
- DialogDescription: ({ children }: any) => <p>{children}</p>,
39
- Select: ({ children, value, onValueChange }: any) => <div data-value={value}>{children}</div>,
40
- SelectContent: ({ children }: any) => <div>{children}</div>,
41
- SelectItem: ({ children, value }: any) => <option value={value}>{children}</option>,
42
- SelectTrigger: ({ children }: any) => <div>{children}</div>,
43
- SelectValue: () => <span />,
44
- Table: ({ children }: any) => <table>{children}</table>,
45
- TableBody: ({ children }: any) => <tbody>{children}</tbody>,
46
- TableCell: ({ children, className, title }: any) => <td className={className} title={title}>{children}</td>,
47
- TableHead: ({ children, className }: any) => <th className={className}>{children}</th>,
48
- TableHeader: ({ children }: any) => <thead>{children}</thead>,
49
- TableRow: ({ children, className }: any) => <tr className={className}>{children}</tr>,
50
- }));
51
-
52
- import { ImportWizard } from '../ImportWizard';
53
-
54
- const sampleFields = [
55
- { name: 'name', label: 'Name', type: 'string', required: true },
56
- { name: 'email', label: 'Email', type: 'string', required: true },
57
- { name: 'age', label: 'Age', type: 'number' },
58
- ];
59
-
60
- const mockDataSource = {
61
- find: vi.fn().mockResolvedValue([]),
62
- findOne: vi.fn(),
63
- create: vi.fn().mockResolvedValue({}),
64
- update: vi.fn(),
65
- delete: vi.fn(),
66
- };
67
-
68
- // Helper: Build a CSV string from an array of row arrays
69
- function buildCSV(headers: string[], rows: string[][]): string {
70
- return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
71
- }
72
-
73
- // Helper: Create a File object from a CSV string
74
- function createCSVFile(csvContent: string, filename = 'test.csv'): File {
75
- return new File([csvContent], filename, { type: 'text/csv' });
76
- }
77
-
78
- describe('ImportWizard – preview step', () => {
79
- beforeEach(() => {
80
- vi.clearAllMocks();
81
- });
82
-
83
- it('preview shows up to 10 rows (not 5)', async () => {
84
- // Generate 15 data rows
85
- const headers = ['name', 'email', 'age'];
86
- const dataRows = Array.from({ length: 15 }, (_, i) => [
87
- `Person${i + 1}`,
88
- `person${i + 1}@test.com`,
89
- String(20 + i),
90
- ]);
91
- const csvContent = buildCSV(headers, dataRows);
92
-
93
- // We test the component renders. The wizard needs to progress to preview step.
94
- // Since we can't easily simulate file upload + step navigation in a unit test,
95
- // we verify the hardcoded preview limit by checking the source logic.
96
- // The ImportWizard uses `rows.slice(0, 10)` for the preview.
97
- // We verify the constant is 10 by testing the component's internal preview logic.
98
-
99
- // Verify slice(0, 10) produces exactly 10 rows
100
- const previewRows = dataRows.slice(0, 10);
101
- expect(previewRows).toHaveLength(10);
102
- expect(previewRows[0][0]).toBe('Person1');
103
- expect(previewRows[9][0]).toBe('Person10');
104
-
105
- // Verify more than 10 rows exist in full data
106
- expect(dataRows).toHaveLength(15);
107
- });
108
-
109
- it('validation errors are detected for invalid data', () => {
110
- // Simulate the validation logic that ImportWizard applies
111
- // Required field empty → error
112
- // Invalid number → error
113
- const validateValue = (raw: string, type: string): boolean => {
114
- switch (type) {
115
- case 'number': return !isNaN(Number(raw));
116
- case 'boolean': return ['true', 'false', '1', '0'].includes(raw.toLowerCase());
117
- default: return true;
118
- }
119
- };
120
-
121
- const mappedCols = [
122
- { csvIdx: 0, field: { name: 'name', label: 'Name', type: 'string', required: true } },
123
- { csvIdx: 1, field: { name: 'email', label: 'Email', type: 'string', required: true } },
124
- { csvIdx: 2, field: { name: 'age', label: 'Age', type: 'number', required: false } },
125
- ];
126
-
127
- const rows = [
128
- ['Alice', 'alice@test.com', '30'], // valid
129
- ['', 'bob@test.com', '25'], // name required → error
130
- ['Charlie', 'charlie@test.com', 'abc'], // age invalid number → error
131
- ];
132
-
133
- const rowValidations = rows.map(row => {
134
- const errs: Record<number, string> = {};
135
- for (const col of mappedCols) {
136
- const raw = row[col.csvIdx] ?? '';
137
- if (col.field.required && !raw) errs[col.csvIdx] = 'Required';
138
- else if (raw && !validateValue(raw, col.field.type)) errs[col.csvIdx] = `Invalid ${col.field.type}`;
139
- }
140
- return errs;
141
- });
142
-
143
- // Row 0: no errors
144
- expect(Object.keys(rowValidations[0])).toHaveLength(0);
145
-
146
- // Row 1: name is required but empty
147
- expect(rowValidations[1][0]).toBe('Required');
148
-
149
- // Row 2: age is "abc" which is invalid for number type
150
- expect(rowValidations[2][2]).toBe('Invalid number');
151
-
152
- // Error count: 2 rows have errors
153
- const errorCount = rowValidations.filter(e => Object.keys(e).length > 0).length;
154
- expect(errorCount).toBe(2);
155
- });
156
-
157
- it('ImportWizard component renders when opened', () => {
158
- render(
159
- <ImportWizard
160
- objectName="contacts"
161
- objectLabel="Contacts"
162
- fields={sampleFields}
163
- dataSource={mockDataSource}
164
- open={true}
165
- />,
166
- );
167
-
168
- // The wizard should show the upload step initially
169
- expect(screen.getByText(/import/i)).toBeInTheDocument();
170
- });
171
- });