@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.
- package/dist/{DataTable-6FN7XDXA.js → DataTable-3Z5HLOWF.js} +6 -6
- package/dist/{PublicLoadingSpinner-CaoRbHvJ.d.ts → PublicLoadingSpinner-CUAnTvcg.d.ts} +41 -21
- package/dist/{UnifiedAuthProvider-6C47WIML.js → UnifiedAuthProvider-CQDZRJIS.js} +3 -3
- package/dist/{chunk-QXGLU2O5.js → chunk-27MGXDD6.js} +282 -147
- package/dist/chunk-27MGXDD6.js.map +1 -0
- package/dist/{chunk-ZBLK676C.js → chunk-3CG5L6RN.js} +1 -19
- package/dist/chunk-3CG5L6RN.js.map +1 -0
- package/dist/{chunk-35ZDPMBM.js → chunk-BYXRHAIF.js} +3 -3
- package/dist/{chunk-IJOZZOGT.js → chunk-CQZU6TFE.js} +5 -5
- package/dist/{chunk-C43QIDN3.js → chunk-CTJRBUX2.js} +2 -2
- package/dist/{chunk-R4CRQUJJ.js → chunk-ENE3AB75.js} +463 -453
- package/dist/chunk-ENE3AB75.js.map +1 -0
- package/dist/{chunk-ESJTIADP.js → chunk-F64FFPOZ.js} +5 -15
- package/dist/{chunk-ESJTIADP.js.map → chunk-F64FFPOZ.js.map} +1 -1
- package/dist/{chunk-4MXVZVNS.js → chunk-TGIY2AR2.js} +2 -2
- package/dist/{chunk-XN6GWKMV.js → chunk-VZ5OR6HD.js} +161 -14
- package/dist/chunk-VZ5OR6HD.js.map +1 -0
- package/dist/{chunk-QWNJCQXZ.js → chunk-ZV77RZMU.js} +2 -2
- package/dist/{chunk-NZGLXZGP.js → chunk-ZYZCRSBD.js} +3 -54
- package/dist/chunk-ZYZCRSBD.js.map +1 -0
- package/dist/components.d.ts +1 -1
- package/dist/components.js +9 -9
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -12
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +10 -62
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +53 -28
- package/docs/api-reference/components.md +24 -0
- package/docs/api-reference/types.md +28 -0
- package/docs/architecture/rpc-function-standards.md +39 -5
- package/docs/implementation-guides/data-tables.md +55 -10
- package/docs/implementation-guides/permission-enforcement.md +4 -0
- package/docs/rbac/super-admin-guide.md +43 -5
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/DataTable/__tests__/DataTable.export.test.tsx +702 -0
- package/src/components/DataTable/components/DataTableCore.tsx +55 -36
- package/src/components/DataTable/components/ImportModal.tsx +134 -2
- package/src/components/DataTable/index.ts +3 -1
- package/src/components/DataTable/types.ts +68 -0
- package/src/components/Dialog/Dialog.tsx +0 -13
- package/src/components/FileDisplay/FileDisplay.tsx +76 -0
- package/src/components/Header/Header.tsx +5 -0
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +72 -50
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +81 -1
- package/src/components/PublicLayout/PublicPageFooter.tsx +1 -1
- package/src/components/PublicLayout/PublicPageHeader.tsx +69 -128
- package/src/components/PublicLayout/PublicPageLayout.tsx +4 -4
- package/src/components/PublicLayout/PublicPageProvider.tsx +12 -3
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +3 -18
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +3 -1
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +11 -5
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +8 -7
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +41 -46
- package/src/hooks/public/usePublicFileDisplay.ts +176 -7
- package/src/hooks/public/usePublicRouteParams.ts +0 -12
- package/src/hooks/useAppConfig.ts +15 -6
- package/src/hooks/usePermissionCache.test.ts +12 -4
- package/src/hooks/usePermissionCache.ts +3 -19
- package/src/hooks/useSecureDataAccess.ts +0 -63
- package/src/services/EventService.ts +0 -19
- package/dist/chunk-NZGLXZGP.js.map +0 -1
- package/dist/chunk-QXGLU2O5.js.map +0 -1
- package/dist/chunk-R4CRQUJJ.js.map +0 -1
- package/dist/chunk-XN6GWKMV.js.map +0 -1
- package/dist/chunk-ZBLK676C.js.map +0 -1
- /package/dist/{DataTable-6FN7XDXA.js.map → DataTable-3Z5HLOWF.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-6C47WIML.js.map → UnifiedAuthProvider-CQDZRJIS.js.map} +0 -0
- /package/dist/{chunk-35ZDPMBM.js.map → chunk-BYXRHAIF.js.map} +0 -0
- /package/dist/{chunk-IJOZZOGT.js.map → chunk-CQZU6TFE.js.map} +0 -0
- /package/dist/{chunk-C43QIDN3.js.map → chunk-CTJRBUX2.js.map} +0 -0
- /package/dist/{chunk-4MXVZVNS.js.map → chunk-TGIY2AR2.js.map} +0 -0
- /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={
|
|
1015
|
+
onExport={async () => {
|
|
1015
1016
|
try {
|
|
1016
|
-
//
|
|
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
|
|
1030
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
{
|
|
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">
|
|
@@ -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)
|