@elytracms/studio 0.0.97 → 0.0.99

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.
Files changed (107) hide show
  1. package/convex/assets.ts +16 -2
  2. package/convex/ingest-attempt.test.ts +161 -0
  3. package/convex/ingest.test.ts +269 -16
  4. package/convex/ingest.ts +238 -47
  5. package/convex/path-segment-refusal.test.ts +201 -0
  6. package/convex/prototype-key.test.ts +92 -0
  7. package/convex/records-column-transform.convex.test.ts +147 -0
  8. package/convex/records-zod-refinements.convex.test.ts +94 -0
  9. package/convex/schema.ts +1 -1
  10. package/dist/addon.d.ts +5 -3
  11. package/dist/addon.js +4 -3
  12. package/dist/addon.js.map +1 -1
  13. package/dist/components/app-views/default-modules.d.ts +10 -3
  14. package/dist/components/app-views/default-modules.js +5 -1
  15. package/dist/components/app-views/default-modules.js.map +1 -1
  16. package/dist/components/app-views/module.d.ts +15 -8
  17. package/dist/components/app-views/module.js.map +1 -1
  18. package/dist/components/builder/reuse-context.d.ts +29 -1
  19. package/dist/components/builder/reuse-context.js +58 -12
  20. package/dist/components/builder/reuse-context.js.map +1 -1
  21. package/dist/components/cms/cms-store.d.ts +5 -6
  22. package/dist/components/cms/cms-store.js +12 -24
  23. package/dist/components/cms/cms-store.js.map +1 -1
  24. package/dist/components/cms/field-control.d.ts +12 -1
  25. package/dist/components/cms/field-control.js +31 -6
  26. package/dist/components/cms/field-control.js.map +1 -1
  27. package/dist/components/cms/import-batch.d.ts +23 -0
  28. package/dist/components/cms/import-batch.js +43 -0
  29. package/dist/components/cms/import-batch.js.map +1 -0
  30. package/dist/components/cms/rich-text/editor.js +13 -10
  31. package/dist/components/cms/rich-text/editor.js.map +1 -1
  32. package/dist/components/cms/rich-text/value.d.ts +9 -0
  33. package/dist/components/cms/rich-text/value.js +13 -0
  34. package/dist/components/cms/rich-text/value.js.map +1 -1
  35. package/dist/components/cms/use-document-draft.d.ts +39 -3
  36. package/dist/components/cms/use-document-draft.js +46 -16
  37. package/dist/components/cms/use-document-draft.js.map +1 -1
  38. package/dist/components/cms/use-publish-controller.d.ts +7 -0
  39. package/dist/components/cms/use-publish-controller.js +17 -3
  40. package/dist/components/cms/use-publish-controller.js.map +1 -1
  41. package/dist/components/cms/versions-pane.js +8 -5
  42. package/dist/components/cms/versions-pane.js.map +1 -1
  43. package/dist/components/delivery-health-banner.d.ts +34 -16
  44. package/dist/components/delivery-health-banner.js +59 -27
  45. package/dist/components/delivery-health-banner.js.map +1 -1
  46. package/dist/components/issue-severity.d.ts +3 -2
  47. package/dist/components/issue-severity.js.map +1 -1
  48. package/dist/components/nav-issues-registry.d.ts +39 -24
  49. package/dist/components/nav-issues-registry.js +50 -21
  50. package/dist/components/nav-issues-registry.js.map +1 -1
  51. package/dist/components/nav-issues.d.ts +23 -50
  52. package/dist/components/nav-issues.js +31 -59
  53. package/dist/components/nav-issues.js.map +1 -1
  54. package/dist/components/notifications/notice-center.d.ts +61 -0
  55. package/dist/components/notifications/notice-center.js +119 -0
  56. package/dist/components/notifications/notice-center.js.map +1 -0
  57. package/dist/components/notifications/notice-model.d.ts +137 -0
  58. package/dist/components/notifications/notice-model.js +107 -0
  59. package/dist/components/notifications/notice-model.js.map +1 -0
  60. package/dist/components/notifications/outcome-notice.d.ts +16 -0
  61. package/dist/components/notifications/outcome-notice.js +34 -0
  62. package/dist/components/notifications/outcome-notice.js.map +1 -0
  63. package/dist/components/notifications/shell-findings.d.ts +36 -0
  64. package/dist/components/notifications/shell-findings.js +140 -0
  65. package/dist/components/notifications/shell-findings.js.map +1 -0
  66. package/dist/components/rail-view-source.js +5 -0
  67. package/dist/components/rail-view-source.js.map +1 -1
  68. package/dist/components/records/record-lens.js.map +1 -1
  69. package/dist/components/records/record-rail.d.ts +10 -2
  70. package/dist/components/records/record-rail.js +74 -4
  71. package/dist/components/records/record-rail.js.map +1 -1
  72. package/dist/components/section-nav.d.ts +15 -3
  73. package/dist/components/section-nav.js +39 -147
  74. package/dist/components/section-nav.js.map +1 -1
  75. package/dist/components/studio-project-screen.js.map +1 -1
  76. package/dist/components/studio-shell.js +8 -3
  77. package/dist/components/studio-shell.js.map +1 -1
  78. package/dist/components/use-narrow-viewport.d.ts +11 -0
  79. package/dist/components/use-narrow-viewport.js +25 -0
  80. package/dist/components/use-narrow-viewport.js.map +1 -0
  81. package/dist/components/users/role-policy-drift-issues.d.ts +14 -7
  82. package/dist/components/users/role-policy-drift-issues.js +17 -8
  83. package/dist/components/users/role-policy-drift-issues.js.map +1 -1
  84. package/dist/lib/collection-view/collection-view-source.d.ts +4 -2
  85. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  86. package/dist/lib/engine-version.d.ts +1 -1
  87. package/dist/lib/engine-version.js +1 -1
  88. package/dist/lib/engine-version.js.map +1 -1
  89. package/dist/lib/i18n/messages.de.js +24 -3
  90. package/dist/lib/i18n/messages.de.js.map +1 -1
  91. package/dist/lib/i18n/messages.en.d.ts +22 -2
  92. package/dist/lib/i18n/messages.en.js +27 -4
  93. package/dist/lib/i18n/messages.en.js.map +1 -1
  94. package/dist/lib/notices/toast-store.d.ts +61 -0
  95. package/dist/lib/notices/toast-store.js +116 -0
  96. package/dist/lib/notices/toast-store.js.map +1 -0
  97. package/dist/lib/operations/cms-persistence.d.ts +32 -7
  98. package/dist/lib/operations/cms-persistence.js +511 -71
  99. package/dist/lib/operations/cms-persistence.js.map +1 -1
  100. package/dist/lib/persistence/convex-adapter.js +61 -30
  101. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  102. package/dist/lib/persona/labels.js +1 -0
  103. package/dist/lib/persona/labels.js.map +1 -1
  104. package/dist/ui/toast.d.ts +24 -16
  105. package/dist/ui/toast.js +23 -32
  106. package/dist/ui/toast.js.map +1 -1
  107. package/package.json +4 -3
