@elytracms/studio 0.0.38 → 0.0.40
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 +75 -11
- 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 +143 -79
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.d.ts +21 -8
- package/dist/components/cms/details-pane.js +31 -11
- package/dist/components/cms/details-pane.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 +167 -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 +154 -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/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/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/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/link-control.js +3 -1
- package/dist/components/cms/rich-text/link-control.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar-state.d.ts +45 -0
- package/dist/components/cms/rich-text/toolbar-state.js +66 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar.js +7 -15
- 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 +217 -64
- 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 +97 -0
- package/dist/components/components/component-preview.js +202 -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 +94 -33
- 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 -25
|
@@ -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, ChevronDown, 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,24 +422,15 @@ 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);
|
|
@@ -435,12 +473,25 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
435
473
|
// memoized: its render closes over per-render edit state (draft, highlight,
|
|
436
474
|
// field errors) via `renderFieldRow`, and the inspector re-renders with the rest
|
|
437
475
|
// of the editor anyway, so a stable pane identity would buy nothing here.
|
|
438
|
-
|
|
476
|
+
// 0.0.39: whether a deployment route addresses this collection — gates the header's
|
|
477
|
+
// "View live" AND the Details pane's document-level URL row (EC-315 hybrid split).
|
|
478
|
+
// A cheap synchronous accessor, resolved on every render (NOT frozen in a `useMemo`
|
|
479
|
+
// with a static dep — the same first-paint-empty-routes trap the SlugField fix
|
|
480
|
+
// removed: routes arrive after the workspace/config loads).
|
|
481
|
+
const routableCollection = resolveDeploymentRoutes().some((route) => route.document?.collection === collection.id);
|
|
482
|
+
const detailsPane = hasAliases(collection) || visibleSidebarFields.length > 0 || routableCollection
|
|
439
483
|
? {
|
|
440
484
|
id: 'details',
|
|
441
485
|
label: translate('cms.editor.detailsPane'),
|
|
442
486
|
icon: _jsx(Tag, { className: "size-3.5" }),
|
|
443
|
-
render: () => (_jsx(DetailsPane, { collection: collection, doc: draft, siblings: source.documents(), readOnly: viewReadOnly, fields: visibleSidebarFields, renderField: renderFieldRow,
|
|
487
|
+
render: () => (_jsx(DetailsPane, { collection: collection, doc: draft, siblings: source.documents(), readOnly: viewReadOnly, fields: visibleSidebarFields, renderField: renderFieldRow, urlContext: routableCollection
|
|
488
|
+
? {
|
|
489
|
+
projectId,
|
|
490
|
+
routes: resolveDeploymentRoutes(),
|
|
491
|
+
localeConfig: source.localeConfig,
|
|
492
|
+
locale,
|
|
493
|
+
}
|
|
494
|
+
: undefined, onAliasChange: (alias) => setDraft((d) => {
|
|
444
495
|
const next = { ...d };
|
|
445
496
|
if (alias === undefined)
|
|
446
497
|
delete next.alias;
|
|
@@ -462,6 +513,10 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
462
513
|
const [publishState, setPublishState] = useState('idle');
|
|
463
514
|
// EC-300/AD-16: confirm-through guard when the page embeds unpublished reusables.
|
|
464
515
|
const [publishConfirm, setPublishConfirm] = useState(false);
|
|
516
|
+
// 0.0.39: the split-button "Publish & close" intent — remembered across the
|
|
517
|
+
// confirm-through dialog so a successful publish returns to the list, matching the
|
|
518
|
+
// retired EC-316 "Save & close". Cleared on cancel or a publish failure (no navigate).
|
|
519
|
+
const closeAfterPublishRef = useRef(false);
|
|
465
520
|
// Gate publish on the confirm-through dialog when there are unpublished hard
|
|
466
521
|
// references (they'd render empty on the live site); schema errors already
|
|
467
522
|
// disable the button, so they never reach here.
|
|
@@ -472,6 +527,13 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
472
527
|
}
|
|
473
528
|
void doPublish();
|
|
474
529
|
}
|
|
530
|
+
// 0.0.39: publish, then return to the rail's list (rail-owned `listPath`, AD-19),
|
|
531
|
+
// the same navigation the removed "Save & close" did. Shares handlePublish's exact
|
|
532
|
+
// gating + confirm-through; the close happens only after the publish resolves.
|
|
533
|
+
function handlePublishAndClose() {
|
|
534
|
+
closeAfterPublishRef.current = true;
|
|
535
|
+
handlePublish();
|
|
536
|
+
}
|
|
475
537
|
async function doPublish() {
|
|
476
538
|
// EC-273: clean conditionally-hidden values from the draft AT PUBLISH TIME.
|
|
477
539
|
// While editing, the draft keeps them so an accidental hide (e.g. flipping a
|
|
@@ -497,8 +559,18 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
497
559
|
await source.publishing?.publish(ref);
|
|
498
560
|
setPublishState('success');
|
|
499
561
|
setTimeout(() => setPublishState('idle'), 1800);
|
|
562
|
+
// "Publish & close": return to the list once the publish landed (never on a
|
|
563
|
+
// failure). Rail-owned `listPath`, falling back to the project index (AD-19).
|
|
564
|
+
if (closeAfterPublishRef.current) {
|
|
565
|
+
closeAfterPublishRef.current = false;
|
|
566
|
+
void navigate({
|
|
567
|
+
to: STUDIO_PROJECT_ROUTE,
|
|
568
|
+
params: { projectId, _splat: source.listPath ?? studioPath.index() },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
500
571
|
}
|
|
501
572
|
catch (error) {
|
|
573
|
+
closeAfterPublishRef.current = false;
|
|
502
574
|
setPublishState('idle');
|
|
503
575
|
toast.error(error instanceof Error
|
|
504
576
|
? translate('cms.editor.publishFailedReason', { error: error.message })
|
|
@@ -560,7 +632,6 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
560
632
|
// URL visitors actually get; `undefined` until the doc is published (and the
|
|
561
633
|
// pool has loaded). The collection being routable gates the disabled hint below.
|
|
562
634
|
const publishedDocs = usePublishedDocuments(source.projectId, collection.id);
|
|
563
|
-
const routableCollection = useMemo(() => resolveDeploymentRoutes().some((route) => route.document?.collection === collection.id), [collection.id]);
|
|
564
635
|
const liveUrl = useMemo(() => {
|
|
565
636
|
const published = publishedDocs.byId.get(doc.id);
|
|
566
637
|
if (!published)
|
|
@@ -571,12 +642,10 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
571
642
|
localeConfig: source.localeConfig,
|
|
572
643
|
}, collection, published, { siblings: [...publishedDocs.byId.values()], locale });
|
|
573
644
|
}, [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]);
|
|
645
|
+
// EC-246d/EC-315: the DRAFT composed path moved INTO the slug control itself
|
|
646
|
+
// (`SlugField`), which derives + decomposes it from the same routes; the editor no
|
|
647
|
+
// longer computes a separate `draftUrl`. `liveUrl` (published) still gates the
|
|
648
|
+
// header's "View live" button — the sole open-live, so draft never links out.
|
|
580
649
|
// EC-240: hide the tab bar when there is nothing to navigate — a single content
|
|
581
650
|
// tab with no References/Compare (the users rail). When framework tabs ARE
|
|
582
651
|
// present we KEEP the content tab(s) so there is always a way back to the form;
|
|
@@ -586,6 +655,18 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
586
655
|
const hasFrameworkTabs = showReferences || compare !== null;
|
|
587
656
|
const showContentTabs = tabs.length > 1 || hasFrameworkTabs;
|
|
588
657
|
const showLensRow = showContentTabs || canLocales || canPublish;
|
|
658
|
+
// 0.0.39 split Publish button — the shared gating/label, computed once so the
|
|
659
|
+
// primary "Publish" and the "Publish & close" item can never diverge.
|
|
660
|
+
const publishDisabled = hasErrors || viewReadOnly || publishState === 'pending';
|
|
661
|
+
const publishTitle = viewReadOnly
|
|
662
|
+
? translate('cms.editor.switchToDraftToPublish')
|
|
663
|
+
: hasErrors
|
|
664
|
+
? translate(errorCount === 1 ? 'cms.editor.fixIssuesToPublish.one' : 'cms.editor.fixIssuesToPublish.other', { count: errorCount })
|
|
665
|
+
: undefined;
|
|
666
|
+
const publishButtonLabel = 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')] }));
|
|
667
|
+
// Offer "Publish & close" only when there IS a list to return to — the same gate
|
|
668
|
+
// the removed EC-316 "Save & close" used (never a singleton or a list-less rail).
|
|
669
|
+
const canPublishClose = !singleton && source.listPath !== undefined;
|
|
589
670
|
return (_jsx(DocumentEditorProvider, { value: {
|
|
590
671
|
projectId,
|
|
591
672
|
collection,
|
|
@@ -614,24 +695,11 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
614
695
|
? translate('cms.editor.loadingLiveUrl')
|
|
615
696
|
: !published
|
|
616
697
|
? translate('cms.editor.publishToViewLive')
|
|
617
|
-
: translate('cms.editor.setFrontendUrlHint'), children: [_jsx(ExternalLink, {}), " ", translate('cms.editor.viewLive')] }))) : null, canPublish ? (_jsx(Button, { size: "sm", onClick: handlePublish, disabled:
|
|
618
|
-
? translate('cms.editor.switchToDraftToPublish')
|
|
619
|
-
: hasErrors
|
|
620
|
-
? translate(errorCount === 1
|
|
621
|
-
? 'cms.editor.fixIssuesToPublish.one'
|
|
622
|
-
: '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
|
|
698
|
+
: translate('cms.editor.setFrontendUrlHint'), children: [_jsx(ExternalLink, {}), " ", translate('cms.editor.viewLive')] }))) : null, canPublish ? (_jsxs(Row, { gap: "px", align: "center", children: [_jsx(Button, { size: "sm", onClick: handlePublish, disabled: publishDisabled, title: publishTitle, className: canPublishClose ? 'rounded-r-none' : undefined, "data-testid": "doc-publish", children: publishButtonLabel }), canPublishClose ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", disabled: publishDisabled, "aria-label": translate('cms.editor.publishActions'), "data-testid": "doc-publish-actions", className: "rounded-l-none border-l border-primary-foreground/25 px-1.5", children: _jsx(ChevronDown, { className: "size-3.5" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsxs(DropdownMenuItem, { onClick: handlePublishAndClose, "data-testid": "doc-publish-and-close", children: [_jsx(Save, {}), " ", translate('cms.editor.publishAndClose')] }) })] })) : null] })) : 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: [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
699
|
? 'bg-accent text-accent-foreground'
|
|
625
700
|
: '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
701
|
? 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) => {
|
|
702
|
+
: 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
703
|
// EC-306: a conditionally/statically hidden group section is skipped
|
|
636
704
|
// entirely (its fields are also exempt from validation).
|
|
637
705
|
if (hiddenGroups.has(group.name))
|
|
@@ -656,10 +724,16 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
656
724
|
}, description: translate('cms.editor.unsavedChangesGuard') })) : null, confirmAction !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: confirmAction, targets: confirmTargets, open: true, onOpenChange: (open) => {
|
|
657
725
|
if (!open)
|
|
658
726
|
setConfirmAction(null);
|
|
659
|
-
}, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open:
|
|
660
|
-
|
|
727
|
+
}, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
|
|
728
|
+
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) {
|
|
729
|
+
setPublishConfirm(false);
|
|
730
|
+
closeAfterPublishRef.current = false;
|
|
731
|
+
} }, 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
732
|
? 'cms.editor.publishAlsoHasIssues.one'
|
|
662
|
-
: 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () =>
|
|
733
|
+
: 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
734
|
+
setPublishConfirm(false);
|
|
735
|
+
closeAfterPublishRef.current = false;
|
|
736
|
+
}, children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: () => {
|
|
663
737
|
setPublishConfirm(false);
|
|
664
738
|
void doPublish();
|
|
665
739
|
}, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }));
|
|
@@ -720,16 +794,6 @@ function PerspectiveButton({ label, active, accent, disabled, title, onClick, })
|
|
|
720
794
|
: 'bg-background text-foreground shadow-sm'
|
|
721
795
|
: 'text-muted-foreground hover:text-foreground'), children: label }));
|
|
722
796
|
}
|
|
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
797
|
/** The implicit fieldset that holds fields with no declared `form.group` (localized via `cms.editor.defaultGroup`). */
|
|
734
798
|
const DEFAULT_FIELD_GROUP = 'fields';
|
|
735
799
|
function groupFields(fields, collection) {
|