@pantheon-systems/puck-css 0.12.0 → 0.14.0
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/README.md +1 -1
- package/dist/agent/components/AgentActionButton.js +0 -13
- package/dist/agent/components/AgentActionButton.js.map +1 -1
- package/dist/agent/components/AgentActionModal.js +0 -13
- package/dist/agent/components/AgentActionModal.js.map +1 -1
- package/dist/agent/components/AgentStatusPanel.js +0 -10
- package/dist/agent/components/AgentStatusPanel.js.map +1 -1
- package/dist/agent/components/index.js +0 -5
- package/dist/agent/components/index.js.map +1 -1
- package/dist/agent/useAgentEdit.js +0 -12
- package/dist/agent/useAgentEdit.js.map +1 -1
- package/dist/agent/useAgentTrigger.js +0 -29
- package/dist/agent/useAgentTrigger.js.map +1 -1
- package/dist/auth/P1AuthProvider.d.ts +9 -2
- package/dist/auth/P1AuthProvider.d.ts.map +1 -1
- package/dist/auth/P1AuthProvider.js +21 -44
- package/dist/auth/P1AuthProvider.js.map +1 -1
- package/dist/auth/P1LoginPage.js +0 -18
- package/dist/auth/P1LoginPage.js.map +1 -1
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -9
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/logout.d.ts +23 -0
- package/dist/auth/logout.d.ts.map +1 -0
- package/dist/auth/logout.js +14 -0
- package/dist/auth/logout.js.map +1 -0
- package/dist/auth/storage-keys.d.ts +5 -0
- package/dist/auth/storage-keys.d.ts.map +1 -0
- package/dist/auth/storage-keys.js +3 -0
- package/dist/auth/storage-keys.js.map +1 -0
- package/dist/collaboration/components/AgentActivityBanner.js +0 -15
- package/dist/collaboration/components/AgentActivityBanner.js.map +1 -1
- package/dist/collaboration/components/CollaboratorAvatars.js +0 -17
- package/dist/collaboration/components/CollaboratorAvatars.js.map +1 -1
- package/dist/collaboration/components/FocusRegionHighlight.js +0 -9
- package/dist/collaboration/components/FocusRegionHighlight.js.map +1 -1
- package/dist/collaboration/components/PresenceIndicator.js +0 -9
- package/dist/collaboration/components/PresenceIndicator.js.map +1 -1
- package/dist/collaboration/useBranchPresence.js +0 -36
- package/dist/collaboration/useBranchPresence.js.map +1 -1
- package/dist/collaboration/useFocusRegionReporting.js +0 -75
- package/dist/collaboration/useFocusRegionReporting.js.map +1 -1
- package/dist/collaboration/usePresence.js +0 -37
- package/dist/collaboration/usePresence.js.map +1 -1
- package/dist/collaboration/useSitePresence.js +0 -36
- package/dist/collaboration/useSitePresence.js.map +1 -1
- package/dist/collaboration/utils/avatarColor.js +0 -3
- package/dist/collaboration/utils/avatarColor.js.map +1 -1
- package/dist/collaboration/utils/focusHighlightConfig.js +0 -61
- package/dist/collaboration/utils/focusHighlightConfig.js.map +1 -1
- package/dist/collaboration/utils/focusRegionMap.js +1 -80
- package/dist/collaboration/utils/focusRegionMap.js.map +1 -1
- package/dist/collaboration/utils/initials.js +0 -14
- package/dist/collaboration/utils/initials.js.map +1 -1
- package/dist/collaboration/utils/presenceIdentity.js +0 -26
- package/dist/collaboration/utils/presenceIdentity.js.map +1 -1
- package/dist/core/FocusHighlightContext.js +0 -31
- package/dist/core/FocusHighlightContext.js.map +1 -1
- package/dist/core/NotificationContext.js +2 -67
- package/dist/core/NotificationContext.js.map +1 -1
- package/dist/core/P1PuckContext.js +0 -31
- package/dist/core/P1PuckContext.js.map +1 -1
- package/dist/core/PresenceContext.js +0 -18
- package/dist/core/PresenceContext.js.map +1 -1
- package/dist/core/config.js +0 -11
- package/dist/core/config.js.map +1 -1
- package/dist/core/featureConfig.js +0 -32
- package/dist/core/featureConfig.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/plugin-types.d.ts +9 -0
- package/dist/core/plugin-types.d.ts.map +1 -1
- package/dist/core/types.d.ts +7 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils/debounce.js +0 -18
- package/dist/core/utils/debounce.js.map +1 -1
- package/dist/core/utils/requestError.js +0 -5
- package/dist/core/utils/requestError.js.map +1 -1
- package/dist/core/utils/retry.js +0 -31
- package/dist/core/utils/retry.js.map +1 -1
- package/dist/core/utils/throttle.js +0 -24
- package/dist/core/utils/throttle.js.map +1 -1
- package/dist/data/cross-reference-resolve.js +0 -6
- package/dist/data/cross-reference-resolve.js.map +1 -1
- package/dist/data/cross-reference.js +0 -15
- package/dist/data/cross-reference.js.map +1 -1
- package/dist/data/css-queries/css-query-registry.d.ts +2 -2
- package/dist/data/css-queries/css-query-registry.js +1 -1
- package/dist/data/css-queries/css-query-registry.js.map +1 -1
- package/dist/data/dal/index.js +0 -21
- package/dist/data/dal/index.js.map +1 -1
- package/dist/data/dal/init.js +0 -40
- package/dist/data/dal/init.js.map +1 -1
- package/dist/data/dal/p1-store.d.ts.map +1 -1
- package/dist/data/dal/p1-store.js +11 -41
- package/dist/data/dal/p1-store.js.map +1 -1
- package/dist/data/data-list-block/builtin-components/cards.d.ts +1 -0
- package/dist/data/data-list-block/builtin-components/cards.d.ts.map +1 -1
- package/dist/data/data-list-block/builtin-components/cards.js +16 -16
- package/dist/data/data-list-block/builtin-components/cards.js.map +1 -1
- package/dist/data/data-list-block/builtin-components/listing.d.ts +1 -0
- package/dist/data/data-list-block/builtin-components/listing.d.ts.map +1 -1
- package/dist/data/data-list-block/builtin-components/listing.js +4 -3
- package/dist/data/data-list-block/builtin-components/listing.js.map +1 -1
- package/dist/data/data-list-block/builtin-components/rows.d.ts +1 -0
- package/dist/data/data-list-block/builtin-components/rows.d.ts.map +1 -1
- package/dist/data/data-list-block/builtin-components/rows.js +3 -3
- package/dist/data/data-list-block/builtin-components/rows.js.map +1 -1
- package/dist/data/data-list-block/create-data-list-block.d.ts +1 -0
- package/dist/data/data-list-block/create-data-list-block.d.ts.map +1 -1
- package/dist/data/data-list-block/create-data-list-block.js +7 -11
- package/dist/data/data-list-block/create-data-list-block.js.map +1 -1
- package/dist/data/data-list-block/data-list.css +314 -0
- package/dist/data/data-list-block/utils.js +0 -1
- package/dist/data/data-list-block/utils.js.map +1 -1
- package/dist/data/fields/pds-field-helpers.js +1 -4
- package/dist/data/fields/pds-field-helpers.js.map +1 -1
- package/dist/data/fields/visible-when.js +0 -5
- package/dist/data/fields/visible-when.js.map +1 -1
- package/dist/data/fields.d.ts.map +1 -1
- package/dist/data/fields.js +7 -14
- package/dist/data/fields.js.map +1 -1
- package/dist/data/get-page.js +0 -1
- package/dist/data/get-page.js.map +1 -1
- package/dist/data/page-editor-meta.js +0 -7
- package/dist/data/page-editor-meta.js.map +1 -1
- package/dist/data/page-store.js +0 -32
- package/dist/data/page-store.js.map +1 -1
- package/dist/data/page-structure.js +0 -3
- package/dist/data/page-structure.js.map +1 -1
- package/dist/data/paths.d.ts +6 -0
- package/dist/data/paths.d.ts.map +1 -1
- package/dist/data/paths.js +3 -16
- package/dist/data/paths.js.map +1 -1
- package/dist/data/query-provider.d.ts +14 -0
- package/dist/data/query-provider.d.ts.map +1 -1
- package/dist/data/query-provider.js +20 -9
- package/dist/data/query-provider.js.map +1 -1
- package/dist/data/remote-datasources/fetch-http-json.js +0 -5
- package/dist/data/remote-datasources/fetch-http-json.js.map +1 -1
- package/dist/data/remote-datasources/loader.js +0 -21
- package/dist/data/remote-datasources/loader.js.map +1 -1
- package/dist/data/remote-datasources/remote-datasource-registry.js +0 -4
- package/dist/data/remote-datasources/remote-datasource-registry.js.map +1 -1
- package/dist/data/resolve-data-templates.js +0 -24
- package/dist/data/resolve-data-templates.js.map +1 -1
- package/dist/data/route-templates.js +0 -29
- package/dist/data/route-templates.js.map +1 -1
- package/dist/data/semantic-ops.js +0 -11
- package/dist/data/semantic-ops.js.map +1 -1
- package/dist/data/styles.js +0 -2
- package/dist/data/styles.js.map +1 -1
- package/dist/data/template-autocomplete.js +0 -8
- package/dist/data/template-autocomplete.js.map +1 -1
- package/dist/data/utils.d.ts +16 -0
- package/dist/data/utils.d.ts.map +1 -1
- package/dist/data/utils.js +21 -1
- package/dist/data/utils.js.map +1 -1
- package/dist/editor/P1App.d.ts.map +1 -1
- package/dist/editor/P1App.js +6 -45
- package/dist/editor/P1App.js.map +1 -1
- package/dist/editor/P1PuckProvider.d.ts +1 -1
- package/dist/editor/P1PuckProvider.d.ts.map +1 -1
- package/dist/editor/P1PuckProvider.js +54 -561
- package/dist/editor/P1PuckProvider.js.map +1 -1
- package/dist/editor/aiPanelStore.js +0 -7
- package/dist/editor/aiPanelStore.js.map +1 -1
- package/dist/editor/components/BranchSelector.js +0 -21
- package/dist/editor/components/BranchSelector.js.map +1 -1
- package/dist/editor/components/CollapsibleSectionHeader.js +1 -4
- package/dist/editor/components/CollapsibleSectionHeader.js.map +1 -1
- package/dist/editor/components/DraggableRow.js +0 -1
- package/dist/editor/components/DraggableRow.js.map +1 -1
- package/dist/editor/components/EditorReloadOverlay.d.ts +16 -0
- package/dist/editor/components/EditorReloadOverlay.d.ts.map +1 -0
- package/dist/editor/components/EditorReloadOverlay.js +13 -0
- package/dist/editor/components/EditorReloadOverlay.js.map +1 -0
- package/dist/editor/components/NotificationContainer.js +0 -20
- package/dist/editor/components/NotificationContainer.js.map +1 -1
- package/dist/editor/components/OutlinePanel.js +1 -12
- package/dist/editor/components/OutlinePanel.js.map +1 -1
- package/dist/editor/components/P1InspectorFields.d.ts.map +1 -1
- package/dist/editor/components/P1InspectorFields.js +12 -14
- package/dist/editor/components/P1InspectorFields.js.map +1 -1
- package/dist/editor/components/P1TemplateFields.js +6 -12
- package/dist/editor/components/P1TemplateFields.js.map +1 -1
- package/dist/editor/components/PanelHeader.d.ts.map +1 -1
- package/dist/editor/components/PanelHeader.js +3 -2
- package/dist/editor/components/PanelHeader.js.map +1 -1
- package/dist/editor/components/PublishButton.js +0 -21
- package/dist/editor/components/PublishButton.js.map +1 -1
- package/dist/editor/components/PuckDataCapture.js +0 -14
- package/dist/editor/components/PuckDataCapture.js.map +1 -1
- package/dist/editor/components/PuckDataSynchronizer.js +0 -67
- package/dist/editor/components/PuckDataSynchronizer.js.map +1 -1
- package/dist/editor/components/PuckSelectionTracker.js +0 -62
- package/dist/editor/components/PuckSelectionTracker.js.map +1 -1
- package/dist/editor/components/SaveIndicator.js +0 -41
- package/dist/editor/components/SaveIndicator.js.map +1 -1
- package/dist/editor/components/Toast.js +1 -32
- package/dist/editor/components/Toast.js.map +1 -1
- package/dist/editor/components/VersionBannerOverride.d.ts +7 -1
- package/dist/editor/components/VersionBannerOverride.d.ts.map +1 -1
- package/dist/editor/components/VersionBannerOverride.js +17 -24
- package/dist/editor/components/VersionBannerOverride.js.map +1 -1
- package/dist/editor/components/collapsibleSectionContext.js +0 -10
- package/dist/editor/components/collapsibleSectionContext.js.map +1 -1
- package/dist/editor/components/componentIconName.js +0 -23
- package/dist/editor/components/componentIconName.js.map +1 -1
- package/dist/editor/components/connectable.js +0 -5
- package/dist/editor/components/connectable.js.map +1 -1
- package/dist/editor/components/merge-preview/MergePreviewRenderer.js +2 -74
- package/dist/editor/components/merge-preview/MergePreviewRenderer.js.map +1 -1
- package/dist/editor/components/merge-preview/ScaledContent.js +0 -6
- package/dist/editor/components/merge-preview/ScaledContent.js.map +1 -1
- package/dist/editor/components/merge-preview/index.js +0 -5
- package/dist/editor/components/merge-preview/index.js.map +1 -1
- package/dist/editor/components/outlineTree.js +0 -31
- package/dist/editor/components/outlineTree.js.map +1 -1
- package/dist/editor/components/useDragReorder.js +0 -1
- package/dist/editor/components/useDragReorder.js.map +1 -1
- package/dist/editor/composePlugins.d.ts +9 -3
- package/dist/editor/composePlugins.d.ts.map +1 -1
- package/dist/editor/composePlugins.js +3 -8
- package/dist/editor/composePlugins.js.map +1 -1
- package/dist/editor/defaultPlugins.d.ts +1 -1
- package/dist/editor/defaultPlugins.d.ts.map +1 -1
- package/dist/editor/defaultPlugins.js +3 -1
- package/dist/editor/defaultPlugins.js.map +1 -1
- package/dist/editor/freshFieldTypes.d.ts +12 -0
- package/dist/editor/freshFieldTypes.d.ts.map +1 -0
- package/dist/editor/freshFieldTypes.js +11 -0
- package/dist/editor/freshFieldTypes.js.map +1 -0
- package/dist/editor/logout-store.d.ts +13 -0
- package/dist/editor/logout-store.d.ts.map +1 -0
- package/dist/editor/logout-store.js +23 -0
- package/dist/editor/logout-store.js.map +1 -0
- package/dist/editor/plugin/P1Plugin.d.ts +16 -3
- package/dist/editor/plugin/P1Plugin.d.ts.map +1 -1
- package/dist/editor/plugin/P1Plugin.js +21 -171
- package/dist/editor/plugin/P1Plugin.js.map +1 -1
- package/dist/editor/plugin/context-sync-key.js +0 -19
- package/dist/editor/plugin/context-sync-key.js.map +1 -1
- package/dist/editor/plugin/createP1Overrides.js +1 -49
- package/dist/editor/plugin/createP1Overrides.js.map +1 -1
- package/dist/editor/plugin/document-sync-plugin.js +0 -50
- package/dist/editor/plugin/document-sync-plugin.js.map +1 -1
- package/dist/editor/plugin/fieldGuidance.js +0 -5
- package/dist/editor/plugin/fieldGuidance.js.map +1 -1
- package/dist/editor/plugin/index.js +0 -6
- package/dist/editor/plugin/index.js.map +1 -1
- package/dist/editor/plugin/selectHeaderCollaborators.js +0 -8
- package/dist/editor/plugin/selectHeaderCollaborators.js.map +1 -1
- package/dist/editor/presets.js +2 -2
- package/dist/editor/stablePluginDeps.d.ts +9 -0
- package/dist/editor/stablePluginDeps.d.ts.map +1 -0
- package/dist/editor/stablePluginDeps.js +15 -0
- package/dist/editor/stablePluginDeps.js.map +1 -0
- package/dist/editor/thumbnails/LiveThumbnail.js +1 -49
- package/dist/editor/thumbnails/LiveThumbnail.js.map +1 -1
- package/dist/editor/thumbnails/ThumbnailCard.js +0 -25
- package/dist/editor/thumbnails/ThumbnailCard.js.map +1 -1
- package/dist/editor/thumbnails/buildLiveThumbnailDrawer.js +0 -21
- package/dist/editor/thumbnails/buildLiveThumbnailDrawer.js.map +1 -1
- package/dist/editor/thumbnails/humanizeComponentName.js +0 -17
- package/dist/editor/thumbnails/humanizeComponentName.js.map +1 -1
- package/dist/editor/thumbnails/resolveLiveThumbnailDrawer.js +0 -11
- package/dist/editor/thumbnails/resolveLiveThumbnailDrawer.js.map +1 -1
- package/dist/editor/thumbnails/thumbnailCache.js +0 -38
- package/dist/editor/thumbnails/thumbnailCache.js.map +1 -1
- package/dist/editor/useAutoSave.js +0 -32
- package/dist/editor/useAutoSave.js.map +1 -1
- package/dist/editor/useBranches.js +0 -30
- package/dist/editor/useBranches.js.map +1 -1
- package/dist/editor/useComponentRegistry.d.ts +1 -1
- package/dist/editor/useComponentRegistry.js +0 -28
- package/dist/editor/useComponentRegistry.js.map +1 -1
- package/dist/editor/useDocuments.js +0 -51
- package/dist/editor/useDocuments.js.map +1 -1
- package/dist/editor/useMergePreview.js +8 -12
- package/dist/editor/useMergePreview.js.map +1 -1
- package/dist/editor/useP1Editor.d.ts +35 -7
- package/dist/editor/useP1Editor.d.ts.map +1 -1
- package/dist/editor/useP1Editor.js +146 -243
- package/dist/editor/useP1Editor.js.map +1 -1
- package/dist/editor/useP1Overrides.js +15 -50
- package/dist/editor/useP1Overrides.js.map +1 -1
- package/dist/editor/useP1Plugin.d.ts +10 -4
- package/dist/editor/useP1Plugin.d.ts.map +1 -1
- package/dist/editor/useP1Plugin.js +34 -56
- package/dist/editor/useP1Plugin.js.map +1 -1
- package/dist/editor/useRealtime.d.ts +15 -3
- package/dist/editor/useRealtime.d.ts.map +1 -1
- package/dist/editor/useRealtime.js +12 -91
- package/dist/editor/useRealtime.js.map +1 -1
- package/dist/editor/useResponsivePanels.js +2 -35
- package/dist/editor/useResponsivePanels.js.map +1 -1
- package/dist/editor/useSidebarScrollPreservation.js +0 -14
- package/dist/editor/useSidebarScrollPreservation.js.map +1 -1
- package/dist/editor/utils/buildThumbnailOverride.js +0 -9
- package/dist/editor/utils/buildThumbnailOverride.js.map +1 -1
- package/dist/editor/utils/componentRegistry.d.ts +6 -11
- package/dist/editor/utils/componentRegistry.d.ts.map +1 -1
- package/dist/editor/utils/componentRegistry.js +1 -71
- package/dist/editor/utils/componentRegistry.js.map +1 -1
- package/dist/editor/utils/createStablePluginArray.js +0 -27
- package/dist/editor/utils/createStablePluginArray.js.map +1 -1
- package/dist/editor/utils/path.js +0 -6
- package/dist/editor/utils/path.js.map +1 -1
- package/dist/editor/utils/puckYjsBinding.d.ts +1 -1
- package/dist/editor/utils/puckYjsBinding.js +0 -99
- package/dist/editor/utils/puckYjsBinding.js.map +1 -1
- package/dist/editor/utils/restoreDocumentVersion.js +0 -12
- package/dist/editor/utils/restoreDocumentVersion.js.map +1 -1
- package/dist/editor/utils/sanitizeRichtextDefaults.js +0 -26
- package/dist/editor/utils/sanitizeRichtextDefaults.js.map +1 -1
- package/dist/editor/utils/snapshotToPuckData.js +0 -8
- package/dist/editor/utils/snapshotToPuckData.js.map +1 -1
- package/dist/editor/utils/syncComponentRegistry.d.ts +2 -2
- package/dist/editor/utils/syncComponentRegistry.js +1 -76
- package/dist/editor/utils/syncComponentRegistry.js.map +1 -1
- package/dist/editor/utils/syncComponentRegistryWriteOnly.d.ts +7 -7
- package/dist/editor/utils/syncComponentRegistryWriteOnly.js +0 -22
- package/dist/editor/utils/syncComponentRegistryWriteOnly.js.map +1 -1
- package/dist/editor/utils/templatePath.js +0 -5
- package/dist/editor/utils/templatePath.js.map +1 -1
- package/dist/features/content-type-templates/editor/useTemplatePermissions.js +0 -3
- package/dist/features/content-type-templates/editor/useTemplatePermissions.js.map +1 -1
- package/dist/features/content-type-templates/hooks/useTemplateList.js +0 -30
- package/dist/features/content-type-templates/hooks/useTemplateList.js.map +1 -1
- package/dist/features/content-type-templates/index.js +0 -8
- package/dist/features/content-type-templates/index.js.map +1 -1
- package/dist/features/content-type-templates/permissions/createPuckPermissions.js +1 -43
- package/dist/features/content-type-templates/permissions/createPuckPermissions.js.map +1 -1
- package/dist/features/content-type-templates/permissions/role-permissions.d.ts +8 -0
- package/dist/features/content-type-templates/permissions/role-permissions.d.ts.map +1 -1
- package/dist/features/content-type-templates/permissions/role-permissions.js +3 -29
- package/dist/features/content-type-templates/permissions/role-permissions.js.map +1 -1
- package/dist/features/content-type-templates/permissions/useContentRole.js +0 -24
- package/dist/features/content-type-templates/permissions/useContentRole.js.map +1 -1
- package/dist/features/content-type-templates/permissions/useResolveContentRole.d.ts +5 -5
- package/dist/features/content-type-templates/permissions/useResolveContentRole.js +2 -22
- package/dist/features/content-type-templates/permissions/useResolveContentRole.js.map +1 -1
- package/dist/features/content-type-templates/stores/index.js +0 -5
- package/dist/features/content-type-templates/stores/index.js.map +1 -1
- package/dist/features/content-type-templates/stores/template-store-api.js +0 -14
- package/dist/features/content-type-templates/stores/template-store-api.js.map +1 -1
- package/dist/features/content-type-templates/stores/template-store.d.ts +1 -1
- package/dist/features/content-type-templates/stores/template-store.js +0 -12
- package/dist/features/content-type-templates/stores/template-store.js.map +1 -1
- package/dist/features/content-type-templates/types.js +0 -5
- package/dist/features/content-type-templates/types.js.map +1 -1
- package/dist/features/content-type-templates/ui/ActionBarPinButton.js +12 -21
- package/dist/features/content-type-templates/ui/ActionBarPinButton.js.map +1 -1
- package/dist/features/content-type-templates/ui/TemplateDetailsPanel.js +0 -23
- package/dist/features/content-type-templates/ui/TemplateDetailsPanel.js.map +1 -1
- package/dist/features/content-type-templates/ui/TemplateSelector.js +0 -19
- package/dist/features/content-type-templates/ui/TemplateSelector.js.map +1 -1
- package/dist/features/content-type-templates/validation/structural-validation.js +0 -40
- package/dist/features/content-type-templates/validation/structural-validation.js.map +1 -1
- package/dist/features/localization/authority.d.ts +26 -0
- package/dist/features/localization/authority.d.ts.map +1 -0
- package/dist/features/localization/authority.js +17 -0
- package/dist/features/localization/authority.js.map +1 -0
- package/dist/features/localization/index.d.ts +11 -0
- package/dist/features/localization/index.d.ts.map +1 -0
- package/dist/features/localization/index.js +7 -0
- package/dist/features/localization/index.js.map +1 -0
- package/dist/features/localization/prop-target.d.ts +26 -0
- package/dist/features/localization/prop-target.d.ts.map +1 -0
- package/dist/features/localization/prop-target.js +15 -0
- package/dist/features/localization/prop-target.js.map +1 -0
- package/dist/features/localization/puck-overrides.d.ts +12 -0
- package/dist/features/localization/puck-overrides.d.ts.map +1 -0
- package/dist/features/localization/puck-overrides.js +12 -0
- package/dist/features/localization/puck-overrides.js.map +1 -0
- package/dist/features/localization/relation.d.ts +14 -0
- package/dist/features/localization/relation.d.ts.map +1 -0
- package/dist/features/localization/relation.js +7 -0
- package/dist/features/localization/relation.js.map +1 -0
- package/dist/features/localization/translatable.d.ts +24 -0
- package/dist/features/localization/translatable.d.ts.map +1 -0
- package/dist/features/localization/translatable.js +30 -0
- package/dist/features/localization/translatable.js.map +1 -0
- package/dist/features/localization/types.d.ts +17 -0
- package/dist/features/localization/types.d.ts.map +1 -0
- package/dist/features/localization/types.js +2 -0
- package/dist/features/localization/types.js.map +1 -0
- package/dist/features/localization/ui/AuthorityFieldControl.d.ts +32 -0
- package/dist/features/localization/ui/AuthorityFieldControl.d.ts.map +1 -0
- package/dist/features/localization/ui/AuthorityFieldControl.js +36 -0
- package/dist/features/localization/ui/AuthorityFieldControl.js.map +1 -0
- package/dist/features/localization/ui/TranslatabilityToggle.d.ts +17 -0
- package/dist/features/localization/ui/TranslatabilityToggle.d.ts.map +1 -0
- package/dist/features/localization/ui/TranslatabilityToggle.js +16 -0
- package/dist/features/localization/ui/TranslatabilityToggle.js.map +1 -0
- package/dist/features/localization/useLocalizationData.d.ts +37 -0
- package/dist/features/localization/useLocalizationData.d.ts.map +1 -0
- package/dist/features/localization/useLocalizationData.js +88 -0
- package/dist/features/localization/useLocalizationData.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -43
- package/dist/index.js.map +1 -1
- package/dist/merge/components/conflict-notifications/ConflictNotificationToast.js +0 -19
- package/dist/merge/components/conflict-notifications/ConflictNotificationToast.js.map +1 -1
- package/dist/merge/components/conflict-notifications/index.js +0 -5
- package/dist/merge/components/conflict-notifications/index.js.map +1 -1
- package/dist/merge/components/conflict-notifications/useConflictNotifications.js +0 -48
- package/dist/merge/components/conflict-notifications/useConflictNotifications.js.map +1 -1
- package/dist/merge/components/conflict-resolution/ComponentConflictGroup.js +0 -36
- package/dist/merge/components/conflict-resolution/ComponentConflictGroup.js.map +1 -1
- package/dist/merge/components/conflict-resolution/PuckFieldResolutionPanel.js +0 -40
- package/dist/merge/components/conflict-resolution/PuckFieldResolutionPanel.js.map +1 -1
- package/dist/merge/components/conflict-resolution/RenderedResolutionPreview.js +0 -21
- package/dist/merge/components/conflict-resolution/RenderedResolutionPreview.js.map +1 -1
- package/dist/merge/components/conflict-resolution/index.js +0 -5
- package/dist/merge/components/conflict-resolution/index.js.map +1 -1
- package/dist/merge/components/merge-resolution/CherryPickVisualPanel.js +0 -44
- package/dist/merge/components/merge-resolution/CherryPickVisualPanel.js.map +1 -1
- package/dist/merge/components/merge-resolution/ComponentClickOverlay.js +0 -24
- package/dist/merge/components/merge-resolution/ComponentClickOverlay.js.map +1 -1
- package/dist/merge/components/merge-resolution/DocumentResolutionDetail.d.ts.map +1 -1
- package/dist/merge/components/merge-resolution/DocumentResolutionDetail.js +6 -31
- package/dist/merge/components/merge-resolution/DocumentResolutionDetail.js.map +1 -1
- package/dist/merge/components/merge-resolution/index.js +0 -3
- package/dist/merge/components/merge-resolution/index.js.map +1 -1
- package/dist/merge/useMergeResolution.d.ts +1 -1
- package/dist/merge/useMergeResolution.d.ts.map +1 -1
- package/dist/merge/useMergeResolution.js +6 -75
- package/dist/merge/useMergeResolution.js.map +1 -1
- package/dist/merge/utils/namespacePuckData.d.ts.map +1 -1
- package/dist/merge/utils/namespacePuckData.js +8 -8
- package/dist/merge/utils/namespacePuckData.js.map +1 -1
- package/dist/merge/utils/puckFieldClassifier.js +0 -183
- package/dist/merge/utils/puckFieldClassifier.js.map +1 -1
- package/dist/p1/editor/connect/connect-preview-config.js +0 -5
- package/dist/p1/editor/connect/connect-preview-config.js.map +1 -1
- package/dist/p1/editor/connect/field-connect-plugin.js +0 -6
- package/dist/p1/editor/connect/field-connect-plugin.js.map +1 -1
- package/dist/p1/editor/editor-preview-resolve.js +0 -13
- package/dist/p1/editor/editor-preview-resolve.js.map +1 -1
- package/dist/p1/editor/hooks/api-hooks.js +0 -7
- package/dist/p1/editor/hooks/api-hooks.js.map +1 -1
- package/dist/p1/editor/hooks/useLiveEditorContext.d.ts.map +1 -1
- package/dist/p1/editor/hooks/useLiveEditorContext.js +2 -13
- package/dist/p1/editor/hooks/useLiveEditorContext.js.map +1 -1
- package/dist/p1/editor/hooks/useLiveRemoteDatasources.js +0 -22
- package/dist/p1/editor/hooks/useLiveRemoteDatasources.js.map +1 -1
- package/dist/p1/editor/hooks/useP1Plugins.js +0 -5
- package/dist/p1/editor/hooks/useP1Plugins.js.map +1 -1
- package/dist/p1/editor/remote-datasources/remote-datasource-explorer-plugin.js +0 -7
- package/dist/p1/editor/remote-datasources/remote-datasource-explorer-plugin.js.map +1 -1
- package/dist/p1/editor/template-autocomplete-layer.js +0 -4
- package/dist/p1/editor/template-autocomplete-layer.js.map +1 -1
- package/dist/pds/components/AgentChip.js +0 -3
- package/dist/pds/components/AgentChip.js.map +1 -1
- package/dist/pds/components/CreatePageModal.js +1 -97
- package/dist/pds/components/CreatePageModal.js.map +1 -1
- package/dist/pds/components/LoadingMessage.d.ts.map +1 -1
- package/dist/pds/components/LoadingMessage.js +2 -1
- package/dist/pds/components/LoadingMessage.js.map +1 -1
- package/dist/pds/components/LoadingMessage.module.css +2 -21
- package/dist/pds/components/LoadingOverlay.d.ts +17 -0
- package/dist/pds/components/LoadingOverlay.d.ts.map +1 -0
- package/dist/pds/components/LoadingOverlay.js +7 -0
- package/dist/pds/components/LoadingOverlay.js.map +1 -0
- package/dist/pds/components/LoadingOverlay.module.css +28 -0
- package/dist/pds/components/P1EditorHeader.d.ts +9 -2
- package/dist/pds/components/P1EditorHeader.d.ts.map +1 -1
- package/dist/pds/components/P1EditorHeader.js +6 -26
- package/dist/pds/components/P1EditorHeader.js.map +1 -1
- package/dist/pds/components/P1EditorHeader.module.css +26 -0
- package/dist/pds/components/PageNavigator.js +0 -16
- package/dist/pds/components/PageNavigator.js.map +1 -1
- package/dist/pds/components/PageNotFound.d.ts +27 -0
- package/dist/pds/components/PageNotFound.d.ts.map +1 -0
- package/dist/pds/components/PageNotFound.js +24 -0
- package/dist/pds/components/PageNotFound.js.map +1 -0
- package/dist/pds/components/PageNotFound.module.css +51 -0
- package/dist/pds/components/PresenceStack.js +0 -9
- package/dist/pds/components/PresenceStack.js.map +1 -1
- package/dist/pds/components/PublishControl.js +0 -6
- package/dist/pds/components/PublishControl.js.map +1 -1
- package/dist/pds/components/WorkstreamSwitcher.js +1 -25
- package/dist/pds/components/WorkstreamSwitcher.js.map +1 -1
- package/dist/pds/index.d.ts +4 -0
- package/dist/pds/index.d.ts.map +1 -1
- package/dist/pds/index.js +2 -29
- package/dist/pds/index.js.map +1 -1
- package/dist/pds/theme/pds-core-content.js +0 -5
- package/dist/pds/theme/pds-core-content.js.map +1 -1
- package/dist/pds/utils/deriveDocState.js +0 -1
- package/dist/pds/utils/deriveDocState.js.map +1 -1
- package/dist/pds/utils/deriveLiveDocState.js +0 -13
- package/dist/pds/utils/deriveLiveDocState.js.map +1 -1
- package/dist/registry-sync.js +0 -4
- package/dist/registry-sync.js.map +1 -1
- package/dist/server.js +0 -7
- package/dist/server.js.map +1 -1
- package/dist/versioning/components/ReadOnlyFieldsGuard.js +0 -3
- package/dist/versioning/components/ReadOnlyFieldsGuard.js.map +1 -1
- package/dist/versioning/components/VersionBannerActions.js +0 -1
- package/dist/versioning/components/VersionBannerActions.js.map +1 -1
- package/dist/versioning/components/version-compare/BranchDiffHeader.js +0 -7
- package/dist/versioning/components/version-compare/BranchDiffHeader.js.map +1 -1
- package/dist/versioning/components/version-compare/BranchMergeCompare.js +0 -19
- package/dist/versioning/components/version-compare/BranchMergeCompare.js.map +1 -1
- package/dist/versioning/components/version-compare/ComponentNode.js +0 -8
- package/dist/versioning/components/version-compare/ComponentNode.js.map +1 -1
- package/dist/versioning/components/version-compare/ComponentTree.js +0 -10
- package/dist/versioning/components/version-compare/ComponentTree.js.map +1 -1
- package/dist/versioning/components/version-compare/DiffHeader.js +0 -3
- package/dist/versioning/components/version-compare/DiffHeader.js.map +1 -1
- package/dist/versioning/components/version-compare/DocumentDiffList.js +0 -14
- package/dist/versioning/components/version-compare/DocumentDiffList.js.map +1 -1
- package/dist/versioning/components/version-compare/PropDiffPanel.js +0 -7
- package/dist/versioning/components/version-compare/PropDiffPanel.js.map +1 -1
- package/dist/versioning/components/version-compare/PropDiffRow.js +0 -3
- package/dist/versioning/components/version-compare/PropDiffRow.js.map +1 -1
- package/dist/versioning/components/version-compare/PropValueDisplay.js +0 -22
- package/dist/versioning/components/version-compare/PropValueDisplay.js.map +1 -1
- package/dist/versioning/components/version-compare/VersionComparePage.js +0 -13
- package/dist/versioning/components/version-compare/VersionComparePage.js.map +1 -1
- package/dist/versioning/components/version-compare/VisualBranchCompare.js +0 -17
- package/dist/versioning/components/version-compare/VisualBranchCompare.js.map +1 -1
- package/dist/versioning/components/version-compare/VisualVersionCompare.js +0 -23
- package/dist/versioning/components/version-compare/VisualVersionCompare.js.map +1 -1
- package/dist/versioning/components/version-compare/index.js +0 -5
- package/dist/versioning/components/version-compare/index.js.map +1 -1
- package/dist/versioning/components/version-history/AgentCheckpointBadge.js +0 -14
- package/dist/versioning/components/version-history/AgentCheckpointBadge.js.map +1 -1
- package/dist/versioning/components/version-history/VersionItem.js +0 -7
- package/dist/versioning/components/version-history/VersionItem.js.map +1 -1
- package/dist/versioning/components/version-history/index.js +0 -5
- package/dist/versioning/components/version-history/index.js.map +1 -1
- package/dist/versioning/useCheckpoints.js +0 -28
- package/dist/versioning/useCheckpoints.js.map +1 -1
- package/dist/versioning/useVersions.js +0 -31
- package/dist/versioning/useVersions.js.map +1 -1
- package/dist/versioning/utils/branchDiff.js +0 -93
- package/dist/versioning/utils/branchDiff.js.map +1 -1
- package/dist/versioning/utils/diff.js +0 -79
- package/dist/versioning/utils/diff.js.map +1 -1
- package/dist/versioning/utils/formatVersionDate.js +0 -12
- package/dist/versioning/utils/formatVersionDate.js.map +1 -1
- package/dist/versioning/utils/highlightConfig.js +0 -42
- package/dist/versioning/utils/highlightConfig.js.map +1 -1
- package/dist/versioning/utils/versionKind.js +0 -10
- package/dist/versioning/utils/versionKind.js.map +1 -1
- package/package.json +7 -7
- package/src/data/data-list-block/data-list.css +314 -0
- package/src/pds/theme/PuckEditorTheme.css +5 -5
- package/src/styles.css +1 -1
- package/src/pds/theme/PuckEditorTheme.test.ts +0 -195
- package/src/pds/theme/pds-core-content.ts +0 -8
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* P1 Puck Provider
|
|
4
|
-
*
|
|
5
|
-
* React context provider for P1 Puck integration.
|
|
6
|
-
*/
|
|
7
2
|
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
8
3
|
import isEqual from 'lodash.isequal';
|
|
9
4
|
import { P1PuckContext } from '../core/P1PuckContext.js';
|
|
10
5
|
import { NotificationProvider, useNotifications } from '../core/NotificationContext.js';
|
|
11
6
|
import { PresenceContext } from '../core/PresenceContext.js';
|
|
7
|
+
import { P1SdkQueryProvider } from '../data/query-provider.js';
|
|
12
8
|
import { debounce } from '../core/utils/debounce.js';
|
|
13
9
|
import { describeRequestFailure } from '../core/utils/requestError.js';
|
|
14
10
|
import { withRetry } from '../core/utils/retry.js';
|
|
@@ -16,75 +12,23 @@ import { resolveFeatureConfig } from '../core/featureConfig.js';
|
|
|
16
12
|
import { createPuckPermissions } from '../features/content-type-templates/permissions/createPuckPermissions.js';
|
|
17
13
|
import { useTemplateList } from '../features/content-type-templates/hooks/useTemplateList.js';
|
|
18
14
|
import { presenceIdentityKey } from '../collaboration/utils/presenceIdentity.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
15
|
+
import { DocumentPathNotFoundError, isNotFoundStatus } from '../data/utils.js';
|
|
16
|
+
import { DEFAULT_CCR_FEATURE_PLUGINS } from './defaultPlugins.js';
|
|
17
|
+
import { resolveActivePlugins, composeProviders, collectPuckPlugins, } from './composePlugins.js';
|
|
18
|
+
import { useStablePluginDeps } from './stablePluginDeps.js';
|
|
21
19
|
import { snapshotToPuckData } from './utils/snapshotToPuckData.js';
|
|
22
20
|
import { useDocuments } from './useDocuments.js';
|
|
23
21
|
import { useRealtime } from './useRealtime.js';
|
|
24
|
-
/**
|
|
25
|
-
* Identity to record alongside a loaded payload.
|
|
26
|
-
*
|
|
27
|
-
* `branch` must be the branch the load was requested for, not the branch on the
|
|
28
|
-
* returned version: for an inherited document the backend serves main's
|
|
29
|
-
* published version, and recording main would make every consumer treat the
|
|
30
|
-
* payload as belonging to another workstream.
|
|
31
|
-
*
|
|
32
|
-
* Returns a fresh object per call so consumers re-run even when two documents
|
|
33
|
-
* share payload identity — snapshotToPuckData yields one shared constant for
|
|
34
|
-
* every blank snapshot.
|
|
35
|
-
*/
|
|
36
22
|
function originFor(doc, branch, versionId, historical) {
|
|
37
23
|
return doc
|
|
38
24
|
? { branchId: branch, documentId: doc.id, documentPath: doc.path, versionId, historical }
|
|
39
25
|
: null;
|
|
40
26
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Provider component for P1 Puck integration.
|
|
43
|
-
*
|
|
44
|
-
* Wraps your Puck editor to provide P1 functionality including:
|
|
45
|
-
* - Auto-save with debouncing
|
|
46
|
-
* - Document loading
|
|
47
|
-
* - Checkpoint (publish) creation
|
|
48
|
-
* - Branch switching
|
|
49
|
-
* - Toast notifications for errors and success
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```tsx
|
|
53
|
-
* import { P1PuckProvider, P1PuckEditor } from '@pantheon-systems/puck-css';
|
|
54
|
-
* import { P1Client } from '@pantheon-systems/css-client';
|
|
55
|
-
*
|
|
56
|
-
* const client = new P1Client({
|
|
57
|
-
* baseUrl: 'http://localhost:8787',
|
|
58
|
-
* apiKey: 'your-api-key',
|
|
59
|
-
* });
|
|
60
|
-
*
|
|
61
|
-
* function App() {
|
|
62
|
-
* return (
|
|
63
|
-
* <P1PuckProvider
|
|
64
|
-
* client={client}
|
|
65
|
-
* siteId="site-123"
|
|
66
|
-
* branchId="branch-456"
|
|
67
|
-
* userId="user-789"
|
|
68
|
-
* >
|
|
69
|
-
* <P1PuckEditor config={puckConfig} documentPath="/home" />
|
|
70
|
-
* </P1PuckProvider>
|
|
71
|
-
* );
|
|
72
|
-
* }
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
75
27
|
export function P1PuckProvider(props) {
|
|
76
28
|
return (_jsx(NotificationProvider, { children: _jsx(P1PuckProviderInner, { ...props }) }));
|
|
77
29
|
}
|
|
78
|
-
/** Query param naming the workstream to open, set by the dashboard's "Open in visual editor". */
|
|
79
30
|
const BRANCH_QUERY_PARAM = 'branch';
|
|
80
|
-
/**
|
|
81
|
-
* Rejected here rather than downstream: the value reaches API paths and the
|
|
82
|
-
* realtime URL unescaped, so anything that could change a URL's shape ("/",
|
|
83
|
-
* ".", "?", "#") must never get that far. refreshBranches still decides whether
|
|
84
|
-
* a well-formed id actually exists.
|
|
85
|
-
*/
|
|
86
31
|
const BRANCH_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
87
|
-
/** Kept pure — the param is consumed in an effect, not while rendering. */
|
|
88
32
|
function readDeepLinkBranchId() {
|
|
89
33
|
if (typeof window === 'undefined')
|
|
90
34
|
return '';
|
|
@@ -97,10 +41,6 @@ function readDeepLinkBranchId() {
|
|
|
97
41
|
return '';
|
|
98
42
|
}
|
|
99
43
|
}
|
|
100
|
-
/**
|
|
101
|
-
* Left in place the param would re-apply on the next remount (this provider
|
|
102
|
-
* remounts on route-param changes), silently undoing a manual workstream switch.
|
|
103
|
-
*/
|
|
104
44
|
function stripDeepLinkBranchParam() {
|
|
105
45
|
if (typeof window === 'undefined')
|
|
106
46
|
return;
|
|
@@ -112,37 +52,30 @@ function stripDeepLinkBranchParam() {
|
|
|
112
52
|
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);
|
|
113
53
|
}
|
|
114
54
|
catch {
|
|
115
|
-
// history/URL unavailable — the param is harmless if it stays
|
|
116
55
|
}
|
|
117
56
|
}
|
|
118
|
-
|
|
119
|
-
* Inner provider component that has access to notification context.
|
|
120
|
-
*/
|
|
121
|
-
function P1PuckProviderInner({ client, siteId, branchId: initialBranchId, userId, autoSaveDelay = 3000, maxRetries = 3, showErrorNotifications = true, enableRealtime = true, wsBaseUrl, realtimeApiKey, realtimeSyncInterval: _realtimeSyncInterval = 250, realtimeTokenRefresher,
|
|
122
|
-
// Phase 9: Presence props
|
|
123
|
-
presenceEnabled = true, presencePollingInterval = 5000, userName: _userName, userAvatar: _userAvatar, userNameResolver,
|
|
124
|
-
// Phase 9: Agent mode props
|
|
125
|
-
agentModeEnabled = false, agentId, agentTrigger: _agentTrigger,
|
|
126
|
-
// Phase 9: Callbacks
|
|
127
|
-
onPresenceChange, onAgentConflict: _onAgentConflict,
|
|
128
|
-
// Plugin system props (B.4)
|
|
129
|
-
featurePlugins, featureConfig,
|
|
130
|
-
// Content Type Templates (PROPOSAL-010)
|
|
131
|
-
// Consumers should resolve the role via useResolveContentRole and pass it here.
|
|
132
|
-
userRole = 'editor', children, }) {
|
|
133
|
-
// Access notification context
|
|
57
|
+
function P1PuckProviderInner({ client, siteId, branchId: initialBranchId, userId, autoSaveDelay = 3000, maxRetries = 3, showErrorNotifications = true, enableRealtime = true, wsBaseUrl, realtimeApiKey, realtimeSyncInterval: _realtimeSyncInterval = 250, realtimeTokenRefresher, presenceEnabled = true, presencePollingInterval = 5000, userName: _userName, userAvatar: _userAvatar, userNameResolver, agentModeEnabled = false, agentId, agentTrigger: _agentTrigger, onPresenceChange, onAgentConflict: _onAgentConflict, featurePlugins, featureConfig, userRole = 'editor', children, }) {
|
|
134
58
|
const notificationContext = useNotifications();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const branchStorageKey = `css-branch-${siteId}`;
|
|
59
|
+
const branchStorageKey = `ccr-branch-${siteId}`;
|
|
60
|
+
const legacyBranchStorageKey = `css-branch-${siteId}`;
|
|
138
61
|
const getPersistedBranchId = useCallback(() => {
|
|
139
62
|
try {
|
|
140
|
-
|
|
63
|
+
if (typeof window === 'undefined')
|
|
64
|
+
return '';
|
|
65
|
+
const persisted = sessionStorage.getItem(branchStorageKey);
|
|
66
|
+
if (persisted !== null)
|
|
67
|
+
return persisted;
|
|
68
|
+
const legacy = sessionStorage.getItem(legacyBranchStorageKey);
|
|
69
|
+
if (legacy !== null) {
|
|
70
|
+
sessionStorage.setItem(branchStorageKey, legacy);
|
|
71
|
+
sessionStorage.removeItem(legacyBranchStorageKey);
|
|
72
|
+
}
|
|
73
|
+
return legacy || '';
|
|
141
74
|
}
|
|
142
75
|
catch {
|
|
143
76
|
return '';
|
|
144
77
|
}
|
|
145
|
-
}, [branchStorageKey]);
|
|
78
|
+
}, [branchStorageKey, legacyBranchStorageKey]);
|
|
146
79
|
const persistBranchId = useCallback((id) => {
|
|
147
80
|
try {
|
|
148
81
|
if (typeof window !== 'undefined') {
|
|
@@ -150,33 +83,21 @@ userRole = 'editor', children, }) {
|
|
|
150
83
|
}
|
|
151
84
|
}
|
|
152
85
|
catch {
|
|
153
|
-
// sessionStorage may be unavailable (SSR, privacy mode)
|
|
154
86
|
}
|
|
155
87
|
}, [branchStorageKey]);
|
|
156
|
-
// Site name — fetched once on mount from the P1 API
|
|
157
88
|
const [siteName, setSiteName] = useState(null);
|
|
158
89
|
useEffect(() => {
|
|
159
90
|
client.sites?.get(siteId)
|
|
160
91
|
?.then((site) => setSiteName(site.name))
|
|
161
92
|
.catch(() => { });
|
|
162
93
|
}, [client, siteId]);
|
|
163
|
-
// Outranks the prop (a build-time default) and sessionStorage (a previous
|
|
164
|
-
// visit): this names the workstream the user just asked for. refreshBranches
|
|
165
|
-
// validates it, so a stale or foreign id still falls back to main.
|
|
166
94
|
const [deepLinkBranchId] = useState(readDeepLinkBranchId);
|
|
167
|
-
// Branch state - start with the deep link, initialBranchId, persisted branch,
|
|
168
|
-
// or empty (will be set to main)
|
|
169
95
|
const [branchId, setBranchId] = useState(() => {
|
|
170
96
|
if (deepLinkBranchId)
|
|
171
97
|
return deepLinkBranchId;
|
|
172
98
|
if (initialBranchId)
|
|
173
99
|
return initialBranchId;
|
|
174
|
-
|
|
175
|
-
return (typeof window !== 'undefined' && sessionStorage.getItem(branchStorageKey)) || '';
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
return '';
|
|
179
|
-
}
|
|
100
|
+
return getPersistedBranchId();
|
|
180
101
|
});
|
|
181
102
|
const [branches, setBranches] = useState([]);
|
|
182
103
|
const [branchResolutionError, setBranchResolutionError] = useState(null);
|
|
@@ -184,115 +105,48 @@ userRole = 'editor', children, }) {
|
|
|
184
105
|
const [branchesLoading, setBranchesLoading] = useState(() => {
|
|
185
106
|
if (deepLinkBranchId || initialBranchId)
|
|
186
107
|
return false;
|
|
187
|
-
|
|
188
|
-
return !(typeof window !== 'undefined' && sessionStorage.getItem(branchStorageKey));
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
108
|
+
return !getPersistedBranchId();
|
|
193
109
|
});
|
|
194
|
-
// Persist before stripping: once the param is gone, a remount landing before
|
|
195
|
-
// the branch list resolves would otherwise fall back to a stale persisted
|
|
196
|
-
// branch. refreshBranches replaces this with a validated value.
|
|
197
110
|
useEffect(() => {
|
|
198
111
|
if (!deepLinkBranchId)
|
|
199
112
|
return;
|
|
200
113
|
persistBranchId(deepLinkBranchId);
|
|
201
114
|
stripDeepLinkBranchParam();
|
|
202
115
|
}, [deepLinkBranchId, persistBranchId]);
|
|
203
|
-
// Document state
|
|
204
116
|
const [currentDocument, setCurrentDocument] = useState(null);
|
|
205
117
|
const [currentData, setCurrentData] = useState(null);
|
|
206
118
|
const [currentDataOrigin, setCurrentDataOrigin] = useState(null);
|
|
207
|
-
// True while loadDocument is in flight. Consumers use it to distinguish
|
|
208
|
-
// "no document while switching" (keep showing the old canvas) from
|
|
209
|
-
// "genuinely no document" (show the empty state).
|
|
210
119
|
const [documentLoading, setDocumentLoading] = useState(false);
|
|
211
|
-
// Template state (PROPOSAL-010)
|
|
212
120
|
const [currentTemplate, setCurrentTemplate] = useState(null);
|
|
213
|
-
// Save state
|
|
214
121
|
const [saveStatus, setSaveStatus] = useState('idle');
|
|
215
122
|
const [lastSaved, setLastSaved] = useState(null);
|
|
216
123
|
const [saveError, setSaveError] = useState(null);
|
|
217
|
-
// Pending data for debounced save - use ref to avoid recreating debounce
|
|
218
124
|
const pendingDataRef = useRef(null);
|
|
219
125
|
const currentDocumentRef = useRef(null);
|
|
220
126
|
const initializedRef = useRef(false);
|
|
221
|
-
// Track the latest Puck action metadata for inclusion in sync payloads.
|
|
222
|
-
// Buffered actions captured by handleAction (Puck's onAction callback)
|
|
223
|
-
// Array accumulates all structural actions during an edit session.
|
|
224
|
-
// Forwarded to backend on save for version history, then cleared.
|
|
225
127
|
const pendingActionsRef = useRef([]);
|
|
226
|
-
// Tracks the document path that the current data in state belongs to.
|
|
227
|
-
// Set alongside every setCurrentData call to record the data's origin.
|
|
228
|
-
// Checked before every write (realtime or REST) to prevent cross-document
|
|
229
|
-
// corruption during rapid document switching.
|
|
230
128
|
const currentDataDocumentPathRef = useRef(null);
|
|
231
129
|
const currentDataOriginRef = useRef(null);
|
|
232
|
-
// Commit data and the identity it was loaded under in a single render pass.
|
|
233
|
-
//
|
|
234
|
-
// Path alone cannot distinguish two branches' copies of the same page, which
|
|
235
|
-
// is what let one workstream's data be published under another's sync key.
|
|
236
|
-
// Because both land in one commit, a consumer reading them together can never
|
|
237
|
-
// see a new identity beside old data.
|
|
238
130
|
const commitCurrentData = useCallback((data, origin) => {
|
|
239
131
|
currentDataDocumentPathRef.current = origin?.documentPath ?? null;
|
|
240
132
|
currentDataOriginRef.current = origin;
|
|
241
133
|
setCurrentDataOrigin(origin);
|
|
242
134
|
setCurrentData(data);
|
|
243
135
|
}, []);
|
|
244
|
-
// Snapshot of the data loadDocument just loaded. The first onChange after a
|
|
245
|
-
// load echoes this back; saveData drops that echo by structural compare
|
|
246
|
-
// (not a one-shot flag, which would swallow a lone genuine first edit like a
|
|
247
|
-
// pin toggle). Structural, not string: Puck re-normalizes key order.
|
|
248
136
|
const suppressNextSaveRef = useRef(null);
|
|
249
|
-
// Monotonically increasing counter for stale loadDocument response detection.
|
|
250
|
-
// Each loadDocument call increments this and captures the current value.
|
|
251
|
-
// After each async operation, the captured value is compared to the current
|
|
252
|
-
// value — if they differ, a newer loadDocument call has started and the
|
|
253
|
-
// current one should bail out.
|
|
254
137
|
const loadRequestIdRef = useRef(0);
|
|
255
|
-
// Track realtime connection state for use in performSave (avoids stale closure)
|
|
256
138
|
const realtimeConnectedRef = useRef(false);
|
|
257
|
-
// Auto-save pause state
|
|
258
139
|
const [autoSavePaused, setAutoSavePaused] = useState(false);
|
|
259
|
-
// Version viewing state
|
|
260
140
|
const [viewingVersion, setViewingVersion] = useState(null);
|
|
261
|
-
// True while returnToLatest is fetching the latest version from the server,
|
|
262
|
-
// so the banner can show progress during the round trip.
|
|
263
141
|
const [isReturningToLatest, setIsReturningToLatest] = useState(false);
|
|
264
|
-
// Last known "latest" snapshot — a write-only internal fallback for
|
|
265
|
-
// returnToLatest when no live Yjs snapshot is available and the server
|
|
266
|
-
// re-fetch fails. Backed by a ref (not state): nothing renders from it, so
|
|
267
|
-
// making it state would add a setState on every autosave and churn the
|
|
268
|
-
// context memo / returnToLatest identity for no consumer benefit.
|
|
269
142
|
const latestVersionDataRef = useRef(null);
|
|
270
|
-
// Remote sync key - changes when remote updates arrive to trigger Puck sync
|
|
271
143
|
const [remoteSyncKey, setRemoteSyncKey] = useState(null);
|
|
272
|
-
// Counter to track pending remote updates that will trigger onChange callbacks.
|
|
273
|
-
// When we receive a remote update and sync it to Puck via setData, Puck fires onChange.
|
|
274
|
-
// We need to skip the realtime send for that onChange to prevent bounce-back loops.
|
|
275
|
-
// Using a counter is more reliable than a timeout because it's synchronized with
|
|
276
|
-
// the actual number of remote updates received.
|
|
277
144
|
const pendingRemoteUpdatesRef = useRef(0);
|
|
278
|
-
// Flag to track if we're currently applying a remote sync.
|
|
279
|
-
// This is set before setCurrentData and cleared after a short delay to ensure
|
|
280
|
-
// all related onChange events are skipped (there may be multiple: one from data
|
|
281
|
-
// prop change and one from setData dispatch).
|
|
282
|
-
// Pending remote data ref - stores latest data during debounce period
|
|
283
145
|
const pendingRemoteDataRef = useRef(null);
|
|
284
|
-
// Debounce timer for remote sync - batches rapid updates to reduce flickering
|
|
285
146
|
const remoteSyncTimerRef = useRef(null);
|
|
286
|
-
// Delay for debouncing remote sync updates (ms)
|
|
287
147
|
const REMOTE_SYNC_DEBOUNCE_DELAY = 50;
|
|
288
|
-
// Ref to track viewingVersion for use in callbacks (avoids stale closure)
|
|
289
148
|
const viewingVersionRef = useRef(null);
|
|
290
|
-
// Tracks the latest data seen by saveData when NOT viewing historical.
|
|
291
|
-
// Used by returnToLatest to restore unsaved edits that were in Puck's
|
|
292
|
-
// memory when the user navigated to a historical version.
|
|
293
149
|
const latestLocalDataRef = useRef(null);
|
|
294
|
-
// Helper to cancel any pending remote sync - used when loading documents/versions
|
|
295
|
-
// This prevents race conditions where a pending remote update overrides loaded data
|
|
296
150
|
const cancelPendingRemoteSync = useCallback(() => {
|
|
297
151
|
if (remoteSyncTimerRef.current) {
|
|
298
152
|
clearTimeout(remoteSyncTimerRef.current);
|
|
@@ -300,10 +154,8 @@ userRole = 'editor', children, }) {
|
|
|
300
154
|
}
|
|
301
155
|
pendingRemoteDataRef.current = null;
|
|
302
156
|
}, []);
|
|
303
|
-
// WebSocket presence state - used when realtime is connected for instant presence updates
|
|
304
157
|
const [wsPresenceActors, setWsPresenceActors] = useState([]);
|
|
305
158
|
const [wsPresenceActive, setWsPresenceActive] = useState(false);
|
|
306
|
-
// Helper to enrich actors with display names from the resolver
|
|
307
159
|
const enrichActorsWithNames = useCallback((actors) => {
|
|
308
160
|
if (!userNameResolver)
|
|
309
161
|
return actors;
|
|
@@ -312,7 +164,8 @@ userRole = 'editor', children, }) {
|
|
|
312
164
|
return resolvedName ? { ...actor, name: resolvedName } : actor;
|
|
313
165
|
});
|
|
314
166
|
}, [userNameResolver]);
|
|
315
|
-
|
|
167
|
+
const [baselineResetKey, setBaselineResetKey] = useState(0);
|
|
168
|
+
const loadDocForResetRef = useRef(null);
|
|
316
169
|
const realtime = useRealtime({
|
|
317
170
|
baseUrl: wsBaseUrl ?? '',
|
|
318
171
|
apiKey: realtimeApiKey,
|
|
@@ -324,47 +177,48 @@ userRole = 'editor', children, }) {
|
|
|
324
177
|
actorType: 'user',
|
|
325
178
|
enabled: enableRealtime && !!wsBaseUrl,
|
|
326
179
|
initialData: currentData,
|
|
180
|
+
resetKey: baselineResetKey,
|
|
327
181
|
onServerReload: () => {
|
|
328
182
|
notificationContext.addInfo('Document is being refreshed by the server. Reconnecting...');
|
|
329
183
|
},
|
|
330
|
-
|
|
184
|
+
onBaselineReset: () => {
|
|
185
|
+
notificationContext.addInfo('Your local changes could not be synced after a branch merge. Reconnecting with fresh content...');
|
|
186
|
+
const docPath = currentDocumentRef.current?.path;
|
|
187
|
+
if (!docPath)
|
|
188
|
+
return;
|
|
189
|
+
void loadDocForResetRef.current?.(docPath)
|
|
190
|
+
.then(() => {
|
|
191
|
+
setBaselineResetKey((k) => k + 1);
|
|
192
|
+
})
|
|
193
|
+
.catch(() => {
|
|
194
|
+
setBaselineResetKey((k) => k + 1);
|
|
195
|
+
notificationContext.addError('Could not reload content after a branch merge. Reconnecting — you may need to refresh if the editor stays blank.');
|
|
196
|
+
});
|
|
197
|
+
},
|
|
331
198
|
onPresenceUpdate: (actors) => {
|
|
332
|
-
// Mark WebSocket presence as active (first update received)
|
|
333
199
|
setWsPresenceActive(true);
|
|
334
|
-
// Filter out self, enrich with names, and update state
|
|
335
200
|
const filtered = actors.filter((a) => a.actorId !== userId);
|
|
336
201
|
const enriched = enrichActorsWithNames(filtered);
|
|
337
202
|
setWsPresenceActors(enriched);
|
|
338
203
|
},
|
|
339
204
|
onFocusRegionBroadcast: (actorId, focusRegions) => {
|
|
340
|
-
// Update focus regions for the specific actor
|
|
341
205
|
setWsPresenceActors((prev) => prev.map((a) => (a.actorId === actorId ? { ...a, focusRegions } : a)));
|
|
342
206
|
},
|
|
343
207
|
onRemoteUpdate: (data) => {
|
|
344
208
|
if (viewingVersionRef.current !== null) {
|
|
345
209
|
return;
|
|
346
210
|
}
|
|
347
|
-
// Reject empty data that would overwrite real editor content.
|
|
348
|
-
// During initial Yjs sync, the remote doc may be empty, producing
|
|
349
|
-
// { content: [], root: { props: {} } }. Applying this would clear
|
|
350
|
-
// the editor and trigger a save loop.
|
|
351
211
|
const rootProps = data.root.props;
|
|
352
212
|
if (data.content.length === 0 &&
|
|
353
213
|
(!rootProps || Object.keys(rootProps).length === 0) &&
|
|
354
214
|
!data.zones) {
|
|
355
215
|
return;
|
|
356
216
|
}
|
|
357
|
-
// Store the latest data - will be used when debounce fires
|
|
358
217
|
pendingRemoteDataRef.current = data;
|
|
359
|
-
// Clear any pending debounce timer
|
|
360
218
|
if (remoteSyncTimerRef.current) {
|
|
361
219
|
clearTimeout(remoteSyncTimerRef.current);
|
|
362
220
|
}
|
|
363
|
-
// Debounce the sync to batch rapid successive updates
|
|
364
|
-
// This prevents plugin recreation and flickering on every update
|
|
365
221
|
remoteSyncTimerRef.current = setTimeout(() => {
|
|
366
|
-
// Double-check we're still not viewing a historical version
|
|
367
|
-
// (user might have switched while debounce was pending)
|
|
368
222
|
if (viewingVersionRef.current !== null) {
|
|
369
223
|
pendingRemoteDataRef.current = null;
|
|
370
224
|
remoteSyncTimerRef.current = null;
|
|
@@ -372,22 +226,10 @@ userRole = 'editor', children, }) {
|
|
|
372
226
|
}
|
|
373
227
|
const dataToSync = pendingRemoteDataRef.current;
|
|
374
228
|
if (dataToSync) {
|
|
375
|
-
// Increment counter to skip the onChange echo(es) that will fire
|
|
376
|
-
// when Puck processes the setCurrentData call below.
|
|
377
|
-
// The content-based guards in puckDataToYMap and RealtimeClient
|
|
378
|
-
// prevent actual echo loops at the Y.Doc/WebSocket layer.
|
|
379
229
|
pendingRemoteUpdatesRef.current += 1;
|
|
380
|
-
// Update current data when remote changes arrive. Same document and
|
|
381
|
-
// branch as whatever is loaded, but no longer a stored version — this
|
|
382
|
-
// syncs through remoteSyncKey below, not the document-sync plugin.
|
|
383
230
|
commitCurrentData(dataToSync, originFor(currentDocumentRef.current, currentDataOriginRef.current?.branchId ?? null, null, currentDataOriginRef.current?.historical ?? false));
|
|
384
|
-
// Update sync key to trigger Puck re-sync
|
|
385
231
|
setRemoteSyncKey(`remote-${Date.now()}`);
|
|
386
232
|
pendingRemoteDataRef.current = null;
|
|
387
|
-
// Safety net: reset counter after React has processed the state updates.
|
|
388
|
-
// If the remote data is identical to current editor state, Puck won't
|
|
389
|
-
// fire onChange and the counter would never be decremented naturally.
|
|
390
|
-
// Leaving it > 0 would cause subsequent local edits to be dropped.
|
|
391
233
|
setTimeout(() => {
|
|
392
234
|
pendingRemoteUpdatesRef.current = 0;
|
|
393
235
|
}, 100);
|
|
@@ -396,10 +238,8 @@ userRole = 'editor', children, }) {
|
|
|
396
238
|
}, REMOTE_SYNC_DEBOUNCE_DELAY);
|
|
397
239
|
},
|
|
398
240
|
});
|
|
399
|
-
// Keep a stable ref to realtime for use in callbacks without dependency churn
|
|
400
241
|
const realtimeRef = useRef(realtime);
|
|
401
242
|
realtimeRef.current = realtime;
|
|
402
|
-
// Cleanup remote sync timer on unmount
|
|
403
243
|
useEffect(() => {
|
|
404
244
|
return () => {
|
|
405
245
|
if (remoteSyncTimerRef.current) {
|
|
@@ -407,33 +247,16 @@ userRole = 'editor', children, }) {
|
|
|
407
247
|
}
|
|
408
248
|
};
|
|
409
249
|
}, []);
|
|
410
|
-
// PuckDataCapture catch-up: detects data that Puck's onChange missed.
|
|
411
|
-
// Puck's onChange can miss the very last keystroke in a typing burst because
|
|
412
|
-
// createOnChange dispatches asynchronously (via resolveComponentData) and
|
|
413
|
-
// the Zustand subscriber may not fire for the final state update in time.
|
|
414
|
-
// PuckDataCapture subscribes to Puck's Zustand store independently via
|
|
415
|
-
// createUsePuck() and writes the true current data to this ref.
|
|
416
|
-
// The onDataChange callback debounces and compares with what saveData last
|
|
417
|
-
// sent — if they differ, it pushes the corrected data through realtime.
|
|
418
250
|
const realtimeDataCaptureRef = useRef(null);
|
|
419
251
|
const lastSentDataRef = useRef(null);
|
|
420
252
|
const captureTimerRef = useRef(null);
|
|
421
|
-
// Track the last data sent via saveData so we can detect missed updates.
|
|
422
|
-
// Updated in saveData's realtime branch (the happy path).
|
|
423
253
|
const trackSentData = useCallback((data) => {
|
|
424
254
|
lastSentDataRef.current = JSON.stringify(data);
|
|
425
255
|
}, []);
|
|
426
|
-
// Callback for PuckDataCapture.onDataChange — fires when Puck's store
|
|
427
|
-
// updates (may fire for changes that onChange misses).
|
|
428
256
|
const handleRealtimeDataCapture = useCallback((data) => {
|
|
429
257
|
if (!enableRealtime)
|
|
430
258
|
return;
|
|
431
|
-
// Store the latest data in the ref for comparison when the timer fires.
|
|
432
|
-
// The ref always holds the most recent Zustand store snapshot.
|
|
433
259
|
realtimeDataCaptureRef.current = data;
|
|
434
|
-
// Debounce: wait 800ms after the last store update to allow onChange
|
|
435
|
-
// to process normally. When the timer fires, compare the ref's current
|
|
436
|
-
// data (not the closure's stale copy) against what was last sent.
|
|
437
260
|
if (captureTimerRef.current)
|
|
438
261
|
clearTimeout(captureTimerRef.current);
|
|
439
262
|
captureTimerRef.current = setTimeout(() => {
|
|
@@ -445,10 +268,8 @@ userRole = 'editor', children, }) {
|
|
|
445
268
|
if (!currentData)
|
|
446
269
|
return;
|
|
447
270
|
const dataJson = JSON.stringify(currentData);
|
|
448
|
-
// If saveData already sent this exact data, no catch-up needed
|
|
449
271
|
if (dataJson === lastSentDataRef.current)
|
|
450
272
|
return;
|
|
451
|
-
// Data origin guard
|
|
452
273
|
const currentPath = currentDocumentRef.current?.path ?? null;
|
|
453
274
|
const dataOriginPath = currentDataDocumentPathRef.current;
|
|
454
275
|
if (dataOriginPath !== currentPath)
|
|
@@ -457,31 +278,24 @@ userRole = 'editor', children, }) {
|
|
|
457
278
|
lastSentDataRef.current = dataJson;
|
|
458
279
|
}, 800);
|
|
459
280
|
}, [enableRealtime]);
|
|
460
|
-
// Cleanup capture timer on unmount
|
|
461
281
|
useEffect(() => {
|
|
462
282
|
return () => {
|
|
463
283
|
if (captureTimerRef.current)
|
|
464
284
|
clearTimeout(captureTimerRef.current);
|
|
465
285
|
};
|
|
466
286
|
}, []);
|
|
467
|
-
// Keep refs in sync with state
|
|
468
287
|
useEffect(() => {
|
|
469
288
|
currentDocumentRef.current = currentDocument;
|
|
470
289
|
}, [currentDocument]);
|
|
471
290
|
useEffect(() => {
|
|
472
291
|
viewingVersionRef.current = viewingVersion;
|
|
473
292
|
}, [viewingVersion]);
|
|
474
|
-
// Keep realtime connection ref in sync (used by performSave to avoid stale closure)
|
|
475
293
|
useEffect(() => {
|
|
476
294
|
realtimeConnectedRef.current = realtime.connected;
|
|
477
295
|
}, [realtime.connected]);
|
|
478
|
-
// Create client with user principal
|
|
479
296
|
const userClient = useMemo(() => client.withPrincipal({ id: userId, type: 'user' }), [client, userId]);
|
|
480
|
-
// Document list for current branch
|
|
481
297
|
const { documents: branchDocuments, loading: documentsLoading, error: documentsError, create: createDocumentRaw, remove: removeDocumentRaw, refresh: refreshDocuments, } = useDocuments({ client: userClient, siteId, branchId });
|
|
482
|
-
// Template list for current branch
|
|
483
298
|
const { templates: branchTemplates, loading: templatesLoading, error: templatesError, refresh: refreshTemplates, } = useTemplateList(userClient, siteId, branchId);
|
|
484
|
-
// Stable document create/delete callbacks
|
|
485
299
|
const branchIdRef = useRef(branchId);
|
|
486
300
|
branchIdRef.current = branchId;
|
|
487
301
|
const createDocumentRawRef = useRef(createDocumentRaw);
|
|
@@ -491,16 +305,10 @@ userRole = 'editor', children, }) {
|
|
|
491
305
|
throw new Error('Cannot create document: no branch selected');
|
|
492
306
|
}
|
|
493
307
|
if (template) {
|
|
494
|
-
// The list endpoint carries no layout content; fetch the full template.
|
|
495
308
|
const fullTemplate = await userClient.templates.get(siteId, branchIdRef.current, template.id);
|
|
496
|
-
// A template without a content array has no stored layout snapshot;
|
|
497
|
-
// creating a page from it would silently produce a blank page. An empty
|
|
498
|
-
// array is a legitimate empty layout and passes.
|
|
499
309
|
if (!fullTemplate.content) {
|
|
500
310
|
throw new Error(`Template "${template.label || template.name}" has no layout yet. Open it in the editor and add components before creating pages from it.`);
|
|
501
311
|
}
|
|
502
|
-
// The backend builds the initial version from the template, preserving
|
|
503
|
-
// each component's durable slot id; no client snapshot is sent.
|
|
504
312
|
await createDocumentRawRef.current(path, undefined, {
|
|
505
313
|
templateId: fullTemplate.id,
|
|
506
314
|
templateVersion: fullTemplate.version,
|
|
@@ -511,14 +319,8 @@ userRole = 'editor', children, }) {
|
|
|
511
319
|
await createDocumentRawRef.current(path, undefined, title ? { title } : undefined);
|
|
512
320
|
}
|
|
513
321
|
}, [userClient, siteId]);
|
|
514
|
-
// Stable update-template callback (template metadata: label/description/URL
|
|
515
|
-
// pattern). Used by the editor's template-mode right sidebar to persist details
|
|
516
|
-
// to the Template record, then refresh the list so the changes propagate.
|
|
517
322
|
const refreshTemplatesRef = useRef(refreshTemplates);
|
|
518
323
|
refreshTemplatesRef.current = refreshTemplates;
|
|
519
|
-
// Create a new template (empty layout, built in the template editor
|
|
520
|
-
// afterwards). Returns the created Template so callers can navigate to its
|
|
521
|
-
// editor. Used by the Create Page modal's "New template" flow.
|
|
522
324
|
const stableCreateTemplate = useCallback(async (params) => {
|
|
523
325
|
if (!branchIdRef.current) {
|
|
524
326
|
throw new Error('Cannot create template: no branch selected');
|
|
@@ -532,7 +334,6 @@ userRole = 'editor', children, }) {
|
|
|
532
334
|
throw new Error('Cannot update template: no branch selected');
|
|
533
335
|
}
|
|
534
336
|
const updated = await userClient.templates.update(siteId, branchIdRef.current, templateId, params);
|
|
535
|
-
// Keep the in-editor template in sync if it's the one being edited.
|
|
536
337
|
setCurrentTemplate((prev) => (prev && prev.id === updated.id ? updated : prev));
|
|
537
338
|
await refreshTemplatesRef.current();
|
|
538
339
|
}, [userClient, siteId]);
|
|
@@ -544,7 +345,6 @@ userRole = 'editor', children, }) {
|
|
|
544
345
|
}
|
|
545
346
|
await removeDocumentRawRef.current(documentId);
|
|
546
347
|
}, []);
|
|
547
|
-
// Load branches
|
|
548
348
|
const refreshBranches = useCallback(async () => {
|
|
549
349
|
try {
|
|
550
350
|
setBranchesLoading(true);
|
|
@@ -552,33 +352,25 @@ userRole = 'editor', children, }) {
|
|
|
552
352
|
const branchList = await userClient.branches.list(siteId);
|
|
553
353
|
setBranches(branchList);
|
|
554
354
|
let effectiveBranchId = branchIdRef.current;
|
|
555
|
-
// If no branchId set, try persisted branch, then default to main
|
|
556
355
|
if (!effectiveBranchId) {
|
|
557
356
|
const persisted = getPersistedBranchId();
|
|
558
357
|
if (persisted && branchList.some((b) => b.id === persisted)) {
|
|
559
358
|
effectiveBranchId = persisted;
|
|
560
359
|
}
|
|
561
360
|
}
|
|
562
|
-
// Validate that the branch exists in the list; fall back to main if not
|
|
563
361
|
if (effectiveBranchId && !branchList.some((b) => b.id === effectiveBranchId)) {
|
|
564
362
|
const mainBranch = branchList.find((b) => b.isMain);
|
|
565
363
|
effectiveBranchId = mainBranch?.id ?? effectiveBranchId;
|
|
566
364
|
}
|
|
567
|
-
// Default to main if still empty
|
|
568
365
|
if (!effectiveBranchId) {
|
|
569
366
|
const mainBranch = branchList.find((b) => b.isMain);
|
|
570
367
|
if (mainBranch) {
|
|
571
368
|
effectiveBranchId = mainBranch.id;
|
|
572
369
|
}
|
|
573
370
|
}
|
|
574
|
-
// A list that resolves without a usable branch strands the editor exactly
|
|
575
|
-
// as a refused list does — most often a siteId that doesn't exist, which
|
|
576
|
-
// this endpoint answers with an empty list rather than a 404.
|
|
577
371
|
if (!effectiveBranchId) {
|
|
578
372
|
setBranchResolutionError(new Error(`GET /api/sites/${siteId}/branches returned no usable branch. Check that the site id is correct and that the site has a main branch.`));
|
|
579
373
|
}
|
|
580
|
-
// Persisting an empty id would poison the next visit and build requests with a
|
|
581
|
-
// blank branch segment; keep whatever is already selected instead.
|
|
582
374
|
if (effectiveBranchId) {
|
|
583
375
|
persistBranchId(effectiveBranchId);
|
|
584
376
|
setBranchId(effectiveBranchId);
|
|
@@ -586,14 +378,8 @@ userRole = 'editor', children, }) {
|
|
|
586
378
|
setCurrentBranch(branchList.find((b) => b.id === effectiveBranchId) ?? null);
|
|
587
379
|
}
|
|
588
380
|
catch (error) {
|
|
589
|
-
// The editor cannot open a document without a branch, so this has to
|
|
590
|
-
// reach the UI: swallowed, it leaves the editor on "Loading document"
|
|
591
|
-
// forever with no indication of what was refused.
|
|
592
381
|
setBranchResolutionError(describeRequestFailure(`GET /api/sites/${siteId}/branches`, error));
|
|
593
382
|
console.error('Failed to load branches:', error);
|
|
594
|
-
// Without a branch nothing else can load: useDocuments' fetch is branch-guarded, so
|
|
595
|
-
// documentsLoading stays true forever and the editor sits blank. Say so rather than
|
|
596
|
-
// leaving the user with an unexplained empty screen.
|
|
597
383
|
if (showErrorNotifications && notificationContext?.addError) {
|
|
598
384
|
const message = error instanceof Error ? error.message : String(error);
|
|
599
385
|
notificationContext.addError(`Failed to load branches: ${message}`, () => void refreshBranchesRef.current?.());
|
|
@@ -603,8 +389,6 @@ userRole = 'editor', children, }) {
|
|
|
603
389
|
setBranchesLoading(false);
|
|
604
390
|
}
|
|
605
391
|
}, [userClient, siteId, getPersistedBranchId, persistBranchId, showErrorNotifications, notificationContext]);
|
|
606
|
-
// Lets the failure notification offer a retry without refreshBranches depending on
|
|
607
|
-
// itself, the same ref idiom the template callbacks above use.
|
|
608
392
|
const refreshBranchesRef = useRef(refreshBranches);
|
|
609
393
|
refreshBranchesRef.current = refreshBranches;
|
|
610
394
|
const createBranch = useCallback(async (name) => {
|
|
@@ -612,36 +396,28 @@ userRole = 'editor', children, }) {
|
|
|
612
396
|
await refreshBranches();
|
|
613
397
|
return branch;
|
|
614
398
|
}, [userClient, siteId, refreshBranches]);
|
|
615
|
-
// Initial branch load - only once
|
|
616
399
|
useEffect(() => {
|
|
617
400
|
if (!initializedRef.current) {
|
|
618
401
|
initializedRef.current = true;
|
|
619
402
|
void refreshBranches();
|
|
620
403
|
}
|
|
621
404
|
}, [refreshBranches]);
|
|
622
|
-
// Perform save operation - uses refs to avoid dependency issues
|
|
623
405
|
const performSave = useCallback(async (options) => {
|
|
624
406
|
const dataToSave = pendingDataRef.current;
|
|
625
407
|
const doc = currentDocumentRef.current;
|
|
626
408
|
if (!dataToSave || !doc) {
|
|
627
409
|
return;
|
|
628
410
|
}
|
|
629
|
-
// Data origin guard: reject writes if data doesn't belong to current document
|
|
630
411
|
if (currentDataDocumentPathRef.current !== doc.path) {
|
|
631
412
|
pendingDataRef.current = null;
|
|
632
413
|
return;
|
|
633
414
|
}
|
|
634
|
-
// When realtime is connected, the DO handles persistence via WebSocket sync.
|
|
635
|
-
// Skip REST API save to avoid creating duplicate versions without CRDT state.
|
|
636
415
|
if (enableRealtime && realtimeConnectedRef.current) {
|
|
637
|
-
// The data is already being sent via realtime.applyLocalChange in saveData.
|
|
638
|
-
// Mark as saved since the DO will persist it.
|
|
639
416
|
pendingDataRef.current = null;
|
|
640
417
|
setSaveStatus('saved');
|
|
641
418
|
setLastSaved(new Date());
|
|
642
419
|
return;
|
|
643
420
|
}
|
|
644
|
-
// Fallback: REST API save when realtime is disabled or disconnected
|
|
645
421
|
setSaveStatus('saving');
|
|
646
422
|
setSaveError(null);
|
|
647
423
|
try {
|
|
@@ -660,47 +436,33 @@ userRole = 'editor', children, }) {
|
|
|
660
436
|
: userClient.versions.create(siteId, params));
|
|
661
437
|
pendingActionsRef.current = [];
|
|
662
438
|
}, { maxAttempts: maxRetries });
|
|
663
|
-
// Note: We intentionally do NOT call setCurrentData(dataToSave) here.
|
|
664
|
-
// The data is already in Puck's internal state (it came from Puck's onChange).
|
|
665
|
-
// Updating currentData would trigger a re-render cascade that recreates the
|
|
666
|
-
// p1Plugin, causing Puck to potentially re-render and flicker.
|
|
667
|
-
// currentData should only be updated when loading a document or switching versions.
|
|
668
439
|
pendingDataRef.current = null;
|
|
669
440
|
setSaveStatus('saved');
|
|
670
441
|
setLastSaved(new Date());
|
|
671
|
-
// Keep the "latest" cache in sync with what was just persisted (REST
|
|
672
|
-
// success = confirmed persistence) so returning from a historical preview
|
|
673
|
-
// restores the correct content even if the on-return re-fetch fails.
|
|
674
442
|
latestVersionDataRef.current = dataToSave;
|
|
675
443
|
}
|
|
676
444
|
catch (error) {
|
|
677
445
|
setSaveStatus('error');
|
|
678
446
|
const saveErr = error instanceof Error ? error : new Error(String(error));
|
|
679
447
|
setSaveError(saveErr);
|
|
680
|
-
// Show error notification with retry action
|
|
681
448
|
if (showErrorNotifications) {
|
|
682
449
|
notificationContext.addError(`Failed to save changes: ${saveErr.message}`, () => void performSave());
|
|
683
450
|
}
|
|
684
451
|
}
|
|
685
452
|
}, [userClient, siteId, branchId, maxRetries, showErrorNotifications, notificationContext, enableRealtime]);
|
|
686
|
-
// Debounced save
|
|
687
453
|
const debouncedSave = useMemo(() => debounce(() => {
|
|
688
454
|
void performSave();
|
|
689
455
|
}, autoSaveDelay), [performSave, autoSaveDelay]);
|
|
690
|
-
// Cleanup debounce on unmount
|
|
691
456
|
useEffect(() => {
|
|
692
457
|
return () => {
|
|
693
458
|
debouncedSave.cancel();
|
|
694
459
|
};
|
|
695
460
|
}, [debouncedSave]);
|
|
696
|
-
// Best-effort flush of a pending debounced save when the tab is closed,
|
|
697
|
-
// navigated away from, or hidden.
|
|
698
461
|
useEffect(() => {
|
|
699
462
|
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
700
463
|
return;
|
|
701
464
|
const flushPendingSave = () => {
|
|
702
465
|
if (pendingDataRef.current) {
|
|
703
|
-
// keepalive lets the POST finish after the page is discarded.
|
|
704
466
|
debouncedSave.cancel();
|
|
705
467
|
void performSave({ keepalive: true });
|
|
706
468
|
}
|
|
@@ -719,19 +481,14 @@ userRole = 'editor', children, }) {
|
|
|
719
481
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
720
482
|
};
|
|
721
483
|
}, [debouncedSave, performSave]);
|
|
722
|
-
// Pause auto-save
|
|
723
484
|
const pauseAutoSave = useCallback(() => {
|
|
724
485
|
debouncedSave.pause();
|
|
725
486
|
setAutoSavePaused(true);
|
|
726
487
|
}, [debouncedSave]);
|
|
727
|
-
// Resume auto-save
|
|
728
488
|
const resumeAutoSave = useCallback(() => {
|
|
729
489
|
debouncedSave.resume();
|
|
730
490
|
setAutoSavePaused(false);
|
|
731
491
|
}, [debouncedSave]);
|
|
732
|
-
// Capture Puck action metadata from the onAction callback.
|
|
733
|
-
// This captures the action type and relevant metadata fields so they
|
|
734
|
-
// can be included in the sync payload for backend version storage.
|
|
735
492
|
const handleAction = useCallback((action) => {
|
|
736
493
|
const puckAction = {
|
|
737
494
|
type: action.type || 'unknown',
|
|
@@ -756,28 +513,17 @@ userRole = 'editor', children, }) {
|
|
|
756
513
|
pendingActionsRef.current = buffer.slice(-500);
|
|
757
514
|
}
|
|
758
515
|
}, []);
|
|
759
|
-
// Public save function (triggers debounce)
|
|
760
|
-
// Also resumes auto-save if paused, per user requirement
|
|
761
|
-
// Sends changes via WebSocket when realtime is enabled (but not for remote updates)
|
|
762
516
|
const saveData = useCallback((data) => {
|
|
763
517
|
if (suppressNextSaveRef.current !== null) {
|
|
764
518
|
const loadedSnapshot = suppressNextSaveRef.current;
|
|
765
519
|
suppressNextSaveRef.current = null;
|
|
766
|
-
// Drop only the echo; a structurally different first onChange is a real edit.
|
|
767
520
|
if (isEqual(data, loadedSnapshot)) {
|
|
768
521
|
return;
|
|
769
522
|
}
|
|
770
523
|
}
|
|
771
|
-
// Only genuine user edits reach here — load/restore echoes arm
|
|
772
|
-
// suppressNextSaveRef before they fire and are returned early above.
|
|
773
524
|
if (!viewingVersionRef.current) {
|
|
774
525
|
latestLocalDataRef.current = data;
|
|
775
526
|
}
|
|
776
|
-
// When realtime is enabled, detect whether this onChange came from a remote
|
|
777
|
-
// sync (Yjs update from another client) vs. a local user edit.
|
|
778
|
-
// Remote updates should NOT trigger a REST save — the DO handles persistence.
|
|
779
|
-
// Important: we must NOT set pendingDataRef for remote syncs, otherwise
|
|
780
|
-
// getHasUnsavedChanges() will incorrectly report unsaved changes.
|
|
781
527
|
if (enableRealtime && realtime.connected) {
|
|
782
528
|
if (pendingRemoteUpdatesRef.current > 0) {
|
|
783
529
|
pendingRemoteUpdatesRef.current -= 1;
|
|
@@ -788,26 +534,16 @@ userRole = 'editor', children, }) {
|
|
|
788
534
|
}
|
|
789
535
|
else {
|
|
790
536
|
const currentPath = currentDocumentRef.current?.path ?? null;
|
|
791
|
-
// Data origin guard: verify the data in state belongs to the
|
|
792
|
-
// current document. During rapid document switching, a stale
|
|
793
|
-
// loadDocument call can resolve and set data from document Y
|
|
794
|
-
// into state after currentDocument has been updated to X.
|
|
795
537
|
const dataOriginPath = currentDataDocumentPathRef.current;
|
|
796
538
|
if (dataOriginPath !== currentPath) {
|
|
797
539
|
console.warn('[P1PuckProvider] saveData SKIPPED: data origin mismatch.', 'dataOrigin:', dataOriginPath, 'currentDoc:', currentPath);
|
|
798
540
|
return;
|
|
799
541
|
}
|
|
800
|
-
// Connection identity guard: verify the realtime connection is
|
|
801
|
-
// bound to the same document we're currently editing.
|
|
802
542
|
const connectedPath = realtime.connectedDocumentPath;
|
|
803
543
|
if (currentPath !== connectedPath) {
|
|
804
544
|
console.warn('[P1PuckProvider] saveData SKIPPED: connection identity mismatch.', 'currentDoc:', currentPath, 'connectedDoc:', connectedPath);
|
|
805
545
|
return;
|
|
806
546
|
}
|
|
807
|
-
// Local user edit — send via WebSocket (DO handles persistence)
|
|
808
|
-
// Echo prevention is handled at lower layers:
|
|
809
|
-
// - puckDataToYMap no-ops when Y.Doc already has identical data
|
|
810
|
-
// - RealtimeClient.lastSentSnapshot drops sends matching last sent/received
|
|
811
547
|
const actions = pendingActionsRef.current;
|
|
812
548
|
if (actions.length > 0) {
|
|
813
549
|
realtime.applyLocalChange(data, actions);
|
|
@@ -817,21 +553,11 @@ userRole = 'editor', children, }) {
|
|
|
817
553
|
}
|
|
818
554
|
trackSentData(data);
|
|
819
555
|
pendingActionsRef.current = [];
|
|
820
|
-
// Data sent via WebSocket — DO handles persistence.
|
|
821
|
-
// Update save status directly (skip debouncedSave/performSave chain).
|
|
822
556
|
setSaveStatus('saved');
|
|
823
557
|
setLastSaved(new Date());
|
|
824
|
-
// NOTE: intentionally do NOT update latestVersionDataRef here. This
|
|
825
|
-
// is the optimistic local send, before the DO confirms persistence —
|
|
826
|
-
// caching it would assert content that may never be saved. Realtime
|
|
827
|
-
// returns use the live Yjs snapshot (or the server re-fetch), not
|
|
828
|
-
// this cache (PCC-3421).
|
|
829
558
|
return;
|
|
830
559
|
}
|
|
831
560
|
}
|
|
832
|
-
// Mirror the realtime guard: don't save when previewing a historical version.
|
|
833
|
-
// Without this, the PuckDataSynchronizer echo from loadVersion resumes the
|
|
834
|
-
// debounce and writes historical data to the server.
|
|
835
561
|
if (viewingVersionRef.current !== null) {
|
|
836
562
|
return;
|
|
837
563
|
}
|
|
@@ -842,15 +568,12 @@ userRole = 'editor', children, }) {
|
|
|
842
568
|
}
|
|
843
569
|
debouncedSave();
|
|
844
570
|
}, [debouncedSave, enableRealtime, realtime.connected, trackSentData]);
|
|
845
|
-
// Force immediate save
|
|
846
571
|
const saveNow = useCallback(async () => {
|
|
847
572
|
debouncedSave.cancel();
|
|
848
|
-
// When realtime is connected, ensure the latest data is sent via WebSocket
|
|
849
573
|
if (enableRealtime && realtimeConnectedRef.current && pendingDataRef.current) {
|
|
850
574
|
const currentPath = currentDocumentRef.current?.path ?? null;
|
|
851
575
|
const dataOriginPath = currentDataDocumentPathRef.current;
|
|
852
576
|
const connectedPath = realtime.connectedDocumentPath;
|
|
853
|
-
// Data origin + connection identity guards
|
|
854
577
|
if (dataOriginPath === currentPath && currentPath === connectedPath) {
|
|
855
578
|
realtime.applyLocalChange(pendingDataRef.current);
|
|
856
579
|
pendingDataRef.current = null;
|
|
@@ -858,13 +581,9 @@ userRole = 'editor', children, }) {
|
|
|
858
581
|
setLastSaved(new Date());
|
|
859
582
|
return;
|
|
860
583
|
}
|
|
861
|
-
// Fall through to REST save if mismatch
|
|
862
584
|
}
|
|
863
585
|
await performSave();
|
|
864
586
|
}, [debouncedSave, performSave, enableRealtime]);
|
|
865
|
-
// Persist current in-memory edits as a REST version before a destructive operation.
|
|
866
|
-
// Realtime: confirms WebSocket delivery then snapshots latestLocalDataRef as a version.
|
|
867
|
-
// Non-realtime: delegates to performSave which creates a version from pendingDataRef.
|
|
868
587
|
const persistCurrentEdits = useCallback(async () => {
|
|
869
588
|
if (!enableRealtime || !realtimeConnectedRef.current) {
|
|
870
589
|
await performSave();
|
|
@@ -890,55 +609,38 @@ userRole = 'editor', children, }) {
|
|
|
890
609
|
}
|
|
891
610
|
}
|
|
892
611
|
}, [enableRealtime, realtime, performSave, userClient, siteId, branchId]);
|
|
893
|
-
// Load document by path
|
|
894
612
|
const loadDocument = useCallback(async (path) => {
|
|
895
|
-
// Flush any pending debounced save while the outgoing document's
|
|
896
|
-
// identity is still current, then drop the pending buffer so a late
|
|
897
|
-
// debounce can never write one document's data into another.
|
|
898
613
|
if (pendingDataRef.current) {
|
|
899
614
|
debouncedSave.cancel();
|
|
900
615
|
await performSave();
|
|
901
|
-
// performSave clears the buffer only on success; if it survived, the
|
|
902
|
-
// flush failed — abort the switch instead of discarding the edits.
|
|
903
616
|
if (pendingDataRef.current) {
|
|
904
617
|
return;
|
|
905
618
|
}
|
|
906
619
|
}
|
|
907
620
|
debouncedSave.cancel();
|
|
908
621
|
pendingDataRef.current = null;
|
|
909
|
-
// Invalidate cached local edit so returnToLatest() doesn't treat it as unsaved.
|
|
910
622
|
latestLocalDataRef.current = null;
|
|
911
|
-
// Cancel any pending remote sync to prevent race conditions
|
|
912
|
-
// where a stale remote update overrides the document we're loading
|
|
913
623
|
cancelPendingRemoteSync();
|
|
914
|
-
// Increment load request counter and capture for staleness detection.
|
|
915
|
-
// If a newer loadDocument call starts before our awaits resolve, the
|
|
916
|
-
// counter will have been incremented again and our captured value
|
|
917
|
-
// will no longer match — signaling that our response is stale.
|
|
918
624
|
loadRequestIdRef.current += 1;
|
|
919
625
|
const thisRequestId = loadRequestIdRef.current;
|
|
920
626
|
setDocumentLoading(true);
|
|
921
|
-
// Pre-clear the current document so nothing can save against the old
|
|
922
|
-
// one while the fetch is in flight. The canvas keeps showing the
|
|
923
|
-
// previous document — documentLoading suppresses the empty state.
|
|
924
627
|
commitCurrentData(null, null);
|
|
925
628
|
try {
|
|
926
|
-
const doc = await userClient.documents
|
|
927
|
-
|
|
629
|
+
const doc = await userClient.documents
|
|
630
|
+
.getByPath(siteId, path.startsWith('/') ? path.slice(1) : path, branchId)
|
|
631
|
+
.catch((err) => {
|
|
632
|
+
throw isNotFoundStatus(err) ? new DocumentPathNotFoundError(path) : err;
|
|
633
|
+
});
|
|
928
634
|
if (thisRequestId !== loadRequestIdRef.current) {
|
|
929
635
|
console.debug('[P1PuckProvider] Stale loadDocument — skipping (after getByPath)');
|
|
930
636
|
return;
|
|
931
637
|
}
|
|
932
|
-
// Use ID not path — paths may differ by normalization, causing a false disconnect.
|
|
933
|
-
// Same-doc keeps Yjs alive; cross-doc nulls out so useRealtime seeds a fresh Y.Doc.
|
|
934
638
|
const isSameDocument = doc.id === currentDocumentRef.current?.id;
|
|
935
|
-
// Null data before identity update so useRealtime sees initialData=null.
|
|
936
639
|
commitCurrentData(null, null);
|
|
937
640
|
if (!isSameDocument) {
|
|
938
641
|
setCurrentDocument(null);
|
|
939
642
|
}
|
|
940
643
|
setCurrentDocument(doc);
|
|
941
|
-
// Fetch template if document is bound to one (PROPOSAL-010)
|
|
942
644
|
if (doc.templateId) {
|
|
943
645
|
try {
|
|
944
646
|
if (!userClient.templates) {
|
|
@@ -955,81 +657,47 @@ userRole = 'editor', children, }) {
|
|
|
955
657
|
else {
|
|
956
658
|
setCurrentTemplate(null);
|
|
957
659
|
}
|
|
958
|
-
// Get latest version
|
|
959
660
|
const version = await userClient.versions.getLatest(siteId, branchId, doc.id);
|
|
960
661
|
const puckData = snapshotToPuckData(version.snapshot);
|
|
961
|
-
// Staleness check: a newer loadDocument call has started
|
|
962
662
|
if (thisRequestId !== loadRequestIdRef.current) {
|
|
963
663
|
console.debug('[P1PuckProvider] Stale loadDocument — skipping (after getLatest)');
|
|
964
664
|
return;
|
|
965
665
|
}
|
|
966
|
-
// Mark REST data as non-local to prevent the Puck onChange handler
|
|
967
|
-
// from echoing it back through the Y.Doc via applyLocalChange.
|
|
968
|
-
// This is critical after the Hibernatable WS migration because
|
|
969
|
-
// initializeIfNeeded() latency can invert the ordering of WebSocket
|
|
970
|
-
// initial state vs REST response, causing the remote-update guard
|
|
971
|
-
// (pendingRemoteUpdatesRef) to be bypassed.
|
|
972
666
|
if (enableRealtime) {
|
|
973
667
|
pendingRemoteUpdatesRef.current += 1;
|
|
974
|
-
// Safety net: reset after React has processed the state updates.
|
|
975
|
-
// If Puck's data is identical to current editor state, onChange
|
|
976
|
-
// won't fire and the counter would never be decremented naturally.
|
|
977
|
-
// Leaving it > 0 would cause subsequent local edits to be dropped.
|
|
978
668
|
setTimeout(() => {
|
|
979
669
|
pendingRemoteUpdatesRef.current = 0;
|
|
980
670
|
}, 100);
|
|
981
671
|
}
|
|
982
|
-
// IMPORTANT: Update the ref BEFORE state updates
|
|
983
|
-
// Setting to null allows remote updates after document loads
|
|
984
672
|
viewingVersionRef.current = null;
|
|
985
|
-
// Cancel any pending PuckDataCapture catch-up timer and reset its
|
|
986
|
-
// ref before arming the data-origin guard below. Without this, the
|
|
987
|
-
// timer can fire in the window between currentDataDocumentPathRef
|
|
988
|
-
// being set to doc.path and the new document's first remote update
|
|
989
|
-
// arriving — passing the origin check while still carrying the
|
|
990
|
-
// previous document's Puck state.
|
|
991
673
|
if (captureTimerRef.current) {
|
|
992
674
|
clearTimeout(captureTimerRef.current);
|
|
993
675
|
captureTimerRef.current = null;
|
|
994
676
|
}
|
|
995
677
|
realtimeDataCaptureRef.current = null;
|
|
996
678
|
suppressNextSaveRef.current = puckData;
|
|
997
|
-
// branchId is the branch this load requested, deliberately not
|
|
998
|
-
// version.branchId: for an inherited document the backend serves main's
|
|
999
|
-
// published version, so the version names main while the payload is what
|
|
1000
|
-
// the requested branch should display. Recording main here would make
|
|
1001
|
-
// every consumer reject the payload as foreign.
|
|
1002
679
|
commitCurrentData(puckData, originFor(doc, branchId, version.id, false));
|
|
1003
680
|
latestVersionDataRef.current = puckData;
|
|
1004
681
|
setViewingVersion(null);
|
|
1005
|
-
// Clear remoteSyncKey so document sync takes priority
|
|
1006
682
|
setRemoteSyncKey(null);
|
|
1007
683
|
pendingDataRef.current = null;
|
|
1008
684
|
setSaveStatus('idle');
|
|
1009
685
|
setSaveError(null);
|
|
1010
686
|
}
|
|
1011
687
|
catch (error) {
|
|
1012
|
-
// Use warn, not error: callers handle this and console.error triggers
|
|
1013
|
-
// the Next.js dev overlay unnecessarily.
|
|
1014
688
|
console.warn('[P1PuckProvider] loadDocument failed:', error);
|
|
1015
|
-
// Unload the current document so VersionBannerOverride shows the empty
|
|
1016
|
-
// state instead of the previous document's content.
|
|
1017
689
|
commitCurrentData(null, null);
|
|
1018
690
|
setCurrentDocument(null);
|
|
1019
691
|
throw error;
|
|
1020
692
|
}
|
|
1021
693
|
finally {
|
|
1022
|
-
// Guarded so a stale call's completion can't clear the flag while a
|
|
1023
|
-
// newer load is still in flight.
|
|
1024
694
|
if (thisRequestId === loadRequestIdRef.current) {
|
|
1025
695
|
setDocumentLoading(false);
|
|
1026
696
|
}
|
|
1027
697
|
}
|
|
1028
698
|
}, [userClient, siteId, branchId, cancelPendingRemoteSync, enableRealtime, debouncedSave, performSave, commitCurrentData]);
|
|
1029
|
-
|
|
699
|
+
loadDocForResetRef.current = loadDocument;
|
|
1030
700
|
const loadVersion = useCallback(async (version) => {
|
|
1031
|
-
// Cancel any pending remote sync to prevent race conditions
|
|
1032
|
-
// where a stale remote update overrides the version we're loading
|
|
1033
701
|
cancelPendingRemoteSync();
|
|
1034
702
|
try {
|
|
1035
703
|
const doc = currentDocumentRef.current;
|
|
@@ -1037,30 +705,19 @@ userRole = 'editor', children, }) {
|
|
|
1037
705
|
throw new Error('No document loaded');
|
|
1038
706
|
}
|
|
1039
707
|
let versionToUse = version;
|
|
1040
|
-
// If the version doesn't have snapshot data, try fetching it from the API
|
|
1041
708
|
if (!version.snapshot || Object.keys(version.snapshot).length === 0) {
|
|
1042
709
|
try {
|
|
1043
710
|
versionToUse = await userClient.versions.get(siteId, branchId, doc.id, version.id);
|
|
1044
711
|
}
|
|
1045
712
|
catch (fetchError) {
|
|
1046
|
-
// If fetching fails, log a warning and use the version as-is
|
|
1047
713
|
console.warn('Could not fetch full version data, using version from list:', fetchError);
|
|
1048
714
|
}
|
|
1049
715
|
}
|
|
1050
|
-
// Parses a JSON-string snapshot and falls back to blank Puck data for
|
|
1051
|
-
// an empty/invalid snapshot (e.g. version 1's initial blank state).
|
|
1052
716
|
const puckData = snapshotToPuckData(versionToUse.snapshot);
|
|
1053
|
-
// IMPORTANT: Update the ref BEFORE state updates to block any incoming remote updates
|
|
1054
|
-
// The effect that syncs viewingVersionRef runs AFTER render, which is too late
|
|
1055
|
-
// Remote updates check this ref, so we need it set synchronously
|
|
1056
717
|
viewingVersionRef.current = versionToUse;
|
|
1057
|
-
// Historical: the document-sync plugin must not push this into the
|
|
1058
|
-
// canvas that owns the live undo stack — the version bridge handles it.
|
|
1059
718
|
commitCurrentData(puckData, originFor(doc, branchId, versionToUse.id, true));
|
|
1060
719
|
setViewingVersion(versionToUse);
|
|
1061
|
-
// Clear remoteSyncKey so version sync takes priority
|
|
1062
720
|
setRemoteSyncKey(null);
|
|
1063
|
-
// Pause auto-save when viewing historical version
|
|
1064
721
|
debouncedSave.pause();
|
|
1065
722
|
setAutoSavePaused(true);
|
|
1066
723
|
pendingDataRef.current = null;
|
|
@@ -1071,13 +728,8 @@ userRole = 'editor', children, }) {
|
|
|
1071
728
|
throw error;
|
|
1072
729
|
}
|
|
1073
730
|
}, [userClient, siteId, branchId, debouncedSave, cancelPendingRemoteSync, commitCurrentData]);
|
|
1074
|
-
// Return to the latest version
|
|
1075
731
|
const returnToLatest = useCallback(async () => {
|
|
1076
|
-
// Cancel any pending remote sync to prevent race conditions
|
|
1077
|
-
// The latest data will be loaded fresh, then remote sync can resume
|
|
1078
732
|
cancelPendingRemoteSync();
|
|
1079
|
-
// Get current state from Yjs if realtime is connected
|
|
1080
|
-
// This ensures we get any changes made by other users while viewing history
|
|
1081
733
|
let dataToRestore = null;
|
|
1082
734
|
if (enableRealtime && realtime.connected) {
|
|
1083
735
|
const currentYjsData = realtime.getSnapshot();
|
|
@@ -1085,20 +737,12 @@ userRole = 'editor', children, }) {
|
|
|
1085
737
|
dataToRestore = currentYjsData;
|
|
1086
738
|
}
|
|
1087
739
|
}
|
|
1088
|
-
// 1. Edits typed inside the debounce window — cleared from pendingDataRef by loadVersion but still live here.
|
|
1089
740
|
const hadLocalEdits = !!latestLocalDataRef.current;
|
|
1090
741
|
dataToRestore ??= latestLocalDataRef.current;
|
|
1091
|
-
latestLocalDataRef.current = null;
|
|
1092
|
-
// Re-fetch the true latest from the server when no live Yjs snapshot is
|
|
1093
|
-
// available. The cached latestVersionDataRef is only a last-resort fallback
|
|
1094
|
-
// (e.g. no current document); it is NOT used on fetch failure — silently
|
|
1095
|
-
// editing on a possibly-stale snapshot and resuming autosave is the exact
|
|
1096
|
-
// data-loss path from PCC-3421. On failure we notify and abort instead,
|
|
1097
|
-
// keeping the user in read-only preview so nothing is overwritten.
|
|
742
|
+
latestLocalDataRef.current = null;
|
|
1098
743
|
if (!dataToRestore) {
|
|
1099
744
|
const doc = currentDocumentRef.current;
|
|
1100
745
|
if (doc) {
|
|
1101
|
-
// 2. Authoritative server state — the only source that reflects saves from other tabs, users, or agents.
|
|
1102
746
|
setIsReturningToLatest(true);
|
|
1103
747
|
try {
|
|
1104
748
|
const version = await userClient.versions.getLatest(siteId, branchId, doc.id);
|
|
@@ -1116,16 +760,10 @@ userRole = 'editor', children, }) {
|
|
|
1116
760
|
setIsReturningToLatest(false);
|
|
1117
761
|
}
|
|
1118
762
|
}
|
|
1119
|
-
// 3. No document context — can't fetch; use last value written by performSave.
|
|
1120
763
|
dataToRestore ??= latestVersionDataRef.current;
|
|
1121
764
|
}
|
|
1122
765
|
if (dataToRestore) {
|
|
1123
|
-
// IMPORTANT: Update the ref BEFORE state updates
|
|
1124
|
-
// Setting to null allows remote updates to resume
|
|
1125
766
|
viewingVersionRef.current = null;
|
|
1126
|
-
// Suppress the PuckDataSynchronizer echo: setCurrentData triggers a setData
|
|
1127
|
-
// dispatch and Puck fires onChange → saveData. Arm the ref so saveData drops
|
|
1128
|
-
// that echo; we control the save explicitly below.
|
|
1129
767
|
suppressNextSaveRef.current = dataToRestore;
|
|
1130
768
|
if (enableRealtime) {
|
|
1131
769
|
pendingRemoteUpdatesRef.current += 1;
|
|
@@ -1136,14 +774,9 @@ userRole = 'editor', children, }) {
|
|
|
1136
774
|
commitCurrentData(dataToRestore, originFor(currentDocumentRef.current, branchId, null, false));
|
|
1137
775
|
latestVersionDataRef.current = dataToRestore;
|
|
1138
776
|
setViewingVersion(null);
|
|
1139
|
-
// Clear remoteSyncKey so latest version sync takes priority
|
|
1140
777
|
setRemoteSyncKey(null);
|
|
1141
|
-
// Resume auto-save
|
|
1142
778
|
debouncedSave.resume();
|
|
1143
779
|
setAutoSavePaused(false);
|
|
1144
|
-
// Only queue a save when restoring from local in-memory edits — those were
|
|
1145
|
-
// never persisted. Data from the server fetch is already the latest saved
|
|
1146
|
-
// version; queuing a save would create a spurious identical version.
|
|
1147
780
|
if (hadLocalEdits) {
|
|
1148
781
|
pendingDataRef.current = dataToRestore;
|
|
1149
782
|
debouncedSave();
|
|
@@ -1151,12 +784,7 @@ userRole = 'editor', children, }) {
|
|
|
1151
784
|
setSaveStatus('idle');
|
|
1152
785
|
}
|
|
1153
786
|
}, [debouncedSave, cancelPendingRemoteSync, enableRealtime, realtime, userClient, siteId, branchId, showErrorNotifications, notificationContext, commitCurrentData]);
|
|
1154
|
-
// Computed property for whether viewing historical version
|
|
1155
787
|
const isViewingHistoricalVersion = viewingVersion !== null;
|
|
1156
|
-
// =========================================================================
|
|
1157
|
-
// Content Type Templates: Permission Resolution (PROPOSAL-010)
|
|
1158
|
-
// =========================================================================
|
|
1159
|
-
// Resolve template: either from document binding or registry path
|
|
1160
788
|
const resolvedTemplate = useMemo(() => {
|
|
1161
789
|
if (currentTemplate)
|
|
1162
790
|
return currentTemplate;
|
|
@@ -1168,17 +796,12 @@ userRole = 'editor', children, }) {
|
|
|
1168
796
|
return null;
|
|
1169
797
|
return branchTemplates.find((t) => t.name === match[1]) ?? null;
|
|
1170
798
|
}, [currentTemplate, currentDocument?.path, branchTemplates]);
|
|
1171
|
-
// Create Puck permissions resolver based on current template and user role
|
|
1172
799
|
const resolvePermissions = useMemo(() => createPuckPermissions(resolvedTemplate, userRole, isViewingHistoricalVersion), [resolvedTemplate, userRole, isViewingHistoricalVersion]);
|
|
1173
|
-
// Show notification when template list fails to load
|
|
1174
800
|
useEffect(() => {
|
|
1175
801
|
if (templatesError && showErrorNotifications && notificationContext?.addError) {
|
|
1176
802
|
notificationContext.addError(`Failed to load templates: ${templatesError.message}`, () => void refreshTemplates());
|
|
1177
803
|
}
|
|
1178
804
|
}, [templatesError]);
|
|
1179
|
-
// =========================================================================
|
|
1180
|
-
// Content Type Templates: Re-fetch template on tab focus (CUJ-15a)
|
|
1181
|
-
// =========================================================================
|
|
1182
805
|
useEffect(() => {
|
|
1183
806
|
if (typeof document === 'undefined')
|
|
1184
807
|
return;
|
|
@@ -1197,37 +820,26 @@ userRole = 'editor', children, }) {
|
|
|
1197
820
|
});
|
|
1198
821
|
}
|
|
1199
822
|
catch {
|
|
1200
|
-
// Silently ignore — stale template is better than no template
|
|
1201
823
|
}
|
|
1202
824
|
};
|
|
1203
825
|
document.addEventListener('visibilitychange', handler);
|
|
1204
826
|
return () => document.removeEventListener('visibilitychange', handler);
|
|
1205
827
|
}, [siteId, userClient]);
|
|
1206
|
-
// =========================================================================
|
|
1207
|
-
// Phase 9: Presence & Agent Mode State
|
|
1208
|
-
// =========================================================================
|
|
1209
|
-
// Presence state (when presenceEnabled)
|
|
1210
828
|
const [presenceActors, setPresenceActors] = useState([]);
|
|
1211
829
|
const presenceInitializedRef = useRef(false);
|
|
1212
|
-
// Track previous actors for onPresenceChange callback comparison
|
|
1213
830
|
const prevActorsRef = useRef([]);
|
|
1214
|
-
// Conflict notifications state
|
|
1215
831
|
const [conflicts, setConflicts] = useState([]);
|
|
1216
|
-
// Agent edit session state (when agentModeEnabled && agentId)
|
|
1217
832
|
const [agentSession, setAgentSession] = useState(null);
|
|
1218
833
|
const [agentEditLoading, setAgentEditLoading] = useState(false);
|
|
1219
834
|
const [agentEditError, setAgentEditError] = useState(null);
|
|
1220
|
-
// Fetch presence data
|
|
1221
835
|
const fetchPresence = useCallback(async () => {
|
|
1222
836
|
if (!presenceEnabled || !branchId)
|
|
1223
837
|
return;
|
|
1224
838
|
try {
|
|
1225
839
|
const branchPresence = await userClient.presence.getBranchPresence(siteId, branchId);
|
|
1226
|
-
// Filter out self and enrich with names
|
|
1227
840
|
const filteredActors = branchPresence.actors.filter((actor) => actor.actorId !== userId);
|
|
1228
841
|
const enrichedActors = enrichActorsWithNames(filteredActors);
|
|
1229
842
|
setPresenceActors(enrichedActors);
|
|
1230
|
-
// Call onPresenceChange if actors changed
|
|
1231
843
|
if (onPresenceChange) {
|
|
1232
844
|
const actorsChanged = JSON.stringify(enrichedActors.map((a) => a.id).sort()) !==
|
|
1233
845
|
JSON.stringify(prevActorsRef.current.map((a) => a.id).sort());
|
|
@@ -1241,27 +853,19 @@ userRole = 'editor', children, }) {
|
|
|
1241
853
|
console.error('Failed to fetch presence:', error);
|
|
1242
854
|
}
|
|
1243
855
|
}, [presenceEnabled, branchId, siteId, userId, userClient, onPresenceChange, enrichActorsWithNames]);
|
|
1244
|
-
// Keep fetchPresence in a ref to avoid restarting the interval when callback changes
|
|
1245
856
|
const fetchPresenceRef = useRef(fetchPresence);
|
|
1246
857
|
useEffect(() => {
|
|
1247
858
|
fetchPresenceRef.current = fetchPresence;
|
|
1248
859
|
}, [fetchPresence]);
|
|
1249
|
-
// Initial presence fetch and polling
|
|
1250
|
-
// HTTP polling is skipped when WebSocket presence is active and connected
|
|
1251
860
|
useEffect(() => {
|
|
1252
861
|
if (!presenceEnabled)
|
|
1253
862
|
return;
|
|
1254
|
-
// Skip HTTP polling if WebSocket presence is handling updates
|
|
1255
863
|
const shouldSkipPolling = wsPresenceActive && realtime.connected;
|
|
1256
|
-
// Initial fetch (only if WS isn't active yet)
|
|
1257
864
|
if (!presenceInitializedRef.current && !shouldSkipPolling) {
|
|
1258
865
|
presenceInitializedRef.current = true;
|
|
1259
866
|
void fetchPresenceRef.current();
|
|
1260
867
|
}
|
|
1261
|
-
// Set up polling - use ref to avoid restarting interval when callback changes
|
|
1262
|
-
// Skip polling when WebSocket is handling presence
|
|
1263
868
|
const intervalId = setInterval(() => {
|
|
1264
|
-
// Check again at each interval - WS state may have changed
|
|
1265
869
|
if (!wsPresenceActive || !realtime.connected) {
|
|
1266
870
|
void fetchPresenceRef.current();
|
|
1267
871
|
}
|
|
@@ -1270,29 +874,21 @@ userRole = 'editor', children, }) {
|
|
|
1270
874
|
clearInterval(intervalId);
|
|
1271
875
|
};
|
|
1272
876
|
}, [presenceEnabled, presencePollingInterval, realtime.connected]);
|
|
1273
|
-
// Reset presence when disabled
|
|
1274
877
|
useEffect(() => {
|
|
1275
878
|
if (!presenceEnabled) {
|
|
1276
879
|
setPresenceActors([]);
|
|
1277
880
|
presenceInitializedRef.current = false;
|
|
1278
881
|
}
|
|
1279
882
|
}, [presenceEnabled]);
|
|
1280
|
-
// Reset WebSocket presence state when disconnected so the UI falls back
|
|
1281
|
-
// to HTTP-polled presence data. Without this, wsPresenceActive stays
|
|
1282
|
-
// true after disconnect and the UI shows stale WebSocket presence
|
|
1283
|
-
// (e.g. an agent that already completed its edit session still appears).
|
|
1284
883
|
useEffect(() => {
|
|
1285
884
|
if (!realtime.connected) {
|
|
1286
885
|
setWsPresenceActive(false);
|
|
1287
886
|
setWsPresenceActors([]);
|
|
1288
887
|
}
|
|
1289
888
|
}, [realtime.connected]);
|
|
1290
|
-
// Compute derived presence values
|
|
1291
|
-
// Prefer WebSocket presence when connected for instant updates
|
|
1292
889
|
const presenceState = useMemo(() => {
|
|
1293
890
|
if (!presenceEnabled)
|
|
1294
891
|
return null;
|
|
1295
|
-
// Use WebSocket presence when active and connected, otherwise fall back to HTTP polling data
|
|
1296
892
|
const effectiveActors = wsPresenceActive && realtime.connected ? wsPresenceActors : presenceActors;
|
|
1297
893
|
const humans = effectiveActors.filter((actor) => actor.role === 'human');
|
|
1298
894
|
const agents = effectiveActors.filter((actor) => actor.role === 'agent');
|
|
@@ -1307,16 +903,10 @@ userRole = 'editor', children, }) {
|
|
|
1307
903
|
refresh: fetchPresence,
|
|
1308
904
|
};
|
|
1309
905
|
}, [presenceEnabled, presenceActors, wsPresenceActors, wsPresenceActive, realtime.connected, fetchPresence]);
|
|
1310
|
-
// Keep presence in a ref so it can be read via getter without triggering
|
|
1311
|
-
// context recreation. Presence changes frequently (focus region broadcasts)
|
|
1312
|
-
// but shouldn't cause PuckDataSynchronizer or plugin re-renders.
|
|
1313
906
|
const presenceStateRef = useRef(presenceState);
|
|
1314
907
|
presenceStateRef.current = presenceState;
|
|
1315
908
|
const humanPresenceKey = presenceIdentityKey(presenceState?.humans);
|
|
1316
909
|
const agentPresenceKey = presenceIdentityKey(presenceState?.agents);
|
|
1317
|
-
// =========================================================================
|
|
1318
|
-
// Phase 9: Agent Edit Capabilities (when this client IS an agent)
|
|
1319
|
-
// =========================================================================
|
|
1320
910
|
const agentEditCapabilities = useMemo(() => {
|
|
1321
911
|
if (!agentModeEnabled || !agentId)
|
|
1322
912
|
return null;
|
|
@@ -1423,11 +1013,7 @@ userRole = 'editor', children, }) {
|
|
|
1423
1013
|
agentEditLoading,
|
|
1424
1014
|
agentEditError,
|
|
1425
1015
|
]);
|
|
1426
|
-
// =========================================================================
|
|
1427
|
-
// Phase 9: Agent Trigger (for human users to trigger agents)
|
|
1428
|
-
// =========================================================================
|
|
1429
1016
|
const triggerAgentFn = useMemo(() => {
|
|
1430
|
-
// Only available when agentModeEnabled but no agentId (human user)
|
|
1431
1017
|
if (!agentModeEnabled || agentId)
|
|
1432
1018
|
return null;
|
|
1433
1019
|
const documentPath = currentDocument?.path ?? null;
|
|
@@ -1436,7 +1022,6 @@ userRole = 'editor', children, }) {
|
|
|
1436
1022
|
return { success: false, error: 'No document loaded' };
|
|
1437
1023
|
}
|
|
1438
1024
|
try {
|
|
1439
|
-
// Check permission
|
|
1440
1025
|
const permission = await userClient.agentEdit.canEdit(siteId, branchId, documentPath, {
|
|
1441
1026
|
agentId: action.agentId,
|
|
1442
1027
|
trigger: 'human_requested',
|
|
@@ -1447,7 +1032,6 @@ userRole = 'editor', children, }) {
|
|
|
1447
1032
|
if (!permission.allowed) {
|
|
1448
1033
|
return { success: false, error: permission.reason };
|
|
1449
1034
|
}
|
|
1450
|
-
// Start edit session
|
|
1451
1035
|
const session = await userClient.agentEdit.startEdit(siteId, branchId, documentPath, {
|
|
1452
1036
|
agentId: action.agentId,
|
|
1453
1037
|
trigger: 'human_requested',
|
|
@@ -1465,13 +1049,9 @@ userRole = 'editor', children, }) {
|
|
|
1465
1049
|
}
|
|
1466
1050
|
};
|
|
1467
1051
|
}, [agentModeEnabled, agentId, currentDocument, siteId, branchId, userId, userClient]);
|
|
1468
|
-
// =========================================================================
|
|
1469
|
-
// Phase 9: Conflict Notifications
|
|
1470
|
-
// =========================================================================
|
|
1471
1052
|
const dismissConflict = useCallback((id) => {
|
|
1472
1053
|
setConflicts((prev) => prev.filter((c) => c.id !== id));
|
|
1473
1054
|
}, []);
|
|
1474
|
-
// Stop an agent's edit session (human-initiated)
|
|
1475
1055
|
const handleStopAgent = useCallback(async (agent) => {
|
|
1476
1056
|
const documentPath = currentDocumentRef.current?.path;
|
|
1477
1057
|
if (!documentPath) {
|
|
@@ -1481,7 +1061,6 @@ userRole = 'editor', children, }) {
|
|
|
1481
1061
|
try {
|
|
1482
1062
|
await userClient.agentEdit.stopAgent(siteId, branchId, documentPath, agent.actorId);
|
|
1483
1063
|
notificationContext.addSuccess(`Agent "${agent.name}" has been stopped`);
|
|
1484
|
-
// Refresh presence to reflect the agent's removal
|
|
1485
1064
|
if (presenceEnabled) {
|
|
1486
1065
|
void fetchPresenceRef.current();
|
|
1487
1066
|
}
|
|
@@ -1491,12 +1070,9 @@ userRole = 'editor', children, }) {
|
|
|
1491
1070
|
notificationContext.addError(`Failed to stop agent: ${message}`);
|
|
1492
1071
|
}
|
|
1493
1072
|
}, [userClient, siteId, branchId, notificationContext, presenceEnabled]);
|
|
1494
|
-
// Create checkpoint
|
|
1495
1073
|
const createCheckpoint = useCallback(async (name) => {
|
|
1496
|
-
// Save any pending changes first
|
|
1497
1074
|
if (pendingDataRef.current) {
|
|
1498
1075
|
if (enableRealtime && realtimeConnectedRef.current) {
|
|
1499
|
-
// Data origin guard: only send if data belongs to current document
|
|
1500
1076
|
const currentPath = currentDocumentRef.current?.path ?? null;
|
|
1501
1077
|
if (currentDataDocumentPathRef.current === currentPath) {
|
|
1502
1078
|
realtime.applyLocalChange(pendingDataRef.current);
|
|
@@ -1508,8 +1084,6 @@ userRole = 'editor', children, }) {
|
|
|
1508
1084
|
await performSave();
|
|
1509
1085
|
}
|
|
1510
1086
|
}
|
|
1511
|
-
// When realtime is connected, wait for the DO to acknowledge receipt of
|
|
1512
|
-
// all preceding WebSocket messages before creating the checkpoint.
|
|
1513
1087
|
if (enableRealtime && realtimeConnectedRef.current) {
|
|
1514
1088
|
try {
|
|
1515
1089
|
await realtime.waitForDelivery();
|
|
@@ -1525,13 +1099,11 @@ userRole = 'editor', children, }) {
|
|
|
1525
1099
|
});
|
|
1526
1100
|
return checkpoint;
|
|
1527
1101
|
}, [userClient, siteId, branchId, debouncedSave, performSave, enableRealtime, realtime]);
|
|
1528
|
-
// Publish current document
|
|
1529
1102
|
const publishDocument = useCallback(async () => {
|
|
1530
1103
|
const doc = currentDocumentRef.current;
|
|
1531
1104
|
if (!doc) {
|
|
1532
1105
|
throw new Error('No document loaded to publish');
|
|
1533
1106
|
}
|
|
1534
|
-
// Send any pending changes before publish
|
|
1535
1107
|
if (pendingDataRef.current) {
|
|
1536
1108
|
if (enableRealtime && realtimeConnectedRef.current) {
|
|
1537
1109
|
const currentPath = currentDocumentRef.current?.path ?? null;
|
|
@@ -1545,10 +1117,6 @@ userRole = 'editor', children, }) {
|
|
|
1545
1117
|
await performSave();
|
|
1546
1118
|
}
|
|
1547
1119
|
}
|
|
1548
|
-
// When realtime is connected, use WebSocket-driven publish.
|
|
1549
|
-
// The DO handles the entire flow: flush CRDT to Postgres, then
|
|
1550
|
-
// create the checkpoint. TCP ordering guarantees all preceding
|
|
1551
|
-
// binary CRDT updates are processed before the publish_request.
|
|
1552
1120
|
if (enableRealtime && realtimeConnectedRef.current) {
|
|
1553
1121
|
const publishResult = await realtime.requestPublish();
|
|
1554
1122
|
if (!publishResult.success) {
|
|
@@ -1559,61 +1127,29 @@ userRole = 'editor', children, }) {
|
|
|
1559
1127
|
}
|
|
1560
1128
|
return publishResult.checkpoint;
|
|
1561
1129
|
}
|
|
1562
|
-
// Fallback: HTTP publish when realtime is not connected
|
|
1563
1130
|
const result = await userClient.documents.publish(siteId, branchId, doc.id);
|
|
1564
1131
|
return result.checkpoint;
|
|
1565
1132
|
}, [userClient, siteId, branchId, debouncedSave, performSave, enableRealtime, realtime]);
|
|
1566
|
-
// Tracks the currently in-flight switchBranch flush (everything after the
|
|
1567
|
-
// synchronous branch-selection commit), so a rapid re-selection can await
|
|
1568
|
-
// the prior call instead of racing it. See switchBranch below.
|
|
1569
1133
|
const switchBranchInFlightRef = useRef(null);
|
|
1570
|
-
// Switch branch
|
|
1571
1134
|
const switchBranch = useCallback(async (newBranchId) => {
|
|
1572
|
-
// Commit the new workstream selection FIRST, synchronously, before the
|
|
1573
|
-
// async save-flush below. The UI (WorkstreamSwitcher) fires this inside
|
|
1574
|
-
// startTransition without awaiting it, so its "Switching..." busy state
|
|
1575
|
-
// clears as soon as this function's synchronous portion returns — well
|
|
1576
|
-
// before a save-flush of the outgoing document would resolve. If the
|
|
1577
|
-
// user navigates to a different document in that window, this provider
|
|
1578
|
-
// can remount (the editor route has no layout.tsx to shield it across
|
|
1579
|
-
// param changes), and its branchId initializer re-reads sessionStorage.
|
|
1580
|
-
// Persisting here — before the flush, not after — guarantees that read
|
|
1581
|
-
// always sees the newly selected branch instead of a stale one,
|
|
1582
|
-
// preventing the workstream selector from snapping back to Live/main.
|
|
1583
1135
|
setBranchId(newBranchId);
|
|
1584
1136
|
persistBranchId(newBranchId);
|
|
1585
1137
|
const branch = branches.find((b) => b.id === newBranchId);
|
|
1586
1138
|
setCurrentBranch(branch ?? null);
|
|
1587
|
-
// Serialize the flush portion below against any switchBranch call
|
|
1588
|
-
// still in flight. Because the branch commit above is synchronous but
|
|
1589
|
-
// the flush is not, a rapid second call (the switcher fires onSwitch
|
|
1590
|
-
// fire-and-forget, so its busy state clears well before the flush
|
|
1591
|
-
// resolves) can otherwise start its own flush while pendingDataRef
|
|
1592
|
-
// still holds the first call's outgoing edit — grabbing a performSave
|
|
1593
|
-
// closure already pointed at the intermediate branch and saving that
|
|
1594
|
-
// same edit a second time, to the wrong branch. Awaiting the prior
|
|
1595
|
-
// in-flight flush first guarantees only one flush ever reads/clears
|
|
1596
|
-
// pendingDataRef at a time, and that each flush's performSave targets
|
|
1597
|
-
// the branch that was current when that flush actually started.
|
|
1598
1139
|
const previousSwitch = switchBranchInFlightRef.current;
|
|
1599
1140
|
const thisSwitch = (async () => {
|
|
1600
1141
|
if (previousSwitch) {
|
|
1601
|
-
// A prior switch's flush failure shouldn't block this one.
|
|
1602
1142
|
await previousSwitch.catch(() => undefined);
|
|
1603
1143
|
}
|
|
1604
|
-
// Cancel any pending remote sync - we're switching to a different branch
|
|
1605
1144
|
cancelPendingRemoteSync();
|
|
1606
|
-
// Save any pending changes on the outgoing document/branch first
|
|
1607
1145
|
if (pendingDataRef.current) {
|
|
1608
1146
|
debouncedSave.cancel();
|
|
1609
1147
|
await performSave();
|
|
1610
1148
|
}
|
|
1611
|
-
// IMPORTANT: Update the ref BEFORE state updates
|
|
1612
1149
|
viewingVersionRef.current = null;
|
|
1613
1150
|
setCurrentDocument(null);
|
|
1614
1151
|
commitCurrentData(null, null);
|
|
1615
1152
|
setViewingVersion(null);
|
|
1616
|
-
// Clear remoteSyncKey so new branch document sync takes priority
|
|
1617
1153
|
setRemoteSyncKey(null);
|
|
1618
1154
|
pendingDataRef.current = null;
|
|
1619
1155
|
setSaveStatus('idle');
|
|
@@ -1629,7 +1165,6 @@ userRole = 'editor', children, }) {
|
|
|
1629
1165
|
}
|
|
1630
1166
|
}
|
|
1631
1167
|
}, [branches, debouncedSave, performSave, cancelPendingRemoteSync, persistBranchId, commitCurrentData]);
|
|
1632
|
-
// Presence context value (for hooks like useFocusRegionReporting)
|
|
1633
1168
|
const presenceContextValue = useMemo(() => {
|
|
1634
1169
|
if (!presenceEnabled)
|
|
1635
1170
|
return null;
|
|
@@ -1649,8 +1184,6 @@ userRole = 'editor', children, }) {
|
|
|
1649
1184
|
?.filter(a => a.state === 'editing')
|
|
1650
1185
|
?.flatMap(a => a.focusRegions ?? []) ?? [],
|
|
1651
1186
|
isAgentEditing: presenceState?.hasActiveAgents ?? false,
|
|
1652
|
-
// Agent edit functions - placeholder implementations
|
|
1653
|
-
// These are handled by agentEditCapabilities in the main context
|
|
1654
1187
|
canEdit: async () => ({ allowed: true }),
|
|
1655
1188
|
startEdit: async () => ({ sessionId: '' }),
|
|
1656
1189
|
completeEdit: async () => ({ success: true }),
|
|
@@ -1667,13 +1200,6 @@ userRole = 'editor', children, }) {
|
|
|
1667
1200
|
realtime.connected,
|
|
1668
1201
|
presenceState,
|
|
1669
1202
|
]);
|
|
1670
|
-
// =========================================================================
|
|
1671
|
-
// Stable Callback Wrappers
|
|
1672
|
-
// =========================================================================
|
|
1673
|
-
// Wrap volatile callbacks in refs + stable wrappers so consumers don't need
|
|
1674
|
-
// to stabilize them manually. The ref is updated inline during render (safe
|
|
1675
|
-
// because it's a ref write, not a state write) and the stable wrapper
|
|
1676
|
-
// delegates to ref.current, ensuring the latest implementation is always used.
|
|
1677
1203
|
const saveDataRef = useRef(saveData);
|
|
1678
1204
|
saveDataRef.current = saveData;
|
|
1679
1205
|
const stableSaveData = useCallback((data) => saveDataRef.current(data), []);
|
|
@@ -1698,9 +1224,8 @@ userRole = 'editor', children, }) {
|
|
|
1698
1224
|
const switchBranchRef = useRef(switchBranch);
|
|
1699
1225
|
switchBranchRef.current = switchBranch;
|
|
1700
1226
|
const stableSwitchBranch = useCallback((newBranchId) => switchBranchRef.current(newBranchId), []);
|
|
1701
|
-
// Get pending actions for forwarding to backend
|
|
1702
1227
|
const getPendingActions = useCallback(() => {
|
|
1703
|
-
return [...pendingActionsRef.current];
|
|
1228
|
+
return [...pendingActionsRef.current];
|
|
1704
1229
|
}, []);
|
|
1705
1230
|
const loadDocumentRef = useRef(loadDocument);
|
|
1706
1231
|
loadDocumentRef.current = loadDocument;
|
|
@@ -1714,10 +1239,6 @@ userRole = 'editor', children, }) {
|
|
|
1714
1239
|
const handleStopAgentRef = useRef(handleStopAgent);
|
|
1715
1240
|
handleStopAgentRef.current = handleStopAgent;
|
|
1716
1241
|
const stableStopAgent = useCallback((agent) => handleStopAgentRef.current(agent), []);
|
|
1717
|
-
// =========================================================================
|
|
1718
|
-
// Stable Getters (Items 2 & 3)
|
|
1719
|
-
// =========================================================================
|
|
1720
|
-
// Refs updated inline during render, exposed as stable getter callbacks.
|
|
1721
1242
|
const saveStatusGetterRef = useRef(saveStatus);
|
|
1722
1243
|
saveStatusGetterRef.current = saveStatus;
|
|
1723
1244
|
const getSaveStatus = useCallback(() => saveStatusGetterRef.current, []);
|
|
@@ -1728,7 +1249,6 @@ userRole = 'editor', children, }) {
|
|
|
1728
1249
|
saveErrorGetterRef.current = saveError;
|
|
1729
1250
|
const getSaveError = useCallback(() => saveErrorGetterRef.current, []);
|
|
1730
1251
|
const getHasUnsavedChanges = useCallback(() => pendingDataRef.current !== null, []);
|
|
1731
|
-
// Data sync getters (Item 3)
|
|
1732
1252
|
const currentDataGetterRef = useRef(currentData);
|
|
1733
1253
|
currentDataGetterRef.current = currentData;
|
|
1734
1254
|
const remoteSyncKeyGetterRef = useRef(remoteSyncKey);
|
|
@@ -1750,9 +1270,6 @@ userRole = 'editor', children, }) {
|
|
|
1750
1270
|
return `doc-${doc.id}-latest`;
|
|
1751
1271
|
return undefined;
|
|
1752
1272
|
}, []);
|
|
1753
|
-
// =========================================================================
|
|
1754
|
-
// Plugin Composition (B.4)
|
|
1755
|
-
// =========================================================================
|
|
1756
1273
|
const resolvedFeatureConfig = useMemo(() => {
|
|
1757
1274
|
if (featureConfig)
|
|
1758
1275
|
return resolveFeatureConfig(featureConfig);
|
|
@@ -1763,27 +1280,24 @@ userRole = 'editor', children, }) {
|
|
|
1763
1280
|
});
|
|
1764
1281
|
}, [featureConfig, enableRealtime, presenceEnabled, agentModeEnabled]);
|
|
1765
1282
|
const activePlugins = useMemo(() => {
|
|
1766
|
-
const plugins = featurePlugins ??
|
|
1283
|
+
const plugins = featurePlugins ?? DEFAULT_CCR_FEATURE_PLUGINS;
|
|
1767
1284
|
return resolveActivePlugins(plugins, resolvedFeatureConfig);
|
|
1768
1285
|
}, [featurePlugins, resolvedFeatureConfig]);
|
|
1769
|
-
const pluginDeps =
|
|
1286
|
+
const pluginDeps = useStablePluginDeps({
|
|
1770
1287
|
client: userClient,
|
|
1771
1288
|
siteId,
|
|
1772
1289
|
branchId,
|
|
1773
1290
|
userId,
|
|
1774
1291
|
config: resolvedFeatureConfig,
|
|
1775
|
-
})
|
|
1292
|
+
});
|
|
1776
1293
|
const ComposedPluginProviders = useMemo(() => composeProviders(activePlugins, resolvedFeatureConfig, pluginDeps), [activePlugins, resolvedFeatureConfig, pluginDeps]);
|
|
1777
|
-
|
|
1778
|
-
// safeData (Item 4) — never null
|
|
1779
|
-
// =========================================================================
|
|
1294
|
+
const featurePuckPlugins = useMemo(() => collectPuckPlugins(activePlugins, pluginDeps), [activePlugins, pluginDeps]);
|
|
1780
1295
|
const EMPTY_PUCK_DATA = useMemo(() => ({ content: [], root: { props: {} } }), []);
|
|
1781
1296
|
const lastGoodDataRef = useRef(EMPTY_PUCK_DATA);
|
|
1782
1297
|
if (currentDocument && currentData) {
|
|
1783
1298
|
lastGoodDataRef.current = currentData;
|
|
1784
1299
|
}
|
|
1785
1300
|
const safeData = lastGoodDataRef.current;
|
|
1786
|
-
// Context value
|
|
1787
1301
|
const contextValue = useMemo(() => ({
|
|
1788
1302
|
client: userClient,
|
|
1789
1303
|
notifications: notificationContext,
|
|
@@ -1805,16 +1319,13 @@ userRole = 'editor', children, }) {
|
|
|
1805
1319
|
createCheckpoint: stableCreateCheckpoint,
|
|
1806
1320
|
publishDocument: stablePublishDocument,
|
|
1807
1321
|
switchBranch: stableSwitchBranch,
|
|
1808
|
-
// Stable getters (Items 2, 3)
|
|
1809
1322
|
getSaveStatus,
|
|
1810
1323
|
getLastSaved,
|
|
1811
1324
|
getSaveError,
|
|
1812
1325
|
getHasUnsavedChanges,
|
|
1813
1326
|
getSyncData,
|
|
1814
1327
|
getDataSyncKey,
|
|
1815
|
-
// safeData (Item 4)
|
|
1816
1328
|
safeData,
|
|
1817
|
-
// Documents (Item 8)
|
|
1818
1329
|
documents: branchDocuments,
|
|
1819
1330
|
documentsLoading,
|
|
1820
1331
|
refreshDocuments,
|
|
@@ -1833,9 +1344,6 @@ userRole = 'editor', children, }) {
|
|
|
1833
1344
|
pauseAutoSave: stablePauseAutoSave,
|
|
1834
1345
|
resumeAutoSave: stableResumeAutoSave,
|
|
1835
1346
|
viewingVersion,
|
|
1836
|
-
// Exposed for type/back-compat only — nothing renders from it. Read from
|
|
1837
|
-
// the ref; the memo already recomputes each save (via lastSaved), so this
|
|
1838
|
-
// stays adequately fresh without being a reactive dependency.
|
|
1839
1347
|
latestVersionData: latestVersionDataRef.current,
|
|
1840
1348
|
isViewingHistoricalVersion,
|
|
1841
1349
|
isReturningToLatest,
|
|
@@ -1844,16 +1352,9 @@ userRole = 'editor', children, }) {
|
|
|
1844
1352
|
realtimeEnabled: enableRealtime,
|
|
1845
1353
|
realtimeConnected: realtime.connected,
|
|
1846
1354
|
remoteSyncKey,
|
|
1847
|
-
// WebSocket presence - send focus regions via WebSocket when connected
|
|
1848
1355
|
sendFocusRegions: realtime.sendFocusRegions,
|
|
1849
|
-
// Puck action metadata capture - pass as onAction to <Puck>
|
|
1850
1356
|
handleAction,
|
|
1851
|
-
// Get pending actions for backend forwarding (PROPOSAL-010)
|
|
1852
1357
|
getPendingActions,
|
|
1853
|
-
// Phase 9: Presence & Agent values
|
|
1854
|
-
// Use getter to avoid context recreation on every focus-region update.
|
|
1855
|
-
// Expose humanPresenceCount/hasActiveHumans/hasActiveAgents directly so every
|
|
1856
|
-
// join/leave triggers context re-renders and causes consumers to re-invoke the getter.
|
|
1857
1358
|
get presence() { return presenceStateRef.current; },
|
|
1858
1359
|
hasActiveHumans: presenceState?.hasActiveHumans ?? false,
|
|
1859
1360
|
humanPresenceCount: presenceState?.humans.length ?? 0,
|
|
@@ -1863,12 +1364,10 @@ userRole = 'editor', children, }) {
|
|
|
1863
1364
|
stopAgent: stableStopAgent,
|
|
1864
1365
|
conflicts,
|
|
1865
1366
|
dismissConflict,
|
|
1866
|
-
// Feature configuration (Phase B.5)
|
|
1867
1367
|
featureConfig: resolvedFeatureConfig,
|
|
1868
|
-
|
|
1368
|
+
featurePuckPlugins,
|
|
1869
1369
|
_realtimeDataCaptureRef: enableRealtime ? realtimeDataCaptureRef : null,
|
|
1870
1370
|
_onRealtimeDataCapture: enableRealtime ? handleRealtimeDataCapture : null,
|
|
1871
|
-
// Content Type Templates (PROPOSAL-010)
|
|
1872
1371
|
userRole,
|
|
1873
1372
|
templates: branchTemplates,
|
|
1874
1373
|
templatesLoading,
|
|
@@ -1930,11 +1429,6 @@ userRole = 'editor', children, }) {
|
|
|
1930
1429
|
handleAction,
|
|
1931
1430
|
getPendingActions,
|
|
1932
1431
|
handleRealtimeDataCapture,
|
|
1933
|
-
// Phase 9 dependencies (full presenceState excluded — accessed via getter/ref,
|
|
1934
|
-
// but humanPresenceCount/hasActiveHumans/hasActiveAgents are direct values so every
|
|
1935
|
-
// join/leave triggers re-renders regardless of how many actors are present).
|
|
1936
|
-
// The roster keys cover the changes a count cannot see: a same-frame join/leave of
|
|
1937
|
-
// two humans would keep the count the same but change the roster.
|
|
1938
1432
|
humanPresenceKey,
|
|
1939
1433
|
agentPresenceKey,
|
|
1940
1434
|
presenceState?.humans.length,
|
|
@@ -1947,7 +1441,7 @@ userRole = 'editor', children, }) {
|
|
|
1947
1441
|
dismissConflict,
|
|
1948
1442
|
enableRealtime,
|
|
1949
1443
|
resolvedFeatureConfig,
|
|
1950
|
-
|
|
1444
|
+
featurePuckPlugins,
|
|
1951
1445
|
userRole,
|
|
1952
1446
|
branchTemplates,
|
|
1953
1447
|
templatesLoading,
|
|
@@ -1956,8 +1450,7 @@ userRole = 'editor', children, }) {
|
|
|
1956
1450
|
currentTemplate,
|
|
1957
1451
|
resolvePermissions,
|
|
1958
1452
|
]);
|
|
1959
|
-
// Wrap with PresenceContext.Provider when presence is enabled
|
|
1960
1453
|
const wrappedChildren = presenceContextValue ? (_jsx(PresenceContext.Provider, { value: presenceContextValue, children: children })) : (children);
|
|
1961
|
-
return (_jsx(P1PuckContext.Provider, { value: contextValue, children: _jsx(ComposedPluginProviders, { children: wrappedChildren }) }));
|
|
1454
|
+
return (_jsx(P1SdkQueryProvider, { children: _jsx(P1PuckContext.Provider, { value: contextValue, children: _jsx(ComposedPluginProviders, { children: wrappedChildren }) }) }));
|
|
1962
1455
|
}
|
|
1963
1456
|
//# sourceMappingURL=P1PuckProvider.js.map
|