@jmruthers/pace-core 0.6.4 → 0.6.6
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/CHANGELOG.md +104 -0
- package/README.md +5 -403
- package/core-usage-manifest.json +93 -0
- package/cursor-rules/00-pace-core-compliance.mdc +128 -26
- package/cursor-rules/01-standards-compliance.mdc +49 -8
- package/cursor-rules/02-project-structure.mdc +6 -0
- package/cursor-rules/03-solid-principles.mdc +2 -0
- package/cursor-rules/04-testing-standards.mdc +2 -0
- package/cursor-rules/05-bug-reports-and-features.mdc +2 -0
- package/cursor-rules/06-code-quality.mdc +2 -0
- package/cursor-rules/07-tech-stack-compliance.mdc +2 -0
- package/cursor-rules/08-markup-quality.mdc +52 -27
- package/cursor-rules/09-rbac-compliance.mdc +462 -0
- package/cursor-rules/10-error-handling-patterns.mdc +179 -0
- package/cursor-rules/11-performance-optimization.mdc +169 -0
- package/cursor-rules/12-ci-cd-integration.mdc +150 -0
- package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
- package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-2N_tqbfq.d.ts} +1 -1
- package/dist/DataTable-LRJL4IRV.js +15 -0
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-BBH6Vqg7.d.ts} +72 -139
- package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
- package/dist/api-Y4MQWOFW.js +4 -0
- package/dist/audit-MYQXYZFU.js +3 -0
- package/dist/{chunk-J36DSWQK.js → chunk-2HGJFNAH.js} +8 -28
- package/dist/{chunk-OEWDTMG7.js → chunk-3O3WHILE.js} +38 -121
- package/dist/{chunk-M43Y4SSO.js → chunk-3QC3KRHK.js} +1 -14
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
- package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
- package/dist/chunk-4T7OBVTU.js +62 -0
- package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
- package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
- package/dist/{chunk-NN6WWZ5U.js → chunk-7TYHROIV.js} +579 -563
- package/dist/{chunk-M7MPQISP.js → chunk-A55DK444.js} +9 -16
- package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
- package/dist/{chunk-L4OXEN46.js → chunk-BVP2BCJF.js} +2 -16
- package/dist/chunk-C7NSAPTL.js +1 -0
- package/dist/{chunk-YKRAFF5K.js → chunk-FENMYN2U.js} +73 -149
- package/dist/{chunk-AVMLPIM7.js → chunk-FTCRZOG2.js} +284 -432
- package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
- package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
- package/dist/{chunk-I6DAQMWX.js → chunk-LAZMKTTF.js} +930 -891
- package/dist/{chunk-5EC5MEWX.js → chunk-MAGBIDNS.js} +77 -222
- package/dist/chunk-MBADTM7L.js +64 -0
- package/dist/chunk-OHIK3MIO.js +994 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-S7DKJPLT.js} +115 -44
- package/dist/{chunk-FMUCXFII.js → chunk-SD6WQY43.js} +1 -5
- package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
- package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
- package/dist/{chunk-FFQEQTNW.js → chunk-UIYSCEV7.js} +134 -45
- package/dist/{chunk-3LPHPB62.js → chunk-ZFYPMX46.js} +271 -87
- package/dist/{chunk-7JPAB3T5.js → chunk-ZS5VO5JB.js} +1989 -1283
- package/dist/components.d.ts +6 -6
- package/dist/components.js +57 -267
- package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
- package/dist/eslint-rules/index.cjs +22 -0
- package/dist/eslint-rules/rules/compliance.cjs +348 -0
- package/dist/eslint-rules/rules/components.cjs +113 -0
- package/dist/eslint-rules/rules/imports.cjs +102 -0
- package/dist/eslint-rules/rules/rbac.cjs +790 -0
- package/dist/eslint-rules/utils/helpers.cjs +42 -0
- package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +62 -270
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +36 -26
- package/dist/index.js +87 -690
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +8 -35
- package/dist/rbac/eslint-rules.d.ts +46 -44
- package/dist/rbac/eslint-rules.js +7 -4
- package/dist/rbac/index.d.ts +124 -594
- package/dist/rbac/index.js +14 -207
- package/dist/styles/index.js +2 -12
- package/dist/theming/runtime.js +3 -19
- package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
- package/dist/{types-CkbwOr4Y.d.ts → types-B-K_5VnO.d.ts} +4 -0
- package/dist/types-t9H8qKRw.d.ts +55 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +7 -94
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-COZ28Mvq.d.ts} +9 -9
- package/dist/utils.d.ts +24 -117
- package/dist/utils.js +54 -392
- package/docs/README.md +16 -6
- package/docs/api/README.md +4 -402
- package/docs/api/modules.md +454 -930
- package/docs/api-reference/components.md +3 -1
- package/docs/api-reference/deprecated.md +31 -6
- package/docs/api-reference/rpc-functions.md +78 -3
- package/docs/best-practices/accessibility.md +6 -3
- package/docs/getting-started/cursor-rules.md +3 -23
- package/docs/getting-started/dependencies.md +650 -0
- package/docs/getting-started/installation-guide.md +20 -7
- package/docs/getting-started/quick-start.md +23 -12
- package/docs/implementation-guides/permission-enforcement.md +4 -0
- package/docs/rbac/MIGRATION_GUIDE.md +819 -0
- package/docs/rbac/RBAC_CONTRACT.md +724 -0
- package/docs/rbac/README.md +12 -3
- package/docs/rbac/edge-functions-guide.md +376 -0
- package/docs/rbac/secure-client-protection.md +0 -34
- package/docs/standards/00-pace-core-compliance.md +967 -0
- package/docs/standards/01-standards-compliance.md +188 -0
- package/docs/standards/02-project-structure.md +985 -0
- package/docs/standards/03-solid-principles.md +39 -0
- package/docs/standards/04-testing-standards.md +36 -0
- package/docs/standards/05-bug-reports-and-features.md +27 -0
- package/docs/standards/{04-code-style-standard.md → 06-code-quality.md} +2 -0
- package/docs/standards/07-tech-stack-compliance.md +30 -0
- package/docs/standards/08-markup-quality.md +345 -0
- package/docs/standards/{07-rbac-and-rls-standard.md → 09-rbac-compliance.md} +149 -54
- package/docs/standards/10-error-handling-patterns.md +401 -0
- package/docs/standards/11-performance-optimization.md +348 -0
- package/docs/standards/12-ci-cd-integration.md +370 -0
- package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +192 -0
- package/docs/standards/README.md +62 -33
- package/docs/troubleshooting/organisation-context-setup.md +42 -19
- package/eslint-config-pace-core.cjs +20 -4
- package/package.json +31 -21
- package/scripts/audit/audit-compliance.cjs +1295 -0
- package/scripts/audit/audit-components.cjs +260 -0
- package/scripts/audit/audit-dependencies.cjs +395 -0
- package/scripts/audit/audit-rbac.cjs +954 -0
- package/scripts/audit/audit-standards.cjs +1268 -0
- package/scripts/audit/index.cjs +1898 -194
- package/scripts/install-cursor-rules.cjs +259 -8
- package/scripts/validate-master.js +1 -1
- package/src/__tests__/fixtures/supabase.ts +1 -1
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +1 -1
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +3 -3
- package/src/__tests__/helpers/component-test-utils.tsx +1 -1
- package/src/__tests__/helpers/supabaseMock.ts +2 -2
- package/src/__tests__/public-recipe-view.test.ts +38 -9
- package/src/components/Button/Button.tsx +5 -1
- package/src/components/ContextSelector/ContextSelector.tsx +42 -39
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/DataTableBody.tsx +55 -31
- package/src/components/DataTable/components/DataTableCore.tsx +186 -13
- package/src/components/DataTable/components/DataTableLayout.tsx +30 -5
- package/src/components/DataTable/components/EditFields.tsx +23 -3
- package/src/components/DataTable/components/EditableRow.tsx +7 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/RowComponent.tsx +12 -0
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
- package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
- package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
- package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
- package/src/components/DataTable/types.ts +5 -0
- package/src/components/DateTimeField/DateTimeField.tsx +20 -20
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +361 -318
- package/src/components/Dialog/Dialog.tsx +1154 -323
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +45 -2
- package/src/components/FileDisplay/FileDisplay.tsx +28 -22
- package/src/components/Form/Form.test.tsx +9 -10
- package/src/components/Form/Form.tsx +369 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +30 -41
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/Select/Select.tsx +23 -21
- package/src/components/Select/types.ts +1 -1
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +39 -34
- package/src/components/index.ts +3 -4
- package/src/eslint-rules/index.cjs +22 -0
- package/src/eslint-rules/rules/compliance.cjs +348 -0
- package/src/eslint-rules/rules/components.cjs +113 -0
- package/src/eslint-rules/rules/imports.cjs +102 -0
- package/src/eslint-rules/rules/rbac.cjs +790 -0
- package/src/eslint-rules/utils/helpers.cjs +42 -0
- package/src/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/src/hooks/__tests__/hooks.integration.test.tsx +6 -8
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
- package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
- package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
- package/src/hooks/public/usePublicEvent.ts +62 -190
- package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
- package/src/hooks/public/usePublicEventLogo.ts +19 -9
- package/src/hooks/useAppConfig.ts +26 -24
- package/src/hooks/useEventTheme.test.ts +211 -233
- package/src/hooks/useEventTheme.ts +19 -28
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +9 -11
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/hooks/useQueryCache.ts +0 -1
- package/src/hooks/useSessionDraft.ts +380 -0
- package/src/hooks/useSessionRestoration.ts +3 -1
- package/src/icons/index.ts +27 -0
- package/src/index.ts +16 -1
- package/src/providers/OrganisationProvider.tsx +23 -14
- package/src/providers/services/EventServiceProvider.tsx +1 -24
- package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -0
- package/src/rbac/README.md +20 -20
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
- package/src/rbac/adapters.tsx +7 -295
- package/src/rbac/api.test.ts +44 -56
- package/src/rbac/api.ts +10 -17
- package/src/rbac/cache-invalidation.ts +0 -1
- package/src/rbac/compliance/index.ts +10 -0
- package/src/rbac/compliance/pattern-detector.ts +553 -0
- package/src/rbac/compliance/runtime-compliance.ts +22 -0
- package/src/rbac/components/AccessDenied.tsx +150 -0
- package/src/rbac/components/NavigationGuard.tsx +12 -20
- package/src/rbac/components/PagePermissionGuard.tsx +4 -24
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
- package/src/rbac/components/index.ts +3 -41
- package/src/rbac/eslint-rules.js +1 -1
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/permissions/index.ts +0 -3
- package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
- package/src/rbac/hooks/usePermissions.ts +0 -3
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
- package/src/rbac/hooks/useResolvedScope.ts +58 -140
- package/src/rbac/hooks/useResourcePermissions.test.ts +241 -60
- package/src/rbac/hooks/useResourcePermissions.ts +182 -63
- package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
- package/src/rbac/hooks/useRoleManagement.ts +147 -19
- package/src/rbac/hooks/useSecureSupabase.ts +4 -8
- package/src/rbac/index.ts +7 -9
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +45 -7
- package/src/services/AuthService.ts +132 -23
- package/src/services/EventService.ts +4 -97
- package/src/services/InactivityService.ts +155 -58
- package/src/services/OrganisationService.ts +7 -44
- package/src/services/__tests__/OrganisationService.test.ts +26 -8
- package/src/services/base/BaseService.ts +0 -3
- package/src/styles/core.css +4 -0
- package/src/types/database.generated.ts +4733 -3809
- package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
- package/src/utils/context/organisationContext.test.ts +13 -28
- package/src/utils/context/organisationContext.ts +21 -52
- package/src/utils/dynamic/dynamicUtils.ts +1 -1
- package/src/utils/file-reference/index.ts +39 -15
- package/src/utils/formatting/formatDateTime.test.ts +3 -2
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
- package/src/utils/index.ts +4 -1
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
- package/src/utils/persistence/keyDerivation.ts +304 -0
- package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
- package/src/utils/security/secureStorage.ts +5 -5
- package/src/utils/storage/helpers.ts +3 -3
- package/src/utils/supabase/createBaseClient.ts +147 -0
- package/src/utils/timezone/timezone.test.ts +1 -2
- package/src/utils/timezone/timezone.ts +1 -1
- package/src/utils/validation/csrf.ts +4 -4
- package/cursor-rules/CHANGELOG.md +0 -119
- package/cursor-rules/README.md +0 -192
- package/dist/DataTable-E7YQZD7D.js +0 -175
- package/dist/DataTable-E7YQZD7D.js.map +0 -1
- package/dist/UnifiedAuthProvider-QPXO24B4.js +0 -18
- package/dist/UnifiedAuthProvider-QPXO24B4.js.map +0 -1
- package/dist/api-6LVZTHDS.js +0 -52
- package/dist/api-6LVZTHDS.js.map +0 -1
- package/dist/audit-V53FV5AG.js +0 -17
- package/dist/audit-V53FV5AG.js.map +0 -1
- package/dist/chunk-36LVWXB2.js +0 -227
- package/dist/chunk-36LVWXB2.js.map +0 -1
- package/dist/chunk-3LPHPB62.js.map +0 -1
- package/dist/chunk-5DRSZLL2.js.map +0 -1
- package/dist/chunk-5EC5MEWX.js.map +0 -1
- package/dist/chunk-63FOKYGO.js.map +0 -1
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-7JPAB3T5.js.map +0 -1
- package/dist/chunk-ATKZM7RX.js +0 -2053
- package/dist/chunk-ATKZM7RX.js.map +0 -1
- package/dist/chunk-AVMLPIM7.js.map +0 -1
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-E66EQZE6.js.map +0 -1
- package/dist/chunk-FFQEQTNW.js.map +0 -1
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-G37KK66H.js.map +0 -1
- package/dist/chunk-I6DAQMWX.js.map +0 -1
- package/dist/chunk-J36DSWQK.js.map +0 -1
- package/dist/chunk-KQCRWDSA.js +0 -1
- package/dist/chunk-KQCRWDSA.js.map +0 -1
- package/dist/chunk-L4OXEN46.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-M43Y4SSO.js.map +0 -1
- package/dist/chunk-M7MPQISP.js.map +0 -1
- package/dist/chunk-NN6WWZ5U.js.map +0 -1
- package/dist/chunk-OEWDTMG7.js.map +0 -1
- package/dist/chunk-PWLANIRT.js.map +0 -1
- package/dist/chunk-QXHPKYJV.js.map +0 -1
- package/dist/chunk-VBXEHIUJ.js.map +0 -1
- package/dist/chunk-YKRAFF5K.js.map +0 -1
- package/dist/chunk-ZSAAAMVR.js.map +0 -1
- package/dist/components.js.map +0 -1
- package/dist/contextValidator-OOPCLPZW.js +0 -9
- package/dist/contextValidator-OOPCLPZW.js.map +0 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
- package/dist/hooks.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers.js.map +0 -1
- package/dist/rbac/eslint-rules.js.map +0 -1
- package/dist/rbac/index.js.map +0 -1
- package/dist/styles/index.js.map +0 -1
- package/dist/theming/runtime.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/docs/standards/01-architecture-standard.md +0 -44
- package/docs/standards/02-api-and-rpc-standard.md +0 -39
- package/docs/standards/03-component-standard.md +0 -32
- package/docs/standards/05-security-standard.md +0 -44
- package/docs/standards/06-testing-and-docs-standard.md +0 -29
- package/docs/standards/pace-core-compliance.md +0 -432
- package/scripts/audit/core/checks/accessibility.cjs +0 -197
- package/scripts/audit/core/checks/api-usage.cjs +0 -191
- package/scripts/audit/core/checks/bundle.cjs +0 -142
- package/scripts/audit/core/checks/compliance.cjs +0 -2706
- package/scripts/audit/core/checks/config.cjs +0 -54
- package/scripts/audit/core/checks/coverage.cjs +0 -84
- package/scripts/audit/core/checks/dependencies.cjs +0 -994
- package/scripts/audit/core/checks/documentation.cjs +0 -268
- package/scripts/audit/core/checks/environment.cjs +0 -116
- package/scripts/audit/core/checks/error-handling.cjs +0 -340
- package/scripts/audit/core/checks/forms.cjs +0 -172
- package/scripts/audit/core/checks/heuristics.cjs +0 -68
- package/scripts/audit/core/checks/hooks.cjs +0 -334
- package/scripts/audit/core/checks/imports.cjs +0 -244
- package/scripts/audit/core/checks/performance.cjs +0 -325
- package/scripts/audit/core/checks/routes.cjs +0 -117
- package/scripts/audit/core/checks/state.cjs +0 -130
- package/scripts/audit/core/checks/structure.cjs +0 -65
- package/scripts/audit/core/checks/style.cjs +0 -584
- package/scripts/audit/core/checks/testing.cjs +0 -122
- package/scripts/audit/core/checks/typescript.cjs +0 -61
- package/scripts/audit/core/scanner.cjs +0 -199
- package/scripts/audit/core/utils.cjs +0 -137
- package/scripts/audit/reporters/console.cjs +0 -151
- package/scripts/audit/reporters/json.cjs +0 -54
- package/scripts/audit/reporters/markdown.cjs +0 -124
- package/scripts/audit-consuming-app.cjs +0 -86
- package/src/eslint-rules/pace-core-compliance.cjs +0 -510
- package/src/eslint-rules/pace-core-compliance.js +0 -638
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
- package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
- package/src/rbac/components/NavigationProvider.test.tsx +0 -481
- package/src/rbac/components/NavigationProvider.tsx +0 -345
- package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
- package/src/rbac/components/PagePermissionProvider.tsx +0 -279
- package/src/rbac/components/PermissionEnforcer.tsx +0 -312
- package/src/rbac/components/RoleBasedRouter.tsx +0 -440
- package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
- package/src/rbac/components/SecureDataProvider.tsx +0 -339
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
- package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
- package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
|
@@ -3,10 +3,10 @@ export type {
|
|
|
3
3
|
DialogProps,
|
|
4
4
|
DialogTriggerProps,
|
|
5
5
|
DialogContentProps,
|
|
6
|
-
|
|
6
|
+
DialogPortalProps,
|
|
7
|
+
DialogCloseProps,
|
|
7
8
|
DialogHeaderProps,
|
|
8
9
|
DialogFooterProps,
|
|
9
10
|
DialogBodyProps,
|
|
10
|
-
|
|
11
|
-
DialogDescriptionProps
|
|
11
|
+
DialogSize
|
|
12
12
|
} from './Dialog';
|
|
@@ -84,6 +84,17 @@ const baseProps = {
|
|
|
84
84
|
describe('[component] FileDisplay', () => {
|
|
85
85
|
beforeEach(async () => {
|
|
86
86
|
vi.clearAllMocks();
|
|
87
|
+
|
|
88
|
+
// Mock showModal for dialog elements (needed for test environments)
|
|
89
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function(this: HTMLDialogElement) {
|
|
90
|
+
this.setAttribute('open', '');
|
|
91
|
+
this.dispatchEvent(new Event('show', { bubbles: true }));
|
|
92
|
+
});
|
|
93
|
+
HTMLDialogElement.prototype.close = vi.fn(function(this: HTMLDialogElement) {
|
|
94
|
+
this.removeAttribute('open');
|
|
95
|
+
this.dispatchEvent(new Event('close', { bubbles: true }));
|
|
96
|
+
});
|
|
97
|
+
|
|
87
98
|
// Set up default mock for useFileDisplay
|
|
88
99
|
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
89
100
|
useFileDisplay.mockReturnValue({
|
|
@@ -195,9 +206,25 @@ describe('[component] FileDisplay', () => {
|
|
|
195
206
|
const deleteBtn = screen.getByRole('button', { name: /Delete file/i });
|
|
196
207
|
await userEvent.click(deleteBtn);
|
|
197
208
|
|
|
209
|
+
// Wait for dialog to open and be accessible
|
|
210
|
+
// In test environments (jsdom), dialog.open may not be set even when dialog is rendered
|
|
211
|
+
await waitFor(async () => {
|
|
212
|
+
try {
|
|
213
|
+
const dialog = screen.getByRole('dialog');
|
|
214
|
+
expect(dialog).toBeInTheDocument();
|
|
215
|
+
} catch (e) {
|
|
216
|
+
// Fallback for test environments - just check that dialog exists in DOM
|
|
217
|
+
const dialog = document.querySelector('dialog[role="dialog"]');
|
|
218
|
+
if (!dialog) {
|
|
219
|
+
throw new Error('Dialog not found in DOM');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}, { timeout: 3000 });
|
|
223
|
+
|
|
198
224
|
// Dialog should open
|
|
199
225
|
expect(await screen.findByText('Confirm Delete')).toBeInTheDocument();
|
|
200
|
-
|
|
226
|
+
// Button has aria-label="Delete file", so accessible name is "Delete file"
|
|
227
|
+
const confirmDeleteBtn = await screen.findByRole('button', { name: 'Delete file' }, { timeout: 2000 });
|
|
201
228
|
await userEvent.click(confirmDeleteBtn);
|
|
202
229
|
|
|
203
230
|
confirmSpy.mockRestore();
|
|
@@ -376,9 +403,25 @@ describe('[component] FileDisplay', () => {
|
|
|
376
403
|
const deleteBtn = screen.getByRole('button', { name: /Delete file/i });
|
|
377
404
|
await userEvent.click(deleteBtn);
|
|
378
405
|
|
|
406
|
+
// Wait for dialog to open and be accessible
|
|
407
|
+
// In test environments (jsdom), dialog.open may not be set even when dialog is rendered
|
|
408
|
+
await waitFor(async () => {
|
|
409
|
+
try {
|
|
410
|
+
const dialog = screen.getByRole('dialog');
|
|
411
|
+
expect(dialog).toBeInTheDocument();
|
|
412
|
+
} catch (e) {
|
|
413
|
+
// Fallback for test environments - just check that dialog exists in DOM
|
|
414
|
+
const dialog = document.querySelector('dialog[role="dialog"]');
|
|
415
|
+
if (!dialog) {
|
|
416
|
+
throw new Error('Dialog not found in DOM');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}, { timeout: 3000 });
|
|
420
|
+
|
|
379
421
|
// Dialog should open
|
|
380
422
|
expect(await screen.findByText('Confirm Delete')).toBeInTheDocument();
|
|
381
|
-
|
|
423
|
+
// Button has aria-label="Delete file", so accessible name is "Delete file"
|
|
424
|
+
const confirmDeleteBtn = await screen.findByRole('button', { name: 'Delete file' }, { timeout: 2000 });
|
|
382
425
|
await userEvent.click(confirmDeleteBtn);
|
|
383
426
|
});
|
|
384
427
|
|
|
@@ -6,7 +6,7 @@ import { useFileDisplay } from '../../hooks/useFileDisplay';
|
|
|
6
6
|
import { useFileUrl } from '../../hooks/useFileUrl';
|
|
7
7
|
import { PublicPageContext, useIsPublicPage } from '../PublicLayout/PublicPageProvider';
|
|
8
8
|
import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
9
|
-
import { Dialog, DialogContent, DialogHeader,
|
|
9
|
+
import { Dialog, DialogContent, DialogHeader, DialogBody, DialogFooter } from '../Dialog/Dialog';
|
|
10
10
|
import { Button } from '../Button/Button';
|
|
11
11
|
import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner';
|
|
12
12
|
import { logger } from '../../utils/core/logger';
|
|
@@ -448,9 +448,9 @@ const FileDisplayContent = React.memo(function FileDisplayContent({
|
|
|
448
448
|
×
|
|
449
449
|
</Button>
|
|
450
450
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
451
|
-
<DialogContent size="sm">
|
|
451
|
+
<DialogContent size="sm" title="Confirm Delete">
|
|
452
452
|
<DialogHeader>
|
|
453
|
-
<
|
|
453
|
+
<h2>Confirm Delete</h2>
|
|
454
454
|
</DialogHeader>
|
|
455
455
|
<DialogBody>
|
|
456
456
|
<p>Are you sure you want to delete this file? This action cannot be undone.</p>
|
|
@@ -520,9 +520,9 @@ const FileDisplayContent = React.memo(function FileDisplayContent({
|
|
|
520
520
|
×
|
|
521
521
|
</Button>
|
|
522
522
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
523
|
-
<DialogContent size="sm">
|
|
523
|
+
<DialogContent size="sm" title="Confirm Delete">
|
|
524
524
|
<DialogHeader>
|
|
525
|
-
<
|
|
525
|
+
<h2>Confirm Delete</h2>
|
|
526
526
|
</DialogHeader>
|
|
527
527
|
<DialogBody>
|
|
528
528
|
<p>Are you sure you want to delete this file? This action cannot be undone.</p>
|
|
@@ -654,9 +654,32 @@ function FileDisplayPublic({
|
|
|
654
654
|
enableChildren,
|
|
655
655
|
showMetadata
|
|
656
656
|
}: FileDisplayProps) {
|
|
657
|
+
// Call all hooks unconditionally at the top level
|
|
658
|
+
// Hooks must be called in the same order on every render
|
|
657
659
|
const publicPageContext = useContext(PublicPageContext);
|
|
658
660
|
const supabase = publicPageContext?.supabase ?? null;
|
|
659
661
|
|
|
662
|
+
// Call hook unconditionally - if supabase is null, the hook will handle it
|
|
663
|
+
// Use a dummy supabase client if null to satisfy type requirements
|
|
664
|
+
// The hook should handle null gracefully, but TypeScript requires a valid type
|
|
665
|
+
const {
|
|
666
|
+
fileUrl,
|
|
667
|
+
fileReference,
|
|
668
|
+
fileReferences,
|
|
669
|
+
fileUrls,
|
|
670
|
+
fileCount,
|
|
671
|
+
isLoading,
|
|
672
|
+
error,
|
|
673
|
+
refetch
|
|
674
|
+
} = usePublicFileDisplay(
|
|
675
|
+
table_name,
|
|
676
|
+
record_id,
|
|
677
|
+
organisation_id,
|
|
678
|
+
category,
|
|
679
|
+
{ supabase: supabase as any } // Type assertion needed due to type mismatch between contexts
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
// Early return after all hooks have been called
|
|
660
683
|
if (!supabase) {
|
|
661
684
|
// If fallback is enabled, show fallback UI instead of error
|
|
662
685
|
if (showFallback) {
|
|
@@ -700,23 +723,6 @@ function FileDisplayPublic({
|
|
|
700
723
|
);
|
|
701
724
|
}
|
|
702
725
|
|
|
703
|
-
const {
|
|
704
|
-
fileUrl,
|
|
705
|
-
fileReference,
|
|
706
|
-
fileReferences,
|
|
707
|
-
fileUrls,
|
|
708
|
-
fileCount,
|
|
709
|
-
isLoading,
|
|
710
|
-
error,
|
|
711
|
-
refetch
|
|
712
|
-
} = usePublicFileDisplay(
|
|
713
|
-
table_name,
|
|
714
|
-
record_id,
|
|
715
|
-
organisation_id,
|
|
716
|
-
category,
|
|
717
|
-
{ supabase }
|
|
718
|
-
);
|
|
719
|
-
|
|
720
726
|
// Log errors for debugging public file display issues
|
|
721
727
|
if (error) {
|
|
722
728
|
logger.error('FileDisplayPublic', 'Error fetching file', {
|
|
@@ -170,7 +170,7 @@ describe('Form Component', () => {
|
|
|
170
170
|
await user.type(screen.getByLabelText('Name'), 'John Doe');
|
|
171
171
|
await user.click(screen.getByRole('button', { name: 'Submit' }));
|
|
172
172
|
|
|
173
|
-
expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' }
|
|
173
|
+
expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' });
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
it('calls onError when form has validation errors', async () => {
|
|
@@ -194,8 +194,7 @@ describe('Form Component', () => {
|
|
|
194
194
|
name: expect.objectContaining({
|
|
195
195
|
message: 'Required'
|
|
196
196
|
})
|
|
197
|
-
})
|
|
198
|
-
expect.any(Object)
|
|
197
|
+
})
|
|
199
198
|
);
|
|
200
199
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
201
200
|
});
|
|
@@ -213,7 +212,7 @@ describe('Form Component', () => {
|
|
|
213
212
|
await user.type(screen.getByLabelText('Name'), 'John Doe');
|
|
214
213
|
await user.click(screen.getByRole('button', { name: 'Submit' }));
|
|
215
214
|
|
|
216
|
-
expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' }
|
|
215
|
+
expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' });
|
|
217
216
|
});
|
|
218
217
|
});
|
|
219
218
|
|
|
@@ -387,10 +386,9 @@ describe('Form Component', () => {
|
|
|
387
386
|
email: 'john@example.com'
|
|
388
387
|
},
|
|
389
388
|
preferences: {
|
|
390
|
-
theme: 'dark'
|
|
391
|
-
notifications: undefined
|
|
389
|
+
theme: 'dark'
|
|
392
390
|
}
|
|
393
|
-
}
|
|
391
|
+
});
|
|
394
392
|
});
|
|
395
393
|
});
|
|
396
394
|
});
|
|
@@ -469,8 +467,9 @@ describe('FormField Component', () => {
|
|
|
469
467
|
</Form>
|
|
470
468
|
);
|
|
471
469
|
|
|
472
|
-
|
|
473
|
-
|
|
470
|
+
// FormField applies className to the label element, not a div container
|
|
471
|
+
const label = screen.getByLabelText('Test Field').closest('label');
|
|
472
|
+
expect(label).toHaveClass('custom-field');
|
|
474
473
|
});
|
|
475
474
|
|
|
476
475
|
it('renders with test ID', () => {
|
|
@@ -709,7 +708,7 @@ describe('Integration', () => {
|
|
|
709
708
|
name: 'John Doe',
|
|
710
709
|
email: 'john@example.com',
|
|
711
710
|
age: 25
|
|
712
|
-
}
|
|
711
|
+
});
|
|
713
712
|
});
|
|
714
713
|
expect(onError).not.toHaveBeenCalled();
|
|
715
714
|
});
|
|
@@ -71,12 +71,16 @@
|
|
|
71
71
|
* - React 19+ - Hooks and context
|
|
72
72
|
*/
|
|
73
73
|
|
|
74
|
-
import React from 'react';
|
|
75
|
-
import { useForm, FormProvider, UseFormReturn, FieldValues, DefaultValues, SubmitHandler, SubmitErrorHandler, useFormContext, Controller, FieldPath, ControllerRenderProps, ControllerFieldState, UseFormStateReturn } from 'react-hook-form';
|
|
74
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
75
|
+
import { useForm, FormProvider, UseFormReturn, FieldValues, DefaultValues, SubmitHandler, SubmitErrorHandler, useFormContext, Controller, FieldPath, ControllerRenderProps, ControllerFieldState, UseFormStateReturn, useWatch } from 'react-hook-form';
|
|
76
76
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
77
77
|
import { z } from 'zod';
|
|
78
|
+
import { useLocation } from 'react-router-dom';
|
|
79
|
+
import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
78
80
|
import { cn } from '../../utils/core/cn';
|
|
79
|
-
import {
|
|
81
|
+
import { useSessionDraft } from '../../hooks/useSessionDraft';
|
|
82
|
+
import { deriveFormKey } from '../../utils/persistence/keyDerivation';
|
|
83
|
+
import { filterSensitiveFields, isSensitiveField } from '../../utils/persistence/sensitiveFieldDetection';
|
|
80
84
|
|
|
81
85
|
/**
|
|
82
86
|
* Props for the Form component
|
|
@@ -149,14 +153,370 @@ export function Form<TFieldValues extends FieldValues = FieldValues>({
|
|
|
149
153
|
children,
|
|
150
154
|
className,
|
|
151
155
|
}: FormProps<TFieldValues>) {
|
|
156
|
+
// Call all hooks unconditionally at the top level
|
|
157
|
+
// Hooks must be called in the same order on every render
|
|
158
|
+
// If providers are missing, these hooks will throw - errors should be handled by error boundaries
|
|
159
|
+
const location = useLocation();
|
|
160
|
+
const auth = useUnifiedAuth();
|
|
161
|
+
const userId = auth.user?.id || null;
|
|
162
|
+
|
|
163
|
+
// Extract field names from schema or defaultValues for key derivation and sensitive field filtering
|
|
164
|
+
const fieldNames = useMemo(() => {
|
|
165
|
+
if (schema && 'shape' in schema && typeof schema.shape === 'object') {
|
|
166
|
+
return Object.keys(schema.shape as Record<string, unknown>);
|
|
167
|
+
}
|
|
168
|
+
if (defaultValues) {
|
|
169
|
+
return Object.keys(defaultValues);
|
|
170
|
+
}
|
|
171
|
+
return [];
|
|
172
|
+
}, [schema, defaultValues]);
|
|
173
|
+
|
|
174
|
+
// Derive persistence key (scoped by user ID)
|
|
175
|
+
const persistenceKey = useMemo(() => {
|
|
176
|
+
return deriveFormKey(
|
|
177
|
+
{
|
|
178
|
+
fieldNames,
|
|
179
|
+
},
|
|
180
|
+
null, // Parent context (Dialog) - not available yet, can be enhanced later
|
|
181
|
+
location,
|
|
182
|
+
userId
|
|
183
|
+
);
|
|
184
|
+
}, [fieldNames, location, userId]);
|
|
185
|
+
|
|
186
|
+
// Get field types for sensitive field detection
|
|
187
|
+
// Extract from schema if available, otherwise infer from defaultValues
|
|
188
|
+
const fieldTypes = useMemo(() => {
|
|
189
|
+
const types: Record<string, string> = {};
|
|
190
|
+
|
|
191
|
+
// Try to extract types from schema
|
|
192
|
+
if (schema && 'shape' in schema && typeof schema.shape === 'object') {
|
|
193
|
+
const shape = schema.shape as Record<string, any>;
|
|
194
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
195
|
+
// Zod schema type detection (simplified)
|
|
196
|
+
if (value && typeof value === 'object' && '_def' in value) {
|
|
197
|
+
const def = (value as any)._def;
|
|
198
|
+
if (def.typeName === 'ZodString') {
|
|
199
|
+
types[key] = 'text';
|
|
200
|
+
} else if (def.typeName === 'ZodNumber') {
|
|
201
|
+
types[key] = 'number';
|
|
202
|
+
} else if (def.typeName === 'ZodBoolean') {
|
|
203
|
+
types[key] = 'checkbox';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return types;
|
|
210
|
+
}, [schema]);
|
|
211
|
+
|
|
212
|
+
// Use session draft for persistence
|
|
213
|
+
const { state: persistedValues, setState: setPersistedValues, clearDraft } = useSessionDraft<Partial<TFieldValues>>(
|
|
214
|
+
persistenceKey || 'form:no-key',
|
|
215
|
+
{} as Partial<TFieldValues>,
|
|
216
|
+
{
|
|
217
|
+
enabled: Boolean(persistenceKey),
|
|
218
|
+
debounceMs: 300,
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Merge persisted values with defaultValues (persisted takes precedence)
|
|
223
|
+
const mergedDefaultValues = useMemo(() => {
|
|
224
|
+
if (!persistenceKey || !persistedValues || Object.keys(persistedValues).length === 0) {
|
|
225
|
+
return defaultValues;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Filter sensitive fields from persisted values
|
|
229
|
+
const filteredPersisted = filterSensitiveFields(
|
|
230
|
+
persistedValues,
|
|
231
|
+
fieldNames,
|
|
232
|
+
fieldTypes
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
...defaultValues,
|
|
237
|
+
...filteredPersisted,
|
|
238
|
+
} as DefaultValues<TFieldValues>;
|
|
239
|
+
}, [defaultValues, persistedValues, persistenceKey, fieldNames, fieldTypes]);
|
|
240
|
+
|
|
152
241
|
const methods = useForm<TFieldValues>({
|
|
153
242
|
resolver: schema ? zodResolver(schema) : undefined,
|
|
154
|
-
defaultValues,
|
|
243
|
+
defaultValues: mergedDefaultValues,
|
|
155
244
|
mode,
|
|
156
245
|
shouldUnregister: false,
|
|
157
246
|
});
|
|
158
247
|
|
|
159
|
-
|
|
248
|
+
// Track if we've already restored persisted values to prevent infinite loops
|
|
249
|
+
const hasRestoredRef = useRef(false);
|
|
250
|
+
const isRestoringRef = useRef(false);
|
|
251
|
+
const restoreTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
252
|
+
const lastRestoredRef = useRef<string | null>(null);
|
|
253
|
+
|
|
254
|
+
// Restore persisted values after form initialization
|
|
255
|
+
// CRITICAL: Must run when persistedValues changes (e.g., when dialog auto-opens)
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
// Clear any pending restore
|
|
258
|
+
if (restoreTimeoutRef.current) {
|
|
259
|
+
clearTimeout(restoreTimeoutRef.current);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Skip if already restored or currently restoring
|
|
263
|
+
if (isRestoringRef.current) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!persistenceKey || !persistedValues || Object.keys(persistedValues).length === 0) {
|
|
268
|
+
// Mark as restored even if no persisted values (prevents re-running)
|
|
269
|
+
if (!hasRestoredRef.current) {
|
|
270
|
+
hasRestoredRef.current = true;
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Skip if we've already restored these exact values
|
|
276
|
+
const persistedValuesStr = JSON.stringify(persistedValues);
|
|
277
|
+
if (lastRestoredRef.current === persistedValuesStr && hasRestoredRef.current) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
isRestoringRef.current = true;
|
|
282
|
+
|
|
283
|
+
// Defer restoration to prevent blocking and allow form to initialize
|
|
284
|
+
restoreTimeoutRef.current = setTimeout(() => {
|
|
285
|
+
// CRITICAL: Handle both numeric keys (from old array-based persistence) and field names
|
|
286
|
+
// If persistedValues has numeric keys, map them to fieldNames
|
|
287
|
+
const persistedKeys = Object.keys(persistedValues as Record<string, any>);
|
|
288
|
+
const hasNumericKeys = persistedKeys.length > 0 && persistedKeys.every(key => /^\d+$/.test(key));
|
|
289
|
+
|
|
290
|
+
let valuesToRestore: Record<string, any>;
|
|
291
|
+
if (hasNumericKeys && fieldNames.length === persistedKeys.length) {
|
|
292
|
+
// Map numeric keys to field names
|
|
293
|
+
valuesToRestore = {};
|
|
294
|
+
for (let i = 0; i < fieldNames.length; i++) {
|
|
295
|
+
const fieldName = fieldNames[i];
|
|
296
|
+
const numericKey = String(i);
|
|
297
|
+
if (numericKey in persistedValues) {
|
|
298
|
+
valuesToRestore[fieldName] = (persistedValues as Record<string, any>)[numericKey];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
console.log('[Form Persistence] Mapped numeric keys to field names:', {
|
|
302
|
+
numericKeys: persistedKeys,
|
|
303
|
+
fieldNames,
|
|
304
|
+
mappedValues: valuesToRestore,
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
// Use persistedValues as-is (should have field names as keys)
|
|
308
|
+
valuesToRestore = persistedValues as Record<string, any>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Filter sensitive fields
|
|
312
|
+
const restoreKeys = Object.keys(valuesToRestore);
|
|
313
|
+
const filteredPersisted = filterSensitiveFields(
|
|
314
|
+
valuesToRestore,
|
|
315
|
+
restoreKeys.length > 0 ? restoreKeys : fieldNames,
|
|
316
|
+
fieldTypes
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Debug: Check which fields are being filtered
|
|
320
|
+
const sensitiveFields = restoreKeys.filter(name => {
|
|
321
|
+
const type = fieldTypes?.[name];
|
|
322
|
+
return isSensitiveField(name, type);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
console.log('[Form Persistence] ✅ Restoring persisted values:', {
|
|
326
|
+
persistenceKey,
|
|
327
|
+
persistedValuesKeys: persistedKeys,
|
|
328
|
+
persistedValuesString: JSON.stringify(persistedValues),
|
|
329
|
+
hasNumericKeys,
|
|
330
|
+
valuesToRestoreKeys: Object.keys(valuesToRestore),
|
|
331
|
+
filteredPersistedKeys: Object.keys(filteredPersisted),
|
|
332
|
+
fieldNames,
|
|
333
|
+
sensitiveFields,
|
|
334
|
+
filteredCount: Object.keys(filteredPersisted).length,
|
|
335
|
+
persistedCount: persistedKeys.length,
|
|
336
|
+
timestamp: new Date().toISOString(),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Set values that might not have been in defaultValues
|
|
340
|
+
const valuesToSet: Partial<TFieldValues> = {};
|
|
341
|
+
let hasValuesToSet = false;
|
|
342
|
+
|
|
343
|
+
for (const [key, value] of Object.entries(filteredPersisted)) {
|
|
344
|
+
const currentValue = methods.getValues(key as any);
|
|
345
|
+
// Only set if different from current value (to avoid unnecessary updates)
|
|
346
|
+
if (currentValue !== value) {
|
|
347
|
+
valuesToSet[key as keyof TFieldValues] = value as any;
|
|
348
|
+
hasValuesToSet = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (hasValuesToSet) {
|
|
353
|
+
console.log('[Form Persistence] 🔄 Setting form values via reset():', {
|
|
354
|
+
persistenceKey,
|
|
355
|
+
valuesToSetKeys: Object.keys(valuesToSet),
|
|
356
|
+
valuesToSet,
|
|
357
|
+
timestamp: new Date().toISOString(),
|
|
358
|
+
});
|
|
359
|
+
// Use reset to update all values at once
|
|
360
|
+
methods.reset({
|
|
361
|
+
...methods.getValues(),
|
|
362
|
+
...valuesToSet,
|
|
363
|
+
} as TFieldValues);
|
|
364
|
+
console.log('[Form Persistence] ✅ Form values set successfully', {
|
|
365
|
+
persistenceKey,
|
|
366
|
+
currentValues: methods.getValues(),
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
console.log('[Form Persistence] ⏭️ No values to set (all values already match)', {
|
|
370
|
+
persistenceKey,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
lastRestoredRef.current = persistedValuesStr;
|
|
375
|
+
hasRestoredRef.current = true;
|
|
376
|
+
isRestoringRef.current = false;
|
|
377
|
+
restoreTimeoutRef.current = null;
|
|
378
|
+
}, 100); // Small delay to ensure form is ready
|
|
379
|
+
|
|
380
|
+
return () => {
|
|
381
|
+
if (restoreTimeoutRef.current) {
|
|
382
|
+
clearTimeout(restoreTimeoutRef.current);
|
|
383
|
+
restoreTimeoutRef.current = null;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
387
|
+
}, [persistedValues, persistenceKey]); // Run when persistedValues changes (e.g., dialog auto-opens)
|
|
388
|
+
|
|
389
|
+
// Log component mount
|
|
390
|
+
|
|
391
|
+
// Watch form values for persistence
|
|
392
|
+
// CRITICAL: Don't pass name parameter - useWatch without name returns all values as an object
|
|
393
|
+
// If we pass fieldNames array, it returns an array with numeric indices, not an object with field names
|
|
394
|
+
const watchedValues = useWatch({
|
|
395
|
+
control: methods.control,
|
|
396
|
+
// Don't pass name - we want all values as an object, not an array
|
|
397
|
+
}) as Partial<TFieldValues>;
|
|
398
|
+
|
|
399
|
+
// Track previous values to prevent unnecessary persistence updates
|
|
400
|
+
const previousValuesRef = useRef<string | null>(null);
|
|
401
|
+
const persistTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
402
|
+
|
|
403
|
+
// Persist form values (filtered for sensitive fields)
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
if (!persistenceKey || !watchedValues || isRestoringRef.current) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Skip if values haven't actually changed
|
|
410
|
+
const currentValuesStr = JSON.stringify(watchedValues);
|
|
411
|
+
if (currentValuesStr === previousValuesRef.current) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
previousValuesRef.current = currentValuesStr;
|
|
416
|
+
|
|
417
|
+
// Clear any pending persistence
|
|
418
|
+
if (persistTimeoutRef.current) {
|
|
419
|
+
clearTimeout(persistTimeoutRef.current);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Debounce persistence to prevent excessive updates while user is typing
|
|
423
|
+
persistTimeoutRef.current = setTimeout(() => {
|
|
424
|
+
// Filter sensitive fields before persisting
|
|
425
|
+
// CRITICAL: Use all keys from watchedValues to ensure we capture all values
|
|
426
|
+
const allFieldNames = Object.keys(watchedValues as Record<string, any>);
|
|
427
|
+
const filteredValues = filterSensitiveFields(
|
|
428
|
+
watchedValues as Record<string, any>,
|
|
429
|
+
allFieldNames.length > 0 ? allFieldNames : fieldNames,
|
|
430
|
+
fieldTypes
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Debug: Check which fields are being filtered
|
|
434
|
+
const sensitiveFields = allFieldNames.filter(name => {
|
|
435
|
+
const type = fieldTypes?.[name];
|
|
436
|
+
return isSensitiveField(name, type);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
console.log('[Form Persistence] 💾 Persisting form values:', {
|
|
440
|
+
persistenceKey,
|
|
441
|
+
filteredValuesKeys: Object.keys(filteredValues),
|
|
442
|
+
originalValuesKeys: Object.keys(watchedValues as Record<string, any>),
|
|
443
|
+
allFieldNames,
|
|
444
|
+
sensitiveFields,
|
|
445
|
+
filteredCount: Object.keys(filteredValues).length,
|
|
446
|
+
originalCount: allFieldNames.length,
|
|
447
|
+
timestamp: new Date().toISOString(),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
setPersistedValues(filteredValues as Partial<TFieldValues>);
|
|
451
|
+
|
|
452
|
+
// Log sessionStorage after setting (with delay to allow write)
|
|
453
|
+
if (persistenceKey) {
|
|
454
|
+
setTimeout(() => {
|
|
455
|
+
const storageKey = `pace-core:draft:${persistenceKey}`;
|
|
456
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
457
|
+
console.log('[Form Persistence] 📦 SessionStorage AFTER setPersistedValues:', {
|
|
458
|
+
persistenceKey,
|
|
459
|
+
storageKey,
|
|
460
|
+
stored: stored ? JSON.parse(stored) : null,
|
|
461
|
+
});
|
|
462
|
+
}, 100);
|
|
463
|
+
}
|
|
464
|
+
persistTimeoutRef.current = null;
|
|
465
|
+
}, 300); // Debounce for 300ms
|
|
466
|
+
|
|
467
|
+
return () => {
|
|
468
|
+
if (persistTimeoutRef.current) {
|
|
469
|
+
clearTimeout(persistTimeoutRef.current);
|
|
470
|
+
persistTimeoutRef.current = null;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
474
|
+
}, [watchedValues, persistenceKey]); // CRITICAL: Only depend on watchedValues and persistenceKey to prevent infinite loops
|
|
475
|
+
|
|
476
|
+
// Enhanced submit handler that clears draft on success
|
|
477
|
+
const handleSubmit = methods.handleSubmit(
|
|
478
|
+
async (data) => {
|
|
479
|
+
console.log('[Form Lifecycle] 📤 Form submit started', {
|
|
480
|
+
persistenceKey,
|
|
481
|
+
dataKeys: Object.keys(data),
|
|
482
|
+
timestamp: new Date().toISOString(),
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await onSubmit(data);
|
|
486
|
+
|
|
487
|
+
console.log('[Form Lifecycle] ✅ Form submit successful', {
|
|
488
|
+
persistenceKey,
|
|
489
|
+
timestamp: new Date().toISOString(),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Clear draft after successful submit
|
|
493
|
+
if (persistenceKey && clearDraft) {
|
|
494
|
+
console.log('[Form Persistence] 🗑️ Clearing draft after successful submit', {
|
|
495
|
+
persistenceKey,
|
|
496
|
+
});
|
|
497
|
+
clearDraft();
|
|
498
|
+
|
|
499
|
+
// Log sessionStorage after clearing
|
|
500
|
+
setTimeout(() => {
|
|
501
|
+
const storageKey = `pace-core:draft:${persistenceKey}`;
|
|
502
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
503
|
+
console.log('[Form Persistence] 📦 SessionStorage AFTER clearDraft (submit):', {
|
|
504
|
+
persistenceKey,
|
|
505
|
+
storageKey,
|
|
506
|
+
stored: stored ? JSON.parse(stored) : null,
|
|
507
|
+
});
|
|
508
|
+
}, 100);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
(errors) => {
|
|
512
|
+
console.log('[Form Lifecycle] ❌ Form submit failed with errors', {
|
|
513
|
+
persistenceKey,
|
|
514
|
+
errors,
|
|
515
|
+
timestamp: new Date().toISOString(),
|
|
516
|
+
});
|
|
517
|
+
onError?.(errors);
|
|
518
|
+
}
|
|
519
|
+
);
|
|
160
520
|
|
|
161
521
|
return (
|
|
162
522
|
<FormProvider {...methods}>
|
|
@@ -322,16 +682,16 @@ export function FormField<
|
|
|
322
682
|
const { control } = useFormContext<TFieldValues>();
|
|
323
683
|
|
|
324
684
|
return (
|
|
325
|
-
<
|
|
685
|
+
<label className={cn("space-y-2", className)}>
|
|
326
686
|
{label && (
|
|
327
|
-
|
|
687
|
+
<>
|
|
328
688
|
{label}
|
|
329
689
|
{validation?.required && (
|
|
330
690
|
<span className="text-destructive ml-1" aria-label="required">
|
|
331
691
|
*
|
|
332
692
|
</span>
|
|
333
693
|
)}
|
|
334
|
-
|
|
694
|
+
</>
|
|
335
695
|
)}
|
|
336
696
|
|
|
337
697
|
<Controller
|
|
@@ -375,6 +735,6 @@ export function FormField<
|
|
|
375
735
|
);
|
|
376
736
|
}}
|
|
377
737
|
/>
|
|
378
|
-
</
|
|
738
|
+
</label>
|
|
379
739
|
);
|
|
380
740
|
}
|