@elytracms/studio 0.0.88 → 0.0.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/asset-revalidate.test.ts +49 -43
- package/convex/cms.ts +3 -4
- package/convex/delivery.ts +8 -11
- package/convex/document-revalidate.test.ts +188 -0
- package/convex/redirect-revalidate.test.ts +50 -37
- package/convex/revalidate.ts +18 -30
- package/convex/triggers.ts +47 -38
- package/convex/webhooks.ts +77 -62
- package/dist/components/account/self-account-view-source.js +2 -1
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +1 -1
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/app-views/default-modules.js +3 -3
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-grid.js +1 -1
- package/dist/components/assets/asset-grid.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +8 -9
- package/dist/components/assets/asset-selection.js +26 -16
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.js +3 -2
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +7 -5
- package/dist/components/builder/blocks-model.js +11 -9
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +1 -1
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +11 -3
- package/dist/components/builder/builder-store.js +12 -5
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -1
- package/dist/components/builder/canvas-dnd.js +6 -3
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +13 -1
- package/dist/components/builder/canvas-render-context.js +28 -18
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas.js +1 -1
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +0 -6
- package/dist/components/builder/clipboard-actions.js +9 -4
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +22 -0
- package/dist/components/builder/composition-field-editor.js +47 -8
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/inline-edit.d.ts +7 -4
- package/dist/components/builder/inline-edit.js +11 -8
- package/dist/components/builder/inline-edit.js.map +1 -1
- package/dist/components/builder/insert-context.js +9 -5
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/layout-inspector.d.ts +2 -2
- package/dist/components/builder/layout-inspector.js +8 -6
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +31 -14
- package/dist/components/builder/node-factory.js +73 -93
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -1
- package/dist/components/builder/page-tree.js +6 -4
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +2 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +2 -2
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +7 -3
- package/dist/components/builder/reuse.js +19 -11
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +2 -2
- package/dist/components/builder/selection-overlay.js +3 -3
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +2 -2
- package/dist/components/builder/spacing-handles.js +3 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +1 -1
- package/dist/components/cms/asset-field.js +15 -13
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.js +3 -4
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +0 -6
- package/dist/components/cms/cell-edit.js +5 -23
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +3 -2
- package/dist/components/cms/cms-store.js +23 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +14 -4
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +28 -24
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +1 -2
- package/dist/components/cms/discard-draft.js +1 -1
- package/dist/components/cms/discard-draft.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +3 -2
- package/dist/components/cms/duplicate-document.js +5 -5
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/editable-cell.js +11 -7
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +10 -0
- package/dist/components/cms/field-control-lib.js +20 -0
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +10 -9
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/object-array-field.js +3 -5
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +16 -3
- package/dist/components/cms/prop-fields.js +57 -12
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/publish-clean.d.ts +13 -6
- package/dist/components/cms/publish-clean.js +14 -6
- package/dist/components/cms/publish-clean.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +6 -1
- package/dist/components/cms/rich-text/embed-props.js +7 -12
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +13 -6
- package/dist/components/cms/rich-text/insert.js +16 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-cell-edit.js +7 -12
- package/dist/components/cms/use-cell-edit.js.map +1 -1
- package/dist/components/cms/use-discard-controller.d.ts +1 -2
- package/dist/components/cms/use-discard-controller.js +1 -2
- package/dist/components/cms/use-discard-controller.js.map +1 -1
- package/dist/components/cms/use-publish-controller.js +3 -2
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/versions-pane.js +2 -2
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +1 -1
- package/dist/components/components/component-detail.js +2 -4
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +9 -4
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/components-index.d.ts +2 -2
- package/dist/components/components/components-index.js +3 -6
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/field-inspector.js +5 -2
- package/dist/components/components/field-inspector.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +6 -5
- package/dist/components/components/registry-browser.js +11 -37
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +14 -21
- package/dist/components/components/taxonomy.js +5 -29
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -0
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +20 -12
- package/dist/components/components/transfer.js +27 -21
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +3 -2
- package/dist/components/delivery-health-banner.js +3 -2
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.js +3 -2
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/users/users-collection-view-source.js +2 -1
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/lib/ai/apply-content.js +32 -28
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +16 -14
- package/dist/lib/ai/constraints.js +64 -48
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/translate-document.js +11 -7
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +9 -5
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +15 -32
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/vite.d.ts +22 -2
- package/dist/lib/ai/vite.js +12 -3
- package/dist/lib/ai/vite.js.map +1 -1
- package/dist/lib/assets/asset-placements.d.ts +3 -10
- package/dist/lib/assets/asset-placements.js +17 -8
- package/dist/lib/assets/asset-placements.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +3 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/published-documents.d.ts +14 -3
- package/dist/lib/cms/published-documents.js +12 -3
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +4 -4
- package/dist/lib/cms-views/view-model.js +26 -20
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +10 -7
- package/dist/lib/data/collection-content.js +70 -29
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/delivery/instant-publish.d.ts +0 -3
- package/dist/lib/delivery/instant-publish.js +0 -1
- package/dist/lib/delivery/instant-publish.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +22 -7
- package/dist/lib/dnd/drop-rules.js +21 -14
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +25 -9
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +21 -7
- package/dist/lib/i18n/messages.en.js +28 -9
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/fallback.d.ts +5 -19
- package/dist/lib/locale/fallback.js +18 -39
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.js +2 -1
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/operations/cms-persistence.js +7 -4
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +4 -6
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +5 -25
- package/dist/lib/persistence/backend-mode.js +13 -30
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +4 -9
- package/dist/lib/persona/labels.js +5 -12
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/reference-model.d.ts +17 -5
- package/dist/lib/references/reference-model.js +20 -28
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -0
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/registry/__fixtures__/demo-components.d.ts +32 -0
- package/dist/lib/registry/{project-components.js → __fixtures__/demo-components.js} +18 -5
- package/dist/lib/registry/__fixtures__/demo-components.js.map +1 -0
- package/dist/lib/registry/index.d.ts +9 -13
- package/dist/lib/registry/index.js +34 -61
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/preview.d.ts +22 -7
- package/dist/lib/registry/preview.js +26 -89
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +0 -7
- package/dist/lib/workspace/index.js +0 -9
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/previews/enumerate.d.ts +23 -27
- package/dist/previews/enumerate.js +27 -48
- package/dist/previews/enumerate.js.map +1 -1
- package/dist/previews/harness.js +1 -1
- package/dist/previews/harness.js.map +1 -1
- package/package.json +4 -7
- package/dist/lib/registry/project-components.d.ts +0 -16
- package/dist/lib/registry/project-components.js.map +0 -1
- package/dist/lib/webhooks/config.d.ts +0 -15
- package/dist/lib/webhooks/config.js +0 -65
- package/dist/lib/webhooks/config.js.map +0 -1
- package/dist/lib/webhooks/dispatcher.d.ts +0 -70
- package/dist/lib/webhooks/dispatcher.js +0 -61
- package/dist/lib/webhooks/dispatcher.js.map +0 -1
- package/dist/lib/webhooks/index.d.ts +0 -11
- package/dist/lib/webhooks/index.js +0 -10
- package/dist/lib/webhooks/index.js.map +0 -1
- package/dist/lib/webhooks/signing.d.ts +0 -15
- package/dist/lib/webhooks/signing.js +0 -24
- package/dist/lib/webhooks/signing.js.map +0 -1
- package/dist/lib/webhooks/tags.d.ts +0 -25
- package/dist/lib/webhooks/tags.js +0 -289
- package/dist/lib/webhooks/tags.js.map +0 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { assetNode } from '@elytracms/core/cms-core';
|
|
2
|
-
import {
|
|
3
|
-
import { isCanvasEligible, isPrimitive } from '@elytracms/core/component-registry';
|
|
2
|
+
import { isCanvasEligible } from '@elytracms/core/component-registry';
|
|
4
3
|
import { COMPONENT_EMBED_NODE_TYPE } from './component-embed.js';
|
|
5
4
|
/**
|
|
6
5
|
* Reference-carrying node insertions shared by the toolbar controls and the slash
|
|
@@ -63,27 +62,27 @@ function selectComponentEmbed(editor, id) {
|
|
|
63
62
|
if (pos >= 0)
|
|
64
63
|
editor.commands.setNodeSelection(pos);
|
|
65
64
|
}
|
|
66
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* A fresh embedded ComponentNode (mirrors builder node-factory). EC-591 / ratified
|
|
67
|
+
* 2f: NO props — an absent prop is `undefined`, so the implementation's own default
|
|
68
|
+
* applies; the manifest `default` is a hint, never baked into the stored node.
|
|
69
|
+
*/
|
|
67
70
|
export function embedNodeFromManifest(manifest) {
|
|
68
|
-
|
|
69
|
-
for (const [name, spec] of Object.entries(manifest.props)) {
|
|
70
|
-
if (spec.default !== undefined)
|
|
71
|
-
props[name] = staticProp(spec.default);
|
|
72
|
-
}
|
|
73
|
-
return { id: newEmbedId(), componentId: manifest.id, props };
|
|
71
|
+
return { id: newEmbedId(), componentId: manifest.id, props: {} };
|
|
74
72
|
}
|
|
75
73
|
/**
|
|
76
74
|
* The components embeddable in a rich-text field: the field's `allow` list (a
|
|
77
|
-
* closed set — EC-544: an empty one offers nothing), or
|
|
78
|
-
* component when absent —
|
|
79
|
-
* `validateRichTextEmbeds` leash
|
|
80
|
-
*
|
|
81
|
-
*
|
|
75
|
+
* closed set — EC-544: an empty one offers nothing), or EVERY registered
|
|
76
|
+
* component when absent — the ONE semantic the `blocks` field, the blocks panel
|
|
77
|
+
* and the `validateRichTextEmbeds` leash already apply (fields.ts: "absent = any").
|
|
78
|
+
* EC-591: the old "every non-primitive" reading keyed on the `base.*` id prefix,
|
|
79
|
+
* which no longer means anything (and diverged from the validator — COMPOSITION
|
|
80
|
+
* #6); a field that wants a narrower offer declares `allow`. Eligibility is
|
|
81
|
+
* `isCanvasEligible`, the one predicate the blocks panel and `elytra previews`
|
|
82
|
+
* read too (EC-514) — never a local reading of the manifest flag.
|
|
82
83
|
*/
|
|
83
84
|
export function listEmbeddable(allow, registered) {
|
|
84
|
-
const permitted = allow
|
|
85
|
-
? registered.filter((m) => allow.includes(m.id))
|
|
86
|
-
: registered.filter((m) => !isPrimitive(m.id));
|
|
85
|
+
const permitted = allow ? registered.filter((m) => allow.includes(m.id)) : registered;
|
|
87
86
|
return permitted.filter(isCanvasEligible);
|
|
88
87
|
}
|
|
89
88
|
/** A unique id for an embedded node (studio runs in the browser; crypto is present). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/insert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"insert.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/insert.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { assetNode } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { isCanvasEligible } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport type { AssetsStore } from '../../assets/assets-store'\n\n/**\n * Reference-carrying node insertions shared by the toolbar controls and the slash\n * menu (EC-314). Centralised so both entry points produce byte-identical envelopes\n * (the shapes `reference-extraction` indexes).\n */\n\n/**\n * What the rich-text image insert accepts (EC-588): images, in the structured `accept`\n * vocabulary every picker takes — stated once here rather than as a file-input literal\n * at the call site. The picker derives its grid filter, its Upload dialog and its drop\n * rule from it, like a field's own `accept`.\n */\nexport const INLINE_IMAGE_ACCEPT: readonly string[] = ['image']\n\n/** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */\nexport function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void {\n const record = assets?.assets.find((a) => a.id === assetId)\n editor\n .chain()\n .focus()\n .insertContent({\n type: 'image',\n attrs: { src: record?.url ?? '', alt: record?.alt ?? '', ref: assetNode(assetId) },\n })\n .run()\n}\n\n/**\n * Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest\n * defaults. With `{ select: true }` the inserted node is then selected — so a host\n * embed inspector (0.0.41) opens straight onto its props for editing.\n */\nexport function insertComponentEmbed(\n editor: Editor,\n manifest: ComponentManifest,\n options?: { select?: boolean },\n): void {\n const embed = embedNodeFromManifest(manifest)\n editor\n .chain()\n .focus()\n .insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embed } })\n .run()\n if (options?.select) selectComponentEmbed(editor, embed.id)\n}\n\n/**\n * Select the just-inserted embed by its unique embedded-node id. Located by scan\n * (rather than fragile position arithmetic after `insertContent`, which may split a\n * paragraph or append a trailing block); the id is unique so the first match is it.\n */\nfunction selectComponentEmbed(editor: Editor, id: string): void {\n let pos = -1\n editor.state.doc.descendants((node, at) => {\n if (pos >= 0) return false\n if (node.type.name === COMPONENT_EMBED_NODE_TYPE) {\n const embedded = node.attrs.node as { id?: string } | null\n if (embedded?.id === id) {\n pos = at\n return false\n }\n }\n return true\n })\n if (pos >= 0) editor.commands.setNodeSelection(pos)\n}\n\n/**\n * A fresh embedded ComponentNode (mirrors builder node-factory). EC-591 / ratified\n * 2f: NO props — an absent prop is `undefined`, so the implementation's own default\n * applies; the manifest `default` is a hint, never baked into the stored node.\n */\nexport function embedNodeFromManifest(manifest: ComponentManifest): ComponentNode {\n return { id: newEmbedId(), componentId: manifest.id, props: {} }\n}\n\n/**\n * The components embeddable in a rich-text field: the field's `allow` list (a\n * closed set — EC-544: an empty one offers nothing), or EVERY registered\n * component when absent — the ONE semantic the `blocks` field, the blocks panel\n * and the `validateRichTextEmbeds` leash already apply (fields.ts: \"absent = any\").\n * EC-591: the old \"every non-primitive\" reading keyed on the `base.*` id prefix,\n * which no longer means anything (and diverged from the validator — COMPOSITION\n * #6); a field that wants a narrower offer declares `allow`. Eligibility is\n * `isCanvasEligible`, the one predicate the blocks panel and `elytra previews`\n * read too (EC-514) — never a local reading of the manifest flag.\n */\nexport function listEmbeddable(\n allow: string[] | undefined,\n registered: ComponentManifest[],\n): ComponentManifest[] {\n const permitted = allow ? registered.filter((m) => allow.includes(m.id)) : registered\n return permitted.filter(isCanvasEligible)\n}\n\n/** A unique id for an embedded node (studio runs in the browser; crypto is present). */\nfunction newEmbedId(): string {\n const uuid = globalThis.crypto?.randomUUID?.()\n return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from 'react';
|
|
2
|
-
import { resolveField } from '@elytracms/core/cms-core';
|
|
2
|
+
import { resolveField, semanticValueEqual } from '@elytracms/core/cms-core';
|
|
3
3
|
import { cellErrorMessage, cellKey, nextEditableCell } from './cell-edit.js';
|
|
4
4
|
import { useCollectionView } from '../collection-view/collection-view-context.js';
|
|
5
5
|
export function useCellEdit({ collection, docs, editableFields, }) {
|
|
@@ -117,15 +117,10 @@ export function useCellEdit({ collection, docs, editableFields, }) {
|
|
|
117
117
|
commit,
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
120
|
+
/**
|
|
121
|
+
* The cell's no-op guard: THE equality verdict (EC-586, `semanticValueEqual` over
|
|
122
|
+
* canonical forms — the same verdict the "has unpublished changes" dot reads), so a
|
|
123
|
+
* commit the store would judge "no change" is not written either.
|
|
124
|
+
*/
|
|
125
|
+
const sameCellValue = semanticValueEqual;
|
|
131
126
|
//# sourceMappingURL=use-cell-edit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/use-cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport { resolveField } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport { cellErrorMessage, cellKey, nextEditableCell } from './cell-edit'\nimport type { AdvanceDirection, CellCoord } from './cell-edit'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/**\n * Table-level cell-edit controller (EC-168). Owns the state machine the cells\n * render from:\n *\n * idle → editing (click/Enter) → committing (Enter/Tab/blur) → idle\n * ↘ cancelled (Esc, no operation) ↘ rejected (rollback + cell error)\n *\n * Exactly one cell edits at a time; commits go through\n * `cms.commitFieldEdit` (one operations command each); rejections park a\n * per-cell error that the cell renders inline until the cell is edited again.\n */\nexport interface CellEditController {\n /** Ref for the table container (focus restoration after commits/cancels). */\n containerRef: RefObject<HTMLDivElement | null>\n /** Key of the cell currently editing, or null. */\n activeKey: string | null\n /** Key of the cell with a commit in flight, or null. */\n busyKey: string | null\n errorOf(key: string): string | null\n start(coord: CellCoord): void\n cancel(coord: CellCoord): void\n commit(coord: CellCoord, value: unknown, advance: AdvanceDirection | null): void\n}\n\nexport function useCellEdit({\n collection,\n docs,\n editableFields,\n}: {\n collection: CollectionDef\n /** Documents in rendered (sorted) row order. */\n docs: readonly CmsDocument[]\n /** Editable column field names in rendered order. */\n editableFields: readonly string[]\n}): CellEditController {\n const source = useCollectionView()\n const [activeKey, setActiveKey] = useState<string | null>(null)\n const [busyKey, setBusyKey] = useState<string | null>(null)\n const [errors, setErrors] = useState<Record<string, string>>({})\n const containerRef = useRef<HTMLDivElement | null>(null)\n\n // Snapshots for async commit completion (row order may re-sort mid-flight).\n const activeRef = useRef(activeKey)\n activeRef.current = activeKey\n const docsRef = useRef(docs)\n docsRef.current = docs\n const fieldsRef = useRef(editableFields)\n fieldsRef.current = editableFields\n\n const clearError = useCallback((key: string) => {\n setErrors((prev) => {\n if (!(key in prev)) return prev\n const next = { ...prev }\n delete next[key]\n return next\n })\n }, [])\n\n const focusCell = useCallback((key: string) => {\n // After state settles: return focus to the cell so Enter re-edits and the\n // keyboard flow continues where the user left off.\n requestAnimationFrame(() => {\n containerRef.current\n ?.querySelector<HTMLElement>(`[data-cell-key=\"${CSS.escape(key)}\"]`)\n ?.focus()\n })\n }, [])\n\n const start = useCallback(\n (coord: CellCoord) => {\n const key = cellKey(coord)\n clearError(key)\n setActiveKey(key)\n },\n [clearError],\n )\n\n const cancel = useCallback(\n (coord: CellCoord) => {\n const key = cellKey(coord)\n if (activeRef.current !== key) return\n setActiveKey(null)\n focusCell(key)\n },\n [focusCell],\n )\n\n /** Close the editor and move on (commit settled or value unchanged). */\n const settle = useCallback(\n (coord: CellCoord, next: CellCoord | null) => {\n const key = cellKey(coord)\n // The user may have clicked into another cell while the commit ran —\n // never clobber their new edit session.\n if (activeRef.current !== key) return\n if (next) {\n clearError(cellKey(next))\n setActiveKey(cellKey(next))\n } else {\n setActiveKey(null)\n focusCell(key)\n }\n },\n [clearError, focusCell],\n )\n\n const commit = useCallback(\n (coord: CellCoord, value: unknown, advance: AdvanceDirection | null) => {\n const key = cellKey(coord)\n const doc = docsRef.current.find((d) => d.id === coord.docId)\n const field = collection.fields.find((f) => f.name === coord.field)\n if (!doc || !field) {\n settle(coord, null)\n return\n }\n // The advance target is computed against the row order at commit time —\n // a re-sort caused by the edit must not teleport the cursor.\n const next = advance\n ? nextEditableCell(\n docsRef.current.map((d) => d.id),\n fieldsRef.current,\n coord,\n advance,\n )\n : null\n\n // Same value as displayed → close (and advance) without any operation.\n // Notably: re-committing an unchanged fallback-shown value dispatches\n // nothing — a localized variant is only created by an actual edit.\n const locale = source.activeLocale\n const original = resolveField(collection, doc, field.name, locale, source.localeConfig).value\n if (sameCellValue(value, original)) {\n settle(coord, next)\n return\n }\n\n // Inline editing is a capability — absent it (a read-only rail) there is\n // nothing to commit; close the editor without dispatching.\n const commitField = source.inlineEditing?.commitField\n if (!commitField) {\n settle(coord, null)\n return\n }\n\n setBusyKey(key)\n void commitField({ collection, doc, field, locale, value })\n .then((outcome) => {\n if (outcome.ok) {\n clearError(key)\n settle(coord, next)\n return\n }\n // Rejected: the session already rolled back — park the error on the\n // cell (never a toast) and return focus there.\n setErrors((prev) => ({ ...prev, [key]: cellErrorMessage(outcome.error) }))\n settle(coord, null)\n })\n .finally(() => {\n setBusyKey((prev) => (prev === key ? null : prev))\n })\n },\n [source, collection, clearError, settle],\n )\n\n return {\n containerRef,\n activeKey,\n busyKey,\n errorOf: (key) => errors[key] ?? null,\n start,\n cancel,\n commit,\n }\n}\n\n/** Structural equality good enough for cell values (scalars, refs, id arrays). */\nfunction sameCellValue(a: unknown, b: unknown): boolean {\n if (a === undefined && b === undefined) return true\n try {\n return JSON.stringify(a) === JSON.stringify(b)\n } catch {\n return false\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/use-cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport { resolveField, semanticValueEqual } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport { cellErrorMessage, cellKey, nextEditableCell } from './cell-edit'\nimport type { AdvanceDirection, CellCoord } from './cell-edit'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/**\n * Table-level cell-edit controller (EC-168). Owns the state machine the cells\n * render from:\n *\n * idle → editing (click/Enter) → committing (Enter/Tab/blur) → idle\n * ↘ cancelled (Esc, no operation) ↘ rejected (rollback + cell error)\n *\n * Exactly one cell edits at a time; commits go through\n * `cms.commitFieldEdit` (one operations command each); rejections park a\n * per-cell error that the cell renders inline until the cell is edited again.\n */\nexport interface CellEditController {\n /** Ref for the table container (focus restoration after commits/cancels). */\n containerRef: RefObject<HTMLDivElement | null>\n /** Key of the cell currently editing, or null. */\n activeKey: string | null\n /** Key of the cell with a commit in flight, or null. */\n busyKey: string | null\n errorOf(key: string): string | null\n start(coord: CellCoord): void\n cancel(coord: CellCoord): void\n commit(coord: CellCoord, value: unknown, advance: AdvanceDirection | null): void\n}\n\nexport function useCellEdit({\n collection,\n docs,\n editableFields,\n}: {\n collection: CollectionDef\n /** Documents in rendered (sorted) row order. */\n docs: readonly CmsDocument[]\n /** Editable column field names in rendered order. */\n editableFields: readonly string[]\n}): CellEditController {\n const source = useCollectionView()\n const [activeKey, setActiveKey] = useState<string | null>(null)\n const [busyKey, setBusyKey] = useState<string | null>(null)\n const [errors, setErrors] = useState<Record<string, string>>({})\n const containerRef = useRef<HTMLDivElement | null>(null)\n\n // Snapshots for async commit completion (row order may re-sort mid-flight).\n const activeRef = useRef(activeKey)\n activeRef.current = activeKey\n const docsRef = useRef(docs)\n docsRef.current = docs\n const fieldsRef = useRef(editableFields)\n fieldsRef.current = editableFields\n\n const clearError = useCallback((key: string) => {\n setErrors((prev) => {\n if (!(key in prev)) return prev\n const next = { ...prev }\n delete next[key]\n return next\n })\n }, [])\n\n const focusCell = useCallback((key: string) => {\n // After state settles: return focus to the cell so Enter re-edits and the\n // keyboard flow continues where the user left off.\n requestAnimationFrame(() => {\n containerRef.current\n ?.querySelector<HTMLElement>(`[data-cell-key=\"${CSS.escape(key)}\"]`)\n ?.focus()\n })\n }, [])\n\n const start = useCallback(\n (coord: CellCoord) => {\n const key = cellKey(coord)\n clearError(key)\n setActiveKey(key)\n },\n [clearError],\n )\n\n const cancel = useCallback(\n (coord: CellCoord) => {\n const key = cellKey(coord)\n if (activeRef.current !== key) return\n setActiveKey(null)\n focusCell(key)\n },\n [focusCell],\n )\n\n /** Close the editor and move on (commit settled or value unchanged). */\n const settle = useCallback(\n (coord: CellCoord, next: CellCoord | null) => {\n const key = cellKey(coord)\n // The user may have clicked into another cell while the commit ran —\n // never clobber their new edit session.\n if (activeRef.current !== key) return\n if (next) {\n clearError(cellKey(next))\n setActiveKey(cellKey(next))\n } else {\n setActiveKey(null)\n focusCell(key)\n }\n },\n [clearError, focusCell],\n )\n\n const commit = useCallback(\n (coord: CellCoord, value: unknown, advance: AdvanceDirection | null) => {\n const key = cellKey(coord)\n const doc = docsRef.current.find((d) => d.id === coord.docId)\n const field = collection.fields.find((f) => f.name === coord.field)\n if (!doc || !field) {\n settle(coord, null)\n return\n }\n // The advance target is computed against the row order at commit time —\n // a re-sort caused by the edit must not teleport the cursor.\n const next = advance\n ? nextEditableCell(\n docsRef.current.map((d) => d.id),\n fieldsRef.current,\n coord,\n advance,\n )\n : null\n\n // Same value as displayed → close (and advance) without any operation.\n // Notably: re-committing an unchanged fallback-shown value dispatches\n // nothing — a localized variant is only created by an actual edit.\n const locale = source.activeLocale\n const original = resolveField(collection, doc, field.name, locale, source.localeConfig).value\n if (sameCellValue(value, original)) {\n settle(coord, next)\n return\n }\n\n // Inline editing is a capability — absent it (a read-only rail) there is\n // nothing to commit; close the editor without dispatching.\n const commitField = source.inlineEditing?.commitField\n if (!commitField) {\n settle(coord, null)\n return\n }\n\n setBusyKey(key)\n void commitField({ collection, doc, field, locale, value })\n .then((outcome) => {\n if (outcome.ok) {\n clearError(key)\n settle(coord, next)\n return\n }\n // Rejected: the session already rolled back — park the error on the\n // cell (never a toast) and return focus there.\n setErrors((prev) => ({ ...prev, [key]: cellErrorMessage(outcome.error) }))\n settle(coord, null)\n })\n .finally(() => {\n setBusyKey((prev) => (prev === key ? null : prev))\n })\n },\n [source, collection, clearError, settle],\n )\n\n return {\n containerRef,\n activeKey,\n busyKey,\n errorOf: (key) => errors[key] ?? null,\n start,\n cancel,\n commit,\n }\n}\n\n/**\n * The cell's no-op guard: THE equality verdict (EC-586, `semanticValueEqual` over\n * canonical forms — the same verdict the \"has unpublished changes\" dot reads), so a\n * commit the store would judge \"no change\" is not written either.\n */\nconst sameCellValue = semanticValueEqual\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { QueryKey } from '@tanstack/react-query';
|
|
2
|
-
import type { CmsDocument,
|
|
2
|
+
import type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core';
|
|
3
3
|
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
4
4
|
import type { DiscardDraftStatus } from './discard-draft.js';
|
|
5
5
|
export interface DiscardController {
|
|
@@ -34,7 +34,6 @@ export declare function useDiscardController(input: {
|
|
|
34
34
|
publishedSnapshotDoc: CmsDocument | undefined;
|
|
35
35
|
publishedVersionNumber: number | undefined;
|
|
36
36
|
versionKey: QueryKey;
|
|
37
|
-
collection: CollectionDef;
|
|
38
37
|
draft: CmsDocument;
|
|
39
38
|
source: CollectionViewSource;
|
|
40
39
|
docRef: DocumentRef;
|
|
@@ -16,7 +16,7 @@ import { useRestoreVersion } from './use-restore-version.js';
|
|
|
16
16
|
* Versions pane's publish gate uses, so the two never contradict (EC-298).
|
|
17
17
|
*/
|
|
18
18
|
export function useDiscardController(input) {
|
|
19
|
-
const { canDiscard, published, viewReadOnly, publishedSnapshotDoc, publishedVersionNumber, versionKey,
|
|
19
|
+
const { canDiscard, published, viewReadOnly, publishedSnapshotDoc, publishedVersionNumber, versionKey, draft, source, docRef, replaceDraft, dirty, save, } = input;
|
|
20
20
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
21
21
|
const { restore, busy } = useRestoreVersion({ source, docRef, versionKey, dirty, save, replaceDraft });
|
|
22
22
|
const status = discardDraftStatus({
|
|
@@ -24,7 +24,6 @@ export function useDiscardController(input) {
|
|
|
24
24
|
published,
|
|
25
25
|
viewReadOnly,
|
|
26
26
|
publishedSnapshot: publishedSnapshotDoc,
|
|
27
|
-
collection,
|
|
28
27
|
draft,
|
|
29
28
|
});
|
|
30
29
|
const disabledReason = status === 'view-read-only'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-discard-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-discard-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAI7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAezD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,
|
|
1
|
+
{"version":3,"file":"use-discard-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-discard-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAI7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAezD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAapC;IACC,MAAM,EACJ,UAAU,EACV,SAAS,EACT,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,KAAK,EACL,MAAM,EACN,MAAM,EACN,YAAY,EACZ,KAAK,EACL,IAAI,GACL,GAAG,KAAK,CAAA;IAET,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACrD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAA;IAEtG,MAAM,MAAM,GAAG,kBAAkB,CAAC;QAChC,UAAU;QACV,SAAS;QACT,YAAY;QACZ,iBAAiB,EAAE,oBAAoB;QACvC,KAAK;KACN,CAAC,CAAA;IACF,MAAM,cAAc,GAClB,MAAM,KAAK,gBAAgB;QACzB,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC;QAChD,CAAC,CAAC,MAAM,KAAK,YAAY;YACvB,CAAC,CAAC,SAAS,CAAC,qCAAqC,CAAC;YAClD,CAAC,CAAC,MAAM,KAAK,SAAS;gBACpB,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC;gBAC7B,CAAC,CAAC,SAAS,CAAA;IAEnB,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACrC,IAAI,sBAAsB,KAAK,SAAS;YAAE,OAAM;QAChD,IAAI,CAAC;YACH,4EAA4E;YAC5E,+EAA+E;YAC/E,MAAM,OAAO,CAAC,sBAAsB,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACxD,CAAC;gBAAS,CAAC;YACT,cAAc,CAAC,KAAK,CAAC,CAAA;QACvB,CAAC;IACH,CAAC,EAAE,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC,CAAA;IAErC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC/E,CAAC","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { QueryKey } from '@tanstack/react-query'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { discardDraftStatus } from './discard-draft'\nimport type { DiscardDraftStatus } from './discard-draft'\nimport { useRestoreVersion } from './use-restore-version'\n\nexport interface DiscardController {\n status: DiscardDraftStatus\n /** The hint shown when the item is disabled (view-read-only / up-to-date / loading). */\n disabledReason: string | undefined\n /** Is the confirmation dialog open? */\n confirmOpen: boolean\n setConfirmOpen: (open: boolean) => void\n /** Is a discard restore round-tripping (gates the dialog buttons)? */\n busy: boolean\n /** Confirm the discard: flush → restore the pinned published version whole. */\n discard: () => Promise<void>\n}\n\n/**\n * EC-316 / EC-325 — the \"Discard changes\" controller. Reverting the draft to the\n * published content IS a whole-document restore of the pinned published version,\n * so it routes through the SAME shared restore sequence\n * ({@link useRestoreVersion}) the Compare view's and Versions pane's restores use\n * — never a parallel write path. That sequence flushes a dirty in-memory buffer\n * FIRST (`documents.restore` snapshots whatever is currently PERSISTED), closing\n * the undo-guarantee gap the header's discard used to have.\n *\n * The enablement is the pure `discardDraftStatus` decision (tested in\n * `discard-draft.test.ts`); its semantic-equality edge is the SAME comparator the\n * Versions pane's publish gate uses, so the two never contradict (EC-298).\n */\nexport function useDiscardController(input: {\n canDiscard: boolean\n published: boolean\n viewReadOnly: boolean\n publishedSnapshotDoc: CmsDocument | undefined\n publishedVersionNumber: number | undefined\n versionKey: QueryKey\n draft: CmsDocument\n source: CollectionViewSource\n docRef: DocumentRef\n replaceDraft: (doc: CmsDocument) => void\n dirty: boolean\n save: () => void\n}): DiscardController {\n const {\n canDiscard,\n published,\n viewReadOnly,\n publishedSnapshotDoc,\n publishedVersionNumber,\n versionKey,\n draft,\n source,\n docRef,\n replaceDraft,\n dirty,\n save,\n } = input\n\n const [confirmOpen, setConfirmOpen] = useState(false)\n const { restore, busy } = useRestoreVersion({ source, docRef, versionKey, dirty, save, replaceDraft })\n\n const status = discardDraftStatus({\n canDiscard,\n published,\n viewReadOnly,\n publishedSnapshot: publishedSnapshotDoc,\n draft,\n })\n const disabledReason =\n status === 'view-read-only'\n ? translate('cms.editor.switchToDraftToDiscard')\n : status === 'up-to-date'\n ? translate('cms.editor.discardDraftUpToDateHint')\n : status === 'loading'\n ? translate('common.loading')\n : undefined\n\n const discard = useCallback(async () => {\n if (publishedVersionNumber === undefined) return\n try {\n // Whole-document restore of the pinned published version (the shared helper\n // flushes any dirty buffer first, mirroring the Versions pane / Compare view).\n await restore(publishedVersionNumber, { kind: 'all' })\n } finally {\n setConfirmOpen(false)\n }\n }, [publishedVersionNumber, restore])\n\n return { status, disabledReason, confirmOpen, setConfirmOpen, busy, discard }\n}\n"]}
|
|
@@ -42,8 +42,9 @@ export function usePublishController(input) {
|
|
|
42
42
|
// AT PUBLISH TIME — into the draft AND the snapshot, so both go clean
|
|
43
43
|
// together. Also captures the latest edit if Publish fires inside the
|
|
44
44
|
// autosave debounce window; the serial persister runs the upsert before the
|
|
45
|
-
// publish so the snapshot is this draft.
|
|
46
|
-
|
|
45
|
+
// publish so the snapshot is this draft. EC-580: judged in the SESSION's
|
|
46
|
+
// locale views — the same ones the form showed and the gate validated.
|
|
47
|
+
const cleaned = cleanHiddenForPublish(collection, draft, undefined, source.localeConfig);
|
|
47
48
|
if (cleaned !== draft)
|
|
48
49
|
setDraft(cleaned);
|
|
49
50
|
if (cleaned !== draft || dirty)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-publish-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-publish-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAGxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAItD;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAK3B;IACC,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,CAAA;IAC1F,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY;QAC9B,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC;QAChD,CAAC,CAAC,KAAK,CAAC,SAAS;YACf,CAAC,CAAC,SAAS,CACP,KAAK,CAAC,UAAU,KAAK,CAAC;gBACpB,CAAC,CAAC,mCAAmC;gBACrC,CAAC,CAAC,qCAAqC,EACzC,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE,CAC5B;YACH,CAAC,CAAC,SAAS,CAAA;IACf,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;AAC5B,CAAC;AAgBD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KASpC;IACC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAA;IACnG,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAe,MAAM,CAAC,CAAA;IACtE,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3D,oFAAoF;IACpF,oFAAoF;IACpF,MAAM,oBAAoB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAE1C,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACvC,8EAA8E;QAC9E,6EAA6E;QAC7E,sEAAsE;QACtE,sEAAsE;QACtE,4EAA4E;QAC5E,
|
|
1
|
+
{"version":3,"file":"use-publish-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-publish-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { cleanHiddenForPublish } from './publish-clean'\nimport { toast } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\nexport type PublishState = 'idle' | 'pending' | 'success'\n\n/**\n * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the\n * primary \"Publish\" and the \"Publish & close\" item can never diverge. A pure\n * decision (directly testable): the Published perspective is read-only (switch to\n * Draft to publish), schema errors block (with a count-aware hint), and an\n * in-flight publish disables re-entry.\n */\nexport function publishGate(input: {\n hasErrors: boolean\n viewReadOnly: boolean\n publishState: PublishState\n errorCount: number\n}): { disabled: boolean; title: string | undefined } {\n const disabled = input.hasErrors || input.viewReadOnly || input.publishState === 'pending'\n const title = input.viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : input.hasErrors\n ? translate(\n input.errorCount === 1\n ? 'cms.editor.fixIssuesToPublish.one'\n : 'cms.editor.fixIssuesToPublish.other',\n { count: input.errorCount },\n )\n : undefined\n return { disabled, title }\n}\n\nexport interface PublishController {\n publishState: PublishState\n /** Is the confirm-through dialog (unpublished hard refs) open? */\n publishConfirm: boolean\n /** Publish (opening the confirm-through dialog first if there are unpublished refs). */\n handlePublish: () => void\n /** Publish and, on success, return to the rail's list (0.0.39 \"Publish & close\"). */\n handlePublishAndClose: () => void\n /** Confirm-through: publish despite the unpublished hard references. */\n confirmPublish: () => void\n /** Dismiss the confirm-through dialog (also clears a pending \"& close\" intent). */\n cancelPublishConfirm: () => void\n}\n\n/**\n * EC-268 / EC-300 / 0.0.39 — the publish controller. Owns the publish indicator\n * state, the EC-300 confirm-through guard (unpublished hard references would render\n * empty on the live site, so publishing confirms rather than blocks), and the\n * 0.0.39 \"Publish & close\" navigation (remembered across the confirm dialog via a\n * ref, so a successful publish returns to the rail-owned list).\n */\nexport function usePublishController(input: {\n source: CollectionViewSource\n docRef: DocumentRef\n collection: CollectionDef\n projectId: string\n draft: CmsDocument\n setDraft: (doc: CmsDocument) => void\n dirty: boolean\n hasUnpublishedRefs: boolean\n}): PublishController {\n const { source, docRef, collection, projectId, draft, setDraft, dirty, hasUnpublishedRefs } = input\n const navigate = useNavigate()\n const [publishState, setPublishState] = useState<PublishState>('idle')\n const [publishConfirm, setPublishConfirm] = useState(false)\n // The split-button \"Publish & close\" intent — remembered across the confirm-through\n // dialog so a successful publish returns to the list. Cleared on cancel or failure.\n const closeAfterPublishRef = useRef(false)\n\n const doPublish = useCallback(async () => {\n // EC-273/EC-394 via the SHARED clean step (EC-425, also run by bulk publish):\n // clean conditionally-hidden field values AND component props from the draft\n // AT PUBLISH TIME — into the draft AND the snapshot, so both go clean\n // together. Also captures the latest edit if Publish fires inside the\n // autosave debounce window; the serial persister runs the upsert before the\n // publish so the snapshot is this draft. EC-580: judged in the SESSION's\n // locale views — the same ones the form showed and the gate validated.\n const cleaned = cleanHiddenForPublish(collection, draft, undefined, source.localeConfig)\n if (cleaned !== draft) setDraft(cleaned)\n if (cleaned !== draft || dirty) source.save(cleaned)\n setPublishState('pending')\n try {\n await source.publishing?.publish(docRef)\n setPublishState('success')\n setTimeout(() => setPublishState('idle'), 1800)\n // \"Publish & close\": return to the list once the publish landed (never on a\n // failure). Rail-owned `listPath`, falling back to the project index (AD-19).\n if (closeAfterPublishRef.current) {\n closeAfterPublishRef.current = false\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: source.listPath ?? studioPath.index() },\n })\n }\n } catch (error) {\n closeAfterPublishRef.current = false\n setPublishState('idle')\n toast.error(\n error instanceof Error\n ? translate('cms.editor.publishFailedReason', { error: error.message })\n : translate('cms.editor.publishFailed'),\n )\n }\n }, [collection, draft, setDraft, dirty, source, docRef, navigate, projectId])\n\n // Gate publish on the confirm-through dialog when there are unpublished hard\n // references; schema errors already disable the button, so they never reach here.\n const handlePublish = useCallback(() => {\n if (hasUnpublishedRefs) {\n setPublishConfirm(true)\n return\n }\n void doPublish()\n }, [hasUnpublishedRefs, doPublish])\n\n const handlePublishAndClose = useCallback(() => {\n closeAfterPublishRef.current = true\n handlePublish()\n }, [handlePublish])\n\n const confirmPublish = useCallback(() => {\n setPublishConfirm(false)\n void doPublish()\n }, [doPublish])\n\n const cancelPublishConfirm = useCallback(() => {\n setPublishConfirm(false)\n closeAfterPublishRef.current = false\n }, [])\n\n return {\n publishState,\n publishConfirm,\n handlePublish,\n handlePublishAndClose,\n confirmPublish,\n cancelPublishConfirm,\n }\n}\n"]}
|
|
@@ -29,7 +29,7 @@ import { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versio
|
|
|
29
29
|
*/
|
|
30
30
|
export function VersionsPane() {
|
|
31
31
|
const source = useCollectionView();
|
|
32
|
-
const {
|
|
32
|
+
const { docRef, draft, dirty, hasErrors, viewReadOnly, save, replaceDraft, compare, compareActive, startCompare, setCompareBase, } = useDocumentEditor();
|
|
33
33
|
const [busy, setBusy] = useState(false);
|
|
34
34
|
const published = source.publishing?.isPublished(docRef) ?? false;
|
|
35
35
|
const cache = useOperationCache();
|
|
@@ -67,7 +67,7 @@ export function VersionsPane() {
|
|
|
67
67
|
// SEMANTIC equality (EC-298) — the same engine the compare view uses, so the two
|
|
68
68
|
// never contradict: a doc that only differs by EC-277 envelope drift reads as
|
|
69
69
|
// "up to date", not "unpublished changes".
|
|
70
|
-
const liveMatchesDraft = liveSnapshot !== undefined && documentsSemanticEqual(
|
|
70
|
+
const liveMatchesDraft = liveSnapshot !== undefined && documentsSemanticEqual(liveSnapshot, draft);
|
|
71
71
|
const canPublish = !liveMatchesDraft;
|
|
72
72
|
const handlePublish = useCallback(async () => {
|
|
73
73
|
setBusy(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"versions-pane.js","sourceRoot":"","sources":["../../../src/components/cms/versions-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { KeyboardEvent } from 'react'\nimport { Eye, GitCompareArrows, MoreHorizontal, RotateCcw } from 'lucide-react'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n StatusDot,\n Text,\n} from '@elytracms/studio/ui'\nimport { documentsSemanticEqual } from '@elytracms/core/content'\nimport { useOperationCache } from '@elytracms/studio/lib/query'\nimport { translate, useUiLocale } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { useDocumentEditor } from './document-editor-context'\nimport { useDocumentVersionsQuery } from './use-document-versions'\nimport { useRestoreVersion } from './use-restore-version'\nimport { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versions-model'\n\n/**\n * EC-224 — the document-level **Versions** inspector pane. Re-homes the EC-180\n * VersionsPanel at the document level (the Sanity model): the continuous draft\n * on top with **Publish**, then the recorded version history (the live pin\n * badged), each older version **restorable** into the draft.\n *\n * Publish routes through the same pointer op as the header control\n * (`cms.publishDocument`); restore loads a recorded snapshot as the new working\n * draft (the operation snapshots the current draft first, so a rollback is\n * itself undoable) and the open editor adopts it via `replaceDraft`. Unpublish\n * deliberately stays in the header — it routes through the EC-173\n * reference-aware confirmation, which this pane does not duplicate.\n *\n * EC-298: each row also opens the compare view (⤢) and, once comparing, doubles\n * as the navigator — clicking a row sets it as the base of the diff.\n */\nexport function VersionsPane() {\n const source = useCollectionView()\n const {\n collection,\n docRef,\n draft,\n dirty,\n hasErrors,\n viewReadOnly,\n save,\n replaceDraft,\n compare,\n compareActive,\n startCompare,\n setCompareBase,\n } = useDocumentEditor()\n const [busy, setBusy] = useState(false)\n const published = source.publishing?.isPublished(docRef) ?? false\n const cache = useOperationCache()\n // EC-446 — the active studio locale + a render-time clock, so each row can show\n // a locale-aware RELATIVE timestamp (\"vor 3 Tagen\") with the absolute value on\n // hover. `now` is captured per render (good enough — the list re-renders on any\n // version mutation; a version's age doesn't need per-second reactivity).\n const locale = useUiLocale()\n const now = Date.now()\n\n // The recorded version history, read through the one shared query (EC-308/AD-17,\n // deduped with the compare view). The flush-before-read ordering lives in the\n // loader, so the listed history is authoritative regardless of which surface\n // triggered the change; it refreshes on any version-mutating command\n // (publish/unpublish/snapshot/restore/pin/delete — EC-322) from anywhere, but\n // NOT on a plain draft save (which appends no version). Editor draft/dirty/busy\n // stays local — it is not server state (principle 12).\n const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef)\n const view = versionsQuery.data ?? null\n // EC-325 — the shared restore sequence (flush → restore → replaceDraft →\n // invalidate). Its `restoreBusy` is OR-ed with the local publish `busy` below so\n // the pane keeps its original single-in-flight gate: while EITHER a publish or a\n // restore is round-tripping, all of the pane's actions disable together.\n const { restore: restoreVersion, busy: restoreBusy } = useRestoreVersion({\n source,\n docRef,\n versionKey,\n dirty,\n save,\n replaceDraft,\n })\n const anyBusy = busy || restoreBusy\n\n const liveSnapshot =\n view?.publishedVersion !== undefined\n ? view.versions.find((v) => v.version === view.publishedVersion)?.snapshot\n : undefined\n // SEMANTIC equality (EC-298) — the same engine the compare view uses, so the two\n // never contradict: a doc that only differs by EC-277 envelope drift reads as\n // \"up to date\", not \"unpublished changes\".\n const liveMatchesDraft =\n liveSnapshot !== undefined && documentsSemanticEqual(collection, liveSnapshot, draft)\n const canPublish = !liveMatchesDraft\n\n const handlePublish = useCallback(async () => {\n setBusy(true)\n try {\n // Publish snapshots the SAVED draft — capture any unsaved edits first so\n // the published version is exactly what the editor shows. Await the\n // publish (it flushes + reconciles the pin, EC-225) before re-reading the\n // version list so the new version shows.\n if (dirty) save()\n await source.publishing?.publish(docRef)\n await cache.invalidate(versionKey)\n } finally {\n setBusy(false)\n }\n }, [source, docRef, dirty, save, cache, versionKey])\n\n // Restore a recorded version into the working draft — the whole-document\n // restore (no scope) via the shared EC-325 helper, which flushes any unsaved\n // buffer FIRST so the operation's pre-restore snapshot records it (else the\n // rollback wouldn't be undoable — restore snapshots the persisted draft).\n const handleRestore = useCallback(\n (version: number) => restoreVersion(version),\n [restoreVersion],\n )\n\n return (\n <Stack gap=\"3\" className=\"p-3\">\n {canPublish ? (\n <Row gap=\"2\" align=\"center\" className=\"rounded-md border border-primary/40 bg-primary/5 px-2.5 py-2\">\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text size=\"sm\" weight=\"medium\">\n {translate('cms.editor.draftState')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {published\n ? translate('cms.version.unpublishedChanges')\n : translate('cms.version.notPublishedYet')}\n </Text>\n </Stack>\n <Spacer />\n <Button\n size=\"sm\"\n onClick={handlePublish}\n disabled={anyBusy || hasErrors || viewReadOnly}\n title={\n viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : hasErrors\n ? translate('cms.version.fixValidationToPublish')\n : undefined\n }\n >\n <Eye /> {translate('cms.editor.publish')}\n </Button>\n </Row>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.liveUpToDate')}\n </Text>\n )}\n\n {compareActive && compare ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.comparingHint', {\n base: compare.base,\n target:\n compare.target === 'draft'\n ? translate('cms.version.currentDraft')\n : `v${compare.target}`,\n })}\n </Text>\n ) : null}\n\n <Stack gap=\"1\">\n {view === null ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('common.loading')}\n </Text>\n ) : view.versions.length === 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.noVersionsYet')}\n </Text>\n ) : (\n view.versions.map((entry) => {\n const isLive = entry.version === view.publishedVersion\n const isBase = compareActive && compare?.base === entry.version\n const isTarget = compareActive && compare?.target === entry.version\n const selectable = compareActive && !isTarget\n return (\n <Row\n key={entry.version}\n gap=\"2\"\n align=\"center\"\n {...(selectable\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: () => setCompareBase(entry.version),\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n setCompareBase(entry.version)\n }\n },\n }\n : {})}\n className={cn(\n 'rounded-md border px-2.5 py-1.5',\n isBase ? 'border-primary bg-primary/5' : 'border-border/60',\n selectable && !isBase && 'cursor-pointer hover:bg-muted/60',\n )}\n >\n {/* EC-446 — the live/published version leads with a green STATUS\n DOT (hover/focus reveals \"Published\"); historical versions\n reserve the same footprint so the version codes stay aligned. */}\n {isLive ? (\n <StatusDot tone=\"success\" label={translate('cms.editor.publishedState')} />\n ) : (\n <span aria-hidden className=\"size-2 shrink-0\" />\n )}\n <Code tone={isLive ? 'primary' : 'muted'}>v{entry.version}</Code>\n {isBase ? (\n <Badge variant=\"outline\">{translate('cms.version.baseBadge')}</Badge>\n ) : isTarget ? (\n <Badge variant=\"outline\">{translate('cms.version.comparedBadge')}</Badge>\n ) : null}\n {entry.origin === 'assistant' ? (\n <Badge variant=\"outline\" title={translate('cms.version.aiAuthoredTitle')}>\n {translate('cms.version.aiAuthored')}\n </Badge>\n ) : null}\n {/* EC-446 — readable RELATIVE date; the full absolute timestamp\n on hover (`title`), replacing the old truncated stub. */}\n <Text\n size=\"xs\"\n tone=\"subtle\"\n className=\"truncate\"\n title={formatVersionTimestamp(entry.createdAt)}\n >\n {formatRelativeVersionTimestamp(entry.createdAt, now, locale)}\n </Text>\n <Spacer />\n {/* EC-446 — actions collapse into a per-row `⋯` overflow menu\n (mirrors the collection-table row menu). Suppressed in compare\n mode exactly as the buttons were, so the row stays a base picker. */}\n {compareActive ? null : (\n <VersionRowActions\n version={entry.version}\n isLive={isLive}\n restoreDisabled={anyBusy || viewReadOnly}\n restoreDisabledTitle={\n viewReadOnly ? translate('cms.version.switchToDraftToRestore') : undefined\n }\n onCompare={() => startCompare(entry.version)}\n onRestore={() => void handleRestore(entry.version)}\n />\n )}\n </Row>\n )\n })\n )}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * EC-446 — one version row's `⋯` overflow menu, mirroring the collection-table\n * row-actions menu (`RowActionsMenu`, built on the same `DropdownMenu`\n * primitive). Compare (⤢) is always offered; Restore (↺) is offered on every\n * version EXCEPT the live/published one (which is already the draft's baseline —\n * matching the pre-redesign behavior). The live version therefore shows a\n * Compare-only menu, never a bare/empty trigger.\n *\n * Restore keeps the pane's original gates: `restoreDisabled` (the single\n * in-flight gate `anyBusy` OR `viewReadOnly`) disables the item, and the\n * read-only hint rides its `title`.\n */\nfunction VersionRowActions({\n version,\n isLive,\n restoreDisabled,\n restoreDisabledTitle,\n onCompare,\n onRestore,\n}: {\n version: number\n isLive: boolean\n restoreDisabled: boolean\n restoreDisabledTitle: string | undefined\n onCompare: () => void\n onRestore: () => void\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.version.rowActionsLabel', { version })}\n data-testid={`version-actions-${version}`}\n >\n <MoreHorizontal />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n onClick={onCompare}\n data-testid={`version-action-compare-${version}`}\n >\n <GitCompareArrows /> {translate('cms.version.compare')}\n </DropdownMenuItem>\n {isLive ? null : (\n <DropdownMenuItem\n onClick={onRestore}\n disabled={restoreDisabled}\n {...(restoreDisabledTitle ? { title: restoreDisabledTitle } : {})}\n data-testid={`version-action-restore-${version}`}\n >\n <RotateCcw /> {translate('cms.version.restore')}\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"versions-pane.js","sourceRoot":"","sources":["../../../src/components/cms/versions-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { KeyboardEvent } from 'react'\nimport { Eye, GitCompareArrows, MoreHorizontal, RotateCcw } from 'lucide-react'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n StatusDot,\n Text,\n} from '@elytracms/studio/ui'\nimport { documentsSemanticEqual } from '@elytracms/core/content'\nimport { useOperationCache } from '@elytracms/studio/lib/query'\nimport { translate, useUiLocale } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { useDocumentEditor } from './document-editor-context'\nimport { useDocumentVersionsQuery } from './use-document-versions'\nimport { useRestoreVersion } from './use-restore-version'\nimport { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versions-model'\n\n/**\n * EC-224 — the document-level **Versions** inspector pane. Re-homes the EC-180\n * VersionsPanel at the document level (the Sanity model): the continuous draft\n * on top with **Publish**, then the recorded version history (the live pin\n * badged), each older version **restorable** into the draft.\n *\n * Publish routes through the same pointer op as the header control\n * (`cms.publishDocument`); restore loads a recorded snapshot as the new working\n * draft (the operation snapshots the current draft first, so a rollback is\n * itself undoable) and the open editor adopts it via `replaceDraft`. Unpublish\n * deliberately stays in the header — it routes through the EC-173\n * reference-aware confirmation, which this pane does not duplicate.\n *\n * EC-298: each row also opens the compare view (⤢) and, once comparing, doubles\n * as the navigator — clicking a row sets it as the base of the diff.\n */\nexport function VersionsPane() {\n const source = useCollectionView()\n const {\n docRef,\n draft,\n dirty,\n hasErrors,\n viewReadOnly,\n save,\n replaceDraft,\n compare,\n compareActive,\n startCompare,\n setCompareBase,\n } = useDocumentEditor()\n const [busy, setBusy] = useState(false)\n const published = source.publishing?.isPublished(docRef) ?? false\n const cache = useOperationCache()\n // EC-446 — the active studio locale + a render-time clock, so each row can show\n // a locale-aware RELATIVE timestamp (\"vor 3 Tagen\") with the absolute value on\n // hover. `now` is captured per render (good enough — the list re-renders on any\n // version mutation; a version's age doesn't need per-second reactivity).\n const locale = useUiLocale()\n const now = Date.now()\n\n // The recorded version history, read through the one shared query (EC-308/AD-17,\n // deduped with the compare view). The flush-before-read ordering lives in the\n // loader, so the listed history is authoritative regardless of which surface\n // triggered the change; it refreshes on any version-mutating command\n // (publish/unpublish/snapshot/restore/pin/delete — EC-322) from anywhere, but\n // NOT on a plain draft save (which appends no version). Editor draft/dirty/busy\n // stays local — it is not server state (principle 12).\n const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef)\n const view = versionsQuery.data ?? null\n // EC-325 — the shared restore sequence (flush → restore → replaceDraft →\n // invalidate). Its `restoreBusy` is OR-ed with the local publish `busy` below so\n // the pane keeps its original single-in-flight gate: while EITHER a publish or a\n // restore is round-tripping, all of the pane's actions disable together.\n const { restore: restoreVersion, busy: restoreBusy } = useRestoreVersion({\n source,\n docRef,\n versionKey,\n dirty,\n save,\n replaceDraft,\n })\n const anyBusy = busy || restoreBusy\n\n const liveSnapshot =\n view?.publishedVersion !== undefined\n ? view.versions.find((v) => v.version === view.publishedVersion)?.snapshot\n : undefined\n // SEMANTIC equality (EC-298) — the same engine the compare view uses, so the two\n // never contradict: a doc that only differs by EC-277 envelope drift reads as\n // \"up to date\", not \"unpublished changes\".\n const liveMatchesDraft =\n liveSnapshot !== undefined && documentsSemanticEqual(liveSnapshot, draft)\n const canPublish = !liveMatchesDraft\n\n const handlePublish = useCallback(async () => {\n setBusy(true)\n try {\n // Publish snapshots the SAVED draft — capture any unsaved edits first so\n // the published version is exactly what the editor shows. Await the\n // publish (it flushes + reconciles the pin, EC-225) before re-reading the\n // version list so the new version shows.\n if (dirty) save()\n await source.publishing?.publish(docRef)\n await cache.invalidate(versionKey)\n } finally {\n setBusy(false)\n }\n }, [source, docRef, dirty, save, cache, versionKey])\n\n // Restore a recorded version into the working draft — the whole-document\n // restore (no scope) via the shared EC-325 helper, which flushes any unsaved\n // buffer FIRST so the operation's pre-restore snapshot records it (else the\n // rollback wouldn't be undoable — restore snapshots the persisted draft).\n const handleRestore = useCallback(\n (version: number) => restoreVersion(version),\n [restoreVersion],\n )\n\n return (\n <Stack gap=\"3\" className=\"p-3\">\n {canPublish ? (\n <Row gap=\"2\" align=\"center\" className=\"rounded-md border border-primary/40 bg-primary/5 px-2.5 py-2\">\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text size=\"sm\" weight=\"medium\">\n {translate('cms.editor.draftState')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {published\n ? translate('cms.version.unpublishedChanges')\n : translate('cms.version.notPublishedYet')}\n </Text>\n </Stack>\n <Spacer />\n <Button\n size=\"sm\"\n onClick={handlePublish}\n disabled={anyBusy || hasErrors || viewReadOnly}\n title={\n viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : hasErrors\n ? translate('cms.version.fixValidationToPublish')\n : undefined\n }\n >\n <Eye /> {translate('cms.editor.publish')}\n </Button>\n </Row>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.liveUpToDate')}\n </Text>\n )}\n\n {compareActive && compare ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.comparingHint', {\n base: compare.base,\n target:\n compare.target === 'draft'\n ? translate('cms.version.currentDraft')\n : `v${compare.target}`,\n })}\n </Text>\n ) : null}\n\n <Stack gap=\"1\">\n {view === null ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('common.loading')}\n </Text>\n ) : view.versions.length === 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.noVersionsYet')}\n </Text>\n ) : (\n view.versions.map((entry) => {\n const isLive = entry.version === view.publishedVersion\n const isBase = compareActive && compare?.base === entry.version\n const isTarget = compareActive && compare?.target === entry.version\n const selectable = compareActive && !isTarget\n return (\n <Row\n key={entry.version}\n gap=\"2\"\n align=\"center\"\n {...(selectable\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: () => setCompareBase(entry.version),\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n setCompareBase(entry.version)\n }\n },\n }\n : {})}\n className={cn(\n 'rounded-md border px-2.5 py-1.5',\n isBase ? 'border-primary bg-primary/5' : 'border-border/60',\n selectable && !isBase && 'cursor-pointer hover:bg-muted/60',\n )}\n >\n {/* EC-446 — the live/published version leads with a green STATUS\n DOT (hover/focus reveals \"Published\"); historical versions\n reserve the same footprint so the version codes stay aligned. */}\n {isLive ? (\n <StatusDot tone=\"success\" label={translate('cms.editor.publishedState')} />\n ) : (\n <span aria-hidden className=\"size-2 shrink-0\" />\n )}\n <Code tone={isLive ? 'primary' : 'muted'}>v{entry.version}</Code>\n {isBase ? (\n <Badge variant=\"outline\">{translate('cms.version.baseBadge')}</Badge>\n ) : isTarget ? (\n <Badge variant=\"outline\">{translate('cms.version.comparedBadge')}</Badge>\n ) : null}\n {entry.origin === 'assistant' ? (\n <Badge variant=\"outline\" title={translate('cms.version.aiAuthoredTitle')}>\n {translate('cms.version.aiAuthored')}\n </Badge>\n ) : null}\n {/* EC-446 — readable RELATIVE date; the full absolute timestamp\n on hover (`title`), replacing the old truncated stub. */}\n <Text\n size=\"xs\"\n tone=\"subtle\"\n className=\"truncate\"\n title={formatVersionTimestamp(entry.createdAt)}\n >\n {formatRelativeVersionTimestamp(entry.createdAt, now, locale)}\n </Text>\n <Spacer />\n {/* EC-446 — actions collapse into a per-row `⋯` overflow menu\n (mirrors the collection-table row menu). Suppressed in compare\n mode exactly as the buttons were, so the row stays a base picker. */}\n {compareActive ? null : (\n <VersionRowActions\n version={entry.version}\n isLive={isLive}\n restoreDisabled={anyBusy || viewReadOnly}\n restoreDisabledTitle={\n viewReadOnly ? translate('cms.version.switchToDraftToRestore') : undefined\n }\n onCompare={() => startCompare(entry.version)}\n onRestore={() => void handleRestore(entry.version)}\n />\n )}\n </Row>\n )\n })\n )}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * EC-446 — one version row's `⋯` overflow menu, mirroring the collection-table\n * row-actions menu (`RowActionsMenu`, built on the same `DropdownMenu`\n * primitive). Compare (⤢) is always offered; Restore (↺) is offered on every\n * version EXCEPT the live/published one (which is already the draft's baseline —\n * matching the pre-redesign behavior). The live version therefore shows a\n * Compare-only menu, never a bare/empty trigger.\n *\n * Restore keeps the pane's original gates: `restoreDisabled` (the single\n * in-flight gate `anyBusy` OR `viewReadOnly`) disables the item, and the\n * read-only hint rides its `title`.\n */\nfunction VersionRowActions({\n version,\n isLive,\n restoreDisabled,\n restoreDisabledTitle,\n onCompare,\n onRestore,\n}: {\n version: number\n isLive: boolean\n restoreDisabled: boolean\n restoreDisabledTitle: string | undefined\n onCompare: () => void\n onRestore: () => void\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.version.rowActionsLabel', { version })}\n data-testid={`version-actions-${version}`}\n >\n <MoreHorizontal />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n onClick={onCompare}\n data-testid={`version-action-compare-${version}`}\n >\n <GitCompareArrows /> {translate('cms.version.compare')}\n </DropdownMenuItem>\n {isLive ? null : (\n <DropdownMenuItem\n onClick={onRestore}\n disabled={restoreDisabled}\n {...(restoreDisabledTitle ? { title: restoreDisabledTitle } : {})}\n data-testid={`version-action-restore-${version}`}\n >\n <RotateCcw /> {translate('cms.version.restore')}\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The component detail view (EC-050/052, overhauled in EC-320) — now on the CMS
|
|
3
|
-
* shell: a header (title ·
|
|
3
|
+
* shell: a header (title · role/category badges · id · status), a lens row of
|
|
4
4
|
* tabs (Preview / Manifest / References — References-as-a-tab, EC-269), and, on the
|
|
5
5
|
* Preview tab, the live editable preview beside the shared right {@link InspectorSidebar}
|
|
6
6
|
* (Props / Slots). "Use in document…" transfers the current combination into a draft.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { ArrowLeft, FilePlus2, Link2, ListTree, Play, SlidersHorizontal, SquareStack } from 'lucide-react';
|
|
4
|
-
import { isPrimitive } from '@elytracms/core/component-registry';
|
|
5
4
|
import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
6
5
|
import { componentStatus, getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
7
6
|
import { ComponentStatusBadge, statusDescription, } from '@elytracms/studio/components/components/component-status';
|
|
@@ -16,7 +15,7 @@ import { Badge, Button, Code, Heading, Row, Spacer, Stack, TabButton, Text } fro
|
|
|
16
15
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
17
16
|
/**
|
|
18
17
|
* The component detail view (EC-050/052, overhauled in EC-320) — now on the CMS
|
|
19
|
-
* shell: a header (title ·
|
|
18
|
+
* shell: a header (title · role/category badges · id · status), a lens row of
|
|
20
19
|
* tabs (Preview / Manifest / References — References-as-a-tab, EC-269), and, on the
|
|
21
20
|
* Preview tab, the live editable preview beside the shared right {@link InspectorSidebar}
|
|
22
21
|
* (Props / Slots). "Use in document…" transfers the current combination into a draft.
|
|
@@ -37,7 +36,6 @@ function ComponentDetailBody({ projectId, studio, manifest, }) {
|
|
|
37
36
|
const [tab, setTab] = useState('preview');
|
|
38
37
|
const [transferOpen, setTransferOpen] = useState(false);
|
|
39
38
|
const status = componentStatus(studio, manifest);
|
|
40
|
-
const namespace = isPrimitive(manifest.id) ? 'base' : 'project';
|
|
41
39
|
const title = localizedLabel(manifest.title, manifest.id);
|
|
42
40
|
// The Preview tab's right inspector: editable Props + declared Slots. Shares the
|
|
43
41
|
// exact frame the document editor uses (EC-224), so a prop edit re-renders the
|
|
@@ -56,6 +54,6 @@ function ComponentDetailBody({ projectId, studio, manifest, }) {
|
|
|
56
54
|
render: () => _jsx(SlotsInfoPane, { manifest: manifest }),
|
|
57
55
|
},
|
|
58
56
|
];
|
|
59
|
-
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.components() }, className: "inline-flex items-center gap-1 px-4 pt-3 text-xs text-muted-foreground transition-colors hover:text-foreground md:hidden", children: [_jsx(ArrowLeft, { className: "size-3.5" }), " ", translate('components.detail.allComponents')] }), _jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0 flex-wrap", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: title }), _jsx(Badge, { variant:
|
|
57
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.components() }, className: "inline-flex items-center gap-1 px-4 pt-3 text-xs text-muted-foreground transition-colors hover:text-foreground md:hidden", children: [_jsx(ArrowLeft, { className: "size-3.5" }), " ", translate('components.detail.allComponents')] }), _jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0 flex-wrap", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: title }), manifest.role ? _jsx(Badge, { variant: "primary", children: manifest.role }) : null, manifest.category ? _jsx(Badge, { variant: "outline", children: manifest.category }) : null] }), _jsx(Spacer, {}), _jsx(ComponentStatusBadge, { status: status }), _jsxs(Button, { size: "sm", onClick: () => setTransferOpen(true), "data-testid": "use-in-document", children: [_jsx(FilePlus2, {}), " ", translate('components.transfer.action')] })] }), _jsx(Row, { gap: "2", align: "center", className: "mt-1", children: _jsx(Code, { tone: "muted", children: manifest.id }) }), manifest.description ? (_jsx(Text, { tone: "muted", size: "sm", className: "mt-1 max-w-prose", children: manifest.description })) : null, status !== 'valid' ? (_jsx(Text, { size: "sm", className: cn('mt-1', status === 'invalid' ? 'text-destructive' : 'text-warning'), children: statusDescription(status) })) : null, _jsxs(Row, { gap: "1", align: "center", className: "mt-3", children: [_jsxs(TabButton, { active: tab === 'preview', onClick: () => setTab('preview'), children: [_jsx(Play, { className: "size-3" }), translate('components.detail.tab.preview')] }), _jsxs(TabButton, { active: tab === 'manifest', onClick: () => setTab('manifest'), children: [_jsx(ListTree, { className: "size-3" }), translate('components.detail.tab.manifest')] }), _jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), _jsxs(TabButton, { active: tab === 'references', onClick: () => setTab('references'), children: [_jsx(Link2, { className: "size-3" }), translate('references.title')] })] })] }), tab === 'preview' ? (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(ComponentPreview, { manifest: manifest, studio: studio, state: preview }) }), _jsx(InspectorSidebar, { panes: panes, className: "w-[320px] shrink-0 border-l border-border" })] })) : tab === 'manifest' ? (_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-4xl", children: _jsx(FieldInspector, { manifest: manifest }) }) })) : (_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsx(ReferencesPanel, { projectId: projectId, target: { type: 'component', id: manifest.id } }) }) })), _jsx(TransferToDocumentDialog, { projectId: projectId, manifest: manifest, node: preview.node, open: transferOpen, onOpenChange: setTransferOpen })] }));
|
|
60
58
|
}
|
|
61
59
|
//# sourceMappingURL=component-detail.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-detail.js","sourceRoot":"","sources":["../../../src/components/components/component-detail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { ArrowLeft, FilePlus2, Link2, ListTree, Play, SlidersHorizontal, SquareStack } from 'lucide-react'\nimport { isPrimitive } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { componentStatus, getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n ComponentStatusBadge,\n statusDescription,\n} from '@elytracms/studio/components/components/component-status'\nimport { FieldInspector } from '@elytracms/studio/components/components/field-inspector'\nimport {\n ComponentPreview,\n PropsEditorPane,\n SlotsInfoPane,\n usePreviewState,\n} from '@elytracms/studio/components/components/component-preview'\nimport { TransferToDocumentDialog } from '@elytracms/studio/components/components/transfer-dialog'\nimport { ReferencesPanel } from '@elytracms/studio/components/references/references-panel'\nimport { InspectorSidebar } from '@elytracms/studio/components/inspector/inspector-sidebar'\nimport type { InspectorPane } from '@elytracms/studio/components/inspector/inspector-sidebar'\nimport { useSetBreadcrumb } from '@elytracms/studio/components/breadcrumb-context'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { Badge, Button, Code, Heading, Row, Spacer, Stack, TabButton, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\n\n/**\n * The component detail view (EC-050/052, overhauled in EC-320) — now on the CMS\n * shell: a header (title · namespace/category badges · id · status), a lens row of\n * tabs (Preview / Manifest / References — References-as-a-tab, EC-269), and, on the\n * Preview tab, the live editable preview beside the shared right {@link InspectorSidebar}\n * (Props / Slots). \"Use in document…\" transfers the current combination into a draft.\n */\nexport function ComponentDetail({\n projectId,\n componentId,\n}: {\n projectId: string\n componentId: string\n}) {\n const studio = getStudioRegistry()\n const manifest = studio.registry.getManifest(componentId)\n\n // EC-214: the component is the active record — name it in the global header\n // breadcrumb (under the Components section crumb). Unconditional (hooks rules).\n useSetBreadcrumb([{ label: localizedLabel(manifest?.title, manifest?.id ?? componentId) }])\n\n if (!manifest) {\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading level={2} size=\"h2\">\n {translate('components.detail.notFound.title')}\n </Heading>\n <Text tone=\"muted\">{translate('components.detail.notFound.body', { id: componentId })}</Text>\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.components() }}\n className=\"text-sm text-primary hover:underline\"\n >\n {translate('components.detail.notFound.back')}\n </Link>\n </Stack>\n </div>\n )\n }\n\n return <ComponentDetailBody key={manifest.id} projectId={projectId} studio={studio} manifest={manifest} />\n}\n\ntype DetailTab = 'preview' | 'manifest' | 'references'\n\nfunction ComponentDetailBody({\n projectId,\n studio,\n manifest,\n}: {\n projectId: string\n studio: StudioRegistry\n manifest: ComponentManifest\n}) {\n const preview = usePreviewState(manifest)\n const [tab, setTab] = useState<DetailTab>('preview')\n const [transferOpen, setTransferOpen] = useState(false)\n\n const status = componentStatus(studio, manifest)\n const namespace = isPrimitive(manifest.id) ? 'base' : 'project'\n const title = localizedLabel(manifest.title, manifest.id)\n\n // The Preview tab's right inspector: editable Props + declared Slots. Shares the\n // exact frame the document editor uses (EC-224), so a prop edit re-renders the\n // preview live through the same control set documents use.\n const panes: InspectorPane[] = [\n {\n id: 'props',\n label: translate('components.fieldInspector.props'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => <PropsEditorPane manifest={manifest} state={preview} />,\n },\n {\n id: 'slots',\n label: translate('components.fieldInspector.slots'),\n icon: <SquareStack className=\"size-3.5\" />,\n render: () => <SlotsInfoPane manifest={manifest} />,\n },\n ]\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n {/* Mobile-only back link (the registry sidebar is hidden under md). */}\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.components() }}\n className=\"inline-flex items-center gap-1 px-4 pt-3 text-xs text-muted-foreground transition-colors hover:text-foreground md:hidden\"\n >\n <ArrowLeft className=\"size-3.5\" /> {translate('components.detail.allComponents')}\n </Link>\n\n {/* Header */}\n <div className=\"shrink-0 border-b border-border px-4 py-3 sm:px-6\">\n <Row gap=\"3\">\n <Row gap=\"2\" align=\"center\" className=\"min-w-0 flex-wrap\">\n <Heading level={1} size=\"h2\" className=\"truncate\">\n {title}\n </Heading>\n <Badge variant={namespace === 'project' ? 'primary' : 'outline'}>{namespace}</Badge>\n {manifest.category ? <Badge variant=\"outline\">{manifest.category}</Badge> : null}\n </Row>\n <Spacer />\n <ComponentStatusBadge status={status} />\n <Button size=\"sm\" onClick={() => setTransferOpen(true)} data-testid=\"use-in-document\">\n <FilePlus2 /> {translate('components.transfer.action')}\n </Button>\n </Row>\n <Row gap=\"2\" align=\"center\" className=\"mt-1\">\n <Code tone=\"muted\">{manifest.id}</Code>\n </Row>\n {manifest.description ? (\n <Text tone=\"muted\" size=\"sm\" className=\"mt-1 max-w-prose\">\n {manifest.description}\n </Text>\n ) : null}\n {status !== 'valid' ? (\n <Text size=\"sm\" className={cn('mt-1', status === 'invalid' ? 'text-destructive' : 'text-warning')}>\n {statusDescription(status)}\n </Text>\n ) : null}\n\n {/* Lens row — the tab set (mirrors the document editor's lens row). */}\n <Row gap=\"1\" align=\"center\" className=\"mt-3\">\n <TabButton active={tab === 'preview'} onClick={() => setTab('preview')}>\n <Play className=\"size-3\" />\n {translate('components.detail.tab.preview')}\n </TabButton>\n <TabButton active={tab === 'manifest'} onClick={() => setTab('manifest')}>\n <ListTree className=\"size-3\" />\n {translate('components.detail.tab.manifest')}\n </TabButton>\n <div className=\"mx-1 h-4 w-px bg-border\" aria-hidden />\n <TabButton active={tab === 'references'} onClick={() => setTab('references')}>\n <Link2 className=\"size-3\" />\n {translate('references.title')}\n </TabButton>\n </Row>\n </div>\n\n {/* Body */}\n {tab === 'preview' ? (\n <div className=\"flex min-h-0 flex-1 overflow-hidden\">\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <ComponentPreview manifest={manifest} studio={studio} state={preview} />\n </div>\n <InspectorSidebar panes={panes} className=\"w-[320px] shrink-0 border-l border-border\" />\n </div>\n ) : tab === 'manifest' ? (\n <div className=\"min-h-0 flex-1 overflow-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-4xl\">\n <FieldInspector manifest={manifest} />\n </div>\n </div>\n ) : (\n // EC-172/EC-269: inbound references over the reference index, as a tab.\n <div className=\"min-h-0 flex-1 overflow-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-2xl\">\n <ReferencesPanel projectId={projectId} target={{ type: 'component', id: manifest.id }} />\n </div>\n </div>\n )}\n\n <TransferToDocumentDialog\n projectId={projectId}\n manifest={manifest}\n node={preview.node}\n open={transferOpen}\n onOpenChange={setTransferOpen}\n />\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"component-detail.js","sourceRoot":"","sources":["../../../src/components/components/component-detail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { ArrowLeft, FilePlus2, Link2, ListTree, Play, SlidersHorizontal, SquareStack } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { componentStatus, getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n ComponentStatusBadge,\n statusDescription,\n} from '@elytracms/studio/components/components/component-status'\nimport { FieldInspector } from '@elytracms/studio/components/components/field-inspector'\nimport {\n ComponentPreview,\n PropsEditorPane,\n SlotsInfoPane,\n usePreviewState,\n} from '@elytracms/studio/components/components/component-preview'\nimport { TransferToDocumentDialog } from '@elytracms/studio/components/components/transfer-dialog'\nimport { ReferencesPanel } from '@elytracms/studio/components/references/references-panel'\nimport { InspectorSidebar } from '@elytracms/studio/components/inspector/inspector-sidebar'\nimport type { InspectorPane } from '@elytracms/studio/components/inspector/inspector-sidebar'\nimport { useSetBreadcrumb } from '@elytracms/studio/components/breadcrumb-context'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { Badge, Button, Code, Heading, Row, Spacer, Stack, TabButton, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\n\n/**\n * The component detail view (EC-050/052, overhauled in EC-320) — now on the CMS\n * shell: a header (title · role/category badges · id · status), a lens row of\n * tabs (Preview / Manifest / References — References-as-a-tab, EC-269), and, on the\n * Preview tab, the live editable preview beside the shared right {@link InspectorSidebar}\n * (Props / Slots). \"Use in document…\" transfers the current combination into a draft.\n */\nexport function ComponentDetail({\n projectId,\n componentId,\n}: {\n projectId: string\n componentId: string\n}) {\n const studio = getStudioRegistry()\n const manifest = studio.registry.getManifest(componentId)\n\n // EC-214: the component is the active record — name it in the global header\n // breadcrumb (under the Components section crumb). Unconditional (hooks rules).\n useSetBreadcrumb([{ label: localizedLabel(manifest?.title, manifest?.id ?? componentId) }])\n\n if (!manifest) {\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading level={2} size=\"h2\">\n {translate('components.detail.notFound.title')}\n </Heading>\n <Text tone=\"muted\">{translate('components.detail.notFound.body', { id: componentId })}</Text>\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.components() }}\n className=\"text-sm text-primary hover:underline\"\n >\n {translate('components.detail.notFound.back')}\n </Link>\n </Stack>\n </div>\n )\n }\n\n return <ComponentDetailBody key={manifest.id} projectId={projectId} studio={studio} manifest={manifest} />\n}\n\ntype DetailTab = 'preview' | 'manifest' | 'references'\n\nfunction ComponentDetailBody({\n projectId,\n studio,\n manifest,\n}: {\n projectId: string\n studio: StudioRegistry\n manifest: ComponentManifest\n}) {\n const preview = usePreviewState(manifest)\n const [tab, setTab] = useState<DetailTab>('preview')\n const [transferOpen, setTransferOpen] = useState(false)\n\n const status = componentStatus(studio, manifest)\n const title = localizedLabel(manifest.title, manifest.id)\n\n // The Preview tab's right inspector: editable Props + declared Slots. Shares the\n // exact frame the document editor uses (EC-224), so a prop edit re-renders the\n // preview live through the same control set documents use.\n const panes: InspectorPane[] = [\n {\n id: 'props',\n label: translate('components.fieldInspector.props'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => <PropsEditorPane manifest={manifest} state={preview} />,\n },\n {\n id: 'slots',\n label: translate('components.fieldInspector.slots'),\n icon: <SquareStack className=\"size-3.5\" />,\n render: () => <SlotsInfoPane manifest={manifest} />,\n },\n ]\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n {/* Mobile-only back link (the registry sidebar is hidden under md). */}\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.components() }}\n className=\"inline-flex items-center gap-1 px-4 pt-3 text-xs text-muted-foreground transition-colors hover:text-foreground md:hidden\"\n >\n <ArrowLeft className=\"size-3.5\" /> {translate('components.detail.allComponents')}\n </Link>\n\n {/* Header */}\n <div className=\"shrink-0 border-b border-border px-4 py-3 sm:px-6\">\n <Row gap=\"3\">\n <Row gap=\"2\" align=\"center\" className=\"min-w-0 flex-wrap\">\n <Heading level={1} size=\"h2\" className=\"truncate\">\n {title}\n </Heading>\n {/* EC-591: no namespace badge (an id prefix means nothing); the canvas role it fills, if any. */}\n {manifest.role ? <Badge variant=\"primary\">{manifest.role}</Badge> : null}\n {manifest.category ? <Badge variant=\"outline\">{manifest.category}</Badge> : null}\n </Row>\n <Spacer />\n <ComponentStatusBadge status={status} />\n <Button size=\"sm\" onClick={() => setTransferOpen(true)} data-testid=\"use-in-document\">\n <FilePlus2 /> {translate('components.transfer.action')}\n </Button>\n </Row>\n <Row gap=\"2\" align=\"center\" className=\"mt-1\">\n <Code tone=\"muted\">{manifest.id}</Code>\n </Row>\n {manifest.description ? (\n <Text tone=\"muted\" size=\"sm\" className=\"mt-1 max-w-prose\">\n {manifest.description}\n </Text>\n ) : null}\n {status !== 'valid' ? (\n <Text size=\"sm\" className={cn('mt-1', status === 'invalid' ? 'text-destructive' : 'text-warning')}>\n {statusDescription(status)}\n </Text>\n ) : null}\n\n {/* Lens row — the tab set (mirrors the document editor's lens row). */}\n <Row gap=\"1\" align=\"center\" className=\"mt-3\">\n <TabButton active={tab === 'preview'} onClick={() => setTab('preview')}>\n <Play className=\"size-3\" />\n {translate('components.detail.tab.preview')}\n </TabButton>\n <TabButton active={tab === 'manifest'} onClick={() => setTab('manifest')}>\n <ListTree className=\"size-3\" />\n {translate('components.detail.tab.manifest')}\n </TabButton>\n <div className=\"mx-1 h-4 w-px bg-border\" aria-hidden />\n <TabButton active={tab === 'references'} onClick={() => setTab('references')}>\n <Link2 className=\"size-3\" />\n {translate('references.title')}\n </TabButton>\n </Row>\n </div>\n\n {/* Body */}\n {tab === 'preview' ? (\n <div className=\"flex min-h-0 flex-1 overflow-hidden\">\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <ComponentPreview manifest={manifest} studio={studio} state={preview} />\n </div>\n <InspectorSidebar panes={panes} className=\"w-[320px] shrink-0 border-l border-border\" />\n </div>\n ) : tab === 'manifest' ? (\n <div className=\"min-h-0 flex-1 overflow-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-4xl\">\n <FieldInspector manifest={manifest} />\n </div>\n </div>\n ) : (\n // EC-172/EC-269: inbound references over the reference index, as a tab.\n <div className=\"min-h-0 flex-1 overflow-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-2xl\">\n <ReferencesPanel projectId={projectId} target={{ type: 'component', id: manifest.id }} />\n </div>\n </div>\n )}\n\n <TransferToDocumentDialog\n projectId={projectId}\n manifest={manifest}\n node={preview.node}\n open={transferOpen}\n onOpenChange={setTransferOpen}\n />\n </div>\n )\n}\n"]}
|