canopycms 0.0.0 → 0.0.1
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/package.json +2 -3
- package/dist/__integration__/fixtures/content-seeds.d.ts +0 -43
- package/dist/__integration__/fixtures/content-seeds.d.ts.map +0 -1
- package/dist/__integration__/fixtures/content-seeds.js +0 -99
- package/dist/__integration__/fixtures/content-seeds.js.map +0 -1
- package/dist/__integration__/fixtures/schemas.d.ts +0 -12
- package/dist/__integration__/fixtures/schemas.d.ts.map +0 -1
- package/dist/__integration__/fixtures/schemas.js +0 -65
- package/dist/__integration__/fixtures/schemas.js.map +0 -1
- package/dist/__integration__/test-utils/api-client.d.ts +0 -123
- package/dist/__integration__/test-utils/api-client.d.ts.map +0 -1
- package/dist/__integration__/test-utils/api-client.js +0 -118
- package/dist/__integration__/test-utils/api-client.js.map +0 -1
- package/dist/__integration__/test-utils/multi-user.d.ts +0 -25
- package/dist/__integration__/test-utils/multi-user.d.ts.map +0 -1
- package/dist/__integration__/test-utils/multi-user.js +0 -105
- package/dist/__integration__/test-utils/multi-user.js.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.d.ts +0 -25
- package/dist/__integration__/test-utils/test-workspace.d.ts.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.js +0 -102
- package/dist/__integration__/test-utils/test-workspace.js.map +0 -1
- package/dist/editor/BranchManager.stories.d.ts +0 -8
- package/dist/editor/BranchManager.stories.d.ts.map +0 -1
- package/dist/editor/BranchManager.stories.js +0 -74
- package/dist/editor/BranchManager.stories.js.map +0 -1
- package/dist/editor/CanopyEditor.stories.d.ts +0 -7
- package/dist/editor/CanopyEditor.stories.d.ts.map +0 -1
- package/dist/editor/CanopyEditor.stories.js +0 -99
- package/dist/editor/CanopyEditor.stories.js.map +0 -1
- package/dist/editor/CommentsPanel.stories.d.ts +0 -10
- package/dist/editor/CommentsPanel.stories.d.ts.map +0 -1
- package/dist/editor/CommentsPanel.stories.js +0 -175
- package/dist/editor/CommentsPanel.stories.js.map +0 -1
- package/dist/editor/Editor.stories.d.ts +0 -7
- package/dist/editor/Editor.stories.d.ts.map +0 -1
- package/dist/editor/Editor.stories.js +0 -95
- package/dist/editor/Editor.stories.js.map +0 -1
- package/dist/editor/EditorPanes.stories.d.ts +0 -7
- package/dist/editor/EditorPanes.stories.d.ts.map +0 -1
- package/dist/editor/EditorPanes.stories.js +0 -116
- package/dist/editor/EditorPanes.stories.js.map +0 -1
- package/dist/editor/EntryNavigator.stories.d.ts +0 -8
- package/dist/editor/EntryNavigator.stories.d.ts.map +0 -1
- package/dist/editor/EntryNavigator.stories.js +0 -42
- package/dist/editor/EntryNavigator.stories.js.map +0 -1
- package/dist/editor/FormRenderer.stories.d.ts +0 -7
- package/dist/editor/FormRenderer.stories.d.ts.map +0 -1
- package/dist/editor/FormRenderer.stories.js +0 -115
- package/dist/editor/FormRenderer.stories.js.map +0 -1
- package/dist/editor/GroupManager.stories.d.ts +0 -19
- package/dist/editor/GroupManager.stories.d.ts.map +0 -1
- package/dist/editor/GroupManager.stories.js +0 -265
- package/dist/editor/GroupManager.stories.js.map +0 -1
- package/dist/editor/PermissionManager.stories.d.ts +0 -20
- package/dist/editor/PermissionManager.stories.d.ts.map +0 -1
- package/dist/editor/PermissionManager.stories.js +0 -506
- package/dist/editor/PermissionManager.stories.js.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.d.ts +0 -10
- package/dist/editor/comments/FieldWrapper.stories.d.ts.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.js +0 -173
- package/dist/editor/comments/FieldWrapper.stories.js.map +0 -1
- package/dist/editor/fields/BlockField.stories.d.ts +0 -7
- package/dist/editor/fields/BlockField.stories.d.ts.map +0 -1
- package/dist/editor/fields/BlockField.stories.js +0 -50
- package/dist/editor/fields/BlockField.stories.js.map +0 -1
- package/dist/editor/fields/fields.stories.d.ts +0 -8
- package/dist/editor/fields/fields.stories.d.ts.map +0 -1
- package/dist/editor/fields/fields.stories.js +0 -34
- package/dist/editor/fields/fields.stories.js.map +0 -1
- package/dist/test-utils/api-test-helpers.d.ts +0 -238
- package/dist/test-utils/api-test-helpers.d.ts.map +0 -1
- package/dist/test-utils/api-test-helpers.js +0 -347
- package/dist/test-utils/api-test-helpers.js.map +0 -1
- package/dist/test-utils/console-spy.d.ts +0 -56
- package/dist/test-utils/console-spy.d.ts.map +0 -1
- package/dist/test-utils/console-spy.js +0 -81
- package/dist/test-utils/console-spy.js.map +0 -1
- package/dist/test-utils/git-helpers.d.ts +0 -21
- package/dist/test-utils/git-helpers.d.ts.map +0 -1
- package/dist/test-utils/git-helpers.js +0 -23
- package/dist/test-utils/git-helpers.js.map +0 -1
- package/dist/test-utils/index.d.ts +0 -5
- package/dist/test-utils/index.d.ts.map +0 -1
- package/dist/test-utils/index.js +0 -4
- package/dist/test-utils/index.js.map +0 -1
- package/src/__integration__/errors/invalid-content.test.ts +0 -238
- package/src/__integration__/errors/permission-denied.test.ts +0 -220
- package/src/__integration__/fixtures/content-seeds.ts +0 -105
- package/src/__integration__/fixtures/schemas.ts +0 -67
- package/src/__integration__/initialization/prod-sim-init.test.ts +0 -139
- package/src/__integration__/permissions/path-permissions.test.ts +0 -314
- package/src/__integration__/permissions/role-permissions.test.ts +0 -354
- package/src/__integration__/permissions/settings-branch-isolation.test.ts +0 -317
- package/src/__integration__/settings/groups-api.test.ts +0 -403
- package/src/__integration__/test-utils/api-client.ts +0 -167
- package/src/__integration__/test-utils/multi-user.ts +0 -129
- package/src/__integration__/test-utils/test-workspace.ts +0 -130
- package/src/__integration__/user/user-context.test.ts +0 -174
- package/src/__integration__/validation/input-validation.test.ts +0 -166
- package/src/__integration__/workflows/api-editing-workflow.test.ts +0 -244
- package/src/__integration__/workflows/conflict-resolution.test.ts +0 -259
- package/src/__integration__/workflows/editing-workflow.test.ts +0 -205
- package/src/__integration__/workflows/review-workflow.test.ts +0 -260
- package/src/ai/__tests__/build.integration.test.ts +0 -224
- package/src/ai/__tests__/generate.integration.test.ts +0 -495
- package/src/ai/__tests__/handler.integration.test.ts +0 -212
- package/src/ai/__tests__/json-to-markdown.test.ts +0 -553
- package/src/ai/generate.ts +0 -410
- package/src/ai/handler.ts +0 -123
- package/src/ai/index.ts +0 -26
- package/src/ai/json-to-markdown.ts +0 -424
- package/src/ai/resolve-branch.ts +0 -34
- package/src/ai/types.ts +0 -160
- package/src/api/AGENTS.md +0 -81
- package/src/api/__test__/mock-client.ts +0 -404
- package/src/api/assets.test.ts +0 -140
- package/src/api/assets.ts +0 -154
- package/src/api/branch-merge.test.ts +0 -163
- package/src/api/branch-merge.ts +0 -113
- package/src/api/branch-review.test.ts +0 -297
- package/src/api/branch-review.ts +0 -136
- package/src/api/branch-status.test.ts +0 -85
- package/src/api/branch-status.ts +0 -153
- package/src/api/branch-withdraw.test.ts +0 -146
- package/src/api/branch-withdraw.ts +0 -81
- package/src/api/branch-workflow.integration.test.ts +0 -578
- package/src/api/branch.test.ts +0 -620
- package/src/api/branch.ts +0 -492
- package/src/api/client.test.ts +0 -349
- package/src/api/client.ts +0 -506
- package/src/api/comments.test.ts +0 -285
- package/src/api/comments.ts +0 -210
- package/src/api/content.test.ts +0 -345
- package/src/api/content.ts +0 -454
- package/src/api/entries.test.ts +0 -1339
- package/src/api/entries.ts +0 -650
- package/src/api/github-sync.ts +0 -144
- package/src/api/groups.test.ts +0 -1013
- package/src/api/groups.ts +0 -375
- package/src/api/guards.test.ts +0 -533
- package/src/api/guards.ts +0 -271
- package/src/api/index.ts +0 -87
- package/src/api/permissions.test.ts +0 -766
- package/src/api/permissions.ts +0 -334
- package/src/api/reference-options.ts +0 -118
- package/src/api/resolve-references.ts +0 -107
- package/src/api/route-builder.ts +0 -289
- package/src/api/schema.test.ts +0 -840
- package/src/api/schema.ts +0 -936
- package/src/api/security.test.ts +0 -233
- package/src/api/settings-helpers.ts +0 -84
- package/src/api/types.ts +0 -40
- package/src/api/user.test.ts +0 -127
- package/src/api/user.ts +0 -42
- package/src/api/validators.test.ts +0 -275
- package/src/api/validators.ts +0 -176
- package/src/asset-store.test.ts +0 -37
- package/src/asset-store.ts +0 -110
- package/src/auth/cache.ts +0 -7
- package/src/auth/caching-auth-plugin.test.ts +0 -154
- package/src/auth/caching-auth-plugin.ts +0 -109
- package/src/auth/context-helpers.ts +0 -75
- package/src/auth/file-based-auth-cache.test.ts +0 -257
- package/src/auth/file-based-auth-cache.ts +0 -279
- package/src/auth/index.ts +0 -12
- package/src/auth/plugin.ts +0 -51
- package/src/auth/types.ts +0 -38
- package/src/authorization/__tests__/branch.test.ts +0 -260
- package/src/authorization/__tests__/content.test.ts +0 -142
- package/src/authorization/__tests__/path.test.ts +0 -133
- package/src/authorization/__tests__/permissions-loader.test.ts +0 -200
- package/src/authorization/branch.ts +0 -94
- package/src/authorization/content.ts +0 -93
- package/src/authorization/groups/index.ts +0 -11
- package/src/authorization/groups/loader.ts +0 -127
- package/src/authorization/groups/schema.ts +0 -48
- package/src/authorization/helpers.ts +0 -48
- package/src/authorization/index.ts +0 -84
- package/src/authorization/path.ts +0 -112
- package/src/authorization/permissions/index.ts +0 -11
- package/src/authorization/permissions/loader.ts +0 -116
- package/src/authorization/permissions/schema.ts +0 -66
- package/src/authorization/test-utils.ts +0 -15
- package/src/authorization/types.ts +0 -66
- package/src/authorization/validation.test.ts +0 -100
- package/src/authorization/validation.ts +0 -62
- package/src/branch-metadata.test.ts +0 -168
- package/src/branch-metadata.ts +0 -166
- package/src/branch-registry.test.ts +0 -248
- package/src/branch-registry.ts +0 -152
- package/src/branch-schema-cache.test.ts +0 -275
- package/src/branch-schema-cache.ts +0 -189
- package/src/branch-workspace.test.ts +0 -183
- package/src/branch-workspace.ts +0 -124
- package/src/build/generate-ai-content.ts +0 -78
- package/src/build/index.ts +0 -8
- package/src/build-mode.ts +0 -27
- package/src/cli/generate-ai-content.ts +0 -100
- package/src/cli/init.test.ts +0 -240
- package/src/cli/templates/Dockerfile.cms.template +0 -19
- package/src/cli/templates/canopy.ts.template +0 -55
- package/src/cli/templates/canopycms.config.ts.template +0 -11
- package/src/cli/templates/deploy-cms.yml.template +0 -27
- package/src/cli/templates/edit-page.tsx.template +0 -32
- package/src/cli/templates/route.ts.template +0 -12
- package/src/cli/templates/schemas.ts.template +0 -16
- package/src/cli/templates.ts +0 -47
- package/src/client.ts +0 -12
- package/src/comment-store.test.ts +0 -442
- package/src/comment-store.ts +0 -301
- package/src/config/__tests__/config.test.ts +0 -513
- package/src/config/flatten.ts +0 -174
- package/src/config/helpers.ts +0 -167
- package/src/config/index.ts +0 -86
- package/src/config/schemas/collection.ts +0 -67
- package/src/config/schemas/config.ts +0 -77
- package/src/config/schemas/field.ts +0 -108
- package/src/config/schemas/media.ts +0 -27
- package/src/config/schemas/permissions.ts +0 -21
- package/src/config/types.ts +0 -321
- package/src/config/validation.ts +0 -70
- package/src/config-test.ts +0 -65
- package/src/config.ts +0 -11
- package/src/content-id-index.test.ts +0 -512
- package/src/content-id-index.ts +0 -479
- package/src/content-reader.test.ts +0 -478
- package/src/content-reader.ts +0 -214
- package/src/content-store.test.ts +0 -1126
- package/src/content-store.ts +0 -793
- package/src/context.ts +0 -111
- package/src/editor/BranchManager.stories.tsx +0 -80
- package/src/editor/BranchManager.test.tsx +0 -324
- package/src/editor/BranchManager.tsx +0 -461
- package/src/editor/CanopyEditor.stories.tsx +0 -128
- package/src/editor/CanopyEditor.test.tsx +0 -81
- package/src/editor/CanopyEditor.tsx +0 -73
- package/src/editor/CanopyEditorPage.test.tsx +0 -59
- package/src/editor/CanopyEditorPage.tsx +0 -25
- package/src/editor/CommentsPanel.stories.tsx +0 -184
- package/src/editor/CommentsPanel.tsx +0 -338
- package/src/editor/Editor.integration.test.tsx +0 -227
- package/src/editor/Editor.stories.tsx +0 -119
- package/src/editor/Editor.tsx +0 -1221
- package/src/editor/EditorPanes.stories.tsx +0 -256
- package/src/editor/EditorPanes.test.tsx +0 -77
- package/src/editor/EditorPanes.tsx +0 -180
- package/src/editor/EntryNavigator.stories.tsx +0 -65
- package/src/editor/EntryNavigator.test.tsx +0 -598
- package/src/editor/EntryNavigator.tsx +0 -665
- package/src/editor/FormRenderer.stories.tsx +0 -212
- package/src/editor/FormRenderer.test.tsx +0 -194
- package/src/editor/FormRenderer.tsx +0 -432
- package/src/editor/GroupManager.stories.tsx +0 -301
- package/src/editor/GroupManager.test.tsx +0 -682
- package/src/editor/GroupManager.tsx +0 -9
- package/src/editor/PermissionManager.stories.tsx +0 -539
- package/src/editor/PermissionManager.test.tsx +0 -864
- package/src/editor/PermissionManager.tsx +0 -12
- package/src/editor/canopy-path.test.ts +0 -23
- package/src/editor/canopy-path.ts +0 -52
- package/src/editor/client-reference-resolver.ts +0 -118
- package/src/editor/comments/BranchComments.tsx +0 -93
- package/src/editor/comments/EntryComments.tsx +0 -94
- package/src/editor/comments/FieldWrapper.stories.tsx +0 -210
- package/src/editor/comments/FieldWrapper.tsx +0 -129
- package/src/editor/comments/InlineCommentThread.test.tsx +0 -384
- package/src/editor/comments/InlineCommentThread.tsx +0 -246
- package/src/editor/comments/ThreadCarousel.test.tsx +0 -393
- package/src/editor/comments/ThreadCarousel.tsx +0 -525
- package/src/editor/components/ConfirmDeleteModal.tsx +0 -49
- package/src/editor/components/EditorContext.tsx +0 -49
- package/src/editor/components/EditorFooter.tsx +0 -47
- package/src/editor/components/EditorHeader.tsx +0 -492
- package/src/editor/components/EditorSidebar.tsx +0 -193
- package/src/editor/components/EntryCreateModal.tsx +0 -193
- package/src/editor/components/RenameEntryModal.tsx +0 -152
- package/src/editor/components/UserBadge.test.tsx +0 -274
- package/src/editor/components/UserBadge.tsx +0 -240
- package/src/editor/components/index.ts +0 -6
- package/src/editor/context/ApiClientContext.tsx +0 -56
- package/src/editor/context/EditorStateContext.tsx +0 -221
- package/src/editor/context/index.ts +0 -40
- package/src/editor/editor-config.test.ts +0 -385
- package/src/editor/editor-config.ts +0 -94
- package/src/editor/editor-utils.test.ts +0 -772
- package/src/editor/editor-utils.ts +0 -303
- package/src/editor/env.ts +0 -4
- package/src/editor/fields/BlockField.stories.tsx +0 -79
- package/src/editor/fields/BlockField.tsx +0 -267
- package/src/editor/fields/CodeField.tsx +0 -41
- package/src/editor/fields/MarkdownField.tsx +0 -205
- package/src/editor/fields/ObjectField.tsx +0 -71
- package/src/editor/fields/ReferenceField.tsx +0 -138
- package/src/editor/fields/SelectField.tsx +0 -76
- package/src/editor/fields/TextField.tsx +0 -35
- package/src/editor/fields/ToggleField.tsx +0 -37
- package/src/editor/fields/fields.stories.tsx +0 -40
- package/src/editor/group-manager/ExternalGroupsTab.tsx +0 -114
- package/src/editor/group-manager/GroupCard.tsx +0 -102
- package/src/editor/group-manager/GroupForm.tsx +0 -66
- package/src/editor/group-manager/InternalGroupsTab.tsx +0 -147
- package/src/editor/group-manager/MemberList.tsx +0 -184
- package/src/editor/group-manager/hooks/useExternalGroupSearch.ts +0 -63
- package/src/editor/group-manager/hooks/useGroupState.ts +0 -134
- package/src/editor/group-manager/hooks/useUserSearch.ts +0 -84
- package/src/editor/group-manager/index.tsx +0 -210
- package/src/editor/group-manager/types.ts +0 -28
- package/src/editor/hooks/README.md +0 -26
- package/src/editor/hooks/__test__/test-utils.tsx +0 -183
- package/src/editor/hooks/index.ts +0 -23
- package/src/editor/hooks/useBranchActions.test.tsx +0 -267
- package/src/editor/hooks/useBranchActions.tsx +0 -121
- package/src/editor/hooks/useBranchManager.test.tsx +0 -391
- package/src/editor/hooks/useBranchManager.tsx +0 -326
- package/src/editor/hooks/useCommentSystem.test.ts +0 -615
- package/src/editor/hooks/useCommentSystem.ts +0 -347
- package/src/editor/hooks/useDraftManager.test.ts +0 -375
- package/src/editor/hooks/useDraftManager.ts +0 -259
- package/src/editor/hooks/useEditorLayout.test.ts +0 -147
- package/src/editor/hooks/useEditorLayout.ts +0 -67
- package/src/editor/hooks/useEntryManager.test.ts +0 -588
- package/src/editor/hooks/useEntryManager.ts +0 -387
- package/src/editor/hooks/useGroupManager.test.ts +0 -277
- package/src/editor/hooks/useGroupManager.ts +0 -139
- package/src/editor/hooks/usePermissionManager.test.ts +0 -211
- package/src/editor/hooks/usePermissionManager.ts +0 -113
- package/src/editor/hooks/useReferenceResolution.ts +0 -248
- package/src/editor/hooks/useSchemaManager.test.ts +0 -370
- package/src/editor/hooks/useSchemaManager.ts +0 -310
- package/src/editor/hooks/useUserContext.tsx +0 -57
- package/src/editor/hooks/useUserMetadata.test.ts +0 -191
- package/src/editor/hooks/useUserMetadata.ts +0 -71
- package/src/editor/permission-manager/GroupSelector.tsx +0 -73
- package/src/editor/permission-manager/PermissionEditor.tsx +0 -321
- package/src/editor/permission-manager/PermissionLevelBadge.tsx +0 -53
- package/src/editor/permission-manager/PermissionTree.tsx +0 -237
- package/src/editor/permission-manager/UserSelector.tsx +0 -95
- package/src/editor/permission-manager/constants.tsx +0 -18
- package/src/editor/permission-manager/hooks/useGroupsAndUsers.ts +0 -153
- package/src/editor/permission-manager/hooks/usePermissionTree.ts +0 -200
- package/src/editor/permission-manager/index.tsx +0 -294
- package/src/editor/permission-manager/types.ts +0 -58
- package/src/editor/permission-manager/utils.ts +0 -179
- package/src/editor/preview-bridge.test.tsx +0 -50
- package/src/editor/preview-bridge.tsx +0 -294
- package/src/editor/schema-editor/CollectionEditor.test.tsx +0 -238
- package/src/editor/schema-editor/CollectionEditor.tsx +0 -520
- package/src/editor/schema-editor/EntryTypeEditor.test.tsx +0 -215
- package/src/editor/schema-editor/EntryTypeEditor.tsx +0 -367
- package/src/editor/schema-editor/index.ts +0 -19
- package/src/editor/setup-test-dom.ts +0 -10
- package/src/editor/test-setup.ts +0 -33
- package/src/editor/theme.tsx +0 -119
- package/src/editor/utils/env.ts +0 -39
- package/src/entry-schema-registry.test.ts +0 -281
- package/src/entry-schema-registry.ts +0 -121
- package/src/entry-schema.ts +0 -84
- package/src/git-manager.test.ts +0 -552
- package/src/git-manager.ts +0 -667
- package/src/github-service.test.ts +0 -312
- package/src/github-service.ts +0 -295
- package/src/http/handler.test.ts +0 -275
- package/src/http/handler.ts +0 -280
- package/src/http/index.ts +0 -11
- package/src/http/router.ts +0 -164
- package/src/http/types.ts +0 -44
- package/src/id.test.ts +0 -48
- package/src/id.ts +0 -22
- package/src/index.ts +0 -26
- package/src/operating-mode/__tests__/strategies.test.ts +0 -511
- package/src/operating-mode/client-safe-strategy.ts +0 -184
- package/src/operating-mode/client-unsafe-strategy.ts +0 -303
- package/src/operating-mode/client.ts +0 -13
- package/src/operating-mode/index.ts +0 -34
- package/src/operating-mode/types.ts +0 -186
- package/src/paths/__tests__/branch.test.ts +0 -53
- package/src/paths/__tests__/normalize.test.ts +0 -141
- package/src/paths/__tests__/resolve.test.ts +0 -207
- package/src/paths/__tests__/validation.test.ts +0 -61
- package/src/paths/branch.ts +0 -115
- package/src/paths/index.ts +0 -73
- package/src/paths/normalize-server.ts +0 -40
- package/src/paths/normalize.ts +0 -107
- package/src/paths/resolve.ts +0 -61
- package/src/paths/test-utils.ts +0 -37
- package/src/paths/types.ts +0 -68
- package/src/paths/validation.test.ts +0 -480
- package/src/paths/validation.ts +0 -391
- package/src/reference-resolver.test.ts +0 -107
- package/src/reference-resolver.ts +0 -157
- package/src/schema/index.ts +0 -29
- package/src/schema/meta-loader.ts +0 -366
- package/src/schema/resolver.ts +0 -83
- package/src/schema/schema-store-types.ts +0 -56
- package/src/schema/schema-store.test.ts +0 -816
- package/src/schema/schema-store.ts +0 -795
- package/src/schema/types.ts +0 -33
- package/src/schema-meta-loader.test.ts +0 -447
- package/src/server.ts +0 -15
- package/src/services.test.ts +0 -559
- package/src/services.ts +0 -373
- package/src/settings-branch-utils.ts +0 -53
- package/src/settings-workspace.ts +0 -156
- package/src/task-queue/README.md +0 -144
- package/src/task-queue/index.ts +0 -14
- package/src/task-queue/task-queue.test.ts +0 -524
- package/src/task-queue/task-queue.ts +0 -514
- package/src/task-queue/types.ts +0 -41
- package/src/test-utils/api-test-helpers.ts +0 -445
- package/src/test-utils/console-spy.test.ts +0 -14
- package/src/test-utils/console-spy.ts +0 -125
- package/src/test-utils/git-helpers.ts +0 -31
- package/src/test-utils/index.ts +0 -4
- package/src/types.ts +0 -54
- package/src/user.ts +0 -118
- package/src/utils/debug.test.ts +0 -114
- package/src/utils/debug.ts +0 -127
- package/src/utils/error.test.ts +0 -92
- package/src/utils/error.ts +0 -83
- package/src/utils/format.ts +0 -12
- package/src/validation/__tests__/field-traversal.test.ts +0 -263
- package/src/validation/deletion-checker.ts +0 -234
- package/src/validation/field-traversal.ts +0 -146
- package/src/validation/reference-validator.ts +0 -168
- package/src/worker/cms-worker-rebase.test.ts +0 -473
- package/src/worker/cms-worker.ts +0 -777
- package/src/worker/integration.test.ts +0 -289
- package/src/worker/task-queue-config.ts +0 -25
- package/src/worker/task-queue.test.ts +0 -452
- package/src/worker/task-queue.ts +0 -58
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
-
import { notifications } from '@mantine/notifications'
|
|
3
|
-
import type { InternalGroup } from '../../authorization'
|
|
4
|
-
import type { UserSearchResult, GroupMetadata } from '../../auth/types'
|
|
5
|
-
import { useApiClient } from '../context'
|
|
6
|
-
|
|
7
|
-
export interface UseGroupManagerOptions {
|
|
8
|
-
/**
|
|
9
|
-
* Whether the group manager is currently open.
|
|
10
|
-
* Groups are loaded when this becomes true.
|
|
11
|
-
*/
|
|
12
|
-
isOpen: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface UseGroupManagerReturn {
|
|
16
|
-
groupsData: InternalGroup[]
|
|
17
|
-
groupsLoading: boolean
|
|
18
|
-
handleSaveGroups: (groups: InternalGroup[]) => Promise<void>
|
|
19
|
-
handleSearchUsers: (query: string, limit?: number) => Promise<UserSearchResult[]>
|
|
20
|
-
handleGetUserMetadata: (userId: string) => Promise<UserSearchResult | null>
|
|
21
|
-
handleSearchExternalGroups: (query: string) => Promise<GroupMetadata[]>
|
|
22
|
-
loadGroups: () => Promise<void>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Custom hook for managing internal groups (CRUD operations).
|
|
27
|
-
*
|
|
28
|
-
* Handles:
|
|
29
|
-
* - Loading groups from API
|
|
30
|
-
* - Saving groups to API
|
|
31
|
-
* - Searching for users to add to groups
|
|
32
|
-
* - Searching for external groups
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```tsx
|
|
36
|
-
* const { groupsData, groupsLoading, handleSaveGroups, handleSearchUsers } = useGroupManager({
|
|
37
|
-
* isOpen: groupManagerOpen
|
|
38
|
-
* })
|
|
39
|
-
*
|
|
40
|
-
* // Groups are automatically loaded when isOpen becomes true
|
|
41
|
-
* // Save groups
|
|
42
|
-
* await handleSaveGroups(updatedGroups)
|
|
43
|
-
*
|
|
44
|
-
* // Search users
|
|
45
|
-
* const users = await handleSearchUsers('john', 10)
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
export function useGroupManager(options: UseGroupManagerOptions): UseGroupManagerReturn {
|
|
49
|
-
const apiClient = useApiClient()
|
|
50
|
-
const [groupsData, setGroupsData] = useState<InternalGroup[]>([])
|
|
51
|
-
const [groupsLoading, setGroupsLoading] = useState(false)
|
|
52
|
-
|
|
53
|
-
const loadGroups = async () => {
|
|
54
|
-
setGroupsLoading(true)
|
|
55
|
-
try {
|
|
56
|
-
const result = await apiClient.groups.getInternal()
|
|
57
|
-
if (!result.ok) throw new Error('Failed to load groups')
|
|
58
|
-
setGroupsData(result.data?.groups ?? [])
|
|
59
|
-
} catch (err) {
|
|
60
|
-
console.error('Failed to load groups:', err)
|
|
61
|
-
notifications.show({ message: 'Failed to load groups', color: 'red' })
|
|
62
|
-
} finally {
|
|
63
|
-
setGroupsLoading(false)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const handleSaveGroups = useCallback(async (groups: InternalGroup[]) => {
|
|
68
|
-
try {
|
|
69
|
-
const result = await apiClient.groups.updateInternal({ groups })
|
|
70
|
-
if (!result.ok) {
|
|
71
|
-
throw new Error(result.error || 'Failed to save groups')
|
|
72
|
-
}
|
|
73
|
-
notifications.show({
|
|
74
|
-
title: 'Groups Saved',
|
|
75
|
-
message: 'Internal groups have been updated',
|
|
76
|
-
color: 'green',
|
|
77
|
-
})
|
|
78
|
-
await loadGroups()
|
|
79
|
-
} catch (err) {
|
|
80
|
-
const message = err instanceof Error ? err.message : 'Failed to save groups'
|
|
81
|
-
notifications.show({ message, color: 'red' })
|
|
82
|
-
throw err
|
|
83
|
-
}
|
|
84
|
-
}, [])
|
|
85
|
-
|
|
86
|
-
const handleSearchUsers = useCallback(async (query: string, limit?: number) => {
|
|
87
|
-
try {
|
|
88
|
-
const params: Record<string, string> = { q: query }
|
|
89
|
-
if (limit) {
|
|
90
|
-
params.limit = String(limit)
|
|
91
|
-
}
|
|
92
|
-
const result = await apiClient.permissions.searchUsers(params)
|
|
93
|
-
if (!result.ok) return []
|
|
94
|
-
return result.data?.users ?? []
|
|
95
|
-
} catch (err) {
|
|
96
|
-
console.error('User search failed:', err)
|
|
97
|
-
return []
|
|
98
|
-
}
|
|
99
|
-
}, [])
|
|
100
|
-
|
|
101
|
-
const handleGetUserMetadata = useCallback(async (userId: string) => {
|
|
102
|
-
try {
|
|
103
|
-
const result = await apiClient.permissions.getUserMetadata({ userId })
|
|
104
|
-
if (!result.ok) return null
|
|
105
|
-
return result.data?.user ?? null
|
|
106
|
-
} catch (err) {
|
|
107
|
-
console.error('Get user metadata failed:', err)
|
|
108
|
-
return null
|
|
109
|
-
}
|
|
110
|
-
}, [])
|
|
111
|
-
|
|
112
|
-
const handleSearchExternalGroups = useCallback(async (query: string) => {
|
|
113
|
-
try {
|
|
114
|
-
const result = await apiClient.groups.searchExternal({ q: query })
|
|
115
|
-
if (!result.ok) return []
|
|
116
|
-
return result.data?.groups ?? []
|
|
117
|
-
} catch (err) {
|
|
118
|
-
console.error('External group search failed:', err)
|
|
119
|
-
return []
|
|
120
|
-
}
|
|
121
|
-
}, [])
|
|
122
|
-
|
|
123
|
-
// Load groups when group manager opens
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (options.isOpen) {
|
|
126
|
-
loadGroups()
|
|
127
|
-
}
|
|
128
|
-
}, [options.isOpen])
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
groupsData,
|
|
132
|
-
groupsLoading,
|
|
133
|
-
handleSaveGroups,
|
|
134
|
-
handleSearchUsers,
|
|
135
|
-
handleGetUserMetadata,
|
|
136
|
-
handleSearchExternalGroups,
|
|
137
|
-
loadGroups,
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { renderHook, waitFor } from '@testing-library/react'
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { usePermissionManager } from './usePermissionManager'
|
|
4
|
-
import type { MockApiClient } from '../../api/__test__/mock-client'
|
|
5
|
-
import { setupMockApiClient, setupMockConsole, createApiClientWrapper } from './__test__/test-utils'
|
|
6
|
-
import { unsafeAsPermissionPath } from '../../authorization/test-utils'
|
|
7
|
-
|
|
8
|
-
// Mock the API client module
|
|
9
|
-
vi.mock('../../api', async () => {
|
|
10
|
-
const actual = await vi.importActual('../../api')
|
|
11
|
-
return {
|
|
12
|
-
...actual,
|
|
13
|
-
createApiClient: vi.fn(),
|
|
14
|
-
}
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
// Mock notifications
|
|
18
|
-
vi.mock('@mantine/notifications', () => ({
|
|
19
|
-
notifications: {
|
|
20
|
-
show: vi.fn(),
|
|
21
|
-
},
|
|
22
|
-
}))
|
|
23
|
-
|
|
24
|
-
describe('usePermissionManager', () => {
|
|
25
|
-
let mockClient: MockApiClient
|
|
26
|
-
let wrapper: ReturnType<typeof createApiClientWrapper>
|
|
27
|
-
|
|
28
|
-
beforeEach(async () => {
|
|
29
|
-
mockClient = await setupMockApiClient()
|
|
30
|
-
wrapper = createApiClientWrapper(mockClient)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
vi.clearAllMocks()
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('initializes with empty permissions', () => {
|
|
38
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: false }), { wrapper })
|
|
39
|
-
|
|
40
|
-
expect(result.current.permissionsData).toEqual([])
|
|
41
|
-
expect(result.current.permissionsLoading).toBe(false)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('loads permissions when isOpen becomes true', async () => {
|
|
45
|
-
const mockPermissions = [
|
|
46
|
-
{
|
|
47
|
-
path: unsafeAsPermissionPath('/content/pages'),
|
|
48
|
-
edit: { allowedGroups: ['editors'] },
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
path: unsafeAsPermissionPath('/content/posts'),
|
|
52
|
-
read: { allowedGroups: ['writers'] },
|
|
53
|
-
},
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
mockClient.permissions.get.mockResolvedValueOnce({
|
|
57
|
-
ok: true,
|
|
58
|
-
status: 200,
|
|
59
|
-
data: { permissions: mockPermissions },
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: true }), { wrapper })
|
|
63
|
-
|
|
64
|
-
// Should start loading
|
|
65
|
-
expect(result.current.permissionsLoading).toBe(true)
|
|
66
|
-
|
|
67
|
-
await waitFor(() => {
|
|
68
|
-
expect(result.current.permissionsLoading).toBe(false)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
expect(result.current.permissionsData).toEqual(mockPermissions)
|
|
72
|
-
expect(mockClient.permissions.get).toHaveBeenCalled()
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('handles load permissions error', async () => {
|
|
76
|
-
const { restore } = setupMockConsole(['error'])
|
|
77
|
-
mockClient.permissions.get.mockResolvedValueOnce({
|
|
78
|
-
ok: false,
|
|
79
|
-
status: 500,
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: true }), { wrapper })
|
|
83
|
-
|
|
84
|
-
await waitFor(() => {
|
|
85
|
-
expect(result.current.permissionsLoading).toBe(false)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
expect(result.current.permissionsData).toEqual([])
|
|
89
|
-
restore()
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('saves permissions successfully', async () => {
|
|
93
|
-
const mockPermissions = [
|
|
94
|
-
{
|
|
95
|
-
path: unsafeAsPermissionPath('/content/pages'),
|
|
96
|
-
edit: { allowedGroups: ['editors'] },
|
|
97
|
-
},
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
// Mock initial load
|
|
101
|
-
mockClient.permissions.get.mockResolvedValueOnce({
|
|
102
|
-
ok: true,
|
|
103
|
-
status: 200,
|
|
104
|
-
data: { permissions: [] },
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: true }), { wrapper })
|
|
108
|
-
|
|
109
|
-
await waitFor(() => {
|
|
110
|
-
expect(result.current.permissionsLoading).toBe(false)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// Mock save
|
|
114
|
-
mockClient.permissions.update.mockResolvedValueOnce({
|
|
115
|
-
ok: true,
|
|
116
|
-
status: 200,
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
// Mock reload after save
|
|
120
|
-
mockClient.permissions.get.mockResolvedValueOnce({
|
|
121
|
-
ok: true,
|
|
122
|
-
status: 200,
|
|
123
|
-
data: { permissions: mockPermissions },
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
await result.current.handleSavePermissions(mockPermissions)
|
|
127
|
-
|
|
128
|
-
await waitFor(() => {
|
|
129
|
-
expect(result.current.permissionsData).toEqual(mockPermissions)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
expect(mockClient.permissions.update).toHaveBeenCalledWith({
|
|
133
|
-
permissions: mockPermissions,
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('handles save permissions error', async () => {
|
|
138
|
-
mockClient.permissions.update.mockResolvedValueOnce({
|
|
139
|
-
ok: false,
|
|
140
|
-
status: 500,
|
|
141
|
-
error: 'Save failed',
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: false }), { wrapper })
|
|
145
|
-
|
|
146
|
-
await expect(result.current.handleSavePermissions([])).rejects.toThrow('Save failed')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('lists groups successfully', async () => {
|
|
150
|
-
const mockGroups = [
|
|
151
|
-
{ id: 'group1', name: 'Editors' },
|
|
152
|
-
{ id: 'group2', name: 'Writers' },
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
mockClient.permissions.listGroups.mockResolvedValueOnce({
|
|
156
|
-
ok: true,
|
|
157
|
-
status: 200,
|
|
158
|
-
data: { groups: mockGroups },
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: false }), { wrapper })
|
|
162
|
-
|
|
163
|
-
const groups = await result.current.handleListGroups()
|
|
164
|
-
|
|
165
|
-
expect(groups).toEqual(mockGroups)
|
|
166
|
-
expect(mockClient.permissions.listGroups).toHaveBeenCalled()
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('handles list groups error', async () => {
|
|
170
|
-
mockClient.permissions.listGroups.mockResolvedValueOnce({
|
|
171
|
-
ok: false,
|
|
172
|
-
status: 500,
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: false }), { wrapper })
|
|
176
|
-
|
|
177
|
-
const groups = await result.current.handleListGroups()
|
|
178
|
-
|
|
179
|
-
expect(groups).toEqual([])
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('does not load permissions when isOpen is false', async () => {
|
|
183
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: false }), { wrapper })
|
|
184
|
-
|
|
185
|
-
expect(result.current.permissionsLoading).toBe(false)
|
|
186
|
-
expect(mockClient.permissions.get).not.toHaveBeenCalled()
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('can manually reload permissions', async () => {
|
|
190
|
-
const mockPermissions = [
|
|
191
|
-
{
|
|
192
|
-
path: unsafeAsPermissionPath('/content/pages'),
|
|
193
|
-
edit: { allowedGroups: ['editors'] },
|
|
194
|
-
},
|
|
195
|
-
]
|
|
196
|
-
|
|
197
|
-
mockClient.permissions.get.mockResolvedValueOnce({
|
|
198
|
-
ok: true,
|
|
199
|
-
status: 200,
|
|
200
|
-
data: { permissions: mockPermissions },
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
const { result } = renderHook(() => usePermissionManager({ isOpen: false }), { wrapper })
|
|
204
|
-
|
|
205
|
-
await result.current.loadPermissions()
|
|
206
|
-
|
|
207
|
-
await waitFor(() => {
|
|
208
|
-
expect(result.current.permissionsData).toEqual(mockPermissions)
|
|
209
|
-
})
|
|
210
|
-
})
|
|
211
|
-
})
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { notifications } from '@mantine/notifications'
|
|
3
|
-
import type { PathPermission } from '../../config'
|
|
4
|
-
import type { GroupMetadata } from '../../auth/types'
|
|
5
|
-
import { useApiClient } from '../context'
|
|
6
|
-
|
|
7
|
-
export interface UsePermissionManagerOptions {
|
|
8
|
-
/**
|
|
9
|
-
* Whether the permission manager is currently open.
|
|
10
|
-
* Permissions are loaded when this becomes true.
|
|
11
|
-
*/
|
|
12
|
-
isOpen: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface UsePermissionManagerReturn {
|
|
16
|
-
permissionsData: PathPermission[]
|
|
17
|
-
permissionsLoading: boolean
|
|
18
|
-
handleSavePermissions: (permissions: PathPermission[]) => Promise<void>
|
|
19
|
-
handleListGroups: () => Promise<GroupMetadata[]>
|
|
20
|
-
loadPermissions: () => Promise<void>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Custom hook for managing path permissions (CRUD operations).
|
|
25
|
-
*
|
|
26
|
-
* Handles:
|
|
27
|
-
* - Loading permissions from API
|
|
28
|
-
* - Saving permissions to API
|
|
29
|
-
* - Listing groups for permission assignment
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```tsx
|
|
33
|
-
* const { permissionsData, permissionsLoading, handleSavePermissions, handleListGroups } = usePermissionManager({
|
|
34
|
-
* isOpen: permissionManagerOpen
|
|
35
|
-
* })
|
|
36
|
-
*
|
|
37
|
-
* // Permissions are automatically loaded when isOpen becomes true
|
|
38
|
-
* // Save permissions
|
|
39
|
-
* await handleSavePermissions(updatedPermissions)
|
|
40
|
-
*
|
|
41
|
-
* // List groups
|
|
42
|
-
* const groups = await handleListGroups()
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
export function usePermissionManager(
|
|
46
|
-
options: UsePermissionManagerOptions,
|
|
47
|
-
): UsePermissionManagerReturn {
|
|
48
|
-
const apiClient = useApiClient()
|
|
49
|
-
const [permissionsData, setPermissionsData] = useState<PathPermission[]>([])
|
|
50
|
-
const [permissionsLoading, setPermissionsLoading] = useState(false)
|
|
51
|
-
|
|
52
|
-
const loadPermissions = async () => {
|
|
53
|
-
setPermissionsLoading(true)
|
|
54
|
-
try {
|
|
55
|
-
const result = await apiClient.permissions.get()
|
|
56
|
-
if (!result.ok) throw new Error('Failed to load permissions')
|
|
57
|
-
setPermissionsData(result.data?.permissions ?? [])
|
|
58
|
-
} catch (err) {
|
|
59
|
-
console.error('Failed to load permissions:', err)
|
|
60
|
-
notifications.show({
|
|
61
|
-
message: 'Failed to load permissions',
|
|
62
|
-
color: 'red',
|
|
63
|
-
})
|
|
64
|
-
} finally {
|
|
65
|
-
setPermissionsLoading(false)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const handleSavePermissions = async (permissions: PathPermission[]) => {
|
|
70
|
-
try {
|
|
71
|
-
const result = await apiClient.permissions.update({ permissions })
|
|
72
|
-
if (!result.ok) {
|
|
73
|
-
throw new Error(result.error || 'Failed to save permissions')
|
|
74
|
-
}
|
|
75
|
-
notifications.show({
|
|
76
|
-
title: 'Permissions Saved',
|
|
77
|
-
message: 'Permissions have been updated',
|
|
78
|
-
color: 'green',
|
|
79
|
-
})
|
|
80
|
-
await loadPermissions()
|
|
81
|
-
} catch (err) {
|
|
82
|
-
const message = err instanceof Error ? err.message : 'Failed to save permissions'
|
|
83
|
-
notifications.show({ message, color: 'red' })
|
|
84
|
-
throw err
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const handleListGroups = async () => {
|
|
89
|
-
try {
|
|
90
|
-
const result = await apiClient.permissions.listGroups()
|
|
91
|
-
if (!result.ok) return []
|
|
92
|
-
return result.data?.groups ?? []
|
|
93
|
-
} catch (err) {
|
|
94
|
-
console.error('Group list failed:', err)
|
|
95
|
-
return []
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Load permissions when permission manager opens
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (options.isOpen) {
|
|
102
|
-
loadPermissions()
|
|
103
|
-
}
|
|
104
|
-
}, [options.isOpen])
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
permissionsData,
|
|
108
|
-
permissionsLoading,
|
|
109
|
-
handleSavePermissions,
|
|
110
|
-
handleListGroups,
|
|
111
|
-
loadPermissions,
|
|
112
|
-
}
|
|
113
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook for live preview reference resolution
|
|
3
|
-
*
|
|
4
|
-
* LIVE PREVIEW REFERENCE RESOLUTION
|
|
5
|
-
*
|
|
6
|
-
* Problem: The preview needs full referenced content (e.g., {name: "Alice", bio: "..."}),
|
|
7
|
-
* but the form only stores IDs (e.g., "5NVkkrB1MJUvnLqEDqDkRN").
|
|
8
|
-
*
|
|
9
|
-
* Solution: Synchronous resolution with background caching
|
|
10
|
-
*
|
|
11
|
-
* 1. SYNCHRONOUS PHASE (useMemo):
|
|
12
|
-
* - Compute resolvedValue by applying cached data to form value
|
|
13
|
-
* - If reference ID is in cache, use full object; otherwise keep ID
|
|
14
|
-
* - Runs during render, so no async gaps or race conditions
|
|
15
|
-
* - Preview always gets complete, valid data
|
|
16
|
-
*
|
|
17
|
-
* 2. BACKGROUND PHASE (useEffect):
|
|
18
|
-
* - Find IDs not in cache
|
|
19
|
-
* - After 300ms debounce, fetch from API
|
|
20
|
-
* - Update cache with resolved data
|
|
21
|
-
* - Trigger useMemo re-run via resolutionTrigger
|
|
22
|
-
* - Preview updates again with full data
|
|
23
|
-
*
|
|
24
|
-
* This two-phase approach eliminates race conditions that occurred with async state,
|
|
25
|
-
* where form data and resolved data could get out of sync during transitions
|
|
26
|
-
* (e.g., "Discard All Drafts" was passing empty objects to preview).
|
|
27
|
-
*
|
|
28
|
-
* Cache structure: Map<string, unknown> with keys like "main:5NVkkrB1MJUvnLqEDqDkRN"
|
|
29
|
-
* - Branch-scoped to prevent stale cross-branch data
|
|
30
|
-
* - Cleared on branch change
|
|
31
|
-
* - Persists across edits for instant re-renders
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
35
|
-
import type { EntrySchema } from '../../config'
|
|
36
|
-
import { resolveChangedReferences } from '../client-reference-resolver'
|
|
37
|
-
|
|
38
|
-
export type FormValue = Record<string, unknown>
|
|
39
|
-
|
|
40
|
-
export interface UseReferenceResolutionOptions {
|
|
41
|
-
value: FormValue
|
|
42
|
-
fields: EntrySchema
|
|
43
|
-
branch: string
|
|
44
|
-
onResolvedValueChange?: (resolved: FormValue) => void
|
|
45
|
-
onLoadingStateChange?: (loadingState: FormValue) => void
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface UseReferenceResolutionResult {
|
|
49
|
-
resolvedValue: FormValue
|
|
50
|
-
loadingState: FormValue
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function useReferenceResolution({
|
|
54
|
-
value,
|
|
55
|
-
fields,
|
|
56
|
-
branch,
|
|
57
|
-
onResolvedValueChange,
|
|
58
|
-
onLoadingStateChange,
|
|
59
|
-
}: UseReferenceResolutionOptions): UseReferenceResolutionResult {
|
|
60
|
-
const resolvedCache = useRef<Map<string, unknown>>(new Map())
|
|
61
|
-
const prevValueRef = useRef<FormValue>({}) // Track previous value for change detection
|
|
62
|
-
const lastNotifiedValueRef = useRef<string>('') // Track last notified value to prevent infinite loops
|
|
63
|
-
const [resolutionTrigger, setResolutionTrigger] = useState(0) // Trigger to force useMemo re-computation
|
|
64
|
-
|
|
65
|
-
// Map field names to their types for fast lookup
|
|
66
|
-
const referenceFieldNames = useMemo(() => {
|
|
67
|
-
const names = new Set<string>()
|
|
68
|
-
for (const field of fields) {
|
|
69
|
-
if (field.type === 'reference') {
|
|
70
|
-
names.add(field.name)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return names
|
|
74
|
-
}, [fields])
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* PHASE 1: SYNCHRONOUS RESOLUTION
|
|
78
|
-
*
|
|
79
|
-
* Compute resolved value by applying cached reference data to form value.
|
|
80
|
-
* This runs synchronously during render (useMemo), so there are no async gaps.
|
|
81
|
-
*
|
|
82
|
-
* For each reference field:
|
|
83
|
-
* - If ID is in cache: substitute full object
|
|
84
|
-
* - If ID not in cache: keep the ID (loading state)
|
|
85
|
-
*
|
|
86
|
-
* Dependencies include resolutionTrigger, which is incremented when cache updates,
|
|
87
|
-
* forcing this to re-run and pick up newly-resolved data.
|
|
88
|
-
*/
|
|
89
|
-
const resolvedValue = useMemo(() => {
|
|
90
|
-
const result = { ...value }
|
|
91
|
-
|
|
92
|
-
// Synchronously apply cached resolutions
|
|
93
|
-
for (const fieldName of referenceFieldNames) {
|
|
94
|
-
const fieldValue = value[fieldName]
|
|
95
|
-
if (fieldValue) {
|
|
96
|
-
if (Array.isArray(fieldValue)) {
|
|
97
|
-
// List of references
|
|
98
|
-
result[fieldName] = fieldValue.map((id) => {
|
|
99
|
-
if (typeof id === 'string') {
|
|
100
|
-
const cached = resolvedCache.current.get(`${branch}:${id}`)
|
|
101
|
-
// Return cached object, or null if not yet resolved
|
|
102
|
-
return cached || null
|
|
103
|
-
}
|
|
104
|
-
return id
|
|
105
|
-
})
|
|
106
|
-
} else if (typeof fieldValue === 'string') {
|
|
107
|
-
// Single reference
|
|
108
|
-
const cached = resolvedCache.current.get(`${branch}:${fieldValue}`)
|
|
109
|
-
// Return cached object, or null if not yet resolved
|
|
110
|
-
result[fieldName] = cached || null
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return result
|
|
116
|
-
}, [value, branch, resolutionTrigger, referenceFieldNames])
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Compute loading state that mirrors the data structure.
|
|
120
|
-
* For each reference field, track if it's currently loading (not in cache).
|
|
121
|
-
*/
|
|
122
|
-
const loadingState = useMemo(() => {
|
|
123
|
-
const result: FormValue = {}
|
|
124
|
-
|
|
125
|
-
for (const fieldName of referenceFieldNames) {
|
|
126
|
-
const fieldValue = value[fieldName]
|
|
127
|
-
if (fieldValue) {
|
|
128
|
-
if (Array.isArray(fieldValue)) {
|
|
129
|
-
// List of references - return array of booleans
|
|
130
|
-
result[fieldName] = fieldValue.map((id) => {
|
|
131
|
-
if (typeof id === 'string') {
|
|
132
|
-
return !resolvedCache.current.has(`${branch}:${id}`)
|
|
133
|
-
}
|
|
134
|
-
return false
|
|
135
|
-
})
|
|
136
|
-
} else if (typeof fieldValue === 'string') {
|
|
137
|
-
// Single reference - return boolean
|
|
138
|
-
result[fieldName] = !resolvedCache.current.has(`${branch}:${fieldValue}`)
|
|
139
|
-
} else {
|
|
140
|
-
result[fieldName] = false
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
result[fieldName] = false
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return result
|
|
148
|
-
}, [value, branch, resolutionTrigger, referenceFieldNames])
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* PHASE 2: BACKGROUND ASYNC RESOLUTION
|
|
152
|
-
*
|
|
153
|
-
* Find reference IDs that aren't in cache yet and fetch them from the API.
|
|
154
|
-
*/
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
// Find all uncached reference IDs
|
|
157
|
-
const uncachedIds = new Set<string>()
|
|
158
|
-
|
|
159
|
-
for (const fieldName of referenceFieldNames) {
|
|
160
|
-
const fieldValue = value[fieldName]
|
|
161
|
-
if (fieldValue) {
|
|
162
|
-
const ids = Array.isArray(fieldValue) ? fieldValue : [fieldValue]
|
|
163
|
-
for (const id of ids) {
|
|
164
|
-
if (typeof id === 'string' && !resolvedCache.current.has(`${branch}:${id}`)) {
|
|
165
|
-
uncachedIds.add(id)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (uncachedIds.size === 0) {
|
|
172
|
-
prevValueRef.current = value
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Debounce API calls to batch multiple rapid changes
|
|
177
|
-
const timeout = setTimeout(async () => {
|
|
178
|
-
try {
|
|
179
|
-
// Resolve uncached IDs via API
|
|
180
|
-
const updates = await resolveChangedReferences(
|
|
181
|
-
prevValueRef.current,
|
|
182
|
-
value,
|
|
183
|
-
fields,
|
|
184
|
-
branch,
|
|
185
|
-
resolvedCache.current,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
// Update cache with resolved values
|
|
189
|
-
for (const [fieldName, resolvedFieldValue] of Object.entries(updates)) {
|
|
190
|
-
if (Array.isArray(resolvedFieldValue)) {
|
|
191
|
-
resolvedFieldValue.forEach((obj, idx) => {
|
|
192
|
-
const fieldValue = value[fieldName]
|
|
193
|
-
if (Array.isArray(fieldValue)) {
|
|
194
|
-
const id = fieldValue[idx]
|
|
195
|
-
if (typeof obj === 'object' && obj !== null && typeof id === 'string') {
|
|
196
|
-
resolvedCache.current.set(`${branch}:${id}`, obj)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
})
|
|
200
|
-
} else if (typeof resolvedFieldValue === 'object' && resolvedFieldValue !== null) {
|
|
201
|
-
const id = value[fieldName] as string
|
|
202
|
-
if (typeof id === 'string') {
|
|
203
|
-
resolvedCache.current.set(`${branch}:${id}`, resolvedFieldValue)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Trigger useMemo re-computation
|
|
209
|
-
setResolutionTrigger((prev) => prev + 1)
|
|
210
|
-
prevValueRef.current = value
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error('Reference resolution failed:', error)
|
|
213
|
-
}
|
|
214
|
-
}, 300) // 300ms debounce
|
|
215
|
-
|
|
216
|
-
return () => clearTimeout(timeout)
|
|
217
|
-
}, [value, fields, branch, referenceFieldNames])
|
|
218
|
-
|
|
219
|
-
// Clear cache when branch changes
|
|
220
|
-
useEffect(() => {
|
|
221
|
-
resolvedCache.current.clear()
|
|
222
|
-
setResolutionTrigger((prev) => prev + 1) // Trigger re-computation with empty cache
|
|
223
|
-
}, [branch])
|
|
224
|
-
|
|
225
|
-
// Notify parent of resolved value changes (with infinite loop prevention)
|
|
226
|
-
useEffect(() => {
|
|
227
|
-
const serialized = JSON.stringify(resolvedValue)
|
|
228
|
-
if (serialized !== lastNotifiedValueRef.current) {
|
|
229
|
-
lastNotifiedValueRef.current = serialized
|
|
230
|
-
onResolvedValueChange?.(resolvedValue)
|
|
231
|
-
}
|
|
232
|
-
}, [resolvedValue, onResolvedValueChange])
|
|
233
|
-
|
|
234
|
-
// Notify parent of loading state changes
|
|
235
|
-
const lastNotifiedLoadingRef = useRef<string>('')
|
|
236
|
-
useEffect(() => {
|
|
237
|
-
const serialized = JSON.stringify(loadingState)
|
|
238
|
-
if (serialized !== lastNotifiedLoadingRef.current) {
|
|
239
|
-
lastNotifiedLoadingRef.current = serialized
|
|
240
|
-
onLoadingStateChange?.(loadingState)
|
|
241
|
-
}
|
|
242
|
-
}, [loadingState, onLoadingStateChange])
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
resolvedValue,
|
|
246
|
-
loadingState,
|
|
247
|
-
}
|
|
248
|
-
}
|