@jmruthers/pace-core 0.5.93 → 0.5.95
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-XENXNMCP.js} +6 -6
- package/dist/{PublicLoadingSpinner-n74JgA9h.d.ts → PublicLoadingSpinner-B84QWsvB.d.ts} +31 -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-TZXYSZT3.js → chunk-EPKHU5SS.js} +314 -245
- package/dist/{chunk-TZXYSZT3.js.map → chunk-EPKHU5SS.js.map} +1 -1
- 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-AYC2P377.js → chunk-ORACUZ7H.js} +2 -2
- package/dist/{chunk-SVMPR5IV.js → chunk-V5CTX4FR.js} +963 -788
- package/dist/chunk-V5CTX4FR.js.map +1 -0
- 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 +52 -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 +35 -10
- 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 +667 -103
- 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/hooks/useFileUrl.ts +130 -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-XENXNMCP.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
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file File Display Hook (Authenticated)
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Hooks
|
|
5
|
+
*
|
|
6
|
+
* A React hook for accessing file references in authenticated contexts.
|
|
7
|
+
* Can handle both public and private files using the file_references system.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Works in authenticated contexts
|
|
11
|
+
* - Supports both public and private files
|
|
12
|
+
* - Automatic signed URL generation for private files
|
|
13
|
+
* - Caching for performance
|
|
14
|
+
* - Error handling and loading states
|
|
15
|
+
* - Supports both single file (category) and multiple files
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { useFileDisplay } from '@jmruthers/pace-core';
|
|
20
|
+
*
|
|
21
|
+
* function FileView() {
|
|
22
|
+
* const { fileUrl, fileReference, isLoading, error } = useFileDisplay(
|
|
23
|
+
* 'event',
|
|
24
|
+
* eventId,
|
|
25
|
+
* organisationId,
|
|
26
|
+
* FileCategory.EVENT_LOGOS
|
|
27
|
+
* );
|
|
28
|
+
*
|
|
29
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
30
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
31
|
+
*
|
|
32
|
+
* return fileUrl ? <img src={fileUrl} alt="File" /> : null;
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
38
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
39
|
+
import { FileReference, FileCategory } from '../types/file-reference';
|
|
40
|
+
import { getPublicUrl, getSignedUrl } from '../utils/storage/helpers';
|
|
41
|
+
import { createFileReferenceService } from '../utils/file-reference';
|
|
42
|
+
|
|
43
|
+
// Simple in-memory cache for authenticated file data
|
|
44
|
+
const authenticatedFileCache = new Map<string, { data: any; timestamp: number; ttl: number }>();
|
|
45
|
+
|
|
46
|
+
// Cache size limit to prevent memory leaks
|
|
47
|
+
const MAX_CACHE_SIZE = 100;
|
|
48
|
+
|
|
49
|
+
// Helper function to clean up expired entries and enforce size limit
|
|
50
|
+
function cleanupCache() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const entries = Array.from(authenticatedFileCache.entries());
|
|
53
|
+
|
|
54
|
+
// Remove expired entries
|
|
55
|
+
const expiredKeys: string[] = [];
|
|
56
|
+
entries.forEach(([key, value]) => {
|
|
57
|
+
if (now - value.timestamp >= value.ttl) {
|
|
58
|
+
expiredKeys.push(key);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
expiredKeys.forEach(key => authenticatedFileCache.delete(key));
|
|
62
|
+
|
|
63
|
+
// If still over limit, remove oldest entries
|
|
64
|
+
if (authenticatedFileCache.size > MAX_CACHE_SIZE) {
|
|
65
|
+
const sorted = entries
|
|
66
|
+
.filter(([key]) => !expiredKeys.includes(key))
|
|
67
|
+
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
68
|
+
const toRemove = sorted.slice(0, authenticatedFileCache.size - MAX_CACHE_SIZE);
|
|
69
|
+
toRemove.forEach(([key]) => authenticatedFileCache.delete(key));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface UseFileDisplayReturn {
|
|
74
|
+
/** Single file URL if category is provided and file found, null otherwise */
|
|
75
|
+
fileUrl: string | null;
|
|
76
|
+
/** Single file reference if category is provided and file found, null otherwise */
|
|
77
|
+
fileReference: FileReference | null;
|
|
78
|
+
/** Array of all file references for the record (when category not provided or for multiple files) */
|
|
79
|
+
fileReferences: FileReference[];
|
|
80
|
+
/** Map of file IDs to URLs for multiple files */
|
|
81
|
+
fileUrls: Map<string, string>;
|
|
82
|
+
/** Total count of files for the record */
|
|
83
|
+
fileCount: number;
|
|
84
|
+
/** Whether the data is currently loading */
|
|
85
|
+
isLoading: boolean;
|
|
86
|
+
/** Any error that occurred during loading */
|
|
87
|
+
error: Error | null;
|
|
88
|
+
/** Function to manually refetch the data */
|
|
89
|
+
refetch: () => Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface UseFileDisplayOptions {
|
|
93
|
+
/** Cache TTL in milliseconds (default: 30 minutes) */
|
|
94
|
+
cacheTtl?: number;
|
|
95
|
+
/** Whether to enable caching (default: true) */
|
|
96
|
+
enableCache?: boolean;
|
|
97
|
+
/** Supabase client instance (required) */
|
|
98
|
+
supabase: SupabaseClient | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Hook for accessing file references in authenticated contexts
|
|
103
|
+
*
|
|
104
|
+
* This hook provides access to file references for authenticated users.
|
|
105
|
+
* It supports both public and private files, generating appropriate URLs
|
|
106
|
+
* (public URLs for public files, signed URLs for private files).
|
|
107
|
+
*
|
|
108
|
+
* @param table_name - The table name containing the file reference
|
|
109
|
+
* @param record_id - The record ID that owns the file(s)
|
|
110
|
+
* @param organisation_id - The organisation ID for storage path
|
|
111
|
+
* @param category - Optional file category to filter by (for single file mode)
|
|
112
|
+
* @param options - Configuration options for caching and behavior
|
|
113
|
+
* @returns Object containing file data, loading state, error, and refetch function
|
|
114
|
+
*/
|
|
115
|
+
export function useFileDisplay(
|
|
116
|
+
table_name: string | undefined,
|
|
117
|
+
record_id: string | undefined,
|
|
118
|
+
organisation_id: string | undefined,
|
|
119
|
+
category: FileCategory | undefined,
|
|
120
|
+
options: UseFileDisplayOptions
|
|
121
|
+
): UseFileDisplayReturn {
|
|
122
|
+
const {
|
|
123
|
+
cacheTtl = 30 * 60 * 1000, // 30 minutes
|
|
124
|
+
enableCache = true,
|
|
125
|
+
supabase
|
|
126
|
+
} = options;
|
|
127
|
+
|
|
128
|
+
const [fileUrl, setFileUrl] = useState<string | null>(null);
|
|
129
|
+
const [fileReference, setFileReference] = useState<FileReference | null>(null);
|
|
130
|
+
const [fileReferences, setFileReferences] = useState<FileReference[]>([]);
|
|
131
|
+
const [fileUrls, setFileUrls] = useState<Map<string, string>>(new Map());
|
|
132
|
+
const [fileCount, setFileCount] = useState<number>(0);
|
|
133
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
134
|
+
const [error, setError] = useState<Error | null>(null);
|
|
135
|
+
|
|
136
|
+
const fetchFiles = useCallback(async (): Promise<void> => {
|
|
137
|
+
if (!table_name || !record_id || !organisation_id || !supabase) {
|
|
138
|
+
setFileUrl(null);
|
|
139
|
+
setFileReference(null);
|
|
140
|
+
setFileReferences([]);
|
|
141
|
+
setFileUrls(new Map());
|
|
142
|
+
setFileCount(0);
|
|
143
|
+
setIsLoading(false);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Validate UUID format for organisationId to prevent database errors
|
|
148
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
149
|
+
if (!uuidRegex.test(organisation_id)) {
|
|
150
|
+
console.warn('[useFileDisplay] Invalid organisationId format (not a valid UUID):', organisation_id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check cache first
|
|
154
|
+
const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
|
|
155
|
+
if (enableCache) {
|
|
156
|
+
const cached = authenticatedFileCache.get(cacheKey);
|
|
157
|
+
if (cached && Date.now() - cached.timestamp < cached.ttl) {
|
|
158
|
+
const cachedData = cached.data;
|
|
159
|
+
setFileUrl(cachedData.fileUrl || null);
|
|
160
|
+
setFileReference(cachedData.fileReference || null);
|
|
161
|
+
setFileReferences(cachedData.fileReferences || []);
|
|
162
|
+
setFileUrls(cachedData.fileUrls || new Map());
|
|
163
|
+
setFileCount(cachedData.fileCount || 0);
|
|
164
|
+
setIsLoading(false);
|
|
165
|
+
setError(null);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
setIsLoading(true);
|
|
172
|
+
setError(null);
|
|
173
|
+
|
|
174
|
+
const service = createFileReferenceService(supabase);
|
|
175
|
+
let files: FileReference[] = [];
|
|
176
|
+
|
|
177
|
+
if (category) {
|
|
178
|
+
// Single file mode - get files by category
|
|
179
|
+
files = await service.getFilesByCategory(
|
|
180
|
+
table_name,
|
|
181
|
+
record_id,
|
|
182
|
+
category,
|
|
183
|
+
organisation_id
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
// Multiple files mode - get all files
|
|
187
|
+
files = await service.listFileReferences(
|
|
188
|
+
table_name,
|
|
189
|
+
record_id,
|
|
190
|
+
organisation_id
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (files.length === 0) {
|
|
195
|
+
setFileUrl(null);
|
|
196
|
+
setFileReference(null);
|
|
197
|
+
setFileReferences([]);
|
|
198
|
+
setFileUrls(new Map());
|
|
199
|
+
setFileCount(0);
|
|
200
|
+
|
|
201
|
+
// Cache empty result
|
|
202
|
+
if (enableCache) {
|
|
203
|
+
authenticatedFileCache.set(cacheKey, {
|
|
204
|
+
data: { fileUrl: null, fileReference: null, fileReferences: [], fileUrls: new Map(), fileCount: 0 },
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
ttl: cacheTtl
|
|
207
|
+
});
|
|
208
|
+
cleanupCache();
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
setFileReferences(files);
|
|
214
|
+
setFileCount(files.length);
|
|
215
|
+
|
|
216
|
+
if (category && files.length > 0) {
|
|
217
|
+
// Single file mode - get first file
|
|
218
|
+
const firstFile = files[0];
|
|
219
|
+
setFileReference(firstFile);
|
|
220
|
+
|
|
221
|
+
// Generate URL based on file visibility
|
|
222
|
+
let url: string | null = null;
|
|
223
|
+
if (firstFile.is_public) {
|
|
224
|
+
url = getPublicUrl(supabase, firstFile.file_path, true);
|
|
225
|
+
} else {
|
|
226
|
+
const signedUrlResult = await getSignedUrl(supabase, firstFile.file_path, {
|
|
227
|
+
appName: 'pace-core',
|
|
228
|
+
orgId: organisation_id,
|
|
229
|
+
expiresIn: 3600
|
|
230
|
+
});
|
|
231
|
+
url = signedUrlResult?.url || null;
|
|
232
|
+
}
|
|
233
|
+
setFileUrl(url);
|
|
234
|
+
} else {
|
|
235
|
+
// Multiple files mode - generate URLs for all files
|
|
236
|
+
const urlMap = new Map<string, string>();
|
|
237
|
+
for (const fileRef of files) {
|
|
238
|
+
let url: string | null = null;
|
|
239
|
+
if (fileRef.is_public) {
|
|
240
|
+
url = getPublicUrl(supabase, fileRef.file_path, true);
|
|
241
|
+
} else {
|
|
242
|
+
const signedUrlResult = await getSignedUrl(supabase, fileRef.file_path, {
|
|
243
|
+
appName: 'pace-core',
|
|
244
|
+
orgId: organisation_id,
|
|
245
|
+
expiresIn: 3600
|
|
246
|
+
});
|
|
247
|
+
url = signedUrlResult?.url || null;
|
|
248
|
+
}
|
|
249
|
+
if (url) {
|
|
250
|
+
urlMap.set(fileRef.id, url);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
setFileUrls(urlMap);
|
|
254
|
+
setFileReference(null);
|
|
255
|
+
setFileUrl(null);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Cache the result
|
|
259
|
+
if (enableCache) {
|
|
260
|
+
// Prepare cache data
|
|
261
|
+
let cacheData: any = {
|
|
262
|
+
fileReference: category && files.length > 0 ? files[0] : null,
|
|
263
|
+
fileReferences: files,
|
|
264
|
+
fileUrls: new Map(),
|
|
265
|
+
fileCount: files.length
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (category && files.length > 0) {
|
|
269
|
+
const firstFile = files[0];
|
|
270
|
+
let url: string | null = null;
|
|
271
|
+
if (firstFile.is_public) {
|
|
272
|
+
url = getPublicUrl(supabase, firstFile.file_path, true);
|
|
273
|
+
}
|
|
274
|
+
cacheData.fileUrl = url;
|
|
275
|
+
} else {
|
|
276
|
+
const urlMap = new Map<string, string>();
|
|
277
|
+
for (const fileRef of files) {
|
|
278
|
+
if (fileRef.is_public) {
|
|
279
|
+
const url = getPublicUrl(supabase, fileRef.file_path, true);
|
|
280
|
+
if (url) {
|
|
281
|
+
urlMap.set(fileRef.id, url);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
cacheData.fileUrls = urlMap;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
authenticatedFileCache.set(cacheKey, {
|
|
289
|
+
data: cacheData,
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
ttl: cacheTtl
|
|
292
|
+
});
|
|
293
|
+
cleanupCache();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error('[useFileDisplay] Error fetching files:', err);
|
|
298
|
+
const error = err instanceof Error ? err : new Error('Unknown error occurred');
|
|
299
|
+
setError(error);
|
|
300
|
+
setFileUrl(null);
|
|
301
|
+
setFileReference(null);
|
|
302
|
+
setFileReferences([]);
|
|
303
|
+
setFileUrls(new Map());
|
|
304
|
+
setFileCount(0);
|
|
305
|
+
} finally {
|
|
306
|
+
setIsLoading(false);
|
|
307
|
+
}
|
|
308
|
+
}, [table_name, record_id, organisation_id, category, supabase, cacheTtl, enableCache]);
|
|
309
|
+
|
|
310
|
+
// Fetch files when parameters change
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (table_name && record_id && organisation_id && supabase) {
|
|
313
|
+
fetchFiles();
|
|
314
|
+
} else {
|
|
315
|
+
setFileUrl(null);
|
|
316
|
+
setFileReference(null);
|
|
317
|
+
setFileReferences([]);
|
|
318
|
+
setFileUrls(new Map());
|
|
319
|
+
setFileCount(0);
|
|
320
|
+
setIsLoading(false);
|
|
321
|
+
setError(null);
|
|
322
|
+
}
|
|
323
|
+
}, [fetchFiles, table_name, record_id, organisation_id, supabase]);
|
|
324
|
+
|
|
325
|
+
const refetch = useCallback(async (): Promise<void> => {
|
|
326
|
+
if (!table_name || !record_id || !organisation_id || !supabase) return;
|
|
327
|
+
|
|
328
|
+
// Clear cache for this file
|
|
329
|
+
if (enableCache) {
|
|
330
|
+
const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
|
|
331
|
+
authenticatedFileCache.delete(cacheKey);
|
|
332
|
+
}
|
|
333
|
+
await fetchFiles();
|
|
334
|
+
}, [fetchFiles, table_name, record_id, organisation_id, category, supabase, enableCache]);
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
fileUrl,
|
|
338
|
+
fileReference,
|
|
339
|
+
fileReferences,
|
|
340
|
+
fileUrls,
|
|
341
|
+
fileCount,
|
|
342
|
+
isLoading,
|
|
343
|
+
error,
|
|
344
|
+
refetch
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Clear all cached authenticated file data
|
|
350
|
+
* Useful for testing or when you need to force refresh all data
|
|
351
|
+
*/
|
|
352
|
+
export function clearFileDisplayCache(): void {
|
|
353
|
+
for (const [key] of authenticatedFileCache) {
|
|
354
|
+
if (key.startsWith('file_')) {
|
|
355
|
+
authenticatedFileCache.delete(key);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get cache statistics for debugging
|
|
362
|
+
*/
|
|
363
|
+
export function getFileDisplayCacheStats(): { size: number; keys: string[] } {
|
|
364
|
+
const keys = Array.from(authenticatedFileCache.keys()).filter(key => key.startsWith('file_'));
|
|
365
|
+
return {
|
|
366
|
+
size: keys.length,
|
|
367
|
+
keys
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Custom Hook for File URL Generation
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Hooks/FileDisplay
|
|
5
|
+
*
|
|
6
|
+
* Extracts URL generation logic to eliminate duplication across FileDisplay components.
|
|
7
|
+
* Handles both public and private file URL generation with proper error handling.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
11
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
import { FileReference } from '../types/file-reference';
|
|
13
|
+
import { getPublicUrl, getSignedUrl } from '../utils/storage/helpers';
|
|
14
|
+
|
|
15
|
+
export interface UseFileUrlOptions {
|
|
16
|
+
/** Organisation ID for signed URL generation */
|
|
17
|
+
organisation_id: string;
|
|
18
|
+
/** Supabase client instance */
|
|
19
|
+
supabase: SupabaseClient;
|
|
20
|
+
/** Whether to auto-load URLs on mount */
|
|
21
|
+
autoLoad?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseFileUrlReturn {
|
|
25
|
+
/** Generated URL for the file reference */
|
|
26
|
+
url: string | null;
|
|
27
|
+
/** Whether URL is currently being generated */
|
|
28
|
+
isLoading: boolean;
|
|
29
|
+
/** Error if URL generation failed */
|
|
30
|
+
error: Error | null;
|
|
31
|
+
/** Manually trigger URL generation */
|
|
32
|
+
loadUrl: () => Promise<void>;
|
|
33
|
+
/** Clear the current URL and error */
|
|
34
|
+
clear: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Custom hook for generating file URLs (public or signed) for a single file reference
|
|
39
|
+
*
|
|
40
|
+
* @param fileReference - The file reference to generate URL for (null if no file)
|
|
41
|
+
* @param options - Configuration options
|
|
42
|
+
* @returns Hook return object with URL, loading state, and controls
|
|
43
|
+
*/
|
|
44
|
+
export function useFileUrl(
|
|
45
|
+
fileReference: FileReference | null,
|
|
46
|
+
options: UseFileUrlOptions
|
|
47
|
+
): UseFileUrlReturn {
|
|
48
|
+
const { organisation_id, supabase, autoLoad = true } = options;
|
|
49
|
+
|
|
50
|
+
const [url, setUrl] = useState<string | null>(null);
|
|
51
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
52
|
+
const [error, setError] = useState<Error | null>(null);
|
|
53
|
+
const fileReferenceIdRef = useRef<string | null>(null);
|
|
54
|
+
|
|
55
|
+
const loadUrl = useCallback(async () => {
|
|
56
|
+
if (!fileReference) {
|
|
57
|
+
setUrl(null);
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
setError(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Skip if already loading or URL already exists for this file
|
|
64
|
+
if (isLoading || (url && fileReferenceIdRef.current === fileReference.id)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setIsLoading(true);
|
|
69
|
+
setError(null);
|
|
70
|
+
fileReferenceIdRef.current = fileReference.id;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
let generatedUrl: string | null = null;
|
|
74
|
+
|
|
75
|
+
if (fileReference.is_public) {
|
|
76
|
+
// Public files: generate public URL
|
|
77
|
+
generatedUrl = getPublicUrl(supabase, fileReference.file_path, true);
|
|
78
|
+
} else {
|
|
79
|
+
// Private files: generate signed URL
|
|
80
|
+
const signedUrlResult = await getSignedUrl(supabase, fileReference.file_path, {
|
|
81
|
+
appName: 'file-reference',
|
|
82
|
+
orgId: organisation_id,
|
|
83
|
+
expiresIn: 3600
|
|
84
|
+
});
|
|
85
|
+
generatedUrl = signedUrlResult?.url || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setUrl(generatedUrl);
|
|
89
|
+
setError(null);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const error = err instanceof Error ? err : new Error('Failed to generate file URL');
|
|
92
|
+
setError(error);
|
|
93
|
+
setUrl(null);
|
|
94
|
+
console.error('[useFileUrl] Error generating URL:', error);
|
|
95
|
+
} finally {
|
|
96
|
+
setIsLoading(false);
|
|
97
|
+
}
|
|
98
|
+
}, [fileReference, supabase, organisation_id, isLoading, url]);
|
|
99
|
+
|
|
100
|
+
const clear = useCallback(() => {
|
|
101
|
+
setUrl(null);
|
|
102
|
+
setError(null);
|
|
103
|
+
setIsLoading(false);
|
|
104
|
+
fileReferenceIdRef.current = null;
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
// Auto-load URL when fileReference changes
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (autoLoad) {
|
|
110
|
+
// Reset URL when file reference changes
|
|
111
|
+
if (fileReferenceIdRef.current !== fileReference?.id) {
|
|
112
|
+
setUrl(null);
|
|
113
|
+
setError(null);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (fileReference && !url && !isLoading) {
|
|
117
|
+
loadUrl();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}, [fileReference?.id, autoLoad, loadUrl, url, isLoading]);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
url,
|
|
124
|
+
isLoading,
|
|
125
|
+
error,
|
|
126
|
+
loadUrl,
|
|
127
|
+
clear
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
@@ -435,16 +435,16 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
435
435
|
|
|
436
436
|
try {
|
|
437
437
|
console.debug('[AuthService] Fetching existing session from Supabase');
|
|
438
|
-
// Safely call getSession without destructuring to avoid runtime errors if undefined
|
|
439
438
|
let currentSession: Session | null = null;
|
|
440
439
|
let sessionError: AuthError | null = null;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
currentSession =
|
|
445
|
-
sessionError =
|
|
446
|
-
}
|
|
447
|
-
//
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const { data, error } = await this.supabaseClient.auth.getSession();
|
|
443
|
+
currentSession = data?.session ?? null;
|
|
444
|
+
sessionError = error ?? null;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
// Handle cases where getSession might not exist (mocked/test clients)
|
|
447
|
+
console.debug('[AuthService] getSession unavailable, treating as no active session');
|
|
448
448
|
currentSession = null;
|
|
449
449
|
sessionError = null;
|
|
450
450
|
}
|
|
@@ -456,20 +456,17 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
456
456
|
|
|
457
457
|
// Attempt getUser as fallback when getSession fails
|
|
458
458
|
try {
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (userError && !this.authError) {
|
|
471
|
-
this.authError = userError;
|
|
472
|
-
}
|
|
459
|
+
const { data, error } = await this.supabaseClient.auth.getUser();
|
|
460
|
+
const currentUser = data?.user ?? null;
|
|
461
|
+
const userError = error ?? null;
|
|
462
|
+
|
|
463
|
+
if (currentUser) {
|
|
464
|
+
this.user = currentUser;
|
|
465
|
+
// If we got a user but no session, we still don't have a valid session
|
|
466
|
+
this.session = null;
|
|
467
|
+
}
|
|
468
|
+
if (userError && !this.authError) {
|
|
469
|
+
this.authError = userError;
|
|
473
470
|
}
|
|
474
471
|
} catch (getUserError) {
|
|
475
472
|
// If getUser also fails, we've already recorded the sessionError
|