@@ -5,6 +5,7 @@ import { readDocumentRef } from '@elytracms/core/cms-core';
5
5
  import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Input, Stack, toast, } from '@elytracms/studio/ui';
6
6
  import { getStudioRegistry } from '../../lib/registry/index.js';
7
7
  import { getStudioOperations } from '../../lib/operations/index.js';
8
+ import { PublishRefusedError } from '../../lib/operations/publish-with-revision.js';
8
9
  import { resolveDeploymentReuse } from '../../lib/workspace/index.js';
9
10
  import { localizedLabel, translate } from '../../lib/i18n/index.js';
10
11
  import { useCms } from '../cms/cms-store.js';
@@ -12,6 +13,48 @@ import { useProjectSession } from '../project-session.js';
12
13
  import { useBuilder } from './builder-store.js';
13
14
  import { detachRefusal, resolveReuseCapability, runPromote } from './reuse.js';
14
15
  import { placementRefusalMessage } from './placement-refusal.js';
16
+ /**
17
+ * EC-690 — the publish step of "Make reusable" and its failure notice, for ONE promote. The
18
+ * store's `publishDocument` reports a refusal on the session (`persistError` — an error notice of
19
+ * the issues tray, with the reason) AND rejects with `PublishRefusedError` (EC-585). The refusal
20
+ * is remembered here, so {@link promotePublishFailureNotice} does not report it a second time;
21
+ * any other throw propagates (`runPromote` reads it as "not published").
22
+ */
23
+ export function promotePublishEffects(deps) {
24
+ let refusalHeld = false;
25
+ return {
26
+ publish: async (ref) => {
27
+ try {
28
+ await deps.publishDocument(ref);
29
+ }
30
+ catch (error) {
31
+ if (error instanceof PublishRefusedError) {
32
+ refusalHeld = true;
33
+ return false;
34
+ }
35
+ throw error;
36
+ }
37
+ return deps.isPublished(ref);
38
+ },
39
+ publishFailed: (name) => {
40
+ const notice = promotePublishFailureNotice(name, refusalHeld);
41
+ deps.notify[notice.variant](notice.message);
42
+ },
43
+ };
44
+ }
45
+ /**
46
+ * EC-690 — the notice "Make reusable" raises when its publish did not land. A publish REFUSAL is
47
+ * already held by the session (`persistError` — an error notice of the issues tray that stays
48
+ * until dismissed, with the refusal's reason), so the outcome here is NOT a second report of the
49
+ * same failure: it is what the action did make — a reusable DRAFT, to publish from its document.
50
+ * A failure nobody reported (the publish threw elsewhere, or resolved without a published
51
+ * version) is the error notice, as before.
52
+ */
53
+ export function promotePublishFailureNotice(name, refusalHeld) {
54
+ return refusalHeld
55
+ ? { variant: 'success', message: translate('builder.reuse.madeReusableDraft', { name }) }
56
+ : { variant: 'error', message: translate('builder.reuse.madeReusablePublishFailed', { name }) };
57
+ }
15
58
  const ReuseContext = createContext(null);
