@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.
- package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
- package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
- package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
- package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
- package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
- package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
- package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
- package/dist/chunk-3IC5WCMO.js.map +1 -0
- package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
- package/dist/chunk-3NFNJOO7.js.map +1 -0
- package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
- package/dist/chunk-63FOKYGO.js.map +1 -0
- package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
- package/dist/chunk-C4OYJOV4.js.map +1 -0
- package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
- package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
- package/dist/chunk-HEHYGYOX.js.map +1 -0
- package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
- package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
- package/dist/chunk-LBBUPSSC.js.map +1 -0
- package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/chunk-SAUPYVLF.js.map +1 -0
- package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
- package/dist/chunk-T6ZJVI3A.js.map +1 -0
- package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
- package/dist/chunk-ULX5FYEM.js.map +1 -0
- package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
- package/dist/chunk-WK2Y6TGA.js.map +1 -0
- package/dist/chunk-YHCN776L.js +447 -0
- package/dist/chunk-YHCN776L.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +12 -10
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
- package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
- package/dist/hooks.d.ts +265 -6
- package/dist/hooks.js +148 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +25 -10
- package/dist/index.js +65 -30
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +125 -8
- package/dist/rbac/index.js +27 -7
- package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
- package/dist/utils.d.ts +214 -4
- package/dist/utils.js +22 -2
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +21 -17
- package/docs/api/classes/RBACCache.md +31 -23
- package/docs/api/classes/RBACEngine.md +6 -6
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +241 -0
- package/docs/api/interfaces/AddressFieldRef.md +94 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +75 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +15 -15
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +33 -9
- package/docs/api/interfaces/FileUploadProps.md +36 -14
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +120 -0
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +27 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +328 -69
- package/docs/api-reference/components.md +26 -12
- package/docs/best-practices/performance.md +11 -0
- package/docs/implementation-guides/file-reference-system.md +24 -2
- package/docs/implementation-guides/file-upload-storage.md +38 -1
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/api-reference.md +11 -0
- package/docs/rbac/performance.md +320 -0
- package/docs/standards/01-architecture-standard.md +5 -0
- package/docs/standards/05-security-standard.md +12 -0
- package/package.json +1 -1
- package/scripts/check-pace-core-compliance.js +512 -0
- package/src/components/AddressField/AddressField.test.tsx +411 -0
- package/src/components/AddressField/AddressField.tsx +323 -0
- package/src/components/AddressField/README.md +336 -0
- package/src/components/AddressField/index.ts +10 -0
- package/src/components/AddressField/types.ts +65 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
- package/src/components/FileDisplay/FileDisplay.tsx +28 -1
- package/src/components/FileUpload/FileUpload.test.tsx +2 -0
- package/src/components/FileUpload/FileUpload.tsx +7 -1
- package/src/components/Header/Header.tsx +2 -5
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
- package/src/components/index.ts +2 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
- package/src/hooks/index.ts +9 -0
- package/src/hooks/public/usePublicFileDisplay.ts +8 -10
- package/src/hooks/useAddressAutocomplete.test.ts +318 -0
- package/src/hooks/useAddressAutocomplete.ts +268 -0
- package/src/hooks/useFileDisplay.ts +3 -15
- package/src/hooks/useFileReference.test.ts +21 -3
- package/src/hooks/useFileReference.ts +3 -24
- package/src/hooks/useFileUrlCache.ts +246 -0
- package/src/hooks/useInactivityTracker.ts +31 -20
- package/src/hooks/useOrganisationSecurity.test.ts +10 -7
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/usePreventTabReload.ts +106 -0
- package/src/hooks/useQueryCache.ts +315 -0
- package/src/hooks/useSecureDataAccess.ts +2 -2
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/api.test.ts +21 -6
- package/src/rbac/api.ts +32 -11
- package/src/rbac/audit-batched.ts +223 -0
- package/src/rbac/audit-enhanced.ts +2 -2
- package/src/rbac/audit.test.ts +6 -5
- package/src/rbac/audit.ts +34 -6
- package/src/rbac/cache-invalidation.ts +63 -12
- package/src/rbac/cache.test.ts +2 -2
- package/src/rbac/cache.ts +61 -14
- package/src/rbac/components/PagePermissionGuard.tsx +19 -10
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
- package/src/rbac/config.ts +9 -0
- package/src/rbac/engine.ts +2 -21
- package/src/rbac/hooks/usePermissions.ts +21 -5
- package/src/rbac/index.ts +19 -0
- package/src/rbac/performance.ts +210 -0
- package/src/rbac/request-deduplication.ts +87 -0
- package/src/rbac/utils/deep-equal.ts +93 -0
- package/src/styles/core.css +5 -5
- package/src/types/database.generated.ts +63 -9
- package/src/types/file-reference.ts +3 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
- package/src/utils/file-reference/index.ts +56 -17
- package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
- package/src/utils/google-places/googlePlacesUtils.ts +475 -0
- package/src/utils/google-places/index.ts +26 -0
- package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
- package/src/utils/google-places/types.ts +94 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/request-deduplication.ts +165 -0
- package/src/utils/security/secureDataAccess.ts +1 -1
- package/src/utils/storage/helpers.ts +211 -4
- package/dist/chunk-445GEP27.js.map +0 -1
- package/dist/chunk-AISXLWGZ.js.map +0 -1
- package/dist/chunk-FMUCXFII.js +0 -76
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-FSFQFJCU.js.map +0 -1
- package/dist/chunk-FXFJRTKI.js.map +0 -1
- package/dist/chunk-HC67NW5K.js.map +0 -1
- package/dist/chunk-IXSNYUCT.js.map +0 -1
- package/dist/chunk-MX3EIJGQ.js.map +0 -1
- package/dist/chunk-OKI34GZD.js.map +0 -1
- package/dist/chunk-STTZQK2I.js.map +0 -1
- package/dist/chunk-U6WNSFX5.js.map +0 -1
- /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
- /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
- /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
- /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
- /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
|
|
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
|
*/
|
|
@@ -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
|
|
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
|
|
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
|
|