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