@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.
Files changed (169) hide show
  1. package/dist/{DataTable-HC5S4RKB.js → DataTable-CHX2EFO3.js} +6 -6
  2. package/dist/{PublicLoadingSpinner-n74JgA9h.d.ts → PublicLoadingSpinner-BWUD6bLU.d.ts} +24 -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-GP3HU6WS.js → chunk-G7UUVEAP.js} +3 -3
  10. package/dist/{chunk-M52CQP5W.js → chunk-MKMKUCPF.js} +762 -12
  11. package/dist/chunk-MKMKUCPF.js.map +1 -0
  12. package/dist/{chunk-OXFOS62D.js → chunk-MVNOAHOP.js} +2 -2
  13. package/dist/{chunk-SVMPR5IV.js → chunk-O6GASC4Q.js} +874 -784
  14. package/dist/chunk-O6GASC4Q.js.map +1 -0
  15. package/dist/{chunk-AYC2P377.js → chunk-ORACUZ7H.js} +2 -2
  16. package/dist/{chunk-TZXYSZT3.js → chunk-PRM6EYO3.js} +298 -238
  17. package/dist/{chunk-TZXYSZT3.js.map → chunk-PRM6EYO3.js.map} +1 -1
  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 +26 -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 +8 -1
  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 +605 -83
  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/services/AuthService.ts +19 -22
  158. package/dist/chunk-AAM57AEU.js.map +0 -1
  159. package/dist/chunk-M52CQP5W.js.map +0 -1
  160. package/dist/chunk-SVMPR5IV.js.map +0 -1
  161. /package/dist/{DataTable-HC5S4RKB.js.map → DataTable-CHX2EFO3.js.map} +0 -0
  162. /package/dist/{UnifiedAuthProvider-ZM7VUC45.js.map → UnifiedAuthProvider-H7RI4KYD.js.map} +0 -0
  163. /package/dist/{chunk-AZ2QJYKU.js.map → chunk-2KLAOD4M.js.map} +0 -0
  164. /package/dist/{chunk-HW5BGOWB.js.map → chunk-2ZYHCFUO.js.map} +0 -0
  165. /package/dist/{chunk-XIBSVWJW.js.map → chunk-7TQDRDSM.js.map} +0 -0
  166. /package/dist/{chunk-GP3HU6WS.js.map → chunk-G7UUVEAP.js.map} +0 -0
  167. /package/dist/{chunk-OXFOS62D.js.map → chunk-MVNOAHOP.js.map} +0 -0
  168. /package/dist/{chunk-AYC2P377.js.map → chunk-ORACUZ7H.js.map} +0 -0
  169. /package/dist/{chunk-6WFM22A4.js.map → chunk-ZGCVJ7WW.js.map} +0 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * @file Public File Display Hook
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/Public
5
+ * @since 1.0.0
6
+ *
7
+ * A React hook for accessing file references in public contexts without authentication.
8
+ * Provides file URLs and metadata for public pages.
9
+ *
10
+ * Features:
11
+ * - No authentication required
12
+ * - Only returns public files (is_public = true)
13
+ * - Caching for performance
14
+ * - Error handling and loading states
15
+ * - TypeScript support
16
+ * - Supports both single file (category) and multiple files
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { usePublicFileDisplay } from '@jmruthers/pace-core';
21
+ *
22
+ * function PublicFileView() {
23
+ * const { fileUrl, fileReference, isLoading, error } = usePublicFileDisplay(
24
+ * 'event',
25
+ * eventId,
26
+ * organisationId,
27
+ * FileCategory.EVENT_LOGOS,
28
+ * { supabase }
29
+ * );
30
+ *
31
+ * if (isLoading) return <div>Loading...</div>;
32
+ * if (error) return <div>Error: {error.message}</div>;
33
+ *
34
+ * return fileUrl ? <img src={fileUrl} alt="File" /> : null;
35
+ * }
36
+ * ```
37
+ */
38
+
39
+ import { useState, useEffect, useCallback, useMemo } from 'react';
40
+ import type { SupabaseClient } from '@supabase/supabase-js';
41
+ import type { Database } from '../../types/database';
42
+ import { FileReference, FileCategory } from '../../types/file-reference';
43
+ import { getPublicUrl } from '../../utils/storage/helpers';
44
+
45
+ // Simple in-memory cache for public file data
46
+ const publicFileCache = new Map<string, { data: any; timestamp: number; ttl: number }>();
47
+
48
+ export interface UsePublicFileDisplayReturn {
49
+ /** Single file URL if category is provided and file found, null otherwise */
50
+ fileUrl: string | null;
51
+ /** Single file reference if category is provided and file found, null otherwise */
52
+ fileReference: FileReference | null;
53
+ /** Array of all file references for the record (when category not provided or for multiple files) */
54
+ fileReferences: FileReference[];
55
+ /** Map of file IDs to URLs for multiple files */
56
+ fileUrls: Map<string, string>;
57
+ /** Total count of files for the record */
58
+ fileCount: number;
59
+ /** Whether the data is currently loading */
60
+ isLoading: boolean;
61
+ /** Any error that occurred during loading */
62
+ error: Error | null;
63
+ /** Function to manually refetch the data */
64
+ refetch: () => Promise<void>;
65
+ }
66
+
67
+ export interface UsePublicFileDisplayOptions {
68
+ /** Cache TTL in milliseconds (default: 30 minutes) */
69
+ cacheTtl?: number;
70
+ /** Whether to enable caching (default: true) */
71
+ enableCache?: boolean;
72
+ /** Supabase client instance (required) */
73
+ supabase: SupabaseClient<Database>;
74
+ }
75
+
76
+ /**
77
+ * Hook for accessing public file references
78
+ *
79
+ * This hook provides access to file references without requiring
80
+ * authentication. It only returns public files and generates public URLs.
81
+ *
82
+ * @param table_name - The table name containing the file reference
83
+ * @param record_id - The record ID that owns the file(s)
84
+ * @param organisation_id - The organisation ID for storage path
85
+ * @param category - Optional file category to filter by (for single file mode)
86
+ * @param options - Configuration options for caching and behavior
87
+ * @returns Object containing file data, loading state, error, and refetch function
88
+ */
89
+ export function usePublicFileDisplay(
90
+ table_name: string | undefined,
91
+ record_id: string | undefined,
92
+ organisation_id: string | undefined,
93
+ category: FileCategory | undefined,
94
+ options: UsePublicFileDisplayOptions
95
+ ): UsePublicFileDisplayReturn {
96
+ const {
97
+ cacheTtl = 30 * 60 * 1000, // 30 minutes
98
+ enableCache = true,
99
+ supabase
100
+ } = options;
101
+
102
+ const [fileUrl, setFileUrl] = useState<string | null>(null);
103
+ const [fileReference, setFileReference] = useState<FileReference | null>(null);
104
+ const [fileReferences, setFileReferences] = useState<FileReference[]>([]);
105
+ const [fileUrls, setFileUrls] = useState<Map<string, string>>(new Map());
106
+ const [fileCount, setFileCount] = useState<number>(0);
107
+ const [isLoading, setIsLoading] = useState<boolean>(false);
108
+ const [error, setError] = useState<Error | null>(null);
109
+
110
+ const fetchFiles = useCallback(async (): Promise<void> => {
111
+ if (!table_name || !record_id || !organisation_id || !supabase) {
112
+ setFileUrl(null);
113
+ setFileReference(null);
114
+ setFileReferences([]);
115
+ setFileUrls(new Map());
116
+ setFileCount(0);
117
+ setIsLoading(false);
118
+ return;
119
+ }
120
+
121
+ // Validate UUID format for organisationId to prevent database errors
122
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
123
+ if (!uuidRegex.test(organisation_id)) {
124
+ console.warn('[usePublicFileDisplay] Invalid organisationId format (not a valid UUID):', organisation_id);
125
+ }
126
+
127
+ // Check cache first
128
+ const cacheKey = `public_file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
129
+ if (enableCache) {
130
+ const cached = publicFileCache.get(cacheKey);
131
+ if (cached && Date.now() - cached.timestamp < cached.ttl) {
132
+ const cachedData = cached.data;
133
+ setFileUrl(cachedData.fileUrl || null);
134
+ setFileReference(cachedData.fileReference || null);
135
+ setFileReferences(cachedData.fileReferences || []);
136
+ setFileUrls(cachedData.fileUrls || new Map());
137
+ setFileCount(cachedData.fileCount || 0);
138
+ setIsLoading(false);
139
+ setError(null);
140
+ return;
141
+ }
142
+ }
143
+
144
+ try {
145
+ setIsLoading(true);
146
+ setError(null);
147
+
148
+ let files: any[] = [];
149
+
150
+ if (category) {
151
+ // Single file mode - use RPC to get files by category
152
+ const { data, error: rpcError } = await (supabase as any)
153
+ .rpc('data_file_reference_by_category_list', {
154
+ p_table_name: table_name,
155
+ p_record_id: record_id,
156
+ p_category: category,
157
+ p_organisation_id: organisation_id
158
+ });
159
+
160
+ if (rpcError) {
161
+ throw new Error(rpcError.message || 'Failed to fetch file reference');
162
+ }
163
+
164
+ files = data || [];
165
+ } else {
166
+ // Multiple files mode - use RPC to get all files
167
+ const { data: fileIds, error: rpcError } = await (supabase as any)
168
+ .rpc('data_file_reference_list', {
169
+ p_table_name: table_name,
170
+ p_record_id: record_id,
171
+ p_organisation_id: organisation_id
172
+ });
173
+
174
+ if (rpcError) {
175
+ throw new Error(rpcError.message || 'Failed to fetch file references');
176
+ }
177
+
178
+ if (!fileIds || fileIds.length === 0) {
179
+ files = [];
180
+ } else {
181
+ // Fetch full file reference data for each ID, but only public files
182
+ const ids = fileIds.map((item: any) => item.id);
183
+ const { data: fullData, error: fetchError } = await supabase
184
+ .from('file_references')
185
+ .select('*')
186
+ .in('id', ids)
187
+ .eq('is_public', true); // Only public files in public context
188
+
189
+ if (fetchError) {
190
+ throw new Error(fetchError.message || 'Failed to fetch file references');
191
+ }
192
+
193
+ files = fullData || [];
194
+ }
195
+ }
196
+
197
+ // Ensure all files are public (category RPC might return both)
198
+ const publicFiles = files.filter((f: any) => f.is_public === true);
199
+
200
+ if (publicFiles.length === 0) {
201
+ setFileUrl(null);
202
+ setFileReference(null);
203
+ setFileReferences([]);
204
+ setFileUrls(new Map());
205
+ setFileCount(0);
206
+
207
+ // Cache empty result
208
+ if (enableCache) {
209
+ publicFileCache.set(cacheKey, {
210
+ data: { fileUrl: null, fileReference: null, fileReferences: [], fileUrls: new Map(), fileCount: 0 },
211
+ timestamp: Date.now(),
212
+ ttl: cacheTtl
213
+ });
214
+ }
215
+ return;
216
+ }
217
+
218
+ // Convert to FileReference format
219
+ const fileRefs: FileReference[] = publicFiles.map((f: any) => ({
220
+ id: f.id,
221
+ table_name: f.table_name,
222
+ record_id: f.record_id,
223
+ file_path: f.file_path,
224
+ file_metadata: f.file_metadata || {},
225
+ organisation_id: f.organisation_id,
226
+ app_id: f.app_id,
227
+ is_public: f.is_public ?? true,
228
+ created_at: f.created_at,
229
+ updated_at: f.updated_at
230
+ }));
231
+
232
+ setFileReferences(fileRefs);
233
+ setFileCount(fileRefs.length);
234
+
235
+ if (category && fileRefs.length > 0) {
236
+ // Single file mode - get first file
237
+ const firstFile = fileRefs[0];
238
+ setFileReference(firstFile);
239
+
240
+ // Generate public URL
241
+ const url = getPublicUrl(supabase, firstFile.file_path, true);
242
+ setFileUrl(url);
243
+ } else {
244
+ // Multiple files mode - generate URLs for all files
245
+ const urlMap = new Map<string, string>();
246
+ for (const fileRef of fileRefs) {
247
+ const url = getPublicUrl(supabase, fileRef.file_path, true);
248
+ if (url) {
249
+ urlMap.set(fileRef.id, url);
250
+ }
251
+ }
252
+ setFileUrls(urlMap);
253
+ setFileReference(null);
254
+ setFileUrl(null);
255
+ }
256
+
257
+ // Cache the result
258
+ if (enableCache) {
259
+ publicFileCache.set(cacheKey, {
260
+ data: {
261
+ fileUrl: category ? (fileRefs.length > 0 ? getPublicUrl(supabase, fileRefs[0].file_path, true) : null) : null,
262
+ fileReference: category && fileRefs.length > 0 ? fileRefs[0] : null,
263
+ fileReferences: fileRefs,
264
+ fileUrls: category ? new Map() : (() => {
265
+ const urlMap = new Map<string, string>();
266
+ for (const fileRef of fileRefs) {
267
+ const url = getPublicUrl(supabase, fileRef.file_path, true);
268
+ if (url) {
269
+ urlMap.set(fileRef.id, url);
270
+ }
271
+ }
272
+ return urlMap;
273
+ })(),
274
+ fileCount: fileRefs.length
275
+ },
276
+ timestamp: Date.now(),
277
+ ttl: cacheTtl
278
+ });
279
+ }
280
+
281
+ } catch (err) {
282
+ console.error('[usePublicFileDisplay] Error fetching files:', err);
283
+ const error = err instanceof Error ? err : new Error('Unknown error occurred');
284
+ setError(error);
285
+ setFileUrl(null);
286
+ setFileReference(null);
287
+ setFileReferences([]);
288
+ setFileUrls(new Map());
289
+ setFileCount(0);
290
+ } finally {
291
+ setIsLoading(false);
292
+ }
293
+ }, [table_name, record_id, organisation_id, category, supabase, cacheTtl, enableCache]);
294
+
295
+ // Fetch files when parameters change
296
+ useEffect(() => {
297
+ if (table_name && record_id && organisation_id) {
298
+ fetchFiles();
299
+ } else {
300
+ setFileUrl(null);
301
+ setFileReference(null);
302
+ setFileReferences([]);
303
+ setFileUrls(new Map());
304
+ setFileCount(0);
305
+ setIsLoading(false);
306
+ setError(null);
307
+ }
308
+ }, [fetchFiles, table_name, record_id, organisation_id]);
309
+
310
+ const refetch = useCallback(async (): Promise<void> => {
311
+ if (!table_name || !record_id || !organisation_id) return;
312
+
313
+ // Clear cache for this file
314
+ if (enableCache) {
315
+ const cacheKey = `public_file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
316
+ publicFileCache.delete(cacheKey);
317
+ }
318
+ await fetchFiles();
319
+ }, [fetchFiles, table_name, record_id, organisation_id, category, enableCache]);
320
+
321
+ return {
322
+ fileUrl,
323
+ fileReference,
324
+ fileReferences,
325
+ fileUrls,
326
+ fileCount,
327
+ isLoading,
328
+ error,
329
+ refetch
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Clear all cached public file data
335
+ * Useful for testing or when you need to force refresh all data
336
+ */
337
+ export function clearPublicFileDisplayCache(): void {
338
+ for (const [key] of publicFileCache) {
339
+ if (key.startsWith('public_file_')) {
340
+ publicFileCache.delete(key);
341
+ }
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Get cache statistics for debugging
347
+ */
348
+ export function getPublicFileDisplayCacheStats(): { size: number; keys: string[] } {
349
+ const keys = Array.from(publicFileCache.keys()).filter(key => key.startsWith('public_file_'));
350
+ return {
351
+ size: keys.length,
352
+ keys
353
+ };
354
+ }
355
+