@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
@@ -46,6 +46,7 @@ import { useCan, useResolvedScope } from '../../../rbac/hooks';
46
46
  // Do NOT set duration or timeout properties - let the toast system use its default.
47
47
  import { toast } from '../../../hooks/useToast';
48
48
  import { exportToCSV, exportToCSVWithTableRows } from '../utils/exportUtils';
49
+ import type { ExportOptions } from '../types';
49
50
  import { useUnifiedAuth } from '../../../providers/UnifiedAuthProvider';
50
51
  import { Scope } from '../../../rbac/types';
51
52
  import { useDataTablePermissions } from '../hooks/useDataTablePermissions';
@@ -150,7 +151,7 @@ export interface DataTableCoreProps<TData extends DataRecord> {
150
151
  onDeleteRow?: (row: TData) => void;
151
152
  onCreateRow?: (data: Partial<TData>) => void;
152
153
  onImport?: (data: TData[]) => void | Promise<void>;
153
- onExport?: () => void;
154
+ onExport?: (options: import('../types').ExportOptions<TData>) => void | Promise<void>;
154
155
  onRowSelectionChange?: (selection: Record<string, boolean>) => void;
155
156
  selection?: Record<string, boolean>;
156
157
  onDeleteSelected?: (selectedRows: Record<string, boolean>) => void;
@@ -1011,9 +1012,9 @@ function DataTableInternal<TData extends DataRecord>({
1011
1012
  }
1012
1013
  stateActions.setImportModal(true);
1013
1014
  }}
