@jmruthers/pace-core 0.5.93 → 0.5.94
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-HC5S4RKB.js → DataTable-CHX2EFO3.js} +6 -6
- package/dist/{PublicLoadingSpinner-n74JgA9h.d.ts → PublicLoadingSpinner-BWUD6bLU.d.ts} +24 -3
- package/dist/{UnifiedAuthProvider-ZM7VUC45.js → UnifiedAuthProvider-H7RI4KYD.js} +3 -3
- package/dist/{chunk-AZ2QJYKU.js → chunk-2KLAOD4M.js} +3 -3
- package/dist/{chunk-HW5BGOWB.js → chunk-2ZYHCFUO.js} +4 -4
- package/dist/{chunk-AAM57AEU.js → chunk-5RYPBJYL.js} +16 -19
- package/dist/chunk-5RYPBJYL.js.map +1 -0
- package/dist/{chunk-XIBSVWJW.js → chunk-7TQDRDSM.js} +5 -5
- package/dist/{chunk-GP3HU6WS.js → chunk-G7UUVEAP.js} +3 -3
- package/dist/{chunk-M52CQP5W.js → chunk-MKMKUCPF.js} +762 -12
- package/dist/chunk-MKMKUCPF.js.map +1 -0
- package/dist/{chunk-OXFOS62D.js → chunk-MVNOAHOP.js} +2 -2
- package/dist/{chunk-SVMPR5IV.js → chunk-O6GASC4Q.js} +874 -784
- package/dist/chunk-O6GASC4Q.js.map +1 -0
- package/dist/{chunk-AYC2P377.js → chunk-ORACUZ7H.js} +2 -2
- package/dist/{chunk-TZXYSZT3.js → chunk-PRM6EYO3.js} +298 -238
- package/dist/{chunk-TZXYSZT3.js.map → chunk-PRM6EYO3.js.map} +1 -1
- package/dist/{chunk-6WFM22A4.js → chunk-ZGCVJ7WW.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +8 -8
- package/dist/hooks.d.ts +94 -3
- package/dist/hooks.js +20 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +17 -11
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/{usePublicRouteParams-BlgwXweB.d.ts → usePublicRouteParams-BwMR2uub.d.ts} +93 -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/EventLogoProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +26 -11
- 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/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/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 +24 -11
- 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 +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.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 +47 -0
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +120 -0
- 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 +102 -16
- package/docs/implementation-guides/file-reference-system.md +15 -0
- package/docs/implementation-guides/file-upload-storage.md +16 -0
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +9 -7
- package/src/components/DataTable/components/DataTableCore.tsx +8 -1
- package/src/components/DataTable/components/EditableRow.tsx +62 -22
- package/src/components/DataTable/components/UnifiedTableBody.tsx +25 -101
- package/src/components/FileDisplay/FileDisplay.test.tsx +263 -39
- package/src/components/FileDisplay/FileDisplay.tsx +605 -83
- package/src/components/PublicLayout/PublicPageHeader.tsx +15 -8
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +71 -28
- package/src/components/Select/Select.test.tsx +83 -6
- package/src/components/Select/Select.tsx +236 -16
- package/src/examples/CorrectPublicPageImplementation.tsx +16 -13
- package/src/examples/PublicEventPage.tsx +9 -6
- package/src/examples/PublicPageApp.tsx +9 -6
- package/src/examples/PublicPageUsageExample.tsx +9 -7
- package/src/hooks/index.ts +4 -0
- package/src/hooks/public/index.ts +2 -0
- package/src/hooks/public/usePublicFileDisplay.ts +355 -0
- package/src/hooks/useFileDisplay.ts +370 -0
- package/src/services/AuthService.ts +19 -22
- package/dist/chunk-AAM57AEU.js.map +0 -1
- package/dist/chunk-M52CQP5W.js.map +0 -1
- package/dist/chunk-SVMPR5IV.js.map +0 -1
- /package/dist/{DataTable-HC5S4RKB.js.map → DataTable-CHX2EFO3.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-ZM7VUC45.js.map → UnifiedAuthProvider-H7RI4KYD.js.map} +0 -0
- /package/dist/{chunk-AZ2QJYKU.js.map → chunk-2KLAOD4M.js.map} +0 -0
- /package/dist/{chunk-HW5BGOWB.js.map → chunk-2ZYHCFUO.js.map} +0 -0
- /package/dist/{chunk-XIBSVWJW.js.map → chunk-7TQDRDSM.js.map} +0 -0
- /package/dist/{chunk-GP3HU6WS.js.map → chunk-G7UUVEAP.js.map} +0 -0
- /package/dist/{chunk-OXFOS62D.js.map → chunk-MVNOAHOP.js.map} +0 -0
- /package/dist/{chunk-AYC2P377.js.map → chunk-ORACUZ7H.js.map} +0 -0
- /package/dist/{chunk-6WFM22A4.js.map → chunk-ZGCVJ7WW.js.map} +0 -0
|
@@ -1,54 +1,79 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef, useContext } from 'react';
|
|
2
2
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
3
3
|
import { FileReference, FileCategory } from '../../types/file-reference';
|
|
4
|
-
import { useFileReferenceForRecord } from '../../hooks/useFileReference';
|
|
4
|
+
import { useFileReferenceForRecord, useFileReference } from '../../hooks/useFileReference';
|
|
5
|
+
import { usePublicFileDisplay } from '../../hooks/public/usePublicFileDisplay';
|
|
6
|
+
import { useFileDisplay } from '../../hooks/useFileDisplay';
|
|
5
7
|
import { getPublicUrl, getSignedUrl } from '../../utils/storage/helpers';
|
|
8
|
+
import { PublicPageContext, useIsPublicPage } from '../PublicLayout/PublicPageProvider';
|
|
9
|
+
import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
6
10
|
|
|
7
11
|
export interface FileDisplayProps {
|
|
8
|
-
|
|
12
|
+
/** Supabase client instance. Optional when used in PublicPageProvider or UnifiedAuthProvider context */
|
|
13
|
+
supabase?: SupabaseClient;
|
|
9
14
|
table_name: string;
|
|
10
15
|
record_id: string;
|
|
11
16
|
organisation_id: string;
|
|
12
17
|
category?: FileCategory;
|
|
18
|
+
/** Display only a single file instead of all files. Uses first file (prefers images) from all files, without category filtering */
|
|
19
|
+
displayOnly?: boolean;
|
|
13
20
|
showUpload?: boolean;
|
|
14
21
|
showDelete?: boolean;
|
|
15
22
|
className?: string;
|
|
16
23
|
children?: React.ReactNode;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
// Shared rendering logic for file display
|
|
27
|
+
interface FileDisplayContentProps {
|
|
28
|
+
isLoading: boolean;
|
|
29
|
+
error: string | Error | null;
|
|
30
|
+
fileUrl: string | null;
|
|
31
|
+
fileReference: FileReference | null;
|
|
32
|
+
fileReferences: FileReference[];
|
|
33
|
+
fileUrls: Map<string, string>;
|
|
34
|
+
fileCount: number;
|
|
35
|
+
category: FileCategory | undefined;
|
|
36
|
+
displayOnly: boolean;
|
|
37
|
+
showDelete: boolean;
|
|
38
|
+
className: string;
|
|
39
|
+
children?: React.ReactNode;
|
|
40
|
+
onDelete?: () => Promise<void>;
|
|
41
|
+
clearError?: () => void;
|
|
42
|
+
supabase?: SupabaseClient;
|
|
43
|
+
organisation_id: string;
|
|
44
|
+
loadingUrls?: Set<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function FileDisplayContent({
|
|
48
|
+
isLoading,
|
|
49
|
+
error,
|
|
50
|
+
fileUrl,
|
|
51
|
+
fileReference,
|
|
52
|
+
fileReferences,
|
|
53
|
+
fileUrls,
|
|
54
|
+
fileCount,
|
|
55
|
+
category,
|
|
56
|
+
displayOnly,
|
|
57
|
+
showDelete,
|
|
58
|
+
className,
|
|
59
|
+
children,
|
|
60
|
+
onDelete,
|
|
61
|
+
clearError,
|
|
20
62
|
supabase,
|
|
21
|
-
table_name,
|
|
22
|
-
record_id,
|
|
23
63
|
organisation_id,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
showDelete = false,
|
|
27
|
-
className = '',
|
|
28
|
-
children
|
|
29
|
-
}: FileDisplayProps) {
|
|
30
|
-
const {
|
|
31
|
-
isLoading,
|
|
32
|
-
error,
|
|
33
|
-
fileUrl,
|
|
34
|
-
fileReference,
|
|
35
|
-
fileReferences,
|
|
36
|
-
fileCount,
|
|
37
|
-
loadFileReference,
|
|
38
|
-
loadFileUrl,
|
|
39
|
-
loadFileReferences,
|
|
40
|
-
loadFileCount,
|
|
41
|
-
deleteFile,
|
|
42
|
-
clearError
|
|
43
|
-
} = useFileReferenceForRecord(supabase, table_name, record_id, organisation_id);
|
|
44
|
-
|
|
64
|
+
loadingUrls = new Set()
|
|
65
|
+
}: FileDisplayContentProps) {
|
|
45
66
|
const [imageError, setImageError] = useState(false);
|
|
46
|
-
const [
|
|
47
|
-
const [loadingUrls, setLoadingUrls] = useState<Set<string>>(new Set());
|
|
67
|
+
const [internalFileUrls, setInternalFileUrls] = useState<Map<string, string>>(new Map(fileUrls));
|
|
48
68
|
const loadedFilesRef = useRef<Set<string>>(new Set());
|
|
49
|
-
const loadingUrlsRef = useRef<Set<string>>(new Set());
|
|
69
|
+
const loadingUrlsRef = useRef<Set<string>>(new Set(loadingUrls));
|
|
50
70
|
const fileReferencesRef = useRef<FileReference[]>([]);
|
|
51
71
|
|
|
72
|
+
// Sync fileUrls prop with internal state
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setInternalFileUrls(new Map(fileUrls));
|
|
75
|
+
}, [fileUrls]);
|
|
76
|
+
|
|
52
77
|
// Track file references to detect when they change
|
|
53
78
|
useEffect(() => {
|
|
54
79
|
const currentIds = fileReferences.map(f => f.id).join(',');
|
|
@@ -58,31 +83,13 @@ export function FileDisplay({
|
|
|
58
83
|
fileReferencesRef.current = fileReferences;
|
|
59
84
|
// Reset loaded files ref when file references change
|
|
60
85
|
loadedFilesRef.current.clear();
|
|
61
|
-
|
|
62
|
-
setLoadingUrls(new Set());
|
|
86
|
+
setInternalFileUrls(new Map());
|
|
63
87
|
}
|
|
64
88
|
}, [fileReferences]);
|
|
65
89
|
|
|
66
|
-
//
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
loadFileCount();
|
|
69
|
-
if (category) {
|
|
70
|
-
loadFileReference();
|
|
71
|
-
} else {
|
|
72
|
-
loadFileReferences();
|
|
73
|
-
}
|
|
74
|
-
}, [loadFileCount, loadFileReference, loadFileReferences, category]);
|
|
75
|
-
|
|
76
|
-
// Load file URL when file reference is available
|
|
90
|
+
// Fetch URLs for all file references (for multiple files view when not using context hooks)
|
|
77
91
|
useEffect(() => {
|
|
78
|
-
if (
|
|
79
|
-
loadFileUrl();
|
|
80
|
-
}
|
|
81
|
-
}, [fileReference, loadFileUrl]);
|
|
82
|
-
|
|
83
|
-
// Fetch URLs for all file references (for multiple files view)
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
if (category || fileReferences.length === 0) return;
|
|
92
|
+
if (!supabase || category || fileReferences.length === 0) return;
|
|
86
93
|
|
|
87
94
|
const loadFileUrls = async () => {
|
|
88
95
|
// Find files that need URLs loaded
|
|
@@ -93,14 +100,7 @@ export function FileDisplay({
|
|
|
93
100
|
if (urlsToLoad.length === 0) return;
|
|
94
101
|
|
|
95
102
|
// Mark files as loading (update both state and ref)
|
|
96
|
-
|
|
97
|
-
const updated = new Set(prev);
|
|
98
|
-
urlsToLoad.forEach(fileRef => {
|
|
99
|
-
updated.add(fileRef.id);
|
|
100
|
-
loadingUrlsRef.current.add(fileRef.id);
|
|
101
|
-
});
|
|
102
|
-
return updated;
|
|
103
|
-
});
|
|
103
|
+
loadingUrlsRef.current = new Set([...loadingUrlsRef.current, ...urlsToLoad.map(f => f.id)]);
|
|
104
104
|
|
|
105
105
|
// Load URLs for files that need them
|
|
106
106
|
for (const fileRef of urlsToLoad) {
|
|
@@ -121,7 +121,7 @@ export function FileDisplay({
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
if (url) {
|
|
124
|
-
|
|
124
|
+
setInternalFileUrls(prev => {
|
|
125
125
|
const updated = new Map(prev);
|
|
126
126
|
updated.set(fileRef.id, url!);
|
|
127
127
|
return updated;
|
|
@@ -131,12 +131,7 @@ export function FileDisplay({
|
|
|
131
131
|
} catch (error) {
|
|
132
132
|
console.error(`Failed to load URL for file ${fileRef.id}:`, error);
|
|
133
133
|
} finally {
|
|
134
|
-
|
|
135
|
-
const updated = new Set(prev);
|
|
136
|
-
updated.delete(fileRef.id);
|
|
137
|
-
loadingUrlsRef.current.delete(fileRef.id);
|
|
138
|
-
return updated;
|
|
139
|
-
});
|
|
134
|
+
loadingUrlsRef.current.delete(fileRef.id);
|
|
140
135
|
}
|
|
141
136
|
}
|
|
142
137
|
};
|
|
@@ -146,10 +141,10 @@ export function FileDisplay({
|
|
|
146
141
|
|
|
147
142
|
const handleDelete = async () => {
|
|
148
143
|
if (window.confirm('Are you sure you want to delete this file?')) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
setImageError(false);
|
|
144
|
+
if (onDelete) {
|
|
145
|
+
await onDelete();
|
|
152
146
|
}
|
|
147
|
+
setImageError(false);
|
|
153
148
|
}
|
|
154
149
|
};
|
|
155
150
|
|
|
@@ -188,14 +183,16 @@ export function FileDisplay({
|
|
|
188
183
|
return (
|
|
189
184
|
<div className={`p-4 bg-acc-50 border border-acc-200 rounded-lg ${className}`}>
|
|
190
185
|
<div className="text-acc-600">
|
|
191
|
-
Error loading file: {error}
|
|
186
|
+
Error loading file: {error instanceof Error ? error.message : String(error)}
|
|
192
187
|
</div>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
188
|
+
{clearError && (
|
|
189
|
+
<button
|
|
190
|
+
onClick={clearError}
|
|
191
|
+
className="mt-2 text-sm text-acc-700 hover:text-acc-800 underline"
|
|
192
|
+
>
|
|
193
|
+
Try again
|
|
194
|
+
</button>
|
|
195
|
+
)}
|
|
199
196
|
</div>
|
|
200
197
|
);
|
|
201
198
|
}
|
|
@@ -209,10 +206,23 @@ export function FileDisplay({
|
|
|
209
206
|
);
|
|
210
207
|
}
|
|
211
208
|
|
|
212
|
-
// Single file display (when category is specified)
|
|
213
|
-
if (category && fileReference) {
|
|
209
|
+
// Single file display (when category or displayOnly is specified)
|
|
210
|
+
if ((category || displayOnly) && fileReference) {
|
|
214
211
|
const isImage = fileReference.file_metadata.fileType?.startsWith('image/');
|
|
215
212
|
|
|
213
|
+
// Simplified image-only display when displayOnly is true and it's an image
|
|
214
|
+
if (displayOnly && isImage && fileUrl && !imageError && !showDelete) {
|
|
215
|
+
return (
|
|
216
|
+
<img
|
|
217
|
+
src={fileUrl}
|
|
218
|
+
alt={fileReference.file_metadata.fileName || 'File'}
|
|
219
|
+
className={className || "max-w-full h-auto"}
|
|
220
|
+
onError={handleImageError}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Standard single file display with wrapper
|
|
216
226
|
return (
|
|
217
227
|
<div className={`space-y-2 ${className}`}>
|
|
218
228
|
{isImage && fileUrl && !imageError ? (
|
|
@@ -220,7 +230,7 @@ export function FileDisplay({
|
|
|
220
230
|
<img
|
|
221
231
|
src={fileUrl}
|
|
222
232
|
alt={fileReference.file_metadata.fileName || 'File'}
|
|
223
|
-
className="max-w-full h-auto
|
|
233
|
+
className="max-w-full h-auto"
|
|
224
234
|
onError={handleImageError}
|
|
225
235
|
/>
|
|
226
236
|
{showDelete && (
|
|
@@ -270,8 +280,8 @@ export function FileDisplay({
|
|
|
270
280
|
<div className={`space-y-2 ${className}`}>
|
|
271
281
|
{fileReferences.map((fileRef) => {
|
|
272
282
|
const isImage = fileRef.file_metadata.fileType?.startsWith('image/');
|
|
273
|
-
const fileUrl =
|
|
274
|
-
const isLoadingUrl =
|
|
283
|
+
const fileUrl = internalFileUrls.get(fileRef.id) || null;
|
|
284
|
+
const isLoadingUrl = loadingUrlsRef.current.has(fileRef.id);
|
|
275
285
|
const canDownload = !isImage && fileUrl;
|
|
276
286
|
|
|
277
287
|
return (
|
|
@@ -313,9 +323,14 @@ export function FileDisplay({
|
|
|
313
323
|
↓
|
|
314
324
|
</a>
|
|
315
325
|
)}
|
|
316
|
-
{showDelete && (
|
|
326
|
+
{showDelete && onDelete && (
|
|
317
327
|
<button
|
|
318
|
-
onClick={() =>
|
|
328
|
+
onClick={() => {
|
|
329
|
+
// For multiple files, we'd need file-specific delete
|
|
330
|
+
// For now, call onDelete which may handle the first file
|
|
331
|
+
// TODO: Implement file-specific delete functionality
|
|
332
|
+
onDelete();
|
|
333
|
+
}}
|
|
319
334
|
className="text-acc-500 hover:text-acc-700 p-1"
|
|
320
335
|
title="Delete file"
|
|
321
336
|
>
|
|
@@ -331,4 +346,511 @@ export function FileDisplay({
|
|
|
331
346
|
);
|
|
332
347
|
}
|
|
333
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Internal component for backwards compatibility (when supabase is explicitly provided)
|
|
351
|
+
*/
|
|
352
|
+
function FileDisplayBackwardsCompat({
|
|
353
|
+
supabase,
|
|
354
|
+
table_name,
|
|
355
|
+
record_id,
|
|
356
|
+
organisation_id,
|
|
357
|
+
category,
|
|
358
|
+
displayOnly = false,
|
|
359
|
+
showUpload = false,
|
|
360
|
+
showDelete = false,
|
|
361
|
+
className = '',
|
|
362
|
+
children
|
|
363
|
+
}: Required<Pick<FileDisplayProps, 'supabase'>> & Omit<FileDisplayProps, 'supabase'>) {
|
|
364
|
+
const {
|
|
365
|
+
isLoading: isLoadingForRecord,
|
|
366
|
+
error: errorForRecord,
|
|
367
|
+
fileUrl,
|
|
368
|
+
fileReference,
|
|
369
|
+
fileReferences,
|
|
370
|
+
fileCount,
|
|
371
|
+
loadFileReference,
|
|
372
|
+
loadFileUrl,
|
|
373
|
+
loadFileReferences,
|
|
374
|
+
loadFileCount,
|
|
375
|
+
deleteFile,
|
|
376
|
+
clearError
|
|
377
|
+
} = useFileReferenceForRecord(supabase, table_name, record_id, organisation_id);
|
|
378
|
+
|
|
379
|
+
// Use useFileReference to get getFilesByCategory when category is provided
|
|
380
|
+
const {
|
|
381
|
+
getFilesByCategory,
|
|
382
|
+
isLoading: isLoadingCategory,
|
|
383
|
+
error: errorCategory
|
|
384
|
+
} = useFileReference(supabase);
|
|
385
|
+
|
|
386
|
+
const [categoryFileReference, setCategoryFileReference] = useState<FileReference | null>(null);
|
|
387
|
+
const [categoryFileUrl, setCategoryFileUrl] = useState<string | null>(null);
|
|
388
|
+
const [categoryFileReferences, setCategoryFileReferences] = useState<FileReference[]>([]);
|
|
389
|
+
const [displayOnlyFileReference, setDisplayOnlyFileReference] = useState<FileReference | null>(null);
|
|
390
|
+
const [displayOnlyFileUrl, setDisplayOnlyFileUrl] = useState<string | null>(null);
|
|
391
|
+
|
|
392
|
+
// Load files by category when category is provided
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
if (category) {
|
|
395
|
+
const loadCategoryFiles = async () => {
|
|
396
|
+
try {
|
|
397
|
+
const files = await getFilesByCategory(table_name, record_id, category, organisation_id);
|
|
398
|
+
setCategoryFileReferences(files);
|
|
399
|
+
|
|
400
|
+
if (files.length > 0) {
|
|
401
|
+
const firstFile = files[0];
|
|
402
|
+
setCategoryFileReference(firstFile);
|
|
403
|
+
|
|
404
|
+
// Generate URL for the file
|
|
405
|
+
let url: string | null = null;
|
|
406
|
+
if (firstFile.is_public) {
|
|
407
|
+
url = getPublicUrl(supabase, firstFile.file_path, true);
|
|
408
|
+
} else {
|
|
409
|
+
const signedUrlResult = await getSignedUrl(supabase, firstFile.file_path, {
|
|
410
|
+
appName: 'file-reference',
|
|
411
|
+
orgId: organisation_id,
|
|
412
|
+
expiresIn: 3600
|
|
413
|
+
});
|
|
414
|
+
url = signedUrlResult?.url || null;
|
|
415
|
+
}
|
|
416
|
+
setCategoryFileUrl(url);
|
|
417
|
+
} else {
|
|
418
|
+
setCategoryFileReference(null);
|
|
419
|
+
setCategoryFileUrl(null);
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error('[FileDisplayBackwardsCompat] Error loading files by category:', err);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
loadCategoryFiles();
|
|
427
|
+
} else {
|
|
428
|
+
// Clear category files when no category
|
|
429
|
+
setCategoryFileReference(null);
|
|
430
|
+
setCategoryFileUrl(null);
|
|
431
|
+
setCategoryFileReferences([]);
|
|
432
|
+
}
|
|
433
|
+
}, [category, table_name, record_id, organisation_id, supabase, getFilesByCategory]);
|
|
434
|
+
|
|
435
|
+
// Load file data on mount (when no category)
|
|
436
|
+
useEffect(() => {
|
|
437
|
+
if (!category) {
|
|
438
|
+
loadFileCount();
|
|
439
|
+
loadFileReferences();
|
|
440
|
+
}
|
|
441
|
+
}, [loadFileCount, loadFileReferences, category]);
|
|
442
|
+
|
|
443
|
+
// Load file URL when file reference is available (when no category)
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
if (!category && fileReference) {
|
|
446
|
+
loadFileUrl();
|
|
447
|
+
}
|
|
448
|
+
}, [category, fileReference, loadFileUrl]);
|
|
449
|
+
|
|
450
|
+
// Handle displayOnly mode: select first file (prefer images) from all files
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
if (displayOnly && !category && fileReferences.length > 0) {
|
|
453
|
+
const loadDisplayOnlyFile = async () => {
|
|
454
|
+
try {
|
|
455
|
+
// Prefer image files
|
|
456
|
+
const imageFiles = fileReferences.filter(f =>
|
|
457
|
+
f.file_metadata.fileType?.startsWith('image/')
|
|
458
|
+
);
|
|
459
|
+
const targetFile = imageFiles.length > 0 ? imageFiles[0] : fileReferences[0];
|
|
460
|
+
|
|
461
|
+
setDisplayOnlyFileReference(targetFile);
|
|
462
|
+
|
|
463
|
+
// Generate URL for the file
|
|
464
|
+
let url: string | null = null;
|
|
465
|
+
if (targetFile.is_public) {
|
|
466
|
+
url = getPublicUrl(supabase, targetFile.file_path, true);
|
|
467
|
+
} else {
|
|
468
|
+
const signedUrlResult = await getSignedUrl(supabase, targetFile.file_path, {
|
|
469
|
+
appName: 'file-reference',
|
|
470
|
+
orgId: organisation_id,
|
|
471
|
+
expiresIn: 3600
|
|
472
|
+
});
|
|
473
|
+
url = signedUrlResult?.url || null;
|
|
474
|
+
}
|
|
475
|
+
setDisplayOnlyFileUrl(url);
|
|
476
|
+
} catch (err) {
|
|
477
|
+
console.error('[FileDisplayBackwardsCompat] Error loading displayOnly file:', err);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
loadDisplayOnlyFile();
|
|
482
|
+
} else if (!displayOnly || category) {
|
|
483
|
+
// Clear displayOnly files when not in displayOnly mode or when category is specified
|
|
484
|
+
setDisplayOnlyFileReference(null);
|
|
485
|
+
setDisplayOnlyFileUrl(null);
|
|
486
|
+
}
|
|
487
|
+
}, [displayOnly, category, fileReferences, supabase, organisation_id]);
|
|
488
|
+
|
|
489
|
+
const handleDelete = async () => {
|
|
490
|
+
if (window.confirm('Are you sure you want to delete this file?')) {
|
|
491
|
+
await deleteFile(true);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Determine final file reference and URL based on mode
|
|
496
|
+
// Priority: category > displayOnly > default (all files)
|
|
497
|
+
let finalFileReference: FileReference | null = null;
|
|
498
|
+
let finalFileUrl: string | null = null;
|
|
499
|
+
let finalFileReferences: FileReference[] = [];
|
|
500
|
+
let finalFileCount = 0;
|
|
501
|
+
let finalIsLoading = false;
|
|
502
|
+
let finalError: string | Error | null = null;
|
|
503
|
+
|
|
504
|
+
if (category) {
|
|
505
|
+
// Category mode: use category-filtered files
|
|
506
|
+
finalFileReference = categoryFileReference;
|
|
507
|
+
finalFileUrl = categoryFileUrl;
|
|
508
|
+
finalFileReferences = categoryFileReferences;
|
|
509
|
+
finalFileCount = categoryFileReferences.length;
|
|
510
|
+
finalIsLoading = isLoadingCategory;
|
|
511
|
+
finalError = errorCategory;
|
|
512
|
+
} else if (displayOnly) {
|
|
513
|
+
// DisplayOnly mode: use first file from all files (prefers images)
|
|
514
|
+
finalFileReference = displayOnlyFileReference;
|
|
515
|
+
finalFileUrl = displayOnlyFileUrl;
|
|
516
|
+
finalFileReferences = displayOnlyFileReference ? [displayOnlyFileReference] : [];
|
|
517
|
+
finalFileCount = displayOnlyFileReference ? 1 : 0;
|
|
518
|
+
finalIsLoading = isLoadingForRecord;
|
|
519
|
+
finalError = errorForRecord;
|
|
520
|
+
} else {
|
|
521
|
+
// Default mode: show all files
|
|
522
|
+
finalFileReference = fileReference;
|
|
523
|
+
finalFileUrl = fileUrl;
|
|
524
|
+
finalFileReferences = fileReferences;
|
|
525
|
+
finalFileCount = fileCount;
|
|
526
|
+
finalIsLoading = isLoadingForRecord;
|
|
527
|
+
finalError = errorForRecord;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<FileDisplayContent
|
|
532
|
+
isLoading={finalIsLoading}
|
|
533
|
+
error={finalError}
|
|
534
|
+
fileUrl={finalFileUrl}
|
|
535
|
+
fileReference={finalFileReference}
|
|
536
|
+
fileReferences={finalFileReferences}
|
|
537
|
+
fileUrls={new Map()} // Will be populated by FileDisplayContent's useEffect
|
|
538
|
+
fileCount={finalFileCount}
|
|
539
|
+
category={category}
|
|
540
|
+
displayOnly={displayOnly}
|
|
541
|
+
showDelete={showDelete}
|
|
542
|
+
className={className}
|
|
543
|
+
children={children}
|
|
544
|
+
onDelete={handleDelete}
|
|
545
|
+
clearError={clearError}
|
|
546
|
+
supabase={supabase}
|
|
547
|
+
organisation_id={organisation_id}
|
|
548
|
+
loadingUrls={new Set()}
|
|
549
|
+
/>
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Internal component for public page context
|
|
555
|
+
* Uses PublicPageContext to get Supabase client
|
|
556
|
+
*/
|
|
557
|
+
function FileDisplayPublic({
|
|
558
|
+
table_name,
|
|
559
|
+
record_id,
|
|
560
|
+
organisation_id,
|
|
561
|
+
category,
|
|
562
|
+
displayOnly = false,
|
|
563
|
+
showUpload = false,
|
|
564
|
+
showDelete = false,
|
|
565
|
+
className = '',
|
|
566
|
+
children
|
|
567
|
+
}: Omit<FileDisplayProps, 'supabase'>) {
|
|
568
|
+
const publicPageContext = useContext(PublicPageContext);
|
|
569
|
+
const supabase = publicPageContext?.supabase ?? null;
|
|
570
|
+
|
|
571
|
+
if (!supabase) {
|
|
572
|
+
return (
|
|
573
|
+
<div className={`text-sec-500 text-center p-4 ${className}`}>
|
|
574
|
+
Supabase client not available in public context
|
|
575
|
+
</div>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const {
|
|
580
|
+
fileUrl,
|
|
581
|
+
fileReference,
|
|
582
|
+
fileReferences,
|
|
583
|
+
fileUrls,
|
|
584
|
+
fileCount,
|
|
585
|
+
isLoading,
|
|
586
|
+
error,
|
|
587
|
+
refetch
|
|
588
|
+
} = usePublicFileDisplay(
|
|
589
|
+
table_name,
|
|
590
|
+
record_id,
|
|
591
|
+
organisation_id,
|
|
592
|
+
category,
|
|
593
|
+
{ supabase }
|
|
594
|
+
);
|
|
334
595
|
|
|
596
|
+
// Note: Public context doesn't support delete operations
|
|
597
|
+
const handleDelete = async () => {
|
|
598
|
+
console.warn('[FileDisplay] Delete operation not supported in public context');
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// Handle displayOnly mode: select first file (prefer images) from all files
|
|
602
|
+
let finalFileReference = fileReference;
|
|
603
|
+
let finalFileUrl = fileUrl;
|
|
604
|
+
let finalFileReferences = fileReferences;
|
|
605
|
+
let finalFileCount = fileCount;
|
|
606
|
+
|
|
607
|
+
if (displayOnly && !category && fileReferences.length > 0) {
|
|
608
|
+
// Prefer image files
|
|
609
|
+
const imageFiles = fileReferences.filter(f =>
|
|
610
|
+
f.file_metadata.fileType?.startsWith('image/')
|
|
611
|
+
);
|
|
612
|
+
const targetFile = imageFiles.length > 0 ? imageFiles[0] : fileReferences[0];
|
|
613
|
+
finalFileReference = targetFile;
|
|
614
|
+
finalFileReferences = [targetFile];
|
|
615
|
+
finalFileCount = 1;
|
|
616
|
+
|
|
617
|
+
// Get URL for target file from fileUrls map
|
|
618
|
+
finalFileUrl = fileUrls.get(targetFile.id) || null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return (
|
|
622
|
+
<FileDisplayContent
|
|
623
|
+
isLoading={isLoading}
|
|
624
|
+
error={error}
|
|
625
|
+
fileUrl={finalFileUrl}
|
|
626
|
+
fileReference={finalFileReference}
|
|
627
|
+
fileReferences={finalFileReferences}
|
|
628
|
+
fileUrls={fileUrls}
|
|
629
|
+
fileCount={finalFileCount}
|
|
630
|
+
category={category}
|
|
631
|
+
displayOnly={displayOnly}
|
|
632
|
+
showDelete={false} // Never show delete in public context
|
|
633
|
+
className={className}
|
|
634
|
+
children={children}
|
|
635
|
+
onDelete={showDelete ? handleDelete : undefined}
|
|
636
|
+
supabase={supabase}
|
|
637
|
+
organisation_id={organisation_id}
|
|
638
|
+
/>
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Internal component for authenticated page context
|
|
644
|
+
* Uses UnifiedAuthProvider to get Supabase client
|
|
645
|
+
*/
|
|
646
|
+
function FileDisplayAuthenticated({
|
|
647
|
+
table_name,
|
|
648
|
+
record_id,
|
|
649
|
+
organisation_id,
|
|
650
|
+
category,
|
|
651
|
+
displayOnly = false,
|
|
652
|
+
showUpload = false,
|
|
653
|
+
showDelete = false,
|
|
654
|
+
className = '',
|
|
655
|
+
children
|
|
656
|
+
}: Omit<FileDisplayProps, 'supabase'>) {
|
|
657
|
+
const { supabase } = useUnifiedAuth();
|
|
658
|
+
|
|
659
|
+
if (!supabase) {
|
|
660
|
+
return (
|
|
661
|
+
<div className={`text-sec-500 text-center p-4 ${className}`}>
|
|
662
|
+
Supabase client not available in authenticated context
|
|
663
|
+
</div>
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const {
|
|
668
|
+
fileUrl,
|
|
669
|
+
fileReference,
|
|
670
|
+
fileReferences,
|
|
671
|
+
fileUrls,
|
|
672
|
+
fileCount,
|
|
673
|
+
isLoading,
|
|
674
|
+
error,
|
|
675
|
+
refetch
|
|
676
|
+
} = useFileDisplay(
|
|
677
|
+
table_name,
|
|
678
|
+
record_id,
|
|
679
|
+
organisation_id,
|
|
680
|
+
category,
|
|
681
|
+
{ supabase }
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// State for displayOnly mode URL generation
|
|
685
|
+
const [displayOnlyFileReference, setDisplayOnlyFileReference] = useState<FileReference | null>(null);
|
|
686
|
+
const [displayOnlyFileUrl, setDisplayOnlyFileUrl] = useState<string | null>(null);
|
|
687
|
+
|
|
688
|
+
// Handle displayOnly mode: select first file (prefer images) from all files and generate URL on-demand
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
if (displayOnly && !category && fileReferences.length > 0) {
|
|
691
|
+
const loadDisplayOnlyFile = async () => {
|
|
692
|
+
try {
|
|
693
|
+
// Prefer image files
|
|
694
|
+
const imageFiles = fileReferences.filter(f =>
|
|
695
|
+
f.file_metadata.fileType?.startsWith('image/')
|
|
696
|
+
);
|
|
697
|
+
const targetFile = imageFiles.length > 0 ? imageFiles[0] : fileReferences[0];
|
|
698
|
+
|
|
699
|
+
setDisplayOnlyFileReference(targetFile);
|
|
700
|
+
|
|
701
|
+
// Check if URL exists in fileUrls Map first
|
|
702
|
+
const existingUrl = fileUrls.get(targetFile.id);
|
|
703
|
+
if (existingUrl) {
|
|
704
|
+
setDisplayOnlyFileUrl(existingUrl);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Generate URL for the file if not in Map
|
|
709
|
+
let url: string | null = null;
|
|
710
|
+
if (targetFile.is_public) {
|
|
711
|
+
url = getPublicUrl(supabase, targetFile.file_path, true);
|
|
712
|
+
} else {
|
|
713
|
+
const signedUrlResult = await getSignedUrl(supabase, targetFile.file_path, {
|
|
714
|
+
appName: 'file-reference',
|
|
715
|
+
orgId: organisation_id,
|
|
716
|
+
expiresIn: 3600
|
|
717
|
+
});
|
|
718
|
+
url = signedUrlResult?.url || null;
|
|
719
|
+
}
|
|
720
|
+
setDisplayOnlyFileUrl(url);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
console.error('[FileDisplayAuthenticated] Error loading displayOnly file:', err);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
loadDisplayOnlyFile();
|
|
727
|
+
} else if (!displayOnly || category) {
|
|
728
|
+
// Clear displayOnly files when not in displayOnly mode or when category is specified
|
|
729
|
+
setDisplayOnlyFileReference(null);
|
|
730
|
+
setDisplayOnlyFileUrl(null);
|
|
731
|
+
}
|
|
732
|
+
}, [displayOnly, category, fileReferences, fileUrls, supabase, organisation_id]);
|
|
733
|
+
|
|
734
|
+
// Note: Delete would need to be implemented via FileReferenceService
|
|
735
|
+
// For now, we'll show a warning
|
|
736
|
+
const handleDelete = async () => {
|
|
737
|
+
console.warn('[FileDisplay] Delete operation needs to be implemented via FileReferenceService');
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// Determine final file reference and URL based on mode
|
|
741
|
+
let finalFileReference = fileReference;
|
|
742
|
+
let finalFileUrl = fileUrl;
|
|
743
|
+
let finalFileReferences = fileReferences;
|
|
744
|
+
let finalFileCount = fileCount;
|
|
745
|
+
|
|
746
|
+
if (displayOnly && !category) {
|
|
747
|
+
// DisplayOnly mode: use state-managed file reference and URL
|
|
748
|
+
finalFileReference = displayOnlyFileReference;
|
|
749
|
+
finalFileUrl = displayOnlyFileUrl;
|
|
750
|
+
finalFileReferences = displayOnlyFileReference ? [displayOnlyFileReference] : [];
|
|
751
|
+
finalFileCount = displayOnlyFileReference ? 1 : 0;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return (
|
|
755
|
+
<FileDisplayContent
|
|
756
|
+
isLoading={isLoading}
|
|
757
|
+
error={error}
|
|
758
|
+
fileUrl={finalFileUrl}
|
|
759
|
+
fileReference={finalFileReference}
|
|
760
|
+
fileReferences={finalFileReferences}
|
|
761
|
+
fileUrls={fileUrls}
|
|
762
|
+
fileCount={finalFileCount}
|
|
763
|
+
category={category}
|
|
764
|
+
displayOnly={displayOnly}
|
|
765
|
+
showDelete={showDelete}
|
|
766
|
+
className={className}
|
|
767
|
+
children={children}
|
|
768
|
+
onDelete={showDelete ? handleDelete : undefined}
|
|
769
|
+
supabase={supabase}
|
|
770
|
+
organisation_id={organisation_id}
|
|
771
|
+
/>
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Component for displaying file references with context-awareness
|
|
777
|
+
*
|
|
778
|
+
* This component is context-aware and automatically detects whether it's being used
|
|
779
|
+
* in a public or authenticated context. It fetches and displays files from storage.
|
|
780
|
+
*
|
|
781
|
+
* When `supabase` prop is provided, it uses the explicit client (backwards compatible).
|
|
782
|
+
* When `supabase` prop is not provided, it automatically detects context and uses:
|
|
783
|
+
* - PublicPageProvider context for public pages
|
|
784
|
+
* - UnifiedAuthProvider context for authenticated pages
|
|
785
|
+
*
|
|
786
|
+
* @param props - File display configuration
|
|
787
|
+
* @param props.displayOnly - Display only a single file instead of all files. Uses first file (prefers images) from all files, without category filtering. When true with an image, renders a simplified image-only display without metadata or wrapper divs.
|
|
788
|
+
* @param props.category - Optional category filter. When specified, only displays files matching this category and uses single file display variant.
|
|
789
|
+
* @returns React element with file display
|
|
790
|
+
*/
|
|
791
|
+
export function FileDisplay({
|
|
792
|
+
supabase,
|
|
793
|
+
table_name,
|
|
794
|
+
record_id,
|
|
795
|
+
organisation_id,
|
|
796
|
+
category,
|
|
797
|
+
displayOnly = false,
|
|
798
|
+
showUpload = false,
|
|
799
|
+
showDelete = false,
|
|
800
|
+
className = '',
|
|
801
|
+
children
|
|
802
|
+
}: FileDisplayProps) {
|
|
803
|
+
// If supabase is explicitly provided, use backwards compatible mode
|
|
804
|
+
if (supabase) {
|
|
805
|
+
return (
|
|
806
|
+
<FileDisplayBackwardsCompat
|
|
807
|
+
supabase={supabase}
|
|
808
|
+
table_name={table_name}
|
|
809
|
+
record_id={record_id}
|
|
810
|
+
organisation_id={organisation_id}
|
|
811
|
+
category={category}
|
|
812
|
+
displayOnly={displayOnly}
|
|
813
|
+
showUpload={showUpload}
|
|
814
|
+
showDelete={showDelete}
|
|
815
|
+
className={className}
|
|
816
|
+
children={children}
|
|
817
|
+
/>
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Check which context we're in and route to the appropriate component
|
|
822
|
+
const isPublicPage = useIsPublicPage();
|
|
823
|
+
|
|
824
|
+
// If we're in a public page context, use the public component
|
|
825
|
+
if (isPublicPage) {
|
|
826
|
+
return (
|
|
827
|
+
<FileDisplayPublic
|
|
828
|
+
table_name={table_name}
|
|
829
|
+
record_id={record_id}
|
|
830
|
+
organisation_id={organisation_id}
|
|
831
|
+
category={category}
|
|
832
|
+
displayOnly={displayOnly}
|
|
833
|
+
showUpload={showUpload}
|
|
834
|
+
showDelete={showDelete}
|
|
835
|
+
className={className}
|
|
836
|
+
children={children}
|
|
837
|
+
/>
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Otherwise, try to use the authenticated component
|
|
842
|
+
// It will show an error if not in UnifiedAuthProvider
|
|
843
|
+
return (
|
|
844
|
+
<FileDisplayAuthenticated
|
|
845
|
+
table_name={table_name}
|
|
846
|
+
record_id={record_id}
|
|
847
|
+
organisation_id={organisation_id}
|
|
848
|
+
category={category}
|
|
849
|
+
displayOnly={displayOnly}
|
|
850
|
+
showUpload={showUpload}
|
|
851
|
+
showDelete={showDelete}
|
|
852
|
+
className={className}
|
|
853
|
+
children={children}
|
|
854
|
+
/>
|
|
855
|
+
);
|
|
856
|
+
}
|