@jmruthers/pace-core 0.5.87 → 0.5.88
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/{AuthService-Df3IozMG.d.ts → AuthService-DcTI5Ov4.d.ts} +9 -0
- package/dist/{DataTable-FA6EUX5M.js → DataTable-PWBMKMOG.js} +7 -7
- package/dist/{PublicLoadingSpinner-DecuJBX0.d.ts → PublicLoadingSpinner-BQXD1fbO.d.ts} +160 -130
- package/dist/{UnifiedAuthProvider-K2IZAY5F.js → UnifiedAuthProvider-5D3HEQND.js} +4 -4
- package/dist/{UnifiedAuthProvider-B391Aqum.d.ts → UnifiedAuthProvider-BVKmQd9u.d.ts} +4 -0
- package/dist/auth-DReDSLq9.d.ts +16 -0
- package/dist/{chunk-CBSD3BZ3.js → chunk-3RZBKQ5Y.js} +2 -6
- package/dist/{chunk-CBSD3BZ3.js.map → chunk-3RZBKQ5Y.js.map} +1 -1
- package/dist/{chunk-NTW3KGS4.js → chunk-6UHXQH7P.js} +5 -5
- package/dist/{chunk-YVUZWLQG.js → chunk-AQGF5OG7.js} +3 -3
- package/dist/{chunk-CVMVPYAL.js → chunk-BDZUMRBD.js} +3 -5
- package/dist/chunk-BDZUMRBD.js.map +1 -0
- package/dist/{chunk-KAY3K5TP.js → chunk-BNXBJOGL.js} +4 -4
- package/dist/{chunk-I7O3RSMN.js → chunk-CJIZS3UE.js} +1298 -769
- package/dist/chunk-CJIZS3UE.js.map +1 -0
- package/dist/{chunk-S3JKDMD5.js → chunk-CXKMRKRF.js} +4 -4
- package/dist/{chunk-5BN3YGNK.js → chunk-DP5X5ORK.js} +217 -27
- package/dist/chunk-DP5X5ORK.js.map +1 -0
- package/dist/{chunk-ZFLOV3OM.js → chunk-H3P2RGKZ.js} +352 -16
- package/dist/chunk-H3P2RGKZ.js.map +1 -0
- package/dist/{chunk-RIXPZJUB.js → chunk-KTPG5VCH.js} +2 -2
- package/dist/{chunk-WUXCWRL6.js → chunk-XJ2HZOBU.js} +6 -1
- package/dist/chunk-XJ2HZOBU.js.map +1 -0
- package/dist/{chunk-2FQEQUJT.js → chunk-XXVM53P4.js} +4 -4
- package/dist/{chunk-I2VVV5PQ.js → chunk-YY4YYM3E.js} +2 -2
- package/dist/components.d.ts +6 -55
- package/dist/components.js +24 -205
- package/dist/components.js.map +1 -1
- package/dist/{file-reference-9xUOnwyt.d.ts → file-reference-C9isKNPn.d.ts} +67 -2
- package/dist/hooks.js +9 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +152 -26
- package/dist/index.js +64 -194
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +5 -3
- package/dist/providers.js +3 -3
- package/dist/rbac/index.js +8 -8
- package/dist/types.d.ts +2 -1
- package/dist/types.js +3 -3
- package/dist/utils.js +2 -2
- package/docs/DOCUMENTATION_AUDIT.md +6 -6
- package/docs/DOCUMENTATION_STANDARD.md +137 -0
- package/docs/README.md +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 +83 -40
- package/docs/api/enums/FileCategory.md +56 -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/EventLogoProps.md +11 -11
- package/docs/api/interfaces/FileDisplayProps.md +10 -10
- 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 +8 -8
- package/docs/api/interfaces/FileUploadProps.md +137 -42
- package/docs/api/interfaces/FooterProps.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 +1 -1
- 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/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 +1 -1
- 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/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.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 +83 -50
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
- package/docs/api/interfaces/UseEventLogoOptions.md +74 -0
- package/docs/api/interfaces/UseEventLogoReturn.md +81 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +6 -6
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.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 +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +290 -95
- package/docs/api-reference/components.md +1 -18
- package/docs/api-reference/hooks.md +1 -4
- package/docs/best-practices/testing.md +2 -0
- package/docs/documentation-index.md +1 -1
- package/docs/getting-started/faq.md +1 -1
- package/docs/implementation-guides/file-reference-system.md +592 -58
- package/docs/implementation-guides/file-upload-storage.md +137 -73
- package/docs/rbac/super-admin-guide.md +18 -70
- package/docs/testing/README.md +2 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +674 -0
- package/src/__tests__/helpers/test-utils.tsx +3 -2
- package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx.skip → DataTable.comprehensive.test.tsx} +17 -18
- package/src/components/DataTable/__tests__/{DataTable.test.tsx.skip → DataTable.test.tsx} +14 -22
- package/src/components/DataTable/__tests__/{ssr.strict-mode.test.tsx.skip → ssr.strict-mode.test.tsx} +42 -47
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +1 -1
- package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +13 -4
- package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +1 -1
- package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +10 -6
- package/src/components/FileDisplay/FileDisplay.test.tsx +257 -0
- package/src/components/{FileDisplay.tsx → FileDisplay/FileDisplay.tsx} +111 -10
- package/src/components/FileDisplay/index.tsx +4 -0
- package/src/components/FileUpload/FileUpload.test.tsx +171 -621
- package/src/components/FileUpload/FileUpload.tsx +512 -168
- package/src/components/FileUpload/index.tsx +4 -0
- package/src/components/Progress/Progress.test.tsx +38 -0
- package/src/components/PublicLayout/EventLogo.tsx +6 -4
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/SessionRestorationLoader.tsx +48 -0
- package/src/components/Toast/Toast.tsx +13 -8
- package/src/components/index.ts +16 -16
- package/src/hooks/__tests__/ServiceHooks.test.tsx +615 -0
- package/src/hooks/public/usePublicEventLogo.ts +16 -20
- package/src/hooks/useEventLogo.ts +316 -0
- package/src/hooks/useEvents.ts +0 -5
- package/src/hooks/useFileReference.test.ts +659 -0
- package/src/hooks/useFileReference.ts +207 -3
- package/src/hooks/useSessionRestoration.ts +64 -0
- package/src/index.ts +17 -5
- package/src/providers/{UnifiedAuthProvider.test.simple.tsx → UnifiedAuthProvider.smoke.test.tsx} +81 -60
- package/src/providers/services/AuthServiceProvider.tsx +27 -3
- package/src/providers/services/UnifiedAuthProvider.tsx +34 -5
- package/src/rbac/{engine.test.simple.ts → RBACEngine.smoke.test.ts} +17 -12
- package/src/services/AuthService.ts +142 -20
- package/src/services/EventService.ts +0 -4
- package/src/types/auth.ts +15 -0
- package/src/types/file-reference.ts +73 -1
- package/src/types/index.ts +1 -0
- package/src/utils/__tests__/organisationContext.unit.test.ts +2 -4
- package/src/utils/appNameResolver.simple.test.ts +99 -29
- package/src/utils/file-reference.test.ts +535 -0
- package/src/utils/file-reference.ts +200 -30
- package/src/utils/organisationContext.test.ts +5 -19
- package/src/utils/organisationContext.ts +3 -5
- package/src/utils/storage/README.md +269 -262
- package/src/utils/storage/config.ts +9 -0
- package/src/utils/storage/helpers.test.ts +631 -0
- package/src/utils/storage/helpers.ts +112 -14
- package/src/utils/storage/index.ts +3 -0
- package/src/validation/__tests__/sanitization.unit.test.ts +1 -1
- package/src/validation/__tests__/schemaUtils.unit.test.ts +1 -1
- package/src/validation/__tests__/user.unit.test.ts +1 -1
- package/dist/chunk-5BN3YGNK.js.map +0 -1
- package/dist/chunk-CVMVPYAL.js.map +0 -1
- package/dist/chunk-I7O3RSMN.js.map +0 -1
- package/dist/chunk-WUXCWRL6.js.map +0 -1
- package/dist/chunk-ZFLOV3OM.js.map +0 -1
- package/docs/CONTENT_AUDIT_REPORT.md +0 -253
- package/docs/STYLE_GUIDE.md +0 -37
- package/examples/RBAC/__tests__/PermissionExample.test.tsx +0 -150
- package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +0 -159
- package/src/__tests__/TEST_GUIDE_CURSOR.md +0 -1605
- package/src/__tests__/TEST_GUIDE_HUMAN.md +0 -103
- package/src/components/FileUpload/FileUpload.example.tsx +0 -218
- package/src/components/FileUpload/index.ts +0 -6
- package/src/components/FileUpload.tsx +0 -176
- package/src/components/Progress/index.ts +0 -3
- package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +0 -666
- package/src/components/SuperAdminGuard.tsx +0 -116
- package/src/components/__tests__/FileDisplay.test.tsx +0 -575
- package/src/components/__tests__/FileUpload.test.tsx +0 -446
- package/src/components/__tests__/SuperAdminGuard.test.tsx +0 -627
- package/src/components/examples/PermissionExample.tsx +0 -173
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -583
- package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +0 -640
- package/src/types/__tests__/file-reference.test.ts +0 -447
- package/src/utils/__tests__/file-reference.test.ts +0 -383
- /package/dist/{DataTable-FA6EUX5M.js.map → DataTable-PWBMKMOG.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-K2IZAY5F.js.map → UnifiedAuthProvider-5D3HEQND.js.map} +0 -0
- /package/dist/{chunk-NTW3KGS4.js.map → chunk-6UHXQH7P.js.map} +0 -0
- /package/dist/{chunk-YVUZWLQG.js.map → chunk-AQGF5OG7.js.map} +0 -0
- /package/dist/{chunk-KAY3K5TP.js.map → chunk-BNXBJOGL.js.map} +0 -0
- /package/dist/{chunk-S3JKDMD5.js.map → chunk-CXKMRKRF.js.map} +0 -0
- /package/dist/{chunk-RIXPZJUB.js.map → chunk-KTPG5VCH.js.map} +0 -0
- /package/dist/{chunk-2FQEQUJT.js.map → chunk-XXVM53P4.js.map} +0 -0
- /package/dist/{chunk-I2VVV5PQ.js.map → chunk-YY4YYM3E.js.map} +0 -0
- /package/src/providers/{OrganisationProvider.test.simple.tsx → OrganisationProvider.context.test.tsx} +0 -0
|
@@ -1,237 +1,581 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
// File Upload Component
|
|
2
|
+
// Provides a file upload interface using the file reference system
|
|
4
3
|
|
|
5
|
-
import React, { useCallback,
|
|
4
|
+
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
6
5
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { Alert } from '../Alert/Alert';
|
|
6
|
+
import { FileCategory, FileUploadResult, UploadProgress } from '../../types/file-reference';
|
|
7
|
+
import { useFileReference } from '../../hooks/useFileReference';
|
|
8
|
+
import { getCurrentAppName } from '../../utils/appNameResolver';
|
|
9
|
+
import { getAppId } from '../../utils/appIdResolver';
|
|
12
10
|
|
|
13
11
|
export interface FileUploadProps {
|
|
14
12
|
supabase: SupabaseClient;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
table_name: string;
|
|
14
|
+
record_id: string;
|
|
15
|
+
organisation_id: string;
|
|
16
|
+
app_id?: string; // Optional - will be resolved from app name if not provided
|
|
17
|
+
category: FileCategory;
|
|
19
18
|
accept?: string;
|
|
20
19
|
maxSize?: number;
|
|
21
20
|
multiple?: boolean;
|
|
22
21
|
disabled?: boolean;
|
|
22
|
+
isPublic?: boolean; // Whether files should be uploaded to public-files bucket
|
|
23
23
|
className?: string;
|
|
24
|
+
showPreview?: boolean; // Show image preview for accepted files
|
|
25
|
+
showProgress?: boolean; // Show upload progress bar
|
|
26
|
+
onUploadSuccess?: (result: FileUploadResult) => void;
|
|
27
|
+
onUploadError?: (error: string, file?: File) => void;
|
|
28
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
24
29
|
children?: React.ReactNode;
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
interface FileUploadState {
|
|
33
|
+
file: File;
|
|
34
|
+
progress: UploadProgress;
|
|
35
|
+
preview?: string;
|
|
36
|
+
result?: FileUploadResult;
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
export function FileUpload({
|
|
28
40
|
supabase,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
table_name,
|
|
42
|
+
record_id,
|
|
43
|
+
organisation_id,
|
|
44
|
+
app_id,
|
|
45
|
+
category,
|
|
33
46
|
accept = '*/*',
|
|
34
|
-
maxSize,
|
|
47
|
+
maxSize = 10 * 1024 * 1024, // 10MB default
|
|
35
48
|
multiple = false,
|
|
36
49
|
disabled = false,
|
|
50
|
+
isPublic = false,
|
|
37
51
|
className = '',
|
|
52
|
+
showPreview = true,
|
|
53
|
+
showProgress = true,
|
|
54
|
+
onUploadSuccess,
|
|
55
|
+
onUploadError,
|
|
56
|
+
onProgress,
|
|
38
57
|
children
|
|
39
58
|
}: FileUploadProps) {
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
42
|
-
const [
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
60
|
+
const [uploadStates, setUploadStates] = useState<Map<string, FileUploadState>>(new Map());
|
|
61
|
+
const [resolvedAppId, setResolvedAppId] = useState<string | null>(app_id || null);
|
|
62
|
+
const [isResolvingAppId, setIsResolvingAppId] = useState(!app_id);
|
|
63
|
+
const [appIdError, setAppIdError] = useState<string | null>(null);
|
|
64
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
65
|
+
const { uploadFile, isLoading, error } = useFileReference(supabase);
|
|
66
|
+
|
|
67
|
+
// Resolve app_id from app name if not provided
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (app_id) {
|
|
70
|
+
// If app_id is provided, use it directly
|
|
71
|
+
setResolvedAppId(app_id);
|
|
72
|
+
setIsResolvingAppId(false);
|
|
73
|
+
setAppIdError(null);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Otherwise, resolve from app name
|
|
78
|
+
const resolveAppId = async () => {
|
|
79
|
+
setIsResolvingAppId(true);
|
|
80
|
+
setAppIdError(null);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const appName = getCurrentAppName();
|
|
84
|
+
if (!appName) {
|
|
85
|
+
const errorMsg = 'App ID is required. Either provide app_id prop or set app name via setRBACAppName()';
|
|
86
|
+
setAppIdError(errorMsg);
|
|
87
|
+
setIsResolvingAppId(false);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolvedId = await getAppId(supabase, appName);
|
|
92
|
+
if (!resolvedId) {
|
|
93
|
+
const errorMsg = `Failed to resolve app ID for app name "${appName}". Make sure the app is registered in rbac_apps table.`;
|
|
94
|
+
setAppIdError(errorMsg);
|
|
95
|
+
setIsResolvingAppId(false);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setResolvedAppId(resolvedId);
|
|
100
|
+
setIsResolvingAppId(false);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to resolve app ID';
|
|
103
|
+
setAppIdError(errorMessage);
|
|
104
|
+
setIsResolvingAppId(false);
|
|
59
105
|
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
resolveAppId();
|
|
109
|
+
}, [app_id, supabase]);
|
|
60
110
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
111
|
+
// Calculate isUploading and isDisabled early so they can be used in callbacks
|
|
112
|
+
const isUploading = useMemo(() => {
|
|
113
|
+
return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
|
|
114
|
+
state.progress.status === 'uploading' || state.progress.status === 'processing'
|
|
115
|
+
);
|
|
116
|
+
}, [uploadStates]);
|
|
117
|
+
|
|
118
|
+
const isDisabled = useMemo(() => {
|
|
119
|
+
return disabled || isUploading || isResolvingAppId || !resolvedAppId;
|
|
120
|
+
}, [disabled, isUploading, isResolvingAppId, resolvedAppId]);
|
|
121
|
+
|
|
122
|
+
// Generate preview URL for images
|
|
123
|
+
const generatePreview = useCallback((file: File): Promise<string | null> => {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
if (!file.type.startsWith('image/')) {
|
|
126
|
+
resolve(null);
|
|
127
|
+
return;
|
|
66
128
|
}
|
|
129
|
+
|
|
130
|
+
const reader = new FileReader();
|
|
131
|
+
reader.onload = (e) => {
|
|
132
|
+
resolve(e.target?.result as string || null);
|
|
133
|
+
};
|
|
134
|
+
reader.onerror = () => resolve(null);
|
|
135
|
+
reader.readAsDataURL(file);
|
|
67
136
|
});
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
// Validate file
|
|
140
|
+
const validateFile = useCallback((file: File): string | null => {
|
|
141
|
+
// Check file size
|
|
142
|
+
if (file.size > maxSize) {
|
|
143
|
+
return `File "${file.name}" exceeds maximum size of ${Math.round(maxSize / 1024 / 1024)}MB`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check file type if accept is specified
|
|
147
|
+
if (accept !== '*/*') {
|
|
148
|
+
const acceptedTypes = accept.split(',').map(type => type.trim());
|
|
149
|
+
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
|
|
150
|
+
const fileMimeType = file.type;
|
|
68
151
|
|
|
69
|
-
|
|
70
|
-
|
|
152
|
+
const isAccepted = acceptedTypes.some(accepted => {
|
|
153
|
+
if (accepted.startsWith('.')) {
|
|
154
|
+
// Extension match
|
|
155
|
+
return accepted === fileExtension;
|
|
156
|
+
} else if (accepted.includes('/*')) {
|
|
157
|
+
// MIME type wildcard (e.g., "image/*")
|
|
158
|
+
const baseType = accepted.split('/')[0];
|
|
159
|
+
return fileMimeType.startsWith(baseType + '/');
|
|
160
|
+
} else {
|
|
161
|
+
// Exact MIME type match
|
|
162
|
+
return accepted === fileMimeType;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
71
165
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
166
|
+
if (!isAccepted) {
|
|
167
|
+
return `File "${file.name}" is not an accepted format. Accepted: ${accept}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}, [accept, maxSize]);
|
|
173
|
+
|
|
174
|
+
const handleFileSelect = useCallback(async (files: FileList | null) => {
|
|
175
|
+
if (!files || files.length === 0) return;
|
|
75
176
|
|
|
76
177
|
const fileArray = Array.from(files);
|
|
77
|
-
|
|
178
|
+
|
|
179
|
+
// Validate all files first
|
|
180
|
+
const validationErrors: string[] = [];
|
|
181
|
+
const validFiles: File[] = [];
|
|
182
|
+
|
|
183
|
+
for (const file of fileArray) {
|
|
184
|
+
const error = validateFile(file);
|
|
185
|
+
if (error) {
|
|
186
|
+
validationErrors.push(error);
|
|
187
|
+
onUploadError?.(error, file);
|
|
188
|
+
} else {
|
|
189
|
+
validFiles.push(file);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (validFiles.length === 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Initialize upload states
|
|
198
|
+
const newUploadStates = new Map<string, FileUploadState>();
|
|
199
|
+
|
|
200
|
+
for (const file of validFiles) {
|
|
201
|
+
const fileId = `${file.name}-${file.size}-${Date.now()}`;
|
|
202
|
+
const preview = showPreview ? (await generatePreview(file)) || undefined : undefined;
|
|
203
|
+
|
|
204
|
+
const progress: UploadProgress = {
|
|
205
|
+
loaded: 0,
|
|
206
|
+
total: file.size,
|
|
207
|
+
percentage: 0,
|
|
208
|
+
fileName: file.name,
|
|
209
|
+
status: 'idle'
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
newUploadStates.set(fileId, {
|
|
213
|
+
file,
|
|
214
|
+
progress,
|
|
215
|
+
preview
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setUploadStates(newUploadStates);
|
|
220
|
+
|
|
221
|
+
// Upload files sequentially
|
|
222
|
+
for (const [fileId, uploadState] of newUploadStates.entries()) {
|
|
223
|
+
const { file } = uploadState;
|
|
224
|
+
|
|
225
|
+
// Update status to uploading
|
|
226
|
+
setUploadStates(prev => {
|
|
227
|
+
const updated = new Map(prev);
|
|
228
|
+
const state = updated.get(fileId);
|
|
229
|
+
if (state) {
|
|
230
|
+
updated.set(fileId, {
|
|
231
|
+
...state,
|
|
232
|
+
progress: {
|
|
233
|
+
...state.progress,
|
|
234
|
+
status: 'uploading'
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return updated;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
// Simulate progress updates (Supabase doesn't provide real progress, so we estimate)
|
|
243
|
+
const progressInterval = setInterval(() => {
|
|
244
|
+
setUploadStates(prev => {
|
|
245
|
+
const updated = new Map(prev);
|
|
246
|
+
const state = updated.get(fileId);
|
|
247
|
+
if (state && state.progress.status === 'uploading') {
|
|
248
|
+
const estimatedProgress = Math.min(
|
|
249
|
+
state.progress.percentage + 10,
|
|
250
|
+
90
|
|
251
|
+
);
|
|
252
|
+
const newProgress: UploadProgress = {
|
|
253
|
+
...state.progress,
|
|
254
|
+
percentage: estimatedProgress,
|
|
255
|
+
loaded: Math.floor((estimatedProgress / 100) * file.size)
|
|
256
|
+
};
|
|
257
|
+
onProgress?.(newProgress);
|
|
258
|
+
updated.set(fileId, {
|
|
259
|
+
...state,
|
|
260
|
+
progress: newProgress
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return updated;
|
|
264
|
+
});
|
|
265
|
+
}, 200);
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
// Use resolved app_id
|
|
269
|
+
if (!resolvedAppId) {
|
|
270
|
+
const errorMsg = appIdError || 'App ID not available. Please provide app_id prop or set app name.';
|
|
271
|
+
throw new Error(errorMsg);
|
|
272
|
+
}
|
|
78
273
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
274
|
+
const result = await uploadFile({
|
|
275
|
+
table_name,
|
|
276
|
+
record_id,
|
|
277
|
+
organisation_id,
|
|
278
|
+
app_id: resolvedAppId,
|
|
279
|
+
category,
|
|
280
|
+
is_public: isPublic
|
|
281
|
+
}, file);
|
|
82
282
|
|
|
83
|
-
|
|
84
|
-
|
|
283
|
+
clearInterval(progressInterval);
|
|
284
|
+
|
|
285
|
+
if (result) {
|
|
286
|
+
// Update status to completed
|
|
287
|
+
setUploadStates(prev => {
|
|
288
|
+
const updated = new Map(prev);
|
|
289
|
+
const state = updated.get(fileId);
|
|
290
|
+
if (state) {
|
|
291
|
+
updated.set(fileId, {
|
|
292
|
+
...state,
|
|
293
|
+
progress: {
|
|
294
|
+
...state.progress,
|
|
295
|
+
status: 'completed',
|
|
296
|
+
percentage: 100,
|
|
297
|
+
loaded: file.size
|
|
298
|
+
},
|
|
299
|
+
result
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return updated;
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const finalProgress: UploadProgress = {
|
|
306
|
+
loaded: file.size,
|
|
307
|
+
total: file.size,
|
|
308
|
+
percentage: 100,
|
|
309
|
+
fileName: file.name,
|
|
310
|
+
status: 'completed'
|
|
311
|
+
};
|
|
312
|
+
onProgress?.(finalProgress);
|
|
313
|
+
onUploadSuccess?.(result);
|
|
314
|
+
|
|
315
|
+
// Remove from upload states after a delay
|
|
316
|
+
setTimeout(() => {
|
|
317
|
+
setUploadStates(prev => {
|
|
318
|
+
const updated = new Map(prev);
|
|
319
|
+
updated.delete(fileId);
|
|
320
|
+
return updated;
|
|
321
|
+
});
|
|
322
|
+
}, 3000); // Keep success state for 3 seconds
|
|
323
|
+
} else {
|
|
324
|
+
// Update status to error
|
|
325
|
+
setUploadStates(prev => {
|
|
326
|
+
const updated = new Map(prev);
|
|
327
|
+
const state = updated.get(fileId);
|
|
328
|
+
if (state) {
|
|
329
|
+
updated.set(fileId, {
|
|
330
|
+
...state,
|
|
331
|
+
progress: {
|
|
332
|
+
...state.progress,
|
|
333
|
+
status: 'error',
|
|
334
|
+
error: 'Upload failed'
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return updated;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const errorProgress: UploadProgress = {
|
|
342
|
+
loaded: 0,
|
|
343
|
+
total: file.size,
|
|
344
|
+
percentage: 0,
|
|
345
|
+
fileName: file.name,
|
|
346
|
+
status: 'error',
|
|
347
|
+
error: 'Upload failed'
|
|
348
|
+
};
|
|
349
|
+
onProgress?.(errorProgress);
|
|
350
|
+
onUploadError?.('Upload failed', file);
|
|
351
|
+
}
|
|
352
|
+
} catch (err) {
|
|
353
|
+
const errorMessage = err instanceof Error ? err.message : 'Upload failed';
|
|
354
|
+
|
|
355
|
+
setUploadStates(prev => {
|
|
356
|
+
const updated = new Map(prev);
|
|
357
|
+
const state = updated.get(fileId);
|
|
358
|
+
if (state) {
|
|
359
|
+
updated.set(fileId, {
|
|
360
|
+
...state,
|
|
361
|
+
progress: {
|
|
362
|
+
...state.progress,
|
|
363
|
+
status: 'error',
|
|
364
|
+
error: errorMessage
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return updated;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const errorProgress: UploadProgress = {
|
|
372
|
+
loaded: 0,
|
|
373
|
+
total: file.size,
|
|
374
|
+
percentage: 0,
|
|
375
|
+
fileName: file.name,
|
|
376
|
+
status: 'error',
|
|
377
|
+
error: errorMessage
|
|
378
|
+
};
|
|
379
|
+
onProgress?.(errorProgress);
|
|
380
|
+
onUploadError?.(errorMessage, file);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}, [uploadFile, table_name, record_id, organisation_id, resolvedAppId, category, isPublic, maxSize, onUploadSuccess, onUploadError, onProgress, validateFile, generatePreview, showPreview, appIdError]);
|
|
384
|
+
|
|
385
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
85
386
|
e.preventDefault();
|
|
86
387
|
e.stopPropagation();
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
} else if (e.type === 'dragleave') {
|
|
90
|
-
setDragActive(false);
|
|
388
|
+
if (!isDisabled) {
|
|
389
|
+
setIsDragging(true);
|
|
91
390
|
}
|
|
391
|
+
}, [isDisabled]);
|
|
392
|
+
|
|
393
|
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
e.stopPropagation();
|
|
396
|
+
setIsDragging(false);
|
|
92
397
|
}, []);
|
|
93
398
|
|
|
94
399
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
95
400
|
e.preventDefault();
|
|
96
401
|
e.stopPropagation();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
402
|
+
setIsDragging(false);
|
|
403
|
+
|
|
404
|
+
if (isDisabled) return;
|
|
405
|
+
|
|
101
406
|
const files = e.dataTransfer.files;
|
|
102
407
|
handleFileSelect(files);
|
|
103
|
-
}, [
|
|
408
|
+
}, [isDisabled, handleFileSelect]);
|
|
104
409
|
|
|
105
|
-
// Handle file input change
|
|
106
410
|
const handleFileInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
107
411
|
handleFileSelect(e.target.files);
|
|
412
|
+
// Reset input value to allow re-uploading the same file
|
|
413
|
+
if (e.target) {
|
|
414
|
+
e.target.value = '';
|
|
415
|
+
}
|
|
108
416
|
}, [handleFileSelect]);
|
|
109
417
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
onUploadStart?.();
|
|
115
|
-
|
|
116
|
-
for (const file of selectedFiles) {
|
|
117
|
-
const result = await uploadWithProgress(file);
|
|
118
|
-
onUploadComplete?.(result);
|
|
418
|
+
const handleClick = useCallback(() => {
|
|
419
|
+
if (!isDisabled && fileInputRef.current) {
|
|
420
|
+
fileInputRef.current.click();
|
|
119
421
|
}
|
|
422
|
+
}, [isDisabled]);
|
|
120
423
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
setSelectedFiles([]);
|
|
129
|
-
setValidationErrors([]);
|
|
130
|
-
}, []);
|
|
424
|
+
const formatFileSize = (bytes: number): string => {
|
|
425
|
+
if (bytes === 0) return '0 Bytes';
|
|
426
|
+
const k = 1024;
|
|
427
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
428
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
429
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
430
|
+
};
|
|
131
431
|
|
|
132
|
-
const
|
|
432
|
+
const dragClasses = isDragging ? 'border-main-500 bg-main-50' : 'border-sec-300 hover:border-sec-400';
|
|
433
|
+
const disabledClasses = isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-sec-50';
|
|
133
434
|
|
|
134
435
|
return (
|
|
135
|
-
<div className={`
|
|
136
|
-
{/* Drop zone */}
|
|
436
|
+
<div className={`space-y-4 ${className}`}>
|
|
137
437
|
<div
|
|
138
|
-
className={`
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
onDragEnter={handleDrag}
|
|
142
|
-
onDragLeave={handleDrag}
|
|
143
|
-
onDragOver={handleDrag}
|
|
438
|
+
className={`relative border-2 border-dashed rounded-lg p-6 text-center transition-colors ${dragClasses} ${disabledClasses}`}
|
|
439
|
+
onDragOver={handleDragOver}
|
|
440
|
+
onDragLeave={handleDragLeave}
|
|
144
441
|
onDrop={handleDrop}
|
|
442
|
+
onClick={!isDisabled ? handleClick : undefined}
|
|
145
443
|
>
|
|
146
|
-
<input
|
|
147
|
-
type="file"
|
|
148
|
-
accept={accept}
|
|
149
|
-
multiple={multiple}
|
|
150
|
-
onChange={handleFileInputChange}
|
|
151
|
-
disabled={disabled}
|
|
152
|
-
className="file-upload__input"
|
|
153
|
-
/>
|
|
154
|
-
|
|
155
444
|
{children || (
|
|
156
|
-
<div className="
|
|
157
|
-
<
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
445
|
+
<div className="space-y-2">
|
|
446
|
+
<input
|
|
447
|
+
ref={fileInputRef}
|
|
448
|
+
type="file"
|
|
449
|
+
accept={accept}
|
|
450
|
+
multiple={multiple}
|
|
451
|
+
onChange={handleFileInputChange}
|
|
452
|
+
className="hidden"
|
|
453
|
+
disabled={isDisabled}
|
|
454
|
+
data-testid="file-input"
|
|
455
|
+
/>
|
|
456
|
+
<div className="text-sec-600">
|
|
457
|
+
{isResolvingAppId ? (
|
|
458
|
+
'Resolving app configuration...'
|
|
459
|
+
) : isDragging ? (
|
|
460
|
+
'Drop files here...'
|
|
461
|
+
) : (
|
|
462
|
+
<>
|
|
463
|
+
<span className="font-medium">Click to upload</span>
|
|
464
|
+
{' '}or drag and drop
|
|
465
|
+
</>
|
|
466
|
+
)}
|
|
467
|
+
</div>
|
|
468
|
+
<div className="text-sm text-sec-500">
|
|
469
|
+
{!isResolvingAppId && accept !== '*/*' && `Accepted formats: ${accept}`}
|
|
470
|
+
{!isResolvingAppId && maxSize && ` • Max size: ${Math.round(maxSize / 1024 / 1024)}MB`}
|
|
471
|
+
{!isResolvingAppId && multiple && ' • Multiple files allowed'}
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
)}
|
|
475
|
+
|
|
476
|
+
{isUploading && !showProgress && (
|
|
477
|
+
<div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center">
|
|
478
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-main-500"></div>
|
|
168
479
|
</div>
|
|
169
480
|
)}
|
|
170
481
|
</div>
|
|
171
482
|
|
|
172
|
-
{/*
|
|
173
|
-
{
|
|
174
|
-
<div className="
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<span className="file-upload__file-size">{formatFileSize(file.size)}</span>
|
|
181
|
-
</li>
|
|
182
|
-
))}
|
|
183
|
-
</ul>
|
|
184
|
-
</div>
|
|
185
|
-
)}
|
|
483
|
+
{/* Upload Progress List */}
|
|
484
|
+
{showProgress && uploadStates.size > 0 && (
|
|
485
|
+
<div className="space-y-2">
|
|
486
|
+
{Array.from(uploadStates.entries()).map(([fileId, uploadState]) => {
|
|
487
|
+
const { file, progress, preview, result } = uploadState;
|
|
488
|
+
const isError = progress.status === 'error';
|
|
489
|
+
const isCompleted = progress.status === 'completed';
|
|
490
|
+
const isUploading = progress.status === 'uploading' || progress.status === 'processing';
|
|
186
491
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
492
|
+
return (
|
|
493
|
+
<div
|
|
494
|
+
key={fileId}
|
|
495
|
+
className={`flex items-center space-x-3 p-3 rounded-lg border ${
|
|
496
|
+
isError
|
|
497
|
+
? 'bg-acc-50 border-acc-200'
|
|
498
|
+
: isCompleted
|
|
499
|
+
? 'bg-success-50 border-success-200'
|
|
500
|
+
: 'bg-sec-50 border-sec-200'
|
|
501
|
+
}`}
|
|
502
|
+
>
|
|
503
|
+
{/* Preview/Icon */}
|
|
504
|
+
<div className="flex-shrink-0">
|
|
505
|
+
{preview ? (
|
|
506
|
+
<img
|
|
507
|
+
src={preview}
|
|
508
|
+
alt={file.name}
|
|
509
|
+
className="w-12 h-12 object-cover rounded"
|
|
510
|
+
/>
|
|
511
|
+
) : (
|
|
512
|
+
<div className="w-12 h-12 flex items-center justify-center bg-sec-200 rounded">
|
|
513
|
+
<span className="text-2xl">📄</span>
|
|
514
|
+
</div>
|
|
515
|
+
)}
|
|
516
|
+
</div>
|
|
197
517
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
518
|
+
{/* File Info */}
|
|
519
|
+
<div className="flex-1 min-w-0">
|
|
520
|
+
<div className="font-medium text-sec-900 truncate">
|
|
521
|
+
{file.name}
|
|
522
|
+
</div>
|
|
523
|
+
<div className="text-sm text-sec-500">
|
|
524
|
+
{formatFileSize(file.size)}
|
|
525
|
+
{isCompleted && result && ' • Uploaded'}
|
|
526
|
+
{isError && progress.error && ` • ${progress.error}`}
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
{/* Progress Bar */}
|
|
530
|
+
{showProgress && (isUploading || isError) && (
|
|
531
|
+
<div className="mt-2">
|
|
532
|
+
<div className="w-full bg-sec-200 rounded-full h-2">
|
|
533
|
+
<div
|
|
534
|
+
className={`h-2 rounded-full transition-all duration-300 ${
|
|
535
|
+
isError ? 'bg-acc-500' : 'bg-main-500'
|
|
536
|
+
}`}
|
|
537
|
+
style={{ width: `${progress.percentage}%` }}
|
|
538
|
+
/>
|
|
539
|
+
</div>
|
|
540
|
+
{isUploading && (
|
|
541
|
+
<div className="text-xs text-sec-500 mt-1">
|
|
542
|
+
{progress.percentage}% • {formatFileSize(progress.loaded)} / {formatFileSize(progress.total)}
|
|
543
|
+
</div>
|
|
544
|
+
)}
|
|
545
|
+
</div>
|
|
546
|
+
)}
|
|
547
|
+
</div>
|
|
548
|
+
|
|
549
|
+
{/* Status Icon */}
|
|
550
|
+
<div className="flex-shrink-0">
|
|
551
|
+
{isCompleted && (
|
|
552
|
+
<span className="text-success-500 text-xl">✓</span>
|
|
553
|
+
)}
|
|
554
|
+
{isError && (
|
|
555
|
+
<span className="text-acc-500 text-xl">✕</span>
|
|
556
|
+
)}
|
|
557
|
+
{isUploading && (
|
|
558
|
+
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-main-500"></div>
|
|
559
|
+
)}
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
})}
|
|
205
564
|
</div>
|
|
206
565
|
)}
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</Alert>
|
|
566
|
+
|
|
567
|
+
{appIdError && (
|
|
568
|
+
<div className="p-3 bg-acc-50 border border-acc-200 rounded-lg text-sm text-acc-600">
|
|
569
|
+
{appIdError}
|
|
570
|
+
</div>
|
|
213
571
|
)}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<div className="file-upload__actions">
|
|
218
|
-
<Button
|
|
219
|
-
onClick={handleUpload}
|
|
220
|
-
disabled={!canUpload}
|
|
221
|
-
className="file-upload__upload-btn"
|
|
222
|
-
>
|
|
223
|
-
{isUploading ? 'Uploading...' : `Upload ${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''}`}
|
|
224
|
-
</Button>
|
|
225
|
-
<Button
|
|
226
|
-
variant="outline"
|
|
227
|
-
onClick={handleClear}
|
|
228
|
-
disabled={isUploading}
|
|
229
|
-
className="file-upload__clear-btn"
|
|
230
|
-
>
|
|
231
|
-
Clear
|
|
232
|
-
</Button>
|
|
572
|
+
{error && (
|
|
573
|
+
<div className="p-3 bg-acc-50 border border-acc-200 rounded-lg text-sm text-acc-600">
|
|
574
|
+
{error}
|
|
233
575
|
</div>
|
|
234
576
|
)}
|
|
235
577
|
</div>
|
|
236
578
|
);
|
|
237
579
|
}
|
|
580
|
+
|
|
581
|
+
|