@elytracms/studio 0.0.91 → 0.0.93
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/convex/asset-alt-migration.test.ts +127 -0
- package/convex/asset-replace.test.ts +168 -0
- package/convex/asset-revalidate.test.ts +6 -6
- package/convex/assets.ts +41 -5
- package/convex/cms.ts +537 -177
- package/convex/contract-parity.test.ts +173 -19
- package/convex/delivery.ts +26 -13
- package/convex/document-revalidate.test.ts +17 -16
- package/convex/orphan-hydration.test.ts +116 -0
- package/convex/presence-cost.test.ts +89 -0
- package/convex/rail-write-parity.test.ts +17 -14
- package/convex/referenceMaintenance.ts +15 -15
- package/convex/schema.ts +70 -5
- package/convex/triggers.ts +21 -8
- package/convex/write-limits.ts +1 -1
- package/dist/components/ai/ai-chat-panel.js +33 -22
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +8 -2
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +1 -1
- package/dist/components/ai/translate-document-dialog.js +14 -8
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -0
- package/dist/components/ai/translate-skipped.js +46 -0
- package/dist/components/ai/translate-skipped.js.map +1 -0
- package/dist/components/app-views/default-modules.d.ts +23 -6
- package/dist/components/app-views/default-modules.js +33 -14
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-detail.js +18 -8
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +15 -4
- package/dist/components/assets/asset-metadata.js +15 -2
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +4 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +0 -8
- package/dist/components/assets/asset-selection.js +4 -23
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +3 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
- package/dist/components/assets/assets-collection-view-source.js +9 -5
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +14 -0
- package/dist/components/assets/assets-store.js +23 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/replace-file.d.ts +22 -0
- package/dist/components/assets/replace-file.js +59 -0
- package/dist/components/assets/replace-file.js.map +1 -0
- package/dist/components/builder/blocks-model.d.ts +14 -26
- package/dist/components/builder/blocks-model.js +35 -41
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +7 -3
- package/dist/components/builder/blocks-panel.js +6 -5
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +126 -30
- package/dist/components/builder/builder-store.js +169 -67
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-body.d.ts +30 -0
- package/dist/components/builder/canvas-body.js +38 -0
- package/dist/components/builder/canvas-body.js.map +1 -0
- package/dist/components/builder/canvas-chrome-css.js +7 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -7
- package/dist/components/builder/canvas-dnd.js +5 -5
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +30 -22
- package/dist/components/builder/canvas-render-context.js +44 -44
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-runs.d.ts +45 -0
- package/dist/components/builder/canvas-runs.js +51 -0
- package/dist/components/builder/canvas-runs.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +5 -4
- package/dist/components/builder/canvas.js +34 -6
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +15 -0
- package/dist/components/builder/clipboard-actions.js +50 -13
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +10 -16
- package/dist/components/builder/composition-field-editor.js +30 -40
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/composition-patches.d.ts +7 -0
- package/dist/components/builder/composition-patches.js +256 -0
- package/dist/components/builder/composition-patches.js.map +1 -0
- package/dist/components/builder/delete-key.d.ts +5 -0
- package/dist/components/builder/delete-key.js +4 -0
- package/dist/components/builder/delete-key.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +17 -25
- package/dist/components/builder/insert-context.js +45 -92
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -3
- package/dist/components/builder/page-tree.js +3 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.d.ts +18 -0
- package/dist/components/builder/placement-refusal.js +46 -0
- package/dist/components/builder/placement-refusal.js.map +1 -0
- package/dist/components/builder/property-inspector.js +4 -0
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +65 -26
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +79 -8
- package/dist/components/builder/reuse.js +71 -23
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/run-inspector.d.ts +11 -0
- package/dist/components/builder/run-inspector.js +33 -0
- package/dist/components/builder/run-inspector.js.map +1 -0
- package/dist/components/cms/asset-field.d.ts +38 -1
- package/dist/components/cms/asset-field.js +182 -58
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +16 -0
- package/dist/components/cms/asset-picker-core.js +20 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +12 -18
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +4 -20
- package/dist/components/cms/bulk-actions.js +3 -24
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +5 -5
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +87 -45
- package/dist/components/cms/cms-store.js +347 -203
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/commit-policy.d.ts +24 -0
- package/dist/components/cms/commit-policy.js +37 -0
- package/dist/components/cms/commit-policy.js.map +1 -0
- package/dist/components/cms/detail-editor.js +83 -137
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +3 -1
- package/dist/components/cms/document-diff-view.js +12 -7
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/draft-commit.d.ts +70 -0
- package/dist/components/cms/draft-commit.js +82 -0
- package/dist/components/cms/draft-commit.js.map +1 -0
- package/dist/components/cms/duplicate-document.d.ts +11 -13
- package/dist/components/cms/duplicate-document.js +26 -34
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/field-anchors.d.ts +7 -4
- package/dist/components/cms/field-anchors.js +9 -7
- package/dist/components/cms/field-anchors.js.map +1 -1
- package/dist/components/cms/field-collaboration.d.ts +47 -0
- package/dist/components/cms/field-collaboration.js +87 -0
- package/dist/components/cms/field-collaboration.js.map +1 -0
- package/dist/components/cms/field-control-lib.d.ts +22 -1
- package/dist/components/cms/field-control-lib.js +33 -4
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +17 -14
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +21 -30
- package/dist/components/cms/gate-validation.js +11 -22
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/object-array-field.js +4 -4
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +7 -2
- package/dist/components/cms/prop-fields.js +13 -6
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +10 -4
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +20 -9
- package/dist/components/cms/rich-text/insert.js +25 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +96 -0
- package/dist/components/cms/use-document-draft.js +283 -0
- package/dist/components/cms/use-document-draft.js.map +1 -0
- package/dist/components/cms/use-document-versions.js +2 -2
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +6 -12
- package/dist/components/cms/use-live-url.js +16 -15
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +13 -18
- package/dist/components/cms/use-publish-controller.js +24 -41
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +2 -3
- package/dist/components/cms/use-published-perspective.js +4 -3
- package/dist/components/cms/use-published-perspective.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -1
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +24 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +28 -23
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +31 -21
- package/dist/components/components/transfer.js +49 -35
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +6 -3
- package/dist/components/project-session.js +14 -8
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-view-source.js +2 -1
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/section-nav.js +1 -1
- package/dist/components/section-nav.js.map +1 -1
- package/dist/lib/ai/apply-content.js +56 -116
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +8 -0
- package/dist/lib/ai/chat-context.js +37 -6
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +14 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +20 -5
- package/dist/lib/ai/constraints.js +125 -33
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +16 -3
- package/dist/lib/ai/fake-provider.js +22 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/leash.d.ts +34 -0
- package/dist/lib/ai/leash.js +28 -0
- package/dist/lib/ai/leash.js.map +1 -0
- package/dist/lib/ai/provenance.d.ts +1 -1
- package/dist/lib/ai/provenance.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +8 -8
- package/dist/lib/ai/rich-text-input.js +2 -1
- package/dist/lib/ai/rich-text-input.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.d.ts +27 -4
- package/dist/lib/ai/rich-text-translate.js +28 -10
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +13 -13
- package/dist/lib/ai/shape.js +69 -93
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +36 -41
- package/dist/lib/ai/tools.js +85 -87
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +39 -5
- package/dist/lib/ai/translate-document.js +160 -51
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +34 -23
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +21 -12
- package/dist/lib/ai/update-fields.js +118 -177
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +116 -0
- package/dist/lib/ai/writer-form.js +429 -0
- package/dist/lib/ai/writer-form.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +11 -11
- package/dist/lib/ai-current-document.js +4 -4
- package/dist/lib/ai-current-document.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +31 -2
- package/dist/lib/assets/upload.js +61 -6
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
- package/dist/lib/clipboard/os-clipboard.js +8 -4
- package/dist/lib/clipboard/os-clipboard.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +18 -9
- package/dist/lib/clipboard/paste-target.js +8 -7
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/field-patch.d.ts +15 -0
- package/dist/lib/cms/field-patch.js +25 -0
- package/dist/lib/cms/field-patch.js.map +1 -0
- package/dist/lib/cms/published-documents.d.ts +1 -1
- package/dist/lib/cms/published-documents.js +1 -1
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
- package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +4 -13
- package/dist/lib/data/collection-content.js +11 -5
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +25 -31
- package/dist/lib/dnd/drop-rules.js +53 -59
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +72 -15
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +61 -12
- package/dist/lib/i18n/messages.en.js +76 -16
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
- package/dist/lib/operations/__fixtures__/write-document.js +16 -0
- package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +124 -31
- package/dist/lib/operations/cms-persistence.js +447 -68
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
- package/dist/lib/operations/config-manifest-lookup.js +56 -0
- package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
- package/dist/lib/operations/project-runtime-config.d.ts +39 -0
- package/dist/lib/operations/project-runtime-config.js +54 -0
- package/dist/lib/operations/project-runtime-config.js.map +1 -0
- package/dist/lib/operations/publish-with-revision.d.ts +64 -0
- package/dist/lib/operations/publish-with-revision.js +66 -0
- package/dist/lib/operations/publish-with-revision.js.map +1 -0
- package/dist/lib/operations/runtime-validator.d.ts +32 -0
- package/dist/lib/operations/runtime-validator.js +18 -0
- package/dist/lib/operations/runtime-validator.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +31 -14
- package/dist/lib/operations/session-hydration.js +12 -16
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +59 -2
- package/dist/lib/operations/studio-operations.js +76 -14
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +13 -5
- package/dist/lib/persistence/convex-adapter.js +189 -74
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/convex-client.d.ts +65 -5
- package/dist/lib/persistence/convex-client.js +106 -3
- package/dist/lib/persistence/convex-client.js.map +1 -1
- package/dist/lib/persistence/index.js +17 -18
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.js +4 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +1 -1
- package/dist/lib/query/operations-query.js +1 -1
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/references/impact.d.ts +21 -15
- package/dist/lib/references/impact.js +13 -26
- package/dist/lib/references/impact.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +9 -0
- package/dist/lib/references/references-store.js +24 -4
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +6 -6
- package/dist/lib/routes/route-issues.js +26 -80
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/workspace/config-issues.js +2 -1
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +31 -1
- package/dist/lib/workspace/index.js +64 -17
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +3 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +2 -2
- package/dist/lib/workspace/workspace-source.js +2 -1
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +3 -3
- package/dist/components/cms/cms-composition-validators.d.ts +0 -16
- package/dist/components/cms/cms-composition-validators.js +0 -133
- package/dist/components/cms/cms-composition-validators.js.map +0 -1
- package/dist/components/cms/publish-clean.d.ts +0 -28
- package/dist/components/cms/publish-clean.js +0 -38
- package/dist/components/cms/publish-clean.js.map +0 -1
- package/dist/components/cms/use-save-mode.d.ts +0 -35
- package/dist/components/cms/use-save-mode.js +0 -54
- package/dist/components/cms/use-save-mode.js.map +0 -1
- package/dist/lib/assets/asset-placements.d.ts +0 -28
- package/dist/lib/assets/asset-placements.js +0 -83
- package/dist/lib/assets/asset-placements.js.map +0 -1
- package/dist/lib/cms/document-gate.d.ts +0 -47
- package/dist/lib/cms/document-gate.js +0 -61
- package/dist/lib/cms/document-gate.js.map +0 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { PRESENCE_TTL_MS, livePresence, } from '@elytracms/core/persistence';
|
|
3
|
+
import { applyDocumentPatches, formatValuePath, readValueAt, semanticValueEqual, writeField, } from '@elytracms/core/cms-core';
|
|
4
|
+
import { fieldPositionPath } from '@elytracms/studio/lib/cms/field-patch';
|
|
5
|
+
import { compositionPatches } from '../builder/composition-patches.js';
|
|
6
|
+
import { commitPolicyOf } from './commit-policy.js';
|
|
7
|
+
import { browserScheduler, createDraftCommitter } from './draft-commit.js';
|
|
8
|
+
/**
|
|
9
|
+
* EC-595 A2 — the detail editor's draft over the revisioned document (and, for the
|
|
10
|
+
* explicit-save rails, over a record).
|
|
11
|
+
*
|
|
12
|
+
* **Document write model** (the CMS): the draft changes per keystroke (live validation
|
|
13
|
+
* reads it); a field's edit is COMMITTED per its control kind's policy
|
|
14
|
+
* ({@link commitPolicyOf} / {@link createDraftCommitter}) as the patch of the position it
|
|
15
|
+
* wrote — never a document. The canvas commits each action's node patches at once
|
|
16
|
+
* (`compositionPatches`). The draft REBASES whenever the session's document changes
|
|
17
|
+
* (a write result, a colleague's change by subscription, a resolved conflict): it is the
|
|
18
|
+
* session's document with this editor's uncommitted positions laid over it — local wins
|
|
19
|
+
* until committed, then the per-path check decides (EC-595 point 7).
|
|
20
|
+
*
|
|
21
|
+
* **Forced flushes** (the ruling: before unmount, tab hide, publish, preview, an AI
|
|
22
|
+
* proposal, duplicate, transfer): {@link DocumentDraft.flush} commits every uncommitted
|
|
23
|
+
* position and awaits the persister. Unmount and tab hide run it here; the editor's
|
|
24
|
+
* publish, duplicate and the AI tap call it.
|
|
25
|
+
*
|
|
26
|
+
* **Thin presence** (Stage A): a heartbeat naming the focused field's path on focus
|
|
27
|
+
* change and every {@link PRESENCE_HEARTBEAT_MS} (under the 30 s TTL), cleared on
|
|
28
|
+
* unmount; the other sessions' live heartbeats arrive with the subscription.
|
|
29
|
+
*
|
|
30
|
+
* **Record write model** (users, redirects, assets, the account): the draft is edited
|
|
31
|
+
* in place and written whole on the editor's Save button (EC-240) — unchanged.
|
|
32
|
+
*/
|
|
33
|
+
/** The presence heartbeat interval — under {@link PRESENCE_TTL_MS} so a present editor never lapses. */
|
|
34
|
+
export const PRESENCE_HEARTBEAT_MS = 10_000;
|
|
35
|
+
export const documentVisibility = {
|
|
36
|
+
onHidden(onHidden) {
|
|
37
|
+
if (typeof document === 'undefined')
|
|
38
|
+
return () => { };
|
|
39
|
+
const listener = () => {
|
|
40
|
+
if (document.visibilityState === 'hidden')
|
|
41
|
+
onHidden();
|
|
42
|
+
};
|
|
43
|
+
document.addEventListener('visibilitychange', listener);
|
|
44
|
+
return () => document.removeEventListener('visibilitychange', listener);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/** The patch that writes the value `doc` holds at `path` (unset when absent). */
|
|
48
|
+
export function positionPatch(doc, path) {
|
|
49
|
+
const value = readValueAt(doc, path);
|
|
50
|
+
return value === undefined ? { op: 'unset', path } : { op: 'set', path, value };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* THE rebase (pure): the session's document with the editor's uncommitted positions laid
|
|
54
|
+
* over it, values from the editor's current draft.
|
|
55
|
+
*/
|
|
56
|
+
export function rebaseDraft(sessionDoc, draft, uncommitted) {
|
|
57
|
+
if (uncommitted.length === 0)
|
|
58
|
+
return sessionDoc;
|
|
59
|
+
const patches = uncommitted.map((position) => positionPatch(draft, position.path));
|
|
60
|
+
return applyDocumentPatches({ document: sessionDoc, patches, since: [] }).document;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The reader's "now" for presence liveness: THIS session's own latest heartbeat `at`
|
|
64
|
+
* (written by the same store clock as everyone else's — the playground store runs a
|
|
65
|
+
* deterministic sequential clock, Convex the writers' wall clocks), falling back to the
|
|
66
|
+
* injected clock before the first heartbeat lands. Comparing others' `at` against a
|
|
67
|
+
* different time base would call every colleague stale (measured live in the
|
|
68
|
+
* playground: the store's clock is months behind the browser's).
|
|
69
|
+
*/
|
|
70
|
+
export function presenceReaderNow(entries, sessionId, fallback) {
|
|
71
|
+
const own = entries.filter((entry) => entry.sessionId === sessionId).map((entry) => entry.at);
|
|
72
|
+
return own.length > 0 ? own.sort().at(-1) : fallback;
|
|
73
|
+
}
|
|
74
|
+
export function useDocumentDraft(input) {
|
|
75
|
+
const { source, collection, doc, locale, lookupManifest, canvasField } = input;
|
|
76
|
+
const writes = source.writes;
|
|
77
|
+
const documentModel = writes.kind === 'document' ? writes : undefined;
|
|
78
|
+
const ref = useMemo(() => ({ collection: doc.collection, id: doc.id }), [doc.collection, doc.id]);
|
|
79
|
+
const [draft, setDraftState] = useState(doc);
|
|
80
|
+
const draftRef = useRef(draft);
|
|
81
|
+
const setDraft = useCallback((next) => {
|
|
82
|
+
draftRef.current = next;
|
|
83
|
+
setDraftState(next);
|
|
84
|
+
}, []);
|
|
85
|
+
const scheduler = input.scheduler ?? browserScheduler;
|
|
86
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
87
|
+
const fieldsByName = useMemo(() => new Map(collection.fields.map((f) => [f.name, f])), [collection]);
|
|
88
|
+
// --- Committing --------------------------------------------------------------------
|
|
89
|
+
const writesRef = useRef(documentModel);
|
|
90
|
+
writesRef.current = documentModel;
|
|
91
|
+
const committer = useMemo(() => createDraftCommitter({
|
|
92
|
+
policyOf: (name) => {
|
|
93
|
+
const field = fieldsByName.get(name);
|
|
94
|
+
return field ? commitPolicyOf(field) : 'blur-or-idle';
|
|
95
|
+
},
|
|
96
|
+
commit: (positions) => {
|
|
97
|
+
const target = writesRef.current;
|
|
98
|
+
if (!target)
|
|
99
|
+
return;
|
|
100
|
+
void target.commit(ref, positions.map((position) => positionPatch(draftRef.current, position.path)), positions.map((position) => position.base));
|
|
101
|
+
},
|
|
102
|
+
baseOf: () => writesRef.current?.revisionOf(ref),
|
|
103
|
+
scheduler,
|
|
104
|
+
}),
|
|
105
|
+
// One committer per open document (the editor is keyed by it).
|
|
106
|
+
[ref]);
|
|
107
|
+
// --- The canvas's value and its remount ---------------------------------------------
|
|
108
|
+
const canvasPath = canvasField ? fieldPositionPath(collection, canvasField.name, locale) : undefined;
|
|
109
|
+
const canvasPathKey = canvasPath ? formatValuePath(canvasPath) : '';
|
|
110
|
+
const canvasValueRef = useRef(canvasPath ? readValueAt(draft, canvasPath) : undefined);
|
|
111
|
+
const [canvasNonce, setCanvasNonce] = useState(0);
|
|
112
|
+
const syncCanvas = useCallback((next) => {
|
|
113
|
+
if (!canvasPath)
|
|
114
|
+
return;
|
|
115
|
+
const value = readValueAt(next, canvasPath);
|
|
116
|
+
if (semanticValueEqual(value, canvasValueRef.current))
|
|
117
|
+
return;
|
|
118
|
+
canvasValueRef.current = value;
|
|
119
|
+
setCanvasNonce((n) => n + 1);
|
|
120
|
+
},
|
|
121
|
+
// The path is keyed by its string form.
|
|
122
|
+
[canvasPathKey]);
|
|
123
|
+
// A locale switch points the canvas at another bucket: remount from it.
|
|
124
|
+
const lastCanvasPathKey = useRef(canvasPathKey);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (lastCanvasPathKey.current === canvasPathKey)
|
|
127
|
+
return;
|
|
128
|
+
lastCanvasPathKey.current = canvasPathKey;
|
|
129
|
+
canvasValueRef.current = canvasPath ? readValueAt(draftRef.current, canvasPath) : undefined;
|
|
130
|
+
setCanvasNonce((n) => n + 1);
|
|
131
|
+
}, [canvasPathKey]);
|
|
132
|
+
// --- The rebase (document model) -------------------------------------------------------
|
|
133
|
+
const firstDoc = useRef(true);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (firstDoc.current) {
|
|
136
|
+
firstDoc.current = false;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!documentModel)
|
|
140
|
+
return;
|
|
141
|
+
const next = rebaseDraft(doc, draftRef.current, committer.uncommitted());
|
|
142
|
+
setDraft(next);
|
|
143
|
+
syncCanvas(next);
|
|
144
|
+
}, [doc]);
|
|
145
|
+
// --- Writers ----------------------------------------------------------------------------
|
|
146
|
+
const setFieldValue = useCallback((field, value) => {
|
|
147
|
+
const next = writeField(collection, draftRef.current, field.name, locale, value, { lookupManifest });
|
|
148
|
+
setDraft(next);
|
|
149
|
+
if (documentModel)
|
|
150
|
+
committer.edited(field.name, fieldPositionPath(collection, field.name, locale));
|
|
151
|
+
}, [collection, locale, lookupManifest, setDraft, documentModel, committer]);
|
|
152
|
+
const editField = useCallback((field, update) => {
|
|
153
|
+
setDraft(update(draftRef.current));
|
|
154
|
+
if (documentModel)
|
|
155
|
+
committer.edited(field.name, fieldPositionPath(collection, field.name, locale));
|
|
156
|
+
}, [collection, locale, setDraft, documentModel, committer]);
|
|
157
|
+
const setCanvasValue = useCallback((field, value) => {
|
|
158
|
+
const path = fieldPositionPath(collection, field.name, locale);
|
|
159
|
+
const before = readValueAt(draftRef.current, path);
|
|
160
|
+
const next = writeField(collection, draftRef.current, field.name, locale, value, { lookupManifest });
|
|
161
|
+
const after = readValueAt(next, path);
|
|
162
|
+
setDraft(next);
|
|
163
|
+
canvasValueRef.current = after;
|
|
164
|
+
if (!documentModel)
|
|
165
|
+
return;
|
|
166
|
+
// The canvas writer's node patches, keyed by node id (computed here, in its write
|
|
167
|
+
// path, from the canonical values before and after the action — never a document diff).
|
|
168
|
+
const localized = path.length > 1 ? locale : undefined;
|
|
169
|
+
void documentModel.commit(ref, compositionPatches(before, after, field.name, localized));
|
|
170
|
+
}, [collection, locale, lookupManifest, setDraft, documentModel, ref]);
|
|
171
|
+
const setAlias = useCallback((alias) => {
|
|
172
|
+
const next = { ...draftRef.current };
|
|
173
|
+
if (alias === undefined)
|
|
174
|
+
delete next.alias;
|
|
175
|
+
else
|
|
176
|
+
next.alias = alias;
|
|
177
|
+
setDraft(next);
|
|
178
|
+
if (!documentModel)
|
|
179
|
+
return;
|
|
180
|
+
committer.commitAll();
|
|
181
|
+
documentModel.replace(next);
|
|
182
|
+
}, [setDraft, documentModel, committer]);
|
|
183
|
+
// --- Forced flush ----------------------------------------------------------------------
|
|
184
|
+
const save = useCallback(() => {
|
|
185
|
+
if (documentModel)
|
|
186
|
+
committer.commitAll();
|
|
187
|
+
else if (writes.kind === 'record')
|
|
188
|
+
writes.save(draftRef.current);
|
|
189
|
+
}, [documentModel, committer, writes]);
|
|
190
|
+
const flush = useCallback(async () => {
|
|
191
|
+
save();
|
|
192
|
+
if (documentModel)
|
|
193
|
+
await documentModel.flush(ref);
|
|
194
|
+
}, [save, documentModel, ref]);
|
|
195
|
+
// Tab hide: commit and flush. Unmount: the same, and leave the document.
|
|
196
|
+
const visibility = input.visibility ?? documentVisibility;
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!documentModel)
|
|
199
|
+
return;
|
|
200
|
+
return visibility.onHidden(() => {
|
|
201
|
+
committer.commitAll();
|
|
202
|
+
void documentModel.flush(ref);
|
|
203
|
+
});
|
|
204
|
+
}, [ref]);
|
|
205
|
+
useEffect(() => () => {
|
|
206
|
+
const target = writesRef.current;
|
|
207
|
+
if (!target)
|
|
208
|
+
return;
|
|
209
|
+
committer.commitAll();
|
|
210
|
+
committer.dispose();
|
|
211
|
+
void target.flush(ref);
|
|
212
|
+
target.clearPresence(ref);
|
|
213
|
+
}, [ref]);
|
|
214
|
+
// --- Subscription + presence ------------------------------------------------------------
|
|
215
|
+
const [presenceEntries, setPresenceEntries] = useState([]);
|
|
216
|
+
const [clock, setClock] = useState(now);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!documentModel)
|
|
219
|
+
return;
|
|
220
|
+
return documentModel.subscribe(ref, setPresenceEntries);
|
|
221
|
+
}, [ref]);
|
|
222
|
+
const focusedPathRef = useRef(undefined);
|
|
223
|
+
const heartbeat = useCallback(() => {
|
|
224
|
+
writesRef.current?.reportPresence(ref, focusedPathRef.current);
|
|
225
|
+
setClock(now());
|
|
226
|
+
}, [ref]);
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!documentModel)
|
|
229
|
+
return;
|
|
230
|
+
heartbeat();
|
|
231
|
+
let handle;
|
|
232
|
+
const tick = () => {
|
|
233
|
+
heartbeat();
|
|
234
|
+
handle = scheduler.setTimeout(tick, PRESENCE_HEARTBEAT_MS);
|
|
235
|
+
};
|
|
236
|
+
handle = scheduler.setTimeout(tick, PRESENCE_HEARTBEAT_MS);
|
|
237
|
+
return () => scheduler.clearTimeout(handle);
|
|
238
|
+
}, [ref]);
|
|
239
|
+
const presence = useMemo(() => documentModel
|
|
240
|
+
? livePresence(presenceEntries, presenceReaderNow(presenceEntries, documentModel.presenceSessionId, clock), PRESENCE_TTL_MS).filter((entry) => entry.sessionId !== documentModel.presenceSessionId)
|
|
241
|
+
: [], [presenceEntries, clock, documentModel]);
|
|
242
|
+
const focused = useCallback((field) => {
|
|
243
|
+
if (!documentModel)
|
|
244
|
+
return;
|
|
245
|
+
committer.focused(field.name);
|
|
246
|
+
focusedPathRef.current = fieldPositionPath(collection, field.name, locale);
|
|
247
|
+
heartbeat();
|
|
248
|
+
}, [documentModel, committer, collection, locale, heartbeat]);
|
|
249
|
+
const blurred = useCallback((field) => {
|
|
250
|
+
if (!documentModel)
|
|
251
|
+
return;
|
|
252
|
+
committer.blurred(field.name);
|
|
253
|
+
focusedPathRef.current = undefined;
|
|
254
|
+
heartbeat();
|
|
255
|
+
}, [documentModel, committer, heartbeat]);
|
|
256
|
+
const entered = useCallback((field) => {
|
|
257
|
+
if (documentModel)
|
|
258
|
+
committer.entered(field.name);
|
|
259
|
+
}, [documentModel, committer]);
|
|
260
|
+
const replaceDraft = useCallback((next) => {
|
|
261
|
+
setDraft(next);
|
|
262
|
+
syncCanvas(next);
|
|
263
|
+
}, [setDraft, syncCanvas]);
|
|
264
|
+
const dirty = useMemo(() => !semanticValueEqual(draft, doc), [draft, doc]);
|
|
265
|
+
return {
|
|
266
|
+
draft,
|
|
267
|
+
dirty,
|
|
268
|
+
explicitSave: writes.kind === 'record',
|
|
269
|
+
setFieldValue,
|
|
270
|
+
editField,
|
|
271
|
+
setCanvasValue,
|
|
272
|
+
setAlias,
|
|
273
|
+
focused,
|
|
274
|
+
blurred,
|
|
275
|
+
entered,
|
|
276
|
+
save,
|
|
277
|
+
flush,
|
|
278
|
+
replaceDraft,
|
|
279
|
+
canvasNonce,
|
|
280
|
+
presence,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=use-document-draft.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-document-draft.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-draft.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzE,OAAO,EACL,eAAe,EACf,YAAY,GACb,MAAM,6BAA6B,CAAA;AAEpC,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,UAAU,GACX,MAAM,0BAA0B,CAAA;AAYjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAGvE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,wGAAwG;AACxG,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAA;AAQ3C,MAAM,CAAC,MAAM,kBAAkB,GAAqB;IAClD,QAAQ,CAAC,QAAQ;QACf,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;QACpD,MAAM,QAAQ,GAAG,GAAG,EAAE;YACpB,IAAI,QAAQ,CAAC,eAAe,KAAK,QAAQ;gBAAE,QAAQ,EAAE,CAAA;QACvD,CAAC,CAAA;QACD,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAA;QACvD,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAA;IACzE,CAAC;CACF,CAAA;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,GAAgB,EAAE,IAAe;IAC7D,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACpC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACjF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,UAAuB,EACvB,KAAkB,EAClB,WAA2C;IAE3C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,UAAU,CAAA;IAC/C,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;IAClF,OAAO,oBAAoB,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAA;AACpF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAiC,EACjC,SAAiB,EACjB,QAAgB;IAEhB,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAC7F,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,QAAQ,CAAA;AACvD,CAAC;AA8CD,MAAM,UAAU,gBAAgB,CAAC,KAA4B;IAC3D,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,KAAK,CAAA;IAC9E,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;IAC5B,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IACrE,MAAM,GAAG,GAAgB,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAC9G,MAAM,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAc,GAAG,CAAC,CAAA;IACzD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC9B,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,IAAiB,EAAE,EAAE;QACjD,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;QACvB,aAAa,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,gBAAgB,CAAA;IACrD,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;IACzD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAA;IAEpG,sFAAsF;IACtF,MAAM,SAAS,GAAG,MAAM,CAAC,aAAa,CAAC,CAAA;IACvC,SAAS,CAAC,OAAO,GAAG,aAAa,CAAA;IACjC,MAAM,SAAS,GAAG,OAAO,CACvB,GAAG,EAAE,CACH,oBAAoB,CAAC;QACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;YACjB,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACpC,OAAO,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,cAAc,CAAA;QACvD,CAAC;QACD,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE;YACpB,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAA;YAChC,IAAI,CAAC,MAAM;gBAAE,OAAM;YACnB,KAAK,MAAM,CAAC,MAAM,CAChB,GAAG,EACH,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,EAC3E,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAC3C,CAAA;QACH,CAAC;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC;QAChD,SAAS;KACV,CAAC;IACJ,+DAA+D;IAC/D,CAAC,GAAG,CAAC,CACN,CAAA;IAED,uFAAuF;IACvF,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,iBAAiB,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACpG,MAAM,aAAa,GAAG,UAAU,CAAC,CAAC,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACnE,MAAM,cAAc,GAAG,MAAM,CAAU,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAC/F,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IACjD,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,IAAiB,EAAE,EAAE;QACpB,IAAI,CAAC,UAAU;YAAE,OAAM;QACvB,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;QAC3C,IAAI,kBAAkB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC;YAAE,OAAM;QAC7D,cAAc,CAAC,OAAO,GAAG,KAAK,CAAA;QAC9B,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9B,CAAC;IACD,wCAAwC;IACxC,CAAC,aAAa,CAAC,CAChB,CAAA;IACD,wEAAwE;IACxE,MAAM,iBAAiB,GAAG,MAAM,CAAC,aAAa,CAAC,CAAA;IAC/C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,iBAAiB,CAAC,OAAO,KAAK,aAAa;YAAE,OAAM;QACvD,iBAAiB,CAAC,OAAO,GAAG,aAAa,CAAA;QACzC,cAAc,CAAC,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC3F,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9B,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAA;IAEnB,0FAA0F;IAC1F,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC7B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAA;YACxB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,EAAE,CAAC,CAAA;QACxE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,UAAU,CAAC,IAAI,CAAC,CAAA;IAClB,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IAET,2FAA2F;IAC3F,MAAM,aAAa,GAAG,WAAW,CAC/B,CAAC,KAAe,EAAE,KAAc,EAAE,EAAE;QAClC,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,CAAC,CAAA;QACpG,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,aAAa;YAAE,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IACpG,CAAC,EACD,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,CAAC,CACzE,CAAA;IACD,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,KAAe,EAAE,MAA6C,EAAE,EAAE;QACjE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;QAClC,IAAI,aAAa;YAAE,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IACpG,CAAC,EACD,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,CAAC,CACzD,CAAA;IACD,MAAM,cAAc,GAAG,WAAW,CAChC,CAAC,KAAe,EAAE,KAAc,EAAE,EAAE;QAClC,MAAM,IAAI,GAAG,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC9D,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QAClD,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,CAAC,CAAA;QACpG,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACrC,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,cAAc,CAAC,OAAO,GAAG,KAAK,CAAA;QAC9B,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,kFAAkF;QAClF,wFAAwF;QACxF,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;QACtD,KAAK,aAAa,CAAC,MAAM,CAAC,GAAG,EAAE,kBAAkB,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAA;IAC1F,CAAC,EACD,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,CACnE,CAAA;IACD,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,KAAyB,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,EAAE,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAA;QACpC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,KAAK,CAAA;;YACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QACvB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,SAAS,CAAC,SAAS,EAAE,CAAA;QACrB,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC,EACD,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,CAAC,CACrC,CAAA;IAED,0FAA0F;IAC1F,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5B,IAAI,aAAa;YAAE,SAAS,CAAC,SAAS,EAAE,CAAA;aACnC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IAClE,CAAC,EAAE,CAAC,aAAa,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACnC,IAAI,EAAE,CAAA;QACN,IAAI,aAAa;YAAE,MAAM,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACnD,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,CAAC,CAAC,CAAA;IAE9B,yEAAyE;IACzE,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,kBAAkB,CAAA;IACzD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,OAAO,UAAU,CAAC,QAAQ,CAAC,GAAG,EAAE;YAC9B,SAAS,CAAC,SAAS,EAAE,CAAA;YACrB,KAAK,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC/B,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACT,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAA;QAChC,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,SAAS,CAAC,SAAS,EAAE,CAAA;QACrB,SAAS,CAAC,OAAO,EAAE,CAAA;QACnB,KAAK,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACtB,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;IAC3B,CAAC,EACD,CAAC,GAAG,CAAC,CACN,CAAA;IAED,2FAA2F;IAC3F,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAA2B,EAAE,CAAC,CAAA;IACpF,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;IACvC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,OAAO,aAAa,CAAC,SAAS,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAA;IACzD,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACT,MAAM,cAAc,GAAG,MAAM,CAAwB,SAAS,CAAC,CAAA;IAC/D,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;QACjC,SAAS,CAAC,OAAO,EAAE,cAAc,CAAC,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,CAAA;QAC9D,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAA;IACjB,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACT,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,SAAS,EAAE,CAAA;QACX,IAAI,MAAe,CAAA;QACnB,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,SAAS,EAAE,CAAA;YACX,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAA;QAC5D,CAAC,CAAA;QACD,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAA;QAC1D,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;IAC7C,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACT,MAAM,QAAQ,GAAG,OAAO,CACtB,GAAG,EAAE,CACH,aAAa;QACX,CAAC,CAAC,YAAY,CACV,eAAe,EACf,iBAAiB,CAAC,eAAe,EAAE,aAAa,CAAC,iBAAiB,EAAE,KAAK,CAAC,EAC1E,eAAe,CAChB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,aAAa,CAAC,iBAAiB,CAAC;QAC1E,CAAC,CAAC,EAAE,EACR,CAAC,eAAe,EAAE,KAAK,EAAE,aAAa,CAAC,CACxC,CAAA;IAED,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,KAAe,EAAE,EAAE;QAClB,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC7B,cAAc,CAAC,OAAO,GAAG,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC1E,SAAS,EAAE,CAAA;IACb,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAC1D,CAAA;IACD,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,KAAe,EAAE,EAAE;QAClB,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC7B,cAAc,CAAC,OAAO,GAAG,SAAS,CAAA;QAClC,SAAS,EAAE,CAAA;IACb,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,EAAE,SAAS,CAAC,CACtC,CAAA;IACD,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,KAAe,EAAE,EAAE;QAClB,IAAI,aAAa;YAAE,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAClD,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,CAAC,CAC3B,CAAA;IAED,MAAM,YAAY,GAAG,WAAW,CAC9B,CAAC,IAAiB,EAAE,EAAE;QACpB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,UAAU,CAAC,IAAI,CAAC,CAAA;IAClB,CAAC,EACD,CAAC,QAAQ,EAAE,UAAU,CAAC,CACvB,CAAA;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAA;IAE1E,OAAO;QACL,KAAK;QACL,KAAK;QACL,YAAY,EAAE,MAAM,CAAC,IAAI,KAAK,QAAQ;QACtC,aAAa;QACb,SAAS;QACT,cAAc;QACd,QAAQ;QACR,OAAO;QACP,OAAO;QACP,OAAO;QACP,IAAI;QACJ,KAAK;QACL,YAAY;QACZ,WAAW;QACX,QAAQ;KACT,CAAA;AACH,CAAC","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n PRESENCE_TTL_MS,\n livePresence,\n} from '@elytracms/core/persistence'\nimport type { PresenceEntry } from '@elytracms/core/persistence'\nimport {\n applyDocumentPatches,\n formatValuePath,\n readValueAt,\n semanticValueEqual,\n writeField,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentPatch,\n DocumentRef,\n FieldDef,\n Locale,\n PropFieldLookup,\n ValuePath,\n} from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { fieldPositionPath } from '@elytracms/studio/lib/cms/field-patch'\nimport { compositionPatches } from '../builder/composition-patches'\nimport { commitPolicyOf } from './commit-policy'\nimport { browserScheduler, createDraftCommitter } from './draft-commit'\nimport type { DraftCommitter, Scheduler, UncommittedPosition } from './draft-commit'\n\n/**\n * EC-595 A2 — the detail editor's draft over the revisioned document (and, for the\n * explicit-save rails, over a record).\n *\n * **Document write model** (the CMS): the draft changes per keystroke (live validation\n * reads it); a field's edit is COMMITTED per its control kind's policy\n * ({@link commitPolicyOf} / {@link createDraftCommitter}) as the patch of the position it\n * wrote — never a document. The canvas commits each action's node patches at once\n * (`compositionPatches`). The draft REBASES whenever the session's document changes\n * (a write result, a colleague's change by subscription, a resolved conflict): it is the\n * session's document with this editor's uncommitted positions laid over it — local wins\n * until committed, then the per-path check decides (EC-595 point 7).\n *\n * **Forced flushes** (the ruling: before unmount, tab hide, publish, preview, an AI\n * proposal, duplicate, transfer): {@link DocumentDraft.flush} commits every uncommitted\n * position and awaits the persister. Unmount and tab hide run it here; the editor's\n * publish, duplicate and the AI tap call it.\n *\n * **Thin presence** (Stage A): a heartbeat naming the focused field's path on focus\n * change and every {@link PRESENCE_HEARTBEAT_MS} (under the 30 s TTL), cleared on\n * unmount; the other sessions' live heartbeats arrive with the subscription.\n *\n * **Record write model** (users, redirects, assets, the account): the draft is edited\n * in place and written whole on the editor's Save button (EC-240) — unchanged.\n */\n\n/** The presence heartbeat interval — under {@link PRESENCE_TTL_MS} so a present editor never lapses. */\nexport const PRESENCE_HEARTBEAT_MS = 10_000\n\n/** An injectable visibility source (the tab-hide flush; tests drive it). */\nexport interface VisibilitySource {\n /** Call `onHidden` whenever the tab becomes hidden; returns the unsubscribe. */\n onHidden(onHidden: () => void): () => void\n}\n\nexport const documentVisibility: VisibilitySource = {\n onHidden(onHidden) {\n if (typeof document === 'undefined') return () => {}\n const listener = () => {\n if (document.visibilityState === 'hidden') onHidden()\n }\n document.addEventListener('visibilitychange', listener)\n return () => document.removeEventListener('visibilitychange', listener)\n },\n}\n\n/** The patch that writes the value `doc` holds at `path` (unset when absent). */\nexport function positionPatch(doc: CmsDocument, path: ValuePath): DocumentPatch {\n const value = readValueAt(doc, path)\n return value === undefined ? { op: 'unset', path } : { op: 'set', path, value }\n}\n\n/**\n * THE rebase (pure): the session's document with the editor's uncommitted positions laid\n * over it, values from the editor's current draft.\n */\nexport function rebaseDraft(\n sessionDoc: CmsDocument,\n draft: CmsDocument,\n uncommitted: readonly UncommittedPosition[],\n): CmsDocument {\n if (uncommitted.length === 0) return sessionDoc\n const patches = uncommitted.map((position) => positionPatch(draft, position.path))\n return applyDocumentPatches({ document: sessionDoc, patches, since: [] }).document\n}\n\n/**\n * The reader's \"now\" for presence liveness: THIS session's own latest heartbeat `at`\n * (written by the same store clock as everyone else's — the playground store runs a\n * deterministic sequential clock, Convex the writers' wall clocks), falling back to the\n * injected clock before the first heartbeat lands. Comparing others' `at` against a\n * different time base would call every colleague stale (measured live in the\n * playground: the store's clock is months behind the browser's).\n */\nexport function presenceReaderNow(\n entries: readonly PresenceEntry[],\n sessionId: string,\n fallback: string,\n): string {\n const own = entries.filter((entry) => entry.sessionId === sessionId).map((entry) => entry.at)\n return own.length > 0 ? own.sort().at(-1)! : fallback\n}\n\nexport interface DocumentDraft {\n draft: CmsDocument\n /** Uncommitted (document model) / unsaved (record model) edits exist. */\n dirty: boolean\n /** The record model: the editor renders a Save button + navigation guard. */\n explicitSave: boolean\n /** A field's control wrote `value` (the active locale for a localized field). */\n setFieldValue(field: FieldDef, value: unknown): void\n /** A writer that transforms ONE field position (fill / reset from base). */\n editField(field: FieldDef, update: (draft: CmsDocument) => CmsDocument): void\n /** The canvas emitted a new value for its field (node patches, committed at once). */\n setCanvasValue(field: FieldDef, value: unknown): void\n /** The slot alias (a document-level key: a whole-document write at the held revision). */\n setAlias(alias: string | undefined): void\n /** Field boundary events (focus / blur / Enter) — the commit policy's triggers. */\n focused(field: FieldDef): void\n blurred(field: FieldDef): void\n entered(field: FieldDef): void\n /** The forced commit (sync; the persister serialises what follows) / the record model's Save. */\n save(): void\n /** The FORCED FLUSH: commit everything uncommitted and await persistence. */\n flush(): Promise<void>\n /** Adopt a draft from outside (a restored version). */\n replaceDraft(next: CmsDocument): void\n /** Bumped when the canvas must remount from the draft (a restore, a colleague's change). */\n canvasNonce: number\n /** The OTHER sessions present on this document, live by the reader's clock. */\n presence: readonly PresenceEntry[]\n}\n\nexport interface UseDocumentDraftInput {\n source: CollectionViewSource\n collection: CollectionDef\n doc: CmsDocument\n locale: Locale\n lookupManifest: PropFieldLookup\n /** The composition field the canvas edits on this screen, if any. */\n canvasField?: FieldDef\n scheduler?: Scheduler\n visibility?: VisibilitySource\n /** The reader's clock for presence liveness (ISO); `new Date().toISOString()` by default. */\n now?: () => string\n}\n\nexport function useDocumentDraft(input: UseDocumentDraftInput): DocumentDraft {\n const { source, collection, doc, locale, lookupManifest, canvasField } = input\n const writes = source.writes\n const documentModel = writes.kind === 'document' ? writes : undefined\n const ref: DocumentRef = useMemo(() => ({ collection: doc.collection, id: doc.id }), [doc.collection, doc.id])\n const [draft, setDraftState] = useState<CmsDocument>(doc)\n const draftRef = useRef(draft)\n const setDraft = useCallback((next: CmsDocument) => {\n draftRef.current = next\n setDraftState(next)\n }, [])\n const scheduler = input.scheduler ?? browserScheduler\n const now = input.now ?? (() => new Date().toISOString())\n const fieldsByName = useMemo(() => new Map(collection.fields.map((f) => [f.name, f])), [collection])\n\n // --- Committing --------------------------------------------------------------------\n const writesRef = useRef(documentModel)\n writesRef.current = documentModel\n const committer = useMemo<DraftCommitter>(\n () =>\n createDraftCommitter({\n policyOf: (name) => {\n const field = fieldsByName.get(name)\n return field ? commitPolicyOf(field) : 'blur-or-idle'\n },\n commit: (positions) => {\n const target = writesRef.current\n if (!target) return\n void target.commit(\n ref,\n positions.map((position) => positionPatch(draftRef.current, position.path)),\n positions.map((position) => position.base),\n )\n },\n baseOf: () => writesRef.current?.revisionOf(ref),\n scheduler,\n }),\n // One committer per open document (the editor is keyed by it).\n [ref],\n )\n\n // --- The canvas's value and its remount ---------------------------------------------\n const canvasPath = canvasField ? fieldPositionPath(collection, canvasField.name, locale) : undefined\n const canvasPathKey = canvasPath ? formatValuePath(canvasPath) : ''\n const canvasValueRef = useRef<unknown>(canvasPath ? readValueAt(draft, canvasPath) : undefined)\n const [canvasNonce, setCanvasNonce] = useState(0)\n const syncCanvas = useCallback(\n (next: CmsDocument) => {\n if (!canvasPath) return\n const value = readValueAt(next, canvasPath)\n if (semanticValueEqual(value, canvasValueRef.current)) return\n canvasValueRef.current = value\n setCanvasNonce((n) => n + 1)\n },\n // The path is keyed by its string form.\n [canvasPathKey],\n )\n // A locale switch points the canvas at another bucket: remount from it.\n const lastCanvasPathKey = useRef(canvasPathKey)\n useEffect(() => {\n if (lastCanvasPathKey.current === canvasPathKey) return\n lastCanvasPathKey.current = canvasPathKey\n canvasValueRef.current = canvasPath ? readValueAt(draftRef.current, canvasPath) : undefined\n setCanvasNonce((n) => n + 1)\n }, [canvasPathKey])\n\n // --- The rebase (document model) -------------------------------------------------------\n const firstDoc = useRef(true)\n useEffect(() => {\n if (firstDoc.current) {\n firstDoc.current = false\n return\n }\n if (!documentModel) return\n const next = rebaseDraft(doc, draftRef.current, committer.uncommitted())\n setDraft(next)\n syncCanvas(next)\n }, [doc])\n\n // --- Writers ----------------------------------------------------------------------------\n const setFieldValue = useCallback(\n (field: FieldDef, value: unknown) => {\n const next = writeField(collection, draftRef.current, field.name, locale, value, { lookupManifest })\n setDraft(next)\n if (documentModel) committer.edited(field.name, fieldPositionPath(collection, field.name, locale))\n },\n [collection, locale, lookupManifest, setDraft, documentModel, committer],\n )\n const editField = useCallback(\n (field: FieldDef, update: (current: CmsDocument) => CmsDocument) => {\n setDraft(update(draftRef.current))\n if (documentModel) committer.edited(field.name, fieldPositionPath(collection, field.name, locale))\n },\n [collection, locale, setDraft, documentModel, committer],\n )\n const setCanvasValue = useCallback(\n (field: FieldDef, value: unknown) => {\n const path = fieldPositionPath(collection, field.name, locale)\n const before = readValueAt(draftRef.current, path)\n const next = writeField(collection, draftRef.current, field.name, locale, value, { lookupManifest })\n const after = readValueAt(next, path)\n setDraft(next)\n canvasValueRef.current = after\n if (!documentModel) return\n // The canvas writer's node patches, keyed by node id (computed here, in its write\n // path, from the canonical values before and after the action — never a document diff).\n const localized = path.length > 1 ? locale : undefined\n void documentModel.commit(ref, compositionPatches(before, after, field.name, localized))\n },\n [collection, locale, lookupManifest, setDraft, documentModel, ref],\n )\n const setAlias = useCallback(\n (alias: string | undefined) => {\n const next = { ...draftRef.current }\n if (alias === undefined) delete next.alias\n else next.alias = alias\n setDraft(next)\n if (!documentModel) return\n committer.commitAll()\n documentModel.replace(next)\n },\n [setDraft, documentModel, committer],\n )\n\n // --- Forced flush ----------------------------------------------------------------------\n const save = useCallback(() => {\n if (documentModel) committer.commitAll()\n else if (writes.kind === 'record') writes.save(draftRef.current)\n }, [documentModel, committer, writes])\n const flush = useCallback(async () => {\n save()\n if (documentModel) await documentModel.flush(ref)\n }, [save, documentModel, ref])\n\n // Tab hide: commit and flush. Unmount: the same, and leave the document.\n const visibility = input.visibility ?? documentVisibility\n useEffect(() => {\n if (!documentModel) return\n return visibility.onHidden(() => {\n committer.commitAll()\n void documentModel.flush(ref)\n })\n }, [ref])\n useEffect(\n () => () => {\n const target = writesRef.current\n if (!target) return\n committer.commitAll()\n committer.dispose()\n void target.flush(ref)\n target.clearPresence(ref)\n },\n [ref],\n )\n\n // --- Subscription + presence ------------------------------------------------------------\n const [presenceEntries, setPresenceEntries] = useState<readonly PresenceEntry[]>([])\n const [clock, setClock] = useState(now)\n useEffect(() => {\n if (!documentModel) return\n return documentModel.subscribe(ref, setPresenceEntries)\n }, [ref])\n const focusedPathRef = useRef<ValuePath | undefined>(undefined)\n const heartbeat = useCallback(() => {\n writesRef.current?.reportPresence(ref, focusedPathRef.current)\n setClock(now())\n }, [ref])\n useEffect(() => {\n if (!documentModel) return\n heartbeat()\n let handle: unknown\n const tick = () => {\n heartbeat()\n handle = scheduler.setTimeout(tick, PRESENCE_HEARTBEAT_MS)\n }\n handle = scheduler.setTimeout(tick, PRESENCE_HEARTBEAT_MS)\n return () => scheduler.clearTimeout(handle)\n }, [ref])\n const presence = useMemo(\n () =>\n documentModel\n ? livePresence(\n presenceEntries,\n presenceReaderNow(presenceEntries, documentModel.presenceSessionId, clock),\n PRESENCE_TTL_MS,\n ).filter((entry) => entry.sessionId !== documentModel.presenceSessionId)\n : [],\n [presenceEntries, clock, documentModel],\n )\n\n const focused = useCallback(\n (field: FieldDef) => {\n if (!documentModel) return\n committer.focused(field.name)\n focusedPathRef.current = fieldPositionPath(collection, field.name, locale)\n heartbeat()\n },\n [documentModel, committer, collection, locale, heartbeat],\n )\n const blurred = useCallback(\n (field: FieldDef) => {\n if (!documentModel) return\n committer.blurred(field.name)\n focusedPathRef.current = undefined\n heartbeat()\n },\n [documentModel, committer, heartbeat],\n )\n const entered = useCallback(\n (field: FieldDef) => {\n if (documentModel) committer.entered(field.name)\n },\n [documentModel, committer],\n )\n\n const replaceDraft = useCallback(\n (next: CmsDocument) => {\n setDraft(next)\n syncCanvas(next)\n },\n [setDraft, syncCanvas],\n )\n\n const dirty = useMemo(() => !semanticValueEqual(draft, doc), [draft, doc])\n\n return {\n draft,\n dirty,\n explicitSave: writes.kind === 'record',\n setFieldValue,\n editField,\n setCanvasValue,\n setAlias,\n focused,\n blurred,\n entered,\n save,\n flush,\n replaceDraft,\n canvasNonce,\n presence,\n }\n}\n"]}
|
|
@@ -4,8 +4,8 @@ import { useCollectionView } from '../collection-view/collection-view-context.js
|
|
|
4
4
|
/**
|
|
5
5
|
* Recorded-version freshness (EC-322): re-read on exactly the commands that
|
|
6
6
|
* change a document's version history or its live pin — NOT the whole `documents`
|
|
7
|
-
* domain. The draft the editor
|
|
8
|
-
*
|
|
7
|
+
* domain. The draft the editor commits (`documents.patch`, on quiescence — EC-595 A2)
|
|
8
|
+
* creates no version and moves no pin (versions are appended only on
|
|
9
9
|
* publish/snapshot/restore — see `documentVersions` in `convex/schema.ts`), so
|
|
10
10
|
* listening on the whole domain refetched `listDocumentVersions` on every
|
|
11
11
|
* keystroke while the Versions pane was open. The comparison the pane shows
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-document-versions.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-versions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAG9E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,kBAAkB,GAAG;IACzB,oBAAoB;IACpB,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV;4EAC4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,SAA6B,EAAE,GAAgB;IACjF,OAAO,CAAC,kBAAkB,EAAE,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAU,CAAA;AAChF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAgB,EAAE,OAA+B;IACxF,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,EAChD,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAC3C,CAAA;IACD,MAAM,aAAa,GAAG,iBAAiB,CAAuB;QAC5D,QAAQ,EAAE,UAAU;QACpB,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5E,YAAY,EAAE,kBAAkB;QAChC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxE,CAAC,CAAA;IACF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport type { DocumentVersionsView } from './cms-store'\n\n/**\n * Recorded-version freshness (EC-322): re-read on exactly the commands that\n * change a document's version history or its live pin — NOT the whole `documents`\n * domain. The draft the editor
|
|
1
|
+
{"version":3,"file":"use-document-versions.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-versions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAG9E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,kBAAkB,GAAG;IACzB,oBAAoB;IACpB,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV;4EAC4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,SAA6B,EAAE,GAAgB;IACjF,OAAO,CAAC,kBAAkB,EAAE,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAU,CAAA;AAChF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAgB,EAAE,OAA+B;IACxF,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,EAChD,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAC3C,CAAA;IACD,MAAM,aAAa,GAAG,iBAAiB,CAAuB;QAC5D,QAAQ,EAAE,UAAU;QACpB,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5E,YAAY,EAAE,kBAAkB;QAChC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxE,CAAC,CAAA;IACF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport type { DocumentVersionsView } from './cms-store'\n\n/**\n * Recorded-version freshness (EC-322): re-read on exactly the commands that\n * change a document's version history or its live pin — NOT the whole `documents`\n * domain. The draft the editor commits (`documents.patch`, on quiescence — EC-595 A2)\n * creates no version and moves no pin (versions are appended only on\n * publish/snapshot/restore — see `documentVersions` in `convex/schema.ts`), so\n * listening on the whole domain refetched `listDocumentVersions` on every\n * keystroke while the Versions pane was open. The comparison the pane shows\n * (`liveMatchesDraft`) reads the live snapshot from THIS query and the draft from\n * local editor state, so a draft edit re-renders it without any refetch.\n *\n * Every command that DOES change the view is enumerated (a new version-affecting\n * op must be added here): `snapshot`/`restore` append a version; `publish` appends\n * one and moves the pin; `unpublish`/`pinVersion` move the pin; `delete` cascades\n * the history away.\n */\nconst VERSION_RELOAD_OPS = [\n 'documents.snapshot',\n 'documents.publish',\n 'documents.unpublish',\n 'documents.pinVersion',\n 'documents.restore',\n 'documents.delete',\n 'documents.clear',\n] as const\n\n/** The one cache key for a document's version history — shared so the Versions\n * pane and the EC-298 compare view read through a single deduped query. */\nexport function documentVersionsKey(projectId: string | undefined, ref: DocumentRef) {\n return ['documentVersions', projectId ?? '∅', ref.collection, ref.id] as const\n}\n\n/**\n * A document's recorded version history, read through the one operations-backed\n * query cache (EC-308/AD-17). The flush-before-read ordering lives IN the loader,\n * so every fetch — initial, invalidated, or refetch — lists an authoritative\n * history regardless of which surface triggered the change. Two mounts with the\n * same document share one fetch (TanStack dedup on the key).\n *\n * `enabled` (EC-316, default `true`) lets a caller that only needs this for a\n * gating check — e.g. the header's Discard-draft enablement, which isn't\n * otherwise on the version-browsing path — skip the fetch until it actually\n * applies (an unpublished/non-versioned document never needs its history).\n */\nexport function useDocumentVersionsQuery(ref: DocumentRef, options?: { enabled?: boolean }) {\n const source = useCollectionView()\n const versionKey = useMemo(\n () => documentVersionsKey(source.projectId, ref),\n [source.projectId, ref.collection, ref.id],\n )\n const versionsQuery = useOperationQuery<DocumentVersionsView>({\n queryKey: versionKey,\n // The versioning capability owns the flush-before-read ordering; a source\n // without versions (never mounts this) degrades to an empty history.\n loader: async () => (await source.versioning?.load(ref)) ?? { versions: [] },\n invalidateOn: VERSION_RELOAD_OPS,\n ...(options?.enabled !== undefined ? { enabled: options.enabled } : {}),\n })\n return { versionsQuery, versionKey }\n}\n"]}
|
|
@@ -36,18 +36,12 @@ export declare function resolveHeaderLive(input: {
|
|
|
36
36
|
published: boolean;
|
|
37
37
|
}): HeaderLive | undefined;
|
|
38
38
|
/**
|
|
39
|
-
* EC-
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* unit-tested directly rather than through the hook. `siblings` doubles as the
|
|
46
|
-
* fallback's candidate pool: when `collection` is the `page` collection it IS
|
|
47
|
-
* the published `page` pool the fallback rule needs; for any other collection
|
|
48
|
-
* the derived fallback routes simply address no route of `collection` (a
|
|
49
|
-
* harmless no-op — `urlForDocument` only matches routes whose `document.collection`
|
|
50
|
-
* equals the target collection).
|
|
39
|
+
* EC-590 — derive a published document's live URL through THE router
|
|
40
|
+
* (`createRouter(...).urlFor`), the same reverse direction delivery's links and sitemap
|
|
41
|
+
* read, in the one precedence order — so the studio's live link can no longer advertise a
|
|
42
|
+
* URL the site 404s (IDENTITY M4). The project's routes as declared: the engine derives no
|
|
43
|
+
* home route any more (the former `mergeRouteRecords` + `fallbackHomeRoutes` merge went
|
|
44
|
+
* with it). Pure (no React), so it is unit-tested directly rather than through the hook.
|
|
51
45
|
*/
|
|
52
46
|
export declare function resolveDocumentLiveUrl(input: {
|
|
53
47
|
routes: readonly RouteRecord[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { createRouter } from '@elytracms/core/cms-core';
|
|
3
3
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
4
|
import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
5
5
|
import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
|
|
@@ -36,22 +36,23 @@ export function resolveHeaderLive(input) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* EC-
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* unit-tested directly rather than through the hook. `siblings` doubles as the
|
|
46
|
-
* fallback's candidate pool: when `collection` is the `page` collection it IS
|
|
47
|
-
* the published `page` pool the fallback rule needs; for any other collection
|
|
48
|
-
* the derived fallback routes simply address no route of `collection` (a
|
|
49
|
-
* harmless no-op — `urlForDocument` only matches routes whose `document.collection`
|
|
50
|
-
* equals the target collection).
|
|
39
|
+
* EC-590 — derive a published document's live URL through THE router
|
|
40
|
+
* (`createRouter(...).urlFor`), the same reverse direction delivery's links and sitemap
|
|
41
|
+
* read, in the one precedence order — so the studio's live link can no longer advertise a
|
|
42
|
+
* URL the site 404s (IDENTITY M4). The project's routes as declared: the engine derives no
|
|
43
|
+
* home route any more (the former `mergeRouteRecords` + `fallbackHomeRoutes` merge went
|
|
44
|
+
* with it). Pure (no React), so it is unit-tested directly rather than through the hook.
|
|
51
45
|
*/
|
|
52
46
|
export function resolveDocumentLiveUrl(input) {
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
return createRouter({
|
|
48
|
+
routes: input.routes,
|
|
49
|
+
collections: [input.collection],
|
|
50
|
+
locales: input.localeConfig,
|
|
51
|
+
}).urlFor(input.doc, {
|
|
52
|
+
siblings: input.siblings,
|
|
53
|
+
locale: input.locale,
|
|
54
|
+
...(input.frontendUrl !== undefined ? { frontendUrl: input.frontendUrl } : {}),
|
|
55
|
+
});
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
57
58
|
* EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-live-url.js","sourceRoot":"","sources":["../../../src/components/cms/use-live-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"use-live-url.js","sourceRoot":"","sources":["../../../src/components/cms/use-live-url.ts"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { createRouter } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentUrl,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents'\nimport type { IssueSeverity } from '../issue-severity'\n\n/**\n * The header's live-link descriptor: an `href` (open live) + a title, or a\n * reason-only title while it isn't openable (loading / publish-first / no\n * frontendUrl). `severity` is set when the reason is a FINDING about the URL\n * rather than a lifecycle state — the renderer then shows the shared issue glyph\n * (EC-507) in place of the muted open-live icon, so the row and the Issues pane\n * say the same thing (EC-518).\n */\nexport interface HeaderLive {\n href?: string\n title: string\n severity?: IssueSeverity\n}\n\n/**\n * 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure\n * decision. Only a routable collection shows the row; its leaf opens the live page\n * when the doc is published AND a frontendUrl is set (an absolute `href`),\n * otherwise it is a muted icon whose tooltip explains WHY (still loading, publish\n * first, or set the frontendUrl). The draft-derived path itself updates as you\n * type — only the OPEN-live action is published-gated, preserving draft/live.\n *\n * EC-518 — a URL the engine has computed as NOT `addressable` (EC-492: the host\n * re-derives a different `{locale, path}` pair from it, so no request reaches the\n * document) never becomes an `href`, frontendUrl or not. The engine's verdict is\n * consumed, not re-derived: the row shows the same `warning` the Issues pane shows\n * for it (`document-reachability.ts`), because a link titled \"Open /de\" a few\n * pixels from that warning is the stronger signal, and it is the wrong one.\n */\nexport function resolveHeaderLive(input: {\n routableCollection: boolean\n liveUrl: Pick<DocumentUrl, 'href' | 'path' | 'addressable'> | undefined\n loading: boolean\n published: boolean\n}): HeaderLive | undefined {\n if (!input.routableCollection) return undefined\n if (input.liveUrl && !input.liveUrl.addressable) {\n return { title: translate('cms.issue.unaddressable-url'), severity: 'warning' }\n }\n if (input.liveUrl?.href) {\n return { href: input.liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: input.liveUrl.path }) }\n }\n return {\n title: input.loading\n ? translate('cms.editor.loadingLiveUrl')\n : !input.published\n ? translate('cms.editor.publishToViewLive')\n : translate('cms.editor.setFrontendUrlHint'),\n }\n}\n\n/**\n * EC-590 — derive a published document's live URL through THE router\n * (`createRouter(...).urlFor`), the same reverse direction delivery's links and sitemap\n * read, in the one precedence order — so the studio's live link can no longer advertise a\n * URL the site 404s (IDENTITY M4). The project's routes as declared: the engine derives no\n * home route any more (the former `mergeRouteRecords` + `fallbackHomeRoutes` merge went\n * with it). Pure (no React), so it is unit-tested directly rather than through the hook.\n */\nexport function resolveDocumentLiveUrl(input: {\n routes: readonly RouteRecord[]\n localeConfig: LocaleConfig\n frontendUrl: string | undefined\n collection: CollectionDef\n doc: CmsDocument\n siblings: readonly CmsDocument[]\n locale: Locale\n}): DocumentUrl | undefined {\n return createRouter({\n routes: input.routes,\n collections: [input.collection],\n locales: input.localeConfig,\n }).urlFor(input.doc, {\n siblings: input.siblings,\n locale: input.locale,\n ...(input.frontendUrl !== undefined ? { frontendUrl: input.frontendUrl } : {}),\n })\n}\n\n/**\n * EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned\n * published snapshot + its published ancestors), never the draft, so an unpublished\n * slug/parent edit can't leak into the live URL. Resolves the header's live-link\n * descriptor for the composed-URL row; `undefined` for a non-routable collection.\n */\nexport function useLiveUrl(input: {\n source: CollectionViewSource\n collection: CollectionDef\n doc: CmsDocument\n locale: Locale\n published: boolean\n routableCollection: boolean\n}): { headerLive: HeaderLive | undefined } {\n const { source, collection, doc, locale, published, routableCollection } = input\n const publishedDocs = usePublishedDocuments(source.projectId, collection.id)\n const liveUrl = useMemo(() => {\n const publishedDoc = publishedDocs.byId.get(doc.id)\n if (!publishedDoc) return undefined\n return resolveDocumentLiveUrl({\n routes: resolveDeploymentRoutes(),\n localeConfig: source.localeConfig,\n frontendUrl: resolveDeploymentFrontendUrl(),\n collection,\n doc: publishedDoc,\n siblings: [...publishedDocs.byId.values()],\n locale,\n })\n }, [publishedDocs, collection, doc.id, locale, source.localeConfig])\n\n const headerLive = resolveHeaderLive({\n routableCollection,\n liveUrl,\n loading: publishedDocs.status === 'loading',\n published,\n })\n return { headerLive }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DocumentRef } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
3
3
|
export type PublishState = 'idle' | 'pending' | 'success';
|
|
4
4
|
/**
|
|
@@ -19,31 +19,26 @@ export declare function publishGate(input: {
|
|
|
19
19
|
};
|
|
20
20
|
export interface PublishController {
|
|
21
21
|
publishState: PublishState;
|
|
22
|
-
/**
|
|
23
|
-
publishConfirm: boolean;
|
|
24
|
-
/** Publish (opening the confirm-through dialog first if there are unpublished refs). */
|
|
22
|
+
/** Publish the draft. */
|
|
25
23
|
handlePublish: () => void;
|
|
26
24
|
/** Publish and, on success, return to the rail's list (0.0.39 "Publish & close"). */
|
|
27
25
|
handlePublishAndClose: () => void;
|
|
28
|
-
/** Confirm-through: publish despite the unpublished hard references. */
|
|
29
|
-
confirmPublish: () => void;
|
|
30
|
-
/** Dismiss the confirm-through dialog (also clears a pending "& close" intent). */
|
|
31
|
-
cancelPublishConfirm: () => void;
|
|
32
26
|
}
|
|
33
27
|
/**
|
|
34
|
-
* EC-268 /
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
28
|
+
* EC-268 / 0.0.39 — the publish controller. Owns the publish indicator state and the
|
|
29
|
+
* 0.0.39 "Publish & close" navigation (a successful publish returns to the rail-owned
|
|
30
|
+
* list).
|
|
31
|
+
*
|
|
32
|
+
* EC-589: the EC-300 confirm-through for unpublished reuse embeds is gone. Integrity is
|
|
33
|
+
* two-state and declared: a STRICT relation to an unpublished target is a blocking
|
|
34
|
+
* validation error (the reuse embed declares `strict: true` on its relation prop), so
|
|
35
|
+
* the publish button is disabled by the same issues that gate everything else — and
|
|
36
|
+
* `documents.publish` refuses it for any client; a weak one publishes with a warning.
|
|
39
37
|
*/
|
|
40
38
|
export declare function usePublishController(input: {
|
|
41
39
|
source: CollectionViewSource;
|
|
42
40
|
docRef: DocumentRef;
|
|
43
|
-
collection: CollectionDef;
|
|
44
41
|
projectId: string;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
dirty: boolean;
|
|
48
|
-
hasUnpublishedRefs: boolean;
|
|
42
|
+
/** EC-595 A2 — the editor's FORCED FLUSH (commit everything uncommitted, await persistence). */
|
|
43
|
+
flushDraft: () => Promise<void>;
|
|
49
44
|
}): PublishController;
|