@elytracms/studio 0.0.39 → 0.0.41
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/dist/components/cms/collection-table.js +13 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +116 -72
- 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/field-control.js +21 -12
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.js +25 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.d.ts +7 -2
- package/dist/components/cms/rich-text/component-embed.js +60 -5
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.js +24 -5
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-inspector-context.d.ts +50 -0
- package/dist/components/cms/rich-text/embed-inspector-context.js +12 -0
- package/dist/components/cms/rich-text/embed-inspector-context.js.map +1 -0
- package/dist/components/cms/rich-text/embed-inspector.d.ts +25 -0
- package/dist/components/cms/rich-text/embed-inspector.js +48 -0
- package/dist/components/cms/rich-text/embed-inspector.js.map +1 -0
- package/dist/components/cms/rich-text/embed-picker.d.ts +0 -2
- package/dist/components/cms/rich-text/embed-picker.js +0 -4
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +7 -0
- package/dist/components/cms/rich-text/embed-props.js +9 -0
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +8 -2
- package/dist/components/cms/rich-text/insert.js +32 -3
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- 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/slash-command.d.ts +23 -6
- package/dist/components/cms/rich-text/slash-command.js +31 -23
- package/dist/components/cms/rich-text/slash-command.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/version-compare.js +51 -22
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +36 -0
- package/dist/components/components/component-preview.js +157 -5
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +18 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/package.json +2 -2
|
@@ -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, Loader2, Lock, MoreHorizontal, RotateCcw, Save, Tag, Trash2, X, } from 'lucide-react';
|
|
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';
|
|
@@ -36,6 +36,10 @@ import { useDocumentVersionsQuery } from './use-document-versions';
|
|
|
36
36
|
import { discardDraftStatus } from './discard-draft';
|
|
37
37
|
import { IssuesPane } from './issues-pane';
|
|
38
38
|
import { DetailsPane } from './details-pane';
|
|
39
|
+
// 0.0.41: selection-driven embed inspector. Both modules are deliberately TipTap-free
|
|
40
|
+
// so the editor runtime stays behind rich-text/lazy (EC-322) — see their doc comments.
|
|
41
|
+
import { EmbedInspectorProvider } from './rich-text/embed-inspector-context';
|
|
42
|
+
import { EmbedInspector } from './rich-text/embed-inspector';
|
|
39
43
|
import { useSetBreadcrumb } from '../breadcrumb-context';
|
|
40
44
|
import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
41
45
|
import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
|
|
@@ -291,6 +295,15 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
291
295
|
// `sidebar`-group field can open the Details pane (where it renders). Undefined
|
|
292
296
|
// = uncontrolled (the sidebar defaults to its first pane).
|
|
293
297
|
const [inspectorPaneId, setInspectorPaneId] = useState();
|
|
298
|
+
// 0.0.41: the selected rich-text component embed, if any — a `componentEmbed` node
|
|
299
|
+
// selection swaps the inspector sidebar for the embed's prop form. Fed by the
|
|
300
|
+
// rich-text field through the EmbedInspector context; `clearActiveEmbed` keys on the
|
|
301
|
+
// node id so a fast A→B reselect can't let A's deselect wipe B (selection race).
|
|
302
|
+
const [activeEmbed, setActiveEmbed] = useState(null);
|
|
303
|
+
const embedInspectorApi = useMemo(() => ({
|
|
304
|
+
setActiveEmbed: (payload) => setActiveEmbed(payload),
|
|
305
|
+
clearActiveEmbed: (nodeId) => setActiveEmbed((prev) => (prev?.nodeId === nodeId ? null : prev)),
|
|
306
|
+
}), []);
|
|
294
307
|
// EC-298: the open version comparison. The Compare tab is STICKY — opening it
|
|
295
308
|
// sets the selection and focuses the tab; tabbing to the real fields keeps it
|
|
296
309
|
// open (the selection persists) so an editor can flip between diff and fields;
|
|
@@ -435,19 +448,6 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
435
448
|
function save() {
|
|
436
449
|
source.save(draft);
|
|
437
450
|
}
|
|
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
|
-
}
|
|
451
451
|
// EC-224: a version restore swaps the working draft for an older snapshot; the
|
|
452
452
|
// open editor adopts it so the form/canvas re-render from the restored content
|
|
453
453
|
// (the operation already persisted it, so it lands clean/non-dirty).
|
|
@@ -486,12 +486,25 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
486
486
|
// memoized: its render closes over per-render edit state (draft, highlight,
|
|
487
487
|
// field errors) via `renderFieldRow`, and the inspector re-renders with the rest
|
|
488
488
|
// of the editor anyway, so a stable pane identity would buy nothing here.
|
|
489
|
-
|
|
489
|
+
// 0.0.39: whether a deployment route addresses this collection — gates the header's
|
|
490
|
+
// "View live" AND the Details pane's document-level URL row (EC-315 hybrid split).
|
|
491
|
+
// A cheap synchronous accessor, resolved on every render (NOT frozen in a `useMemo`
|
|
492
|
+
// with a static dep — the same first-paint-empty-routes trap the SlugField fix
|
|
493
|
+
// removed: routes arrive after the workspace/config loads).
|
|
494
|
+
const routableCollection = resolveDeploymentRoutes().some((route) => route.document?.collection === collection.id);
|
|
495
|
+
const detailsPane = hasAliases(collection) || visibleSidebarFields.length > 0 || routableCollection
|
|
490
496
|
? {
|
|
491
497
|
id: 'details',
|
|
492
498
|
label: translate('cms.editor.detailsPane'),
|
|
493
499
|
icon: _jsx(Tag, { className: "size-3.5" }),
|
|
494
|
-
render: () => (_jsx(DetailsPane, { collection: collection, doc: draft, siblings: source.documents(), readOnly: viewReadOnly, fields: visibleSidebarFields, renderField: renderFieldRow,
|
|
500
|
+
render: () => (_jsx(DetailsPane, { collection: collection, doc: draft, siblings: source.documents(), readOnly: viewReadOnly, fields: visibleSidebarFields, renderField: renderFieldRow, urlContext: routableCollection
|
|
501
|
+
? {
|
|
502
|
+
projectId,
|
|
503
|
+
routes: resolveDeploymentRoutes(),
|
|
504
|
+
localeConfig: source.localeConfig,
|
|
505
|
+
locale,
|
|
506
|
+
}
|
|
507
|
+
: undefined, onAliasChange: (alias) => setDraft((d) => {
|
|
495
508
|
const next = { ...d };
|
|
496
509
|
if (alias === undefined)
|
|
497
510
|
delete next.alias;
|
|
@@ -513,6 +526,10 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
513
526
|
const [publishState, setPublishState] = useState('idle');
|
|
514
527
|
// EC-300/AD-16: confirm-through guard when the page embeds unpublished reusables.
|
|
515
528
|
const [publishConfirm, setPublishConfirm] = useState(false);
|
|
529
|
+
// 0.0.39: the split-button "Publish & close" intent — remembered across the
|
|
530
|
+
// confirm-through dialog so a successful publish returns to the list, matching the
|
|
531
|
+
// retired EC-316 "Save & close". Cleared on cancel or a publish failure (no navigate).
|
|
532
|
+
const closeAfterPublishRef = useRef(false);
|
|
516
533
|
// Gate publish on the confirm-through dialog when there are unpublished hard
|
|
517
534
|
// references (they'd render empty on the live site); schema errors already
|
|
518
535
|
// disable the button, so they never reach here.
|
|
@@ -523,6 +540,13 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
523
540
|
}
|
|
524
541
|
void doPublish();
|
|
525
542
|
}
|
|
543
|
+
// 0.0.39: publish, then return to the rail's list (rail-owned `listPath`, AD-19),
|
|
544
|
+
// the same navigation the removed "Save & close" did. Shares handlePublish's exact
|
|
545
|
+
// gating + confirm-through; the close happens only after the publish resolves.
|
|
546
|
+
function handlePublishAndClose() {
|
|
547
|
+
closeAfterPublishRef.current = true;
|
|
548
|
+
handlePublish();
|
|
549
|
+
}
|
|
526
550
|
async function doPublish() {
|
|
527
551
|
// EC-273: clean conditionally-hidden values from the draft AT PUBLISH TIME.
|
|
528
552
|
// While editing, the draft keeps them so an accidental hide (e.g. flipping a
|
|
@@ -548,8 +572,18 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
548
572
|
await source.publishing?.publish(ref);
|
|
549
573
|
setPublishState('success');
|
|
550
574
|
setTimeout(() => setPublishState('idle'), 1800);
|
|
575
|
+
// "Publish & close": return to the list once the publish landed (never on a
|
|
576
|
+
// failure). Rail-owned `listPath`, falling back to the project index (AD-19).
|
|
577
|
+
if (closeAfterPublishRef.current) {
|
|
578
|
+
closeAfterPublishRef.current = false;
|
|
579
|
+
void navigate({
|
|
580
|
+
to: STUDIO_PROJECT_ROUTE,
|
|
581
|
+
params: { projectId, _splat: source.listPath ?? studioPath.index() },
|
|
582
|
+
});
|
|
583
|
+
}
|
|
551
584
|
}
|
|
552
585
|
catch (error) {
|
|
586
|
+
closeAfterPublishRef.current = false;
|
|
553
587
|
setPublishState('idle');
|
|
554
588
|
toast.error(error instanceof Error
|
|
555
589
|
? translate('cms.editor.publishFailedReason', { error: error.message })
|
|
@@ -611,7 +645,6 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
611
645
|
// URL visitors actually get; `undefined` until the doc is published (and the
|
|
612
646
|
// pool has loaded). The collection being routable gates the disabled hint below.
|
|
613
647
|
const publishedDocs = usePublishedDocuments(source.projectId, collection.id);
|
|
614
|
-
const routableCollection = useMemo(() => resolveDeploymentRoutes().some((route) => route.document?.collection === collection.id), [collection.id]);
|
|
615
648
|
const liveUrl = useMemo(() => {
|
|
616
649
|
const published = publishedDocs.byId.get(doc.id);
|
|
617
650
|
if (!published)
|
|
@@ -635,6 +668,18 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
635
668
|
const hasFrameworkTabs = showReferences || compare !== null;
|
|
636
669
|
const showContentTabs = tabs.length > 1 || hasFrameworkTabs;
|
|
637
670
|
const showLensRow = showContentTabs || canLocales || canPublish;
|
|
671
|
+
// 0.0.39 split Publish button — the shared gating/label, computed once so the
|
|
672
|
+
// primary "Publish" and the "Publish & close" item can never diverge.
|
|
673
|
+
const publishDisabled = hasErrors || viewReadOnly || publishState === 'pending';
|
|
674
|
+
const publishTitle = viewReadOnly
|
|
675
|
+
? translate('cms.editor.switchToDraftToPublish')
|
|
676
|
+
: hasErrors
|
|
677
|
+
? translate(errorCount === 1 ? 'cms.editor.fixIssuesToPublish.one' : 'cms.editor.fixIssuesToPublish.other', { count: errorCount })
|
|
678
|
+
: undefined;
|
|
679
|
+
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')] }));
|
|
680
|
+
// Offer "Publish & close" only when there IS a list to return to — the same gate
|
|
681
|
+
// the removed EC-316 "Save & close" used (never a singleton or a list-less rail).
|
|
682
|
+
const canPublishClose = !singleton && source.listPath !== undefined;
|
|
638
683
|
return (_jsx(DocumentEditorProvider, { value: {
|
|
639
684
|
projectId,
|
|
640
685
|
collection,
|
|
@@ -651,61 +696,60 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
651
696
|
setCompareBase,
|
|
652
697
|
setCompareTarget,
|
|
653
698
|
exitCompare,
|
|
654
|
-
}, children: _jsxs("div", { className: cn('flex h-full min-h-0 flex-col', viewReadOnly && 'bg-info/[0.04]'), children: [_jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: singleton ? localizedLabel(collection.form?.label, collection.id) : title || doc.id }), singleton ? null : _jsx(FallbackChip, { fallback: titleFallback, testId: "doc-title-fallback" })] }), _jsx(Spacer, {}), _jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", "data-testid": "autosave-status", children: source.lastError
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }));
|
|
699
|
+
}, children: _jsx(EmbedInspectorProvider, { value: embedInspectorApi, children: _jsxs("div", { className: cn('flex h-full min-h-0 flex-col', viewReadOnly && 'bg-info/[0.04]'), children: [_jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: singleton ? localizedLabel(collection.form?.label, collection.id) : title || doc.id }), singleton ? null : _jsx(FallbackChip, { fallback: titleFallback, testId: "doc-title-fallback" })] }), _jsx(Spacer, {}), _jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", "data-testid": "autosave-status", children: source.lastError
|
|
700
|
+
? translate('cms.editor.saveFailed')
|
|
701
|
+
: explicitSave
|
|
702
|
+
? dirty
|
|
703
|
+
? translate('cms.editor.unsavedChanges')
|
|
704
|
+
: translate('cms.editor.saved')
|
|
705
|
+
: dirty
|
|
706
|
+
? translate('cms.editor.saving')
|
|
707
|
+
: translate('cms.editor.saved') }), canPublish ? _jsx(DocStateBadge, { published: published }) : null, routableCollection ? (liveUrl?.href ? (_jsx(Button, { asChild: true, variant: "outline", size: "sm", title: translate('cms.editor.openLiveUrl', { path: liveUrl.path }), children: _jsxs("a", { href: liveUrl.href, target: "_blank", rel: "noreferrer", "data-testid": "view-live", children: [_jsx(ExternalLink, {}), " ", translate('cms.editor.viewLive')] }) })) : (_jsxs(Button, { variant: "outline", size: "sm", disabled: true, "data-testid": "view-live-disabled", title: publishedDocs.status === 'loading'
|
|
708
|
+
? translate('cms.editor.loadingLiveUrl')
|
|
709
|
+
: !published
|
|
710
|
+
? translate('cms.editor.publishToViewLive')
|
|
711
|
+
: 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
|
|
712
|
+
? 'bg-accent text-accent-foreground'
|
|
713
|
+
: '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'
|
|
714
|
+
? translate('cms.editor.loadingPublishedVersion')
|
|
715
|
+
: 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) => {
|
|
716
|
+
// EC-306: a conditionally/statically hidden group section is skipped
|
|
717
|
+
// entirely (its fields are also exempt from validation).
|
|
718
|
+
if (hiddenGroups.has(group.name))
|
|
719
|
+
return null;
|
|
720
|
+
// EC-273/EC-240: drop a fieldset whose every field is hidden — whether
|
|
721
|
+
// content-conditionally or for this principal — so the form never
|
|
722
|
+
// shows an empty section header.
|
|
723
|
+
const visible = group.fields.filter(isFieldShown);
|
|
724
|
+
if (visible.length === 0)
|
|
725
|
+
return null;
|
|
726
|
+
const section = group.section;
|
|
727
|
+
// EC-241: a section `title` is a LocalizedString ({ en, de } or a plain
|
|
728
|
+
// string) — resolve it against the active UI locale before rendering.
|
|
729
|
+
const sectionTitle = section?.title !== undefined ? localizedLabel(section.title, group.name) : undefined;
|
|
730
|
+
return (_jsx(CollapsibleSection, { collapsible: section?.collapsible, defaultOpen: section?.defaultOpen, title: _jsx(Text, { size: "sm", weight: "semibold", className: sectionTitle || group.name === DEFAULT_FIELD_GROUP ? undefined : 'capitalize', children: sectionTitle ??
|
|
731
|
+
(group.name === DEFAULT_FIELD_GROUP
|
|
732
|
+
? translate('cms.editor.defaultGroup')
|
|
733
|
+
: group.name) }), children: _jsx("div", { className: "grid grid-cols-1 gap-4 p-4 md:grid-cols-12", children: visible.map((field) => (_jsx("div", { className: cn('col-span-1', fieldWidthColSpan(field.form?.width)), children: renderFieldRow(field) }, field.name))) }) }, group.name));
|
|
734
|
+
}) }) }) }), activeEmbed ? (_jsx(EmbedInspector, { payload: activeEmbed, className: "w-[320px] shrink-0 border-l border-border" })) : (_jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" }))] })), explicitSave ? (_jsx(UnsavedChangesGuard, { when: dirty, onSave: () => {
|
|
735
|
+
save();
|
|
736
|
+
return true;
|
|
737
|
+
}, description: translate('cms.editor.unsavedChangesGuard') })) : null, confirmAction !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: confirmAction, targets: confirmTargets, open: true, onOpenChange: (open) => {
|
|
738
|
+
if (!open)
|
|
739
|
+
setConfirmAction(null);
|
|
740
|
+
}, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
|
|
741
|
+
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) {
|
|
742
|
+
setPublishConfirm(false);
|
|
743
|
+
closeAfterPublishRef.current = false;
|
|
744
|
+
} }, 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
|
|
745
|
+
? 'cms.editor.publishAlsoHasIssues.one'
|
|
746
|
+
: 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
747
|
+
setPublishConfirm(false);
|
|
748
|
+
closeAfterPublishRef.current = false;
|
|
749
|
+
}, children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: () => {
|
|
750
|
+
setPublishConfirm(false);
|
|
751
|
+
void doPublish();
|
|
752
|
+
}, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }) }));
|
|
709
753
|
}
|
|
710
754
|
/**
|
|
711
755
|
* EC-300/AD-16 — errors for composition embeds (hard references) whose target isn't
|