@elytracms/studio 0.0.42 → 0.0.44
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/app-views/studio-config.d.ts +12 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-detail.js +1 -1
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +13 -1
- package/dist/components/assets/assets-collection-view-source.js +42 -51
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-module.js +19 -6
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +2 -1
- package/dist/components/assets/assets-store.js +4 -8
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.d.ts +0 -8
- package/dist/components/assets/assets-view-preference.js +12 -13
- package/dist/components/assets/assets-view-preference.js.map +1 -1
- package/dist/components/auth-context.d.ts +2 -1
- package/dist/components/auth-context.js +6 -8
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +2 -2
- package/dist/components/builder/builder-store.js +4 -8
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +2 -1
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.js +2 -1
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-composition-validators.d.ts +25 -0
- package/dist/components/cms/cms-composition-validators.js +75 -0
- package/dist/components/cms/cms-composition-validators.js.map +1 -0
- package/dist/components/cms/cms-labels.d.ts +15 -0
- package/dist/components/cms/cms-labels.js +27 -0
- package/dist/components/cms/cms-labels.js.map +1 -0
- package/dist/components/cms/cms-store.d.ts +23 -10
- package/dist/components/cms/cms-store.js +87 -165
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +1 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +111 -348
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +18 -0
- package/dist/components/cms/discard-draft.js +18 -0
- package/dist/components/cms/discard-draft.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +2 -1
- package/dist/components/cms/document-editor-context.js +4 -9
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/document-ref-utils.d.ts +33 -0
- package/dist/components/cms/document-ref-utils.js +47 -0
- package/dist/components/cms/document-ref-utils.js.map +1 -0
- package/dist/components/cms/document-staging.d.ts +8 -3
- package/dist/components/cms/document-staging.js +81 -21
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/field-control.js +1 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/issues-pane.js +1 -1
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/rich-text/block-commands.d.ts +36 -0
- package/dist/components/cms/rich-text/block-commands.js +47 -0
- package/dist/components/cms/rich-text/block-commands.js.map +1 -0
- package/dist/components/cms/rich-text/slash-command.js +25 -8
- package/dist/components/cms/rich-text/slash-command.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.js +3 -2
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/use-compare-selection.d.ts +27 -0
- package/dist/components/cms/use-compare-selection.js +29 -0
- package/dist/components/cms/use-compare-selection.js.map +1 -0
- package/dist/components/cms/use-discard-controller.d.ts +44 -0
- package/dist/components/cms/use-discard-controller.js +51 -0
- package/dist/components/cms/use-discard-controller.js.map +1 -0
- package/dist/components/cms/use-embed-inspector-host.d.ts +12 -0
- package/dist/components/cms/use-embed-inspector-host.js +17 -0
- package/dist/components/cms/use-embed-inspector-host.js.map +1 -0
- package/dist/components/cms/use-field-focus.d.ts +38 -0
- package/dist/components/cms/use-field-focus.js +70 -0
- package/dist/components/cms/use-field-focus.js.map +1 -0
- package/dist/components/cms/use-live-url.d.ts +41 -0
- package/dist/components/cms/use-live-url.js +55 -0
- package/dist/components/cms/use-live-url.js.map +1 -0
- package/dist/components/cms/use-publish-controller.d.ts +49 -0
- package/dist/components/cms/use-publish-controller.js +102 -0
- package/dist/components/cms/use-publish-controller.js.map +1 -0
- package/dist/components/cms/use-published-perspective.d.ts +67 -0
- package/dist/components/cms/use-published-perspective.js +67 -0
- package/dist/components/cms/use-published-perspective.js.map +1 -0
- package/dist/components/cms/use-restore-version.d.ts +57 -0
- package/dist/components/cms/use-restore-version.js +66 -0
- package/dist/components/cms/use-restore-version.js.map +1 -0
- package/dist/components/cms/use-save-mode.d.ts +35 -0
- package/dist/components/cms/use-save-mode.js +54 -0
- package/dist/components/cms/use-save-mode.js.map +1 -0
- package/dist/components/cms/version-compare.js +14 -17
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-model.d.ts +31 -0
- package/dist/components/cms/versions-model.js +30 -0
- package/dist/components/cms/versions-model.js.map +1 -1
- package/dist/components/cms/versions-pane.js +21 -19
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.js +9 -6
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collab/collab-context.d.ts +2 -1
- package/dist/components/collab/collab-context.js +4 -8
- package/dist/components/collab/collab-context.js.map +1 -1
- package/dist/components/collection-view/collection-view-context.d.ts +9 -1
- package/dist/components/collection-view/collection-view-context.js +4 -9
- package/dist/components/collection-view/collection-view-context.js.map +1 -1
- package/dist/components/components/component-detail.js +2 -5
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +8 -4
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/registry-browser.js +24 -5
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/transfer-dialog.d.ts +21 -6
- package/dist/components/components/transfer-dialog.js +120 -31
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +74 -8
- package/dist/components/components/transfer.js +87 -2
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +12 -6
- package/dist/components/delivery-health-banner.js +27 -29
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/dev-login-badge.d.ts +0 -2
- package/dist/components/dev-login-badge.js +1 -3
- package/dist/components/dev-login-badge.js.map +1 -1
- package/dist/components/playground-badge.d.ts +0 -2
- package/dist/components/playground-badge.js +0 -2
- package/dist/components/playground-badge.js.map +1 -1
- package/dist/components/project-list.js +2 -6
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +6 -2
- package/dist/components/project-session.js +32 -83
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js +2 -6
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +7 -5
- package/dist/components/rail-store-status.js +12 -9
- package/dist/components/rail-store-status.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +111 -0
- package/dist/components/rail-view-source.js +139 -0
- package/dist/components/rail-view-source.js.map +1 -0
- package/dist/components/redirects/redirects-collection-view-source.d.ts +13 -1
- package/dist/components/redirects/redirects-collection-view-source.js +42 -70
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/redirects/redirects-store.d.ts +2 -1
- package/dist/components/redirects/redirects-store.js +4 -8
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/section-state.d.ts +12 -3
- package/dist/components/section-state.js +8 -4
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/studio-context.d.ts +2 -1
- package/dist/components/studio-context.js +4 -8
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-shell.js +1 -1
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/theme-context.d.ts +2 -1
- package/dist/components/theme-context.js +4 -8
- package/dist/components/theme-context.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +65 -0
- package/dist/components/use-hydration-phase.js +64 -0
- package/dist/components/use-hydration-phase.js.map +1 -0
- package/dist/components/users/users-collection-view-source.d.ts +13 -1
- package/dist/components/users/users-collection-view-source.js +60 -79
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/components/users/users-store.d.ts +2 -1
- package/dist/components/users/users-store.js +4 -8
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/components/workspace-projects.d.ts +14 -0
- package/dist/components/workspace-projects.js +21 -0
- package/dist/components/workspace-projects.js.map +1 -1
- package/dist/lib/ai/anthropic-provider.d.ts +29 -0
- package/dist/lib/ai/anthropic-provider.js +59 -0
- package/dist/lib/ai/anthropic-provider.js.map +1 -0
- package/dist/lib/ai/apply-content.d.ts +9 -0
- package/dist/lib/ai/apply-content.js +170 -0
- package/dist/lib/ai/apply-content.js.map +1 -0
- package/dist/lib/ai/config.d.ts +58 -0
- package/dist/lib/ai/config.js +134 -0
- package/dist/lib/ai/config.js.map +1 -0
- package/dist/lib/ai/fake-provider.d.ts +34 -0
- package/dist/lib/ai/fake-provider.js +31 -0
- package/dist/lib/ai/fake-provider.js.map +1 -0
- package/dist/lib/ai/index.d.ts +22 -0
- package/dist/lib/ai/index.js +23 -0
- package/dist/lib/ai/index.js.map +1 -0
- package/dist/lib/ai/knowledge.d.ts +39 -0
- package/dist/lib/ai/knowledge.js +60 -0
- package/dist/lib/ai/knowledge.js.map +1 -0
- package/dist/lib/ai/provenance.d.ts +20 -0
- package/dist/lib/ai/provenance.js +15 -0
- package/dist/lib/ai/provenance.js.map +1 -0
- package/dist/lib/ai/route.d.ts +24 -0
- package/dist/lib/ai/route.js +90 -0
- package/dist/lib/ai/route.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +35 -0
- package/dist/lib/ai/shape.js +113 -0
- package/dist/lib/ai/shape.js.map +1 -0
- package/dist/lib/ai/types.d.ts +227 -0
- package/dist/lib/ai/types.js +2 -0
- package/dist/lib/ai/types.js.map +1 -0
- package/dist/lib/data/data-store.d.ts +2 -1
- package/dist/lib/data/data-store.js +4 -8
- package/dist/lib/data/data-store.js.map +1 -1
- package/dist/lib/delivery/instant-publish.d.ts +9 -13
- package/dist/lib/delivery/instant-publish.js +0 -7
- package/dist/lib/delivery/instant-publish.js.map +1 -1
- package/dist/lib/diff-view-preference.js +4 -4
- package/dist/lib/diff-view-preference.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +5 -2
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/local-preference.d.ts +37 -0
- package/dist/lib/local-preference.js +36 -0
- package/dist/lib/local-preference.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +3 -1
- package/dist/lib/persistence/convex-adapter.js +20 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/rail.js +7 -6
- package/dist/lib/rail.js.map +1 -1
- package/dist/lib/required-context.d.ts +24 -0
- package/dist/lib/required-context.js +28 -0
- package/dist/lib/required-context.js.map +1 -0
- package/dist/lib/theme.js +11 -4
- package/dist/lib/theme.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +9 -0
- package/dist/lib/workspace/index.js +11 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/segmented-control.d.ts +46 -0
- package/dist/ui/segmented-control.js +13 -0
- package/dist/ui/segmented-control.js.map +1 -0
- package/package.json +8 -2
|
@@ -1,24 +1,22 @@
|
|
|
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
3
|
import { Check, ChevronDown, CircleAlert, CircleCheck, Eye, EyeOff, GitCompareArrows, History, Loader2, Lock, MoreHorizontal, RotateCcw, Save, Tag, Trash2, X, } from 'lucide-react';
|
|
4
|
-
import { useCallback,
|
|
5
|
-
import { FIELD_WIDTH_SPANS, SIDEBAR_FIELD_GROUP,
|
|
4
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
+
import { FIELD_WIDTH_SPANS, SIDEBAR_FIELD_GROUP, documentKey, groupOrder, groupSectionOf, hasAliases, hiddenGroupNames, isBlocksField, isFieldHidden, isFieldLocalized, isSingleton, resolveDocumentForLocale, resolveDocumentTitle, resolveField, sidebarFields, tabDescriptorOf, tabOrder, titleFieldOf, staticFieldHidden, } from '@elytracms/core/cms-core';
|
|
6
6
|
import { blockRootsOf } from '@elytracms/core/project-graph';
|
|
7
7
|
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, CollapsibleSection, Field, Heading, Row, Spacer, Stack, Text, toast, } from '@elytracms/studio/ui';
|
|
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';
|
|
12
11
|
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
13
|
-
import { LocaleSwitcher, activeLocaleValues, displayedFieldFallback, fieldConnection, fillFieldFromBase, resetFieldToBase, setFieldValue,
|
|
14
|
-
import { parseDocumentFieldPath } from '@elytracms/studio/lib/references';
|
|
12
|
+
import { LocaleSwitcher, activeLocaleValues, displayedFieldFallback, fieldConnection, fillFieldFromBase, resetFieldToBase, setFieldValue, } from '@elytracms/studio/lib/locale';
|
|
15
13
|
import { FallbackChip } from './cell';
|
|
16
14
|
import { FieldConnectionControl } from './field-connection';
|
|
17
15
|
import { FieldControl } from './field-control';
|
|
18
16
|
// EC-322: the lazy-wrapped page builder — the heavy canvas subtree code-splits out
|
|
19
17
|
// of the detail-view critical path and downloads only when a `blocks` field opens.
|
|
20
18
|
import { CompositionFieldEditor } from '../builder/composition-field-editor-lazy';
|
|
21
|
-
import { fieldLabel } from './cms-
|
|
19
|
+
import { fieldLabel } from './cms-labels';
|
|
22
20
|
import { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source';
|
|
23
21
|
import { CollectionViewProvider, useCollectionView } from '../collection-view/collection-view-context';
|
|
24
22
|
import { resolveFieldAccess } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
@@ -32,8 +30,6 @@ import { VersionsPane } from './versions-pane';
|
|
|
32
30
|
// EC-322: the lazy-wrapped compare view — its diff UI code-splits off the detail-view
|
|
33
31
|
// critical path and loads only when the Compare tab opens.
|
|
34
32
|
import { VersionCompare } from './version-compare-lazy';
|
|
35
|
-
import { useDocumentVersionsQuery } from './use-document-versions';
|
|
36
|
-
import { discardDraftStatus } from './discard-draft';
|
|
37
33
|
import { IssuesPane } from './issues-pane';
|
|
38
34
|
import { DetailsPane } from './details-pane';
|
|
39
35
|
import { DocumentUrlPath } from './document-url-path';
|
|
@@ -42,8 +38,18 @@ import { DocumentUrlPath } from './document-url-path';
|
|
|
42
38
|
import { EmbedInspectorProvider } from './rich-text/embed-inspector-context';
|
|
43
39
|
import { EmbedInspector } from './rich-text/embed-inspector';
|
|
44
40
|
import { useSetBreadcrumb } from '../breadcrumb-context';
|
|
45
|
-
import {
|
|
46
|
-
|
|
41
|
+
import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
42
|
+
// EC-325 — the controller hooks EditorBody orchestrates (each independently
|
|
43
|
+
// unit-tested at its pure core); the shell threads shared draft/ref/replaceDraft
|
|
44
|
+
// state across their boundaries.
|
|
45
|
+
import { usePublishedPerspective } from './use-published-perspective';
|
|
46
|
+
import { useSaveMode } from './use-save-mode';
|
|
47
|
+
import { useDiscardController } from './use-discard-controller';
|
|
48
|
+
import { usePublishController, publishGate } from './use-publish-controller';
|
|
49
|
+
import { useFieldFocus } from './use-field-focus';
|
|
50
|
+
import { useCompareSelection } from './use-compare-selection';
|
|
51
|
+
import { useLiveUrl } from './use-live-url';
|
|
52
|
+
import { useEmbedInspectorHost } from './use-embed-inspector-host';
|
|
47
53
|
export function DetailEditor({ projectId, collectionId, documentId, focusPath, }) {
|
|
48
54
|
// AD-19: the edit shell is a portable surface over a CollectionViewSource, just
|
|
49
55
|
// like the list. `DetailEditor` is the CMS entry point — it builds the CMS source
|
|
@@ -114,99 +120,24 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
114
120
|
// Stable so the document-editor context value (EC-224) doesn't churn each render.
|
|
115
121
|
const ref = useMemo(() => ({ collection: collection.id, id: doc.id }), [collection.id, doc.id]);
|
|
116
122
|
const confirmTargets = useMemo(() => [{ collection: collection.id, id: doc.id }], [collection.id, doc.id]);
|
|
117
|
-
// EC-
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (!published)
|
|
126
|
-
setPerspective('draft');
|
|
127
|
-
}, [published]);
|
|
128
|
-
// Load the pinned published snapshot when viewing Published (fresh each switch,
|
|
129
|
-
// so it reflects the current pin after a re-publish). `loadDocumentVersions` is
|
|
130
|
-
// a stable callback, so this only re-runs on a perspective/ref change.
|
|
131
|
-
const loadVersions = source.versioning?.load;
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
if (perspective !== 'published')
|
|
134
|
-
return;
|
|
135
|
-
if (!loadVersions)
|
|
136
|
-
return;
|
|
137
|
-
let cancelled = false;
|
|
138
|
-
setPublishedSnapshot({ status: 'loading' });
|
|
139
|
-
void (async () => {
|
|
140
|
-
const view = await loadVersions(ref);
|
|
141
|
-
if (cancelled)
|
|
142
|
-
return;
|
|
143
|
-
const pinned = view.publishedVersion !== undefined
|
|
144
|
-
? view.versions.find((v) => v.version === view.publishedVersion)?.snapshot
|
|
145
|
-
: undefined;
|
|
146
|
-
setPublishedSnapshot(pinned ? { status: 'ready', doc: pinned } : { status: 'missing' });
|
|
147
|
-
})();
|
|
148
|
-
return () => {
|
|
149
|
-
cancelled = true;
|
|
150
|
-
};
|
|
151
|
-
}, [perspective, ref, loadVersions]);
|
|
152
|
-
// The whole fieldset is read-only when viewing Published. The form/canvas
|
|
153
|
-
// render the pinned snapshot ONLY once it has loaded — never the draft, which
|
|
154
|
-
// must not be presented as published; until then the body shows a placeholder.
|
|
155
|
-
const viewReadOnly = perspective === 'published';
|
|
156
|
-
const viewDoc = viewReadOnly && publishedSnapshot.status === 'ready' ? publishedSnapshot.doc : draft;
|
|
157
|
-
const publishedPending = viewReadOnly && publishedSnapshot.status !== 'ready';
|
|
158
|
-
// EC-316 — "Discard changes": revert the draft to exactly the published
|
|
159
|
-
// content. Only meaningful for a publishing + versioned rail AND a published
|
|
160
|
-
// document, so the history query is gated off otherwise (never a wasted
|
|
161
|
-
// fetch on e.g. the users rail or a never-published draft).
|
|
162
|
-
const canDiscard = canPublish && canVersions;
|
|
163
|
-
const { versionsQuery: discardVersionsQuery, versionKey: discardVersionKey } = useDocumentVersionsQuery(ref, {
|
|
164
|
-
enabled: canDiscard && published,
|
|
165
|
-
});
|
|
166
|
-
const discardCache = useOperationCache();
|
|
167
|
-
const [discardConfirm, setDiscardConfirm] = useState(false);
|
|
168
|
-
const [discardBusy, setDiscardBusy] = useState(false);
|
|
169
|
-
const discardView = discardVersionsQuery.data ?? null;
|
|
170
|
-
const publishedVersionNumber = discardView?.publishedVersion;
|
|
171
|
-
const publishedSnapshotForDiscard = publishedVersionNumber !== undefined
|
|
172
|
-
? discardView?.versions.find((v) => v.version === publishedVersionNumber)?.snapshot
|
|
173
|
-
: undefined;
|
|
174
|
-
// One pure decision (tested in discard-draft.test.ts): hidden / disabled-
|
|
175
|
-
// with-reason / ready. Its semantic equality is the SAME comparator the
|
|
176
|
-
// Versions pane's publish gate uses, so the two never contradict (EC-298).
|
|
177
|
-
const discardStatus = discardDraftStatus({
|
|
178
|
-
canDiscard,
|
|
179
|
-
published,
|
|
180
|
-
viewReadOnly,
|
|
181
|
-
publishedSnapshot: publishedSnapshotForDiscard,
|
|
182
|
-
collection,
|
|
183
|
-
draft,
|
|
184
|
-
});
|
|
185
|
-
const discardDisabledReason = discardStatus === 'view-read-only'
|
|
186
|
-
? translate('cms.editor.switchToDraftToDiscard')
|
|
187
|
-
: discardStatus === 'up-to-date'
|
|
188
|
-
? translate('cms.editor.discardDraftUpToDateHint')
|
|
189
|
-
: discardStatus === 'loading'
|
|
190
|
-
? translate('common.loading')
|
|
191
|
-
: undefined;
|
|
192
|
-
// Reuses the EC-298 restore machinery (the same op the Compare view's
|
|
193
|
-
// "Restore version" button calls) — never a parallel write path: reverting
|
|
194
|
-
// the draft IS restoring the published version's snapshot, whole-document.
|
|
195
|
-
async function handleDiscardDraft() {
|
|
196
|
-
if (publishedVersionNumber === undefined)
|
|
197
|
-
return;
|
|
198
|
-
setDiscardBusy(true);
|
|
199
|
-
try {
|
|
200
|
-
const restored = await source.versioning?.restore(ref, publishedVersionNumber, { kind: 'all' });
|
|
201
|
-
if (restored)
|
|
202
|
-
replaceDraft(restored);
|
|
203
|
-
await discardCache.invalidate(discardVersionKey);
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
setDiscardBusy(false);
|
|
207
|
-
setDiscardConfirm(false);
|
|
208
|
-
}
|
|
123
|
+
// EC-224 — the working draft's two out-of-band mutators, shared across the
|
|
124
|
+
// controller hooks below and the document-editor context. `save` persists the
|
|
125
|
+
// current buffer; `replaceDraft` adopts a restored snapshot and bumps the nonce
|
|
126
|
+
// to REMOUNT the uncontrolled canvas from it (else the next edit clobbers the
|
|
127
|
+
// restore — the canvas builds its graph once at mount and lifts edits OUT).
|
|
128
|
+
function save() {
|
|
129
|
+
source.save(draft);
|
|
209
130
|
}
|
|
131
|
+
function replaceDraft(next) {
|
|
132
|
+
setDraft(next);
|
|
133
|
+
setRestoreNonce((n) => n + 1);
|
|
134
|
+
}
|
|
135
|
+
// EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective AND the pinned
|
|
136
|
+
// published snapshot, both derived from the ONE shared version-history query
|
|
137
|
+
// (AD-17: the single operations-backed cache — no raw `source.versioning.load`
|
|
138
|
+
// in this component). `canDiscard`/`versionKey`/`publishedVersionNumber`/
|
|
139
|
+
// `publishedSnapshotDoc` feed the Discard controller below off that same query.
|
|
140
|
+
const { perspective, setPerspective, published, canDiscard, versionKey, publishedVersionNumber, publishedSnapshotDoc, publishedSnapshot, viewReadOnly, viewDoc, publishedPending, } = usePublishedPerspective({ ref, source, canPublish, canVersions, draft });
|
|
210
141
|
const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(doc), [draft, doc]);
|
|
211
142
|
const schemaIssues = useMemo(() => source.validate(draft), [source, draft]);
|
|
212
143
|
// EC-300/AD-16: composition embeds (hard references) whose target isn't published
|
|
@@ -219,38 +150,29 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
219
150
|
const errorCount = schemaIssues.filter((i) => i.severity === 'error').length;
|
|
220
151
|
const hasErrors = errorCount > 0;
|
|
221
152
|
const hasUnpublishedRefs = referenceIssues.length > 0;
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return () => window.clearTimeout(timer);
|
|
246
|
-
}, [dirty, draft, explicitSave]);
|
|
247
|
-
// Flush a still-pending AUTOSAVE edit on unmount (navigating away inside the
|
|
248
|
-
// debounce window) so nothing is lost. Explicit rails never flush silently —
|
|
249
|
-
// their `UnsavedChangesGuard` catches the navigation instead. Unmount-only.
|
|
250
|
-
useEffect(() => () => {
|
|
251
|
-
if (!explicitSaveRef.current && dirtyRef.current)
|
|
252
|
-
sourceRef.current.save(draftRef.current);
|
|
253
|
-
}, []);
|
|
153
|
+
// EC-225 autosave · EC-240 explicit save — the persistence discipline for this
|
|
154
|
+
// rail (a publishing rail autosaves + flushes on unmount; a non-publishing rail
|
|
155
|
+
// saves explicitly and relies on the `UnsavedChangesGuard` below for navigation).
|
|
156
|
+
const { explicitSave } = useSaveMode({ canPublish, dirty, draft, source });
|
|
157
|
+
// EC-316 / EC-325 — the "Discard changes" controller: the pure enablement
|
|
158
|
+
// decision plus the shared restore sequence (flush → restore the pinned published
|
|
159
|
+
// version whole → replaceDraft → invalidate). Destructured to the names the header
|
|
160
|
+
// menu + confirm dialog already use.
|
|
161
|
+
const { status: discardStatus, disabledReason: discardDisabledReason, confirmOpen: discardConfirm, setConfirmOpen: setDiscardConfirm, busy: discardBusy, discard: handleDiscardDraft, } = useDiscardController({
|
|
162
|
+
canDiscard,
|
|
163
|
+
published,
|
|
164
|
+
viewReadOnly,
|
|
165
|
+
publishedSnapshotDoc,
|
|
166
|
+
publishedVersionNumber,
|
|
167
|
+
versionKey,
|
|
168
|
+
collection,
|
|
169
|
+
draft,
|
|
170
|
+
source,
|
|
171
|
+
docRef: ref,
|
|
172
|
+
replaceDraft,
|
|
173
|
+
dirty,
|
|
174
|
+
save,
|
|
175
|
+
});
|
|
254
176
|
const errorsByField = useMemo(() => {
|
|
255
177
|
const map = new Map();
|
|
256
178
|
for (const issue of issues) {
|
|
@@ -291,40 +213,28 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
291
213
|
// not a field-row — it takes over its tab with the full page-builder canvas.
|
|
292
214
|
// Convention: such a field lives on its own dedicated fieldset tab.
|
|
293
215
|
const canvasField = tabFields.find(isBlocksField);
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
setInspectorPaneId('versions');
|
|
317
|
-
}, []);
|
|
318
|
-
const setCompareBase = useCallback((base) => {
|
|
319
|
-
setCompare((c) => (c ? { ...c, base } : { base, target: 'draft' }));
|
|
320
|
-
}, []);
|
|
321
|
-
const setCompareTarget = useCallback((target) => {
|
|
322
|
-
setCompare((c) => (c ? { ...c, target } : c));
|
|
323
|
-
}, []);
|
|
324
|
-
const exitCompare = useCallback(() => {
|
|
325
|
-
setCompare(null);
|
|
326
|
-
setActiveTab((prev) => (prev === COMPARE_TAB ? tabs[0] : prev));
|
|
327
|
-
}, [tabs]);
|
|
216
|
+
// EC-172 / EC-283 — the field-focus/highlight controller (jump-to-field from the
|
|
217
|
+
// reference deep-link `focusPath` and the Issues pane). Owns the highlight + the
|
|
218
|
+
// controlled inspector-pane state and the scroll/clear timers.
|
|
219
|
+
const { highlightedField, inspectorPaneId, setInspectorPaneId, focusField } = useFieldFocus({
|
|
220
|
+
collection,
|
|
221
|
+
source,
|
|
222
|
+
focusPath,
|
|
223
|
+
setActiveTab,
|
|
224
|
+
});
|
|
225
|
+
// 0.0.41 — the selection-driven embed-inspector host: a rich-text `componentEmbed`
|
|
226
|
+
// selection swaps the inspector sidebar for the embed's prop form (the `api` is fed
|
|
227
|
+
// to the rich-text field through the EmbedInspector context).
|
|
228
|
+
const { activeEmbed, embedInspectorApi } = useEmbedInspectorHost();
|
|
229
|
+
// EC-298 — the sticky version-compare selection. Opening it focuses the Compare tab
|
|
230
|
+
// + the Versions pane; tabbing to the real fields keeps it open (only its ✕ closes
|
|
231
|
+
// it). `activeTab === COMPARE_TAB` is when the diff is visible.
|
|
232
|
+
const { compare, startCompare, setCompareBase, setCompareTarget, exitCompare } = useCompareSelection({
|
|
233
|
+
tabs,
|
|
234
|
+
compareTab: COMPARE_TAB,
|
|
235
|
+
setActiveTab,
|
|
236
|
+
setInspectorPaneId,
|
|
237
|
+
});
|
|
328
238
|
// A content tab (schema tab or References). Selecting one leaves the Compare
|
|
329
239
|
// view VISIBLE-wise but keeps the comparison open (sticky) — the diff is one
|
|
330
240
|
// click away on its still-present tab chip.
|
|
@@ -342,49 +252,6 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
342
252
|
const titleCase = tab !== REFERENCES_TAB && declaredLabel === undefined;
|
|
343
253
|
return (_jsx("button", { type: "button", "aria-pressed": active, onClick: () => setActiveTab(tab), className: cn('rounded-md px-2.5 py-1 text-xs font-medium transition-colors', titleCase && 'capitalize', active ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-muted'), children: display }, tab));
|
|
344
254
|
};
|
|
345
|
-
// Focus a field by name: switch to its tab (and locale, when given), scroll the
|
|
346
|
-
// control into view, and ring-highlight it. Shared by the reference deep-link
|
|
347
|
-
// (`focusPath`, EC-172) and the Issues pane's jump-to-field (EC-283).
|
|
348
|
-
const focusTimersRef = useRef([]);
|
|
349
|
-
const focusField = useCallback((fieldName, localeHint) => {
|
|
350
|
-
const field = collection.fields.find((f) => f.name === fieldName);
|
|
351
|
-
if (!field)
|
|
352
|
-
return;
|
|
353
|
-
// EC-285: a `sidebar`-group field renders in the Details inspector pane, not
|
|
354
|
-
// a content tab — open that pane instead of switching tabs.
|
|
355
|
-
if (field.form?.group === SIDEBAR_FIELD_GROUP) {
|
|
356
|
-
setInspectorPaneId('details');
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
const fieldTabs = tabOrder(collection.form);
|
|
360
|
-
setActiveTab(field.form?.tab ?? fieldTabs[0] ?? 'main');
|
|
361
|
-
}
|
|
362
|
-
if (localeHint !== undefined && source.localeConfig.locales.includes(localeHint)) {
|
|
363
|
-
setStudioLocale(localeHint);
|
|
364
|
-
}
|
|
365
|
-
setHighlightedField(field.name);
|
|
366
|
-
// Clear any timers from a prior jump so back-to-back jumps don't fight.
|
|
367
|
-
focusTimersRef.current.forEach((t) => window.clearTimeout(t));
|
|
368
|
-
const scrollTimer = window.setTimeout(() => {
|
|
369
|
-
// Some controls (relation/asset pickers) don't render the input id —
|
|
370
|
-
// the field label (`htmlFor`) is always present.
|
|
371
|
-
const control = document.getElementById(`f-${field.name}`) ??
|
|
372
|
-
document.querySelector(`label[for="${CSS.escape(`f-${field.name}`)}"]`);
|
|
373
|
-
control?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
374
|
-
}, 60);
|
|
375
|
-
const clearTimer = window.setTimeout(() => setHighlightedField(null), 3200);
|
|
376
|
-
focusTimersRef.current = [scrollTimer, clearTimer];
|
|
377
|
-
}, [collection, source.localeConfig]);
|
|
378
|
-
useEffect(() => () => focusTimersRef.current.forEach((t) => window.clearTimeout(t)), []);
|
|
379
|
-
// Reference deep links (EC-172): `?field=<index path>` pinpoints the exact field.
|
|
380
|
-
useEffect(() => {
|
|
381
|
-
if (!focusPath)
|
|
382
|
-
return;
|
|
383
|
-
const parsed = parseDocumentFieldPath(focusPath);
|
|
384
|
-
if (!parsed)
|
|
385
|
-
return;
|
|
386
|
-
focusField(parsed.field, parsed.locale);
|
|
387
|
-
}, [focusPath, focusField]);
|
|
388
255
|
function getValue(field) {
|
|
389
256
|
// EC-226: read from the active VIEW (the draft, or the pinned published
|
|
390
257
|
// snapshot when viewing Published) — never directly from `draft`.
|
|
@@ -446,20 +313,6 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
446
313
|
: null;
|
|
447
314
|
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));
|
|
448
315
|
}
|
|
449
|
-
function save() {
|
|
450
|
-
source.save(draft);
|
|
451
|
-
}
|
|
452
|
-
// EC-224: a version restore swaps the working draft for an older snapshot; the
|
|
453
|
-
// open editor adopts it so the form/canvas re-render from the restored content
|
|
454
|
-
// (the operation already persisted it, so it lands clean/non-dirty).
|
|
455
|
-
function replaceDraft(next) {
|
|
456
|
-
setDraft(next);
|
|
457
|
-
// The canvas editor is uncontrolled after mount (it builds its graph once and
|
|
458
|
-
// lifts edits OUT). A restore replaces the draft from outside, so bump the
|
|
459
|
-
// nonce to REMOUNT the canvas from the restored value — otherwise it keeps the
|
|
460
|
-
// pre-restore tree and the next edit would clobber the restore.
|
|
461
|
-
setRestoreNonce((n) => n + 1);
|
|
462
|
-
}
|
|
463
316
|
// The frame-owned `Versions` pane (EC-224) — appended after any view-supplied
|
|
464
317
|
// pane (the canvas's `Properties`). One instance feeds both the form view's
|
|
465
318
|
// sidebar and the canvas view's sidebar.
|
|
@@ -520,75 +373,20 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
520
373
|
...(canVersions ? [versionsPane] : []),
|
|
521
374
|
issuesPane,
|
|
522
375
|
];
|
|
523
|
-
// EC-268
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
setPublishConfirm(true);
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
void doPublish();
|
|
541
|
-
}
|
|
542
|
-
// 0.0.39: publish, then return to the rail's list (rail-owned `listPath`, AD-19),
|
|
543
|
-
// the same navigation the removed "Save & close" did. Shares handlePublish's exact
|
|
544
|
-
// gating + confirm-through; the close happens only after the publish resolves.
|
|
545
|
-
function handlePublishAndClose() {
|
|
546
|
-
closeAfterPublishRef.current = true;
|
|
547
|
-
handlePublish();
|
|
548
|
-
}
|
|
549
|
-
async function doPublish() {
|
|
550
|
-
// EC-273: clean conditionally-hidden values from the draft AT PUBLISH TIME.
|
|
551
|
-
// While editing, the draft keeps them so an accidental hide (e.g. flipping a
|
|
552
|
-
// link's type) is recoverable; publishing is the intentional moment we commit
|
|
553
|
-
// the cleanup — into the draft AND the snapshot, so both go clean together (the
|
|
554
|
-
// reference index updates off the saved draft, so a now-unlinked page becomes
|
|
555
|
-
// deletable once the change is published). Also captures the latest edit in case
|
|
556
|
-
// Publish is clicked inside the autosave debounce window; the serial persister
|
|
557
|
-
// runs the upsert before the publish so the snapshot is exactly this draft (EC-225).
|
|
558
|
-
const cleaned = clearHiddenValues(collection, draft);
|
|
559
|
-
if (cleaned !== draft)
|
|
560
|
-
setDraft(cleaned);
|
|
561
|
-
if (cleaned !== draft || dirty)
|
|
562
|
-
source.save(cleaned);
|
|
563
|
-
// Publish snapshots the draft as a version and pins it (EC-224). The live
|
|
564
|
-
// row stays a continuous draft — published-ness is the version pointer
|
|
565
|
-
// (`source.publishing.isPublished`). Pre-gated by `disabled={hasErrors}`; the optimistic
|
|
566
|
-
// flip reconciles against the backend (EC-225), so a logical rejection the
|
|
567
|
-
// gate doesn't cover (schema divergence) self-corrects the badge. A hard
|
|
568
|
-
// failure (transport) rejects the flush — surface it loudly (EC-268).
|
|
569
|
-
setPublishState('pending');
|
|
570
|
-
try {
|
|
571
|
-
await source.publishing?.publish(ref);
|
|
572
|
-
setPublishState('success');
|
|
573
|
-
setTimeout(() => setPublishState('idle'), 1800);
|
|
574
|
-
// "Publish & close": return to the list once the publish landed (never on a
|
|
575
|
-
// failure). Rail-owned `listPath`, falling back to the project index (AD-19).
|
|
576
|
-
if (closeAfterPublishRef.current) {
|
|
577
|
-
closeAfterPublishRef.current = false;
|
|
578
|
-
void navigate({
|
|
579
|
-
to: STUDIO_PROJECT_ROUTE,
|
|
580
|
-
params: { projectId, _splat: source.listPath ?? studioPath.index() },
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
catch (error) {
|
|
585
|
-
closeAfterPublishRef.current = false;
|
|
586
|
-
setPublishState('idle');
|
|
587
|
-
toast.error(error instanceof Error
|
|
588
|
-
? translate('cms.editor.publishFailedReason', { error: error.message })
|
|
589
|
-
: translate('cms.editor.publishFailed'));
|
|
590
|
-
}
|
|
591
|
-
}
|
|
376
|
+
// EC-268 / EC-300 / 0.0.39 — the publish controller: the publish indicator state,
|
|
377
|
+
// the EC-300 confirm-through guard (unpublished hard refs would render empty on
|
|
378
|
+
// the live site), and the "Publish & close" navigation. Handlers are named to
|
|
379
|
+
// match the header split button + the confirm-through dialog below.
|
|
380
|
+
const { publishState, publishConfirm, handlePublish, handlePublishAndClose, confirmPublish, cancelPublishConfirm, } = usePublishController({
|
|
381
|
+
source,
|
|
382
|
+
docRef: ref,
|
|
383
|
+
collection,
|
|
384
|
+
projectId,
|
|
385
|
+
draft,
|
|
386
|
+
setDraft,
|
|
387
|
+
dirty,
|
|
388
|
+
hasUnpublishedRefs,
|
|
389
|
+
});
|
|
592
390
|
function handleConfirmedAction() {
|
|
593
391
|
if (confirmAction === 'unpublish') {
|
|
594
392
|
setConfirmAction(null);
|
|
@@ -637,41 +435,13 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
637
435
|
];
|
|
638
436
|
}, [singleton, collection, draft, locale, source.localeConfig, source.listPath, doc.id, projectId]);
|
|
639
437
|
useSetBreadcrumb(breadcrumbItems);
|
|
640
|
-
// EC-205/EC-216
|
|
641
|
-
//
|
|
642
|
-
// draft
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
const
|
|
647
|
-
const liveUrl = useMemo(() => {
|
|
648
|
-
const published = publishedDocs.byId.get(doc.id);
|
|
649
|
-
if (!published)
|
|
650
|
-
return undefined;
|
|
651
|
-
return urlForDocument({
|
|
652
|
-
routes: resolveDeploymentRoutes(),
|
|
653
|
-
frontendUrl: resolveDeploymentFrontendUrl(),
|
|
654
|
-
localeConfig: source.localeConfig,
|
|
655
|
-
}, collection, published, { siblings: [...publishedDocs.byId.values()], locale });
|
|
656
|
-
}, [publishedDocs, collection, doc.id, locale, source.localeConfig]);
|
|
657
|
-
// 0.0.42 (round 3): the composed URL row lives in the header under the title; its
|
|
658
|
-
// LEAF segment carries the open-live affordance, mirroring the retired "View live"
|
|
659
|
-
// button's gating EXACTLY. `href` present (published + frontendUrl) → the leaf opens
|
|
660
|
-
// the live page in a new tab; absent → a muted, non-interactive icon with the same
|
|
661
|
-
// reason tooltip (loading / publish-first / set-frontendUrl). The draft-derived path
|
|
662
|
-
// itself still updates as you type — only the OPEN-live action is published-gated, so
|
|
663
|
-
// the draft/live distinction is preserved.
|
|
664
|
-
const headerLive = routableCollection
|
|
665
|
-
? liveUrl?.href
|
|
666
|
-
? { href: liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: liveUrl.path }) }
|
|
667
|
-
: {
|
|
668
|
-
title: publishedDocs.status === 'loading'
|
|
669
|
-
? translate('cms.editor.loadingLiveUrl')
|
|
670
|
-
: !published
|
|
671
|
-
? translate('cms.editor.publishToViewLive')
|
|
672
|
-
: translate('cms.editor.setFrontendUrlHint'),
|
|
673
|
-
}
|
|
674
|
-
: undefined;
|
|
438
|
+
// EC-205/EC-216 — the header composed-URL row's live-link descriptor. Derives
|
|
439
|
+
// strictly from PUBLISHED data (the pinned snapshot + published ancestors), never
|
|
440
|
+
// the draft, so an unpublished slug/parent edit can't leak into the live URL; the
|
|
441
|
+
// draft-derived path itself (rendered by `DocumentUrlPath`) still updates as you
|
|
442
|
+
// type — only the OPEN-live action is published-gated. `undefined` when the
|
|
443
|
+
// collection is non-routable.
|
|
444
|
+
const { headerLive } = useLiveUrl({ source, collection, doc, locale, published, routableCollection });
|
|
675
445
|
// EC-240: hide the tab bar when there is nothing to navigate — a single content
|
|
676
446
|
// tab with no References/Compare (the users rail). When framework tabs ARE
|
|
677
447
|
// present we KEEP the content tab(s) so there is always a way back to the form;
|
|
@@ -681,14 +451,15 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
681
451
|
const hasFrameworkTabs = showReferences || compare !== null;
|
|
682
452
|
const showContentTabs = tabs.length > 1 || hasFrameworkTabs;
|
|
683
453
|
const showLensRow = showContentTabs || canLocales || canPublish;
|
|
684
|
-
// 0.0.39 split Publish button — the shared
|
|
685
|
-
// primary "Publish" and the "Publish & close" item can
|
|
686
|
-
|
|
687
|
-
const publishTitle =
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
454
|
+
// 0.0.39 split Publish button — the shared gate (disabled + title), computed once
|
|
455
|
+
// (pure `publishGate`) so the primary "Publish" and the "Publish & close" item can
|
|
456
|
+
// never diverge.
|
|
457
|
+
const { disabled: publishDisabled, title: publishTitle } = publishGate({
|
|
458
|
+
hasErrors,
|
|
459
|
+
viewReadOnly,
|
|
460
|
+
publishState,
|
|
461
|
+
errorCount,
|
|
462
|
+
});
|
|
692
463
|
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')] }));
|
|
693
464
|
// Offer "Publish & close" only when there IS a list to return to — the same gate
|
|
694
465
|
// the removed EC-316 "Save & close" used (never a singleton or a list-less rail).
|
|
@@ -747,18 +518,10 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
747
518
|
if (!open)
|
|
748
519
|
setConfirmAction(null);
|
|
749
520
|
}, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
|
|
750
|
-
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)
|
|
751
|
-
|
|
752
|
-
closeAfterPublishRef.current = false;
|
|
753
|
-
} }, 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
|
|
521
|
+
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)
|
|
522
|
+
cancelPublishConfirm(); }, 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
|
|
754
523
|
? 'cms.editor.publishAlsoHasIssues.one'
|
|
755
|
-
: 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: ()
|
|
756
|
-
setPublishConfirm(false);
|
|
757
|
-
closeAfterPublishRef.current = false;
|
|
758
|
-
}, children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: () => {
|
|
759
|
-
setPublishConfirm(false);
|
|
760
|
-
void doPublish();
|
|
761
|
-
}, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }) }));
|
|
524
|
+
: 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: cancelPublishConfirm, children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: confirmPublish, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }) }));
|
|
762
525
|
}
|
|
763
526
|
/**
|
|
764
527
|
* EC-300/AD-16 — errors for composition embeds (hard references) whose target isn't
|