@jmruthers/pace-core 0.5.126 → 0.5.128

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 (180) hide show
  1. package/dist/{DataTable-6FN7XDXA.js → DataTable-3Z5HLOWF.js} +6 -6
  2. package/dist/{PublicLoadingSpinner-CaoRbHvJ.d.ts → PublicLoadingSpinner-CUAnTvcg.d.ts} +41 -21
  3. package/dist/{UnifiedAuthProvider-6C47WIML.js → UnifiedAuthProvider-CQDZRJIS.js} +3 -3
  4. package/dist/{chunk-QXGLU2O5.js → chunk-27MGXDD6.js} +282 -147
  5. package/dist/chunk-27MGXDD6.js.map +1 -0
  6. package/dist/{chunk-ZBLK676C.js → chunk-3CG5L6RN.js} +1 -19
  7. package/dist/chunk-3CG5L6RN.js.map +1 -0
  8. package/dist/{chunk-35ZDPMBM.js → chunk-BYXRHAIF.js} +3 -3
  9. package/dist/{chunk-IJOZZOGT.js → chunk-CQZU6TFE.js} +5 -5
  10. package/dist/{chunk-C43QIDN3.js → chunk-CTJRBUX2.js} +2 -2
  11. package/dist/{chunk-R4CRQUJJ.js → chunk-ENE3AB75.js} +463 -453
  12. package/dist/chunk-ENE3AB75.js.map +1 -0
  13. package/dist/{chunk-ESJTIADP.js → chunk-F64FFPOZ.js} +5 -15
  14. package/dist/{chunk-ESJTIADP.js.map → chunk-F64FFPOZ.js.map} +1 -1
  15. package/dist/{chunk-4MXVZVNS.js → chunk-TGIY2AR2.js} +2 -2
  16. package/dist/{chunk-XN6GWKMV.js → chunk-VZ5OR6HD.js} +161 -14
  17. package/dist/chunk-VZ5OR6HD.js.map +1 -0
  18. package/dist/{chunk-QWNJCQXZ.js → chunk-ZV77RZMU.js} +2 -2
  19. package/dist/{chunk-NZGLXZGP.js → chunk-ZYZCRSBD.js} +3 -54
  20. package/dist/chunk-ZYZCRSBD.js.map +1 -0
  21. package/dist/components.d.ts +1 -1
  22. package/dist/components.js +9 -9
  23. package/dist/hooks.js +7 -7
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +12 -12
  26. package/dist/providers.js +2 -2
  27. package/dist/rbac/index.js +7 -7
  28. package/dist/utils.d.ts +1 -1
  29. package/dist/utils.js +1 -1
  30. package/docs/api/classes/ColumnFactory.md +1 -1
  31. package/docs/api/classes/ErrorBoundary.md +1 -1
  32. package/docs/api/classes/InvalidScopeError.md +1 -1
  33. package/docs/api/classes/MissingUserContextError.md +1 -1
  34. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  35. package/docs/api/classes/PermissionDeniedError.md +1 -1
  36. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  37. package/docs/api/classes/RBACAuditManager.md +1 -1
  38. package/docs/api/classes/RBACCache.md +1 -1
  39. package/docs/api/classes/RBACEngine.md +1 -1
  40. package/docs/api/classes/RBACError.md +1 -1
  41. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  42. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  43. package/docs/api/classes/StorageUtils.md +1 -1
  44. package/docs/api/enums/FileCategory.md +1 -1
  45. package/docs/api/interfaces/AggregateConfig.md +1 -1
  46. package/docs/api/interfaces/ButtonProps.md +1 -1
  47. package/docs/api/interfaces/CardProps.md +1 -1
  48. package/docs/api/interfaces/ColorPalette.md +1 -1
  49. package/docs/api/interfaces/ColorShade.md +1 -1
  50. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  51. package/docs/api/interfaces/DataRecord.md +1 -1
  52. package/docs/api/interfaces/DataTableAction.md +1 -1
  53. package/docs/api/interfaces/DataTableColumn.md +1 -1
  54. package/docs/api/interfaces/DataTableProps.md +1 -1
  55. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  56. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  57. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  58. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  59. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  60. package/docs/api/interfaces/FileMetadata.md +1 -1
  61. package/docs/api/interfaces/FileReference.md +1 -1
  62. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  63. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  64. package/docs/api/interfaces/FileUploadProps.md +1 -1
  65. package/docs/api/interfaces/FooterProps.md +1 -1
  66. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  67. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  68. package/docs/api/interfaces/InputProps.md +1 -1
  69. package/docs/api/interfaces/LabelProps.md +1 -1
  70. package/docs/api/interfaces/LoginFormProps.md +1 -1
  71. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  72. package/docs/api/interfaces/NavigationContextType.md +1 -1
  73. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  74. package/docs/api/interfaces/NavigationItem.md +1 -1
  75. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  76. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  77. package/docs/api/interfaces/Organisation.md +1 -1
  78. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  79. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  80. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  81. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  82. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  83. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  84. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  85. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  86. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  87. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  88. package/docs/api/interfaces/PaletteData.md +1 -1
  89. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  90. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  91. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  92. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  93. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  94. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  95. package/docs/api/interfaces/PublicPageHeaderProps.md +10 -62
  96. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  97. package/docs/api/interfaces/RBACConfig.md +1 -1
  98. package/docs/api/interfaces/RBACLogger.md +1 -1
  99. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  100. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  101. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  102. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  103. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  104. package/docs/api/interfaces/RouteConfig.md +1 -1
  105. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  106. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  107. package/docs/api/interfaces/StorageConfig.md +1 -1
  108. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  109. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  110. package/docs/api/interfaces/StorageListOptions.md +1 -1
  111. package/docs/api/interfaces/StorageListResult.md +1 -1
  112. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  113. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  114. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  115. package/docs/api/interfaces/StyleImport.md +1 -1
  116. package/docs/api/interfaces/SwitchProps.md +1 -1
  117. package/docs/api/interfaces/ToastActionElement.md +1 -1
  118. package/docs/api/interfaces/ToastProps.md +1 -1
  119. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  120. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  121. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  122. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  123. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  124. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  125. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  126. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  128. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  129. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  130. package/docs/api/interfaces/UserEventAccess.md +1 -1
  131. package/docs/api/interfaces/UserMenuProps.md +1 -1
  132. package/docs/api/interfaces/UserProfile.md +1 -1
  133. package/docs/api/modules.md +53 -28
  134. package/docs/api-reference/components.md +24 -0
  135. package/docs/api-reference/types.md +28 -0
  136. package/docs/architecture/rpc-function-standards.md +39 -5
  137. package/docs/implementation-guides/data-tables.md +55 -10
  138. package/docs/implementation-guides/permission-enforcement.md +4 -0
  139. package/docs/rbac/super-admin-guide.md +43 -5
  140. package/package.json +1 -1
  141. package/src/components/Button/Button.tsx +1 -1
  142. package/src/components/DataTable/__tests__/DataTable.export.test.tsx +702 -0
  143. package/src/components/DataTable/components/DataTableCore.tsx +55 -36
  144. package/src/components/DataTable/components/ImportModal.tsx +134 -2
  145. package/src/components/DataTable/index.ts +3 -1
  146. package/src/components/DataTable/types.ts +68 -0
  147. package/src/components/Dialog/Dialog.tsx +0 -13
  148. package/src/components/FileDisplay/FileDisplay.tsx +76 -0
  149. package/src/components/Header/Header.tsx +5 -0
  150. package/src/components/PaceAppLayout/PaceAppLayout.tsx +72 -50
  151. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +81 -1
  152. package/src/components/PublicLayout/PublicPageFooter.tsx +1 -1
  153. package/src/components/PublicLayout/PublicPageHeader.tsx +69 -128
  154. package/src/components/PublicLayout/PublicPageLayout.tsx +4 -4
  155. package/src/components/PublicLayout/PublicPageProvider.tsx +12 -3
  156. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  157. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +3 -18
  158. package/src/hooks/__tests__/useAppConfig.unit.test.ts +3 -1
  159. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +11 -5
  160. package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +8 -7
  161. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +41 -46
  162. package/src/hooks/public/usePublicFileDisplay.ts +176 -7
  163. package/src/hooks/public/usePublicRouteParams.ts +0 -12
  164. package/src/hooks/useAppConfig.ts +15 -6
  165. package/src/hooks/usePermissionCache.test.ts +12 -4
  166. package/src/hooks/usePermissionCache.ts +3 -19
  167. package/src/hooks/useSecureDataAccess.ts +0 -63
  168. package/src/services/EventService.ts +0 -19
  169. package/dist/chunk-NZGLXZGP.js.map +0 -1
  170. package/dist/chunk-QXGLU2O5.js.map +0 -1
  171. package/dist/chunk-R4CRQUJJ.js.map +0 -1
  172. package/dist/chunk-XN6GWKMV.js.map +0 -1
  173. package/dist/chunk-ZBLK676C.js.map +0 -1
  174. /package/dist/{DataTable-6FN7XDXA.js.map → DataTable-3Z5HLOWF.js.map} +0 -0
  175. /package/dist/{UnifiedAuthProvider-6C47WIML.js.map → UnifiedAuthProvider-CQDZRJIS.js.map} +0 -0
  176. /package/dist/{chunk-35ZDPMBM.js.map → chunk-BYXRHAIF.js.map} +0 -0
  177. /package/dist/{chunk-IJOZZOGT.js.map → chunk-CQZU6TFE.js.map} +0 -0
  178. /package/dist/{chunk-C43QIDN3.js.map → chunk-CTJRBUX2.js.map} +0 -0
  179. /package/dist/{chunk-4MXVZVNS.js.map → chunk-TGIY2AR2.js.map} +0 -0
  180. /package/dist/{chunk-QWNJCQXZ.js.map → chunk-ZV77RZMU.js.map} +0 -0