16
59
  export function useReuse() {
17
60
  return useContext(ReuseContext);
@@ -89,21 +132,24 @@ export function ReuseProvider({ children }) {
89
132
  newDocumentId: () => cms.newDocumentId(),
90
133
  insertDocument: (doc) => cms.insertDocument(doc),
91
134
  replaceInCanvas: (nodeId, embed) => builder.dispatch({ type: 'replace', id: nodeId, nodes: [embed] }),
92
- // The store's publish reports a refusal through its error state, not the
93
- // promise — so success is the AUTHORITATIVE pointer after it settles (the
94
- // same read the store reconciles its badge from), never the resolution.
95
- publish: async (ref) => {
96
- await cms.publishDocument(ref);
97
- if (projectId === undefined)
98
- return false;
99
- const { operations } = await getStudioOperations();
100
- const record = await operations.documents.get({ projectId, ref });
101
- return record.ok && record.value.publishedVersion !== undefined;
102
- },
103
135
  refused: toastRefusal,
104
136
  madeDraft: (label) => toast.success(translate('builder.reuse.madeReusableDraft', { name: label })),
105
137
  madePublished: (label) => toast.success(translate('builder.reuse.madeReusablePublished', { name: label })),
106
- publishFailed: (label) => toast.error(translate('builder.reuse.madeReusablePublishFailed', { name: label })),
138
+ // The publish and its failure notice (EC-690: a refusal the session already holds is
139
+ // never reported twice) — see `promotePublishEffects`.
140
+ ...promotePublishEffects({
141
+ publishDocument: (ref) => cms.publishDocument(ref),
142
+ // Success is the AUTHORITATIVE pointer after the publish settles (the same read the
143
+ // store reconciles its badge from), never the resolution.
144
+ isPublished: async (ref) => {
145
+ if (projectId === undefined)
146
+ return false;
147
+ const { operations } = await getStudioOperations();
148
+ const record = await operations.documents.get({ projectId, ref });
149
+ return record.ok && record.value.publishedVersion !== undefined;
150
+ },
151
+ notify: { success: (message) => toast.success(message), error: (message) => toast.error(message) },
152
+ }),
107
153
  },
108
154
  });
109
155
  }, [capability, pending, builder, cms, projectId]);
