@elytracms/studio 0.0.96 → 0.0.98
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/assets.ts +16 -2
- package/convex/cms.ts +153 -13
- package/convex/delivery-folded-drift.test.ts +104 -0
- package/convex/delivery.ts +24 -8
- package/convex/depth-codec-lookalike.test.ts +192 -0
- package/convex/fixture-accounts.test.ts +305 -0
- package/convex/fixtureAccounts.ts +298 -0
- package/convex/incarnation.test.ts +149 -0
- package/convex/ingest-attempt.test.ts +161 -0
- package/convex/ingest.test.ts +327 -9
- package/convex/ingest.ts +242 -24
- package/convex/prototype-key.test.ts +92 -0
- package/convex/receipt-races.test.ts +184 -0
- package/convex/records-zod-copy.test.ts +76 -0
- package/convex/records-zod-refinements.convex.test.ts +94 -0
- package/convex/schema.ts +20 -1
- package/convex/strict-delete.test.ts +129 -0
- package/dist/components/cms/cms-store.d.ts +6 -5
- package/dist/components/cms/cms-store.js +14 -34
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +12 -1
- package/dist/components/cms/field-control.js +16 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/import-batch.d.ts +23 -0
- package/dist/components/cms/import-batch.js +43 -0
- package/dist/components/cms/import-batch.js.map +1 -0
- package/dist/components/cms/rich-text/editor.js +13 -10
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/value.d.ts +9 -0
- package/dist/components/cms/rich-text/value.js +13 -0
- package/dist/components/cms/rich-text/value.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +76 -4
- package/dist/components/cms/use-document-draft.js +94 -25
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/records/record-lens.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +5 -2
- package/dist/lib/collection-view/collection-view-source.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 +5 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +5 -0
- package/dist/lib/i18n/messages.en.js +5 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +49 -5
- package/dist/lib/operations/cms-persistence.js +237 -47
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +27 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.js +2 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/package.json +5 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/value.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createBlockSequenceValue,\n isBlockSequenceValueShape,\n componentBlockNode,\n richTextDocsEquivalent,\n} from '@elytracms/core/rich-text'\nimport type {\n BlockNode,\n BlockSequenceValue,\n RichTextDoc,\n RichTextNode,\n TypographyNode,\n} from '@elytracms/core/rich-text'\nimport { isComponentBlockNode, isComponentNode } from '@elytracms/core/project-graph'\nimport type { JsonValue } from '@elytracms/core/project-graph'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport { UNKNOWN_NODE_CARRIER_TYPE } from './unknown-node'\n\n/**\n * Storage ⟷ editor projection for the CMS TipTap flow editor (EC-314, converged\n * by EC-547 per the EC-500 design).\n *\n * Storage is the converged block sequence `{ version: 2, content: BlockNode[] }`\n * — typography nodes byte-identical to the historical rich-text shape plus\n * first-class `elytra/component` nodes. TipTap requires ITS grammar (a\n * `{ type: 'doc', content }` document over a CLOSED schema), so this module is\n * the just-in-time projection between the two:\n *\n * - **Way in** ({@link toEditorDoc}): typography passes near-identically; an\n * `elytra/component` node becomes the existing ComponentEmbed atom (the\n * original node riding verbatim in `attrs.node`); any node type the editor\n * does not natively edit is wrapped into the opaque\n * {@link UNKNOWN_NODE_CARRIER_TYPE} atom so the closed ProseMirror schema\n * cannot drop it.\n * - **Way out** ({@link blockSequenceValueFromDoc}): embed atoms convert back to\n * tagged `elytra/component` nodes, carriers restore their carried JSON\n * byte-for-byte, typography is re-emitted with {@link normalizeEditorDoc}'s\n * lean-attrs canonicalisation applied.\n *\n * The round-trip law (EC-500 D3): the projection may only rewrite nodes the flow\n * editor natively edits; every other node survives byte-for-byte.\n *\n * Two editor-ergonomics concerns carry over unchanged from EC-314:\n *\n * 1. **Lean canonicalisation.** ProseMirror stamps every declared attribute onto a\n * node/mark, so TipTap emits `null` defaults the lean storage convention omits\n * (`link` → `ref: null`, `codeBlock` → `language: null`, `image` → `title: null`).\n * {@link normalizeEditorDoc} prunes those so an untouched document round-trips to\n * the same shape the extractor/renderer already read.\n *\n * 2. **No false dirty.** Opening a field must not mark it changed, and typing then\n * undoing back to the original must clear dirty. {@link isEquivalentToStored}\n * compares semantically in editor-projection space so the editor can re-emit the\n * *exact stored value* when nothing really changed.\n */\n\n/**\n * The storage node types the flow editor natively edits — the projection's ONE\n * wrap/pass decision: anything outside this set rides through the editor inside\n * the opaque carrier atom. Everything listed here MUST be declared by\n * `buildRichTextExtensions`' ProseMirror schema (an undeclared type would be\n * silently dropped); `extensions.test.ts` pins the two sets equal, so drift\n * between this declaration and the schema is loud.\n *\n * The ProseMirror schema declares TWO more types than this set: the\n * ComponentEmbed atom and the carrier — both editor-only projection vehicles\n * that are never valid STORAGE vocabulary (EC-548 retired `componentEmbed` at\n * rest). Excluding them here is deliberate and load-bearing: a STORED node\n * reusing either name is a foreigner and rides the carrier byte-for-byte,\n * instead of being misread as an editor atom (which for a stored\n * `componentEmbed` would silently rewrite it to `elytra/component` on save —\n * a compat migration the owner ruled out).\n */\nexport const EDITOR_NATIVE_NODE_TYPES: ReadonlySet<string> = new Set([\n 'doc',\n 'paragraph',\n 'text',\n 'heading',\n 'bulletList',\n 'orderedList',\n 'listItem',\n 'blockquote',\n 'codeBlock',\n 'hardBreak',\n 'horizontalRule',\n 'image',\n])\n\n/** ProseMirror node types that carry no reference/atom payload and render as prose. */\nconst EMPTY_TOLERANT_BLOCKS = new Set(['paragraph', 'heading'])\n\n/**\n * Project whatever a body-capable field currently holds into an editor document.\n *\n * Accepts the canonical v2 `{ version: 2, content }` sequence (object or JSON\n * string) and projects it per the module contract. Anything unparseable becomes\n * an empty document (never throws; validation is first-class elsewhere). The\n * projection is deliberately TOTAL and shape-driven, not schema-validating: a\n * v2 value with one malformed node must still open (the bad node rides in a\n * carrier, visibly labelled) rather than blanking the whole field — the editor\n * is not the validator.\n */\nexport function toEditorDoc(value: unknown): RichTextDoc {\n const sequence = asBlockSequenceShape(value)\n if (sequence) {\n return { type: 'doc', content: sequence.content.map(projectStoredNode) }\n }\n // Anything that is not the v2 sequence (a pre-convergence envelope, a bare\n // doc, legacy plaintext, junk) opens EMPTY — deliberately not projected:\n // validation names those shapes with the explicit \"re-seed\" issue (EC-548\n // deleted the staging acceptance), and quietly editing over them here would\n // hide it.\n return { type: 'doc', content: [] }\n}\n\n/** Structural v2 test, tolerating a JSON-string form of the same value. */\nfunction asBlockSequenceShape(value: unknown): { version: 2; content: unknown[] } | undefined {\n if (isBlockSequenceValueShape(value)) return value\n if (typeof value === 'string') {\n try {\n const parsed: unknown = JSON.parse(value)\n if (isBlockSequenceValueShape(parsed)) return parsed\n } catch {\n /* not JSON — fall through */\n }\n }\n return undefined\n}\n\n/**\n * One stored node → its editor projection. Total over arbitrary JSON:\n *\n * - a tagged component node → the ComponentEmbed atom, the node itself riding\n * verbatim (tag included) in `attrs.node`;\n * - a node type the editor natively edits → passed through, `content` recursed\n * (so a component nested in a blockquote still projects to an embed);\n * - EVERYTHING else — foreign types, a tagged-but-malformed component node,\n * even non-node JSON in the content array — → the opaque carrier atom, the\n * original payload riding verbatim in `attrs.node`.\n */\nfunction projectStoredNode(node: unknown): RichTextNode {\n if (isComponentBlockNode(node)) {\n return { type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: node as unknown as JsonValue } }\n }\n if (isEditorNativeNode(node)) {\n const out: RichTextNode = { ...node }\n if (node.content) out.content = node.content.map(projectStoredNode)\n return out\n }\n return { type: UNKNOWN_NODE_CARRIER_TYPE, attrs: { node: node as JsonValue } }\n}\n\nfunction isEditorNativeNode(node: unknown): node is RichTextNode {\n return (\n typeof node === 'object' &&\n node !== null &&\n !Array.isArray(node) &&\n typeof (node as { type?: unknown }).type === 'string' &&\n EDITOR_NATIVE_NODE_TYPES.has((node as { type: string }).type)\n )\n}\n\n/**\n * Project a (normalized) editor document back into the stored v2 sequence —\n * the way-out half of the module contract: embed atoms → tagged\n * `elytra/component` nodes, carriers → their carried JSON byte-for-byte,\n * typography re-emitted as-is (lean canonicalisation is\n * {@link normalizeEditorDoc}'s job, upstream).\n */\nexport function blockSequenceValueFromDoc(doc: RichTextDoc): BlockSequenceValue {\n return createBlockSequenceValue((doc.content ?? []).map(restoreStoredNode))\n}\n\nfunction restoreStoredNode(node: RichTextNode): BlockNode {\n if (node.type === COMPONENT_EMBED_NODE_TYPE) {\n const embedded: unknown = node.attrs?.node\n if (isComponentNode(embedded)) {\n // Re-tag through the one constructor (identity on already-tagged nodes),\n // so an atom payload the node factory built untagged leaves the editor\n // as a first-class tagged citizen — \"write current\".\n const { type: _tag, ...rest } = embedded\n return componentBlockNode(rest)\n }\n // An embed whose payload is missing/malformed: keep the node verbatim as\n // typography — never a silent drop; validation reports it elsewhere.\n return node as TypographyNode\n }\n if (node.type === UNKNOWN_NODE_CARRIER_TYPE) {\n const carried: unknown = node.attrs?.node\n // Byte-for-byte restore of the carried original (the D3 round-trip law).\n // An empty carrier (nothing to restore — not constructible via the\n // projection) degrades to the carrier node itself staying visible.\n if (carried !== null && carried !== undefined) return carried as BlockNode\n return node as TypographyNode\n }\n const out: TypographyNode = { ...(node as TypographyNode) }\n if (node.content) out.content = node.content.map(restoreStoredNode)\n return out\n}\n\n/**\n * Prune the `null` attribute values ProseMirror stamps from declared-but-unset\n * attributes, and drop the resulting empty `attrs` bags, so the editor's output\n * matches the lean storage convention. Recurses `content`; leaves `marks` attrs\n * pruned the same way. Pure; returns a fresh tree. (Embed and carrier payloads\n * are single non-null `attrs` entries, so pruning passes them through by\n * reference — their bytes are never rewritten here.)\n */\nexport function normalizeEditorDoc(doc: RichTextDoc): RichTextDoc {\n return { type: 'doc', content: (doc.content ?? []).map(pruneNode) }\n}\n\nfunction pruneNode(node: RichTextNode): RichTextNode {\n const out: RichTextNode = { type: node.type }\n const attrs = pruneAttrs(node.attrs)\n if (attrs) out.attrs = attrs\n if (typeof node.text === 'string') out.text = node.text\n if (node.marks && node.marks.length > 0) {\n out.marks = node.marks.map((mark) => {\n const markAttrs = pruneAttrs(mark.attrs)\n return markAttrs ? { type: mark.type, attrs: markAttrs } : { type: mark.type }\n })\n }\n if (node.content) out.content = node.content.map(pruneNode)\n return out\n}\n\nfunction pruneAttrs(\n attrs: Record<string, unknown> | undefined,\n): Record<string, never> | undefined {\n if (!attrs) return undefined\n const entries = Object.entries(attrs).filter(([, v]) => v !== null && v !== undefined)\n return entries.length > 0 ? (Object.fromEntries(entries) as Record<string, never>) : undefined\n}\n\n/**\n * Whether a document carries no author-visible content — only empty paragraphs or\n * headings (TipTap keeps a mandatory trailing block even when the field is blank).\n * Any text, atom (image/embed/carrier/hardBreak/hr), list, or blockquote makes it\n * non-empty.\n */\nexport function isEffectivelyEmpty(doc: RichTextDoc): boolean {\n return (doc.content ?? []).every(isEmptyBlock)\n}\n\nfunction isEmptyBlock(node: RichTextNode): boolean {\n if (!EMPTY_TOLERANT_BLOCKS.has(node.type)) return false\n return (node.content ?? []).every(\n (child) => child.type === 'text' && (child.text ?? '').length === 0,\n )\n}\n\n/**\n * Whether the (already normalized) editor document is semantically the stored one —\n * so the editor can re-emit the exact stored value instead of a re-serialization,\n * keeping the field un-dirtied. Both sides are compared in editor-projection space\n * ({@link toEditorDoc} is deterministic), so a stored v2 component/carrier and its\n * on-screen atom compare equal. Two empty documents are always equivalent (the\n * trailing-paragraph difference between `content: []` and `[{ paragraph }]` is not\n * a change); otherwise defers to the shared structural comparison.\n */\nexport function isEquivalentToStored(current: RichTextDoc, storedValue: unknown): boolean {\n const stored = toEditorDoc(storedValue)\n if (isEffectivelyEmpty(current) && isEffectivelyEmpty(stored)) return true\n return richTextDocsEquivalent(current, stored)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"value.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/value.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createBlockSequenceValue,\n isBlockSequenceValueShape,\n componentBlockNode,\n richTextDocsEquivalent,\n} from '@elytracms/core/rich-text'\nimport type {\n BlockNode,\n BlockSequenceValue,\n RichTextDoc,\n RichTextNode,\n TypographyNode,\n} from '@elytracms/core/rich-text'\nimport { isComponentBlockNode, isComponentNode } from '@elytracms/core/project-graph'\nimport type { JsonValue } from '@elytracms/core/project-graph'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport { UNKNOWN_NODE_CARRIER_TYPE } from './unknown-node'\n\n/**\n * Storage ⟷ editor projection for the CMS TipTap flow editor (EC-314, converged\n * by EC-547 per the EC-500 design).\n *\n * Storage is the converged block sequence `{ version: 2, content: BlockNode[] }`\n * — typography nodes byte-identical to the historical rich-text shape plus\n * first-class `elytra/component` nodes. TipTap requires ITS grammar (a\n * `{ type: 'doc', content }` document over a CLOSED schema), so this module is\n * the just-in-time projection between the two:\n *\n * - **Way in** ({@link toEditorDoc}): typography passes near-identically; an\n * `elytra/component` node becomes the existing ComponentEmbed atom (the\n * original node riding verbatim in `attrs.node`); any node type the editor\n * does not natively edit is wrapped into the opaque\n * {@link UNKNOWN_NODE_CARRIER_TYPE} atom so the closed ProseMirror schema\n * cannot drop it.\n * - **Way out** ({@link blockSequenceValueFromDoc}): embed atoms convert back to\n * tagged `elytra/component` nodes, carriers restore their carried JSON\n * byte-for-byte, typography is re-emitted with {@link normalizeEditorDoc}'s\n * lean-attrs canonicalisation applied.\n *\n * The round-trip law (EC-500 D3): the projection may only rewrite nodes the flow\n * editor natively edits; every other node survives byte-for-byte.\n *\n * Two editor-ergonomics concerns carry over unchanged from EC-314:\n *\n * 1. **Lean canonicalisation.** ProseMirror stamps every declared attribute onto a\n * node/mark, so TipTap emits `null` defaults the lean storage convention omits\n * (`link` → `ref: null`, `codeBlock` → `language: null`, `image` → `title: null`).\n * {@link normalizeEditorDoc} prunes those so an untouched document round-trips to\n * the same shape the extractor/renderer already read.\n *\n * 2. **No false dirty.** Opening a field must not mark it changed, and typing then\n * undoing back to the original must clear dirty. {@link isEquivalentToStored}\n * compares semantically in editor-projection space so the editor can re-emit the\n * *exact stored value* when nothing really changed.\n */\n\n/**\n * The storage node types the flow editor natively edits — the projection's ONE\n * wrap/pass decision: anything outside this set rides through the editor inside\n * the opaque carrier atom. Everything listed here MUST be declared by\n * `buildRichTextExtensions`' ProseMirror schema (an undeclared type would be\n * silently dropped); `extensions.test.ts` pins the two sets equal, so drift\n * between this declaration and the schema is loud.\n *\n * The ProseMirror schema declares TWO more types than this set: the\n * ComponentEmbed atom and the carrier — both editor-only projection vehicles\n * that are never valid STORAGE vocabulary (EC-548 retired `componentEmbed` at\n * rest). Excluding them here is deliberate and load-bearing: a STORED node\n * reusing either name is a foreigner and rides the carrier byte-for-byte,\n * instead of being misread as an editor atom (which for a stored\n * `componentEmbed` would silently rewrite it to `elytra/component` on save —\n * a compat migration the owner ruled out).\n */\nexport const EDITOR_NATIVE_NODE_TYPES: ReadonlySet<string> = new Set([\n 'doc',\n 'paragraph',\n 'text',\n 'heading',\n 'bulletList',\n 'orderedList',\n 'listItem',\n 'blockquote',\n 'codeBlock',\n 'hardBreak',\n 'horizontalRule',\n 'image',\n])\n\n/** ProseMirror node types that carry no reference/atom payload and render as prose. */\nconst EMPTY_TOLERANT_BLOCKS = new Set(['paragraph', 'heading'])\n\n/**\n * Project whatever a body-capable field currently holds into an editor document.\n *\n * Accepts the canonical v2 `{ version: 2, content }` sequence (object or JSON\n * string) and projects it per the module contract. Anything unparseable becomes\n * an empty document (never throws; validation is first-class elsewhere). The\n * projection is deliberately TOTAL and shape-driven, not schema-validating: a\n * v2 value with one malformed node must still open (the bad node rides in a\n * carrier, visibly labelled) rather than blanking the whole field — the editor\n * is not the validator.\n */\nexport function toEditorDoc(value: unknown): RichTextDoc {\n const sequence = asBlockSequenceShape(value)\n if (sequence) {\n return { type: 'doc', content: sequence.content.map(projectStoredNode) }\n }\n // Anything that is not the v2 sequence (a pre-convergence envelope, a bare\n // doc, legacy plaintext, junk) opens EMPTY — deliberately not projected:\n // validation names those shapes with the explicit \"re-seed\" issue (EC-548\n // deleted the staging acceptance), and quietly editing over them here would\n // hide it.\n return { type: 'doc', content: [] }\n}\n\n/** Structural v2 test, tolerating a JSON-string form of the same value. */\nfunction asBlockSequenceShape(value: unknown): { version: 2; content: unknown[] } | undefined {\n if (isBlockSequenceValueShape(value)) return value\n if (typeof value === 'string') {\n try {\n const parsed: unknown = JSON.parse(value)\n if (isBlockSequenceValueShape(parsed)) return parsed\n } catch {\n /* not JSON — fall through */\n }\n }\n return undefined\n}\n\n/**\n * One stored node → its editor projection. Total over arbitrary JSON:\n *\n * - a tagged component node → the ComponentEmbed atom, the node itself riding\n * verbatim (tag included) in `attrs.node`;\n * - a node type the editor natively edits → passed through, `content` recursed\n * (so a component nested in a blockquote still projects to an embed);\n * - EVERYTHING else — foreign types, a tagged-but-malformed component node,\n * even non-node JSON in the content array — → the opaque carrier atom, the\n * original payload riding verbatim in `attrs.node`.\n */\nfunction projectStoredNode(node: unknown): RichTextNode {\n if (isComponentBlockNode(node)) {\n return { type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: node as unknown as JsonValue } }\n }\n if (isEditorNativeNode(node)) {\n const out: RichTextNode = { ...node }\n if (node.content) out.content = node.content.map(projectStoredNode)\n return out\n }\n return { type: UNKNOWN_NODE_CARRIER_TYPE, attrs: { node: node as JsonValue } }\n}\n\nfunction isEditorNativeNode(node: unknown): node is RichTextNode {\n return (\n typeof node === 'object' &&\n node !== null &&\n !Array.isArray(node) &&\n typeof (node as { type?: unknown }).type === 'string' &&\n EDITOR_NATIVE_NODE_TYPES.has((node as { type: string }).type)\n )\n}\n\n/**\n * Project a (normalized) editor document back into the stored v2 sequence —\n * the way-out half of the module contract: embed atoms → tagged\n * `elytra/component` nodes, carriers → their carried JSON byte-for-byte,\n * typography re-emitted as-is (lean canonicalisation is\n * {@link normalizeEditorDoc}'s job, upstream).\n */\nexport function blockSequenceValueFromDoc(doc: RichTextDoc): BlockSequenceValue {\n return createBlockSequenceValue((doc.content ?? []).map(restoreStoredNode))\n}\n\nfunction restoreStoredNode(node: RichTextNode): BlockNode {\n if (node.type === COMPONENT_EMBED_NODE_TYPE) {\n const embedded: unknown = node.attrs?.node\n if (isComponentNode(embedded)) {\n // Re-tag through the one constructor (identity on already-tagged nodes),\n // so an atom payload the node factory built untagged leaves the editor\n // as a first-class tagged citizen — \"write current\".\n const { type: _tag, ...rest } = embedded\n return componentBlockNode(rest)\n }\n // An embed whose payload is missing/malformed: keep the node verbatim as\n // typography — never a silent drop; validation reports it elsewhere.\n return node as TypographyNode\n }\n if (node.type === UNKNOWN_NODE_CARRIER_TYPE) {\n const carried: unknown = node.attrs?.node\n // Byte-for-byte restore of the carried original (the D3 round-trip law).\n // An empty carrier (nothing to restore — not constructible via the\n // projection) degrades to the carrier node itself staying visible.\n if (carried !== null && carried !== undefined) return carried as BlockNode\n return node as TypographyNode\n }\n const out: TypographyNode = { ...(node as TypographyNode) }\n if (node.content) out.content = node.content.map(restoreStoredNode)\n return out\n}\n\n/**\n * Prune the `null` attribute values ProseMirror stamps from declared-but-unset\n * attributes, and drop the resulting empty `attrs` bags, so the editor's output\n * matches the lean storage convention. Recurses `content`; leaves `marks` attrs\n * pruned the same way. Pure; returns a fresh tree. (Embed and carrier payloads\n * are single non-null `attrs` entries, so pruning passes them through by\n * reference — their bytes are never rewritten here.)\n */\nexport function normalizeEditorDoc(doc: RichTextDoc): RichTextDoc {\n return { type: 'doc', content: (doc.content ?? []).map(pruneNode) }\n}\n\nfunction pruneNode(node: RichTextNode): RichTextNode {\n const out: RichTextNode = { type: node.type }\n const attrs = pruneAttrs(node.attrs)\n if (attrs) out.attrs = attrs\n if (typeof node.text === 'string') out.text = node.text\n if (node.marks && node.marks.length > 0) {\n out.marks = node.marks.map((mark) => {\n const markAttrs = pruneAttrs(mark.attrs)\n return markAttrs ? { type: mark.type, attrs: markAttrs } : { type: mark.type }\n })\n }\n if (node.content) out.content = node.content.map(pruneNode)\n return out\n}\n\nfunction pruneAttrs(\n attrs: Record<string, unknown> | undefined,\n): Record<string, never> | undefined {\n if (!attrs) return undefined\n const entries = Object.entries(attrs).filter(([, v]) => v !== null && v !== undefined)\n return entries.length > 0 ? (Object.fromEntries(entries) as Record<string, never>) : undefined\n}\n\n/**\n * Whether a document carries no author-visible content — only empty paragraphs or\n * headings (TipTap keeps a mandatory trailing block even when the field is blank).\n * Any text, atom (image/embed/carrier/hardBreak/hr), list, or blockquote makes it\n * non-empty.\n */\nexport function isEffectivelyEmpty(doc: RichTextDoc): boolean {\n return (doc.content ?? []).every(isEmptyBlock)\n}\n\nfunction isEmptyBlock(node: RichTextNode): boolean {\n if (!EMPTY_TOLERANT_BLOCKS.has(node.type)) return false\n return (node.content ?? []).every(\n (child) => child.type === 'text' && (child.text ?? '').length === 0,\n )\n}\n\n/**\n * Whether the (already normalized) editor document is semantically the stored one —\n * so the editor can re-emit the exact stored value instead of a re-serialization,\n * keeping the field un-dirtied. Both sides are compared in editor-projection space\n * ({@link toEditorDoc} is deterministic), so a stored v2 component/carrier and its\n * on-screen atom compare equal. Two empty documents are always equivalent (the\n * trailing-paragraph difference between `content: []` and `[{ paragraph }]` is not\n * a change); otherwise defers to the shared structural comparison.\n */\nexport function isEquivalentToStored(current: RichTextDoc, storedValue: unknown): boolean {\n const stored = toEditorDoc(storedValue)\n if (isEffectivelyEmpty(current) && isEffectivelyEmpty(stored)) return true\n return richTextDocsEquivalent(current, stored)\n}\n\n/**\n * EC-679 — the value the editor REPORTS for the (normalized) document on screen (pure;\n * the editor's `onUpdate` reports exactly this). Three outcomes: unchanged → the stored\n * value verbatim (never a re-serialization, so the field is not dirtied), emptied →\n * `undefined` (the field is cleared), otherwise the new stamped value. An empty editor\n * over an ABSENT body therefore reports `undefined` — the value the draft already holds,\n * which the form's writer judges as no edit ({@link fieldEdit} in `use-document-draft.ts`).\n */\nexport function reportedValue(current: RichTextDoc, stored: unknown): unknown {\n if (isEquivalentToStored(current, stored)) return stored\n return isEffectivelyEmpty(current) ? undefined : blockSequenceValueFromDoc(current)\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PresenceEntry } from '@elytracms/core/persistence';
|
|
2
2
|
import type { CmsDocument, CollectionDef, DocumentPatch, FieldDef, Locale, PropFieldLookup, ValuePath } from '@elytracms/core/cms-core';
|
|
3
|
-
import type { CollectionViewSource } from '../../lib/collection-view/collection-view-source.js';
|
|
3
|
+
import type { CollectionViewSource, DocumentWrites } from '../../lib/collection-view/collection-view-source.js';
|
|
4
4
|
import type { CanvasFieldChange, CanvasLift } from '../builder/canvas-rebase.js';
|
|
5
5
|
import type { CommitPolicy } from './commit-policy.js';
|
|
6
6
|
import type { CanvasControl, CanvasControlPosition } from './canvas-commit.js';
|
|
@@ -45,11 +45,44 @@ export declare const documentVisibility: VisibilitySource;
|
|
|
45
45
|
* onto, as ONE value: every edit made on the draft was authored against that revision (an
|
|
46
46
|
* uncommitted position keeps the revision its first keystroke saw). `undefined`: the view
|
|
47
47
|
* had no revision (a fixture session, a record rail, a document still being created).
|
|
48
|
+
*
|
|
49
|
+
* EC-667 — that session view is a COMPOSITE while it shows a pending write (Codex F18):
|
|
50
|
+
* `revision` is its whole-document token (the EARLIEST authoring revision of any position
|
|
51
|
+
* — what a whole-document writer, the slot alias or the translation, writes at), and
|
|
52
|
+
* `shown` is the view itself, the token a position's edit reads ITS provenance from at
|
|
53
|
+
* the first keystroke ({@link positionBase}).
|
|
48
54
|
*/
|
|
49
55
|
export interface DraftView {
|
|
50
56
|
readonly document: CmsDocument;
|
|
51
57
|
readonly revision: number | undefined;
|
|
58
|
+
/** EC-667 — the session view the draft was last rebased onto (absent: none — a fixture, a record rail). */
|
|
59
|
+
readonly shown?: CmsDocument;
|
|
52
60
|
}
|
|
61
|
+
/** What a draft reads its tokens from: the write model's view registry (EC-647 / EC-667). */
|
|
62
|
+
export type ViewTokens = Pick<DocumentWrites, 'revisionOfView'>;
|
|
63
|
+
/** EC-667 — the draft of a session view as it is shown (pure): its value, its whole-document token, the view itself. */
|
|
64
|
+
export declare function sessionDraftView(doc: CmsDocument, tokens: ViewTokens | undefined): DraftView;
|
|
65
|
+
/**
|
|
66
|
+
* EC-667 — the base an edit of `path` that STARTS now on `draft` is authored against
|
|
67
|
+
* (pure; the committer captures it at the first keystroke): the revision the content at
|
|
68
|
+
* that position of the shown session view was authored against — under a pending
|
|
69
|
+
* whole-document snapshot the snapshot's revision, under a pending own write that write's
|
|
70
|
+
* base — never the revision of a row adopted underneath it. Captured at the start, it
|
|
71
|
+
* survives the pending write's refusal or failure (looking it up when the commit arrives
|
|
72
|
+
* would be too late). Without a view token: the draft's whole-document revision.
|
|
73
|
+
*/
|
|
74
|
+
export declare function positionBase(draft: DraftView, path: ValuePath, tokens: ViewTokens | undefined): number | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* THE draft rebase as one value (pure — what the editor runs when the session's document
|
|
77
|
+
* changes): the session view with the uncommitted positions laid over it ({@link
|
|
78
|
+
* rebaseDraft}), its whole-document token, the view itself (EC-647 → EC-667: the rebase
|
|
79
|
+
* moves the value AND its tokens), and — when the screen has a canvas — the token of the
|
|
80
|
+
* canvas field's position, which the canvas's next edit is authored at.
|
|
81
|
+
*/
|
|
82
|
+
export declare function rebaseDraftView(doc: CmsDocument, previous: DraftView, uncommitted: readonly UncommittedPosition[], tokens: ViewTokens, canvasPath?: ValuePath): {
|
|
83
|
+
view: DraftView;
|
|
84
|
+
canvasRevision: number | undefined;
|
|
85
|
+
};
|
|
53
86
|
/**
|
|
54
87
|
* EC-647 — the slot alias write (pure): the draft with `alias` set (or removed), written
|
|
55
88
|
* whole AT THE DRAFT'S REVISION — never the session's tracked one, which may already hold
|
|
@@ -67,6 +100,36 @@ export declare function positionPatch(doc: CmsDocument, path: ValuePath): Docume
|
|
|
67
100
|
* over it, values from the editor's current draft.
|
|
68
101
|
*/
|
|
69
102
|
export declare function rebaseDraft(sessionDoc: CmsDocument, draft: CmsDocument, uncommitted: readonly UncommittedPosition[]): CmsDocument;
|
|
103
|
+
/**
|
|
104
|
+
* THE form writers' core (pure; what {@link DocumentDraft.setFieldValue} and {@link
|
|
105
|
+
* DocumentDraft.editField} run): `update` transforms the draft at ONE field position;
|
|
106
|
+
* the draft takes the result (`apply`) and, with a revisioned write model, the position
|
|
107
|
+
* is the committer's, authored at its token read BEFORE the draft moves (EC-667).
|
|
108
|
+
*
|
|
109
|
+
* **EC-679 — an editor that opens a document never writes.** The position is EDITED only
|
|
110
|
+
* when the value AT THE POSITION differs from what the DRAFT held there before the call,
|
|
111
|
+
* by the write path's own comparison: {@link semanticValueEqual} over the value the
|
|
112
|
+
* update stores (`writeField` canonicalises, so an emptied value that stores as absent
|
|
113
|
+
* equals absent). A control that re-reports what the draft already holds — the rich-text
|
|
114
|
+
* editor on mount, on a remount (a tab or Draft/Published switch) or on a read-only flip,
|
|
115
|
+
* measured on the base as an `unset` of an absent body or a same-value `set` per open —
|
|
116
|
+
* moves nothing and commits nothing (`undefined`). Compared against the draft, never the
|
|
117
|
+
* session's base: typing a value and typing it away again are two edits, and the revert
|
|
118
|
+
* writes too. Every field type is covered here by construction; the canvas writes through
|
|
119
|
+
* {@link canvasCommit}, not this.
|
|
120
|
+
*/
|
|
121
|
+
export declare function fieldEdit(input: {
|
|
122
|
+
collection: CollectionDef;
|
|
123
|
+
draft: DraftView;
|
|
124
|
+
field: string;
|
|
125
|
+
locale: Locale;
|
|
126
|
+
update: (current: CmsDocument) => CmsDocument;
|
|
127
|
+
/** The write model's view tokens (absent: no revisioned write model). */
|
|
128
|
+
tokens: ViewTokens | undefined;
|
|
129
|
+
/** Absent: no revisioned write model (the draft moves, nothing is committed). */
|
|
130
|
+
committer: Pick<DraftCommitter, 'edited'> | undefined;
|
|
131
|
+
apply: (next: CmsDocument) => void;
|
|
132
|
+
}): 'edited' | 'draft' | undefined;
|
|
70
133
|
/**
|
|
71
134
|
* EC-630 — the canvas's write into the draft (pure). The canvas writer's node patches,
|
|
72
135
|
* keyed by node id, are the canvas's OWN delta: from `base`, the value it last agreed on
|
|
@@ -99,6 +162,11 @@ export declare function canvasWrite(input: {
|
|
|
99
162
|
* reported when only THAT moved too (`from` ≡ `to`): the canvas's agreed value is valid
|
|
100
163
|
* at the newer revision, so its next edit is authored there — the revision follows the
|
|
101
164
|
* value, never runs ahead of it.
|
|
165
|
+
*
|
|
166
|
+
* EC-667: `revision` is the token of the canvas FIELD's position in the shown view
|
|
167
|
+
* ({@link rebaseDraftView}) — under a pending snapshot the snapshot's revision. The canvas
|
|
168
|
+
* mounts at the draft's whole-document token (the earliest, possibly earlier than its
|
|
169
|
+
* field's), so the first rebase with a token reports it even when nothing moved.
|
|
102
170
|
*/
|
|
103
171
|
export declare function canvasFieldChange(previous: CmsDocument, next: CmsDocument, path: ValuePath, last: CanvasFieldChange | undefined, revision: number | undefined): CanvasFieldChange | undefined;
|
|
104
172
|
/**
|
|
@@ -140,16 +208,20 @@ export interface DocumentDraft {
|
|
|
140
208
|
/**
|
|
141
209
|
* EC-647 — the revision of the session view the draft was last rebased onto (see
|
|
142
210
|
* {@link DraftView}): a writer that authors from {@link draft} (the translation) writes
|
|
143
|
-
* at this revision.
|
|
211
|
+
* at this revision. EC-667: the view's WHOLE-DOCUMENT token — the earliest authoring
|
|
212
|
+
* revision of any position it shows (a field's own edit reads its position's token).
|
|
144
213
|
*/
|
|
145
214
|
revision: number | undefined;
|
|
146
215
|
/** Uncommitted (document model) / unsaved (record model) edits exist. */
|
|
147
216
|
dirty: boolean;
|
|
148
217
|
/** The record model: the editor renders a Save button + navigation guard. */
|
|
149
218
|
explicitSave: boolean;
|
|
150
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* A field's control wrote `value` (the active locale for a localized field). EC-679: a
|
|
221
|
+
* value the draft already holds at the position is no edit ({@link fieldEdit}).
|
|
222
|
+
*/
|
|
151
223
|
setFieldValue(field: FieldDef, value: unknown): void;
|
|
152
|
-
/** A writer that transforms ONE field position (fill / reset from base). */
|
|
224
|
+
/** A writer that transforms ONE field position (fill / reset from base) — the same no-op rule. */
|
|
153
225
|
editField(field: FieldDef, update: (draft: CmsDocument) => CmsDocument): void;
|
|
154
226
|
/**
|
|
155
227
|
* The canvas emitted a new value for its field (node patches, committed at once). The
|
|
@@ -47,6 +47,35 @@ export const documentVisibility = {
|
|
|
47
47
|
return () => document.removeEventListener('visibilitychange', listener);
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
|
+
/** EC-667 — the draft of a session view as it is shown (pure): its value, its whole-document token, the view itself. */
|
|
51
|
+
export function sessionDraftView(doc, tokens) {
|
|
52
|
+
return { document: doc, revision: tokens?.revisionOfView(doc), shown: doc };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* EC-667 — the base an edit of `path` that STARTS now on `draft` is authored against
|
|
56
|
+
* (pure; the committer captures it at the first keystroke): the revision the content at
|
|
57
|
+
* that position of the shown session view was authored against — under a pending
|
|
58
|
+
* whole-document snapshot the snapshot's revision, under a pending own write that write's
|
|
59
|
+
* base — never the revision of a row adopted underneath it. Captured at the start, it
|
|
60
|
+
* survives the pending write's refusal or failure (looking it up when the commit arrives
|
|
61
|
+
* would be too late). Without a view token: the draft's whole-document revision.
|
|
62
|
+
*/
|
|
63
|
+
export function positionBase(draft, path, tokens) {
|
|
64
|
+
return tokens && draft.shown ? tokens.revisionOfView(draft.shown, path) : draft.revision;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* THE draft rebase as one value (pure — what the editor runs when the session's document
|
|
68
|
+
* changes): the session view with the uncommitted positions laid over it ({@link
|
|
69
|
+
* rebaseDraft}), its whole-document token, the view itself (EC-647 → EC-667: the rebase
|
|
70
|
+
* moves the value AND its tokens), and — when the screen has a canvas — the token of the
|
|
71
|
+
* canvas field's position, which the canvas's next edit is authored at.
|
|
72
|
+
*/
|
|
73
|
+
export function rebaseDraftView(doc, previous, uncommitted, tokens, canvasPath) {
|
|
74
|
+
return {
|
|
75
|
+
view: { document: rebaseDraft(doc, previous.document, uncommitted), revision: tokens.revisionOfView(doc), shown: doc },
|
|
76
|
+
canvasRevision: canvasPath ? tokens.revisionOfView(doc, canvasPath) : undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
50
79
|
/**
|
|
51
80
|
* EC-647 — the slot alias write (pure): the draft with `alias` set (or removed), written
|
|
52
81
|
* whole AT THE DRAFT'S REVISION — never the session's tracked one, which may already hold
|
|
@@ -78,6 +107,37 @@ export function rebaseDraft(sessionDoc, draft, uncommitted) {
|
|
|
78
107
|
const patches = uncommitted.map((position) => positionPatch(draft, position.path));
|
|
79
108
|
return applyDocumentPatches({ document: sessionDoc, patches, since: [] }).document;
|
|
80
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* THE form writers' core (pure; what {@link DocumentDraft.setFieldValue} and {@link
|
|
112
|
+
* DocumentDraft.editField} run): `update` transforms the draft at ONE field position;
|
|
113
|
+
* the draft takes the result (`apply`) and, with a revisioned write model, the position
|
|
114
|
+
* is the committer's, authored at its token read BEFORE the draft moves (EC-667).
|
|
115
|
+
*
|
|
116
|
+
* **EC-679 — an editor that opens a document never writes.** The position is EDITED only
|
|
117
|
+
* when the value AT THE POSITION differs from what the DRAFT held there before the call,
|
|
118
|
+
* by the write path's own comparison: {@link semanticValueEqual} over the value the
|
|
119
|
+
* update stores (`writeField` canonicalises, so an emptied value that stores as absent
|
|
120
|
+
* equals absent). A control that re-reports what the draft already holds — the rich-text
|
|
121
|
+
* editor on mount, on a remount (a tab or Draft/Published switch) or on a read-only flip,
|
|
122
|
+
* measured on the base as an `unset` of an absent body or a same-value `set` per open —
|
|
123
|
+
* moves nothing and commits nothing (`undefined`). Compared against the draft, never the
|
|
124
|
+
* session's base: typing a value and typing it away again are two edits, and the revert
|
|
125
|
+
* writes too. Every field type is covered here by construction; the canvas writes through
|
|
126
|
+
* {@link canvasCommit}, not this.
|
|
127
|
+
*/
|
|
128
|
+
export function fieldEdit(input) {
|
|
129
|
+
const { draft, field, committer } = input;
|
|
130
|
+
const path = fieldPositionPath(input.collection, field, input.locale);
|
|
131
|
+
const authored = { base: positionBase(draft, path, input.tokens) };
|
|
132
|
+
const next = input.update(draft.document);
|
|
133
|
+
if (semanticValueEqual(readValueAt(draft.document, path), readValueAt(next, path)))
|
|
134
|
+
return undefined;
|
|
135
|
+
input.apply(next);
|
|
136
|
+
if (!committer)
|
|
137
|
+
return 'draft';
|
|
138
|
+
committer.edited(field, path, authored);
|
|
139
|
+
return 'edited';
|
|
140
|
+
}
|
|
81
141
|
/**
|
|
82
142
|
* EC-630 — the canvas's write into the draft (pure). The canvas writer's node patches,
|
|
83
143
|
* keyed by node id, are the canvas's OWN delta: from `base`, the value it last agreed on
|
|
@@ -108,11 +168,17 @@ export function canvasWrite(input) {
|
|
|
108
168
|
* reported when only THAT moved too (`from` ≡ `to`): the canvas's agreed value is valid
|
|
109
169
|
* at the newer revision, so its next edit is authored there — the revision follows the
|
|
110
170
|
* value, never runs ahead of it.
|
|
171
|
+
*
|
|
172
|
+
* EC-667: `revision` is the token of the canvas FIELD's position in the shown view
|
|
173
|
+
* ({@link rebaseDraftView}) — under a pending snapshot the snapshot's revision. The canvas
|
|
174
|
+
* mounts at the draft's whole-document token (the earliest, possibly earlier than its
|
|
175
|
+
* field's), so the first rebase with a token reports it even when nothing moved.
|
|
111
176
|
*/
|
|
112
177
|
export function canvasFieldChange(previous, next, path, last, revision) {
|
|
113
178
|
const from = readValueAt(previous, path);
|
|
114
179
|
const to = readValueAt(next, path);
|
|
115
|
-
|
|
180
|
+
const tokenHeld = last === undefined ? revision === undefined : last.revision === revision;
|
|
181
|
+
if (semanticValueEqual(from, to) && tokenHeld)
|
|
116
182
|
return undefined;
|
|
117
183
|
return { from, to, seq: (last?.seq ?? 0) + 1, revision };
|
|
118
184
|
}
|
|
@@ -178,11 +244,9 @@ export function useDocumentDraft(input) {
|
|
|
178
244
|
const writes = source.writes;
|
|
179
245
|
const documentModel = writes.kind === 'document' ? writes : undefined;
|
|
180
246
|
const ref = useMemo(() => ({ collection: doc.collection, id: doc.id }), [doc.collection, doc.id]);
|
|
181
|
-
// EC-647: the draft and the revision of the view it rebased onto travel as ONE value
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
revision: documentModel?.revisionOfView(doc),
|
|
185
|
-
}));
|
|
247
|
+
// EC-647: the draft and the revision of the view it rebased onto travel as ONE value
|
|
248
|
+
// (EC-667: with the view itself — the per-position token).
|
|
249
|
+
const [view, setViewState] = useState(() => sessionDraftView(doc, documentModel));
|
|
186
250
|
const draft = view.document;
|
|
187
251
|
const draftRef = useRef(view);
|
|
188
252
|
// EC-616: the record as the editor opened it (or as of their last successful save) — the
|
|
@@ -195,8 +259,8 @@ export function useDocumentDraft(input) {
|
|
|
195
259
|
draftRef.current = next;
|
|
196
260
|
setViewState(next);
|
|
197
261
|
}, []);
|
|
198
|
-
/** An edit of the draft: its value moves, the
|
|
199
|
-
const setDraft = useCallback((next) => setView({ document: next
|
|
262
|
+
/** An edit of the draft: its value moves, the tokens it is authored against do not. */
|
|
263
|
+
const setDraft = useCallback((next) => setView({ ...draftRef.current, document: next }), [setView]);
|
|
200
264
|
const scheduler = input.scheduler ?? browserScheduler;
|
|
201
265
|
const now = input.now ?? (() => new Date().toISOString());
|
|
202
266
|
const fieldsByName = useMemo(() => new Map(collection.fields.map((f) => [f.name, f])), [collection]);
|
|
@@ -220,7 +284,9 @@ export function useDocumentDraft(input) {
|
|
|
220
284
|
},
|
|
221
285
|
// EC-647: a position's base is the revision of the DRAFT its first keystroke was
|
|
222
286
|
// made on — never the session's tracked revision, which may already hold a
|
|
223
|
-
// colleague's change the draft has not rebased onto yet.
|
|
287
|
+
// colleague's change the draft has not rebased onto yet. EC-667: the form's
|
|
288
|
+
// writers name the POSITION's token ({@link positionBase}, passed to `edited`);
|
|
289
|
+
// this whole-document token (the earliest) is the fallback.
|
|
224
290
|
baseOf: () => draftRef.current.revision,
|
|
225
291
|
scheduler,
|
|
226
292
|
}),
|
|
@@ -249,30 +315,33 @@ export function useDocumentDraft(input) {
|
|
|
249
315
|
if (!documentModel)
|
|
250
316
|
return;
|
|
251
317
|
const previous = draftRef.current.document;
|
|
252
|
-
|
|
253
|
-
// EC-
|
|
254
|
-
const
|
|
255
|
-
setView(
|
|
318
|
+
// EC-647: the rebase moves the value AND the revision — the session view's own
|
|
319
|
+
// (EC-667: its tokens, whole-document and per position).
|
|
320
|
+
const { view: next, canvasRevision } = rebaseDraftView(doc, draftRef.current, committer.uncommitted(), documentModel, canvasPath);
|
|
321
|
+
setView(next);
|
|
256
322
|
// EC-630: the canvas field moved underneath the canvas — the canvas rebases onto the
|
|
257
323
|
// change (its own edits are in both values: the session's view holds every patch it
|
|
258
324
|
// committed, and the draft overlays every position it has not). EC-647: the change
|
|
259
|
-
// carries the revision
|
|
325
|
+
// carries the revision — EC-667: the canvas FIELD's token — so the canvas's next edit
|
|
326
|
+
// is authored where its value is.
|
|
260
327
|
if (!canvasPath)
|
|
261
328
|
return;
|
|
262
|
-
setCanvasChange((last) => canvasFieldChange(previous, next, canvasPath, last,
|
|
329
|
+
setCanvasChange((last) => canvasFieldChange(previous, next.document, canvasPath, last, canvasRevision) ?? last);
|
|
263
330
|
}, [doc]);
|
|
264
331
|
// --- Writers ----------------------------------------------------------------------------
|
|
265
|
-
const setFieldValue = useCallback((field, value) => {
|
|
266
|
-
const next = writeField(collection, draftRef.current.document, field.name, locale, value, { lookupManifest });
|
|
267
|
-
setDraft(next);
|
|
268
|
-
if (documentModel)
|
|
269
|
-
committer.edited(field.name, fieldPositionPath(collection, field.name, locale));
|
|
270
|
-
}, [collection, locale, lookupManifest, setDraft, documentModel, committer]);
|
|
271
332
|
const editField = useCallback((field, update) => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
333
|
+
fieldEdit({
|
|
334
|
+
collection,
|
|
335
|
+
draft: draftRef.current,
|
|
336
|
+
field: field.name,
|
|
337
|
+
locale,
|
|
338
|
+
update,
|
|
339
|
+
tokens: documentModel,
|
|
340
|
+
committer: documentModel ? committer : undefined,
|
|
341
|
+
apply: setDraft,
|
|
342
|
+
});
|
|
275
343
|
}, [collection, locale, setDraft, documentModel, committer]);
|
|
344
|
+
const setFieldValue = useCallback((field, value) => editField(field, (current) => writeField(collection, current, field.name, locale, value, { lookupManifest })), [collection, locale, lookupManifest, editField]);
|
|
276
345
|
const setCanvasValue = useCallback((field, lift) => {
|
|
277
346
|
const focus = committer.focusedField();
|
|
278
347
|
canvasCommit({
|
|
@@ -407,7 +476,7 @@ export function useDocumentDraft(input) {
|
|
|
407
476
|
// canvas remounts from it (its history belonged to the replaced value).
|
|
408
477
|
const moved = canvasPath && !semanticValueEqual(readValueAt(draftRef.current.document, canvasPath), readValueAt(next, canvasPath));
|
|
409
478
|
// EC-647: the restored value comes with the revision the session showed it at.
|
|
410
|
-
setView(
|
|
479
|
+
setView(sessionDraftView(next, documentModel));
|
|
411
480
|
if (moved)
|
|
412
481
|
setCanvasNonce((n) => n + 1);
|
|
413
482
|
},
|