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