@elytracms/studio 0.0.37 → 0.0.39
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 +8 -2
- package/dist/components/app-views/default-modules.d.ts +60 -5
- package/dist/components/app-views/default-modules.js +53 -15
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +2 -0
- package/dist/components/app-views/module.js +8 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/assets/asset-detail.d.ts +24 -0
- package/dist/components/assets/asset-detail.js +163 -0
- package/dist/components/assets/asset-detail.js.map +1 -0
- package/dist/components/assets/asset-grid.d.ts +28 -0
- package/dist/components/assets/asset-grid.js +68 -0
- package/dist/components/assets/asset-grid.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +19 -0
- package/dist/components/assets/asset-metadata.js +44 -0
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
- package/dist/components/assets/assets-collection-view-source.js +186 -0
- package/dist/components/assets/assets-collection-view-source.js.map +1 -0
- package/dist/components/assets/assets-module.d.ts +18 -0
- package/dist/components/assets/assets-module.js +94 -0
- package/dist/components/assets/assets-module.js.map +1 -0
- package/dist/components/assets/assets-store.js +2 -7
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.d.ts +15 -0
- package/dist/components/assets/assets-view-preference.js +22 -0
- package/dist/components/assets/assets-view-preference.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +12 -0
- package/dist/components/builder/builder-store.js +18 -16
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -14
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
- package/dist/components/builder/composition-field-editor-lazy.js +29 -0
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
- package/dist/components/builder/composition-field-editor.js +1 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +8 -0
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +1 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +7 -134
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +15 -4
- package/dist/components/cms/cms-store.js +42 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +8 -1
- package/dist/components/cms/collection-table.js +62 -10
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/column-width.d.ts +25 -0
- package/dist/components/cms/column-width.js +47 -0
- package/dist/components/cms/column-width.js.map +1 -0
- package/dist/components/cms/detail-editor.js +101 -68
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +32 -0
- package/dist/components/cms/discard-draft.js +13 -0
- package/dist/components/cms/discard-draft.js.map +1 -0
- package/dist/components/cms/document-staging.d.ts +7 -0
- package/dist/components/cms/document-staging.js +44 -0
- package/dist/components/cms/document-staging.js.map +1 -0
- package/dist/components/cms/field-connection.d.ts +22 -0
- package/dist/components/cms/field-connection.js +37 -0
- package/dist/components/cms/field-connection.js.map +1 -0
- package/dist/components/cms/field-control.js +158 -34
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +43 -0
- package/dist/components/cms/prop-fields.js +130 -0
- package/dist/components/cms/prop-fields.js.map +1 -0
- package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
- package/dist/components/cms/rich-text/component-embed.js +29 -7
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +3 -0
- package/dist/components/cms/rich-text/editor.js +21 -3
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +17 -0
- package/dist/components/cms/rich-text/embed-picker.js +34 -0
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -0
- package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
- package/dist/components/cms/rich-text/embed-props.js +106 -0
- package/dist/components/cms/rich-text/embed-props.js.map +1 -0
- package/dist/components/cms/rich-text/extensions.d.ts +7 -1
- package/dist/components/cms/rich-text/extensions.js +6 -1
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/index.d.ts +6 -1
- package/dist/components/cms/rich-text/index.js +6 -1
- package/dist/components/cms/rich-text/index.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +21 -0
- package/dist/components/cms/rich-text/insert.js +55 -0
- package/dist/components/cms/rich-text/insert.js.map +1 -0
- package/dist/components/cms/rich-text/lazy.d.ts +19 -0
- package/dist/components/cms/rich-text/lazy.js +26 -0
- package/dist/components/cms/rich-text/lazy.js.map +1 -0
- package/dist/components/cms/rich-text/slash-command.d.ts +27 -0
- package/dist/components/cms/rich-text/slash-command.js +158 -0
- package/dist/components/cms/rich-text/slash-command.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar.d.ts +5 -6
- package/dist/components/cms/rich-text/toolbar.js +3 -5
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +28 -0
- package/dist/components/cms/row-actions.js +20 -0
- package/dist/components/cms/row-actions.js.map +1 -0
- package/dist/components/cms/use-document-versions.d.ts +8 -1
- package/dist/components/cms/use-document-versions.js +31 -4
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.d.ts +1 -0
- package/dist/components/cms/version-compare-lazy.js +23 -0
- package/dist/components/cms/version-compare-lazy.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +46 -0
- package/dist/components/cms/version-compare-text.js +78 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/cms/version-compare.d.ts +9 -5
- package/dist/components/cms/version-compare.js +184 -59
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +10 -4
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +5 -3
- package/dist/components/components/component-detail.js +41 -8
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +61 -0
- package/dist/components/components/component-preview.js +103 -0
- package/dist/components/components/component-preview.js.map +1 -0
- package/dist/components/components/components-index.d.ts +5 -3
- package/dist/components/components/components-index.js +19 -24
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +7 -0
- package/dist/components/components/registry-browser.js +20 -36
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +51 -0
- package/dist/components/components/taxonomy.js +73 -0
- package/dist/components/components/taxonomy.js.map +1 -0
- package/dist/components/components/transfer-dialog.d.ts +20 -0
- package/dist/components/components/transfer-dialog.js +64 -0
- package/dist/components/components/transfer-dialog.js.map +1 -0
- package/dist/components/components/transfer.d.ts +43 -0
- package/dist/components/components/transfer.js +31 -0
- package/dist/components/components/transfer.js.map +1 -0
- package/dist/components/project-list.js +3 -4
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.js +3 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +21 -0
- package/dist/components/rail-store-status.js +21 -0
- package/dist/components/rail-store-status.js.map +1 -0
- package/dist/components/redirects/redirects-module.js +4 -8
- package/dist/components/redirects/redirects-module.js.map +1 -1
- package/dist/components/redirects/redirects-store.js +2 -7
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/sign-in-needed-badge.d.ts +6 -0
- package/dist/components/sign-in-needed-badge.js +13 -0
- package/dist/components/sign-in-needed-badge.js.map +1 -0
- package/dist/components/studio-project-screen.js +1 -4
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +1 -4
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/users/users-module.js +5 -7
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-store.js +2 -7
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +1 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +0 -2
- package/dist/lib/cms-views/view-model.js +0 -7
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/diff-view-preference.d.ts +7 -0
- package/dist/lib/diff-view-preference.js +21 -0
- package/dist/lib/diff-view-preference.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +81 -30
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/local-preference.d.ts +11 -0
- package/dist/lib/local-preference.js +26 -0
- package/dist/lib/local-preference.js.map +1 -0
- package/dist/lib/locale/fallback.d.ts +45 -0
- package/dist/lib/locale/fallback.js +85 -0
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +6 -0
- package/dist/lib/persona/labels.js +8 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +1 -1
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +14 -2
- package/dist/lib/query/operations-query.js +11 -6
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/rail.d.ts +0 -6
- package/dist/lib/rail.js +6 -25
- package/dist/lib/rail.js.map +1 -1
- package/dist/lib/theme.d.ts +0 -6
- package/dist/lib/theme.js +10 -13
- package/dist/lib/theme.js.map +1 -1
- package/dist/router/match.d.ts +1 -0
- package/dist/router/match.js +4 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +22 -24
- package/src/styles/studio.css +17 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Link, STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
|
|
3
|
-
import { Check, CircleAlert, CircleCheck, ExternalLink, Eye, EyeOff, GitCompareArrows, History,
|
|
3
|
+
import { Check, CircleAlert, CircleCheck, ExternalLink, Eye, EyeOff, GitCompareArrows, History, Loader2, Lock, MoreHorizontal, RotateCcw, Save, Tag, Trash2, X, } from 'lucide-react';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { FIELD_WIDTH_SPANS, SIDEBAR_FIELD_GROUP, clearHiddenValues, documentKey, groupOrder, groupSectionOf, hasAliases, hiddenGroupNames, isBlocksField, isFieldHidden, isFieldLocalized, isSingleton, resolveDocumentForLocale, resolveDocumentTitle, resolveField, sidebarFields, tabDescriptorOf, tabOrder, titleFieldOf, staticFieldHidden, urlForDocument, } from '@elytracms/core/cms-core';
|
|
6
6
|
import { blockRootsOf } from '@elytracms/core/project-graph';
|
|
@@ -8,12 +8,16 @@ import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter,
|
|
|
8
8
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
9
9
|
import { collectEmbedRefs, resolveReuseCapability } from '../builder/reuse';
|
|
10
10
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
11
|
+
import { useOperationCache } from '@elytracms/studio/lib/query';
|
|
11
12
|
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
12
|
-
import { LocaleSwitcher, displayedFieldFallback,
|
|
13
|
+
import { LocaleSwitcher, activeLocaleValues, displayedFieldFallback, fieldConnection, fillFieldFromBase, resetFieldToBase, setFieldValue, setStudioLocale, } from '@elytracms/studio/lib/locale';
|
|
13
14
|
import { parseDocumentFieldPath } from '@elytracms/studio/lib/references';
|
|
14
|
-
import { FallbackChip
|
|
15
|
+
import { FallbackChip } from './cell';
|
|
16
|
+
import { FieldConnectionControl } from './field-connection';
|
|
15
17
|
import { FieldControl } from './field-control';
|
|
16
|
-
|
|
18
|
+
// EC-322: the lazy-wrapped page builder — the heavy canvas subtree code-splits out
|
|
19
|
+
// of the detail-view critical path and downloads only when a `blocks` field opens.
|
|
20
|
+
import { CompositionFieldEditor } from '../builder/composition-field-editor-lazy';
|
|
17
21
|
import { fieldLabel } from './cms-store';
|
|
18
22
|
import { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source';
|
|
19
23
|
import { CollectionViewProvider, useCollectionView } from '../collection-view/collection-view-context';
|
|
@@ -25,14 +29,16 @@ import { ReferencesPanel } from '../references/references-panel';
|
|
|
25
29
|
import { InspectorSidebar } from '../inspector/inspector-sidebar';
|
|
26
30
|
import { DocumentEditorProvider } from './document-editor-context';
|
|
27
31
|
import { VersionsPane } from './versions-pane';
|
|
28
|
-
|
|
32
|
+
// EC-322: the lazy-wrapped compare view — its diff UI code-splits off the detail-view
|
|
33
|
+
// critical path and loads only when the Compare tab opens.
|
|
34
|
+
import { VersionCompare } from './version-compare-lazy';
|
|
35
|
+
import { useDocumentVersionsQuery } from './use-document-versions';
|
|
36
|
+
import { discardDraftStatus } from './discard-draft';
|
|
29
37
|
import { IssuesPane } from './issues-pane';
|
|
30
38
|
import { DetailsPane } from './details-pane';
|
|
31
39
|
import { useSetBreadcrumb } from '../breadcrumb-context';
|
|
32
40
|
import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
33
41
|
import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
|
|
34
|
-
/** The conventional slug field name (mirrors `url-for-document`'s `SLUG_FIELD`). */
|
|
35
|
-
const SLUG_FIELD = 'slug';
|
|
36
42
|
export function DetailEditor({ projectId, collectionId, documentId, focusPath, }) {
|
|
37
43
|
// AD-19: the edit shell is a portable surface over a CollectionViewSource, just
|
|
38
44
|
// like the list. `DetailEditor` is the CMS entry point — it builds the CMS source
|
|
@@ -144,7 +150,59 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
144
150
|
const viewReadOnly = perspective === 'published';
|
|
145
151
|
const viewDoc = viewReadOnly && publishedSnapshot.status === 'ready' ? publishedSnapshot.doc : draft;
|
|
146
152
|
const publishedPending = viewReadOnly && publishedSnapshot.status !== 'ready';
|
|
147
|
-
|
|
153
|
+
// EC-316 — "Discard changes": revert the draft to exactly the published
|
|
154
|
+
// content. Only meaningful for a publishing + versioned rail AND a published
|
|
155
|
+
// document, so the history query is gated off otherwise (never a wasted
|
|
156
|
+
// fetch on e.g. the users rail or a never-published draft).
|
|
157
|
+
const canDiscard = canPublish && canVersions;
|
|
158
|
+
const { versionsQuery: discardVersionsQuery, versionKey: discardVersionKey } = useDocumentVersionsQuery(ref, {
|
|
159
|
+
enabled: canDiscard && published,
|
|
160
|
+
});
|
|
161
|
+
const discardCache = useOperationCache();
|
|
162
|
+
const [discardConfirm, setDiscardConfirm] = useState(false);
|
|
163
|
+
const [discardBusy, setDiscardBusy] = useState(false);
|
|
164
|
+
const discardView = discardVersionsQuery.data ?? null;
|
|
165
|
+
const publishedVersionNumber = discardView?.publishedVersion;
|
|
166
|
+
const publishedSnapshotForDiscard = publishedVersionNumber !== undefined
|
|
167
|
+
? discardView?.versions.find((v) => v.version === publishedVersionNumber)?.snapshot
|
|
168
|
+
: undefined;
|
|
169
|
+
// One pure decision (tested in discard-draft.test.ts): hidden / disabled-
|
|
170
|
+
// with-reason / ready. Its semantic equality is the SAME comparator the
|
|
171
|
+
// Versions pane's publish gate uses, so the two never contradict (EC-298).
|
|
172
|
+
const discardStatus = discardDraftStatus({
|
|
173
|
+
canDiscard,
|
|
174
|
+
published,
|
|
175
|
+
viewReadOnly,
|
|
176
|
+
publishedSnapshot: publishedSnapshotForDiscard,
|
|
177
|
+
collection,
|
|
178
|
+
draft,
|
|
179
|
+
});
|
|
180
|
+
const discardDisabledReason = discardStatus === 'view-read-only'
|
|
181
|
+
? translate('cms.editor.switchToDraftToDiscard')
|
|
182
|
+
: discardStatus === 'up-to-date'
|
|
183
|
+
? translate('cms.editor.discardDraftUpToDateHint')
|
|
184
|
+
: discardStatus === 'loading'
|
|
185
|
+
? translate('common.loading')
|
|
186
|
+
: undefined;
|
|
187
|
+
// Reuses the EC-298 restore machinery (the same op the Compare view's
|
|
188
|
+
// "Restore version" button calls) — never a parallel write path: reverting
|
|
189
|
+
// the draft IS restoring the published version's snapshot, whole-document.
|
|
190
|
+
async function handleDiscardDraft() {
|
|
191
|
+
if (publishedVersionNumber === undefined)
|
|
192
|
+
return;
|
|
193
|
+
setDiscardBusy(true);
|
|
194
|
+
try {
|
|
195
|
+
const restored = await source.versioning?.restore(ref, publishedVersionNumber, { kind: 'all' });
|
|
196
|
+
if (restored)
|
|
197
|
+
replaceDraft(restored);
|
|
198
|
+
await discardCache.invalidate(discardVersionKey);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
setDiscardBusy(false);
|
|
202
|
+
setDiscardConfirm(false);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(doc), [draft, doc]);
|
|
148
206
|
const schemaIssues = useMemo(() => source.validate(draft), [source, draft]);
|
|
149
207
|
// EC-300/AD-16: composition embeds (hard references) whose target isn't published
|
|
150
208
|
// would render empty on the live site. Surface them as errors in the Issues panel,
|
|
@@ -327,29 +385,18 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
327
385
|
}
|
|
328
386
|
return viewDoc.values[field.name];
|
|
329
387
|
}
|
|
388
|
+
// EC-176/EC-318: the present/absent-key write is single-sourced in `setFieldValue`
|
|
389
|
+
// — a localized field writes `localized[locale]`, clearing (value === undefined)
|
|
390
|
+
// DELETES the key (reconnecting to base). `fill from base` / `reset to base` below
|
|
391
|
+
// reuse the same foundation, so typing, filling, and resetting can never diverge.
|
|
330
392
|
function setValue(field, value) {
|
|
331
|
-
setDraft((d) =>
|
|
332
|
-
if (isFieldLocalized(collection, field.name)) {
|
|
333
|
-
const perLocale = { ...(d.localized?.[locale] ?? {}) };
|
|
334
|
-
if (value === undefined)
|
|
335
|
-
delete perLocale[field.name];
|
|
336
|
-
else
|
|
337
|
-
perLocale[field.name] = value;
|
|
338
|
-
return { ...d, localized: { ...d.localized, [locale]: perLocale } };
|
|
339
|
-
}
|
|
340
|
-
const values = { ...d.values };
|
|
341
|
-
if (value === undefined)
|
|
342
|
-
delete values[field.name];
|
|
343
|
-
else
|
|
344
|
-
values[field.name] = value;
|
|
345
|
-
return { ...d, values };
|
|
346
|
-
});
|
|
393
|
+
setDraft((d) => setFieldValue(collection, d, field.name, locale, value));
|
|
347
394
|
}
|
|
348
395
|
// EC-273: conditional-visibility context — the draft's values for the active
|
|
349
396
|
// locale, against which a field's `hidden(values)` predicate is evaluated. A
|
|
350
397
|
// hidden field's row is omitted (and it is not validated). Recomputed as the
|
|
351
398
|
// draft changes, so a field shows/hides live as the editor types.
|
|
352
|
-
const conditionValues =
|
|
399
|
+
const conditionValues = activeLocaleValues(draft, locale);
|
|
353
400
|
const isHidden = (field) => isFieldHidden(field, { values: conditionValues });
|
|
354
401
|
// Visible = not content-hidden AND not principal-hidden (EC-240 access === 'hidden').
|
|
355
402
|
const isFieldShown = (field) => !isHidden(field) && accessOf(field) !== 'hidden';
|
|
@@ -375,28 +422,32 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
375
422
|
const fieldLevelReadOnly = access === 'read';
|
|
376
423
|
const fieldReadOnly = viewReadOnly || fieldLevelReadOnly;
|
|
377
424
|
const localized = isFieldLocalized(collection, field.name);
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
425
|
+
// EC-318 — connected fields: the per-locale connection state of a localized
|
|
426
|
+
// field (connected/diverged + fill-from-base), DERIVED from the draft, never
|
|
427
|
+
// stored. Null for a non-localized field or the base-locale view. Suppressed
|
|
428
|
+
// while the row is read-only (Published view or a field-level `read` grant) —
|
|
429
|
+
// there is nothing to connect or diverge there.
|
|
430
|
+
const connection = localized
|
|
431
|
+
? fieldConnection(collection, draft, field.name, locale, source.localeConfig)
|
|
382
432
|
: null;
|
|
383
|
-
|
|
384
|
-
// have it yet): editing a fallback-shown value is an explicit translation act.
|
|
385
|
-
// Moot (and hidden) while viewing the read-only published snapshot.
|
|
386
|
-
const newlyTranslated = !viewReadOnly &&
|
|
387
|
-
localized &&
|
|
388
|
-
hasLocaleVariant(draft, field.name, locale) &&
|
|
389
|
-
!hasLocaleVariant(doc, field.name, locale);
|
|
390
|
-
return (_jsxs(Field, { className: cn('rounded-md transition-shadow', highlightedField === field.name && 'bg-primary/5 p-2 ring-2 ring-primary/70'), label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(field) }), field.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null, localized ? _jsx(Badge, { variant: "outline", children: locale }) : null, fallback ? (_jsx(Badge, { variant: "warning", title: fallbackTitle(fallback), "data-testid": `field-fallback-${field.name}`, children: translate('cms.editor.fieldShowingFallback', {
|
|
391
|
-
locale: localeChipLabel(fallback.sourceLocale),
|
|
392
|
-
}) })) : null, newlyTranslated ? (_jsxs(Badge, { variant: "success", "data-testid": `field-translated-${field.name}`, children: [_jsx(Languages, { className: "size-3" }), ' ', translate('cms.editor.fieldTranslatedTo', { locale: localeChipLabel(locale) })] })) : null, fieldLevelReadOnly ? (_jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })) : null, _jsx(Spacer, {})] }), htmlFor: `f-${field.name}`, hint: fallback ? (translate('cms.editor.fieldFallbackHint', {
|
|
393
|
-
source: localeChipLabel(fallback.sourceLocale),
|
|
394
|
-
locale: localeChipLabel(locale),
|
|
395
|
-
})) : (localizedLabel(field.form?.description) || undefined), error: fieldReadOnly ? undefined : errorsByField.get(field.name), children: [_jsx(FieldControl, { id: `f-${field.name}`, field: field, value: getValue(field), onChange: (v) => setValue(field, v), readOnly: fieldReadOnly }), field.name === SLUG_FIELD && routableCollection ? _jsx(PathPreview, { url: draftUrl }) : null] }, field.name));
|
|
433
|
+
return (_jsx(Field, { className: cn('rounded-md transition-shadow', highlightedField === field.name && 'bg-primary/5 p-2 ring-2 ring-primary/70'), label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(field) }), field.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null, localized ? _jsx(Badge, { variant: "outline", children: locale }) : null, fieldLevelReadOnly ? (_jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })) : null, _jsx(Spacer, {}), connection && !fieldReadOnly ? (_jsx(FieldConnectionControl, { connection: connection, fieldName: field.name, locale: locale, onFillFromBase: () => setDraft((d) => fillFieldFromBase(collection, d, field.name, locale, source.localeConfig)), onResetToBase: () => setDraft((d) => resetFieldToBase(collection, d, field.name, locale)) })) : null] }), htmlFor: `f-${field.name}`, hint: localizedLabel(field.form?.description) || undefined, error: fieldReadOnly ? undefined : errorsByField.get(field.name), children: _jsx(FieldControl, { id: `f-${field.name}`, field: field, value: getValue(field), onChange: (v) => setValue(field, v), readOnly: fieldReadOnly }) }, field.name));
|
|
396
434
|
}
|
|
397
435
|
function save() {
|
|
398
436
|
source.save(draft);
|
|
399
437
|
}
|
|
438
|
+
// EC-316: "Save & close" — the SAME save path as everywhere else in this
|
|
439
|
+
// header (autosave's debounce, the explicit-save rail's Save button, and the
|
|
440
|
+
// unmount flush all call this exact `save()`), immediately, then navigate
|
|
441
|
+
// back to the rail's own list route. Rail-owned (`listPath`, AD-19) so this
|
|
442
|
+
// never hard-codes the CMS route; falls back to the project index for the
|
|
443
|
+
// rare rail with no list surface at all (mirrors the delete flow below).
|
|
444
|
+
function saveAndClose() {
|
|
445
|
+
save();
|
|
446
|
+
void navigate({
|
|
447
|
+
to: STUDIO_PROJECT_ROUTE,
|
|
448
|
+
params: { projectId, _splat: source.listPath ?? studioPath.index() },
|
|
449
|
+
});
|
|
450
|
+
}
|
|
400
451
|
// EC-224: a version restore swaps the working draft for an older snapshot; the
|
|
401
452
|
// open editor adopts it so the form/canvas re-render from the restored content
|
|
402
453
|
// (the operation already persisted it, so it lands clean/non-dirty).
|
|
@@ -571,12 +622,10 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
571
622
|
localeConfig: source.localeConfig,
|
|
572
623
|
}, collection, published, { siblings: [...publishedDocs.byId.values()], locale });
|
|
573
624
|
}, [publishedDocs, collection, doc.id, locale, source.localeConfig]);
|
|
574
|
-
// EC-246d: the DRAFT path
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
// resolves a route (a fresh hierarchy page with no slug has no path yet).
|
|
579
|
-
const draftUrl = useMemo(() => urlForDocument({ routes: resolveDeploymentRoutes(), localeConfig: source.localeConfig }, collection, viewDoc, { siblings: source.documents(), locale }), [collection, viewDoc, locale, source]);
|
|
625
|
+
// EC-246d/EC-315: the DRAFT composed path moved INTO the slug control itself
|
|
626
|
+
// (`SlugField`), which derives + decomposes it from the same routes; the editor no
|
|
627
|
+
// longer computes a separate `draftUrl`. `liveUrl` (published) still gates the
|
|
628
|
+
// header's "View live" button — the sole open-live, so draft never links out.
|
|
580
629
|
// EC-240: hide the tab bar when there is nothing to navigate — a single content
|
|
581
630
|
// tab with no References/Compare (the users rail). When framework tabs ARE
|
|
582
631
|
// present we KEEP the content tab(s) so there is always a way back to the form;
|
|
@@ -620,18 +669,11 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
620
669
|
? translate(errorCount === 1
|
|
621
670
|
? 'cms.editor.fixIssuesToPublish.one'
|
|
622
671
|
: 'cms.editor.fixIssuesToPublish.other', { count: errorCount })
|
|
623
|
-
: undefined, children: publishState === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.publishing')] })) : publishState === 'success' ? (_jsxs(_Fragment, { children: [_jsx(CircleCheck, {}), " ", translate('cms.editor.publishedState')] })) : (_jsxs(_Fragment, { children: [_jsx(Eye, {}), " ", translate('cms.editor.publish')] })) })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : null, published || !singleton ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.editor.moreActions'), children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [published ? (_jsxs(DropdownMenuItem, { onClick: () => setConfirmAction('unpublish'), children: [_jsx(EyeOff, {}), " ", translate('cms.editor.unpublish')] })) : null, singleton ? null : (_jsxs(DropdownMenuItem, { "data-testid": "doc-delete", className: "text-destructive", onClick: () => setConfirmAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }))] })] })) : null] }), showLensRow ? (_jsxs(Row, { gap: "3", className: "mt-3", wrap: true, children: [_jsxs(Row, { gap: "1", align: "center", children: [showContentTabs ? tabs.map((tab) => renderContentTab(tab)) : null, showReferences ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), renderContentTab(REFERENCES_TAB)] })) : null, compare ? (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', activeTab === COMPARE_TAB
|
|
672
|
+
: undefined, children: publishState === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.publishing')] })) : publishState === 'success' ? (_jsxs(_Fragment, { children: [_jsx(CircleCheck, {}), " ", translate('cms.editor.publishedState')] })) : (_jsxs(_Fragment, { children: [_jsx(Eye, {}), " ", translate('cms.editor.publish')] })) })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : null, !singleton && source.listPath !== undefined ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: saveAndClose, "data-testid": "doc-save-and-close", children: [_jsx(Save, {}), " ", translate('cms.editor.saveAndClose')] })) : null, published || !singleton ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.editor.moreActions'), children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [discardStatus !== 'hidden' ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-discard-draft", disabled: discardStatus !== 'ready', title: discardDisabledReason, onClick: () => setDiscardConfirm(true), children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraft')] })) : null, published ? (_jsxs(DropdownMenuItem, { onClick: () => setConfirmAction('unpublish'), children: [_jsx(EyeOff, {}), " ", translate('cms.editor.unpublish')] })) : null, singleton ? null : (_jsxs(DropdownMenuItem, { "data-testid": "doc-delete", className: "text-destructive", onClick: () => setConfirmAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }))] })] })) : null] }), showLensRow ? (_jsxs(Row, { gap: "3", className: "mt-3", wrap: true, children: [_jsxs(Row, { gap: "1", align: "center", children: [showContentTabs ? tabs.map((tab) => renderContentTab(tab)) : null, showReferences ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), renderContentTab(REFERENCES_TAB)] })) : null, compare ? (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', activeTab === COMPARE_TAB
|
|
624
673
|
? 'bg-accent text-accent-foreground'
|
|
625
674
|
: 'text-muted-foreground hover:bg-muted'), children: [_jsxs("button", { type: "button", onClick: () => setActiveTab(COMPARE_TAB), "aria-pressed": activeTab === COMPARE_TAB, className: "inline-flex items-center gap-1", children: [_jsx(GitCompareArrows, { className: "size-3", "aria-hidden": true }), " ", translate('cms.version.compare')] }), _jsx("button", { type: "button", onClick: exitCompare, "aria-label": translate('cms.version.closeCompare'), className: "ml-0.5 rounded p-0.5 hover:bg-background/40", children: _jsx(X, { className: "size-3" }) })] })) : null] }), _jsx(Spacer, {}), canLocales ? _jsx(LocaleSwitcher, {}) : null, canPublish ? (_jsxs(Row, { gap: "px", className: "rounded-md bg-muted/60 p-0.5", children: [_jsx(PerspectiveButton, { label: translate('cms.editor.draftState'), active: perspective === 'draft', onClick: () => setPerspective('draft') }), _jsx(PerspectiveButton, { label: translate('cms.editor.publishedState'), active: perspective === 'published', accent: "info", disabled: !published, title: published ? undefined : translate('cms.editor.publishToViewPublished'), onClick: () => setPerspective('published') })] })) : null] })) : null] }), viewReadOnly ? (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-info/30 bg-info/10 px-4 py-1.5 text-info sm:px-6", "data-testid": "published-view-banner", children: [_jsx(Lock, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "text-info", children: translate('cms.editor.publishedReadOnlyBanner') }), _jsx(Spacer, {}), _jsx("button", { type: "button", onClick: () => setPerspective('draft'), className: "text-xs font-medium underline-offset-2 hover:underline", children: translate('cms.editor.switchToDraft') })] })) : null, publishedPending ? (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: publishedSnapshot.status === 'loading'
|
|
626
675
|
? translate('cms.editor.loadingPublishedVersion')
|
|
627
|
-
: translate('cms.editor.noPublishedVersion') }) })) : activeTab === COMPARE_TAB && compare ? (
|
|
628
|
-
// EC-298: the version compare view takes over the main area, with the
|
|
629
|
-
// shared inspector (Versions pane) staying on as the navigator — an
|
|
630
|
-
// in-app surface, not a modal, so arbitrary versions can be browsed.
|
|
631
|
-
_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: _jsx(VersionCompare, {}) }), _jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" })] })) : canvasField ? (_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(CompositionFieldEditor, { field: canvasField, value: getValue(canvasField), onChange: (v) => setValue(canvasField, v), readOnly: viewReadOnly || source.isFieldReadOnly(canvasField), trailingPanes: trailingPanes }, `${canvasField.name}:${restoreNonce}:${perspective}`) })) : activeTab === REFERENCES_TAB ? (
|
|
632
|
-
// EC-269: mounted only while this tab is active → reference data loads on
|
|
633
|
-
// demand and is torn down on leave (no fetching during normal editing).
|
|
634
|
-
_jsx("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: _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, source: { type: 'document', id: documentKey(ref) }, target: { type: 'document', id: documentKey(ref) } }) }) }) })) : (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_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(Stack, { gap: "6", children: groups.map((group) => {
|
|
676
|
+
: translate('cms.editor.noPublishedVersion') }) })) : activeTab === COMPARE_TAB && compare ? (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: _jsx(VersionCompare, {}) }), _jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" })] })) : canvasField ? (_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(CompositionFieldEditor, { field: canvasField, value: getValue(canvasField), onChange: (v) => setValue(canvasField, v), readOnly: viewReadOnly || source.isFieldReadOnly(canvasField), trailingPanes: trailingPanes }, `${canvasField.name}:${restoreNonce}:${perspective}`) })) : activeTab === REFERENCES_TAB ? (_jsx("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: _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, source: { type: 'document', id: documentKey(ref) }, target: { type: 'document', id: documentKey(ref) } }) }) }) })) : (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_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(Stack, { gap: "6", children: groups.map((group) => {
|
|
635
677
|
// EC-306: a conditionally/statically hidden group section is skipped
|
|
636
678
|
// entirely (its fields are also exempt from validation).
|
|
637
679
|
if (hiddenGroups.has(group.name))
|
|
@@ -656,7 +698,8 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
656
698
|
}, description: translate('cms.editor.unsavedChangesGuard') })) : null, confirmAction !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: confirmAction, targets: confirmTargets, open: true, onOpenChange: (open) => {
|
|
657
699
|
if (!open)
|
|
658
700
|
setConfirmAction(null);
|
|
659
|
-
}, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open:
|
|
701
|
+
}, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
|
|
702
|
+
setDiscardConfirm(false); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.discardDraftTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.discardDraftDescription') })] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setDiscardConfirm(false), disabled: discardBusy, children: translate('common.cancel') }), _jsx(Button, { variant: "destructive", size: "sm", "data-testid": "discard-draft-confirm", disabled: discardBusy, onClick: () => void handleDiscardDraft(), children: discardBusy ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.discardingDraft')] })) : (_jsxs(_Fragment, { children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraftConfirm')] })) })] })] }) }), _jsx(Dialog, { open: publishConfirm, onOpenChange: (open) => { if (!open)
|
|
660
703
|
setPublishConfirm(false); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.publishUnpublishedRefsTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.publishUnpublishedRefsDescription') })] }), _jsxs(Stack, { gap: "1", children: [referenceIssues.map((issue, i) => (_jsxs(Text, { size: "xs", tone: "muted", children: ["\u2022 ", issue.message] }, `${issue.meta?.id ?? i}`))), schemaIssues.length > 0 ? (_jsx(Text, { size: "xs", tone: "muted", className: "mt-1", children: translate(schemaIssues.length === 1
|
|
661
704
|
? 'cms.editor.publishAlsoHasIssues.one'
|
|
662
705
|
: 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setPublishConfirm(false), children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: () => {
|
|
@@ -720,16 +763,6 @@ function PerspectiveButton({ label, active, accent, disabled, title, onClick, })
|
|
|
720
763
|
: 'bg-background text-foreground shadow-sm'
|
|
721
764
|
: 'text-muted-foreground hover:text-foreground'), children: label }));
|
|
722
765
|
}
|
|
723
|
-
/**
|
|
724
|
-
* EC-246d: the slug's derived route path, shown read-only directly under the slug
|
|
725
|
-
* input. The slug is the single editable source; the path is composed by the route
|
|
726
|
-
* resolver (`urlForDocument`) from slug + parent chain and never stored. Draft-aware,
|
|
727
|
-
* so it updates as the editor types. Until a slug resolves a route (a fresh
|
|
728
|
-
* hierarchy page), a hint explains where the URL comes from.
|
|
729
|
-
*/
|
|
730
|
-
function PathPreview({ url }) {
|
|
731
|
-
return (_jsxs(Row, { gap: "1", className: "text-xs text-muted-foreground", "data-testid": "slug-path-preview", children: [_jsx(Link2, { className: "size-3 shrink-0", "aria-hidden": true }), url ? (_jsxs(Text, { as: "span", size: "xs", tone: "muted", className: "min-w-0 truncate", title: url.path, children: ["Served at ", _jsx("span", { className: "font-medium text-foreground/80", children: url.path })] })) : (_jsx(Text, { as: "span", size: "xs", tone: "muted", children: translate('cms.editor.slugUrlHint') }))] }));
|
|
732
|
-
}
|
|
733
766
|
/** The implicit fieldset that holds fields with no declared `form.group` (localized via `cms.editor.defaultGroup`). */
|
|
734
767
|
const DEFAULT_FIELD_GROUP = 'fields';
|
|
735
768
|
function groupFields(fields, collection) {
|