@jmruthers/pace-core 0.5.185 → 0.5.187

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 (300) hide show
  1. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  2. package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
  3. package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
  8. package/dist/chunk-3IC5WCMO.js.map +1 -0
  9. package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
  10. package/dist/chunk-3NFNJOO7.js.map +1 -0
  11. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  12. package/dist/chunk-63FOKYGO.js.map +1 -0
  13. package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
  14. package/dist/chunk-C4OYJOV4.js.map +1 -0
  15. package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
  19. package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/chunk-SAUPYVLF.js.map +1 -0
  23. package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
  26. package/dist/chunk-ULX5FYEM.js.map +1 -0
  27. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  28. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  29. package/dist/chunk-YHCN776L.js +447 -0
  30. package/dist/chunk-YHCN776L.js.map +1 -0
  31. package/dist/components.d.ts +4 -4
  32. package/dist/components.js +12 -10
  33. package/dist/components.js.map +1 -1
  34. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  35. package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
  36. package/dist/hooks.d.ts +265 -6
  37. package/dist/hooks.js +148 -49
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +25 -10
  40. package/dist/index.js +65 -30
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.js +1 -1
  43. package/dist/rbac/index.d.ts +125 -8
  44. package/dist/rbac/index.js +27 -7
  45. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  46. package/dist/types.d.ts +2 -2
  47. package/dist/types.js +1 -1
  48. package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
  49. package/dist/utils.d.ts +214 -4
  50. package/dist/utils.js +22 -2
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/classes/ColumnFactory.md +1 -1
  53. package/docs/api/classes/ErrorBoundary.md +1 -1
  54. package/docs/api/classes/InvalidScopeError.md +1 -1
  55. package/docs/api/classes/Logger.md +1 -1
  56. package/docs/api/classes/MissingUserContextError.md +1 -1
  57. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  58. package/docs/api/classes/PermissionDeniedError.md +1 -1
  59. package/docs/api/classes/RBACAuditManager.md +21 -17
  60. package/docs/api/classes/RBACCache.md +31 -23
  61. package/docs/api/classes/RBACEngine.md +6 -6
  62. package/docs/api/classes/RBACError.md +1 -1
  63. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  64. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  65. package/docs/api/classes/StorageUtils.md +1 -1
  66. package/docs/api/enums/FileCategory.md +1 -1
  67. package/docs/api/enums/LogLevel.md +1 -1
  68. package/docs/api/enums/RBACErrorCode.md +1 -1
  69. package/docs/api/enums/RPCFunction.md +1 -1
  70. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  71. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  72. package/docs/api/interfaces/AggregateConfig.md +1 -1
  73. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  74. package/docs/api/interfaces/BadgeProps.md +1 -1
  75. package/docs/api/interfaces/ButtonProps.md +1 -1
  76. package/docs/api/interfaces/CalendarProps.md +1 -1
  77. package/docs/api/interfaces/CardProps.md +1 -1
  78. package/docs/api/interfaces/ColorPalette.md +1 -1
  79. package/docs/api/interfaces/ColorShade.md +1 -1
  80. package/docs/api/interfaces/ComplianceResult.md +1 -1
  81. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  82. package/docs/api/interfaces/DataRecord.md +1 -1
  83. package/docs/api/interfaces/DataTableAction.md +1 -1
  84. package/docs/api/interfaces/DataTableColumn.md +1 -1
  85. package/docs/api/interfaces/DataTableProps.md +1 -1
  86. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  87. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  88. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  92. package/docs/api/interfaces/ExportColumn.md +1 -1
  93. package/docs/api/interfaces/ExportOptions.md +1 -1
  94. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  95. package/docs/api/interfaces/FileMetadata.md +1 -1
  96. package/docs/api/interfaces/FileReference.md +1 -1
  97. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  98. package/docs/api/interfaces/FileUploadOptions.md +33 -9
  99. package/docs/api/interfaces/FileUploadProps.md +36 -14
  100. package/docs/api/interfaces/FooterProps.md +1 -1
  101. package/docs/api/interfaces/FormFieldProps.md +1 -1
  102. package/docs/api/interfaces/FormProps.md +1 -1
  103. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  104. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  105. package/docs/api/interfaces/InputProps.md +1 -1
  106. package/docs/api/interfaces/LabelProps.md +1 -1
  107. package/docs/api/interfaces/LoggerConfig.md +1 -1
  108. package/docs/api/interfaces/LoginFormProps.md +1 -1
  109. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  110. package/docs/api/interfaces/NavigationContextType.md +1 -1
  111. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  112. package/docs/api/interfaces/NavigationItem.md +1 -1
  113. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  114. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  115. package/docs/api/interfaces/Organisation.md +1 -1
  116. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  117. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  118. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  119. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  120. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  121. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  122. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  123. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  124. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  125. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  126. package/docs/api/interfaces/PaletteData.md +1 -1
  127. package/docs/api/interfaces/ParsedAddress.md +120 -0
  128. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  129. package/docs/api/interfaces/ProgressProps.md +1 -1
  130. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  131. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  132. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  133. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  134. package/docs/api/interfaces/QuickFix.md +1 -1
  135. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  136. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  137. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  138. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  139. package/docs/api/interfaces/RBACConfig.md +27 -4
  140. package/docs/api/interfaces/RBACContext.md +1 -1
  141. package/docs/api/interfaces/RBACLogger.md +5 -5
  142. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  144. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  146. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  148. package/docs/api/interfaces/RBACResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  153. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  154. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  155. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  156. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  157. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  158. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  159. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  160. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  161. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  162. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  163. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  164. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  165. package/docs/api/interfaces/RouteConfig.md +1 -1
  166. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  167. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  168. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  169. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  170. package/docs/api/interfaces/SetupIssue.md +1 -1
  171. package/docs/api/interfaces/StorageConfig.md +1 -1
  172. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  173. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  174. package/docs/api/interfaces/StorageListOptions.md +1 -1
  175. package/docs/api/interfaces/StorageListResult.md +1 -1
  176. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  177. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  178. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  179. package/docs/api/interfaces/StyleImport.md +1 -1
  180. package/docs/api/interfaces/SwitchProps.md +1 -1
  181. package/docs/api/interfaces/TabsContentProps.md +1 -1
  182. package/docs/api/interfaces/TabsListProps.md +1 -1
  183. package/docs/api/interfaces/TabsProps.md +1 -1
  184. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  185. package/docs/api/interfaces/TextareaProps.md +1 -1
  186. package/docs/api/interfaces/ToastActionElement.md +1 -1
  187. package/docs/api/interfaces/ToastProps.md +1 -1
  188. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  189. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  190. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  191. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  192. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  193. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  195. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  199. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  201. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  202. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  203. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  204. package/docs/api/interfaces/UserEventAccess.md +1 -1
  205. package/docs/api/interfaces/UserMenuProps.md +1 -1
  206. package/docs/api/interfaces/UserProfile.md +1 -1
  207. package/docs/api/modules.md +328 -69
  208. package/docs/api-reference/components.md +26 -12
  209. package/docs/best-practices/performance.md +11 -0
  210. package/docs/implementation-guides/file-reference-system.md +24 -2
  211. package/docs/implementation-guides/file-upload-storage.md +38 -1
  212. package/docs/rbac/README.md +2 -1
  213. package/docs/rbac/api-reference.md +11 -0
  214. package/docs/rbac/performance.md +320 -0
  215. package/docs/standards/01-architecture-standard.md +5 -0
  216. package/docs/standards/05-security-standard.md +12 -0
  217. package/package.json +1 -1
  218. package/scripts/check-pace-core-compliance.js +512 -0
  219. package/src/components/AddressField/AddressField.test.tsx +411 -0
  220. package/src/components/AddressField/AddressField.tsx +323 -0
  221. package/src/components/AddressField/README.md +336 -0
  222. package/src/components/AddressField/index.ts +10 -0
  223. package/src/components/AddressField/types.ts +65 -0
  224. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  225. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  226. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  227. package/src/components/FileUpload/FileUpload.tsx +7 -1
  228. package/src/components/Header/Header.tsx +2 -5
  229. package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
  230. package/src/components/index.ts +2 -0
  231. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  232. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  233. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  234. package/src/hooks/index.ts +9 -0
  235. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  236. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  237. package/src/hooks/useAddressAutocomplete.ts +268 -0
  238. package/src/hooks/useFileDisplay.ts +3 -15
  239. package/src/hooks/useFileReference.test.ts +21 -3
  240. package/src/hooks/useFileReference.ts +3 -24
  241. package/src/hooks/useFileUrlCache.ts +246 -0
  242. package/src/hooks/useInactivityTracker.ts +31 -20
  243. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  244. package/src/hooks/useOrganisationSecurity.ts +3 -3
  245. package/src/hooks/usePreventTabReload.ts +106 -0
  246. package/src/hooks/useQueryCache.ts +315 -0
  247. package/src/hooks/useSecureDataAccess.ts +2 -2
  248. package/src/index.ts +2 -0
  249. package/src/providers/services/EventServiceProvider.tsx +4 -1
  250. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  251. package/src/rbac/api.test.ts +21 -6
  252. package/src/rbac/api.ts +32 -11
  253. package/src/rbac/audit-batched.ts +223 -0
  254. package/src/rbac/audit-enhanced.ts +2 -2
  255. package/src/rbac/audit.test.ts +6 -5
  256. package/src/rbac/audit.ts +34 -6
  257. package/src/rbac/cache-invalidation.ts +63 -12
  258. package/src/rbac/cache.test.ts +2 -2
  259. package/src/rbac/cache.ts +61 -14
  260. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  261. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  262. package/src/rbac/config.ts +9 -0
  263. package/src/rbac/engine.ts +2 -21
  264. package/src/rbac/hooks/usePermissions.ts +21 -5
  265. package/src/rbac/index.ts +19 -0
  266. package/src/rbac/performance.ts +210 -0
  267. package/src/rbac/request-deduplication.ts +87 -0
  268. package/src/rbac/utils/deep-equal.ts +93 -0
  269. package/src/styles/core.css +5 -5
  270. package/src/types/database.generated.ts +63 -9
  271. package/src/types/file-reference.ts +3 -1
  272. package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
  273. package/src/utils/file-reference/index.ts +56 -17
  274. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  275. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  276. package/src/utils/google-places/index.ts +26 -0
  277. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  278. package/src/utils/google-places/types.ts +94 -0
  279. package/src/utils/index.ts +23 -0
  280. package/src/utils/request-deduplication.ts +165 -0
  281. package/src/utils/security/secureDataAccess.ts +1 -1
  282. package/src/utils/storage/helpers.ts +211 -4
  283. package/dist/chunk-445GEP27.js.map +0 -1
  284. package/dist/chunk-AISXLWGZ.js.map +0 -1
  285. package/dist/chunk-FMUCXFII.js +0 -76
  286. package/dist/chunk-FMUCXFII.js.map +0 -1
  287. package/dist/chunk-FSFQFJCU.js.map +0 -1
  288. package/dist/chunk-FXFJRTKI.js.map +0 -1
  289. package/dist/chunk-HC67NW5K.js.map +0 -1
  290. package/dist/chunk-IXSNYUCT.js.map +0 -1
  291. package/dist/chunk-MX3EIJGQ.js.map +0 -1
  292. package/dist/chunk-OKI34GZD.js.map +0 -1
  293. package/dist/chunk-STTZQK2I.js.map +0 -1
  294. package/dist/chunk-U6WNSFX5.js.map +0 -1
  295. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  296. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  297. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  298. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  299. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  300. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @file AddressField Component Types
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/AddressField
5
+ * @since 0.1.0
6
+ */
7
+
8
+ import type { ParsedAddress, AutocompleteOptions } from '../../utils/google-places';
9
+
10
+ /**
11
+ * Props for AddressField component
12
+ */
13
+ export interface AddressFieldProps
14
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'defaultValue' | 'size'> {
15
+ /** Google Places API key (required) */
16
+ apiKey: string;
17
+ /** Controlled input value */
18
+ value?: string;
19
+ /** Uncontrolled default value */
20
+ defaultValue?: string;
21
+ /** Callback when address is selected */
22
+ onChange?: (address: ParsedAddress | null) => void;
23
+ /** Callback when input value changes */
24
+ onInputChange?: (value: string) => void;
25
+ /** Placeholder text */
26
+ placeholder?: string;
27
+ /** Error state styling */
28
+ error?: boolean;
29
+ /** Disabled state */
30
+ disabled?: boolean;
31
+ /** Input size */
32
+ size?: 'sm' | 'md' | 'lg';
33
+ /** Input variant */
34
+ variant?: 'default' | 'destructive';
35
+ /** Google Places API autocomplete options */
36
+ autocompleteOptions?: AutocompleteOptions;
37
+ /** Debounce delay in milliseconds */
38
+ debounceDelay?: number;
39
+ /** Enable caching (default: true) */
40
+ cacheEnabled?: boolean;
41
+ /** Cache TTL configuration */
42
+ cacheTTL?: {
43
+ /** Autocomplete cache TTL in seconds */
44
+ autocomplete?: number;
45
+ /** Place details cache TTL in seconds */
46
+ placeDetails?: number;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Ref type for AddressField component
52
+ */
53
+ export interface AddressFieldRef {
54
+ /** Focus the input */
55
+ focus: () => void;
56
+ /** Blur the input */
57
+ blur: () => void;
58
+ /** Get current input value */
59
+ getValue: () => string;
60
+ /** Clear the input and suggestions */
61
+ clear: () => void;
62
+ }
63
+
64
+ export type { ParsedAddress, AutocompleteOptions } from '../../utils/google-places';
65
+
@@ -39,6 +39,10 @@ vi.mock('../../hooks/public/usePublicFileDisplay', () => ({
39
39
  usePublicFileDisplay: vi.fn()
40
40
  }));
41
41
 
42
+ vi.mock('../../hooks/useFileUrl', () => ({
43
+ useFileUrl: vi.fn()
44
+ }));
45
+
42
46
  vi.mock('../../utils/storage/helpers', () => ({
43
47
  getPublicUrl: vi.fn((_, path: string) => `https://example.com/${path}`),
44
48
  getSignedUrl: vi.fn(async (_: unknown, path: string) => ({ url: `https://signed.example.com/${path}` })),
@@ -104,6 +108,15 @@ describe('[component] FileDisplay', () => {
104
108
  fileUrls: new Map(),
105
109
  refetch: vi.fn(),
106
110
  });
111
+
112
+ // Set up default mock for useFileUrl (used in displayOnly mode)
113
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
114
+ useFileUrl.mockReturnValue({
115
+ url: null,
116
+ isLoading: false,
117
+ error: null,
118
+ clear: vi.fn(),
119
+ });
107
120
  });
108
121
 
109
122
  it('renders error state and clears error on retry', async () => {
@@ -458,6 +471,15 @@ describe('[component] FileDisplay', () => {
458
471
  refetch: vi.fn(),
459
472
  });
460
473
 
474
+ // Mock useFileUrl to return null URL so fallback is shown
475
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
476
+ useFileUrl.mockReturnValue({
477
+ url: null,
478
+ isLoading: false,
479
+ error: null,
480
+ clear: vi.fn(),
481
+ });
482
+
461
483
  const fallbackGenerator = vi.fn(() => 'DOC');
462
484
 
463
485
  renderWithUnifiedAuth(
@@ -519,4 +541,436 @@ describe('[component] FileDisplay', () => {
519
541
  expect(image).toBeInTheDocument();
520
542
  expect(screen.queryByRole('button', { name: /Delete file/i })).toBeNull();
521
543
  });
544
+
545
+ describe('Document link rendering in displayOnly mode', () => {
546
+ it('renders non-image files as clickable links when displayOnly is true and fileUrl exists', async () => {
547
+ const pdfFile = {
548
+ id: 'fr-pdf',
549
+ table_name: 'person',
550
+ record_id: 'rec-1',
551
+ organisation_id: 'org-1',
552
+ file_path: 'bucket/path/document.pdf',
553
+ is_public: false,
554
+ file_metadata: { fileName: 'document.pdf', fileSize: 2048, fileType: 'application/pdf' },
555
+ };
556
+
557
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
558
+ useFileDisplay.mockReturnValue({
559
+ isLoading: false,
560
+ error: null,
561
+ fileUrl: 'https://signed.example.com/document.pdf',
562
+ fileReference: pdfFile,
563
+ fileReferences: [pdfFile],
564
+ fileCount: 1,
565
+ fileUrls: new Map([['fr-pdf', 'https://signed.example.com/document.pdf']]),
566
+ refetch: vi.fn(),
567
+ });
568
+
569
+ // Mock useFileUrl to return URL from map (since it's in the map, hook won't be used, but mock it anyway)
570
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
571
+ useFileUrl.mockReturnValue({
572
+ url: null,
573
+ isLoading: false,
574
+ error: null,
575
+ clear: vi.fn(),
576
+ });
577
+
578
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
579
+
580
+ const link = await screen.findByRole('link', { name: /Open document\.pdf in new tab/i });
581
+ expect(link).toBeInTheDocument();
582
+ expect(link).toHaveAttribute('href', 'https://signed.example.com/document.pdf');
583
+ });
584
+
585
+ it('renders link with target="_blank" for new tab', async () => {
586
+ const docFile = {
587
+ id: 'fr-doc',
588
+ table_name: 'person',
589
+ record_id: 'rec-1',
590
+ organisation_id: 'org-1',
591
+ file_path: 'bucket/path/file.docx',
592
+ is_public: false,
593
+ file_metadata: { fileName: 'file.docx', fileSize: 4096, fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
594
+ };
595
+
596
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
597
+ useFileDisplay.mockReturnValue({
598
+ isLoading: false,
599
+ error: null,
600
+ fileUrl: 'https://signed.example.com/file.docx',
601
+ fileReference: docFile,
602
+ fileReferences: [docFile],
603
+ fileCount: 1,
604
+ fileUrls: new Map([['fr-doc', 'https://signed.example.com/file.docx']]),
605
+ refetch: vi.fn(),
606
+ });
607
+
608
+ // Mock useFileUrl (won't be used since URL is in map, but mock it anyway)
609
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
610
+ useFileUrl.mockReturnValue({
611
+ url: null,
612
+ isLoading: false,
613
+ error: null,
614
+ clear: vi.fn(),
615
+ });
616
+
617
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
618
+
619
+ const link = await screen.findByRole('link');
620
+ expect(link).toHaveAttribute('target', '_blank');
621
+ });
622
+
623
+ it('renders link with rel="noopener noreferrer" for security', async () => {
624
+ const excelFile = {
625
+ id: 'fr-xls',
626
+ table_name: 'person',
627
+ record_id: 'rec-1',
628
+ organisation_id: 'org-1',
629
+ file_path: 'bucket/path/spreadsheet.xlsx',
630
+ is_public: false,
631
+ file_metadata: { fileName: 'spreadsheet.xlsx', fileSize: 8192, fileType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
632
+ };
633
+
634
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
635
+ useFileDisplay.mockReturnValue({
636
+ isLoading: false,
637
+ error: null,
638
+ fileUrl: 'https://signed.example.com/spreadsheet.xlsx',
639
+ fileReference: excelFile,
640
+ fileReferences: [excelFile],
641
+ fileCount: 1,
642
+ fileUrls: new Map([['fr-xls', 'https://signed.example.com/spreadsheet.xlsx']]),
643
+ refetch: vi.fn(),
644
+ });
645
+
646
+ // Mock useFileUrl (won't be used since URL is in map, but mock it anyway)
647
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
648
+ useFileUrl.mockReturnValue({
649
+ url: null,
650
+ isLoading: false,
651
+ error: null,
652
+ clear: vi.fn(),
653
+ });
654
+
655
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
656
+
657
+ const link = await screen.findByRole('link');
658
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
659
+ });
660
+
661
+ it('displays correct filename in link', async () => {
662
+ const pdfFile = {
663
+ id: 'fr-pdf',
664
+ table_name: 'person',
665
+ record_id: 'rec-1',
666
+ organisation_id: 'org-1',
667
+ file_path: 'bucket/path/my-document.pdf',
668
+ is_public: false,
669
+ file_metadata: { fileName: 'my-document.pdf', fileSize: 2048, fileType: 'application/pdf' },
670
+ };
671
+
672
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
673
+ useFileDisplay.mockReturnValue({
674
+ isLoading: false,
675
+ error: null,
676
+ fileUrl: 'https://signed.example.com/my-document.pdf',
677
+ fileReference: pdfFile,
678
+ fileReferences: [pdfFile],
679
+ fileCount: 1,
680
+ fileUrls: new Map([['fr-pdf', 'https://signed.example.com/my-document.pdf']]),
681
+ refetch: vi.fn(),
682
+ });
683
+
684
+ // Mock useFileUrl (won't be used since URL is in map, but mock it anyway)
685
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
686
+ useFileUrl.mockReturnValue({
687
+ url: null,
688
+ isLoading: false,
689
+ error: null,
690
+ clear: vi.fn(),
691
+ });
692
+
693
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
694
+
695
+ const link = await screen.findByRole('link');
696
+ expect(link).toHaveTextContent('my-document.pdf');
697
+ });
698
+
699
+ it('renders FileText and ExternalLink icons', async () => {
700
+ const pdfFile = {
701
+ id: 'fr-pdf',
702
+ table_name: 'person',
703
+ record_id: 'rec-1',
704
+ organisation_id: 'org-1',
705
+ file_path: 'bucket/path/doc.pdf',
706
+ is_public: false,
707
+ file_metadata: { fileName: 'doc.pdf', fileSize: 2048, fileType: 'application/pdf' },
708
+ };
709
+
710
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
711
+ useFileDisplay.mockReturnValue({
712
+ isLoading: false,
713
+ error: null,
714
+ fileUrl: 'https://signed.example.com/doc.pdf',
715
+ fileReference: pdfFile,
716
+ fileReferences: [pdfFile],
717
+ fileCount: 1,
718
+ fileUrls: new Map([['fr-pdf', 'https://signed.example.com/doc.pdf']]),
719
+ refetch: vi.fn(),
720
+ });
721
+
722
+ // Mock useFileUrl (won't be used since URL is in map, but mock it anyway)
723
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
724
+ useFileUrl.mockReturnValue({
725
+ url: null,
726
+ isLoading: false,
727
+ error: null,
728
+ clear: vi.fn(),
729
+ });
730
+
731
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
732
+
733
+ const link = await screen.findByRole('link');
734
+ // Icons should be present (they have aria-hidden="true")
735
+ // Icons are mocked as divs with data-testid in tests
736
+ const fileTextIcon = link.querySelector('[data-testid="lucide-filetext"]');
737
+ const externalLinkIcon = link.querySelector('[data-testid="lucide-externallink"]');
738
+ expect(fileTextIcon).toBeInTheDocument();
739
+ expect(externalLinkIcon).toBeInTheDocument();
740
+ });
741
+
742
+ it('falls back to existing behavior when fileUrl is null', async () => {
743
+ const pdfFile = {
744
+ id: 'fr-pdf',
745
+ table_name: 'person',
746
+ record_id: 'rec-1',
747
+ organisation_id: 'org-1',
748
+ file_path: 'bucket/path/doc.pdf',
749
+ is_public: false,
750
+ file_metadata: { fileName: 'doc.pdf', fileSize: 2048, fileType: 'application/pdf' },
751
+ };
752
+
753
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
754
+ useFileDisplay.mockReturnValue({
755
+ isLoading: false,
756
+ error: null,
757
+ fileUrl: null,
758
+ fileReference: pdfFile,
759
+ fileReferences: [pdfFile],
760
+ fileCount: 1,
761
+ fileUrls: new Map(),
762
+ refetch: vi.fn(),
763
+ });
764
+
765
+ // Mock useFileUrl to return null URL so fallback is shown
766
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
767
+ useFileUrl.mockReturnValue({
768
+ url: null,
769
+ isLoading: false,
770
+ error: null,
771
+ clear: vi.fn(),
772
+ });
773
+
774
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly showFallback fallbackText="View Document" />);
775
+
776
+ // Should show fallback, not a link
777
+ expect(await screen.findByText('View Document')).toBeInTheDocument();
778
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
779
+ });
780
+
781
+ it('still renders images inline (no regression)', async () => {
782
+ const imageFile = {
783
+ id: 'fr-img',
784
+ table_name: 'person',
785
+ record_id: 'rec-1',
786
+ organisation_id: 'org-1',
787
+ file_path: 'bucket/path/image.png',
788
+ is_public: false,
789
+ file_metadata: { fileName: 'image.png', fileSize: 1024, fileType: 'image/png' },
790
+ };
791
+
792
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
793
+ useFileDisplay.mockReturnValue({
794
+ isLoading: false,
795
+ error: null,
796
+ fileUrl: 'https://signed.example.com/image.png',
797
+ fileReference: imageFile,
798
+ fileReferences: [imageFile],
799
+ fileCount: 1,
800
+ fileUrls: new Map([['fr-img', 'https://signed.example.com/image.png']]),
801
+ refetch: vi.fn(),
802
+ });
803
+
804
+ // Mock useFileUrl (won't be used since URL is in map, but mock it anyway)
805
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
806
+ useFileUrl.mockReturnValue({
807
+ url: null,
808
+ isLoading: false,
809
+ error: null,
810
+ clear: vi.fn(),
811
+ });
812
+
813
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
814
+
815
+ const image = await screen.findByRole('img', { name: /image\.png/i });
816
+ expect(image).toBeInTheDocument();
817
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
818
+ });
819
+
820
+ it('uses wrapper behavior when showDelete is true', async () => {
821
+ const pdfFile = {
822
+ id: 'fr-pdf',
823
+ table_name: 'person',
824
+ record_id: 'rec-1',
825
+ organisation_id: 'org-1',
826
+ file_path: 'bucket/path/doc.pdf',
827
+ is_public: false,
828
+ file_metadata: { fileName: 'doc.pdf', fileSize: 2048, fileType: 'application/pdf' },
829
+ };
830
+
831
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
832
+ useFileDisplay.mockReturnValue({
833
+ isLoading: false,
834
+ error: null,
835
+ fileUrl: 'https://signed.example.com/doc.pdf',
836
+ fileReference: pdfFile,
837
+ fileReferences: [pdfFile],
838
+ fileCount: 1,
839
+ fileUrls: new Map([['fr-pdf', 'https://signed.example.com/doc.pdf']]),
840
+ refetch: vi.fn(),
841
+ });
842
+
843
+ // Mock useFileUrl (won't be used since URL is in map, but mock it anyway)
844
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
845
+ useFileUrl.mockReturnValue({
846
+ url: null,
847
+ isLoading: false,
848
+ error: null,
849
+ clear: vi.fn(),
850
+ });
851
+
852
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly showDelete />);
853
+
854
+ // Should show wrapper with delete button, not simplified link
855
+ expect(screen.getByText('doc.pdf')).toBeInTheDocument();
856
+ expect(screen.getByRole('button', { name: /Delete file/i })).toBeInTheDocument();
857
+ // Should not be a simplified link (no link with target="_blank" in simplified form)
858
+ const link = screen.queryByRole('link', { name: /Open.*in new tab/i });
859
+ expect(link).not.toBeInTheDocument();
860
+ });
861
+
862
+ it('renders document links in public context', async () => {
863
+ mockUseIsPublicPage.mockReturnValueOnce(true);
864
+
865
+ const pdfFile = {
866
+ id: 'pub-pdf',
867
+ table_name: 'event',
868
+ record_id: 'event-1',
869
+ organisation_id: 'org-1',
870
+ file_path: 'bucket/path/public-doc.pdf',
871
+ is_public: true,
872
+ file_metadata: { fileName: 'public-doc.pdf', fileSize: 2048, fileType: 'application/pdf' },
873
+ };
874
+
875
+ const fileUrlsMap = new Map<string, string>([['pub-pdf', 'https://public.example.com/public-doc.pdf']]);
876
+ const usePublicFileDisplay = (await import('../../hooks/public/usePublicFileDisplay')).usePublicFileDisplay as unknown as vi.Mock;
877
+ usePublicFileDisplay.mockReturnValue({
878
+ isLoading: false,
879
+ error: null,
880
+ fileUrl: 'https://public.example.com/public-doc.pdf',
881
+ fileReference: pdfFile,
882
+ fileReferences: [pdfFile],
883
+ fileCount: 1,
884
+ fileUrls: fileUrlsMap,
885
+ refetch: vi.fn(),
886
+ });
887
+
888
+ renderWithPublicContext(
889
+ <FileDisplay {...baseProps} displayOnly />,
890
+ { supabase }
891
+ );
892
+
893
+ const link = await screen.findByRole('link', { name: /Open public-doc\.pdf in new tab/i });
894
+ expect(link).toBeInTheDocument();
895
+ expect(link).toHaveAttribute('href', 'https://public.example.com/public-doc.pdf');
896
+ expect(link).toHaveAttribute('target', '_blank');
897
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
898
+ });
899
+
900
+ it('includes proper ARIA label for accessibility', async () => {
901
+ const pdfFile = {
902
+ id: 'fr-pdf',
903
+ table_name: 'person',
904
+ record_id: 'rec-1',
905
+ organisation_id: 'org-1',
906
+ file_path: 'bucket/path/accessible.pdf',
907
+ is_public: false,
908
+ file_metadata: { fileName: 'accessible.pdf', fileSize: 2048, fileType: 'application/pdf' },
909
+ };
910
+
911
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
912
+ useFileDisplay.mockReturnValue({
913
+ isLoading: false,
914
+ error: null,
915
+ fileUrl: 'https://signed.example.com/accessible.pdf',
916
+ fileReference: pdfFile,
917
+ fileReferences: [pdfFile],
918
+ fileCount: 1,
919
+ fileUrls: new Map([['fr-pdf', 'https://signed.example.com/accessible.pdf']]),
920
+ refetch: vi.fn(),
921
+ });
922
+
923
+ // Mock useFileUrl (won't be used since URL is in map, but mock it anyway)
924
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
925
+ useFileUrl.mockReturnValue({
926
+ url: null,
927
+ isLoading: false,
928
+ error: null,
929
+ clear: vi.fn(),
930
+ });
931
+
932
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
933
+
934
+ const link = await screen.findByRole('link', { name: /Open accessible\.pdf in new tab/i });
935
+ expect(link).toHaveAttribute('aria-label', 'Open accessible.pdf in new tab');
936
+ });
937
+
938
+ it('uses fallback text "Document" when fileName is missing', async () => {
939
+ const pdfFile = {
940
+ id: 'fr-pdf',
941
+ table_name: 'person',
942
+ record_id: 'rec-1',
943
+ organisation_id: 'org-1',
944
+ file_path: 'bucket/path/file.pdf',
945
+ is_public: false,
946
+ file_metadata: { fileName: '', fileSize: 2048, fileType: 'application/pdf' },
947
+ };
948
+
949
+ const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
950
+ useFileDisplay.mockReturnValue({
951
+ isLoading: false,
952
+ error: null,
953
+ fileUrl: 'https://signed.example.com/file.pdf',
954
+ fileReference: pdfFile,
955
+ fileReferences: [pdfFile],
956
+ fileCount: 1,
957
+ fileUrls: new Map([['fr-pdf', 'https://signed.example.com/file.pdf']]),
958
+ refetch: vi.fn(),
959
+ });
960
+
961
+ // Mock useFileUrl (won't be used since URL is in map, but mock it anyway)
962
+ const useFileUrl = (await import('../../hooks/useFileUrl')).useFileUrl as unknown as vi.Mock;
963
+ useFileUrl.mockReturnValue({
964
+ url: null,
965
+ isLoading: false,
966
+ error: null,
967
+ clear: vi.fn(),
968
+ });
969
+
970
+ renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
971
+
972
+ const link = await screen.findByRole('link', { name: /Open Document in new tab/i });
973
+ expect(link).toHaveTextContent('Document');
974
+ });
975
+ });
522
976
  });
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect, useCallback, useRef, useContext, useMemo } from 'react';
2
+ import { FileText, ExternalLink } from 'lucide-react';
2
3
  import { FileReference, FileCategory } from '../../types/file-reference';
3
4
  import { usePublicFileDisplay } from '../../hooks/public/usePublicFileDisplay';
4
5
  import { useFileDisplay } from '../../hooks/useFileDisplay';
@@ -312,6 +313,29 @@ function FileDisplayContent({
312
313
  );
313
314
  }
314
315
 
316
+ // Document link display when displayOnly is true and file is not an image
317
+ // Render non-image files as clickable links that open in a new tab
318
+ if (displayOnly && !isImage && fileUrl && fileReference && !showDelete) {
319
+ const fileName = fileReference.file_metadata?.fileName || 'Document';
320
+ const ariaLabel = `Open ${fileName} in new tab`;
321
+
322
+ return (
323
+ <a
324
+ href={fileUrl}
325
+ target="_blank"
326
+ rel="noopener noreferrer"
327
+ aria-label={ariaLabel}
328
+ className={`flex items-center gap-2 p-3 bg-sec-50 border border-sec-200 rounded-lg hover:bg-sec-100 transition-colors text-main-600 hover:text-main-700 focus:outline-none focus:ring-2 focus:ring-main-500 focus:ring-offset-2 ${className || ''}`.trim()}
329
+ >
330
+ <FileText className="h-5 w-5 shrink-0" aria-hidden="true" />
331
+ <span className="flex-1 font-medium truncate">
332
+ {fileName}
333
+ </span>
334
+ <ExternalLink className="h-5 w-5 shrink-0" aria-hidden="true" />
335
+ </a>
336
+ );
337
+ }
338
+
315
339
  // Standard single file display with wrapper
316
340
  // For displayOnly mode, if fallback is enabled and there's no URL or image error, show fallback instead of folder icon
317
341
  if (displayOnly && showFallback && (!fileUrl || imageError || !isImage)) {
@@ -763,7 +787,10 @@ function FileDisplayAuthenticated({
763
787
  * - UnifiedAuthProvider context for authenticated pages
764
788
  *
765
789
  * @param props - File display configuration
766
- * @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.
790
+ * @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:
791
+ * - **Image files**: Renders a simplified image-only display without metadata or wrapper divs
792
+ * - **Non-image files** (PDFs, Word docs, Excel files, etc.): Renders as clickable links that open in a new tab with document icon, filename, and external link icon. Links include proper security attributes (`rel="noopener noreferrer"`) and accessibility features (ARIA labels, keyboard navigation, visible focus states)
793
+ * - If `showDelete={true}`, uses standard wrapper behavior instead of simplified display
767
794
  * @param props.category - Optional category filter. When specified, only displays files matching this category and uses single file display variant.
768
795
  * @returns React element with file display
769
796
  */
@@ -71,6 +71,8 @@ describe('[component] FileUpload', () => {
71
71
  organisation_id: 'org-1',
72
72
  app_id: 'app-123',
73
73
  category: FileCategory.PROFILE_PHOTOS,
74
+ folder: 'profile_photos',
75
+ pageContext: 'test',
74
76
  accept: 'image/*',
75
77
  } as const;
76
78
 
@@ -22,7 +22,9 @@ export interface FileUploadProps {
22
22
  organisation_id: string;
23
23
  app_id?: string; // Optional - will be resolved from app name if not provided
24
24
  category: FileCategory;
25
+ folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
25
26
  pageContext: string; // The page context where the file upload occurs (e.g., 'configuration', 'forms', 'applications')
27
+ event_id?: string; // Optional event ID for event-scoped permission checks (required for event-based apps)
26
28
  accept?: string;
27
29
  maxSize?: number;
28
30
  multiple?: boolean;
@@ -51,7 +53,9 @@ export function FileUpload({
51
53
  organisation_id,
52
54
  app_id,
53
55
  category,
56
+ folder,
54
57
  pageContext,
58
+ event_id,
55
59
  accept = '*/*',
56
60
  maxSize = 10 * 1024 * 1024, // 10MB default
57
61
  multiple = false,
@@ -286,7 +290,9 @@ export function FileUpload({
286
290
  organisation_id,
287
291
  app_id: resolvedAppId ? assertAppId(resolvedAppId) : assertAppId(''),
288
292
  category,
293
+ folder,
289
294
  pageContext,
295
+ event_id,
290
296
  is_public: isPublic
291
297
  }, file);
292
298
 
@@ -384,7 +390,7 @@ export function FileUpload({
384
390
  onUploadError?.(errorMessage, file);
385
391
  }
386
392
  }
387
- }, [uploadFile, table_name, record_id, organisation_id, resolvedAppId, category, isPublic, maxSize, onUploadSuccess, onUploadError, onProgress, validateFile, generatePreview, showPreview, appIdError]);
393
+ }, [uploadFile, table_name, record_id, organisation_id, resolvedAppId, category, folder, isPublic, maxSize, onUploadSuccess, onUploadError, onProgress, validateFile, generatePreview, showPreview, appIdError]);
388
394
 
389
395
  const handleDragOver = useCallback((e: React.DragEvent) => {
390
396
  e.preventDefault();
@@ -260,7 +260,7 @@ export function Header({
260
260
  "w-full border-b border-main-200 h-16 shadow-sm bg-main-100 ",
261
261
  className
262
262
  )} role="banner">
263
- <nav className="px-4 w-[min(var(--app-width),100%)] mx-auto grid grid-cols-[auto_auto_1fr_auto] gap-4 h-full items-center">
263
+ <nav className="px-4 w-[min(var(--app-width),100%)] mx-auto flex items-center gap-4 h-full">
264
264
  {/* Logo */}
265
265
  {logo ? (
266
266
  logoHref ? (
@@ -318,7 +318,7 @@ export function Header({
318
318
 
319
319
 
320
320
  {/* Right side: Organisation Selector, Event Selector, Actions, and User Menu */}
321
- <div className="flex items-center gap-4 justify-end">
321
+ <div className="flex items-center gap-4 ml-auto">
322
322
  {/* Organisation Selector */}
323
323
  {showOrgSelector ? (
324
324
  <OrganisationSelector
@@ -337,10 +337,7 @@ export function Header({
337
337
  data-testid="event-selector"
338
338
  />
339
339
  ) : null}
340
- </div>
341
340
 
342
- {/* Custom Actions and User Menu */}
343
- <div className="flex items-center gap-4">
344
341
  {/* Custom Actions */}
345
342
  {actions}
346
343