1014
- onExport={secureHandlers.onExport || (async () => {
1015
+ onExport={async () => {
1015
1016
  try {
1016
- // Automatic export: exports exactly what's shown in the table
1017
+ // Prepare export options with all available data
1017
1018
  // Get the table rows (which have getValue() that properly evaluates accessorFn)
1018
1019
  const tableRows = table.getFilteredRowModel().rows;
1019
1020
 
@@ -1026,16 +1027,8 @@ function DataTableInternal<TData extends DataRecord>({
1026
1027
  return !isSystemColumn && col.getIsVisible();
1027
1028
  });
1028
1029
 
1029
- // Map table columns to export columns
1030
- // Use TanStack Table's getValue() which properly handles accessorFn
1031
- const visibleColumns: Array<{
1032
- header?: string;
1033
- id?: string;
1034
- accessorKey?: string;
1035
- accessorFn?: (row: any) => any;
1036
- editAccessorKey?: string;
1037
- isIdColumn?: boolean;
1038
- }> = [];
1030
+ // Map table columns to visible columns
1031
+ const visibleColumns: DataTableColumn<TData>[] = [];
1039
1032
 
1040
1033
  // Store mapping of column IDs to table column instances for getValue() calls
1041
1034
  const columnIdToTableColumn = new Map<string, typeof visibleTableColumns[0]>();
@@ -1052,42 +1045,68 @@ function DataTableInternal<TData extends DataRecord>({
1052
1045
  // Store the table column for getValue() calls
1053
1046
  columnIdToTableColumn.set(tableCol.id, tableCol);
1054
1047
 
1055
- const hasAccessorFn = 'accessorFn' in originalCol && (originalCol as any).accessorFn;
1056
- const editAccessorKey = originalCol.editAccessorKey;
1057
-
1058
1048
  // Add the display column (what's shown in the table)
1059
- // For columns with accessorFn, we'll use the table's getValue() method instead of calling accessorFn directly
1060
- const displayHeader = typeof originalCol.header === 'string'
1061
- ? originalCol.header
1062
- : originalCol.accessorKey || tableCol.id || 'Column';
1063
-
1064
- visibleColumns.push({
1065
- ...originalCol,
1066
- header: displayHeader,
1067
- // Store table column ID for getValue() lookup
1068
- id: tableCol.id,
1069
- // Preserve accessorFn if present (will use getValue() if available)
1070
- accessorFn: hasAccessorFn ? (originalCol as any).accessorFn : undefined,
1071
- });
1072
-
1073
- // Note: We do NOT export editAccessorKey fields as separate columns
1074
- // Only visible columns (with their display values via accessorFn) are exported
1075
- // This ensures exports match what users see in the table
1049
+ visibleColumns.push(originalCol);
1076
1050
  });
1077
1051
 
1078
1052
  // Generate filename with timestamp
1079
1053
  const timestamp = new Date().toISOString().split('T')[0];
1080
1054
  const filename = title ? `${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${timestamp}.csv` : `data_export_${timestamp}.csv`;
1081
1055
 
1056
+ // Create export options
1057
+ const exportOptions: ExportOptions<TData> = {
1058
+ tableRows,
1059
+ allColumns: columns,
1060
+ visibleColumns,
1061
+ columnIdToTableColumn,
1062
+ data,
1063
+ filename,
1064
+ table
1065
+ };
1066
+
1067
+ // If custom handler provided, call it with options
1068
+ if (secureHandlers.onExport) {
1069
+ await secureHandlers.onExport(exportOptions);
1070
+ return;
1071
+ }
1072
+
1073
+ // Default export: exports exactly what's shown in the table
1074
+ // Convert visible columns to ExportColumn format
1075
+ const exportColumns: Array<{
1076
+ header?: string;
1077
+ id?: string;
1078
+ accessorKey?: string;
1079
+ accessorFn?: (row: any) => any;
1080
+ editAccessorKey?: string;
1081
+ isIdColumn?: boolean;
1082
+ }> = exportOptions.visibleColumns.map(col => {
1083
+ const colId = col.id || col.accessorKey;
1084
+ const hasAccessorFn = 'accessorFn' in col && (col as any).accessorFn;
1085
+
1086
+ return {
1087
+ ...col,
1088
+ header: typeof col.header === 'string'
1089
+ ? col.header
1090
+ : col.accessorKey || colId || 'Column',
1091
+ id: colId ? String(colId) : undefined,
1092
+ accessorFn: hasAccessorFn ? (col as any).accessorFn : undefined,
1093
+ };
1094
+ });
1095
+
1082
1096
  // Export using table rows with getValue() for proper accessorFn evaluation
1083
1097
  // This ensures we get the same values that are displayed in the table
1084
- await exportToCSVWithTableRows(tableRows, visibleColumns, columnIdToTableColumn, filename);
1098
+ await exportToCSVWithTableRows(
1099
+ exportOptions.tableRows,
1100
+ exportColumns,
1101
+ exportOptions.columnIdToTableColumn,
1102
+ exportOptions.filename
1103
+ );
1085
1104
 
1086
1105
  // Show success toast notification
1087
1106
  // NOTE: Toast notifications use default timeout (5 seconds) - do not set duration property
1088
1107
  toast({
1089
1108
  title: "Export Successful",
1090
- description: `Data exported to ${filename}`,
1109
+ description: `Data exported to ${exportOptions.filename}`,
1091
1110
  variant: "default"
1092
1111
  });
1093
1112
 
@@ -1103,7 +1122,7 @@ function DataTableInternal<TData extends DataRecord>({
1103
1122
  variant: "destructive"
1104
1123
  });
1105
1124
  }
1106
- })}
1125
+ }}
1107
1126
  rowSelection={rowSelection}
