@jmruthers/pace-core 0.5.190 → 0.5.191
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-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
- package/dist/{DataTable-ON3IXISJ.js → DataTable-WKRZD47S.js} +6 -6
- package/dist/{PublicPageProvider-C4uxosp6.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +1 -1
- package/dist/{UnifiedAuthProvider-X5NXANVI.js → UnifiedAuthProvider-FTSG5XH7.js} +3 -3
- package/dist/{api-I6UCQ5S6.js → api-IHKALJZD.js} +2 -2
- package/dist/{chunk-J2XXC7R5.js → chunk-6LTQQAT6.js} +77 -111
- package/dist/chunk-6LTQQAT6.js.map +1 -0
- package/dist/{chunk-STYK4OH2.js → chunk-6TQDD426.js} +10 -10
- package/dist/chunk-6TQDD426.js.map +1 -0
- package/dist/{chunk-DZWK57KZ.js → chunk-G37KK66H.js} +1 -1
- package/dist/{chunk-DZWK57KZ.js.map → chunk-G37KK66H.js.map} +1 -1
- package/dist/{chunk-73HSNNOQ.js → chunk-LOMZXPSN.js} +13 -13
- package/dist/{chunk-Y4BUBBHD.js → chunk-OETXORNB.js} +3 -3
- package/dist/{chunk-RUYZKXOD.js → chunk-ROXMHMY2.js} +5 -3
- package/dist/chunk-ROXMHMY2.js.map +1 -0
- package/dist/{chunk-SDMHPX3X.js → chunk-ULHIJK66.js} +56 -21
- package/dist/{chunk-SDMHPX3X.js.map → chunk-ULHIJK66.js.map} +1 -1
- package/dist/{chunk-VVBAW5A5.js → chunk-VKB2CO4Z.js} +46 -35
- package/dist/chunk-VKB2CO4Z.js.map +1 -0
- package/dist/{chunk-HQVPB5MZ.js → chunk-VRGWKHDB.js} +6 -6
- package/dist/{chunk-NIU6J6OX.js → chunk-XNYQOL3Z.js} +16 -16
- package/dist/chunk-XNYQOL3Z.js.map +1 -0
- package/dist/{chunk-4QYC5L4K.js → chunk-XYXSXPUK.js} +22 -27
- package/dist/chunk-XYXSXPUK.js.map +1 -0
- package/dist/components.d.ts +3 -3
- package/dist/components.js +8 -8
- package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
- package/dist/hooks.d.ts +12 -12
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +18 -23
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +6 -6
- package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/{usePublicRouteParams-DxIDS4bC.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +1 -1
- package/dist/utils.d.ts +8 -8
- package/dist/utils.js +2 -2
- 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 +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- 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 +1 -1
- package/docs/api/interfaces/AddressFieldRef.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +2 -2
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- 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 +2 -2
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
- 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 +16 -16
- package/docs/migration/README.md +18 -0
- package/docs/migration/database-changes-december-2025.md +767 -0
- package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
- package/package.json +1 -1
- package/src/__tests__/public-recipe-view.test.ts +10 -10
- package/src/__tests__/rls-policies.test.ts +13 -13
- package/src/components/AddressField/README.md +6 -6
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +35 -15
- package/src/components/Select/Select.test.tsx +4 -1
- package/src/components/Select/Select.tsx +60 -15
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +192 -0
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +741 -0
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +703 -0
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +581 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +9 -8
- package/src/hooks/public/usePublicEvent.ts +8 -8
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/useFileDisplay.ts +8 -9
- package/src/hooks/useQueryCache.ts +6 -6
- package/src/hooks/useSecureDataAccess.test.ts +8 -8
- package/src/hooks/useSecureDataAccess.ts +15 -11
- package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
- package/src/rbac/hooks/useRBAC.simple.test.ts +95 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +2 -2
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +490 -0
- package/src/rbac/utils/eventContext.ts +5 -2
- package/src/services/AuthService.ts +37 -8
- package/src/services/OrganisationService.ts +92 -139
- package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
- package/src/services/__tests__/OrganisationService.test.ts +218 -86
- package/src/types/database.generated.ts +166 -201
- package/src/types/supabase.ts +2 -2
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
- package/src/utils/file-reference/index.ts +4 -4
- package/src/utils/google-places/googlePlacesUtils.ts +1 -1
- package/src/utils/google-places/types.ts +1 -1
- package/src/utils/request-deduplication.ts +4 -4
- package/src/utils/security/secureDataAccess.test.ts +1 -1
- package/src/utils/security/secureDataAccess.ts +7 -4
- package/src/utils/storage/README.md +1 -1
- package/dist/chunk-4QYC5L4K.js.map +0 -1
- package/dist/chunk-J2XXC7R5.js.map +0 -1
- package/dist/chunk-NIU6J6OX.js.map +0 -1
- package/dist/chunk-RUYZKXOD.js.map +0 -1
- package/dist/chunk-STYK4OH2.js.map +0 -1
- package/dist/chunk-VVBAW5A5.js.map +0 -1
- /package/dist/{DataTable-ON3IXISJ.js.map → DataTable-WKRZD47S.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-X5NXANVI.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
- /package/dist/{api-I6UCQ5S6.js.map → api-IHKALJZD.js.map} +0 -0
- /package/dist/{chunk-73HSNNOQ.js.map → chunk-LOMZXPSN.js.map} +0 -0
- /package/dist/{chunk-Y4BUBBHD.js.map → chunk-OETXORNB.js.map} +0 -0
- /package/dist/{chunk-HQVPB5MZ.js.map → chunk-VRGWKHDB.js.map} +0 -0
|
@@ -154,7 +154,7 @@ if (address) {
|
|
|
154
154
|
|
|
155
155
|
## ParsedAddress Type
|
|
156
156
|
|
|
157
|
-
The `onChange` callback receives a `ParsedAddress` object matching the `
|
|
157
|
+
The `onChange` callback receives a `ParsedAddress` object matching the `core_address` table structure:
|
|
158
158
|
|
|
159
159
|
```typescript
|
|
160
160
|
interface ParsedAddress {
|
|
@@ -202,7 +202,7 @@ The component uses intelligent caching to reduce API costs:
|
|
|
202
202
|
|
|
203
203
|
The `place_id` is a stable identifier for a location that doesn't change. It's crucial for:
|
|
204
204
|
|
|
205
|
-
1. **Storing in Database**: Save `place_id` in your `
|
|
205
|
+
1. **Storing in Database**: Save `place_id` in your `core_address` table
|
|
206
206
|
2. **Retrieving Later**: Use `getAddressByPlaceId()` to get full address without autocomplete
|
|
207
207
|
3. **Verification**: Verify addresses haven't changed
|
|
208
208
|
4. **Cost Efficiency**: Place details lookups are cheaper than autocomplete searches
|
|
@@ -215,7 +215,7 @@ The `place_id` is a stable identifier for a location that doesn't change. It's c
|
|
|
215
215
|
apiKey={apiKey}
|
|
216
216
|
onChange={async (address) => {
|
|
217
217
|
// Store in database
|
|
218
|
-
await supabase.from('
|
|
218
|
+
await supabase.from('core_address').insert({
|
|
219
219
|
place_id: address.place_id, // Store this!
|
|
220
220
|
full_address: address.full_address,
|
|
221
221
|
lat: address.lat,
|
|
@@ -257,16 +257,16 @@ The component handles various error scenarios:
|
|
|
257
257
|
- **Rate Limiting**: Informs user about quota limits
|
|
258
258
|
- **Invalid Requests**: Validates input and displays errors
|
|
259
259
|
|
|
260
|
-
## Integration with
|
|
260
|
+
## Integration with core_address Table
|
|
261
261
|
|
|
262
|
-
The `ParsedAddress` type matches the `
|
|
262
|
+
The `ParsedAddress` type matches the `core_address` table structure, making it easy to store results:
|
|
263
263
|
|
|
264
264
|
```tsx
|
|
265
265
|
<AddressField
|
|
266
266
|
apiKey={apiKey}
|
|
267
267
|
onChange={async (address) => {
|
|
268
268
|
const { data, error } = await supabase
|
|
269
|
-
.from('
|
|
269
|
+
.from('core_address')
|
|
270
270
|
.insert({
|
|
271
271
|
place_id: address.place_id,
|
|
272
272
|
full_address: address.full_address,
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
* - Secure organisation data handling
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
-
import React, { useState, useCallback } from 'react';
|
|
56
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
57
57
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../Select';
|
|
58
58
|
import { Alert, AlertDescription } from '../Alert/Alert';
|
|
59
59
|
import { Button } from '../Button/Button';
|
|
@@ -118,6 +118,9 @@ export function OrganisationSelector({
|
|
|
118
118
|
refreshOrganisations
|
|
119
119
|
} = useOrganisations();
|
|
120
120
|
|
|
121
|
+
// Removed debug logging useEffect - it was causing render loops because organisations array
|
|
122
|
+
// is recreated on every render, triggering the effect constantly
|
|
123
|
+
|
|
121
124
|
|
|
122
125
|
const handleOrganisationChange = useCallback(async (orgId: string) => {
|
|
123
126
|
if (disabled || isLoading) return;
|
|
@@ -242,23 +245,37 @@ export function OrganisationSelector({
|
|
|
242
245
|
</Alert>
|
|
243
246
|
);
|
|
244
247
|
|
|
245
|
-
// Normal selector state -
|
|
248
|
+
// Normal selector state - allow opening even if no organisation is selected
|
|
249
|
+
const isSelectDisabled = disabled || isLoading;
|
|
250
|
+
|
|
251
|
+
// Memoize the value to prevent render loops
|
|
252
|
+
const selectValue = useMemo(() => {
|
|
253
|
+
return selectedOrganisation?.id || '';
|
|
254
|
+
}, [selectedOrganisation?.id]);
|
|
255
|
+
|
|
246
256
|
return (
|
|
247
|
-
<div className={
|
|
257
|
+
<div className={className}>
|
|
248
258
|
<Select
|
|
249
|
-
value={
|
|
259
|
+
value={selectValue}
|
|
250
260
|
onValueChange={handleOrganisationChange}
|
|
251
|
-
disabled={
|
|
261
|
+
disabled={isSelectDisabled}
|
|
252
262
|
>
|
|
253
|
-
<SelectTrigger
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
<SelectTrigger
|
|
264
|
+
className="text-left"
|
|
265
|
+
variant="outline"
|
|
266
|
+
>
|
|
267
|
+
<SelectValue placeholder={placeholder}>
|
|
268
|
+
{selectedOrganisation && (
|
|
269
|
+
<div className="flex items-center gap-2">
|
|
270
|
+
{isLoading ? (
|
|
271
|
+
<LoadingSpinner size="sm" />
|
|
272
|
+
) : (
|
|
273
|
+
<Building2 className="size-4 flex-shrink-0" />
|
|
274
|
+
)}
|
|
275
|
+
<span className="truncate">{selectedOrganisation.display_name}</span>
|
|
276
|
+
</div>
|
|
259
277
|
)}
|
|
260
|
-
|
|
261
|
-
</div>
|
|
278
|
+
</SelectValue>
|
|
262
279
|
</SelectTrigger>
|
|
263
280
|
<SelectContent>
|
|
264
281
|
{organisations.map((org) => {
|
|
@@ -298,8 +315,11 @@ export function OrganisationSelector({
|
|
|
298
315
|
})}
|
|
299
316
|
</SelectContent>
|
|
300
317
|
</Select>
|
|
301
|
-
|
|
302
|
-
|
|
318
|
+
{switchErrorDisplay && (
|
|
319
|
+
<div className="mt-2">
|
|
320
|
+
{switchErrorDisplay}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
303
323
|
</div>
|
|
304
324
|
);
|
|
305
325
|
}
|
|
@@ -465,11 +465,14 @@ describe('Select Component', () => {
|
|
|
465
465
|
await user.click(screen.getByRole('combobox'));
|
|
466
466
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
467
467
|
|
|
468
|
+
// Wait for the event listener to be added (100ms delay in implementation)
|
|
469
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
470
|
+
|
|
468
471
|
await user.click(document.body);
|
|
469
472
|
|
|
470
473
|
await waitFor(() => {
|
|
471
474
|
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
472
|
-
});
|
|
475
|
+
}, { timeout: 2000 });
|
|
473
476
|
});
|
|
474
477
|
|
|
475
478
|
it('prevents opening when trigger is disabled', async () => {
|
|
@@ -145,7 +145,9 @@ export const useSelectState = ({
|
|
|
145
145
|
}, [controlledValue, onValueChange, controlledOpen, onOpenChange]);
|
|
146
146
|
|
|
147
147
|
const setOpen = React.useCallback((newOpen: boolean) => {
|
|
148
|
-
if (disabled)
|
|
148
|
+
if (disabled) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
149
151
|
|
|
150
152
|
// Close all other select dropdowns when opening this one
|
|
151
153
|
if (newOpen) {
|
|
@@ -210,24 +212,30 @@ export const useSelectEvents = ({ state, actions, selectRef }: UseSelectEventsPr
|
|
|
210
212
|
|
|
211
213
|
// Handle click outside to close dropdown
|
|
212
214
|
React.useEffect(() => {
|
|
215
|
+
if (!state.open) return;
|
|
216
|
+
|
|
213
217
|
const handleClickOutside = (event: MouseEvent) => {
|
|
214
218
|
const selectElement = selectRef.current;
|
|
215
|
-
|
|
216
|
-
const isSelectItem = clickedElement?.closest('[data-testid="select-item"]');
|
|
217
|
-
const isSearchInput = clickedElement?.closest('[data-testid="select-search-input"]');
|
|
218
|
-
const isSelectContent = clickedElement?.closest('[data-testid="select-content"]');
|
|
219
|
+
if (!selectElement) return;
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
const target = event.target as Node;
|
|
222
|
+
|
|
223
|
+
// Close if clicking outside the select element
|
|
224
|
+
if (!selectElement.contains(target) && !isSelecting) {
|
|
221
225
|
actions.setOpen(false);
|
|
222
226
|
}
|
|
223
227
|
};
|
|
224
228
|
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
// Add a small delay to avoid closing immediately when opening
|
|
230
|
+
// The delay ensures the click event that opened the dropdown has fully processed
|
|
231
|
+
const timeoutId = setTimeout(() => {
|
|
232
|
+
document.addEventListener('click', handleClickOutside, true); // Use capture phase
|
|
233
|
+
}, 100); // Small delay to let the opening click complete
|
|
234
|
+
|
|
227
235
|
return () => {
|
|
228
|
-
|
|
236
|
+
clearTimeout(timeoutId);
|
|
237
|
+
document.removeEventListener('click', handleClickOutside, true);
|
|
229
238
|
};
|
|
230
|
-
}
|
|
231
239
|
}, [state.open, actions, selectRef, isSelecting]);
|
|
232
240
|
|
|
233
241
|
// Handle SelectItem mousedown events
|
|
@@ -588,6 +596,10 @@ export const Select = React.forwardRef<HTMLFormElement, SelectProps & UseSelectS
|
|
|
588
596
|
className={cn("relative", className)}
|
|
589
597
|
data-value={state.value}
|
|
590
598
|
data-testid="select-root"
|
|
599
|
+
onSubmit={(e) => {
|
|
600
|
+
e.preventDefault();
|
|
601
|
+
e.stopPropagation();
|
|
602
|
+
}}
|
|
591
603
|
>
|
|
592
604
|
<SelectContext.Provider value={contextValue}>
|
|
593
605
|
{children}
|
|
@@ -607,9 +619,25 @@ export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerPr
|
|
|
607
619
|
const { open, disabled, value, actions, direction = 'down' } = useSelectContext();
|
|
608
620
|
const opensUpward = direction === 'up';
|
|
609
621
|
|
|
610
|
-
|
|
622
|
+
// Use ref to store the latest handleClick to avoid re-creating the effect
|
|
623
|
+
const handleClickRef = React.useRef<(e: React.MouseEvent) => void>();
|
|
624
|
+
|
|
625
|
+
const handleClick = React.useCallback((e: React.MouseEvent) => {
|
|
626
|
+
if (disabled) {
|
|
627
|
+
e.preventDefault();
|
|
628
|
+
e.stopPropagation();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
e.preventDefault();
|
|
633
|
+
e.stopPropagation();
|
|
611
634
|
actions.setOpen(!open);
|
|
612
|
-
};
|
|
635
|
+
}, [disabled, open, actions]);
|
|
636
|
+
|
|
637
|
+
// Update ref whenever handleClick changes
|
|
638
|
+
React.useEffect(() => {
|
|
639
|
+
handleClickRef.current = handleClick;
|
|
640
|
+
}, [handleClick]);
|
|
613
641
|
|
|
614
642
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
615
643
|
if (disabled) return;
|
|
@@ -690,9 +718,19 @@ export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerPr
|
|
|
690
718
|
});
|
|
691
719
|
}
|
|
692
720
|
|
|
721
|
+
|
|
722
|
+
// Simple ref forwarding
|
|
723
|
+
const handleRef = React.useCallback((node: HTMLButtonElement | null) => {
|
|
724
|
+
if (typeof ref === 'function') {
|
|
725
|
+
ref(node);
|
|
726
|
+
} else if (ref) {
|
|
727
|
+
(ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;
|
|
728
|
+
}
|
|
729
|
+
}, [ref]);
|
|
730
|
+
|
|
693
731
|
return (
|
|
694
732
|
<Button
|
|
695
|
-
ref={
|
|
733
|
+
ref={handleRef}
|
|
696
734
|
type="button"
|
|
697
735
|
role="combobox"
|
|
698
736
|
aria-expanded={open}
|
|
@@ -713,7 +751,9 @@ export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerPr
|
|
|
713
751
|
textOverflow: 'ellipsis',
|
|
714
752
|
whiteSpace: 'nowrap'
|
|
715
753
|
}}
|
|
716
|
-
onClick={
|
|
754
|
+
onClick={(e) => {
|
|
755
|
+
handleClick(e);
|
|
756
|
+
}}
|
|
717
757
|
onKeyDown={handleKeyDown}
|
|
718
758
|
data-testid="select-trigger"
|
|
719
759
|
data-value={value}
|
|
@@ -741,7 +781,12 @@ export const SelectValue = React.forwardRef<HTMLSpanElement, SelectValueProps>(
|
|
|
741
781
|
const { selectedText } = useSelectContext();
|
|
742
782
|
|
|
743
783
|
return (
|
|
744
|
-
<span
|
|
784
|
+
<span
|
|
785
|
+
ref={ref}
|
|
786
|
+
data-testid="select-value"
|
|
787
|
+
style={{ pointerEvents: 'none' }}
|
|
788
|
+
className="pointer-events-none"
|
|
789
|
+
>
|
|
745
790
|
{children || (selectedText ? selectedText : placeholder)}
|
|
746
791
|
</span>
|
|
747
792
|
);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { TestWrapper } from '../../__tests__/helpers/test-utils';
|
|
5
|
+
import { useRBAC } from '../../rbac/hooks/useRBAC';
|
|
6
|
+
|
|
7
|
+
// Mock the useRBAC hook
|
|
8
|
+
vi.mock('../../rbac/hooks/useRBAC');
|
|
9
|
+
|
|
10
|
+
// Mock isPermittedCached from RBAC API (used by usePermissionCache)
|
|
11
|
+
vi.mock('../../rbac/api', async () => {
|
|
12
|
+
const actual = await vi.importActual('../../rbac/api');
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
isPermittedCached: vi.fn().mockResolvedValue(true),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Mock logger
|
|
20
|
+
vi.mock('../../utils/core/logger', () => {
|
|
21
|
+
const mockLoggerInstance = {
|
|
22
|
+
debug: vi.fn(),
|
|
23
|
+
info: vi.fn(),
|
|
24
|
+
warn: vi.fn(),
|
|
25
|
+
error: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
createLogger: vi.fn(() => mockLoggerInstance),
|
|
29
|
+
logger: mockLoggerInstance,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Mock useOrganisations hook (required by usePermissionCache)
|
|
34
|
+
const mockOrganisationContext = {
|
|
35
|
+
selectedOrganisation: {
|
|
36
|
+
id: 'test-org-id',
|
|
37
|
+
name: 'Test Organisation',
|
|
38
|
+
display_name: 'Test Organisation',
|
|
39
|
+
slug: 'test-org',
|
|
40
|
+
description: 'Test organisation',
|
|
41
|
+
subscription_tier: 'basic',
|
|
42
|
+
settings: {},
|
|
43
|
+
is_active: true,
|
|
44
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
45
|
+
updated_at: '2023-01-01T00:00:00Z',
|
|
46
|
+
},
|
|
47
|
+
organisations: [],
|
|
48
|
+
userMemberships: [],
|
|
49
|
+
isLoading: false,
|
|
50
|
+
error: null,
|
|
51
|
+
hasValidOrganisationContext: true,
|
|
52
|
+
setSelectedOrganisation: vi.fn(),
|
|
53
|
+
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
54
|
+
getUserRole: vi.fn().mockReturnValue('member'),
|
|
55
|
+
validateOrganisationAccess: vi.fn().mockReturnValue(true),
|
|
56
|
+
ensureOrganisationContext: vi.fn().mockReturnValue(null),
|
|
57
|
+
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
58
|
+
getPrimaryOrganisation: vi.fn().mockReturnValue(null),
|
|
59
|
+
isOrganisationSecure: vi.fn().mockReturnValue(true),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
vi.mock('../useOrganisations', () => ({
|
|
63
|
+
useOrganisations: () => mockOrganisationContext,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Mock useUnifiedAuth (required by usePermissionCache)
|
|
67
|
+
const mockUseUnifiedAuthFn = vi.fn(() => ({
|
|
68
|
+
user: { id: 'test-user-id' },
|
|
69
|
+
session: null,
|
|
70
|
+
appName: 'test-app',
|
|
71
|
+
selectedOrganisation: { id: 'test-org-id' },
|
|
72
|
+
selectedEvent: null,
|
|
73
|
+
supabase: {},
|
|
74
|
+
isLoading: false,
|
|
75
|
+
error: null,
|
|
76
|
+
}));
|
|
77
|
+
vi.mock('../../providers/services/UnifiedAuthProvider', () => ({
|
|
78
|
+
useUnifiedAuth: () => mockUseUnifiedAuthFn(),
|
|
79
|
+
UnifiedAuthProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
// Mock useEvents hook (optional - wrapped in try/catch in usePermissionCache)
|
|
83
|
+
vi.mock('../useEvents', () => ({
|
|
84
|
+
useEvents: vi.fn(() => ({
|
|
85
|
+
selectedEvent: { event_id: 'event-123' },
|
|
86
|
+
events: [],
|
|
87
|
+
isLoading: false,
|
|
88
|
+
error: null,
|
|
89
|
+
})),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
const mockUseRBAC = vi.mocked(useRBAC);
|
|
93
|
+
|
|
94
|
+
// Import after mocking
|
|
95
|
+
import { usePermissionCache } from '../usePermissionCache';
|
|
96
|
+
import { isPermittedCached } from '../../rbac/api';
|
|
97
|
+
const mockIsPermittedCached = vi.mocked(isPermittedCached);
|
|
98
|
+
|
|
99
|
+
describe('usePermissionCache - Simple Tests', () => {
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
vi.clearAllMocks();
|
|
102
|
+
|
|
103
|
+
// Reset isPermittedCached mock
|
|
104
|
+
mockIsPermittedCached.mockClear();
|
|
105
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
106
|
+
|
|
107
|
+
// Mock the useRBAC hook return value
|
|
108
|
+
mockUseRBAC.mockReturnValue({
|
|
109
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
110
|
+
user: { id: 'test-user-id' }
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
vi.clearAllTimers();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should render without errors', () => {
|
|
119
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
120
|
+
wrapper: TestWrapper
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.current).toHaveProperty('checkPermission');
|
|
124
|
+
expect(result.current).toHaveProperty('checkMultiplePermissions');
|
|
125
|
+
expect(result.current).toHaveProperty('getCachedPermissions');
|
|
126
|
+
expect(result.current).toHaveProperty('invalidateCache');
|
|
127
|
+
expect(result.current).toHaveProperty('getDebugInfo');
|
|
128
|
+
expect(result.current).toHaveProperty('getAuditTrail');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should check permission and return result', async () => {
|
|
132
|
+
mockIsPermittedCached.mockResolvedValueOnce(true);
|
|
133
|
+
mockUseRBAC.mockReturnValue({
|
|
134
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
135
|
+
user: { id: 'test-user-id' }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
139
|
+
wrapper: TestWrapper
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const permission = await result.current.checkPermission('read', 'dashboard');
|
|
143
|
+
|
|
144
|
+
expect(permission).toBe(true);
|
|
145
|
+
expect(mockIsPermittedCached).toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle permission check errors gracefully', async () => {
|
|
149
|
+
const { logger } = await import('../../utils/core/logger');
|
|
150
|
+
mockIsPermittedCached.mockRejectedValueOnce(new Error('Database error'));
|
|
151
|
+
|
|
152
|
+
mockUseRBAC.mockReturnValue({
|
|
153
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
154
|
+
user: { id: 'test-user-id' }
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
158
|
+
wrapper: TestWrapper
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const permission = await result.current.checkPermission('read', 'dashboard');
|
|
162
|
+
|
|
163
|
+
expect(permission).toBe(false);
|
|
164
|
+
// Verify error was logged using logger
|
|
165
|
+
expect(logger.error).toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should provide debug information', () => {
|
|
169
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
170
|
+
wrapper: TestWrapper
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const debugInfo = result.current.getDebugInfo();
|
|
174
|
+
|
|
175
|
+
expect(debugInfo).toHaveProperty('cacheSize');
|
|
176
|
+
expect(debugInfo).toHaveProperty('cacheHits');
|
|
177
|
+
expect(debugInfo).toHaveProperty('cacheMisses');
|
|
178
|
+
expect(debugInfo).toHaveProperty('totalChecks');
|
|
179
|
+
expect(debugInfo).toHaveProperty('averageResponseTime');
|
|
180
|
+
expect(debugInfo).toHaveProperty('lastInvalidation');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should provide audit trail', () => {
|
|
184
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
185
|
+
wrapper: TestWrapper
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const auditTrail = result.current.getAuditTrail();
|
|
189
|
+
|
|
190
|
+
expect(Array.isArray(auditTrail)).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|