@@ -0,0 +1,702 @@
1
+ /**
2
+ * @file DataTable Export Functionality Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/__tests__
5
+ * @since 0.5.76
6
+ *
7
+ * Comprehensive tests for DataTable export functionality, including:
8
+ * - Default export behavior (visible columns)
9
+ * - Custom onExport handler with ExportOptions
10
+ * - Custom column selection
11
+ * - Export with different columns than visible
12
+ */
13
+
14
+ import React from 'react';
15
+ import { render, screen, waitFor } from '@testing-library/react';
16
+ import userEvent from '@testing-library/user-event';
17
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
18
+ import { DataTable } from '../DataTable';
19
+ import type { DataTableColumn, ExportOptions } from '../types';
20
+ import { exportToCSVWithTableRows } from '../utils/exportUtils';
21
+ import { createTestData, createTestColumns } from './test-utils/dataFactories';
22
+ import { createDefaultFeatures } from './test-utils';
23
+
24
+ // Mock the export utilities
25
+ vi.mock('../utils/exportUtils', () => ({
26
+ exportToCSVWithTableRows: vi.fn().mockResolvedValue(undefined),
27
+ exportToCSV: vi.fn().mockResolvedValue(undefined),
28
+ generateCSVContent: vi.fn(),
29
+ }));
30
+
31
+ // Mock the toast hook
32
+ const mockToast = vi.fn();
33
+ vi.mock('../../../hooks/useToast', () => ({
34
+ toast: (...args: any[]) => mockToast(...args),
35
+ }));
36
+
37
+ // Mock RBAC hooks
38
+ vi.mock('../../../rbac/hooks', () => ({
39
+ useCan: vi.fn(() => ({
40
+ can: true,
41
+ isLoading: false,
42
+ error: null,
43
+ })),
44
+ useResolvedScope: vi.fn(() => ({
45
+ resolvedScope: {
46
+ organisationId: 'test-org-id',
47
+ eventId: 'test-event-id',
48
+ appId: 'test-app-id'
49
+ }
50
+ }))
51
+ }));
52
+
53
+ // Mock auth provider
54
+ vi.mock('../../../providers/UnifiedAuthProvider', () => ({
55
+ useUnifiedAuth: vi.fn(() => ({
56
+ user: { id: 'test-user', email: 'test@example.com' },
57
+ isAuthenticated: true,
58
+ isLoading: false,
59
+ error: null,
60
+ selectedOrganisationId: 'test-org-id',
61
+ selectedEventId: 'test-event-id',
62
+ supabase: null,
63
+ })),
64
+ }));
65
+
66
+ // Mock DataTableCore to test the wrapper logic
67
+ vi.mock('../components/DataTableCore', async () => {
68
+ const React = await import('react');
69
+ const { exportToCSVWithTableRows } = await import('../utils/exportUtils');
70
+ const { toast } = await import('../../../hooks/useToast');
71
+
72
+ return {
73
+ DataTableCore: React.forwardRef(({ onExport, data, columns, title, ...props }: any, ref: any) => {
74
+ // Simulate export button click
75
+ const handleExport = async () => {
76
+ try {
77
+ // Create mock table rows with getValue method
78
+ const tableRows = (data || []).map((row: any, index: number) => ({
79
+ original: row,
80
+ getValue: (columnId: string) => {
81
+ const col = columns?.find((c: any) => (c.id || c.accessorKey) === columnId);
82
+ if (col && 'accessorFn' in col && col.accessorFn) {
83
+ return col.accessorFn(row);
84
+ }
85
+ return row[col?.accessorKey || columnId];
86
+ },
87
+ id: String(row.id || index),
88
+ }));
89
+
90
+ // Get visible columns (all columns for simplicity in tests)
91
+ const visibleColumns = columns || [];
92
+
93
+ // Create column mapping
94
+ const columnIdToTableColumn = new Map();
95
+ visibleColumns.forEach((col: any) => {
96
+ const colId = col.id || col.accessorKey;
97
+ if (colId) {
98
+ columnIdToTableColumn.set(String(colId), { id: String(colId) });
99
+ }
100
+ });
101
+
102
+ // Generate filename
103
+ const timestamp = new Date().toISOString().split('T')[0];
104
+ const filename = title
105
+ ? `${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${timestamp}.csv`
106
+ : `data_export_${timestamp}.csv`;
107
+
108
+ // Create export options
109
+ const exportOptions = {
110
+ tableRows,
111
+ allColumns: columns || [],
112
+ visibleColumns,
113
+ columnIdToTableColumn,
114
+ data: data || [],
115
+ filename,
116
+ table: {}, // Mock table instance
117
+ };
118
+
119
+ // If custom handler provided, call it with options
120
+ if (onExport) {
121
+ await onExport(exportOptions);
122
+ return;
123
+ }
124
+
125
+ // Default export behavior
126
+ const exportColumns = visibleColumns.map((col: any) => ({
127
+ header: col.header || col.accessorKey,
128
+ id: col.id || col.accessorKey,
129
+ accessorKey: col.accessorKey,
130
+ accessorFn: 'accessorFn' in col ? col.accessorFn : undefined,
131
+ }));
132
+
133
+ await exportToCSVWithTableRows(
134
+ tableRows,
135
+ exportColumns,
136
+ columnIdToTableColumn,
137
+ filename
138
+ );
139
+
140
+ toast({
141
+ title: "Export Successful",
142
+ description: `Data exported to ${filename}`,
143
+ variant: "default"
144
+ });
145
+ } catch (error) {
146
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
147
+ toast({
148
+ title: "Export Failed",
149
+ description: `Failed to export data: ${errorMessage}`,
150
+ variant: "destructive"
151
+ });
152
+ }
153
+ };
154
+
155
+ return (
156
+ <div data-testid="data-table-core">
157
+ {props.features?.export && (
158
+ <button
159
+ data-testid="export-button"
160
+ onClick={handleExport}
161
+ >
162
+ Export
163
+ </button>
164
+ )}
165
+ <div data-testid="table-content">{props.children}</div>
166
+ </div>
167
+ );
168
+ }),
169
+ };
170
+ });
171
+
172
+ interface TestData {
173
+ id: string;
174
+ name: string;
175
+ email: string;
176
+ role: string;
177
+ status: 'active' | 'inactive';
178
+ createdAt: string;
179
+ }
180
+
181
+ describe('DataTable Export Functionality', () => {
182
+ const mockData: TestData[] = [
183
+ { id: '1', name: 'John Doe', email: 'john@example.com', role: 'Admin', status: 'active', createdAt: '2024-01-01' },
184
+ { id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'User', status: 'active', createdAt: '2024-01-02' },
185
+ { id: '3', name: 'Bob Johnson', email: 'bob@example.com', role: 'User', status: 'inactive', createdAt: '2024-01-03' },
186
+ ];
187
+
188
+ const mockColumns: DataTableColumn<TestData>[] = [
189
+ { id: 'id', accessorKey: 'id', header: 'ID' },
190
+ { id: 'name', accessorKey: 'name', header: 'Name' },
191
+ { id: 'email', accessorKey: 'email', header: 'Email' },
192
+ { id: 'role', accessorKey: 'role', header: 'Role' },
193
+ { id: 'status', accessorKey: 'status', header: 'Status' },
194
+ { id: 'createdAt', accessorKey: 'createdAt', header: 'Created At' },
195
+ ];
196
+
197
+ const mockRBAC = { pageId: 'test-page' };
198
+
199
+ beforeEach(() => {
200
+ vi.clearAllMocks();
201
+ delete (window as any).__testOnExport;
202
+ });
203
+
204
+ describe('Default Export Behavior', () => {
205
+ it('exports visible columns when no custom handler is provided', async () => {
206
+ const features = createDefaultFeatures();
207
+ features.export = true;
208
+
209
+ render(
210
+ <DataTable
211
+ data={mockData}
212
+ columns={mockColumns}
213
+ rbac={mockRBAC}
214
+ features={features}
215
+ title="Test Table"
216
+ />
217
+ );
218
+
219
+ const exportButton = screen.getByTestId('export-button');
220
+ await userEvent.click(exportButton);
221
+
222
+ await waitFor(() => {
223
+ expect(exportToCSVWithTableRows).toHaveBeenCalled();
224
+ });
225
+
226
+ // Verify default export was called with visible columns
227
+ const callArgs = vi.mocked(exportToCSVWithTableRows).mock.calls[0];
228
+ expect(callArgs).toBeDefined();
229
+ expect(callArgs[2]).toBeDefined(); // columnIdToTableColumn
230
+ expect(callArgs[3]).toMatch(/test_table_\d{4}-\d{2}-\d{2}\.csv/); // filename
231
+ });
232
+
233
+ it('generates filename from title', async () => {
234
+ const features = createDefaultFeatures();
235
+ features.export = true;
236
+
237
+ render(
238
+ <DataTable
239
+ data={mockData}
240
+ columns={mockColumns}
241
+ rbac={mockRBAC}
242
+ features={features}
243
+ title="My Custom Table"
244
+ />
245
+ );
246
+
247
+ const exportButton = screen.getByTestId('export-button');
248
+ await userEvent.click(exportButton);
249
+
250
+ await waitFor(() => {
251
+ expect(exportToCSVWithTableRows).toHaveBeenCalled();
252
+ });
253
+
254
+ const callArgs = vi.mocked(exportToCSVWithTableRows).mock.calls[0];
255
+ expect(callArgs[3]).toMatch(/my_custom_table_\d{4}-\d{2}-\d{2}\.csv/);
256
+ });
257
+
258
+ it('uses default filename when no title is provided', async () => {
259
+ const features = createDefaultFeatures();
260
+ features.export = true;
261
+
262
+ render(
263
+ <DataTable
264
+ data={mockData}
265
+ columns={mockColumns}
266
+ rbac={mockRBAC}
267
+ features={features}
268
+ />
269
+ );
270
+
271
+ const exportButton = screen.getByTestId('export-button');
272
+ await userEvent.click(exportButton);
273
+
274
+ await waitFor(() => {
275
+ expect(exportToCSVWithTableRows).toHaveBeenCalled();
276
+ });
277
+
278
+ const callArgs = vi.mocked(exportToCSVWithTableRows).mock.calls[0];
279
+ expect(callArgs[3]).toMatch(/data_export_\d{4}-\d{2}-\d{2}\.csv/);
280
+ });
281
+ });
282
+
283
+ describe('Custom onExport Handler', () => {
284
+ it('calls custom onExport handler with ExportOptions', async () => {
285
+ const features = createDefaultFeatures();
286
+ features.export = true;
287
+ const mockOnExport = vi.fn().mockResolvedValue(undefined);
288
+
289
+ render(
290
+ <DataTable
291
+ data={mockData}
292
+ columns={mockColumns}
293
+ rbac={mockRBAC}
294
+ features={features}
295
+ onExport={mockOnExport}
296
+ />
297
+ );
298
+
299
+ const exportButton = screen.getByTestId('export-button');
300
+ await userEvent.click(exportButton);
301
+
302
+ await waitFor(() => {
303
+ expect(mockOnExport).toHaveBeenCalled();
304
+ });
305
+
306
+ // Verify ExportOptions structure
307
+ const exportOptions = mockOnExport.mock.calls[0][0] as ExportOptions<TestData>;
308
+ expect(exportOptions).toBeDefined();
309
+ expect(exportOptions.tableRows).toBeDefined();
310
+ expect(exportOptions.allColumns).toEqual(mockColumns);
311
+ expect(exportOptions.visibleColumns).toBeDefined();
312
+ expect(exportOptions.columnIdToTableColumn).toBeDefined();
313
+ expect(exportOptions.data).toEqual(mockData);
314
+ expect(exportOptions.filename).toBeDefined();
315
+ expect(exportOptions.table).toBeDefined();
316
+ });
317
+
318
+ it('provides all columns in ExportOptions.allColumns', async () => {
319
+ const features = createDefaultFeatures();
320
+ features.export = true;
321
+ const mockOnExport = vi.fn().mockResolvedValue(undefined);
322
+
323
+ render(
324
+ <DataTable
325
+ data={mockData}
326
+ columns={mockColumns}
327
+ rbac={mockRBAC}
328
+ features={features}
329
+ onExport={mockOnExport}
330
+ />
331
+ );
332
+
333
+ const exportButton = screen.getByTestId('export-button');
334
+ await userEvent.click(exportButton);
335
+
336
+ await waitFor(() => {
337
+ expect(mockOnExport).toHaveBeenCalled();
338
+ });
339
+
340
+ const exportOptions = mockOnExport.mock.calls[0][0] as ExportOptions<TestData>;
341
+ expect(exportOptions.allColumns).toHaveLength(mockColumns.length);
342
+ expect(exportOptions.allColumns).toEqual(mockColumns);
343
+ });
344
+
345
+ it('provides visible columns in ExportOptions.visibleColumns', async () => {
346
+ const features = createDefaultFeatures();
347
+ features.export = true;
348
+ const mockOnExport = vi.fn().mockResolvedValue(undefined);
349
+
350
+ render(
351
+ <DataTable
352
+ data={mockData}
353
+ columns={mockColumns}
354
+ rbac={mockRBAC}
355
+ features={features}
356
+ onExport={mockOnExport}
357
+ />
358
+ );
359
+
360
+ const exportButton = screen.getByTestId('export-button');
361
+ await userEvent.click(exportButton);
362
+
363
+ await waitFor(() => {
364
+ expect(mockOnExport).toHaveBeenCalled();
365
+ });
366
+
367
+ const exportOptions = mockOnExport.mock.calls[0][0] as ExportOptions<TestData>;
368
+ expect(exportOptions.visibleColumns).toBeDefined();
369
+ expect(Array.isArray(exportOptions.visibleColumns)).toBe(true);
370
+ // Visible columns should be a subset of all columns
371
+ expect(exportOptions.visibleColumns.length).toBeLessThanOrEqual(exportOptions.allColumns.length);
372
+ });
373
+
374
+ it('provides tableRows with getValue method', async () => {
375
+ const features = createDefaultFeatures();
376
+ features.export = true;
377
+ const mockOnExport = vi.fn().mockResolvedValue(undefined);
378
+
379
+ render(
380
+ <DataTable
381
+ data={mockData}
382
+ columns={mockColumns}
383
+ rbac={mockRBAC}
384
+ features={features}
385
+ onExport={mockOnExport}
386
+ />
387
+ );
388
+
389
+ const exportButton = screen.getByTestId('export-button');
390
+ await userEvent.click(exportButton);
391
+
392
+ await waitFor(() => {
393
+ expect(mockOnExport).toHaveBeenCalled();
394
+ });
395
+
396
+ const exportOptions = mockOnExport.mock.calls[0][0] as ExportOptions<TestData>;
397
+ expect(exportOptions.tableRows).toBeDefined();
398
+ expect(Array.isArray(exportOptions.tableRows)).toBe(true);
399
+ expect(exportOptions.tableRows.length).toBeGreaterThan(0);
400
+
401
+ // Verify tableRows have getValue method
402
+ const firstRow = exportOptions.tableRows[0];
403
+ expect(firstRow).toHaveProperty('getValue');
404
+ expect(firstRow).toHaveProperty('original');
405
+ expect(firstRow).toHaveProperty('id');
406
+ expect(typeof firstRow.getValue).toBe('function');
407
+ });
408
+
409
+ it('provides columnIdToTableColumn mapping', async () => {
410
+ const features = createDefaultFeatures();
411
+ features.export = true;
412
+ const mockOnExport = vi.fn().mockResolvedValue(undefined);
413
+
414
+ render(
415
+ <DataTable
416
+ data={mockData}
417
+ columns={mockColumns}
418
+ rbac={mockRBAC}
419
+ features={features}
420
+ onExport={mockOnExport}
421
+ />
422
+ );
423
+
424
+ const exportButton = screen.getByTestId('export-button');
425
+ await userEvent.click(exportButton);
426
+
427
+ await waitFor(() => {
428
+ expect(mockOnExport).toHaveBeenCalled();
429
+ });
430
+
431
+ const exportOptions = mockOnExport.mock.calls[0][0] as ExportOptions<TestData>;
432
+ expect(exportOptions.columnIdToTableColumn).toBeDefined();
433
+ expect(exportOptions.columnIdToTableColumn instanceof Map).toBe(true);
434
+ });
435
+ });
436
+
437
+ describe('Custom Column Selection', () => {
438
+ it('allows exporting different columns than visible', async () => {
439
+ const features = createDefaultFeatures();
440
+ features.export = true;
441
+ const mockOnExport = vi.fn(async (options: ExportOptions<TestData>) => {
442
+ // Export only name and email columns (different from all visible columns)
443
+ const exportColumns = options.allColumns
444
+ .filter(col => ['name', 'email'].includes(col.accessorKey || ''))
445
+ .map(col => ({
446
+ header: col.header || col.accessorKey,
447
+ id: col.id || col.accessorKey,
448
+ accessorKey: col.accessorKey,
449
+ accessorFn: 'accessorFn' in col ? col.accessorFn : undefined,
450
+ }));
451
+
452
+ await exportToCSVWithTableRows(
453
+ options.tableRows,
454
+ exportColumns,
455
+ options.columnIdToTableColumn,
456
+ 'custom-export.csv'
457
+ );
458
+ });
459
+
460
+ render(
461
+ <DataTable
462
+ data={mockData}
463
+ columns={mockColumns}
464
+ rbac={mockRBAC}
465
+ features={features}
466
+ onExport={mockOnExport}
467
+ />
468
+ );
469
+
470
+ const exportButton = screen.getByTestId('export-button');
471
+ await userEvent.click(exportButton);
472
+
473
+ await waitFor(() => {
474
+ expect(mockOnExport).toHaveBeenCalled();
475
+ expect(exportToCSVWithTableRows).toHaveBeenCalled();
476
+ });
477
+
478
+ // Verify custom export was called with only name and email columns
479
+ const callArgs = vi.mocked(exportToCSVWithTableRows).mock.calls[0];
480
+ const exportedColumns = callArgs[1];
481
+ expect(exportedColumns).toHaveLength(2);
482
+ expect(exportedColumns.map((c: any) => c.accessorKey)).toEqual(['name', 'email']);
483
+ expect(callArgs[3]).toBe('custom-export.csv');
484
+ });
485
+
486
+ it('allows exporting all columns including hidden ones', async () => {
487
+ const features = createDefaultFeatures();
488
+ features.export = true;
489
+ const mockOnExport = vi.fn(async (options: ExportOptions<TestData>) => {
490
+ // Export all columns (including potentially hidden ones)
491
+ const exportColumns = options.allColumns.map(col => ({
492
+ header: col.header || col.accessorKey,
493
+ id: col.id || col.accessorKey,
494
+ accessorKey: col.accessorKey,
495
+ accessorFn: 'accessorFn' in col ? col.accessorFn : undefined,
496
+ }));
497
+
498
+ await exportToCSVWithTableRows(
499
+ options.tableRows,
500
+ exportColumns,
501
+ options.columnIdToTableColumn,
502
+ 'all-columns-export.csv'
503
+ );
504
+ });
505
+
506
+ render(
507
+ <DataTable
508
+ data={mockData}
509
+ columns={mockColumns}
510
+ rbac={mockRBAC}
511
+ features={features}
512
+ onExport={mockOnExport}
513
+ />
514
+ );
515
+
516
+ const exportButton = screen.getByTestId('export-button');
517
+ await userEvent.click(exportButton);
518
+
519
+ await waitFor(() => {
520
+ expect(mockOnExport).toHaveBeenCalled();
521
+ expect(exportToCSVWithTableRows).toHaveBeenCalled();
522
+ });
523
+
524
+ // Verify all columns were exported
525
+ const callArgs = vi.mocked(exportToCSVWithTableRows).mock.calls[0];
526
+ const exportedColumns = callArgs[1];
527
+ expect(exportedColumns).toHaveLength(mockColumns.length);
528
+ });
529
+
530
+ it('allows custom filename in export handler', async () => {
531
+ const features = createDefaultFeatures();
532
+ features.export = true;
533
+ const customFilename = 'my-custom-export-2024.csv';
534
+ const mockOnExport = vi.fn(async (options: ExportOptions<TestData>) => {
535
+ const exportColumns = options.visibleColumns.map(col => ({
536
+ header: col.header || col.accessorKey,
537
+ id: col.id || col.accessorKey,
538
+ accessorKey: col.accessorKey,
539
+ accessorFn: 'accessorFn' in col ? col.accessorFn : undefined,
540
+ }));
541
+
542
+ await exportToCSVWithTableRows(
543
+ options.tableRows,
544
+ exportColumns,
545
+ options.columnIdToTableColumn,
546
+ customFilename
547
+ );
548
+ });
549
+
550
+ render(
551
+ <DataTable
552
+ data={mockData}
553
+ columns={mockColumns}
554
+ rbac={mockRBAC}
555
+ features={features}
556
+ onExport={mockOnExport}
557
+ title="Test Table"
558
+ />
559
+ );
560
+
561
+ const exportButton = screen.getByTestId('export-button');
562
+ await userEvent.click(exportButton);
563
+
564
+ await waitFor(() => {
565
+ expect(mockOnExport).toHaveBeenCalled();
566
+ expect(exportToCSVWithTableRows).toHaveBeenCalled();
567
+ });
568
+
569
+ const callArgs = vi.mocked(exportToCSVWithTableRows).mock.calls[0];
570
+ expect(callArgs[3]).toBe(customFilename);
571
+ });
572
+ });
573
+
574
+ describe('Export with accessorFn', () => {
575
+ it('handles columns with accessorFn in custom export', async () => {
576
+ const columnsWithAccessorFn: DataTableColumn<TestData>[] = [
577
+ { id: 'name', accessorKey: 'name', header: 'Name' },
578
+ {
579
+ id: 'fullInfo',
580
+ header: 'Full Info',
581
+ accessorFn: (row) => `${row.name} (${row.email})`,
582
+ },
583
+ ];
584
+
585
+ const features = createDefaultFeatures();
586
+ features.export = true;
587
+ const mockOnExport = vi.fn(async (options: ExportOptions<TestData>) => {
588
+ const exportColumns = options.allColumns.map(col => ({
589
+ header: col.header || col.accessorKey,
590
+ id: col.id || col.accessorKey,
591
+ accessorKey: col.accessorKey,
592
+ accessorFn: 'accessorFn' in col ? col.accessorFn : undefined,
593
+ }));
594
+
595
+ await exportToCSVWithTableRows(
596
+ options.tableRows,
597
+ exportColumns,
598
+ options.columnIdToTableColumn,
599
+ 'export-with-accessorfn.csv'
600
+ );
601
+ });
602
+
603
+ render(
604
+ <DataTable
605
+ data={mockData}
606
+ columns={columnsWithAccessorFn}
607
+ rbac={mockRBAC}
608
+ features={features}
609
+ onExport={mockOnExport}
610
+ />
611
+ );
612
+
613
+ const exportButton = screen.getByTestId('export-button');
614
+ await userEvent.click(exportButton);
615
+
616
+ await waitFor(() => {
617
+ expect(mockOnExport).toHaveBeenCalled();
618
+ expect(exportToCSVWithTableRows).toHaveBeenCalled();
619
+ });
620
+
621
+ // Verify export was called with columns including accessorFn
622
+ const callArgs = vi.mocked(exportToCSVWithTableRows).mock.calls[0];
623
+ const exportedColumns = callArgs[1];
624
+ const fullInfoColumn = exportedColumns.find((c: any) => c.id === 'fullInfo');
625
+ expect(fullInfoColumn).toBeDefined();
626
+ expect(fullInfoColumn.accessorFn).toBeDefined();
627
+ });
628
+ });
629
+
630
+ describe('Error Handling', () => {
631
+ it('handles errors in custom export handler gracefully', async () => {
632
+ const features = createDefaultFeatures();
633
+ features.export = true;
634
+ const mockOnExport = vi.fn().mockRejectedValue(new Error('Export failed'));
635
+
636
+ render(
637
+ <DataTable
638
+ data={mockData}
639
+ columns={mockColumns}
640
+ rbac={mockRBAC}
641
+ features={features}
642
+ onExport={mockOnExport}
643
+ />
644
+ );
645
+
646
+ const exportButton = screen.getByTestId('export-button');
647
+ await userEvent.click(exportButton);
648
+
649
+ await waitFor(() => {
650
+ expect(mockOnExport).toHaveBeenCalled();
651
+ });
652
+
653
+ // Verify error toast was shown
654
+ await waitFor(() => {
655
+ expect(mockToast).toHaveBeenCalledWith(
656
+ expect.objectContaining({
657
+ title: 'Export Failed',
658
+ variant: 'destructive',
659
+ })
660
+ );
661
+ });
662
+ });
663
+
664
+ it('handles errors in default export gracefully', async () => {
665
+ vi.mocked(exportToCSVWithTableRows).mockRejectedValueOnce(new Error('Export failed'));
666
+
667
+ const features = createDefaultFeatures();
668
+ features.export = true;
669
+
670
+ render(
671
+ <DataTable
672
+ data={mockData}
673
+ columns={mockColumns}
674
+ rbac={mockRBAC}
675
+ features={features}
676
+ />
677
+ );
678
+
679
+ const exportButton = screen.getByTestId('export-button');
680
+ await userEvent.click(exportButton);
681
+
682
+ await waitFor(() => {
683
+ expect(mockToast).toHaveBeenCalledWith(
684
+ expect.objectContaining({
685
+ title: 'Export Failed',
686
+ variant: 'destructive',
687
+ })
688
+ );
689
+ });
690
+ });
691
+ });
692
+
693
+ describe('RBAC Integration', () => {
694
+ it('skips RBAC test - tested in DataTableCore tests', () => {
695
+ // RBAC integration is tested in DataTableCore.test.tsx
696
+ // This test file focuses on the export functionality itself
697
+ // The mock DataTableCore doesn't implement RBAC checks, so we skip here
698
+ expect(true).toBe(true);
699
+ });
700
+ });
701
+ });
702
+