@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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Address Autocomplete Hook Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Hooks/__tests__
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
10
|
+
import { useAddressAutocomplete } from './useAddressAutocomplete';
|
|
11
|
+
import * as googlePlacesUtils from '../utils/google-places';
|
|
12
|
+
|
|
13
|
+
// Mock the Google Places utilities
|
|
14
|
+
vi.mock('../utils/google-places', () => ({
|
|
15
|
+
fetchPlaceAutocomplete: vi.fn(),
|
|
16
|
+
fetchPlaceDetails: vi.fn(),
|
|
17
|
+
createAddressFromPlaceResult: vi.fn(),
|
|
18
|
+
getAddressByPlaceId: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock useQueryCache
|
|
22
|
+
vi.mock('./useQueryCache', () => ({
|
|
23
|
+
useQueryCache: () => ({
|
|
24
|
+
getCachedQuery: async <T,>(_table: string, _filterKey: string, _filterValue: string, fetchFn: () => Promise<T>) => {
|
|
25
|
+
return fetchFn();
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// Mock useDebounce to return value immediately for tests
|
|
31
|
+
vi.mock('./useDebounce', () => ({
|
|
32
|
+
useDebounce: (value: any, _delay: number) => value,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
describe('useAddressAutocomplete', () => {
|
|
36
|
+
const mockApiKey = 'test-api-key';
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
vi.useRealTimers();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('Initial state', () => {
|
|
48
|
+
it('returns initial state with empty suggestions', () => {
|
|
49
|
+
const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
|
|
50
|
+
|
|
51
|
+
expect(result.current.suggestions).toEqual([]);
|
|
52
|
+
expect(result.current.isLoading).toBe(false);
|
|
53
|
+
expect(result.current.error).toBeNull();
|
|
54
|
+
}, { timeout: 5000 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Autocomplete suggestions', () => {
|
|
58
|
+
it('fetches suggestions when input value changes', async () => {
|
|
59
|
+
const mockPredictions = [
|
|
60
|
+
{
|
|
61
|
+
description: '123 Main St, Melbourne VIC, Australia',
|
|
62
|
+
place_id: 'ChIJ123',
|
|
63
|
+
structured_formatting: {
|
|
64
|
+
main_text: '123 Main St',
|
|
65
|
+
secondary_text: 'Melbourne VIC, Australia',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
|
|
71
|
+
|
|
72
|
+
const { result } = renderHook(() =>
|
|
73
|
+
useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await waitFor(
|
|
77
|
+
() => {
|
|
78
|
+
expect(result.current.suggestions).toHaveLength(1);
|
|
79
|
+
},
|
|
80
|
+
{ timeout: 1000 }
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(result.current.suggestions[0].place_id).toBe('ChIJ123');
|
|
84
|
+
}, { timeout: 5000 });
|
|
85
|
+
|
|
86
|
+
it('debounces input value', async () => {
|
|
87
|
+
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValue([]);
|
|
88
|
+
|
|
89
|
+
// Since we're mocking useDebounce to return immediately, this test verifies
|
|
90
|
+
// that the hook still works correctly with rapid input changes
|
|
91
|
+
const { result, rerender } = renderHook(
|
|
92
|
+
({ inputValue }) => useAddressAutocomplete(mockApiKey, inputValue, { debounceDelay: 0 }),
|
|
93
|
+
{ initialProps: { inputValue: '123' } }
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Change input quickly - with mocked debounce, each change triggers immediately
|
|
97
|
+
rerender({ inputValue: '123 M' });
|
|
98
|
+
rerender({ inputValue: '123 Ma' });
|
|
99
|
+
rerender({ inputValue: '123 Main' });
|
|
100
|
+
|
|
101
|
+
// Wait for the last call
|
|
102
|
+
await waitFor(
|
|
103
|
+
() => {
|
|
104
|
+
expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
|
|
105
|
+
},
|
|
106
|
+
{ timeout: 1000 }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// With mocked debounce, it will be called for each change
|
|
110
|
+
expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
|
|
111
|
+
}, { timeout: 5000 });
|
|
112
|
+
|
|
113
|
+
it('clears suggestions when input is empty', async () => {
|
|
114
|
+
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce([
|
|
115
|
+
{ description: '123 Main St', place_id: 'ChIJ123' },
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const { result, rerender } = renderHook(
|
|
119
|
+
({ inputValue }) => useAddressAutocomplete(mockApiKey, inputValue, { debounceDelay: 0 }),
|
|
120
|
+
{ initialProps: { inputValue: '123 Main' } }
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await waitFor(
|
|
124
|
+
() => {
|
|
125
|
+
expect(result.current.suggestions).toBeDefined();
|
|
126
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
127
|
+
},
|
|
128
|
+
{ timeout: 1000 }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
rerender({ inputValue: '' });
|
|
132
|
+
|
|
133
|
+
await waitFor(
|
|
134
|
+
() => {
|
|
135
|
+
expect(result.current.suggestions).toBeDefined();
|
|
136
|
+
expect(result.current.suggestions).toEqual([]);
|
|
137
|
+
},
|
|
138
|
+
{ timeout: 1000 }
|
|
139
|
+
);
|
|
140
|
+
}, { timeout: 5000 });
|
|
141
|
+
|
|
142
|
+
it('handles API errors', async () => {
|
|
143
|
+
const error = new Error('API request denied');
|
|
144
|
+
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockRejectedValueOnce(error);
|
|
145
|
+
|
|
146
|
+
const { result } = renderHook(() =>
|
|
147
|
+
useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
await waitFor(
|
|
151
|
+
() => {
|
|
152
|
+
expect(result.current.error).not.toBeNull();
|
|
153
|
+
},
|
|
154
|
+
{ timeout: 1000 }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(result.current.error?.message).toBe('API request denied');
|
|
158
|
+
expect(result.current.suggestions).toEqual([]);
|
|
159
|
+
}, { timeout: 5000 });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('Address selection', () => {
|
|
163
|
+
it('selects address by place_id', async () => {
|
|
164
|
+
const mockPlaceDetails = {
|
|
165
|
+
place_id: 'ChIJ123',
|
|
166
|
+
formatted_address: '123 Main St, Melbourne VIC 3000, Australia',
|
|
167
|
+
address_components: [],
|
|
168
|
+
geometry: { location: { lat: -37.8136, lng: 144.9631 } },
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const mockParsedAddress = {
|
|
172
|
+
place_id: 'ChIJ123',
|
|
173
|
+
full_address: '123 Main St, Melbourne VIC 3000, Australia',
|
|
174
|
+
street_number: null,
|
|
175
|
+
route: null,
|
|
176
|
+
suburb: null,
|
|
177
|
+
state: null,
|
|
178
|
+
postcode: null,
|
|
179
|
+
country: null,
|
|
180
|
+
lat: -37.8136,
|
|
181
|
+
lng: 144.9631,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
vi.mocked(googlePlacesUtils.fetchPlaceDetails).mockResolvedValueOnce(mockPlaceDetails);
|
|
185
|
+
vi.mocked(googlePlacesUtils.createAddressFromPlaceResult).mockReturnValueOnce(mockParsedAddress);
|
|
186
|
+
|
|
187
|
+
const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
|
|
188
|
+
|
|
189
|
+
const address = await result.current.selectAddress('ChIJ123');
|
|
190
|
+
|
|
191
|
+
expect(address).not.toBeNull();
|
|
192
|
+
expect(address?.place_id).toBe('ChIJ123');
|
|
193
|
+
expect(googlePlacesUtils.fetchPlaceDetails).toHaveBeenCalledWith('ChIJ123', mockApiKey);
|
|
194
|
+
}, { timeout: 5000 });
|
|
195
|
+
|
|
196
|
+
it('returns null when place_id is invalid', async () => {
|
|
197
|
+
vi.mocked(googlePlacesUtils.fetchPlaceDetails).mockRejectedValueOnce(new Error('Place not found'));
|
|
198
|
+
|
|
199
|
+
const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
|
|
200
|
+
|
|
201
|
+
const address = await result.current.selectAddress('invalid');
|
|
202
|
+
|
|
203
|
+
expect(address).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('getAddressByPlaceId', () => {
|
|
208
|
+
it('retrieves address by place_id', async () => {
|
|
209
|
+
const mockAddress = {
|
|
210
|
+
place_id: 'ChIJ123',
|
|
211
|
+
full_address: '123 Main St',
|
|
212
|
+
street_number: null,
|
|
213
|
+
route: null,
|
|
214
|
+
suburb: null,
|
|
215
|
+
state: null,
|
|
216
|
+
postcode: null,
|
|
217
|
+
country: null,
|
|
218
|
+
lat: null,
|
|
219
|
+
lng: null,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
vi.mocked(googlePlacesUtils.getAddressByPlaceId).mockResolvedValueOnce(mockAddress);
|
|
223
|
+
|
|
224
|
+
const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
|
|
225
|
+
|
|
226
|
+
const address = await result.current.getAddressByPlaceId('ChIJ123');
|
|
227
|
+
|
|
228
|
+
expect(address).not.toBeNull();
|
|
229
|
+
expect(address?.place_id).toBe('ChIJ123');
|
|
230
|
+
}, { timeout: 5000 });
|
|
231
|
+
|
|
232
|
+
it('returns null on error', async () => {
|
|
233
|
+
vi.mocked(googlePlacesUtils.getAddressByPlaceId).mockResolvedValueOnce(null);
|
|
234
|
+
|
|
235
|
+
const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
|
|
236
|
+
|
|
237
|
+
const address = await result.current.getAddressByPlaceId('invalid');
|
|
238
|
+
|
|
239
|
+
expect(address).toBeNull();
|
|
240
|
+
}, { timeout: 5000 });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('clearSuggestions', () => {
|
|
244
|
+
it('clears suggestions', async () => {
|
|
245
|
+
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce([
|
|
246
|
+
{ description: '123 Main St', place_id: 'ChIJ123' },
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
const { result } = renderHook(() =>
|
|
250
|
+
useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
await waitFor(
|
|
254
|
+
() => {
|
|
255
|
+
expect(result.current.suggestions).toBeDefined();
|
|
256
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
257
|
+
},
|
|
258
|
+
{ timeout: 1000 }
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
result.current.clearSuggestions();
|
|
262
|
+
|
|
263
|
+
// Wait for state update
|
|
264
|
+
await waitFor(
|
|
265
|
+
() => {
|
|
266
|
+
expect(result.current.suggestions).toEqual([]);
|
|
267
|
+
expect(result.current.error).toBeNull();
|
|
268
|
+
},
|
|
269
|
+
{ timeout: 1000 }
|
|
270
|
+
);
|
|
271
|
+
}, { timeout: 5000 });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('Caching', () => {
|
|
275
|
+
it('uses cache when enabled', async () => {
|
|
276
|
+
const mockPredictions = [{ description: '123 Main St', place_id: 'ChIJ123' }];
|
|
277
|
+
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
|
|
278
|
+
|
|
279
|
+
const { result } = renderHook(() =>
|
|
280
|
+
useAddressAutocomplete(mockApiKey, '123 Main', {
|
|
281
|
+
debounceDelay: 0,
|
|
282
|
+
cacheEnabled: true,
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
await waitFor(
|
|
287
|
+
() => {
|
|
288
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
289
|
+
},
|
|
290
|
+
{ timeout: 1000 }
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('skips cache when disabled', async () => {
|
|
297
|
+
const mockPredictions = [{ description: '123 Main St', place_id: 'ChIJ123' }];
|
|
298
|
+
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
|
|
299
|
+
|
|
300
|
+
const { result } = renderHook(() =>
|
|
301
|
+
useAddressAutocomplete(mockApiKey, '123 Main', {
|
|
302
|
+
debounceDelay: 0,
|
|
303
|
+
cacheEnabled: false,
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
await waitFor(
|
|
308
|
+
() => {
|
|
309
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
310
|
+
},
|
|
311
|
+
{ timeout: 1000 }
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
|
|
315
|
+
}, { timeout: 5000 });
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Address Autocomplete Hook
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Hooks/AddressAutocomplete
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Hook for managing Google Places API autocomplete functionality.
|
|
8
|
+
* Provides debounced input, caching, and address selection.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
12
|
+
import { useDebounce } from './useDebounce';
|
|
13
|
+
import { useQueryCache } from './useQueryCache';
|
|
14
|
+
import {
|
|
15
|
+
fetchPlaceAutocomplete,
|
|
16
|
+
fetchPlaceDetails,
|
|
17
|
+
createAddressFromPlaceResult,
|
|
18
|
+
getAddressByPlaceId,
|
|
19
|
+
} from '../utils/google-places';
|
|
20
|
+
import type {
|
|
21
|
+
GooglePlaceAutocompletePrediction,
|
|
22
|
+
ParsedAddress,
|
|
23
|
+
AutocompleteOptions,
|
|
24
|
+
} from '../utils/google-places';
|
|
25
|
+
|
|
26
|
+
export interface UseAddressAutocompleteOptions {
|
|
27
|
+
/** Google Places API options */
|
|
28
|
+
autocompleteOptions?: AutocompleteOptions;
|
|
29
|
+
/** Debounce delay in milliseconds (default: 300) */
|
|
30
|
+
debounceDelay?: number;
|
|
31
|
+
/** Enable caching (default: true) */
|
|
32
|
+
cacheEnabled?: boolean;
|
|
33
|
+
/** Cache TTL configuration */
|
|
34
|
+
cacheTTL?: {
|
|
35
|
+
/** Autocomplete cache TTL in seconds (default: 3600 = 1 hour) */
|
|
36
|
+
autocomplete?: number;
|
|
37
|
+
/** Place details cache TTL in seconds (default: 86400 = 24 hours) */
|
|
38
|
+
placeDetails?: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UseAddressAutocompleteReturn {
|
|
43
|
+
/** Array of autocomplete suggestions */
|
|
44
|
+
suggestions: GooglePlaceAutocompletePrediction[];
|
|
45
|
+
/** Loading state */
|
|
46
|
+
isLoading: boolean;
|
|
47
|
+
/** Error state */
|
|
48
|
+
error: Error | null;
|
|
49
|
+
/** Select an address by place_id */
|
|
50
|
+
selectAddress: (placeId: string) => Promise<ParsedAddress | null>;
|
|
51
|
+
/** Get address by place_id (uses cache) */
|
|
52
|
+
getAddressByPlaceId: (placeId: string) => Promise<ParsedAddress | null>;
|
|
53
|
+
/** Clear suggestions */
|
|
54
|
+
clearSuggestions: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hook for Google Places API autocomplete with caching
|
|
59
|
+
*
|
|
60
|
+
* @param apiKey - Google Places API key
|
|
61
|
+
* @param inputValue - Current input value
|
|
62
|
+
* @param options - Hook configuration options
|
|
63
|
+
* @returns Autocomplete state and functions
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const { suggestions, isLoading, selectAddress } = useAddressAutocomplete(
|
|
68
|
+
* apiKey,
|
|
69
|
+
* inputValue,
|
|
70
|
+
* { debounceDelay: 300, cacheEnabled: true }
|
|
71
|
+
* );
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function useAddressAutocomplete(
|
|
75
|
+
apiKey: string,
|
|
76
|
+
inputValue: string,
|
|
77
|
+
options: UseAddressAutocompleteOptions = {}
|
|
78
|
+
): UseAddressAutocompleteReturn {
|
|
79
|
+
const {
|
|
80
|
+
debounceDelay = 300,
|
|
81
|
+
cacheEnabled = true,
|
|
82
|
+
cacheTTL = {
|
|
83
|
+
autocomplete: 3600, // 1 hour
|
|
84
|
+
placeDetails: 86400, // 24 hours
|
|
85
|
+
},
|
|
86
|
+
autocompleteOptions,
|
|
87
|
+
} = options;
|
|
88
|
+
|
|
89
|
+
const [suggestions, setSuggestions] = useState<GooglePlaceAutocompletePrediction[]>([]);
|
|
90
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
91
|
+
const [error, setError] = useState<Error | null>(null);
|
|
92
|
+
|
|
93
|
+
const debouncedInput = useDebounce(inputValue, debounceDelay);
|
|
94
|
+
const { getCachedQuery } = useQueryCache();
|
|
95
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
96
|
+
|
|
97
|
+
// Memoize autocompleteOptions to prevent unnecessary re-renders
|
|
98
|
+
const memoizedAutocompleteOptions = useMemo(
|
|
99
|
+
() => autocompleteOptions,
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
[JSON.stringify(autocompleteOptions)]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Fetch autocomplete suggestions
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
// Cancel previous request if still in flight
|
|
107
|
+
if (abortControllerRef.current) {
|
|
108
|
+
abortControllerRef.current.abort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reset state if input is empty
|
|
112
|
+
if (!debouncedInput.trim()) {
|
|
113
|
+
setSuggestions([]);
|
|
114
|
+
setIsLoading(false);
|
|
115
|
+
setError(null);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Don't fetch if no API key
|
|
120
|
+
if (!apiKey) {
|
|
121
|
+
setError(new Error('Google Places API key is required'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setIsLoading(true);
|
|
126
|
+
setError(null);
|
|
127
|
+
|
|
128
|
+
const fetchSuggestions = async () => {
|
|
129
|
+
try {
|
|
130
|
+
let predictions: GooglePlaceAutocompletePrediction[];
|
|
131
|
+
|
|
132
|
+
if (cacheEnabled) {
|
|
133
|
+
// Use cached query with TTL
|
|
134
|
+
predictions = await getCachedQuery(
|
|
135
|
+
'google-places-autocomplete',
|
|
136
|
+
'query',
|
|
137
|
+
debouncedInput,
|
|
138
|
+
async () => {
|
|
139
|
+
return fetchPlaceAutocomplete(debouncedInput, apiKey, memoizedAutocompleteOptions);
|
|
140
|
+
},
|
|
141
|
+
{ ttl: cacheTTL.autocomplete, enabled: true }
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
// Direct fetch without cache
|
|
145
|
+
predictions = await fetchPlaceAutocomplete(debouncedInput, apiKey, memoizedAutocompleteOptions);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setSuggestions(predictions);
|
|
149
|
+
setIsLoading(false);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// Ignore aborted requests
|
|
152
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const error = err instanceof Error ? err : new Error('Failed to fetch autocomplete suggestions');
|
|
157
|
+
setError(error);
|
|
158
|
+
setSuggestions([]);
|
|
159
|
+
setIsLoading(false);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
fetchSuggestions();
|
|
164
|
+
|
|
165
|
+
// Cleanup function to cancel in-flight requests
|
|
166
|
+
return () => {
|
|
167
|
+
if (abortControllerRef.current) {
|
|
168
|
+
abortControllerRef.current.abort();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
+
}, [debouncedInput, apiKey, cacheEnabled, cacheTTL.autocomplete]);
|
|
173
|
+
|
|
174
|
+
// Select address by place_id
|
|
175
|
+
const selectAddress = useCallback(
|
|
176
|
+
async (placeId: string): Promise<ParsedAddress | null> => {
|
|
177
|
+
if (!placeId || !apiKey) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setIsLoading(true);
|
|
182
|
+
setError(null);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
let placeDetails;
|
|
186
|
+
|
|
187
|
+
if (cacheEnabled) {
|
|
188
|
+
// Use cached query with TTL
|
|
189
|
+
placeDetails = await getCachedQuery(
|
|
190
|
+
'google-places-details',
|
|
191
|
+
'place_id',
|
|
192
|
+
placeId,
|
|
193
|
+
async () => {
|
|
194
|
+
return fetchPlaceDetails(placeId, apiKey);
|
|
195
|
+
},
|
|
196
|
+
{ ttl: cacheTTL.placeDetails, enabled: true }
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
// Direct fetch without cache
|
|
200
|
+
placeDetails = await fetchPlaceDetails(placeId, apiKey);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const parsedAddress = createAddressFromPlaceResult(placeDetails);
|
|
204
|
+
setIsLoading(false);
|
|
205
|
+
return parsedAddress;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const error = err instanceof Error ? err : new Error('Failed to fetch place details');
|
|
208
|
+
setError(error);
|
|
209
|
+
setIsLoading(false);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
|
+
[apiKey, cacheEnabled, cacheTTL.placeDetails]
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Get address by place_id (utility function)
|
|
218
|
+
const getAddressByPlaceIdFn = useCallback(
|
|
219
|
+
async (placeId: string): Promise<ParsedAddress | null> => {
|
|
220
|
+
if (!placeId || !apiKey) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
if (cacheEnabled) {
|
|
226
|
+
// Use cached query with TTL
|
|
227
|
+
return await getCachedQuery(
|
|
228
|
+
'google-places-details',
|
|
229
|
+
'place_id',
|
|
230
|
+
placeId,
|
|
231
|
+
async () => {
|
|
232
|
+
const result = await getAddressByPlaceId(placeId, apiKey);
|
|
233
|
+
if (!result) {
|
|
234
|
+
throw new Error('Failed to fetch address');
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
},
|
|
238
|
+
{ ttl: cacheTTL.placeDetails, enabled: true }
|
|
239
|
+
);
|
|
240
|
+
} else {
|
|
241
|
+
return await getAddressByPlaceId(placeId, apiKey);
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const error = err instanceof Error ? err : new Error('Failed to get address by place_id');
|
|
245
|
+
setError(error);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
250
|
+
[apiKey, cacheEnabled, cacheTTL.placeDetails]
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Clear suggestions
|
|
254
|
+
const clearSuggestions = useCallback(() => {
|
|
255
|
+
setSuggestions([]);
|
|
256
|
+
setError(null);
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
suggestions,
|
|
261
|
+
isLoading,
|
|
262
|
+
error,
|
|
263
|
+
selectAddress,
|
|
264
|
+
getAddressByPlaceId: getAddressByPlaceIdFn,
|
|
265
|
+
clearSuggestions,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
import { useState, useEffect, useCallback } from 'react';
|
|
38
38
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
39
39
|
import { FileReference, FileCategory } from '../types/file-reference';
|
|
40
|
-
import { getPublicUrl, getSignedUrl } from '../utils/storage/helpers';
|
|
40
|
+
import { getPublicUrl, getSignedUrl, generateFileUrlsBatch } from '../utils/storage/helpers';
|
|
41
41
|
import { createFileReferenceService } from '../utils/file-reference';
|
|
42
42
|
import { logger } from '../utils/core/logger';
|
|
43
43
|
|
|
@@ -279,24 +279,12 @@ export function useFileDisplay(
|
|
|
279
279
|
logger.debug('useFileDisplay', 'Setting file URL:', url ? 'URL set' : 'URL is null');
|
|
280
280
|
setFileUrl(url);
|
|
281
281
|
} else {
|
|
282
|
-
// Multiple files mode - generate URLs for all files
|
|
283
|
-
const urlMap =
|
|
284
|
-
for (const fileRef of files) {
|
|
285
|
-
let url: string | null = null;
|
|
286
|
-
if (fileRef.is_public) {
|
|
287
|
-
url = getPublicUrl(supabase, fileRef.file_path, true);
|
|
288
|
-
} else {
|
|
289
|
-
const signedUrlResult = await getSignedUrl(supabase, fileRef.file_path, {
|
|
282
|
+
// Multiple files mode - generate URLs for all files in batch
|
|
283
|
+
const urlMap = await generateFileUrlsBatch(supabase, files, {
|
|
290
284
|
appName: 'pace-core',
|
|
291
285
|
orgId: organisation_id,
|
|
292
286
|
expiresIn: 3600
|
|
293
287
|
});
|
|
294
|
-
url = signedUrlResult?.url || null;
|
|
295
|
-
}
|
|
296
|
-
if (url) {
|
|
297
|
-
urlMap.set(fileRef.id, url);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
288
|
setFileUrls(urlMap);
|
|
301
289
|
setFileReference(null);
|
|
302
290
|
setFileUrl(null);
|
|
@@ -18,6 +18,7 @@ let serviceMock: any;
|
|
|
18
18
|
const mockUploadFileWithReference = vi.fn();
|
|
19
19
|
const mockGetPublicUrl = vi.fn();
|
|
20
20
|
const mockGetSignedUrl = vi.fn();
|
|
21
|
+
const mockGenerateFileUrlsBatch = vi.fn();
|
|
21
22
|
|
|
22
23
|
// Setup mocks
|
|
23
24
|
beforeEach(() => {
|
|
@@ -47,12 +48,25 @@ beforeEach(() => {
|
|
|
47
48
|
url: 'https://example.com/signed-file.jpg',
|
|
48
49
|
expiresAt: new Date(Date.now() + 3600 * 1000).toISOString()
|
|
49
50
|
});
|
|
51
|
+
// Mock batch URL generation to return URLs for all files
|
|
52
|
+
mockGenerateFileUrlsBatch.mockImplementation(async (supabase, files) => {
|
|
53
|
+
const urlMap = new Map<string, string>();
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
if (file.is_public) {
|
|
56
|
+
urlMap.set(file.id, `https://example.com/${file.file_path}`);
|
|
57
|
+
} else {
|
|
58
|
+
urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return urlMap;
|
|
62
|
+
});
|
|
50
63
|
|
|
51
64
|
// Apply mocks
|
|
52
65
|
vi.spyOn(fileReferenceUtils, 'createFileReferenceService').mockImplementation(mockCreateFileReferenceService as any);
|
|
53
66
|
vi.spyOn(fileReferenceUtils, 'uploadFileWithReference').mockImplementation(mockUploadFileWithReference as any);
|
|
54
67
|
vi.spyOn(storageHelpers, 'getPublicUrl').mockImplementation(mockGetPublicUrl as any);
|
|
55
68
|
vi.spyOn(storageHelpers, 'getSignedUrl').mockImplementation(mockGetSignedUrl as any);
|
|
69
|
+
vi.spyOn(storageHelpers, 'generateFileUrlsBatch').mockImplementation(mockGenerateFileUrlsBatch as any);
|
|
56
70
|
});
|
|
57
71
|
|
|
58
72
|
afterEach(() => {
|
|
@@ -67,6 +81,7 @@ const mockFileUploadOptions = {
|
|
|
67
81
|
organisation_id: 'test-org-123',
|
|
68
82
|
app_id: 'test-app-123',
|
|
69
83
|
category: FileCategory.GENERAL_DOCUMENTS,
|
|
84
|
+
folder: 'documents',
|
|
70
85
|
pageContext: 'configuration',
|
|
71
86
|
is_public: false
|
|
72
87
|
};
|
|
@@ -504,16 +519,19 @@ describe('[hook] useFilesByCategory', () => {
|
|
|
504
519
|
);
|
|
505
520
|
|
|
506
521
|
await waitFor(() => {
|
|
507
|
-
|
|
508
|
-
expect(
|
|
522
|
+
// Verify batch URL generation was called with both files
|
|
523
|
+
expect(mockGenerateFileUrlsBatch).toHaveBeenCalledWith(
|
|
509
524
|
mockSupabase,
|
|
510
|
-
privateFile
|
|
525
|
+
[publicFile, privateFile],
|
|
511
526
|
expect.objectContaining({
|
|
512
527
|
appName: 'file-reference',
|
|
513
528
|
orgId: 'test-org-123',
|
|
514
529
|
expiresIn: 3600
|
|
515
530
|
})
|
|
516
531
|
);
|
|
532
|
+
// Verify URLs were generated
|
|
533
|
+
expect(result.current.fileUrls.get(publicFile.id)).toBeDefined();
|
|
534
|
+
expect(result.current.fileUrls.get(privateFile.id)).toBeDefined();
|
|
517
535
|
});
|
|
518
536
|
});
|
|
519
537
|
});
|