@@ -1 +1 @@
1
- {"version":3,"file":"reuse-context.js","sourceRoot":"","sources":["../../../src/components/builder/reuse-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { blockRootsOf, getNode, isStaticProp } from '@elytracms/core/project-graph'\nimport { readDocumentRef } from '@elytracms/core/cms-core'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Input,\n Stack,\n toast,\n} from '@elytracms/studio/ui'\nimport { getStudioRegistry } from '../../lib/registry'\nimport { getStudioOperations } from '../../lib/operations'\nimport { resolveDeploymentReuse } from '../../lib/workspace'\nimport { localizedLabel, translate } from '../../lib/i18n'\nimport { useCms } from '../cms/cms-store'\nimport { useProjectSession } from '../project-session'\nimport { useBuilder } from './builder-store'\nimport { detachRefusal, resolveReuseCapability, runPromote } from './reuse'\nimport type { ReuseCapability, ReuseRefusal } from './reuse'\nimport { placementRefusalMessage } from './placement-refusal'\n\n/**\n * In-canvas reuse gestures (EC-282): \"Make reusable\" promotes a selected block to\n * a new reusable document and replaces it in place with a transcluding embed;\n * \"Detach to local copy\" inlines an embed's referenced blocks so a page can\n * diverge. Both round-trip through the existing persistence + in-Convex reference\n * index (EC-277), so where-used is correct immediately.\n *\n * The provider needs BOTH the builder store (the edited composition) and the CMS\n * store (to create/read the reusable document), so it mounts inside the\n * composition-field editor's `BuilderProvider`, itself inside the CMS document\n * editor. Where no provider is mounted (or the capability is absent), the menu\n * reads `null` and simply omits the gestures.\n */\nexport interface ReuseGestures {\n capability: ReuseCapability | null\n canMakeReusable: (nodeId: string) => boolean\n canDetach: (nodeId: string) => boolean\n /** Open the name dialog for promoting `nodeId`. */\n startMakeReusable: (nodeId: string) => void\n /** Replace a `Reusable` embed with an editable inline copy of its target's blocks. */\n detach: (nodeId: string) => void\n}\n\nconst ReuseContext = createContext<ReuseGestures | null>(null)\n\nexport function useReuse(): ReuseGestures | null {\n return useContext(ReuseContext)\n}\n\n/** A refused reuse gesture's visible reason (EC-584) — the judge's verdict, worded once. */\nfunction toastRefusal(refusal: ReuseRefusal): void {\n toast.error(placementRefusalMessage(refusal.verdict, refusal.fieldLabel))\n}\n\nexport function ReuseProvider({ children }: { children: ReactNode }) {\n const builder = useBuilder()\n const cms = useCms()\n const { projectId } = useProjectSession()\n const [pending, setPending] = useState<{ nodeId: string; defaultName: string } | null>(null)\n\n const manifests = useMemo(() => getStudioRegistry().registry.list(), [])\n // EC-467: the optional `reuse` declaration wins; absent, the heuristic runs\n // exactly as before. The config holder is static per studio boot, so it is read\n // inside the memo (like `useStudioNav` reads the sidebar holder). Resolution\n // ISSUES are surfaced once, by the shell rail — this provider is mounted per\n // composition field, so reporting here would repeat them per field.\n const capability = useMemo(\n () => resolveReuseCapability(manifests, cms.collections, resolveDeploymentReuse()),\n [manifests, cms.collections],\n )\n\n const canMakeReusable = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n const node = getNode(builder.present, nodeId)\n // Anything but the synthetic composition root can be promoted; nesting a\n // reusable inside a reusable is allowed (the renderer + bake are cycle-\n // guarded, EC-281), so an embed itself is still promotable.\n return node !== undefined && node.id !== builder.present.id\n },\n [capability, builder],\n )\n\n const canDetach = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n return getNode(builder.present, nodeId)?.componentId === capability.embedComponentId\n },\n [capability, builder],\n )\n\n const startMakeReusable = useCallback(\n (nodeId: string) => {\n if (!canMakeReusable(nodeId)) return\n const node = getNode(builder.present, nodeId)\n if (!node) return\n const manifestTitle = localizedLabel(manifests.find((m) => m.id === node.componentId)?.title) || undefined\n const collection = capability ? cms.getCollection(capability.collectionId) : undefined\n const defaultName =\n manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || translate('builder.reuse.defaultElementName'))\n setPending({ nodeId, defaultName })\n },\n [canMakeReusable, builder, manifests, capability, cms],\n )\n\n const confirmMakeReusable = useCallback(\n (name: string, publish: boolean) => {\n const cap = capability\n if (!cap || !pending) return\n const node = getNode(builder.present, pending.nodeId)\n const collection = cms.getCollection(cap.collectionId)\n setPending(null)\n if (!node || !collection) return\n // EC-584 / EC-560: the placement preflight runs FIRST — a refused promote creates,\n // publishes and replaces nothing. Then (EC-595 A2 / EC-578) ONE insert-if-absent\n // create with a factory id, the in-place swap for the transcluding embed (the\n // action clones to a fresh id; the source `node` stays untouched in the new doc),\n // and the chosen publish. EC-300: PUBLISH keeps the section visible everywhere\n // immediately (the continuity-preserving default); SAVE DRAFT keeps it editable,\n // surfaced as a pending reference until published. No silent auto-publish (AD-16).\n void runPromote({\n root: builder.present,\n nodeId: pending.nodeId,\n node,\n name,\n publish,\n capability: cap,\n collection,\n existing: cms.documentsOf(cap.collectionId),\n locales: cms.localeConfig,\n placement: builder.placement,\n canvasFieldLabel: builder.field.label,\n effects: {\n newDocumentId: () => cms.newDocumentId(),\n insertDocument: (doc) => cms.insertDocument(doc),\n replaceInCanvas: (nodeId, embed) => builder.dispatch({ type: 'replace', id: nodeId, nodes: [embed] }),\n // The store's publish reports a refusal through its error state, not the\n // promise — so success is the AUTHORITATIVE pointer after it settles (the\n // same read the store reconciles its badge from), never the resolution.\n publish: async (ref) => {\n await cms.publishDocument(ref)\n if (projectId === undefined) return false\n const { operations } = await getStudioOperations()\n const record = await operations.documents.get({ projectId, ref })\n return record.ok && record.value.publishedVersion !== undefined\n },\n refused: toastRefusal,\n madeDraft: (label) => toast.success(translate('builder.reuse.madeReusableDraft', { name: label })),\n madePublished: (label) => toast.success(translate('builder.reuse.madeReusablePublished', { name: label })),\n publishFailed: (label) => toast.error(translate('builder.reuse.madeReusablePublishFailed', { name: label })),\n },\n })\n },\n [capability, pending, builder, cms, projectId],\n )\n\n const detach = useCallback(\n (nodeId: string) => {\n const cap = capability\n if (!cap) return\n const node = getNode(builder.present, nodeId)\n if (!node || node.componentId !== cap.embedComponentId) return\n const prop = node.props?.[cap.relationProp]\n const ref = isStaticProp(prop) ? readDocumentRef(prop.value) : undefined\n if (!ref) {\n toast.error(translate('builder.reuse.noTargetToDetach'))\n return\n }\n const target = cms.getDocument(ref)\n const blocks = blockRootsOf(target?.values?.[cap.contentField])\n if (blocks.length === 0) {\n toast.error(translate('builder.reuse.noContentToDetach'))\n return\n }\n // EC-584 / EC-560: the inlined blocks land where the embed is — judged first, so\n // a refusal keeps the reference in place, says why, and dispatches nothing.\n const refusal = detachRefusal({\n root: builder.present,\n nodeId,\n blocks,\n placement: builder.placement,\n canvasFieldLabel: builder.field.label,\n })\n if (refusal) {\n toastRefusal(refusal)\n return\n }\n builder.dispatch({ type: 'replace', id: nodeId, nodes: blocks })\n toast.success(translate('builder.reuse.detachedToCopy'))\n },\n [capability, builder, cms],\n )\n\n const value = useMemo<ReuseGestures>(\n () => ({ capability, canMakeReusable, canDetach, startMakeReusable, detach }),\n [capability, canMakeReusable, canDetach, startMakeReusable, detach],\n )\n\n return (\n <ReuseContext.Provider value={value}>\n {children}\n <MakeReusableDialog\n open={pending !== null}\n defaultName={pending?.defaultName ?? ''}\n onConfirm={confirmMakeReusable}\n onCancel={() => setPending(null)}\n />\n </ReuseContext.Provider>\n )\n}\n\n/** Name-and-create dialog for the \"Make reusable\" gesture. */\nfunction MakeReusableDialog({\n open,\n defaultName,\n onConfirm,\n onCancel,\n}: {\n open: boolean\n defaultName: string\n /** `publish` chooses the new reusable's initial state (EC-300/AD-16). */\n onConfirm: (name: string, publish: boolean) => void\n onCancel: () => void\n}) {\n const [name, setName] = useState(defaultName)\n // Re-seed the field each time the dialog opens for a new selection.\n const [seededFor, setSeededFor] = useState<string | null>(null)\n if (open && seededFor !== defaultName) {\n setName(defaultName)\n setSeededFor(defaultName)\n }\n\n const empty = name.trim() === ''\n function commit(publish: boolean) {\n if (empty) return\n onConfirm(name.trim(), publish)\n setSeededFor(null)\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) {\n onCancel()\n setSeededFor(null)\n }\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{translate('builder.reuse.makeReusable')}</DialogTitle>\n <DialogDescription>\n {translate('builder.reuse.makeReusableDescription')}\n </DialogDescription>\n </DialogHeader>\n {/* Enter submits the primary action (Publish) — the continuity-preserving default. */}\n <form\n onSubmit={(event: FormEvent) => {\n event.preventDefault()\n commit(true)\n }}\n >\n <Stack gap=\"3\">\n <Field label={translate('builder.reuse.nameLabel')} htmlFor=\"make-reusable-name\" required>\n <Input\n id=\"make-reusable-name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder={translate('builder.reuse.namePlaceholder')}\n autoFocus\n required\n />\n </Field>\n </Stack>\n <DialogFooter className=\"mt-4\">\n {/* Cancel pinned left; the two commit actions group right. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"mr-auto\" onClick={onCancel}>\n {translate('common.cancel')}\n </Button>\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={empty}\n onClick={() => commit(false)}\n data-testid=\"make-reusable-draft\"\n >\n {translate('builder.reuse.saveDraft')}\n </Button>\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={empty}\n data-testid=\"make-reusable-publish\"\n >\n {translate('builder.reuse.publish')}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
1
+ {"version":3,"file":"reuse-context.js","sourceRoot":"","sources":["../../../src/components/builder/reuse-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { blockRootsOf, getNode, isStaticProp } from '@elytracms/core/project-graph'\nimport { readDocumentRef } from '@elytracms/core/cms-core'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Input,\n Stack,\n toast,\n} from '@elytracms/studio/ui'\nimport { getStudioRegistry } from '../../lib/registry'\nimport { getStudioOperations } from '../../lib/operations'\nimport { PublishRefusedError } from '../../lib/operations/publish-with-revision'\nimport { resolveDeploymentReuse } from '../../lib/workspace'\nimport { localizedLabel, translate } from '../../lib/i18n'\nimport { useCms } from '../cms/cms-store'\nimport { useProjectSession } from '../project-session'\nimport { useBuilder } from './builder-store'\nimport { detachRefusal, resolveReuseCapability, runPromote } from './reuse'\nimport type { PromoteEffects, ReuseCapability, ReuseRefusal } from './reuse'\nimport { placementRefusalMessage } from './placement-refusal'\n\n/**\n * In-canvas reuse gestures (EC-282): \"Make reusable\" promotes a selected block to\n * a new reusable document and replaces it in place with a transcluding embed;\n * \"Detach to local copy\" inlines an embed's referenced blocks so a page can\n * diverge. Both round-trip through the existing persistence + in-Convex reference\n * index (EC-277), so where-used is correct immediately.\n *\n * The provider needs BOTH the builder store (the edited composition) and the CMS\n * store (to create/read the reusable document), so it mounts inside the\n * composition-field editor's `BuilderProvider`, itself inside the CMS document\n * editor. Where no provider is mounted (or the capability is absent), the menu\n * reads `null` and simply omits the gestures.\n */\nexport interface ReuseGestures {\n capability: ReuseCapability | null\n canMakeReusable: (nodeId: string) => boolean\n canDetach: (nodeId: string) => boolean\n /** Open the name dialog for promoting `nodeId`. */\n startMakeReusable: (nodeId: string) => void\n /** Replace a `Reusable` embed with an editable inline copy of its target's blocks. */\n detach: (nodeId: string) => void\n}\n\n/**\n * EC-690 — the publish step of \"Make reusable\" and its failure notice, for ONE promote. The\n * store's `publishDocument` reports a refusal on the session (`persistError` — an error notice of\n * the issues tray, with the reason) AND rejects with `PublishRefusedError` (EC-585). The refusal\n * is remembered here, so {@link promotePublishFailureNotice} does not report it a second time;\n * any other throw propagates (`runPromote` reads it as \"not published\").\n */\nexport function promotePublishEffects(deps: {\n publishDocument(ref: DocumentRef): Promise<void>\n isPublished(ref: DocumentRef): Promise<boolean>\n notify: { success(message: string): void; error(message: string): void }\n}): Pick<PromoteEffects, 'publish' | 'publishFailed'> {\n let refusalHeld = false\n return {\n publish: async (ref) => {\n try {\n await deps.publishDocument(ref)\n } catch (error) {\n if (error instanceof PublishRefusedError) {\n refusalHeld = true\n return false\n }\n throw error\n }\n return deps.isPublished(ref)\n },\n publishFailed: (name) => {\n const notice = promotePublishFailureNotice(name, refusalHeld)\n deps.notify[notice.variant](notice.message)\n },\n }\n}\n\n/**\n * EC-690 — the notice \"Make reusable\" raises when its publish did not land. A publish REFUSAL is\n * already held by the session (`persistError` — an error notice of the issues tray that stays\n * until dismissed, with the refusal's reason), so the outcome here is NOT a second report of the\n * same failure: it is what the action did make — a reusable DRAFT, to publish from its document.\n * A failure nobody reported (the publish threw elsewhere, or resolved without a published\n * version) is the error notice, as before.\n */\nexport function promotePublishFailureNotice(\n name: string,\n refusalHeld: boolean,\n): { variant: 'success' | 'error'; message: string } {\n return refusalHeld\n ? { variant: 'success', message: translate('builder.reuse.madeReusableDraft', { name }) }\n : { variant: 'error', message: translate('builder.reuse.madeReusablePublishFailed', { name }) }\n}\n\nconst ReuseContext = createContext<ReuseGestures | null>(null)\n\nexport function useReuse(): ReuseGestures | null {\n return useContext(ReuseContext)\n}\n\n/** A refused reuse gesture's visible reason (EC-584) — the judge's verdict, worded once. */\nfunction toastRefusal(refusal: ReuseRefusal): void {\n toast.error(placementRefusalMessage(refusal.verdict, refusal.fieldLabel))\n}\n\nexport function ReuseProvider({ children }: { children: ReactNode }) {\n const builder = useBuilder()\n const cms = useCms()\n const { projectId } = useProjectSession()\n const [pending, setPending] = useState<{ nodeId: string; defaultName: string } | null>(null)\n\n const manifests = useMemo(() => getStudioRegistry().registry.list(), [])\n // EC-467: the optional `reuse` declaration wins; absent, the heuristic runs\n // exactly as before. The config holder is static per studio boot, so it is read\n // inside the memo (like `useStudioNav` reads the sidebar holder). Resolution\n // ISSUES are surfaced once, by the shell rail — this provider is mounted per\n // composition field, so reporting here would repeat them per field.\n const capability = useMemo(\n () => resolveReuseCapability(manifests, cms.collections, resolveDeploymentReuse()),\n [manifests, cms.collections],\n )\n\n const canMakeReusable = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n const node = getNode(builder.present, nodeId)\n // Anything but the synthetic composition root can be promoted; nesting a\n // reusable inside a reusable is allowed (the renderer + bake are cycle-\n // guarded, EC-281), so an embed itself is still promotable.\n return node !== undefined && node.id !== builder.present.id\n },\n [capability, builder],\n )\n\n const canDetach = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n return getNode(builder.present, nodeId)?.componentId === capability.embedComponentId\n },\n [capability, builder],\n )\n\n const startMakeReusable = useCallback(\n (nodeId: string) => {\n if (!canMakeReusable(nodeId)) return\n const node = getNode(builder.present, nodeId)\n if (!node) return\n const manifestTitle = localizedLabel(manifests.find((m) => m.id === node.componentId)?.title) || undefined\n const collection = capability ? cms.getCollection(capability.collectionId) : undefined\n const defaultName =\n manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || translate('builder.reuse.defaultElementName'))\n setPending({ nodeId, defaultName })\n },\n [canMakeReusable, builder, manifests, capability, cms],\n )\n\n const confirmMakeReusable = useCallback(\n (name: string, publish: boolean) => {\n const cap = capability\n if (!cap || !pending) return\n const node = getNode(builder.present, pending.nodeId)\n const collection = cms.getCollection(cap.collectionId)\n setPending(null)\n if (!node || !collection) return\n // EC-584 / EC-560: the placement preflight runs FIRST — a refused promote creates,\n // publishes and replaces nothing. Then (EC-595 A2 / EC-578) ONE insert-if-absent\n // create with a factory id, the in-place swap for the transcluding embed (the\n // action clones to a fresh id; the source `node` stays untouched in the new doc),\n // and the chosen publish. EC-300: PUBLISH keeps the section visible everywhere\n // immediately (the continuity-preserving default); SAVE DRAFT keeps it editable,\n // surfaced as a pending reference until published. No silent auto-publish (AD-16).\n void runPromote({\n root: builder.present,\n nodeId: pending.nodeId,\n node,\n name,\n publish,\n capability: cap,\n collection,\n existing: cms.documentsOf(cap.collectionId),\n locales: cms.localeConfig,\n placement: builder.placement,\n canvasFieldLabel: builder.field.label,\n effects: {\n newDocumentId: () => cms.newDocumentId(),\n insertDocument: (doc) => cms.insertDocument(doc),\n replaceInCanvas: (nodeId, embed) => builder.dispatch({ type: 'replace', id: nodeId, nodes: [embed] }),\n refused: toastRefusal,\n madeDraft: (label) => toast.success(translate('builder.reuse.madeReusableDraft', { name: label })),\n madePublished: (label) => toast.success(translate('builder.reuse.madeReusablePublished', { name: label })),\n // The publish and its failure notice (EC-690: a refusal the session already holds is\n // never reported twice) — see `promotePublishEffects`.\n ...promotePublishEffects({\n publishDocument: (ref) => cms.publishDocument(ref),\n // Success is the AUTHORITATIVE pointer after the publish settles (the same read the\n // store reconciles its badge from), never the resolution.\n isPublished: async (ref) => {\n if (projectId === undefined) return false\n const { operations } = await getStudioOperations()\n const record = await operations.documents.get({ projectId, ref })\n return record.ok && record.value.publishedVersion !== undefined\n },\n notify: { success: (message) => toast.success(message), error: (message) => toast.error(message) },\n }),\n },\n })\n },\n [capability, pending, builder, cms, projectId],\n )\n\n const detach = useCallback(\n (nodeId: string) => {\n const cap = capability\n if (!cap) return\n const node = getNode(builder.present, nodeId)\n if (!node || node.componentId !== cap.embedComponentId) return\n const prop = node.props?.[cap.relationProp]\n const ref = isStaticProp(prop) ? readDocumentRef(prop.value) : undefined\n if (!ref) {\n toast.error(translate('builder.reuse.noTargetToDetach'))\n return\n }\n const target = cms.getDocument(ref)\n const blocks = blockRootsOf(target?.values?.[cap.contentField])\n if (blocks.length === 0) {\n toast.error(translate('builder.reuse.noContentToDetach'))\n return\n }\n // EC-584 / EC-560: the inlined blocks land where the embed is — judged first, so\n // a refusal keeps the reference in place, says why, and dispatches nothing.\n const refusal = detachRefusal({\n root: builder.present,\n nodeId,\n blocks,\n placement: builder.placement,\n canvasFieldLabel: builder.field.label,\n })\n if (refusal) {\n toastRefusal(refusal)\n return\n }\n builder.dispatch({ type: 'replace', id: nodeId, nodes: blocks })\n toast.success(translate('builder.reuse.detachedToCopy'))\n },\n [capability, builder, cms],\n )\n\n const value = useMemo<ReuseGestures>(\n () => ({ capability, canMakeReusable, canDetach, startMakeReusable, detach }),\n [capability, canMakeReusable, canDetach, startMakeReusable, detach],\n )\n\n return (\n <ReuseContext.Provider value={value}>\n {children}\n <MakeReusableDialog\n open={pending !== null}\n defaultName={pending?.defaultName ?? ''}\n onConfirm={confirmMakeReusable}\n onCancel={() => setPending(null)}\n />\n </ReuseContext.Provider>\n )\n}\n\n/** Name-and-create dialog for the \"Make reusable\" gesture. */\nfunction MakeReusableDialog({\n open,\n defaultName,\n onConfirm,\n onCancel,\n}: {\n open: boolean\n defaultName: string\n /** `publish` chooses the new reusable's initial state (EC-300/AD-16). */\n onConfirm: (name: string, publish: boolean) => void\n onCancel: () => void\n}) {\n const [name, setName] = useState(defaultName)\n // Re-seed the field each time the dialog opens for a new selection.\n const [seededFor, setSeededFor] = useState<string | null>(null)\n if (open && seededFor !== defaultName) {\n setName(defaultName)\n setSeededFor(defaultName)\n }\n\n const empty = name.trim() === ''\n function commit(publish: boolean) {\n if (empty) return\n onConfirm(name.trim(), publish)\n setSeededFor(null)\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) {\n onCancel()\n setSeededFor(null)\n }\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{translate('builder.reuse.makeReusable')}</DialogTitle>\n <DialogDescription>\n {translate('builder.reuse.makeReusableDescription')}\n </DialogDescription>\n </DialogHeader>\n {/* Enter submits the primary action (Publish) — the continuity-preserving default. */}\n <form\n onSubmit={(event: FormEvent) => {\n event.preventDefault()\n commit(true)\n }}\n >\n <Stack gap=\"3\">\n <Field label={translate('builder.reuse.nameLabel')} htmlFor=\"make-reusable-name\" required>\n <Input\n id=\"make-reusable-name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder={translate('builder.reuse.namePlaceholder')}\n autoFocus\n required\n />\n </Field>\n </Stack>\n <DialogFooter className=\"mt-4\">\n {/* Cancel pinned left; the two commit actions group right. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"mr-auto\" onClick={onCancel}>\n {translate('common.cancel')}\n </Button>\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={empty}\n onClick={() => commit(false)}\n data-testid=\"make-reusable-draft\"\n >\n {translate('builder.reuse.saveDraft')}\n </Button>\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={empty}\n data-testid=\"make-reusable-publish\"\n >\n {translate('builder.reuse.publish')}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentPatch, DocumentRef, FieldDef, ValuePath, Locale, LocaleConfig, LocalizedString, RestoreScope, ValidateDocumentOptions } from '@elytracms/core/cms-core';
3
3
  import type { CmsDocumentRecord, DocumentVersionRecord, IdFactory, PresenceEntry } from '@elytracms/core/persistence';
4
+ import type { ImportRejection } from './import-batch.js';
4
5
  import type { CommitOutcome, DocumentConflict, OperationError, OperationsClient } from '../../lib/operations/index.js';
5
6
  import type { TableViewState } from '../../lib/cms-views/index.js';
6
7
  import type { CellCommitInput, CellCommitOutcome } from './cell-edit.js';
@@ -24,10 +25,7 @@ export interface SavedView {
24
25
  }
25
26
  export interface ImportReport {
26
27
  added: number;
27
- rejected: {
28
- id: string;
29
- issues: CmsValidationIssue[];
30
- }[];
28
+ rejected: ImportRejection[];
31
29
  }
32
30
  /**
33
31
  * A document's recorded version history plus its live pin (EC-224), as the
@@ -120,9 +118,10 @@ export interface CmsStore {
120
118
  presenceSessionId: string;
121
119
  /**
122
120
  * Write a whole document at `revision` — the revision of the view its snapshot was
123
- * authored on (EC-647; a document-level key — the alias).
121
+ * authored on (EC-647; a document-level key — the alias). `authoredOn`: the document the
122
+ * snapshot was made from — what a refusal decomposes it against, per position (EC-686).
124
123
  */
125
- replaceDocument(doc: CmsDocument, revision: number | undefined): void;
124
+ replaceDocument(doc: CmsDocument, revision: number | undefined, authoredOn?: CmsDocument): void;
126
125
  deleteDocument(ref: DocumentRef): void;
127
126
  /**
128
127
  * Is this document currently published? Sourced from the version POINTER
@@ -1,6 +1,6 @@
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 { applyDocumentPatches, createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, seedDefaults, titleFieldOf, touchedPaths, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
3
+ import { applyDocumentPatches, createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, resolveDocumentTitle, seedDefaults, titleFieldOf, touchedPaths, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
4
4
  import { createUuidIdFactory } from '@elytracms/core/persistence';
5
5
  import { translate } from '../../lib/i18n/index.js';
6
6
  import { configureStudioLocales, useStudioLocale } from '../../lib/locale/index.js';
@@ -9,6 +9,7 @@ import { resolveDeploymentCollections, resolveDeploymentLocaleConfig, resolveDep
9
9
  import { Heading, Panel, Stack, Text } from '@elytracms/studio/ui';
10
10
  import { assetLibraryOracle, gateValidateOptions } from './gate-validation.js';
11
11
  import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils.js';
12
+ import { parseImportBatch } from './import-batch.js';
12
13
  import { createRequiredContext } from '../../lib/required-context.js';
13
14
  import { createCmsPersister, getStudioActor, getStudioOperations, importWrite } from '../../lib/operations/index.js';
14
15
  import { publishInSession, publishWithRevision as publishRevision, } from '../../lib/operations/publish-with-revision.js';
@@ -19,6 +20,7 @@ import { useOptionalAssets } from '../assets/assets-store.js';
19
20
  import { consumeStagedDocuments } from './document-staging.js';
20
21
  import { commitCellEdit } from './cell-edit.js';
21
22
  import { runBulkAction } from './bulk-actions.js';
23
+ import { useOutcomeNotice } from '../notifications/outcome-notice.js';
22
24
  const { Context: CmsContext, useRequired: useCms } = createRequiredContext('Cms');
23
25
  /** The stable empty conflict list (referential stability for the surfaces' memos). */
24
26
  const NO_CONFLICTS = [];
@@ -290,6 +292,11 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
290
292
  ...(initialCollections ?? []),
291
293
  ]);
292
294
  const [persistError, setPersistError] = useState(null);
295
+ // EC-690: the held failure is ALSO an error notice of the shell's issues tray — registered
296
+ // once, here at its owner (never by an edit shell that renders it); dismissing it there
297
+ // clears it here. A session expiry is excluded (it raises the re-auth overlay).
298
+ const dismissPersistError = useCallback(() => setPersistError(null), []);
299
+ useOutcomeNotice(persistError, dismissPersistError);
293
300
  // EC-595 A2: the open per-field conflicts, by document key.
294
301
  const [conflictsByKey, setConflictsByKey] = useState(() => new Map());
295
302
  // EC-274 → EC-595 A2: collection ids whose singleton create this session already
@@ -684,28 +691,9 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
684
691
  // transfer dialog) can no longer change a verdict. `validate` above stays the
685
692
  // session's DISPLAY validation (the editor's issues, cell edit, import, AI).
686
693
  const importDocuments = useCallback((collectionId, raw) => {
687
- const report = { added: 0, rejected: [] };
688
694
  // Pre-parse so intra-batch relation/asset targets resolve during validation.
689
- const parsedBatch = [];
690
- const schemaFails = [];
691
- for (const item of raw) {
692
- const parsed = documentSchema.safeParse({ ...item, collection: collectionId });
693
- if (parsed.success)
694
- parsedBatch.push(parsed.data);
695
- else
696
- schemaFails.push({ item, error: parsed });
697
- }
698
- for (const { item, error } of schemaFails) {
699
- report.rejected.push({
700
- id: item?.id ?? translate('cms.list.import.noId'),
701
- issues: (error.success ? [] : error.error.issues).map((e) => ({
702
- code: 'invalid-field-value',
703
- severity: 'error',
704
- message: e.message,
705
- path: e.path.map((p) => (typeof p === 'symbol' ? String(p) : p)),
706
- })),
707
- });
708
- }
695
+ const { parsed: parsedBatch, rejected } = parseImportBatch(collectionId, raw);
696
+ const report = { added: 0, rejected: [...rejected] };
709
697
  // Validate each against existing docs PLUS the rest of the batch.
710
698
  const batchStore = createDocumentStore([...documents, ...parsedBatch]);
711
699
  const accepted = [];
@@ -991,10 +979,10 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
991
979
  .then((studio) => studio.clearPresence(projectId, ref, PRESENCE_SESSION_ID))
992
980
  .catch(() => { });
993
981
  }, [projectId]);
994
- const replaceDocument = useCallback((doc, revision) => {
982
+ const replaceDocument = useCallback((doc, revision, authoredOn) => {
995
983
  const persister = persisterRef.current;
996
984
  if (persister)
997
- persister.replace(doc, revision);
985
+ persister.replace(doc, revision, authoredOn);
998
986
  else
999
987
  setDocuments((prev) => replaceDoc(prev, doc));
1000
988
  }, []);