@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
|
@@ -1,66 +1,73 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, seedDefaults,
|
|
3
|
+
import { applyDocumentPatches, createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, seedDefaults, titleFieldOf, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
|
|
4
|
+
import { createUuidIdFactory } from '@elytracms/core/persistence';
|
|
4
5
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
6
|
import { configureStudioLocales, useStudioLocale } from '@elytracms/studio/lib/locale';
|
|
6
7
|
import { studioManifestLookup } from '@elytracms/studio/lib/registry';
|
|
7
8
|
import { resolveDeploymentCollections, resolveDeploymentLocaleConfig, resolveDeploymentRoutes, } from '@elytracms/studio/lib/workspace';
|
|
8
9
|
import { Heading, Panel, Stack, Text } from '@elytracms/studio/ui';
|
|
9
10
|
import { assetLibraryOracle, gateValidateOptions } from './gate-validation.js';
|
|
10
|
-
import { clearSessionDocumentValidator, setSessionDocumentValidator, } from '@elytracms/studio/lib/cms/document-gate';
|
|
11
11
|
import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils.js';
|
|
12
12
|
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
13
|
-
import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
13
|
+
import { createCmsPersister, getStudioActor, getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
14
|
+
import { publishInSession, publishWithRevision as publishRevision, } from '@elytracms/studio/lib/operations/publish-with-revision';
|
|
15
|
+
import { operationErrorMessage } from '@elytracms/studio/lib/persona/labels';
|
|
16
|
+
import { fieldWritePatch } from '@elytracms/studio/lib/cms/field-patch';
|
|
14
17
|
import { ALL_VIEW_ID, DEFAULT_VIEW_OVERRIDE_ID, canonicalViewState, configViewIssues, defaultConfigViewId, defaultViewState, deprecatedDefaultFlagViewIds, hasDefaultViewOverride, pseudoColumns, resolveCollectionViews, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
|
|
15
18
|
import { useOptionalAssets } from '../assets/assets-store.js';
|
|
16
19
|
import { consumeStagedDocuments } from './document-staging.js';
|
|
17
20
|
import { commitCellEdit } from './cell-edit.js';
|
|
18
21
|
import { runBulkAction } from './bulk-actions.js';
|
|
19
|
-
import { cleanHiddenForPublish } from './publish-clean.js';
|
|
20
22
|
const { Context: CmsContext, useRequired: useCms } = createRequiredContext('Cms');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
23
|
+
/** The stable empty conflict list (referential stability for the surfaces' memos). */
|
|
24
|
+
const NO_CONFLICTS = [];
|
|
24
25
|
/**
|
|
25
|
-
* EC-
|
|
26
|
-
*
|
|
27
|
-
* `seq` is an in-memory, per-session counter (reset to 0 on every reload). A
|
|
28
|
-
* document minted `new-<collection>-<n>` in a PRIOR session can already occupy
|
|
29
|
-
* whatever `n` this session's counter would next reissue — the pre-fix bug:
|
|
30
|
-
* `+ New` on a `post` collection that already held a (possibly published)
|
|
31
|
-
* `new-post-1` handed out that SAME id again, silently opening the existing
|
|
32
|
-
* document for editing instead of creating a fresh draft. Walking the counter
|
|
33
|
-
* past every id already in `existingIds` makes the mint collision-free
|
|
34
|
-
* regardless of what a prior session left behind.
|
|
26
|
+
* EC-595 thin presence — ONE id per tab (per page load): the heartbeat's identity, so a
|
|
27
|
+
* second tab of the same user is a second presence, and the editor leaves itself out.
|
|
35
28
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
seq.current += 1;
|
|
40
|
-
id = `new-${collectionId}-${seq.current}`;
|
|
41
|
-
} while (existingIds.has(id));
|
|
42
|
-
return id;
|
|
29
|
+
const PRESENCE_SESSION_ID = createUuidIdFactory()();
|
|
30
|
+
function primaryField(collection) {
|
|
31
|
+
return titleFieldOf(collection)?.name ?? collection.fields[0]?.name ?? 'id';
|
|
43
32
|
}
|
|
44
33
|
/**
|
|
45
|
-
* Build a fresh document for `collectionId
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* private `insertNewDocument` below), factored out so it's unit-testable
|
|
52
|
-
* without a `CmsProvider`.
|
|
34
|
+
* Build a fresh document for `collectionId` (EC-297 / EC-578): an id from THE injected
|
|
35
|
+
* id factory — UUIDs in the studio, so two sessions can never mint the same id (the old
|
|
36
|
+
* per-session `new-<collection>-<n>` counter minted `new-post-1` in both, and the second
|
|
37
|
+
* create overwrote the first) — and the collection's declared field defaults
|
|
38
|
+
* (`seedDefaults`; a localized default lands in the project's default-locale bucket,
|
|
39
|
+
* EC-580). Pure — unit-testable without a `CmsProvider`.
|
|
53
40
|
*/
|
|
54
|
-
export function buildNewDocument(collectionId, registry,
|
|
55
|
-
const existingIds = new Set(existingDocs.filter((d) => d.collection === collectionId).map((d) => d.id));
|
|
56
|
-
const id = mintDocumentId(collectionId, seq, existingIds);
|
|
41
|
+
export function buildNewDocument(collectionId, registry, newId, locales) {
|
|
57
42
|
const collection = registry.get(collectionId);
|
|
58
43
|
return {
|
|
59
44
|
collection: collectionId,
|
|
60
|
-
id,
|
|
45
|
+
id: newId(),
|
|
61
46
|
...(collection ? seedDefaults(collection, locales) : { values: {} }),
|
|
62
47
|
};
|
|
63
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* EC-274 → EC-595 A2 (critic correction 9) — a singleton's first document:
|
|
51
|
+
* `documents.create({ singleton: true })` (the store's `ifCollectionEmpty`
|
|
52
|
+
* precondition). The race loser's `conflict` carries the winner's row, which it ADOPTS
|
|
53
|
+
* — two sessions opening an empty singleton end up on ONE document. Exported for tests.
|
|
54
|
+
*/
|
|
55
|
+
export async function createOrAdoptSingleton(operations, projectId, collectionId, seeded) {
|
|
56
|
+
const result = await operations.documents.create({
|
|
57
|
+
projectId,
|
|
58
|
+
collection: collectionId,
|
|
59
|
+
values: seeded.values ?? {},
|
|
60
|
+
...(seeded.localized ? { localized: seeded.localized } : {}),
|
|
61
|
+
singleton: true,
|
|
62
|
+
});
|
|
63
|
+
if (result.ok)
|
|
64
|
+
return { ok: true, record: result.value, adopted: false };
|
|
65
|
+
const error = result.error;
|
|
66
|
+
if (error.code === 'conflict' && 'current' in error && error.current) {
|
|
67
|
+
return { ok: true, record: error.current, adopted: true };
|
|
68
|
+
}
|
|
69
|
+
return { ok: false, error };
|
|
70
|
+
}
|
|
64
71
|
/**
|
|
65
72
|
* Does `docs` already hold a document for `collectionId`? `ensureSingletonDocument`'s
|
|
66
73
|
* empty branch re-checks this INSIDE the `setDocuments` updater (against the
|
|
@@ -148,7 +155,9 @@ export function CmsProvider(props) {
|
|
|
148
155
|
function MissingProjectLocales() {
|
|
149
156
|
return (_jsx(Panel, { className: "m-6 p-6", children: _jsxs(Stack, { gap: "2", children: [_jsx(Heading, { level: 2, size: "h4", children: translate('cms.session.noLocales.title') }), _jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.session.noLocales.body') })] }) }));
|
|
150
157
|
}
|
|
151
|
-
function CmsSession({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialChangedKeys, seedReflectsWrites, localeConfig, }) {
|
|
158
|
+
function CmsSession({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialChangedKeys, initialRevisions, idFactory, seedReflectsWrites, localeConfig, }) {
|
|
159
|
+
// EC-578: THE id factory for this session (documents created here).
|
|
160
|
+
const [newId] = useState(() => idFactory ?? createUuidIdFactory());
|
|
152
161
|
const [documents, setDocuments] = useState(() => {
|
|
153
162
|
const seeded = (initialDocuments ?? []).map((d) => structuredClone(d));
|
|
154
163
|
// EC-320 (0.0.44 follow-up): merge any documents STAGED by the
|
|
@@ -259,8 +268,12 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
259
268
|
// changed verdict for the record's ref. Referentially stable — the persister
|
|
260
269
|
// closes over it once.
|
|
261
270
|
const adoptSavedRecord = useCallback((record) => {
|
|
262
|
-
|
|
263
|
-
|
|
271
|
+
const ref = { collection: record.document.collection, id: record.document.id };
|
|
272
|
+
stampHasUnpublishedChanges(ref, record.hasUnpublishedChanges);
|
|
273
|
+
// EC-595 A2: an adopted row also carries the chokepoint's last-write instant.
|
|
274
|
+
if (record.updatedAt)
|
|
275
|
+
stampUpdatedAt(ref, record.updatedAt);
|
|
276
|
+
}, [stampHasUnpublishedChanges, stampUpdatedAt]);
|
|
264
277
|
const [activeViews, setActiveViews] = useState({});
|
|
265
278
|
// The live table state per collection where it diverges from the active
|
|
266
279
|
// view; absent key = exactly the active view's state (not dirty).
|
|
@@ -277,10 +290,11 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
277
290
|
...(initialCollections ?? []),
|
|
278
291
|
]);
|
|
279
292
|
const [persistError, setPersistError] = useState(null);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
//
|
|
293
|
+
// EC-595 A2: the open per-field conflicts, by document key.
|
|
294
|
+
const [conflictsByKey, setConflictsByKey] = useState(() => new Map());
|
|
295
|
+
// EC-274 → EC-595 A2: collection ids whose singleton create this session already
|
|
296
|
+
// issued — a synchronous guard so a re-invoked effect (or StrictMode's double mount)
|
|
297
|
+
// never issues a second create (the store's `ifCollectionEmpty` would refuse it anyway).
|
|
284
298
|
const ensuredSingletons = useRef(new Set());
|
|
285
299
|
// Latest session state for the persister's lazy baseline (no re-subscribing).
|
|
286
300
|
const collectionsRef = useRef(collections);
|
|
@@ -293,17 +307,34 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
293
307
|
projectId,
|
|
294
308
|
getOperations: async () => (await getStudioOperations()).operations,
|
|
295
309
|
getCollections: () => collectionsRef.current,
|
|
296
|
-
getDocuments: () => documentsRef.current,
|
|
297
310
|
// EC-277: self-type composition static props on write (the manifest half of
|
|
298
311
|
// the lens), so extraction stays schema-free. Document fields self-type via
|
|
299
312
|
// the schema alone (above); the registry adds blocks/embed prop coverage.
|
|
300
313
|
lookupManifest: studioManifestLookup(),
|
|
301
314
|
onError: setPersistError,
|
|
302
|
-
// EC-
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
315
|
+
// EC-595 A2: the persister owns the session's view of every document it writes —
|
|
316
|
+
// the adopted server row with the unflushed local patches re-applied — so an
|
|
317
|
+
// adopted row (a write result, a subscription push, a conflict's current) shows
|
|
318
|
+
// here at once, canonical values included (EC-586 found-but-left).
|
|
319
|
+
onDocument: (ref, doc) => setDocuments((prev) => (doc ? upsertDoc(prev, doc) : removeDoc(prev, ref))),
|
|
320
|
+
// EC-449: adopt the server-computed `hasUnpublishedChanges` verdict (and the
|
|
321
|
+
// timestamps) from every adopted row — the chokepoint's, never a client guess.
|
|
322
|
+
onRecord: adoptSavedRecord,
|
|
323
|
+
onConflicts: (ref, conflicts) => setConflictsByKey((prev) => {
|
|
324
|
+
const next = new Map(prev);
|
|
325
|
+
if (conflicts.length === 0)
|
|
326
|
+
next.delete(documentKey(ref));
|
|
327
|
+
else
|
|
328
|
+
next.set(documentKey(ref), conflicts);
|
|
329
|
+
return next;
|
|
330
|
+
}),
|
|
306
331
|
});
|
|
332
|
+
// The base every hydrated document's patches are made against (EC-595 A2).
|
|
333
|
+
for (const doc of initialDocuments ?? []) {
|
|
334
|
+
const revision = initialRevisions?.[documentKey(doc)];
|
|
335
|
+
if (revision !== undefined)
|
|
336
|
+
persisterRef.current.track({ document: doc, revision });
|
|
337
|
+
}
|
|
307
338
|
}
|
|
308
339
|
// EC-224: published-ness IS the version pointer (`publishedKeys`) — the SOLE
|
|
309
340
|
// studio source of truth. Declared up here (EC-497) because it is part of THE
|
|
@@ -311,10 +342,6 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
311
342
|
// helpers — now also takes.
|
|
312
343
|
const isPublished = useCallback((ref) => publishedKeys.has(documentKey(ref)), [publishedKeys]);
|
|
313
344
|
const registry = useMemo(() => createCollectionRegistry(collections), [collections]);
|
|
314
|
-
// EC-497: the gate seam registers ONCE and reads through refs, so it always sees
|
|
315
|
-
// the latest session without re-registering on every keystroke.
|
|
316
|
-
const registryRef = useRef(registry);
|
|
317
|
-
registryRef.current = registry;
|
|
318
345
|
const store = useMemo(() => createDocumentStore(documents), [documents]);
|
|
319
346
|
// Asset references are validated against the REAL asset library (assets live in
|
|
320
347
|
// their own repository, not as CMS documents). EC-588: the oracle returns the
|
|
@@ -348,18 +375,36 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
348
375
|
// detail header, which read the same view.
|
|
349
376
|
return resolveDocumentTitle(collection, valuesInLocale(collection, doc, activeLocale, localeConfig), ref.id);
|
|
350
377
|
}, [documents, registry, localeConfig, activeLocale]);
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
378
|
+
const commitPatches = useCallback((ref, patches, bases) => {
|
|
379
|
+
if (patches.length === 0)
|
|
380
|
+
return Promise.resolve(undefined);
|
|
381
|
+
stampUpdatedAt(ref); // EC-445: a write bumps the $updatedAt column (the row's own stamp follows)
|
|
382
|
+
const persister = persisterRef.current;
|
|
383
|
+
const current = documentsRef.current.find((d) => matchesRef(d, ref));
|
|
384
|
+
if (!persister) {
|
|
385
|
+
// A fixture session (no project): apply locally, the same merge the stores run.
|
|
386
|
+
if (current) {
|
|
387
|
+
const next = applyDocumentPatches({ document: current, patches, since: [] }).document;
|
|
388
|
+
setDocuments((prev) => replaceDoc(prev, next));
|
|
389
|
+
}
|
|
390
|
+
return Promise.resolve(undefined);
|
|
391
|
+
}
|
|
392
|
+
return persister.commit(ref, patches, current, bases);
|
|
393
|
+
}, [stampUpdatedAt]);
|
|
394
|
+
const insertDocument = useCallback((doc) => {
|
|
395
|
+
stampUpdatedAt(doc); // EC-445: a fresh document's $updatedAt starts at creation
|
|
396
|
+
const persister = persisterRef.current;
|
|
397
|
+
if (persister)
|
|
398
|
+
persister.create(doc);
|
|
399
|
+
else
|
|
400
|
+
setDocuments((prev) => upsertDoc(prev, doc));
|
|
357
401
|
}, [stampUpdatedAt]);
|
|
358
402
|
const commitFieldEdit = useCallback((input) => {
|
|
359
403
|
// Pure map-replace: an edit always targets a document already open in
|
|
360
404
|
// the session, so a miss (already gone) is a no-op, never an append.
|
|
361
405
|
const replaceWith = (doc) => setDocuments((prev) => replaceDoc(prev, doc));
|
|
362
406
|
const persister = persisterRef.current;
|
|
407
|
+
const ref = { collection: input.doc.collection, id: input.doc.id };
|
|
363
408
|
return commitCellEdit({
|
|
364
409
|
registry,
|
|
365
410
|
documents: documentsRef.current,
|
|
@@ -375,20 +420,35 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
375
420
|
replaceWith(doc);
|
|
376
421
|
stampUpdatedAt(doc);
|
|
377
422
|
},
|
|
378
|
-
|
|
423
|
+
// EC-595 A2: in a persisted session the persister's view is the truth — a
|
|
424
|
+
// refused cell shows theirs (mine is dropped, see below), a failed one keeps
|
|
425
|
+
// the local value queued for the next flush (no silent loss).
|
|
426
|
+
rollbackSession: (previous) => replaceWith(persister?.documentOf(ref) ?? previous),
|
|
379
427
|
...(persister && projectId !== undefined
|
|
380
428
|
? {
|
|
429
|
+
// EC-595 A2: ONE field patch through the persister's queue — at the
|
|
430
|
+
// revision the row was at — never a whole-document side channel.
|
|
381
431
|
dispatch: async (next) => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
432
|
+
const patch = fieldWritePatch(input.collection, next, input.field.name, input.locale);
|
|
433
|
+
const outcome = await persister.commit(ref, [patch], input.doc);
|
|
434
|
+
if (outcome.status === 'applied')
|
|
435
|
+
return { ok: true, value: outcome.record };
|
|
436
|
+
if (outcome.status === 'failed')
|
|
437
|
+
return { ok: false, error: outcome.error };
|
|
438
|
+
// Refused: someone changed this cell's field since the row loaded. A
|
|
439
|
+
// table cell has no room for take/keep, so it takes theirs and says so;
|
|
440
|
+
// the editor offers the choice.
|
|
441
|
+
for (const conflict of outcome.conflicts)
|
|
442
|
+
persister.resolveConflict(ref, conflict.id, 'theirs');
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
error: {
|
|
446
|
+
code: 'conflict',
|
|
447
|
+
key: documentKey(ref),
|
|
448
|
+
message: 'This field was changed by someone else since you loaded it — their value is shown.',
|
|
449
|
+
messageRef: { key: 'cms.conflict.cellRefused' },
|
|
450
|
+
},
|
|
451
|
+
};
|
|
392
452
|
},
|
|
393
453
|
}
|
|
394
454
|
: {}),
|
|
@@ -400,61 +460,47 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
400
460
|
isPublished,
|
|
401
461
|
findAsset,
|
|
402
462
|
stampUpdatedAt,
|
|
403
|
-
adoptSavedRecord,
|
|
404
463
|
]);
|
|
405
|
-
// Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
|
|
406
|
-
// `buildNewDocument`) + optimistic append + persist, consumed by both
|
|
407
|
-
// `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
|
|
408
|
-
// branch (`guardExisting: true` — re-checks "no document for this collection
|
|
409
|
-
// yet" against the LATEST `prev` inside the `setDocuments` updater, not the
|
|
410
|
-
// `documentsRef.current` snapshot the caller already checked, so a
|
|
411
|
-
// concurrent invocation — StrictMode's double-invoked effect — can never
|
|
412
|
-
// materialize a second singleton document).
|
|
413
|
-
const insertNewDocument = useCallback((collectionId, options) => {
|
|
414
|
-
const doc = buildNewDocument(collectionId, registry, seq, documentsRef.current, localeConfig);
|
|
415
|
-
setDocuments((prev) => options?.guardExisting && hasDocumentFor(prev, collectionId) ? prev : [...prev, doc]);
|
|
416
|
-
stampUpdatedAt(doc); // EC-445: a fresh draft's $updatedAt starts at creation time
|
|
417
|
-
persisterRef.current?.upsertDocument(doc);
|
|
418
|
-
return doc;
|
|
419
|
-
}, [registry, stampUpdatedAt, localeConfig]);
|
|
420
464
|
const createDocument = useCallback((collectionId) => {
|
|
421
|
-
|
|
465
|
+
// EC-578: the id comes from the injected factory; create is insert-if-absent.
|
|
466
|
+
const doc = buildNewDocument(collectionId, registry, newId, localeConfig);
|
|
467
|
+
insertDocument(doc);
|
|
422
468
|
return { collection: collectionId, id: doc.id };
|
|
423
|
-
}, [
|
|
424
|
-
// EC-274 (re-scopes EC-217): a singleton is a UI lens, not a fixed id.
|
|
425
|
-
// the collection's
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
469
|
+
}, [registry, newId, localeConfig, insertDocument]);
|
|
470
|
+
// EC-274 (re-scopes EC-217): a singleton is a UI lens, not a fixed id. When the
|
|
471
|
+
// session holds none of the collection's documents, create one through
|
|
472
|
+
// `documents.create({ singleton: true })` (EC-595 A2) — the store's
|
|
473
|
+
// `ifCollectionEmpty` precondition, so two sessions on an empty singleton make ONE
|
|
474
|
+
// document; the loser's conflict carries the winner's row, which it adopts. Either
|
|
475
|
+
// way the document lands in the session and the gate opens it. Idempotent (the ref
|
|
476
|
+
// guard); a fixture session (no project) creates locally.
|
|
431
477
|
const ensureSingletonDocument = useCallback((collectionId) => {
|
|
478
|
+
if (documentsRef.current.some((d) => d.collection === collectionId))
|
|
479
|
+
return;
|
|
480
|
+
if (ensuredSingletons.current.has(collectionId))
|
|
481
|
+
return;
|
|
482
|
+
ensuredSingletons.current.add(collectionId);
|
|
432
483
|
const collection = registry.get(collectionId);
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (resolution.kind === 'drift') {
|
|
440
|
-
return { collection: collectionId, id: resolution.docs[0].id };
|
|
441
|
-
}
|
|
442
|
-
// Empty: create the lone document once. The ref guard (synchronous) plus the
|
|
443
|
-
// updater's "any doc in collection" check make this idempotent under a
|
|
444
|
-
// re-invoked effect or StrictMode's double mount — never a second draft.
|
|
445
|
-
const existing = documentsRef.current.find((d) => d.collection === collectionId);
|
|
446
|
-
if (existing)
|
|
447
|
-
return { collection: collectionId, id: existing.id };
|
|
448
|
-
if (ensuredSingletons.current.has(collectionId)) {
|
|
449
|
-
return { collection: collectionId, id: `new-${collectionId}-pending` };
|
|
484
|
+
const seeded = collection ? seedDefaults(collection, localeConfig) : { values: {} };
|
|
485
|
+
const persister = persisterRef.current;
|
|
486
|
+
if (!persister || projectId === undefined) {
|
|
487
|
+
const doc = { collection: collectionId, id: newId(), ...seeded };
|
|
488
|
+
setDocuments((prev) => (hasDocumentFor(prev, collectionId) ? prev : [...prev, doc]));
|
|
489
|
+
return;
|
|
450
490
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
491
|
+
void (async () => {
|
|
492
|
+
const { operations } = await getStudioOperations();
|
|
493
|
+
const outcome = await createOrAdoptSingleton(operations, projectId, collectionId, seeded);
|
|
494
|
+
if (!outcome.ok) {
|
|
495
|
+
setPersistError(outcome.error);
|
|
496
|
+
ensuredSingletons.current.delete(collectionId);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
persister.track(outcome.record);
|
|
500
|
+
adoptSavedRecord(outcome.record);
|
|
501
|
+
setDocuments((prev) => upsertDoc(prev, outcome.record.document));
|
|
502
|
+
})();
|
|
503
|
+
}, [registry, localeConfig, projectId, newId, adoptSavedRecord]);
|
|
458
504
|
const deleteDocument = useCallback((ref) => {
|
|
459
505
|
setDocuments((prev) => removeDoc(prev, ref));
|
|
460
506
|
persisterRef.current?.deleteDocument(ref);
|
|
@@ -501,19 +547,47 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
501
547
|
stampHasUnpublishedChanges(ref, record.value.hasUnpublishedChanges);
|
|
502
548
|
}
|
|
503
549
|
}, [projectId, setPublished, stampUpdatedAt, stampPublishedAt, stampHasUnpublishedChanges]);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
550
|
+
/**
|
|
551
|
+
* EC-595 binding point 6 — publish the revision the client validated. FORCED FLUSH
|
|
552
|
+
* first (the pending patches land, the session adopts the committed draft), then
|
|
553
|
+
* `documents.publish({ expectedRevision })`: the store refuses when the draft moved
|
|
554
|
+
* since (a colleague's save landed in between — Codex P05). On that refusal the
|
|
555
|
+
* session adopts the current row, RE-VALIDATES it, and retries — bounded, and never
|
|
556
|
+
* past a validation error the new draft carries. Returns the outcome so a bulk run
|
|
557
|
+
* can report it.
|
|
558
|
+
*/
|
|
559
|
+
const publishWithRevision = useCallback(async (ref) => {
|
|
560
|
+
const persister = persisterRef.current;
|
|
561
|
+
if (!persister || projectId === undefined)
|
|
562
|
+
return { ok: true, value: undefined };
|
|
563
|
+
const { operations } = await getStudioOperations();
|
|
564
|
+
return publishRevision(ref, {
|
|
565
|
+
flush: (target) => persister.flush(target),
|
|
566
|
+
revisionOf: (target) => persister.revisionOf(target),
|
|
567
|
+
publish: (target, expectedRevision) => operations.documents.publish({
|
|
568
|
+
projectId,
|
|
569
|
+
ref: target,
|
|
570
|
+
...(expectedRevision !== undefined ? { expectedRevision } : {}),
|
|
571
|
+
}),
|
|
572
|
+
adopt: (record) => persister.adoptRecord(record),
|
|
573
|
+
documentOf: (target) => persister.documentOf(target),
|
|
574
|
+
validate: (doc) => validateRef.current(doc),
|
|
575
|
+
});
|
|
576
|
+
}, [projectId]);
|
|
577
|
+
const publishDocument = useCallback((ref) =>
|
|
578
|
+
// EC-585: the hidden-clear runs INSIDE `documents.publish` (for this and every
|
|
579
|
+
// other caller), and a refusal rejects — see `publishInSession`.
|
|
580
|
+
publishInSession(ref, {
|
|
581
|
+
markPublished: (target) => setPublished(target, true), // optimistic
|
|
582
|
+
publish: publishWithRevision,
|
|
583
|
+
reconcile: reconcilePublished,
|
|
584
|
+
report: setPersistError,
|
|
585
|
+
describe: operationErrorMessage,
|
|
586
|
+
}), [setPublished, reconcilePublished, publishWithRevision]);
|
|
513
587
|
const unpublishDocument = useCallback(async (ref) => {
|
|
514
588
|
setPublished(ref, false); // optimistic
|
|
515
589
|
persisterRef.current?.unpublishDocument(ref);
|
|
516
|
-
await persisterRef.current?.flush();
|
|
590
|
+
await persisterRef.current?.flush(ref);
|
|
517
591
|
await reconcilePublished(ref);
|
|
518
592
|
}, [setPublished, reconcilePublished]);
|
|
519
593
|
const loadDocumentVersions = useCallback(async (ref) => {
|
|
@@ -540,10 +614,10 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
540
614
|
const restoreDocumentVersion = useCallback(async (ref, version, scope) => {
|
|
541
615
|
if (projectId === undefined)
|
|
542
616
|
return null;
|
|
543
|
-
//
|
|
544
|
-
// snapshot it
|
|
545
|
-
//
|
|
546
|
-
await persisterRef.current?.
|
|
617
|
+
// FORCED FLUSH: land any queued edits before the restore reads the current draft
|
|
618
|
+
// to snapshot it. This is what makes a granular (field/block) restore merge over
|
|
619
|
+
// the LATEST draft — and the restore replaces AT the revision it read (EC-595).
|
|
620
|
+
await persisterRef.current?.flush(ref);
|
|
547
621
|
const { operations } = await getStudioOperations();
|
|
548
622
|
const result = await operations.documents.restore({
|
|
549
623
|
projectId,
|
|
@@ -555,15 +629,15 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
555
629
|
setPersistError(result.error);
|
|
556
630
|
return null;
|
|
557
631
|
}
|
|
558
|
-
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
632
|
+
// EC-595 A2: the restored row is adopted as the new base (its revision, its
|
|
633
|
+
// canonical values, its changed flag — EC-449) and shows in the session.
|
|
634
|
+
const persister = persisterRef.current;
|
|
635
|
+
if (persister)
|
|
636
|
+
persister.adoptRecord(result.value);
|
|
637
|
+
else
|
|
638
|
+
setDocuments((prev) => replaceDoc(prev, result.value.document));
|
|
565
639
|
adoptSavedRecord(result.value);
|
|
566
|
-
return
|
|
640
|
+
return persister?.documentOf(ref) ?? result.value.document;
|
|
567
641
|
}, [projectId, adoptSavedRecord]);
|
|
568
642
|
/**
|
|
569
643
|
* EC-497 — THE option bag (AD-497a), built once and used by every surface that
|
|
@@ -596,22 +670,15 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
596
670
|
...documentReachabilityIssues({ routes: resolveDeploymentRoutes(), localeConfig }, collection, doc, { siblings: documents.filter((sibling) => sibling.collection === doc.collection) }),
|
|
597
671
|
];
|
|
598
672
|
}, [registry, validateOptions, documents, localeConfig]);
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const gateRef = useRef(validate);
|
|
609
|
-
gateRef.current = validate;
|
|
610
|
-
useEffect(() => {
|
|
611
|
-
const owned = (doc) => registryRef.current.has(doc.collection) ? gateRef.current(doc) : [];
|
|
612
|
-
setSessionDocumentValidator(owned);
|
|
613
|
-
return () => clearSessionDocumentValidator(owned);
|
|
614
|
-
}, []);
|
|
673
|
+
// The publish retry re-validates with the session's latest rules (a ref: no churn).
|
|
674
|
+
const validateRef = useRef(validate);
|
|
675
|
+
validateRef.current = validate;
|
|
676
|
+
// EC-595 A2 (F23): this session no longer publishes its validator to a module-global
|
|
677
|
+
// gate seam. The publish gate validates with the operations bundle's runtime
|
|
678
|
+
// validator (`lib/operations/runtime-validator.ts` — the operation's project config +
|
|
679
|
+
// oracles prefetched through the adapter), so mounting a second provider (the
|
|
680
|
+
// transfer dialog) can no longer change a verdict. `validate` above stays the
|
|
681
|
+
// session's DISPLAY validation (the editor's issues, cell edit, import, AI).
|
|
615
682
|
const importDocuments = useCallback((collectionId, raw) => {
|
|
616
683
|
const report = { added: 0, rejected: [] };
|
|
617
684
|
// Pre-parse so intra-batch relation/asset targets resolve during validation.
|
|
@@ -659,9 +726,20 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
659
726
|
// Replace-or-append (upsert): an imported row may be brand new to the
|
|
660
727
|
// session, so each accepted document must land even with no prior
|
|
661
728
|
// entry for its ref.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
729
|
+
// EC-595 A2: a row new to the store is CREATED (insert-if-absent); a row the
|
|
730
|
+
// session already holds is REPLACED at the revision it was loaded at — an import
|
|
731
|
+
// never overwrites a change made since.
|
|
732
|
+
const persister = persisterRef.current;
|
|
733
|
+
if (!persister)
|
|
734
|
+
setDocuments((prev) => accepted.reduce((acc, doc) => upsertDoc(acc, doc), prev));
|
|
735
|
+
for (const doc of accepted) {
|
|
736
|
+
if (!persister)
|
|
737
|
+
continue;
|
|
738
|
+
if (persister.revisionOf(doc) !== undefined)
|
|
739
|
+
persister.replace(doc);
|
|
740
|
+
else
|
|
741
|
+
persister.create(doc);
|
|
742
|
+
}
|
|
665
743
|
// EC-224: imports land as the working DRAFT. Publish-ness is the version
|
|
666
744
|
// pointer (`publishedKeys`), set by publishing — never carried in the
|
|
667
745
|
// imported row — so an import never auto-publishes; the editor publishes
|
|
@@ -762,20 +840,19 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
762
840
|
let dispatch;
|
|
763
841
|
const persister = persisterRef.current;
|
|
764
842
|
if (persister && projectId !== undefined) {
|
|
765
|
-
// Pending queued
|
|
766
|
-
|
|
767
|
-
await persister.ensureBaseline();
|
|
843
|
+
// Pending queued writes land first, then one awaited command per item.
|
|
844
|
+
await persister.flush();
|
|
768
845
|
const { operations } = await getStudioOperations();
|
|
769
|
-
dispatch = (bulkAction, ref) => {
|
|
846
|
+
dispatch = async (bulkAction, ref) => {
|
|
770
847
|
if (bulkAction === 'delete')
|
|
771
848
|
return operations.documents.delete({ projectId, ref });
|
|
849
|
+
// EC-595 point 6: publish the revision validated, with the same retry.
|
|
772
850
|
if (bulkAction === 'publish')
|
|
773
|
-
return
|
|
851
|
+
return publishWithRevision(ref);
|
|
774
852
|
if (bulkAction === 'unpublish')
|
|
775
853
|
return operations.documents.unpublish({ projectId, ref });
|
|
776
|
-
// set-field:
|
|
777
|
-
//
|
|
778
|
-
// stores its scalar as-is.
|
|
854
|
+
// set-field: ONE field patch per document through the persister (EC-595 A2) —
|
|
855
|
+
// the operation canonicalises the value by the field (EC-277/EC-586).
|
|
779
856
|
const current = documentsRef.current.find((d) => matchesRef(d, ref));
|
|
780
857
|
if (!current) {
|
|
781
858
|
return Promise.resolve({
|
|
@@ -788,15 +865,24 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
788
865
|
},
|
|
789
866
|
});
|
|
790
867
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
868
|
+
const written = setFieldDoc(current);
|
|
869
|
+
const outcome = await persister.commit(ref, [fieldWritePatch(collection, written, setField.field, activeLocale)], current);
|
|
870
|
+
if (outcome.status === 'applied')
|
|
871
|
+
return { ok: true, value: outcome.record };
|
|
872
|
+
if (outcome.status === 'failed')
|
|
873
|
+
return { ok: false, error: outcome.error };
|
|
874
|
+
// Refused: the field changed since — the item fails visibly; theirs stays.
|
|
875
|
+
for (const conflict of outcome.conflicts)
|
|
876
|
+
persister.resolveConflict(ref, conflict.id, 'theirs');
|
|
877
|
+
return {
|
|
878
|
+
ok: false,
|
|
879
|
+
error: {
|
|
880
|
+
code: 'conflict',
|
|
881
|
+
key: documentKey(ref),
|
|
882
|
+
message: 'This field was changed by someone else since you loaded it — their value is kept.',
|
|
883
|
+
messageRef: { key: 'cms.conflict.cellRefused' },
|
|
884
|
+
},
|
|
885
|
+
};
|
|
800
886
|
};
|
|
801
887
|
}
|
|
802
888
|
return runBulkAction({
|
|
@@ -813,27 +899,9 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
813
899
|
isPublished,
|
|
814
900
|
...(findAsset ? { findAsset } : {}),
|
|
815
901
|
})),
|
|
816
|
-
// EC-
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
// EC-580: in the session's locale views — the single publish's same step.
|
|
820
|
-
...(collection
|
|
821
|
-
? {
|
|
822
|
-
cleanForPublish: (doc) => cleanHiddenForPublish(collection, doc, undefined, localeConfig),
|
|
823
|
-
}
|
|
824
|
-
: {}),
|
|
825
|
-
// Land a cleaned draft exactly the way the editor's publish does:
|
|
826
|
-
// session apply + the persister's self-typing upsert (EC-277 manifest
|
|
827
|
-
// lens), flushed so the serial queue lands the upsert BEFORE the
|
|
828
|
-
// awaited publish command snapshots the stored draft.
|
|
829
|
-
applyCleaned: async (_ref, cleaned) => {
|
|
830
|
-
setDocuments((prev) => replaceDoc(prev, cleaned));
|
|
831
|
-
const persister = persisterRef.current;
|
|
832
|
-
if (persister && projectId !== undefined) {
|
|
833
|
-
persister.upsertDocument(cleaned);
|
|
834
|
-
await persister.flush();
|
|
835
|
-
}
|
|
836
|
-
},
|
|
902
|
+
// EC-585: no clean step here — the hidden-clear runs inside the one publish
|
|
903
|
+
// operation each item's `publishWithRevision` dispatches (EC-425's shared
|
|
904
|
+
// clean point, moved to where every publish path crosses it).
|
|
837
905
|
// EC-224: a succeeded publish/unpublish moves the version pointer
|
|
838
906
|
// (`publishedKeys`) via setPublished — there is no separate flat
|
|
839
907
|
// `state` mirror to keep in sync (published-ness is derived from the
|
|
@@ -850,7 +918,13 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
850
918
|
// locale-aware path the dispatched upsert used). Not a plain
|
|
851
919
|
// `replaceDoc` — the replacement is computed FROM each matched
|
|
852
920
|
// document (`setFieldDoc(d)`), not a fixed value to swap in.
|
|
853
|
-
|
|
921
|
+
// A persisted session's view already holds the committed patch (the persister);
|
|
922
|
+
// a fixture session folds the value locally.
|
|
923
|
+
applySetField: (ref) => {
|
|
924
|
+
if (persisterRef.current)
|
|
925
|
+
return;
|
|
926
|
+
setDocuments((prev) => prev.map((d) => (matchesRef(d, ref) ? setFieldDoc(d) : d)));
|
|
927
|
+
},
|
|
854
928
|
...(dispatch ? { dispatch } : {}),
|
|
855
929
|
}, collectionId, ids, action, setField);
|
|
856
930
|
}, [
|
|
@@ -862,9 +936,62 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
862
936
|
setPublished,
|
|
863
937
|
isPublished,
|
|
864
938
|
findAsset,
|
|
865
|
-
adoptSavedRecord,
|
|
866
939
|
stampHasUnpublishedChanges,
|
|
940
|
+
publishWithRevision,
|
|
867
941
|
]);
|
|
942
|
+
// EC-595 point 7 — follow one open document: pushed rows go to the persister (applied
|
|
943
|
+
// under the local edits), presence to the caller. The capability lives on the
|
|
944
|
+
// operations bundle (the UI never holds a reactive Convex client).
|
|
945
|
+
const subscribeDocument = useCallback((ref, onPresence) => {
|
|
946
|
+
if (projectId === undefined)
|
|
947
|
+
return () => { };
|
|
948
|
+
let stop = () => { };
|
|
949
|
+
let stopped = false;
|
|
950
|
+
void getStudioOperations().then((studio) => {
|
|
951
|
+
if (stopped)
|
|
952
|
+
return;
|
|
953
|
+
stop = studio.subscribeDocument(projectId, ref, {
|
|
954
|
+
onSnapshot: (snapshot) => {
|
|
955
|
+
persisterRef.current?.adoptSnapshot(ref, snapshot.record);
|
|
956
|
+
onPresence(snapshot.presence);
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
return () => {
|
|
961
|
+
stopped = true;
|
|
962
|
+
stop();
|
|
963
|
+
};
|
|
964
|
+
}, [projectId]);
|
|
965
|
+
// EC-595 thin presence: this tab's heartbeat on a document (the focused field's path).
|
|
966
|
+
const reportPresence = useCallback((ref, path) => {
|
|
967
|
+
if (projectId === undefined)
|
|
968
|
+
return;
|
|
969
|
+
const actor = getStudioActor();
|
|
970
|
+
void getStudioOperations()
|
|
971
|
+
.then((studio) => studio.reportPresence(projectId, ref, {
|
|
972
|
+
sessionId: PRESENCE_SESSION_ID,
|
|
973
|
+
...(actor ? { actor: actor.id } : {}),
|
|
974
|
+
...(actor?.name ? { actorName: actor.name } : {}),
|
|
975
|
+
...(path ? { path } : {}),
|
|
976
|
+
}))
|
|
977
|
+
.catch(() => {
|
|
978
|
+
// Reported on the operations bus (EC-403 sees an expiry); presence is advisory.
|
|
979
|
+
});
|
|
980
|
+
}, [projectId]);
|
|
981
|
+
const clearPresence = useCallback((ref) => {
|
|
982
|
+
if (projectId === undefined)
|
|
983
|
+
return;
|
|
984
|
+
void getStudioOperations()
|
|
985
|
+
.then((studio) => studio.clearPresence(projectId, ref, PRESENCE_SESSION_ID))
|
|
986
|
+
.catch(() => { });
|
|
987
|
+
}, [projectId]);
|
|
988
|
+
const replaceDocument = useCallback((doc) => {
|
|
989
|
+
const persister = persisterRef.current;
|
|
990
|
+
if (persister)
|
|
991
|
+
persister.replace(doc);
|
|
992
|
+
else
|
|
993
|
+
setDocuments((prev) => replaceDoc(prev, doc));
|
|
994
|
+
}, []);
|
|
868
995
|
const store$ = useMemo(() => ({
|
|
869
996
|
...(projectId !== undefined ? { projectId } : {}),
|
|
870
997
|
collections,
|
|
@@ -874,10 +1001,21 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
874
1001
|
documentsOf: (collectionId) => documents.filter((d) => d.collection === collectionId),
|
|
875
1002
|
getDocument,
|
|
876
1003
|
titleOf,
|
|
877
|
-
|
|
1004
|
+
commitPatches,
|
|
1005
|
+
revisionOf: (ref) => persisterRef.current?.revisionOf(ref),
|
|
1006
|
+
insertDocument,
|
|
1007
|
+
newDocumentId: newId,
|
|
878
1008
|
commitFieldEdit,
|
|
879
1009
|
createDocument,
|
|
880
1010
|
ensureSingletonDocument,
|
|
1011
|
+
documentConflicts: (ref) => conflictsByKey.get(documentKey(ref)) ?? NO_CONFLICTS,
|
|
1012
|
+
resolveConflict: (ref, conflictId, choice) => persisterRef.current?.resolveConflict(ref, conflictId, choice),
|
|
1013
|
+
adoptDocumentSnapshot: (ref, record) => persisterRef.current?.adoptSnapshot(ref, record),
|
|
1014
|
+
subscribeDocument,
|
|
1015
|
+
reportPresence,
|
|
1016
|
+
clearPresence,
|
|
1017
|
+
presenceSessionId: PRESENCE_SESSION_ID,
|
|
1018
|
+
replaceDocument,
|
|
881
1019
|
deleteDocument,
|
|
882
1020
|
isPublished,
|
|
883
1021
|
documentUpdatedAt,
|
|
@@ -919,8 +1057,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
919
1057
|
addCollection: (def) => setCollections((prev) => [...prev.filter((c) => c.id !== def.id), def]),
|
|
920
1058
|
persistError,
|
|
921
1059
|
clearPersistError: () => setPersistError(null),
|
|
922
|
-
|
|
923
|
-
flushPersistence: () => persisterRef.current?.flush() ?? Promise.resolve(),
|
|
1060
|
+
flushPersistence: (ref) => persisterRef.current?.flush(ref) ?? Promise.resolve(),
|
|
924
1061
|
}), [
|
|
925
1062
|
projectId,
|
|
926
1063
|
collections,
|
|
@@ -930,10 +1067,17 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
|
|
|
930
1067
|
getCollection,
|
|
931
1068
|
getDocument,
|
|
932
1069
|
titleOf,
|
|
933
|
-
|
|
1070
|
+
commitPatches,
|
|
1071
|
+
insertDocument,
|
|
1072
|
+
newId,
|
|
934
1073
|
commitFieldEdit,
|
|
935
1074
|
createDocument,
|
|
936
1075
|
ensureSingletonDocument,
|
|
1076
|
+
conflictsByKey,
|
|
1077
|
+
subscribeDocument,
|
|
1078
|
+
reportPresence,
|
|
1079
|
+
clearPresence,
|
|
1080
|
+
replaceDocument,
|
|
937
1081
|
deleteDocument,
|
|
938
1082
|
isPublished,
|
|
939
1083
|
documentUpdatedAt,
|