@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.
Files changed (170) hide show
  1. package/dist/{DataTable-HC5S4RKB.js → DataTable-XENXNMCP.js} +6 -6
  2. package/dist/{PublicLoadingSpinner-n74JgA9h.d.ts → PublicLoadingSpinner-B84QWsvB.d.ts} +31 -3
  3. package/dist/{UnifiedAuthProvider-ZM7VUC45.js → UnifiedAuthProvider-H7RI4KYD.js} +3 -3
  4. package/dist/{chunk-AZ2QJYKU.js → chunk-2KLAOD4M.js} +3 -3
  5. package/dist/{chunk-HW5BGOWB.js → chunk-2ZYHCFUO.js} +4 -4
  6. package/dist/{chunk-AAM57AEU.js → chunk-5RYPBJYL.js} +16 -19
  7. package/dist/chunk-5RYPBJYL.js.map +1 -0
  8. package/dist/{chunk-XIBSVWJW.js → chunk-7TQDRDSM.js} +5 -5
  9. package/dist/{chunk-TZXYSZT3.js → chunk-EPKHU5SS.js} +314 -245
  10. package/dist/{chunk-TZXYSZT3.js.map → chunk-EPKHU5SS.js.map} +1 -1
  11. package/dist/{chunk-GP3HU6WS.js → chunk-G7UUVEAP.js} +3 -3
  12. package/dist/{chunk-M52CQP5W.js → chunk-MKMKUCPF.js} +762 -12
  13. package/dist/chunk-MKMKUCPF.js.map +1 -0
  14. package/dist/{chunk-OXFOS62D.js → chunk-MVNOAHOP.js} +2 -2
  15. package/dist/{chunk-AYC2P377.js → chunk-ORACUZ7H.js} +2 -2
  16. package/dist/{chunk-SVMPR5IV.js → chunk-V5CTX4FR.js} +963 -788
  17. package/dist/chunk-V5CTX4FR.js.map +1 -0
  18. package/dist/{chunk-6WFM22A4.js → chunk-ZGCVJ7WW.js} +2 -2
  19. package/dist/components.d.ts +1 -1
  20. package/dist/components.js +8 -8
  21. package/dist/hooks.d.ts +94 -3
  22. package/dist/hooks.js +20 -8
  23. package/dist/hooks.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +17 -11
  26. package/dist/index.js.map +1 -1
  27. package/dist/providers.js +2 -2
  28. package/dist/rbac/index.js +7 -7
  29. package/dist/{usePublicRouteParams-BlgwXweB.d.ts → usePublicRouteParams-BwMR2uub.d.ts} +93 -1
  30. package/dist/utils.js +1 -1
  31. package/docs/api/classes/ColumnFactory.md +1 -1
  32. package/docs/api/classes/ErrorBoundary.md +1 -1
  33. package/docs/api/classes/InvalidScopeError.md +1 -1
  34. package/docs/api/classes/MissingUserContextError.md +1 -1
  35. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  36. package/docs/api/classes/PermissionDeniedError.md +1 -1
  37. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  38. package/docs/api/classes/RBACAuditManager.md +1 -1
  39. package/docs/api/classes/RBACCache.md +1 -1
  40. package/docs/api/classes/RBACEngine.md +1 -1
  41. package/docs/api/classes/RBACError.md +1 -1
  42. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  43. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  44. package/docs/api/classes/StorageUtils.md +1 -1
  45. package/docs/api/enums/FileCategory.md +1 -1
  46. package/docs/api/interfaces/AggregateConfig.md +1 -1
  47. package/docs/api/interfaces/ButtonProps.md +1 -1
  48. package/docs/api/interfaces/CardProps.md +1 -1
  49. package/docs/api/interfaces/ColorPalette.md +1 -1
  50. package/docs/api/interfaces/ColorShade.md +1 -1
  51. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  52. package/docs/api/interfaces/DataRecord.md +1 -1
  53. package/docs/api/interfaces/DataTableAction.md +1 -1
  54. package/docs/api/interfaces/DataTableColumn.md +1 -1
  55. package/docs/api/interfaces/DataTableProps.md +1 -1
  56. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  57. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  58. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  59. package/docs/api/interfaces/EventLogoProps.md +1 -1
  60. package/docs/api/interfaces/FileDisplayProps.md +52 -11
  61. package/docs/api/interfaces/FileMetadata.md +1 -1
  62. package/docs/api/interfaces/FileReference.md +1 -1
  63. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  64. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  65. package/docs/api/interfaces/FileUploadProps.md +1 -1
  66. package/docs/api/interfaces/FooterProps.md +1 -1
  67. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  68. package/docs/api/interfaces/InputProps.md +1 -1
  69. package/docs/api/interfaces/LabelProps.md +1 -1
  70. package/docs/api/interfaces/LoginFormProps.md +1 -1
  71. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  72. package/docs/api/interfaces/NavigationContextType.md +1 -1
  73. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  74. package/docs/api/interfaces/NavigationItem.md +1 -1
  75. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  76. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  77. package/docs/api/interfaces/Organisation.md +1 -1
  78. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  79. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  80. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  81. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  82. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  83. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  84. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  85. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  86. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  87. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  88. package/docs/api/interfaces/PaletteData.md +1 -1
  89. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  90. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  91. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  92. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  93. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  94. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  95. package/docs/api/interfaces/PublicPageHeaderProps.md +24 -11
  96. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  97. package/docs/api/interfaces/RBACConfig.md +1 -1
  98. package/docs/api/interfaces/RBACLogger.md +1 -1
  99. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  100. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  101. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  102. package/docs/api/interfaces/RouteConfig.md +1 -1
  103. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  104. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  105. package/docs/api/interfaces/StorageConfig.md +1 -1
  106. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  107. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  108. package/docs/api/interfaces/StorageListOptions.md +1 -1
  109. package/docs/api/interfaces/StorageListResult.md +1 -1
  110. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  111. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  112. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  113. package/docs/api/interfaces/StyleImport.md +1 -1
  114. package/docs/api/interfaces/SwitchProps.md +1 -1
  115. package/docs/api/interfaces/ToastActionElement.md +1 -1
  116. package/docs/api/interfaces/ToastProps.md +1 -1
  117. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  118. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  119. package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
  120. package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
  121. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  122. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  123. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  124. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  125. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +47 -0
  128. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +120 -0
  129. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  130. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  132. package/docs/api/interfaces/UserEventAccess.md +1 -1
  133. package/docs/api/interfaces/UserMenuProps.md +1 -1
  134. package/docs/api/interfaces/UserProfile.md +1 -1
  135. package/docs/api/modules.md +102 -16
  136. package/docs/implementation-guides/file-reference-system.md +15 -0
  137. package/docs/implementation-guides/file-upload-storage.md +16 -0
  138. package/package.json +1 -1
  139. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +9 -7
  140. package/src/components/DataTable/components/DataTableCore.tsx +35 -10
  141. package/src/components/DataTable/components/EditableRow.tsx +62 -22
  142. package/src/components/DataTable/components/UnifiedTableBody.tsx +25 -101
  143. package/src/components/FileDisplay/FileDisplay.test.tsx +263 -39
  144. package/src/components/FileDisplay/FileDisplay.tsx +667 -103
  145. package/src/components/PublicLayout/PublicPageHeader.tsx +15 -8
  146. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +71 -28
  147. package/src/components/Select/Select.test.tsx +83 -6
  148. package/src/components/Select/Select.tsx +236 -16
  149. package/src/examples/CorrectPublicPageImplementation.tsx +16 -13
  150. package/src/examples/PublicEventPage.tsx +9 -6
  151. package/src/examples/PublicPageApp.tsx +9 -6
  152. package/src/examples/PublicPageUsageExample.tsx +9 -7
  153. package/src/hooks/index.ts +4 -0
  154. package/src/hooks/public/index.ts +2 -0
  155. package/src/hooks/public/usePublicFileDisplay.ts +355 -0
  156. package/src/hooks/useFileDisplay.ts +370 -0
  157. package/src/hooks/useFileUrl.ts +130 -0
  158. package/src/services/AuthService.ts +19 -22
  159. package/dist/chunk-AAM57AEU.js.map +0 -1
  160. package/dist/chunk-M52CQP5W.js.map +0 -1
  161. package/dist/chunk-SVMPR5IV.js.map +0 -1
  162. /package/dist/{DataTable-HC5S4RKB.js.map → DataTable-XENXNMCP.js.map} +0 -0
  163. /package/dist/{UnifiedAuthProvider-ZM7VUC45.js.map → UnifiedAuthProvider-H7RI4KYD.js.map} +0 -0
  164. /package/dist/{chunk-AZ2QJYKU.js.map → chunk-2KLAOD4M.js.map} +0 -0
  165. /package/dist/{chunk-HW5BGOWB.js.map → chunk-2ZYHCFUO.js.map} +0 -0
  166. /package/dist/{chunk-XIBSVWJW.js.map → chunk-7TQDRDSM.js.map} +0 -0
  167. /package/dist/{chunk-GP3HU6WS.js.map → chunk-G7UUVEAP.js.map} +0 -0
  168. /package/dist/{chunk-OXFOS62D.js.map → chunk-MVNOAHOP.js.map} +0 -0
  169. /package/dist/{chunk-AYC2P377.js.map → chunk-ORACUZ7H.js.map} +0 -0
  170. /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
- const getSessionFn = (this.supabaseClient.auth as any)?.getSession as (() => Promise<{ data?: { session?: Session | null }, error?: AuthError | null }>) | undefined;
442
- if (typeof getSessionFn === 'function') {
443
- const sessionResult = await getSessionFn();
444
- currentSession = sessionResult?.data?.session ?? null;
445
- sessionError = sessionResult?.error ?? null;
446
- } else {
447
- // If getSession is unavailable in this environment/mocked client, treat as no active session
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 getUserFn = (this.supabaseClient.auth as any)?.getUser as (() => Promise<{ data?: { user?: User | null }, error?: AuthError | null }>) | undefined;
460
- if (typeof getUserFn === 'function') {
461
- const userResult = await getUserFn();
462
- const currentUser = userResult?.data?.user ?? null;
463
- const userError = userResult?.error ?? null;
464
-
465
- if (currentUser) {
466
- this.user = currentUser;
467
- // If we got a user but no session, we still don't have a valid session
468
- this.session = null;
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