1108
1127
  onDeleteSelected={secureHandlers.onDeleteSelected ? async (selectedRows: Record<string, boolean>) => {
1109
1128
  const selectedCount = Object.values(selectedRows).filter(Boolean).length;
@@ -39,6 +39,7 @@ import React, { useState, useRef, useEffect } from 'react';
39
39
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../Dialog';
40
40
  import { Button } from '../../Button/Button';
41
41
  import { Input } from '../../Input/Input';
42
+ import { Progress } from '../../Progress/Progress';
42
43
  import { Upload, FileText, AlertCircle } from 'lucide-react';
43
44
  import { createLogger } from '../../../utils/logger';
44
45
 
@@ -115,6 +116,7 @@ export function ImportModal({ isOpen, onClose, onImport, config = {} }: ImportMo
115
116
  const [previewData, setPreviewData] = useState<Array<Record<string, unknown>> | null>(null);
116
117
  const [totalCount, setTotalCount] = useState<number>(0);
117
118
  const [validationErrors, setValidationErrors] = useState<Array<{row: number; field: string; message: string}>>([]);
119
+ const [importProgress, setImportProgress] = useState<{ current: number; total: number; stage: 'parsing' | 'importing' } | null>(null);
118
120
  const fileInputRef = useRef<HTMLInputElement>(null);
119
121
  const isMountedRef = useRef(true);
120
122
 
@@ -135,6 +137,7 @@ export function ImportModal({ isOpen, onClose, onImport, config = {} }: ImportMo
135
137
  setError(null);
136
138
  setValidationErrors([]);
137
139
  setIsProcessing(false);
140
+ setImportProgress(null);
138
141
  // Reset file input
139
142
  if (fileInputRef.current) {
140
143
  fileInputRef.current.value = '';
@@ -211,24 +214,126 @@ export function ImportModal({ isOpen, onClose, onImport, config = {} }: ImportMo
211
214
 
212
215
  setIsProcessing(true);
213
216
  setError(null);
217
+ setImportProgress({ current: 0, total: 0, stage: 'parsing' });
214
218
 
215
219
  try {
220
+ // Step 1: Parse CSV with progress indication
216
221
  const text = await file.text();
217
- const data = processCSV(text);
222
+ const lines = text.split('\n').filter(line => line.trim());
223
+ const totalLines = lines.length;
224
+
225
+ if (totalLines < 2) {
226
+ throw new Error('CSV must have at least a header row and one data row');
227
+ }
228
+
229
+ // For large files, process in chunks to show progress
230
+ const CHUNK_SIZE = 1000; // Process 1000 rows at a time
231
+ const data: Array<Record<string, unknown>> = [];
232
+
233
+ if (totalLines > CHUNK_SIZE) {
234
+ // Large file - process in chunks with progress
235
+ const parseCSVLine = (line: string): string[] => {
236
+ const result: string[] = [];
237
+ let current = '';
238
+ let inQuotes = false;
239
+
240
+ for (let i = 0; i < line.length; i++) {
241
+ const char = line[i];
242
+
243
+ if (char === '"') {
244
+ inQuotes = !inQuotes;
245
+ } else if (char === ',' && !inQuotes) {
246
+ result.push(current.trim());
247
+ current = '';
248
+ } else {
249
+ current += char;
250
+ }
251
+ }
252
+ result.push(current.trim());
253
+ return result;
254
+ };
255
+
256
+ const headers = parseCSVLine(lines[0]).map(h => h.replace(/"/g, '').trim());
257
+
258
+ // Process data rows in chunks
259
+ for (let i = 1; i < totalLines; i += CHUNK_SIZE) {
260
+ const chunkEnd = Math.min(i + CHUNK_SIZE, totalLines);
261
+ const chunk = lines.slice(i, chunkEnd);
262
+
263
+ chunk.forEach((line, index) => {
264
+ const values = parseCSVLine(line).map(v => v.replace(/"/g, '').trim());
265
+ const row: Record<string, unknown> = {};
266
+ headers.forEach((header, colIndex) => {
267
+ row[header] = values[colIndex] || '';
268
+ });
269
+ data.push(row);
270
+ });
271
+
272
+ // Update progress
273
+ const processed = Math.min(chunkEnd - 1, totalLines - 1);
274
+ if (isMountedRef.current) {
275
+ setImportProgress({
276
+ current: processed,
277
+ total: totalLines - 1,
278
+ stage: 'parsing'
279
+ });
280
+ }
281
+
282
+ // Yield to browser to update UI
283
+ await new Promise(resolve => setTimeout(resolve, 0));
284
+ }
285
+ } else {
286
+ // Small file - process normally
287
+ const parsedData = processCSV(text);
288
+ data.push(...parsedData);
289
+ if (isMountedRef.current) {
290
+ setImportProgress({
291
+ current: totalLines - 1,
292
+ total: totalLines - 1,
293
+ stage: 'parsing'
294
+ });
295
+ }
296
+ }
297
+
298
+ // Step 2: Import data with progress indication
299
+ if (isMountedRef.current) {
300
+ setImportProgress({
301
+ current: 0,
302
+ total: data.length,
303
+ stage: 'importing'
304
+ });
305
+ }
218
306
 
219
307
  // Await the onImport callback in case it returns a promise
220
308
  const result = onImport(data);
221
309
  if (result && typeof result.then === 'function') {
310
+ // For async imports, we can't track exact progress, but we show it's processing
311
+ // The progress will remain at 0 until the import completes
222
312
  await result;
223
313
  }
314
+ // Note: For synchronous imports, the progress stays at 0 until we mark it complete
315
+
316
+ // Mark as complete
317
+ if (isMountedRef.current) {
318
+ setImportProgress({
319
+ current: data.length,
320
+ total: data.length,
321
+ stage: 'importing'
322
+ });
323
+ }
324
+
325
+ // Small delay to show completion
326
+ await new Promise(resolve => setTimeout(resolve, 300));
224
327
 
225
328
  onClose();
226
329
  setFile(null);
227
330
  } catch (err) {
228
331
  setError(err instanceof Error ? err.message : 'Failed to process file');
332
+ setImportProgress(null);
229
333
  } finally {
230
334
  if (isMountedRef.current) {
231
335
  setIsProcessing(false);
336
+ setImportProgress(null);
232
337
  }
233
338
  }
234
339
  };
@@ -239,6 +344,7 @@ export function ImportModal({ isOpen, onClose, onImport, config = {} }: ImportMo
239
344
  setPreviewData(null);
240
345
  setTotalCount(0);
241
346
  setValidationErrors([]);
347
+ setImportProgress(null);
242
348
  onClose();
243
349
  };
244
350
 
@@ -338,7 +444,33 @@ export function ImportModal({ isOpen, onClose, onImport, config = {} }: ImportMo
338
444
  )}
339
445
 
340
446
 
341
- {file && previewData && previewData.length > 0 ? (
447
+ {importProgress && isProcessing && (
448
+ <div className="space-y-2 p-4 bg-sec-50 rounded-lg border border-sec-200">
449
+ <div className="flex items-center justify-between">
450
+ <span className="text-sm font-medium text-sec-900">
451
+ {importProgress.stage === 'parsing' ? 'Parsing CSV file...' : 'Importing data...'}
452
+ </span>
453
+ <span className="text-sm text-sec-600">
454
+ {importProgress.current.toLocaleString()} / {importProgress.total.toLocaleString()} rows
455
+ </span>
456
+ </div>
457
+ <Progress
458
+ value={importProgress.total > 0 ? (importProgress.current / importProgress.total) * 100 : 0}
459
+ className="h-2 bg-sec-200"
460
+ />
461
+ <p className="text-xs text-sec-500">
462
+ {importProgress.total > 0 && importProgress.current < importProgress.total
463
+ ? `${Math.round((importProgress.current / importProgress.total) * 100)}% complete`
464
+ : importProgress.stage === 'importing' && importProgress.current === 0
465
+ ? 'Processing your data...'
466
+ : importProgress.current === importProgress.total
467
+ ? 'Complete!'
468
+ : 'Processing...'}
469
+ </p>
470
+ </div>
471
+ )}
472
+
473
+ {file && previewData && previewData.length > 0 && !isProcessing ? (
342
474
  <div className="space-y-3">
343
475
  <h4 className="text-sec-900">{previewHeaderText}</h4>
344
476
  <div className="border rounded-lg overflow-hidden">
@@ -44,7 +44,9 @@ export type {
44
44
  PerformanceConfig,
45
45
  ServerSideConfig,
46
46
  ChunkingConfig,
47
- SearchIndexConfig
47
+ SearchIndexConfig,
48
+ // Export types
49
+ ExportOptions
48
50
  } from './types';
49
51
 
50
52
  export {
@@ -604,6 +604,52 @@ export interface RBACContext {
604
604
  pageId: string;
605
605
  }
606
606
 
607
+ // ============================================================================
608
+ // EXPORT TYPES
609
+ // ============================================================================
610
+
611
+ /**
612
+ * Options provided to the onExport handler for custom export functionality
613
+ *
614
+ * @example
615
+ * ```tsx
616
+ * <DataTable
617
+ * onExport={async (options) => {
618
+ * // Export only specific columns
619
+ * const customColumns = options.allColumns.filter(col =>
620
+ * ['name', 'email', 'role'].includes(col.accessorKey || '')
621
+ * );
622
+ * await exportToCSVWithTableRows(
623
+ * options.tableRows,
624
+ * customColumns,
625
+ * options.columnIdToTableColumn,
626
+ * 'custom-export.csv'
627
+ * );
628
+ * }}
629
+ * />
630
+ * ```
631
+ */
632
+ export interface ExportOptions<TData extends DataRecord> {
633
+ /** Filtered table rows with getValue() method for proper accessorFn evaluation */
634
+ tableRows: Array<{
635
+ original: TData;
636
+ getValue: (columnId: string) => any;
637
+ id: string;
638
+ }>;
639
+ /** All column definitions passed to the DataTable */
640
+ allColumns: DataTableColumn<TData>[];
641
+ /** Currently visible columns in the table */
642
+ visibleColumns: DataTableColumn<TData>[];
643
+ /** Mapping of column IDs to TanStack table column instances (for getValue() calls) */
644
+ columnIdToTableColumn: Map<string, any>;
645
+ /** Raw data array (unfiltered) */
646
+ data: TData[];
647
+ /** Default filename generated from table title */
648
+ filename: string;
649
+ /** TanStack table instance for advanced operations */
650
+ table: any; // Using any to avoid tight coupling with TanStack types
651
+ }
652
+
607
653
  // ============================================================================
608
654
  // MAIN COMPONENT PROPS
609
655
  // ============================================================================
@@ -672,6 +718,28 @@ export interface DataTableProps<TData extends DataRecord> {
672
718
  onCreateRow?: (data: Partial<TData>) => void;
673
719
  /** Import handler */
674
720
  onImport?: (data: TData[]) => void | Promise<void>;
721
+ /**
722
+ * Export handler - allows custom export logic with full control over columns and data
723
+ *
724
+ * If not provided, defaults to exporting all visible columns.
725
+ *
726
+ * @example
727
+ * ```tsx
728
+ * // Custom export with specific columns
729
+ * onExport={async (options) => {
730
+ * const exportColumns = options.allColumns.filter(col =>
731
+ * ['name', 'email'].includes(col.accessorKey || '')
732
+ * );
733
+ * await exportToCSVWithTableRows(
734
+ * options.tableRows,
735
+ * exportColumns,
736
+ * options.columnIdToTableColumn,
737
+ * 'users-export.csv'
738
+ * );
739
+ * }}
740
+ * ```
741
+ */
742
+ onExport?: (options: ExportOptions<TData>) => void | Promise<void>;
675
743
  /** Row selection change handler */
676
744
  onRowSelectionChange?: (selection: Record<string, boolean>) => void;
677
745
  /** Controlled selection state */
@@ -617,24 +617,11 @@ const DialogBody = ({
617
617
  return null;
618
618
  }
619
619
 
620
- console.log('🔍 Dialog HTML Debug:', {
621
- originalHtml: htmlContent,
622
- allowHtml,
623
- strictSanitization,
624
- logWarnings
625
- });
626
-
627
620
  const result = renderSafeHtml(htmlContent, {
628
621
  strict: strictSanitization,
629
622
  logWarnings
630
623
  });
631
624
 
632
- console.log('🔍 Dialog HTML Result:', {
633
- sanitizedHtml: result.html,
634
- isValid: result.isValid,
635
- warnings: result.warnings
636
- });
637
-
638
625
  return result.html;
639
626
  }, [htmlContent, allowHtml, strictSanitization, logWarnings]);
640
627
 
@@ -502,7 +502,51 @@ function FileDisplayPublic({
502
502
  const publicPageContext = useContext(PublicPageContext);
503
503
  const supabase = publicPageContext?.supabase ?? null;
504
504
 
505
+ // Step 4: Log Supabase client context
506
+ console.log('[FileDisplayPublic] Supabase Client Context:', {
507
+ hasPublicPageContext: !!publicPageContext,
508
+ hasSupabaseClient: !!supabase,
509
+ supabaseUrl: publicPageContext?.environment?.supabaseUrl || 'not available',
510
+ hasAnonKey: !!publicPageContext?.environment?.supabaseKey,
511
+ hasAuth: !!supabase?.auth,
512
+ organisation_id,
513
+ table_name,
514
+ record_id,
515
+ category,
516
+ context: 'public_page_anonymous_user',
517
+ note: 'Public pages use anonymous Supabase client (no user session)'
518
+ });
519
+
505
520
  if (!supabase) {
521
+ // If fallback is enabled, show fallback UI instead of error
522
+ if (showFallback) {
523
+ return (
524
+ <FileDisplayContent
525
+ isLoading={false}
526
+ error={null}
527
+ fileUrl={null}
528
+ fileReference={null}
529
+ fileReferences={[]}
530
+ fileUrls={new Map()}
531
+ fileCount={0}
532
+ category={category}
533
+ displayOnly={displayOnly}
534
+ showDelete={false}
535
+ className={className}
536
+ children={children}
537
+ onDelete={undefined}
538
+ organisation_id={organisation_id}
539
+ loadingComponent={loadingComponent}
540
+ errorComponent={errorComponent}
541
+ showFallback={showFallback}
542
+ generateFallbackText={generateFallbackText}
543
+ fallbackText={fallbackText}
544
+ fallbackSize={fallbackSize}
545
+ />
546
+ );
547
+ }
548
+
549
+ // Only show error if fallback is not enabled
506
550
  return (
507
551
  <div className={`text-sec-500 text-center p-4 ${className}`}>
508
552
  Supabase client not available in public context
@@ -527,6 +571,38 @@ function FileDisplayPublic({
527
571
  { supabase }
528
572
  );
529
573
 
574
+ // Log errors for debugging public file display issues
575
+ if (error) {
576
+ console.error('[FileDisplayPublic] Error fetching file:', {
577
+ table_name,
578
+ record_id,
579
+ organisation_id,
580
+ category,
581
+ error: error.message,
582
+ errorStack: error.stack
583
+ });
584
+ }
585
+
586
+ // Log when file is successfully loaded
587
+ if (fileUrl && !isLoading && !error) {
588
+ console.log('[FileDisplayPublic] File loaded successfully:', {
589
+ table_name,
590
+ record_id,
591
+ category,
592
+ fileUrl: fileUrl.substring(0, 50) + '...' // Truncate URL for logging
593
+ });
594
+ }
595
+
596
+ // Log when no file is found (but not an error - might be expected)
597
+ if (!isLoading && !error && !fileUrl && !fileReference) {
598
+ console.log('[FileDisplayPublic] No file found (will show fallback if enabled):', {
599
+ table_name,
600
+ record_id,
601
+ category,
602
+ showFallback
603
+ });
604
+ }
605
+
530
606
  // Public context doesn't support delete operations
531
607
  const handleDelete = async () => {
532
608
  // Delete operations are not available in public context for security reasons
@@ -137,6 +137,11 @@ export interface HeaderProps {
137
137
  * A flexible header component that supports various configurations including custom logos,
138
138
  * navigation menus, user authentication, event selection, and custom actions.
139
139
  *
140
+ * **Logo Display:** When used via PaceAppLayout, the logo URL is automatically constructed
141
+ * from the appName prop as `/${appName.toLowerCase()}_logo_wide.svg`. The appName should
142
+ * come from an APP_NAME constant declared in your App.tsx file to ensure consistency across
143
+ * authenticated and public pages.
144
+ *
140
145
  * Features:
141
146
  * - Customizable logo (URL or custom component)
142
147
  * - Clickable logo that automatically routes to dashboard (configurable via logoHref)