@elytracms/studio 0.0.43 → 0.0.45
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/cms.ts +59 -21
- package/convex/content.ts +6 -3
- package/convex/delivery.ts +8 -5
- package/convex/triggers.ts +8 -1
- package/dist/components/app-views/studio-config.d.ts +12 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +13 -1
- package/dist/components/assets/assets-collection-view-source.js +42 -51
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/cms/cms-store.js +9 -11
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +110 -338
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-staging.d.ts +8 -3
- package/dist/components/cms/document-staging.js +81 -21
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/use-compare-selection.d.ts +27 -0
- package/dist/components/cms/use-compare-selection.js +29 -0
- package/dist/components/cms/use-compare-selection.js.map +1 -0
- package/dist/components/cms/use-discard-controller.d.ts +44 -0
- package/dist/components/cms/use-discard-controller.js +51 -0
- package/dist/components/cms/use-discard-controller.js.map +1 -0
- package/dist/components/cms/use-embed-inspector-host.d.ts +12 -0
- package/dist/components/cms/use-embed-inspector-host.js +17 -0
- package/dist/components/cms/use-embed-inspector-host.js.map +1 -0
- package/dist/components/cms/use-field-focus.d.ts +38 -0
- package/dist/components/cms/use-field-focus.js +70 -0
- package/dist/components/cms/use-field-focus.js.map +1 -0
- package/dist/components/cms/use-live-url.d.ts +41 -0
- package/dist/components/cms/use-live-url.js +55 -0
- package/dist/components/cms/use-live-url.js.map +1 -0
- package/dist/components/cms/use-publish-controller.d.ts +49 -0
- package/dist/components/cms/use-publish-controller.js +102 -0
- package/dist/components/cms/use-publish-controller.js.map +1 -0
- package/dist/components/cms/use-published-perspective.d.ts +67 -0
- package/dist/components/cms/use-published-perspective.js +67 -0
- package/dist/components/cms/use-published-perspective.js.map +1 -0
- package/dist/components/cms/use-restore-version.d.ts +57 -0
- package/dist/components/cms/use-restore-version.js +66 -0
- package/dist/components/cms/use-restore-version.js.map +1 -0
- package/dist/components/cms/use-save-mode.d.ts +35 -0
- package/dist/components/cms/use-save-mode.js +54 -0
- package/dist/components/cms/use-save-mode.js.map +1 -0
- package/dist/components/cms/version-compare.js +13 -16
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +21 -19
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/transfer-dialog.d.ts +11 -6
- package/dist/components/components/transfer-dialog.js +98 -25
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +74 -8
- package/dist/components/components/transfer.js +87 -2
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +7 -5
- package/dist/components/rail-store-status.js +12 -9
- package/dist/components/rail-store-status.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +111 -0
- package/dist/components/rail-view-source.js +139 -0
- package/dist/components/rail-view-source.js.map +1 -0
- package/dist/components/redirects/redirects-collection-view-source.d.ts +13 -1
- package/dist/components/redirects/redirects-collection-view-source.js +42 -70
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/section-state.d.ts +12 -3
- package/dist/components/section-state.js +8 -4
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/users/users-collection-view-source.d.ts +13 -1
- package/dist/components/users/users-collection-view-source.js +60 -79
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/lib/ai/anthropic-provider.d.ts +29 -0
- package/dist/lib/ai/anthropic-provider.js +59 -0
- package/dist/lib/ai/anthropic-provider.js.map +1 -0
- package/dist/lib/ai/apply-content.d.ts +9 -0
- package/dist/lib/ai/apply-content.js +170 -0
- package/dist/lib/ai/apply-content.js.map +1 -0
- package/dist/lib/ai/config.d.ts +58 -0
- package/dist/lib/ai/config.js +134 -0
- package/dist/lib/ai/config.js.map +1 -0
- package/dist/lib/ai/fake-provider.d.ts +34 -0
- package/dist/lib/ai/fake-provider.js +31 -0
- package/dist/lib/ai/fake-provider.js.map +1 -0
- package/dist/lib/ai/index.d.ts +22 -0
- package/dist/lib/ai/index.js +23 -0
- package/dist/lib/ai/index.js.map +1 -0
- package/dist/lib/ai/knowledge.d.ts +39 -0
- package/dist/lib/ai/knowledge.js +60 -0
- package/dist/lib/ai/knowledge.js.map +1 -0
- package/dist/lib/ai/provenance.d.ts +20 -0
- package/dist/lib/ai/provenance.js +15 -0
- package/dist/lib/ai/provenance.js.map +1 -0
- package/dist/lib/ai/route.d.ts +24 -0
- package/dist/lib/ai/route.js +90 -0
- package/dist/lib/ai/route.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +35 -0
- package/dist/lib/ai/shape.js +113 -0
- package/dist/lib/ai/shape.js.map +1 -0
- package/dist/lib/ai/types.d.ts +227 -0
- package/dist/lib/ai/types.js +2 -0
- package/dist/lib/ai/types.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +4 -2
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +9 -0
- package/dist/lib/workspace/index.js +11 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/package.json +8 -2
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from 'react';
|
|
3
|
-
import { FilePlus2 } from 'lucide-react';
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { FilePlus2, FileText, Loader2, Search } from 'lucide-react';
|
|
4
4
|
import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
|
|
5
|
-
import { Button, Code, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Row, Stack, Text, } from '@elytracms/studio/ui';
|
|
5
|
+
import { Button, Code, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Row, Stack, Text, } from '@elytracms/studio/ui';
|
|
6
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
6
7
|
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
7
8
|
import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
|
|
8
9
|
import { CmsSessionGate } from '../project-session';
|
|
9
10
|
import { CmsProvider, useCms } from '../cms/cms-store';
|
|
10
11
|
import { stageDocument } from '../cms/document-staging';
|
|
11
12
|
import { resolveCmsInitialCollections } from '../app-views/default-modules';
|
|
12
|
-
import { buildBlockDocument, collectionsWithBlocks } from './transfer';
|
|
13
|
-
|
|
13
|
+
import { appendToBlockDocument, buildBlockDocument, canAppendTo, collectionsWithBlocks, isComponentAllowed, } from './transfer';
|
|
14
|
+
/** Existing documents offered per target, beyond which the rest are hidden (filter to find them). */
|
|
15
|
+
const MAX_EXISTING_TARGETS = 8;
|
|
14
16
|
/**
|
|
15
|
-
* EC-320 — "Use in document…". Transfers the current
|
|
16
|
-
* live preview {@link ComponentNode}, edited props
|
|
17
|
-
* document
|
|
17
|
+
* EC-320 (+ 0.0.44 follow-up) — "Use in document…". Transfers the current
|
|
18
|
+
* component combination (the live preview {@link ComponentNode}, edited props
|
|
19
|
+
* and all) into a document's blocks field, then opens that document. Each
|
|
20
|
+
* eligible collection+field target offers a NEW draft AND its existing
|
|
21
|
+
* documents to append into.
|
|
18
22
|
*
|
|
19
23
|
* The dialog mounts a CMS session (so it reuses the store's create/save machinery,
|
|
20
24
|
* collision-free ids and persistence) only while open. On confirm it builds the
|
|
21
|
-
* draft (pure {@link buildBlockDocument}
|
|
22
|
-
* it
|
|
23
|
-
*
|
|
25
|
+
* draft or the append (pure {@link buildBlockDocument} / {@link appendToBlockDocument}),
|
|
26
|
+
* saves it and AWAITS the persisted write landing (belt and braces alongside the
|
|
27
|
+
* STAGE for the next CMS mount, see `document-staging` — both matter: staging
|
|
28
|
+
* covers the very next mount regardless of mode, the await guards any later
|
|
29
|
+
* reload), then navigates to the document — focused on its blocks field so it
|
|
30
|
+
* opens on the canvas.
|
|
24
31
|
*/
|
|
25
32
|
export function TransferToDocumentDialog({ projectId, manifest, node, open, onOpenChange, }) {
|
|
26
33
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('components.transfer.title', { name: manifest.title ?? manifest.id }) }), _jsx(DialogDescription, { children: translate('components.transfer.description') })] }), open ? (_jsx(CmsSessionGate, { children: (cms) => {
|
|
@@ -36,6 +43,13 @@ export function TransferToDocumentDialog({ projectId, manifest, node, open, onOp
|
|
|
36
43
|
return (_jsx(CmsProvider, { projectId: projectId, initialCollections: collections, initialDocuments: cms?.documents ?? kit?.documents, initialPublishedKeys: cms?.publishedKeys, children: _jsx(TransferPicker, { projectId: projectId, node: node, onClose: () => onOpenChange(false) }) }));
|
|
37
44
|
} })) : null] }) }));
|
|
38
45
|
}
|
|
46
|
+
/** A pending transfer's in-flight target key: `<collectionId>/new` or `<collectionId>/<docId>`. */
|
|
47
|
+
function newTargetKey(target) {
|
|
48
|
+
return `${target.collection.id}/new`;
|
|
49
|
+
}
|
|
50
|
+
function existingTargetKey(target, doc) {
|
|
51
|
+
return `${target.collection.id}/${doc.id}`;
|
|
52
|
+
}
|
|
39
53
|
/**
|
|
40
54
|
* Exported for the fresh-Convex-project regression test (bug fix,
|
|
41
55
|
* transfer-dialog.test.tsx) — `TransferToDocumentDialog` is otherwise its only
|
|
@@ -44,27 +58,35 @@ export function TransferToDocumentDialog({ projectId, manifest, node, open, onOp
|
|
|
44
58
|
export function TransferPicker({ projectId, node, onClose, }) {
|
|
45
59
|
const cms = useCms();
|
|
46
60
|
const navigate = useNavigate();
|
|
61
|
+
const [query, setQuery] = useState('');
|
|
62
|
+
// The in-flight target's key while its save is awaited (disables every row
|
|
63
|
+
// + shows a spinner on the clicked one) — `null` when idle.
|
|
64
|
+
const [pendingKey, setPendingKey] = useState(null);
|
|
47
65
|
// Read the LIVE store schema (`cms.collections`) rather than re-deriving it
|
|
48
66
|
// from the same `resolveCmsInitialCollections` result passed to `CmsProvider`
|
|
49
67
|
// above — `cms.collections` is what the provider actually resolved to
|
|
50
68
|
// (its OWN `initialCollections ?? sampleFilterableCollections` fallback,
|
|
51
69
|
// EC-320), so the targets list can never drift from — or reintroduce the
|
|
52
70
|
// `[]`-suppresses-the-fixture-fallback bug against — what the CMS/Assets
|
|
53
|
-
// tabs show for this same session.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
existingIds,
|
|
62
|
-
seq: { current: 0 },
|
|
63
|
-
});
|
|
64
|
-
// Persist through the store (upsert + reference index) and stage for the next
|
|
65
|
-
// CMS mount (playground handoff), then open the document on its canvas.
|
|
71
|
+
// tabs show for this same session. Allow-gated (0.0.44 follow-up): a target
|
|
72
|
+
// whose field's `allow` vocabulary rejects this component is never offered,
|
|
73
|
+
// new-document or existing-document alike — the validator would flag it.
|
|
74
|
+
const targets = useMemo(() => collectionsWithBlocks(cms.collections).filter((t) => isComponentAllowed(t.field, node.componentId)), [cms.collections, node.componentId]);
|
|
75
|
+
/** Persist (awaited) + stage + navigate — shared by both transfer paths. */
|
|
76
|
+
async function persistAndOpen(doc, target) {
|
|
77
|
+
// Optimistic session apply + queues the persisted upsert (EC-141's normal
|
|
78
|
+
// fire-and-forget autosave path — unchanged for every OTHER caller).
|
|
66
79
|
cms.saveDocument(doc);
|
|
80
|
+
// Cross-mount handoff for the very next `CmsProvider` mount, in EVERY mode
|
|
81
|
+
// (see document-staging.ts) — covers the instant after this write, before
|
|
82
|
+
// any later hydration is guaranteed to observe it.
|
|
67
83
|
stageDocument(projectId, doc);
|
|
84
|
+
// Belt and braces: AWAIT the queued persistence command landing (the same
|
|
85
|
+
// `flushPersistence` publish/unpublish already await) so a reload of the
|
|
86
|
+
// destination editor — not just its very next mount — also finds the
|
|
87
|
+
// document. `saveDocument` itself stays fire-and-forget for every other
|
|
88
|
+
// caller; only this explicit await makes the transfer's save synchronous.
|
|
89
|
+
await cms.flushPersistence();
|
|
68
90
|
onClose();
|
|
69
91
|
void navigate({
|
|
70
92
|
to: STUDIO_PROJECT_ROUTE,
|
|
@@ -72,9 +94,60 @@ export function TransferPicker({ projectId, node, onClose, }) {
|
|
|
72
94
|
search: { field: `values/${target.field.name}` },
|
|
73
95
|
});
|
|
74
96
|
}
|
|
97
|
+
async function transferNew(target) {
|
|
98
|
+
const key = newTargetKey(target);
|
|
99
|
+
setPendingKey(key);
|
|
100
|
+
try {
|
|
101
|
+
const existingIds = new Set(cms.documentsOf(target.collection.id).map((d) => d.id));
|
|
102
|
+
const doc = buildBlockDocument({
|
|
103
|
+
collection: target.collection,
|
|
104
|
+
blocksField: target.field,
|
|
105
|
+
node,
|
|
106
|
+
existingIds,
|
|
107
|
+
seq: { current: 0 },
|
|
108
|
+
});
|
|
109
|
+
await persistAndOpen(doc, target);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
setPendingKey(null);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function transferInto(target, existing) {
|
|
116
|
+
const key = existingTargetKey(target, existing);
|
|
117
|
+
setPendingKey(key);
|
|
118
|
+
try {
|
|
119
|
+
const doc = appendToBlockDocument({
|
|
120
|
+
collection: target.collection,
|
|
121
|
+
document: existing,
|
|
122
|
+
blocksField: target.field,
|
|
123
|
+
node,
|
|
124
|
+
locale: cms.activeLocale,
|
|
125
|
+
});
|
|
126
|
+
await persistAndOpen(doc, target);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
setPendingKey(null);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
75
132
|
if (targets.length === 0) {
|
|
76
133
|
return (_jsx(Text, { size: "sm", tone: "muted", "data-testid": "transfer-no-targets", children: translate('components.transfer.noTargets') }));
|
|
77
134
|
}
|
|
78
|
-
|
|
135
|
+
const trimmedQuery = query.trim().toLowerCase();
|
|
136
|
+
const hasAnyExistingDocs = targets.some((t) => cms.documentsOf(t.collection.id).length > 0);
|
|
137
|
+
return (_jsxs(_Fragment, { children: [hasAnyExistingDocs ? (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: translate('common.searchPlaceholder'), className: "pl-8" })] })) : null, _jsx(Stack, { gap: "4", className: "max-h-[50vh] overflow-y-auto", children: targets.map((target) => {
|
|
138
|
+
const existing = cms
|
|
139
|
+
.documentsOf(target.collection.id)
|
|
140
|
+
.filter((doc) => canAppendTo({ collection: target.collection, document: doc, blocksField: target.field, locale: cms.activeLocale }))
|
|
141
|
+
.filter((doc) => trimmedQuery === '' ||
|
|
142
|
+
cms.titleOf({ collection: target.collection.id, id: doc.id }).toLowerCase().includes(trimmedQuery))
|
|
143
|
+
.slice(0, MAX_EXISTING_TARGETS);
|
|
144
|
+
return (_jsxs(Stack, { gap: "2", children: [_jsxs(Stack, { gap: "px", children: [_jsx(Text, { size: "sm", weight: "medium", className: "truncate", children: localizedLabel(target.collection.form?.label, target.collection.id) }), _jsxs(Row, { gap: "1", align: "center", children: [_jsx(Text, { size: "xs", tone: "muted", children: translate('components.transfer.intoField') }), _jsx(Code, { tone: "muted", children: target.field.name })] })] }), _jsxs(Stack, { gap: "1", children: [_jsx(TransferTargetRow, { testId: "transfer-target", icon: _jsx(FilePlus2, { className: "size-4" }), label: translate('components.transfer.newDocument', {
|
|
145
|
+
collection: localizedLabel(target.collection.form?.labelSingular, target.collection.id),
|
|
146
|
+
}), pending: pendingKey === newTargetKey(target), disabled: pendingKey !== null, onClick: () => void transferNew(target) }), existing.length > 0 ? (_jsx(Text, { size: "xs", tone: "muted", className: "px-1 pt-1", children: translate('components.transfer.orExisting') })) : null, existing.map((doc) => (_jsx(TransferTargetRow, { testId: "transfer-target-existing", icon: _jsx(FileText, { className: "size-4" }), label: cms.titleOf({ collection: target.collection.id, id: doc.id }), pending: pendingKey === existingTargetKey(target, doc), disabled: pendingKey !== null, onClick: () => void transferInto(target, doc) }, doc.id)))] })] }, target.collection.id));
|
|
147
|
+
}) }), _jsx(DialogFooter, { children: _jsx(Button, { type: "button", variant: "ghost", onClick: onClose, disabled: pendingKey !== null, children: translate('common.cancel') }) })] }));
|
|
148
|
+
}
|
|
149
|
+
/** One selectable transfer target row — a new draft, or an existing document to append into. */
|
|
150
|
+
function TransferTargetRow({ testId, icon, label, pending, disabled, onClick, }) {
|
|
151
|
+
return (_jsxs("button", { type: "button", "data-testid": testId, disabled: disabled, onClick: onClick, className: cn('flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted', disabled && 'cursor-not-allowed opacity-60'), children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: pending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : icon }), _jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 flex-1 truncate", children: label })] }));
|
|
79
152
|
}
|
|
80
153
|
//# sourceMappingURL=transfer-dialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transfer-dialog.js","sourceRoot":"","sources":["../../../src/components/components/transfer-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { FilePlus2 } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport {\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { stageDocument } from '../cms/document-staging'\nimport { resolveCmsInitialCollections } from '../app-views/default-modules'\nimport { buildBlockDocument, collectionsWithBlocks } from './transfer'\nimport type { CollectionBlocksTarget } from './transfer'\nimport { embedNodeFrom } from './component-preview'\n\n/**\n * EC-320 — \"Use in document…\". Transfers the current component combination (the\n * live preview {@link ComponentNode}, edited props and all) into a NEW DRAFT\n * document whose blocks field holds it, then opens that document.\n *\n * The dialog mounts a CMS session (so it reuses the store's create/save machinery,\n * collision-free ids and persistence) only while open. On confirm it builds the\n * draft (pure {@link buildBlockDocument}), persists it via `saveDocument`, STAGES\n * it for the next CMS mount (the playground handoff, see document-staging), and\n * navigates to the document — focused on its blocks field so it opens on the canvas.\n */\nexport function TransferToDocumentDialog({\n projectId,\n manifest,\n node,\n open,\n onOpenChange,\n}: {\n projectId: string\n manifest: ComponentManifest\n node: ComponentNode\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate('components.transfer.title', { name: manifest.title ?? manifest.id })}\n </DialogTitle>\n <DialogDescription>{translate('components.transfer.description')}</DialogDescription>\n </DialogHeader>\n {open ? (\n <CmsSessionGate>\n {(cms) => {\n const kit = getInstantiatedKit(projectId)\n // Bug fix: this used to reimplement `resolveCmsInitialCollections`'s\n // fallback chain inline with a final `[]` instead of `undefined` — `[]`\n // is \"present\" to `CmsProvider`'s `initialCollections ??\n // sampleFilterableCollections`, so it suppressed the fixture fallback a\n // fresh Convex-mode project (`cms === null`, no kit) needs. Delegating to\n // the shared, tested resolver (also used by the CMS/Assets tabs) keeps\n // this dialog's targets in sync with what those tabs show.\n const collections = resolveCmsInitialCollections(cms, kit)\n return (\n <CmsProvider\n projectId={projectId}\n initialCollections={collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n >\n <TransferPicker projectId={projectId} node={node} onClose={() => onOpenChange(false)} />\n </CmsProvider>\n )\n }}\n </CmsSessionGate>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/**\n * Exported for the fresh-Convex-project regression test (bug fix,\n * transfer-dialog.test.tsx) — `TransferToDocumentDialog` is otherwise its only\n * caller.\n */\nexport function TransferPicker({\n projectId,\n node,\n onClose,\n}: {\n projectId: string\n node: ComponentNode\n onClose: () => void\n}) {\n const cms = useCms()\n const navigate = useNavigate()\n // Read the LIVE store schema (`cms.collections`) rather than re-deriving it\n // from the same `resolveCmsInitialCollections` result passed to `CmsProvider`\n // above — `cms.collections` is what the provider actually resolved to\n // (its OWN `initialCollections ?? sampleFilterableCollections` fallback,\n // EC-320), so the targets list can never drift from — or reintroduce the\n // `[]`-suppresses-the-fixture-fallback bug against — what the CMS/Assets\n // tabs show for this same session.\n const targets = useMemo(() => collectionsWithBlocks(cms.collections), [cms.collections])\n\n function transfer(target: CollectionBlocksTarget) {\n const existingIds = new Set(cms.documentsOf(target.collection.id).map((d) => d.id))\n const doc = buildBlockDocument({\n collection: target.collection,\n blocksField: target.field,\n node: embedNodeFrom(node, 'node-1'),\n existingIds,\n seq: { current: 0 },\n })\n // Persist through the store (upsert + reference index) and stage for the next\n // CMS mount (playground handoff), then open the document on its canvas.\n cms.saveDocument(doc)\n stageDocument(projectId, doc)\n onClose()\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(target.collection.id, doc.id) },\n search: { field: `values/${target.field.name}` },\n })\n }\n\n if (targets.length === 0) {\n return (\n <Text size=\"sm\" tone=\"muted\" data-testid=\"transfer-no-targets\">\n {translate('components.transfer.noTargets')}\n </Text>\n )\n }\n\n return (\n <>\n <Stack gap=\"2\" className=\"max-h-[50vh] overflow-y-auto\">\n {targets.map((target) => (\n <button\n key={target.collection.id}\n type=\"button\"\n data-testid=\"transfer-target\"\n onClick={() => transfer(target)}\n className=\"flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n <FilePlus2 className=\"size-4\" />\n </span>\n <Stack gap=\"px\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate\">\n {localizedLabel(target.collection.form?.label, target.collection.id)}\n </Text>\n <Row gap=\"1\" align=\"center\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('components.transfer.intoField')}\n </Text>\n <Code tone=\"muted\">{target.field.name}</Code>\n </Row>\n </Stack>\n </button>\n ))}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={onClose}>\n {translate('common.cancel')}\n </Button>\n </DialogFooter>\n </>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"transfer-dialog.js","sourceRoot":"","sources":["../../../src/components/components/transfer-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { FilePlus2, FileText, Loader2, Search } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport {\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { stageDocument } from '../cms/document-staging'\nimport { resolveCmsInitialCollections } from '../app-views/default-modules'\nimport {\n appendToBlockDocument,\n buildBlockDocument,\n canAppendTo,\n collectionsWithBlocks,\n isComponentAllowed,\n} from './transfer'\nimport type { CollectionBlocksTarget } from './transfer'\n\n/** Existing documents offered per target, beyond which the rest are hidden (filter to find them). */\nconst MAX_EXISTING_TARGETS = 8\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — \"Use in document…\". Transfers the current\n * component combination (the live preview {@link ComponentNode}, edited props\n * and all) into a document's blocks field, then opens that document. Each\n * eligible collection+field target offers a NEW draft AND its existing\n * documents to append into.\n *\n * The dialog mounts a CMS session (so it reuses the store's create/save machinery,\n * collision-free ids and persistence) only while open. On confirm it builds the\n * draft or the append (pure {@link buildBlockDocument} / {@link appendToBlockDocument}),\n * saves it and AWAITS the persisted write landing (belt and braces alongside the\n * STAGE for the next CMS mount, see `document-staging` — both matter: staging\n * covers the very next mount regardless of mode, the await guards any later\n * reload), then navigates to the document — focused on its blocks field so it\n * opens on the canvas.\n */\nexport function TransferToDocumentDialog({\n projectId,\n manifest,\n node,\n open,\n onOpenChange,\n}: {\n projectId: string\n manifest: ComponentManifest\n node: ComponentNode\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate('components.transfer.title', { name: manifest.title ?? manifest.id })}\n </DialogTitle>\n <DialogDescription>{translate('components.transfer.description')}</DialogDescription>\n </DialogHeader>\n {open ? (\n <CmsSessionGate>\n {(cms) => {\n const kit = getInstantiatedKit(projectId)\n // Bug fix: this used to reimplement `resolveCmsInitialCollections`'s\n // fallback chain inline with a final `[]` instead of `undefined` — `[]`\n // is \"present\" to `CmsProvider`'s `initialCollections ??\n // sampleFilterableCollections`, so it suppressed the fixture fallback a\n // fresh Convex-mode project (`cms === null`, no kit) needs. Delegating to\n // the shared, tested resolver (also used by the CMS/Assets tabs) keeps\n // this dialog's targets in sync with what those tabs show.\n const collections = resolveCmsInitialCollections(cms, kit)\n return (\n <CmsProvider\n projectId={projectId}\n initialCollections={collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n >\n <TransferPicker projectId={projectId} node={node} onClose={() => onOpenChange(false)} />\n </CmsProvider>\n )\n }}\n </CmsSessionGate>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** A pending transfer's in-flight target key: `<collectionId>/new` or `<collectionId>/<docId>`. */\nfunction newTargetKey(target: CollectionBlocksTarget): string {\n return `${target.collection.id}/new`\n}\nfunction existingTargetKey(target: CollectionBlocksTarget, doc: CmsDocument): string {\n return `${target.collection.id}/${doc.id}`\n}\n\n/**\n * Exported for the fresh-Convex-project regression test (bug fix,\n * transfer-dialog.test.tsx) — `TransferToDocumentDialog` is otherwise its only\n * caller.\n */\nexport function TransferPicker({\n projectId,\n node,\n onClose,\n}: {\n projectId: string\n node: ComponentNode\n onClose: () => void\n}) {\n const cms = useCms()\n const navigate = useNavigate()\n const [query, setQuery] = useState('')\n // The in-flight target's key while its save is awaited (disables every row\n // + shows a spinner on the clicked one) — `null` when idle.\n const [pendingKey, setPendingKey] = useState<string | null>(null)\n\n // Read the LIVE store schema (`cms.collections`) rather than re-deriving it\n // from the same `resolveCmsInitialCollections` result passed to `CmsProvider`\n // above — `cms.collections` is what the provider actually resolved to\n // (its OWN `initialCollections ?? sampleFilterableCollections` fallback,\n // EC-320), so the targets list can never drift from — or reintroduce the\n // `[]`-suppresses-the-fixture-fallback bug against — what the CMS/Assets\n // tabs show for this same session. Allow-gated (0.0.44 follow-up): a target\n // whose field's `allow` vocabulary rejects this component is never offered,\n // new-document or existing-document alike — the validator would flag it.\n const targets = useMemo(\n () => collectionsWithBlocks(cms.collections).filter((t) => isComponentAllowed(t.field, node.componentId)),\n [cms.collections, node.componentId],\n )\n\n /** Persist (awaited) + stage + navigate — shared by both transfer paths. */\n async function persistAndOpen(doc: CmsDocument, target: CollectionBlocksTarget) {\n // Optimistic session apply + queues the persisted upsert (EC-141's normal\n // fire-and-forget autosave path — unchanged for every OTHER caller).\n cms.saveDocument(doc)\n // Cross-mount handoff for the very next `CmsProvider` mount, in EVERY mode\n // (see document-staging.ts) — covers the instant after this write, before\n // any later hydration is guaranteed to observe it.\n stageDocument(projectId, doc)\n // Belt and braces: AWAIT the queued persistence command landing (the same\n // `flushPersistence` publish/unpublish already await) so a reload of the\n // destination editor — not just its very next mount — also finds the\n // document. `saveDocument` itself stays fire-and-forget for every other\n // caller; only this explicit await makes the transfer's save synchronous.\n await cms.flushPersistence()\n onClose()\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(target.collection.id, doc.id) },\n search: { field: `values/${target.field.name}` },\n })\n }\n\n async function transferNew(target: CollectionBlocksTarget) {\n const key = newTargetKey(target)\n setPendingKey(key)\n try {\n const existingIds = new Set(cms.documentsOf(target.collection.id).map((d) => d.id))\n const doc = buildBlockDocument({\n collection: target.collection,\n blocksField: target.field,\n node,\n existingIds,\n seq: { current: 0 },\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n async function transferInto(target: CollectionBlocksTarget, existing: CmsDocument) {\n const key = existingTargetKey(target, existing)\n setPendingKey(key)\n try {\n const doc = appendToBlockDocument({\n collection: target.collection,\n document: existing,\n blocksField: target.field,\n node,\n locale: cms.activeLocale,\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n if (targets.length === 0) {\n return (\n <Text size=\"sm\" tone=\"muted\" data-testid=\"transfer-no-targets\">\n {translate('components.transfer.noTargets')}\n </Text>\n )\n }\n\n const trimmedQuery = query.trim().toLowerCase()\n const hasAnyExistingDocs = targets.some((t) => cms.documentsOf(t.collection.id).length > 0)\n\n return (\n <>\n {hasAnyExistingDocs ? (\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('common.searchPlaceholder')}\n className=\"pl-8\"\n />\n </div>\n ) : null}\n <Stack gap=\"4\" className=\"max-h-[50vh] overflow-y-auto\">\n {targets.map((target) => {\n const existing = cms\n .documentsOf(target.collection.id)\n .filter((doc) => canAppendTo({ collection: target.collection, document: doc, blocksField: target.field, locale: cms.activeLocale }))\n .filter(\n (doc) =>\n trimmedQuery === '' ||\n cms.titleOf({ collection: target.collection.id, id: doc.id }).toLowerCase().includes(trimmedQuery),\n )\n .slice(0, MAX_EXISTING_TARGETS)\n\n return (\n <Stack key={target.collection.id} gap=\"2\">\n <Stack gap=\"px\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate\">\n {localizedLabel(target.collection.form?.label, target.collection.id)}\n </Text>\n <Row gap=\"1\" align=\"center\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('components.transfer.intoField')}\n </Text>\n <Code tone=\"muted\">{target.field.name}</Code>\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <TransferTargetRow\n testId=\"transfer-target\"\n icon={<FilePlus2 className=\"size-4\" />}\n label={translate('components.transfer.newDocument', {\n collection: localizedLabel(target.collection.form?.labelSingular, target.collection.id),\n })}\n pending={pendingKey === newTargetKey(target)}\n disabled={pendingKey !== null}\n onClick={() => void transferNew(target)}\n />\n {existing.length > 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-1 pt-1\">\n {translate('components.transfer.orExisting')}\n </Text>\n ) : null}\n {existing.map((doc) => (\n <TransferTargetRow\n key={doc.id}\n testId=\"transfer-target-existing\"\n icon={<FileText className=\"size-4\" />}\n label={cms.titleOf({ collection: target.collection.id, id: doc.id })}\n pending={pendingKey === existingTargetKey(target, doc)}\n disabled={pendingKey !== null}\n onClick={() => void transferInto(target, doc)}\n />\n ))}\n </Stack>\n </Stack>\n )\n })}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={onClose} disabled={pendingKey !== null}>\n {translate('common.cancel')}\n </Button>\n </DialogFooter>\n </>\n )\n}\n\n/** One selectable transfer target row — a new draft, or an existing document to append into. */\nfunction TransferTargetRow({\n testId,\n icon,\n label,\n pending,\n disabled,\n onClick,\n}: {\n testId: string\n icon: ReactNode\n label: string\n pending: boolean\n disabled: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n data-testid={testId}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n 'flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted',\n disabled && 'cursor-not-allowed opacity-60',\n )}\n >\n <span className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n {pending ? <Loader2 className=\"size-4 animate-spin\" /> : icon}\n </span>\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 flex-1 truncate\">\n {label}\n </Text>\n </button>\n )\n}\n"]}
|
|
@@ -1,28 +1,44 @@
|
|
|
1
|
-
import type { CmsDocument, CollectionDef,
|
|
1
|
+
import type { BlocksFieldDef, CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
3
3
|
/**
|
|
4
|
-
* EC-320 — the "design guide → real content" bridge. A
|
|
5
|
-
* user has arranged in the Components view (a
|
|
6
|
-
* captured as a {@link ComponentNode}) is
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* EC-320 (+ 0.0.44 follow-up) — the "design guide → real content" bridge. A
|
|
5
|
+
* component combination the user has arranged in the Components view (a
|
|
6
|
+
* manifest + edited prop values, captured as a {@link ComponentNode}) is
|
|
7
|
+
* transferred into a document's blocks/composition field — either a NEW DRAFT
|
|
8
|
+
* document created for it ({@link buildBlockDocument}) or an EXISTING
|
|
9
|
+
* document's field, appended alongside whatever it already holds
|
|
10
|
+
* ({@link appendToBlockDocument}). Pure so id-minting/embedding/appending are
|
|
11
|
+
* unit-testable; the React dialog owns the picker, persistence, and
|
|
9
12
|
* navigation.
|
|
10
13
|
*/
|
|
11
14
|
export interface CollectionBlocksTarget {
|
|
12
15
|
collection: CollectionDef;
|
|
13
16
|
/** The (first) blocks field the node lands in. */
|
|
14
|
-
field:
|
|
17
|
+
field: BlocksFieldDef;
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
17
20
|
* Collections that can receive a transferred combination — those declaring a
|
|
18
21
|
* `blocks` (composition) field. The first blocks field wins (a collection with a
|
|
19
22
|
* single canvas is the norm; a second is an unusual edge we don't disambiguate).
|
|
23
|
+
* Callers additionally gate each target through {@link isComponentAllowed} —
|
|
24
|
+
* this only reports which collections HAVE a blocks field, not which ones
|
|
25
|
+
* accept a given component.
|
|
20
26
|
*/
|
|
21
27
|
export declare function collectionsWithBlocks(collections: readonly CollectionDef[]): CollectionBlocksTarget[];
|
|
28
|
+
/**
|
|
29
|
+
* Is `componentId` permitted as a top-level block of `field`? Mirrors
|
|
30
|
+
* `validateCompositionValue`'s (`@elytracms/core/project-graph`) top-level
|
|
31
|
+
* `allow` rule EXACTLY: an absent or empty list permits any registered
|
|
32
|
+
* component; a non-empty list is the closed vocabulary. A target this
|
|
33
|
+
* predicate accepts can never fail that validator's `forbidden-component`
|
|
34
|
+
* check on save — the transfer dialog uses this to hide targets the
|
|
35
|
+
* component isn't allowed into (new AND existing document targets alike).
|
|
36
|
+
*/
|
|
37
|
+
export declare function isComponentAllowed(field: BlocksFieldDef, componentId: string): boolean;
|
|
22
38
|
export interface BuildBlockDocumentInput {
|
|
23
39
|
collection: CollectionDef;
|
|
24
40
|
/** The blocks field the node lands in (from {@link collectionsWithBlocks}). */
|
|
25
|
-
blocksField:
|
|
41
|
+
blocksField: BlocksFieldDef;
|
|
26
42
|
/** The component node — a manifest id + the edited static props (+ slots). */
|
|
27
43
|
node: ComponentNode;
|
|
28
44
|
/** Every doc id already in the target collection (collision guard, EC-316). */
|
|
@@ -41,3 +57,53 @@ export interface BuildBlockDocumentInput {
|
|
|
41
57
|
* composition editor persists and `blockRootsOf` reads it back.
|
|
42
58
|
*/
|
|
43
59
|
export declare function buildBlockDocument({ collection, blocksField, node, existingIds, seq, }: BuildBlockDocumentInput): CmsDocument;
|
|
60
|
+
export interface CanAppendToInput {
|
|
61
|
+
collection: CollectionDef;
|
|
62
|
+
document: CmsDocument;
|
|
63
|
+
blocksField: BlocksFieldDef;
|
|
64
|
+
/** The locale being edited (EC-176 ambient active locale) for a localized field. */
|
|
65
|
+
locale: Locale;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Can `node` be appended to `document`'s blocks field without destroying
|
|
69
|
+
* existing content? Always true for a `multiple` field (an ordered list has
|
|
70
|
+
* room for one more); a single-root (`multiple: false`) field only while it
|
|
71
|
+
* is empty — occupied, there is no second root to hold it without
|
|
72
|
+
* overwriting the existing block, so the transfer dialog excludes that
|
|
73
|
+
* document from its offered targets rather than risk data loss.
|
|
74
|
+
*/
|
|
75
|
+
export declare function canAppendTo({ collection, document, blocksField, locale }: CanAppendToInput): boolean;
|
|
76
|
+
export interface AppendToBlockDocumentInput {
|
|
77
|
+
collection: CollectionDef;
|
|
78
|
+
/** The document to append into (the transfer dialog's "insert into existing document"). */
|
|
79
|
+
document: CmsDocument;
|
|
80
|
+
blocksField: BlocksFieldDef;
|
|
81
|
+
/** The component node to append — not yet re-ided (fresh id minted here). */
|
|
82
|
+
node: ComponentNode;
|
|
83
|
+
/** The locale being edited (EC-176 ambient active locale) for a localized field. */
|
|
84
|
+
locale: Locale;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Append `node` into an EXISTING document's blocks field (EC-320 follow-up,
|
|
88
|
+
* 0.0.44 — "insert into existing document"), leaving everything else about
|
|
89
|
+
* the document untouched: other fields, other locale bundles, and the
|
|
90
|
+
* field's own existing content, which the embed always lands AFTER.
|
|
91
|
+
*
|
|
92
|
+
* Reads the current roots via `blockRootsOf` (`@elytracms/core/project-graph`
|
|
93
|
+
* — the same reader the composition editor, table cell, and reuse-context
|
|
94
|
+
* use), so this reads the field back exactly like every other consumer,
|
|
95
|
+
* undefined-safe for a never-set field. Locale-aware writes go through
|
|
96
|
+
* {@link applyCellValue} (the same locale-write helper inline cell edits use,
|
|
97
|
+
* `cell-edit.ts`) so a localized blocks field's OTHER locale bundles are
|
|
98
|
+
* never touched, only the edited locale's.
|
|
99
|
+
*
|
|
100
|
+
* Node-id collision guard: `node` is re-embedded under a fresh id minted past
|
|
101
|
+
* every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`
|
|
102
|
+
* — a document whose blocks already hold that id would otherwise collide.
|
|
103
|
+
*
|
|
104
|
+
* Throws if `blocksField` is a single-root (`multiple: false`) field that
|
|
105
|
+
* already holds a root — a defensive invariant guard, not a user-facing
|
|
106
|
+
* validation path: the caller must pre-filter candidates with
|
|
107
|
+
* {@link canAppendTo} so this is never reached in that state.
|
|
108
|
+
*/
|
|
109
|
+
export declare function appendToBlockDocument({ collection, document, blocksField, node, locale, }: AppendToBlockDocumentInput): CmsDocument;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { isBlocksField, seedDefaults } from '@elytracms/core/cms-core';
|
|
1
|
+
import { isBlocksField, isFieldLocalized, seedDefaults } from '@elytracms/core/cms-core';
|
|
2
|
+
import { blockRootsOf, maxNodeIdSeq } from '@elytracms/core/project-graph';
|
|
3
|
+
import { makeNodeIdCounter } from '../builder/builder-store';
|
|
4
|
+
import { applyCellValue } from '../cms/cell-edit';
|
|
2
5
|
import { mintDocumentId } from '../cms/cms-store';
|
|
6
|
+
import { embedNodeFrom } from './component-preview';
|
|
3
7
|
/**
|
|
4
8
|
* Collections that can receive a transferred combination — those declaring a
|
|
5
9
|
* `blocks` (composition) field. The first blocks field wins (a collection with a
|
|
6
10
|
* single canvas is the norm; a second is an unusual edge we don't disambiguate).
|
|
11
|
+
* Callers additionally gate each target through {@link isComponentAllowed} —
|
|
12
|
+
* this only reports which collections HAVE a blocks field, not which ones
|
|
13
|
+
* accept a given component.
|
|
7
14
|
*/
|
|
8
15
|
export function collectionsWithBlocks(collections) {
|
|
9
16
|
const targets = [];
|
|
@@ -14,6 +21,32 @@ export function collectionsWithBlocks(collections) {
|
|
|
14
21
|
}
|
|
15
22
|
return targets;
|
|
16
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Is `componentId` permitted as a top-level block of `field`? Mirrors
|
|
26
|
+
* `validateCompositionValue`'s (`@elytracms/core/project-graph`) top-level
|
|
27
|
+
* `allow` rule EXACTLY: an absent or empty list permits any registered
|
|
28
|
+
* component; a non-empty list is the closed vocabulary. A target this
|
|
29
|
+
* predicate accepts can never fail that validator's `forbidden-component`
|
|
30
|
+
* check on save — the transfer dialog uses this to hide targets the
|
|
31
|
+
* component isn't allowed into (new AND existing document targets alike).
|
|
32
|
+
*/
|
|
33
|
+
export function isComponentAllowed(field, componentId) {
|
|
34
|
+
return !field.allow || field.allow.length === 0 || field.allow.includes(componentId);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Mint a fresh `node-<n>` id for `node`, guaranteed not to collide with any id
|
|
38
|
+
* already present anywhere in `existingRoots`. Reuses the SAME mint the
|
|
39
|
+
* canvas reducer and clipboard-paste path share (`maxNodeIdSeq` +
|
|
40
|
+
* `makeNodeIdCounter`, `builder-store.tsx`) rather than a fixed literal: a
|
|
41
|
+
* brand-new document (`existingRoots` empty) mints `node-1` — matching the
|
|
42
|
+
* transfer's prior behavior — but appending into a document whose blocks
|
|
43
|
+
* already hold `node-1..N` mints past that, never re-using an id already in
|
|
44
|
+
* the tree (the EC-320 follow-up collision hazard).
|
|
45
|
+
*/
|
|
46
|
+
function embedFreshNode(node, existingRoots) {
|
|
47
|
+
const maxSeq = existingRoots.reduce((max, root) => Math.max(max, maxNodeIdSeq(root)), 0);
|
|
48
|
+
return embedNodeFrom(node, makeNodeIdCounter(maxSeq).genId());
|
|
49
|
+
}
|
|
17
50
|
/**
|
|
18
51
|
* Build the draft document that embeds `node` in the collection's blocks field.
|
|
19
52
|
* Reuses the CMS create machinery: {@link mintDocumentId} for a collision-free id
|
|
@@ -25,7 +58,59 @@ export function collectionsWithBlocks(collections) {
|
|
|
25
58
|
export function buildBlockDocument({ collection, blocksField, node, existingIds, seq, }) {
|
|
26
59
|
const id = mintDocumentId(collection.id, seq, existingIds);
|
|
27
60
|
const values = seedDefaults(collection);
|
|
28
|
-
|
|
61
|
+
const embedded = embedFreshNode(node, []);
|
|
62
|
+
values[blocksField.name] = blocksField.multiple ? [embedded] : embedded;
|
|
29
63
|
return { collection: collection.id, id, values };
|
|
30
64
|
}
|
|
65
|
+
/** The blocks field's current value on `document`, respecting locale (EC-018). */
|
|
66
|
+
function readBlocksValue(collection, document, blocksField, locale) {
|
|
67
|
+
return isFieldLocalized(collection, blocksField.name)
|
|
68
|
+
? document.localized?.[locale]?.[blocksField.name]
|
|
69
|
+
: document.values[blocksField.name];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Can `node` be appended to `document`'s blocks field without destroying
|
|
73
|
+
* existing content? Always true for a `multiple` field (an ordered list has
|
|
74
|
+
* room for one more); a single-root (`multiple: false`) field only while it
|
|
75
|
+
* is empty — occupied, there is no second root to hold it without
|
|
76
|
+
* overwriting the existing block, so the transfer dialog excludes that
|
|
77
|
+
* document from its offered targets rather than risk data loss.
|
|
78
|
+
*/
|
|
79
|
+
export function canAppendTo({ collection, document, blocksField, locale }) {
|
|
80
|
+
if (blocksField.multiple)
|
|
81
|
+
return true;
|
|
82
|
+
return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length === 0;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Append `node` into an EXISTING document's blocks field (EC-320 follow-up,
|
|
86
|
+
* 0.0.44 — "insert into existing document"), leaving everything else about
|
|
87
|
+
* the document untouched: other fields, other locale bundles, and the
|
|
88
|
+
* field's own existing content, which the embed always lands AFTER.
|
|
89
|
+
*
|
|
90
|
+
* Reads the current roots via `blockRootsOf` (`@elytracms/core/project-graph`
|
|
91
|
+
* — the same reader the composition editor, table cell, and reuse-context
|
|
92
|
+
* use), so this reads the field back exactly like every other consumer,
|
|
93
|
+
* undefined-safe for a never-set field. Locale-aware writes go through
|
|
94
|
+
* {@link applyCellValue} (the same locale-write helper inline cell edits use,
|
|
95
|
+
* `cell-edit.ts`) so a localized blocks field's OTHER locale bundles are
|
|
96
|
+
* never touched, only the edited locale's.
|
|
97
|
+
*
|
|
98
|
+
* Node-id collision guard: `node` is re-embedded under a fresh id minted past
|
|
99
|
+
* every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`
|
|
100
|
+
* — a document whose blocks already hold that id would otherwise collide.
|
|
101
|
+
*
|
|
102
|
+
* Throws if `blocksField` is a single-root (`multiple: false`) field that
|
|
103
|
+
* already holds a root — a defensive invariant guard, not a user-facing
|
|
104
|
+
* validation path: the caller must pre-filter candidates with
|
|
105
|
+
* {@link canAppendTo} so this is never reached in that state.
|
|
106
|
+
*/
|
|
107
|
+
export function appendToBlockDocument({ collection, document, blocksField, node, locale, }) {
|
|
108
|
+
const existingRoots = blockRootsOf(readBlocksValue(collection, document, blocksField, locale));
|
|
109
|
+
if (!blocksField.multiple && existingRoots.length > 0) {
|
|
110
|
+
throw new Error(`Cannot append to single-root blocks field "${blocksField.name}": already holds a component (caller must gate with canAppendTo).`);
|
|
111
|
+
}
|
|
112
|
+
const embedded = embedFreshNode(node, existingRoots);
|
|
113
|
+
const nextValue = blocksField.multiple ? [...existingRoots, embedded] : embedded;
|
|
114
|
+
return applyCellValue(collection, document, blocksField.name, locale, nextValue);
|
|
115
|
+
}
|
|
31
116
|
//# sourceMappingURL=transfer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transfer.js","sourceRoot":"","sources":["../../../src/components/components/transfer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAGtE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAiBjD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,WAAqC;IAErC,MAAM,OAAO,GAA6B,EAAE,CAAA;IAC5C,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QACnD,IAAI,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;IAChD,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAcD;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,UAAU,EACV,WAAW,EACX,IAAI,EACJ,WAAW,EACX,GAAG,GACqB;IACxB,MAAM,EAAE,GAAG,cAAc,CAAC,UAAU,CAAC,EAAE,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;IAC1D,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,CAAA;IACvC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,WAAW,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC7F,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAA;AAClD,CAAC","sourcesContent":["import { isBlocksField, seedDefaults } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { mintDocumentId } from '../cms/cms-store'\n\n/**\n * EC-320 — the \"design guide → real content\" bridge. A component combination the\n * user has arranged in the Components view (a manifest + edited prop values,\n * captured as a {@link ComponentNode}) is transferred into a NEW DRAFT document\n * whose blocks/composition field holds that node. Pure so the id-minting and\n * embedding are unit-testable; the React dialog owns the picker, persistence, and\n * navigation.\n */\n\nexport interface CollectionBlocksTarget {\n collection: CollectionDef\n /** The (first) blocks field the node lands in. */\n field: FieldDef\n}\n\n/**\n * Collections that can receive a transferred combination — those declaring a\n * `blocks` (composition) field. The first blocks field wins (a collection with a\n * single canvas is the norm; a second is an unusual edge we don't disambiguate).\n */\nexport function collectionsWithBlocks(\n collections: readonly CollectionDef[],\n): CollectionBlocksTarget[] {\n const targets: CollectionBlocksTarget[] = []\n for (const collection of collections) {\n const field = collection.fields.find(isBlocksField)\n if (field) targets.push({ collection, field })\n }\n return targets\n}\n\nexport interface BuildBlockDocumentInput {\n collection: CollectionDef\n /** The blocks field the node lands in (from {@link collectionsWithBlocks}). */\n blocksField: FieldDef\n /** The component node — a manifest id + the edited static props (+ slots). */\n node: ComponentNode\n /** Every doc id already in the target collection (collision guard, EC-316). */\n existingIds: ReadonlySet<string>\n /** The session id counter (mutated by {@link mintDocumentId}). */\n seq: { current: number }\n}\n\n/**\n * Build the draft document that embeds `node` in the collection's blocks field.\n * Reuses the CMS create machinery: {@link mintDocumentId} for a collision-free id\n * (EC-316) and {@link seedDefaults} so declared field defaults are present (as a\n * normal `+ New` would be). A `multiple` blocks field stores an array; a single\n * (`multiple: false`) blocks field stores the lone node — matching how the\n * composition editor persists and `blockRootsOf` reads it back.\n */\nexport function buildBlockDocument({\n collection,\n blocksField,\n node,\n existingIds,\n seq,\n}: BuildBlockDocumentInput): CmsDocument {\n const id = mintDocumentId(collection.id, seq, existingIds)\n const values = seedDefaults(collection)\n values[blocksField.name] = isBlocksField(blocksField) && blocksField.multiple ? [node] : node\n return { collection: collection.id, id, values }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"transfer.js","sourceRoot":"","sources":["../../../src/components/components/transfer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isBlocksField, isFieldLocalized, seedDefaults } from '@elytracms/core/cms-core'\nimport type { BlocksFieldDef, CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core'\nimport { blockRootsOf, maxNodeIdSeq } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { makeNodeIdCounter } from '../builder/builder-store'\nimport { applyCellValue } from '../cms/cell-edit'\nimport { mintDocumentId } from '../cms/cms-store'\nimport { embedNodeFrom } from './component-preview'\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — the \"design guide → real content\" bridge. A\n * component combination the user has arranged in the Components view (a\n * manifest + edited prop values, captured as a {@link ComponentNode}) is\n * transferred into a document's blocks/composition field — either a NEW DRAFT\n * document created for it ({@link buildBlockDocument}) or an EXISTING\n * document's field, appended alongside whatever it already holds\n * ({@link appendToBlockDocument}). Pure so id-minting/embedding/appending are\n * unit-testable; the React dialog owns the picker, persistence, and\n * navigation.\n */\n\nexport interface CollectionBlocksTarget {\n collection: CollectionDef\n /** The (first) blocks field the node lands in. */\n field: BlocksFieldDef\n}\n\n/**\n * Collections that can receive a transferred combination — those declaring a\n * `blocks` (composition) field. The first blocks field wins (a collection with a\n * single canvas is the norm; a second is an unusual edge we don't disambiguate).\n * Callers additionally gate each target through {@link isComponentAllowed} —\n * this only reports which collections HAVE a blocks field, not which ones\n * accept a given component.\n */\nexport function collectionsWithBlocks(\n collections: readonly CollectionDef[],\n): CollectionBlocksTarget[] {\n const targets: CollectionBlocksTarget[] = []\n for (const collection of collections) {\n const field = collection.fields.find(isBlocksField)\n if (field) targets.push({ collection, field })\n }\n return targets\n}\n\n/**\n * Is `componentId` permitted as a top-level block of `field`? Mirrors\n * `validateCompositionValue`'s (`@elytracms/core/project-graph`) top-level\n * `allow` rule EXACTLY: an absent or empty list permits any registered\n * component; a non-empty list is the closed vocabulary. A target this\n * predicate accepts can never fail that validator's `forbidden-component`\n * check on save — the transfer dialog uses this to hide targets the\n * component isn't allowed into (new AND existing document targets alike).\n */\nexport function isComponentAllowed(field: BlocksFieldDef, componentId: string): boolean {\n return !field.allow || field.allow.length === 0 || field.allow.includes(componentId)\n}\n\n/**\n * Mint a fresh `node-<n>` id for `node`, guaranteed not to collide with any id\n * already present anywhere in `existingRoots`. Reuses the SAME mint the\n * canvas reducer and clipboard-paste path share (`maxNodeIdSeq` +\n * `makeNodeIdCounter`, `builder-store.tsx`) rather than a fixed literal: a\n * brand-new document (`existingRoots` empty) mints `node-1` — matching the\n * transfer's prior behavior — but appending into a document whose blocks\n * already hold `node-1..N` mints past that, never re-using an id already in\n * the tree (the EC-320 follow-up collision hazard).\n */\nfunction embedFreshNode(node: ComponentNode, existingRoots: readonly ComponentNode[]): ComponentNode {\n const maxSeq = existingRoots.reduce((max, root) => Math.max(max, maxNodeIdSeq(root)), 0)\n return embedNodeFrom(node, makeNodeIdCounter(maxSeq).genId())\n}\n\nexport interface BuildBlockDocumentInput {\n collection: CollectionDef\n /** The blocks field the node lands in (from {@link collectionsWithBlocks}). */\n blocksField: BlocksFieldDef\n /** The component node — a manifest id + the edited static props (+ slots). */\n node: ComponentNode\n /** Every doc id already in the target collection (collision guard, EC-316). */\n existingIds: ReadonlySet<string>\n /** The session id counter (mutated by {@link mintDocumentId}). */\n seq: { current: number }\n}\n\n/**\n * Build the draft document that embeds `node` in the collection's blocks field.\n * Reuses the CMS create machinery: {@link mintDocumentId} for a collision-free id\n * (EC-316) and {@link seedDefaults} so declared field defaults are present (as a\n * normal `+ New` would be). A `multiple` blocks field stores an array; a single\n * (`multiple: false`) blocks field stores the lone node — matching how the\n * composition editor persists and `blockRootsOf` reads it back.\n */\nexport function buildBlockDocument({\n collection,\n blocksField,\n node,\n existingIds,\n seq,\n}: BuildBlockDocumentInput): CmsDocument {\n const id = mintDocumentId(collection.id, seq, existingIds)\n const values = seedDefaults(collection)\n const embedded = embedFreshNode(node, [])\n values[blocksField.name] = blocksField.multiple ? [embedded] : embedded\n return { collection: collection.id, id, values }\n}\n\n/** The blocks field's current value on `document`, respecting locale (EC-018). */\nfunction readBlocksValue(\n collection: CollectionDef,\n document: CmsDocument,\n blocksField: BlocksFieldDef,\n locale: Locale,\n): unknown {\n return isFieldLocalized(collection, blocksField.name)\n ? document.localized?.[locale]?.[blocksField.name]\n : document.values[blocksField.name]\n}\n\nexport interface CanAppendToInput {\n collection: CollectionDef\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Can `node` be appended to `document`'s blocks field without destroying\n * existing content? Always true for a `multiple` field (an ordered list has\n * room for one more); a single-root (`multiple: false`) field only while it\n * is empty — occupied, there is no second root to hold it without\n * overwriting the existing block, so the transfer dialog excludes that\n * document from its offered targets rather than risk data loss.\n */\nexport function canAppendTo({ collection, document, blocksField, locale }: CanAppendToInput): boolean {\n if (blocksField.multiple) return true\n return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length === 0\n}\n\nexport interface AppendToBlockDocumentInput {\n collection: CollectionDef\n /** The document to append into (the transfer dialog's \"insert into existing document\"). */\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The component node to append — not yet re-ided (fresh id minted here). */\n node: ComponentNode\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Append `node` into an EXISTING document's blocks field (EC-320 follow-up,\n * 0.0.44 — \"insert into existing document\"), leaving everything else about\n * the document untouched: other fields, other locale bundles, and the\n * field's own existing content, which the embed always lands AFTER.\n *\n * Reads the current roots via `blockRootsOf` (`@elytracms/core/project-graph`\n * — the same reader the composition editor, table cell, and reuse-context\n * use), so this reads the field back exactly like every other consumer,\n * undefined-safe for a never-set field. Locale-aware writes go through\n * {@link applyCellValue} (the same locale-write helper inline cell edits use,\n * `cell-edit.ts`) so a localized blocks field's OTHER locale bundles are\n * never touched, only the edited locale's.\n *\n * Node-id collision guard: `node` is re-embedded under a fresh id minted past\n * every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`\n * — a document whose blocks already hold that id would otherwise collide.\n *\n * Throws if `blocksField` is a single-root (`multiple: false`) field that\n * already holds a root — a defensive invariant guard, not a user-facing\n * validation path: the caller must pre-filter candidates with\n * {@link canAppendTo} so this is never reached in that state.\n */\nexport function appendToBlockDocument({\n collection,\n document,\n blocksField,\n node,\n locale,\n}: AppendToBlockDocumentInput): CmsDocument {\n const existingRoots = blockRootsOf(readBlocksValue(collection, document, blocksField, locale))\n if (!blocksField.multiple && existingRoots.length > 0) {\n throw new Error(\n `Cannot append to single-root blocks field \"${blocksField.name}\": already holds a component (caller must gate with canAppendTo).`,\n )\n }\n const embedded = embedFreshNode(node, existingRoots)\n const nextValue = blocksField.multiple ? [...existingRoots, embedded] : embedded\n return applyCellValue(collection, document, blocksField.name, locale, nextValue)\n}\n"]}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ReactElement } from 'react';
|
|
2
2
|
/**
|
|
3
|
-
* The shared error/loading status gate for a rail module (EC-319/EC-323
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* The shared error/loading status gate for a rail module (EC-319/EC-323,
|
|
4
|
+
* repointed onto `SectionStateView` in EC-327). The assets, users, and
|
|
5
|
+
* redirects rails each read a list store with the same
|
|
6
|
+
* `{ status, errorMessage, refresh }` shape; this renders the shared
|
|
7
|
+
* error/loading section state — each rail's own translated `errorLabel`/
|
|
8
|
+
* `loadingLabel` copy, not a generic one — instead of a rail re-inlining its
|
|
9
|
+
* own AlertTriangle+retry panel. Returns `null` for a `ready` store.
|
|
8
10
|
*/
|
|
9
11
|
export declare function RailStoreStatus({ store, errorLabel, loadingLabel, errorTestId, }: {
|
|
10
12
|
store: {
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { Button
|
|
2
|
+
import { RotateCcw } from 'lucide-react';
|
|
3
|
+
import { Button } from '@elytracms/studio/ui';
|
|
4
4
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
|
+
import { SectionStateView } from './section-state';
|
|
5
6
|
/**
|
|
6
|
-
* The shared error/loading status gate for a rail module (EC-319/EC-323
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The shared error/loading status gate for a rail module (EC-319/EC-323,
|
|
8
|
+
* repointed onto `SectionStateView` in EC-327). The assets, users, and
|
|
9
|
+
* redirects rails each read a list store with the same
|
|
10
|
+
* `{ status, errorMessage, refresh }` shape; this renders the shared
|
|
11
|
+
* error/loading section state — each rail's own translated `errorLabel`/
|
|
12
|
+
* `loadingLabel` copy, not a generic one — instead of a rail re-inlining its
|
|
13
|
+
* own AlertTriangle+retry panel. Returns `null` for a `ready` store.
|
|
11
14
|
*/
|
|
12
15
|
export function RailStoreStatus({ store, errorLabel, loadingLabel, errorTestId, }) {
|
|
13
16
|
if (store.status === 'error') {
|
|
14
|
-
return (
|
|
17
|
+
return (_jsx("div", { "data-testid": errorTestId, children: _jsx(SectionStateView, { state: "error", title: errorLabel, description: store.errorMessage ?? translate('common.backendUnreachable'), action: _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void store.refresh(), children: [_jsx(RotateCcw, {}), " ", translate('common.retry')] }) }) }));
|
|
15
18
|
}
|
|
16
19
|
if (store.status === 'loading') {
|
|
17
|
-
return
|
|
20
|
+
return _jsx(SectionStateView, { state: "loading", title: loadingLabel });
|
|
18
21
|
}
|
|
19
22
|
return null;
|
|
20
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rail-store-status.js","sourceRoot":"","sources":["../../src/components/rail-store-status.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"rail-store-status.js","sourceRoot":"","sources":["../../src/components/rail-store-status.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,KAAK,EACL,UAAU,EACV,YAAY,EACZ,WAAW,GAaZ;IACC,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,6BAAkB,WAAW,YAC3B,KAAC,gBAAgB,IACf,KAAK,EAAC,OAAO,EACb,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,KAAK,CAAC,YAAY,IAAI,SAAS,CAAC,2BAA2B,CAAC,EACzE,MAAM,EACJ,MAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,aACrE,KAAC,SAAS,KAAG,OAAE,SAAS,CAAC,cAAc,CAAC,IACjC,GAEX,GACE,CACP,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,KAAC,gBAAgB,IAAC,KAAK,EAAC,SAAS,EAAC,KAAK,EAAE,YAAY,GAAI,CAAA;IAClE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import type { ReactElement } from 'react'\nimport { RotateCcw } from 'lucide-react'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { SectionStateView } from './section-state'\n\n/**\n * The shared error/loading status gate for a rail module (EC-319/EC-323,\n * repointed onto `SectionStateView` in EC-327). The assets, users, and\n * redirects rails each read a list store with the same\n * `{ status, errorMessage, refresh }` shape; this renders the shared\n * error/loading section state — each rail's own translated `errorLabel`/\n * `loadingLabel` copy, not a generic one — instead of a rail re-inlining its\n * own AlertTriangle+retry panel. Returns `null` for a `ready` store.\n */\nexport function RailStoreStatus({\n store,\n errorLabel,\n loadingLabel,\n errorTestId,\n}: {\n store: {\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n refresh: () => Promise<void>\n }\n /** Localized \"…failed to load\" heading for this rail. */\n errorLabel: string\n /** Localized \"Loading …\" text for this rail. */\n loadingLabel: string\n /** data-testid on the error container (per-rail, for tests). */\n errorTestId: string\n}): ReactElement | null {\n if (store.status === 'error') {\n return (\n <div data-testid={errorTestId}>\n <SectionStateView\n state=\"error\"\n title={errorLabel}\n description={store.errorMessage ?? translate('common.backendUnreachable')}\n action={\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> {translate('common.retry')}\n </Button>\n }\n />\n </div>\n )\n }\n if (store.status === 'loading') {\n return <SectionStateView state=\"loading\" title={loadingLabel} />\n }\n return null\n}\n"]}
|