@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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 0.0.41 — the selection-driven embed-inspector host. A `componentEmbed` node
|
|
4
|
+
* selection in a rich-text field swaps the inspector sidebar for the embed's prop
|
|
5
|
+
* form; deselect returns the normal inspector. `clearActiveEmbed` keys on the node
|
|
6
|
+
* id so a fast A→B reselect can't let A's deselect wipe B (the selection race). Fed
|
|
7
|
+
* by the rich-text field through the EmbedInspector context (the returned api).
|
|
8
|
+
*/
|
|
9
|
+
export function useEmbedInspectorHost() {
|
|
10
|
+
const [activeEmbed, setActiveEmbed] = useState(null);
|
|
11
|
+
const embedInspectorApi = useMemo(() => ({
|
|
12
|
+
setActiveEmbed: (payload) => setActiveEmbed(payload),
|
|
13
|
+
clearActiveEmbed: (nodeId) => setActiveEmbed((prev) => (prev?.nodeId === nodeId ? null : prev)),
|
|
14
|
+
}), []);
|
|
15
|
+
return { activeEmbed, embedInspectorApi };
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=use-embed-inspector-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-embed-inspector-host.js","sourceRoot":"","sources":["../../../src/components/cms/use-embed-inspector-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGzC;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB;IAInC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAA+B,IAAI,CAAC,CAAA;IAClF,MAAM,iBAAiB,GAAG,OAAO,CAC/B,GAAG,EAAE,CAAC,CAAC;QACL,cAAc,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC;QACpD,gBAAgB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;KAChG,CAAC,EACF,EAAE,CACH,CAAA;IACD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAA;AAC3C,CAAC","sourcesContent":["import { useMemo, useState } from 'react'\nimport type { EmbedInspectorApi, EmbedInspectorPayload } from './rich-text/embed-inspector-context'\n\n/**\n * 0.0.41 — the selection-driven embed-inspector host. A `componentEmbed` node\n * selection in a rich-text field swaps the inspector sidebar for the embed's prop\n * form; deselect returns the normal inspector. `clearActiveEmbed` keys on the node\n * id so a fast A→B reselect can't let A's deselect wipe B (the selection race). Fed\n * by the rich-text field through the EmbedInspector context (the returned api).\n */\nexport function useEmbedInspectorHost(): {\n activeEmbed: EmbedInspectorPayload | null\n embedInspectorApi: EmbedInspectorApi\n} {\n const [activeEmbed, setActiveEmbed] = useState<EmbedInspectorPayload | null>(null)\n const embedInspectorApi = useMemo<EmbedInspectorApi>(\n () => ({\n setActiveEmbed: (payload) => setActiveEmbed(payload),\n clearActiveEmbed: (nodeId) => setActiveEmbed((prev) => (prev?.nodeId === nodeId ? null : prev)),\n }),\n [],\n )\n return { activeEmbed, embedInspectorApi }\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
3
|
+
/**
|
|
4
|
+
* EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in
|
|
5
|
+
* the reserved `sidebar` group renders in the Details inspector pane (open that
|
|
6
|
+
* pane, EC-285); any other field lives on a content tab (switch to it). Returns
|
|
7
|
+
* `null` for an unknown field name (no-op). Directly testable, no DOM.
|
|
8
|
+
*/
|
|
9
|
+
export declare function focusTarget(collection: CollectionDef, fieldName: string): {
|
|
10
|
+
field: FieldDef;
|
|
11
|
+
paneId: 'details';
|
|
12
|
+
} | {
|
|
13
|
+
field: FieldDef;
|
|
14
|
+
tab: string;
|
|
15
|
+
} | null;
|
|
16
|
+
export interface FieldFocus {
|
|
17
|
+
/** The field ring-highlighted right now (or null). */
|
|
18
|
+
highlightedField: string | null;
|
|
19
|
+
/** The inspector sidebar's active pane, controlled so a jump can open Details. */
|
|
20
|
+
inspectorPaneId: string | undefined;
|
|
21
|
+
setInspectorPaneId: (id: string | undefined) => void;
|
|
22
|
+
/** Focus a field by name: switch to its tab/pane (+ locale), scroll, highlight. */
|
|
23
|
+
focusField: (fieldName: string, localeHint?: string) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field
|
|
27
|
+
* switches to its tab (or opens the Details pane for a sidebar field) and the
|
|
28
|
+
* given locale, scrolls its control into view, and ring-highlights it briefly.
|
|
29
|
+
* Shared by the reference deep-link (`focusPath`) and the Issues pane's
|
|
30
|
+
* jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear
|
|
31
|
+
* timers (cleaned up on unmount, and cleared between back-to-back jumps).
|
|
32
|
+
*/
|
|
33
|
+
export declare function useFieldFocus(input: {
|
|
34
|
+
collection: CollectionDef;
|
|
35
|
+
source: CollectionViewSource;
|
|
36
|
+
focusPath: string | undefined;
|
|
37
|
+
setActiveTab: (tab: string) => void;
|
|
38
|
+
}): FieldFocus;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { SIDEBAR_FIELD_GROUP, tabOrder } from '@elytracms/core/cms-core';
|
|
3
|
+
import { setStudioLocale } from '@elytracms/studio/lib/locale';
|
|
4
|
+
import { parseDocumentFieldPath } from '@elytracms/studio/lib/references';
|
|
5
|
+
/**
|
|
6
|
+
* EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in
|
|
7
|
+
* the reserved `sidebar` group renders in the Details inspector pane (open that
|
|
8
|
+
* pane, EC-285); any other field lives on a content tab (switch to it). Returns
|
|
9
|
+
* `null` for an unknown field name (no-op). Directly testable, no DOM.
|
|
10
|
+
*/
|
|
11
|
+
export function focusTarget(collection, fieldName) {
|
|
12
|
+
const field = collection.fields.find((f) => f.name === fieldName);
|
|
13
|
+
if (!field)
|
|
14
|
+
return null;
|
|
15
|
+
if (field.form?.group === SIDEBAR_FIELD_GROUP)
|
|
16
|
+
return { field, paneId: 'details' };
|
|
17
|
+
const fieldTabs = tabOrder(collection.form);
|
|
18
|
+
return { field, tab: field.form?.tab ?? fieldTabs[0] ?? 'main' };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field
|
|
22
|
+
* switches to its tab (or opens the Details pane for a sidebar field) and the
|
|
23
|
+
* given locale, scrolls its control into view, and ring-highlights it briefly.
|
|
24
|
+
* Shared by the reference deep-link (`focusPath`) and the Issues pane's
|
|
25
|
+
* jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear
|
|
26
|
+
* timers (cleaned up on unmount, and cleared between back-to-back jumps).
|
|
27
|
+
*/
|
|
28
|
+
export function useFieldFocus(input) {
|
|
29
|
+
const { collection, source, focusPath, setActiveTab } = input;
|
|
30
|
+
const [highlightedField, setHighlightedField] = useState(null);
|
|
31
|
+
const [inspectorPaneId, setInspectorPaneId] = useState();
|
|
32
|
+
const focusTimersRef = useRef([]);
|
|
33
|
+
const focusField = useCallback((fieldName, localeHint) => {
|
|
34
|
+
const target = focusTarget(collection, fieldName);
|
|
35
|
+
if (!target)
|
|
36
|
+
return;
|
|
37
|
+
// A sidebar-group field renders in the Details pane; any other on its tab.
|
|
38
|
+
if ('paneId' in target)
|
|
39
|
+
setInspectorPaneId(target.paneId);
|
|
40
|
+
else
|
|
41
|
+
setActiveTab(target.tab);
|
|
42
|
+
if (localeHint !== undefined && source.localeConfig.locales.includes(localeHint)) {
|
|
43
|
+
setStudioLocale(localeHint);
|
|
44
|
+
}
|
|
45
|
+
setHighlightedField(target.field.name);
|
|
46
|
+
// Clear any timers from a prior jump so back-to-back jumps don't fight.
|
|
47
|
+
focusTimersRef.current.forEach((t) => window.clearTimeout(t));
|
|
48
|
+
const scrollTimer = window.setTimeout(() => {
|
|
49
|
+
// Some controls (relation/asset pickers) don't render the input id —
|
|
50
|
+
// the field label (`htmlFor`) is always present.
|
|
51
|
+
const control = document.getElementById(`f-${target.field.name}`) ??
|
|
52
|
+
document.querySelector(`label[for="${CSS.escape(`f-${target.field.name}`)}"]`);
|
|
53
|
+
control?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
54
|
+
}, 60);
|
|
55
|
+
const clearTimer = window.setTimeout(() => setHighlightedField(null), 3200);
|
|
56
|
+
focusTimersRef.current = [scrollTimer, clearTimer];
|
|
57
|
+
}, [collection, source.localeConfig, setActiveTab]);
|
|
58
|
+
useEffect(() => () => focusTimersRef.current.forEach((t) => window.clearTimeout(t)), []);
|
|
59
|
+
// Reference deep links (EC-172): `?field=<index path>` pinpoints the exact field.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!focusPath)
|
|
62
|
+
return;
|
|
63
|
+
const parsed = parseDocumentFieldPath(focusPath);
|
|
64
|
+
if (!parsed)
|
|
65
|
+
return;
|
|
66
|
+
focusField(parsed.field, parsed.locale);
|
|
67
|
+
}, [focusPath, focusField]);
|
|
68
|
+
return { highlightedField, inspectorPaneId, setInspectorPaneId, focusField };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=use-field-focus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-field-focus.js","sourceRoot":"","sources":["../../../src/components/cms/use-field-focus.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { SIDEBAR_FIELD_GROUP, tabOrder } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { setStudioLocale } from '@elytracms/studio/lib/locale'\nimport { parseDocumentFieldPath } from '@elytracms/studio/lib/references'\n\n/**\n * EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in\n * the reserved `sidebar` group renders in the Details inspector pane (open that\n * pane, EC-285); any other field lives on a content tab (switch to it). Returns\n * `null` for an unknown field name (no-op). Directly testable, no DOM.\n */\nexport function focusTarget(\n collection: CollectionDef,\n fieldName: string,\n): { field: FieldDef; paneId: 'details' } | { field: FieldDef; tab: string } | null {\n const field = collection.fields.find((f) => f.name === fieldName)\n if (!field) return null\n if (field.form?.group === SIDEBAR_FIELD_GROUP) return { field, paneId: 'details' }\n const fieldTabs = tabOrder(collection.form)\n return { field, tab: field.form?.tab ?? fieldTabs[0] ?? 'main' }\n}\n\nexport interface FieldFocus {\n /** The field ring-highlighted right now (or null). */\n highlightedField: string | null\n /** The inspector sidebar's active pane, controlled so a jump can open Details. */\n inspectorPaneId: string | undefined\n setInspectorPaneId: (id: string | undefined) => void\n /** Focus a field by name: switch to its tab/pane (+ locale), scroll, highlight. */\n focusField: (fieldName: string, localeHint?: string) => void\n}\n\n/**\n * EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field\n * switches to its tab (or opens the Details pane for a sidebar field) and the\n * given locale, scrolls its control into view, and ring-highlights it briefly.\n * Shared by the reference deep-link (`focusPath`) and the Issues pane's\n * jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear\n * timers (cleaned up on unmount, and cleared between back-to-back jumps).\n */\nexport function useFieldFocus(input: {\n collection: CollectionDef\n source: CollectionViewSource\n focusPath: string | undefined\n setActiveTab: (tab: string) => void\n}): FieldFocus {\n const { collection, source, focusPath, setActiveTab } = input\n const [highlightedField, setHighlightedField] = useState<string | null>(null)\n const [inspectorPaneId, setInspectorPaneId] = useState<string>()\n const focusTimersRef = useRef<number[]>([])\n\n const focusField = useCallback(\n (fieldName: string, localeHint?: string) => {\n const target = focusTarget(collection, fieldName)\n if (!target) return\n // A sidebar-group field renders in the Details pane; any other on its tab.\n if ('paneId' in target) setInspectorPaneId(target.paneId)\n else setActiveTab(target.tab)\n if (localeHint !== undefined && source.localeConfig.locales.includes(localeHint)) {\n setStudioLocale(localeHint)\n }\n setHighlightedField(target.field.name)\n // Clear any timers from a prior jump so back-to-back jumps don't fight.\n focusTimersRef.current.forEach((t) => window.clearTimeout(t))\n const scrollTimer = window.setTimeout(() => {\n // Some controls (relation/asset pickers) don't render the input id —\n // the field label (`htmlFor`) is always present.\n const control =\n document.getElementById(`f-${target.field.name}`) ??\n document.querySelector(`label[for=\"${CSS.escape(`f-${target.field.name}`)}\"]`)\n control?.scrollIntoView({ block: 'center', behavior: 'smooth' })\n }, 60)\n const clearTimer = window.setTimeout(() => setHighlightedField(null), 3200)\n focusTimersRef.current = [scrollTimer, clearTimer]\n },\n [collection, source.localeConfig, setActiveTab],\n )\n useEffect(() => () => focusTimersRef.current.forEach((t) => window.clearTimeout(t)), [])\n\n // Reference deep links (EC-172): `?field=<index path>` pinpoints the exact field.\n useEffect(() => {\n if (!focusPath) return\n const parsed = parseDocumentFieldPath(focusPath)\n if (!parsed) return\n focusField(parsed.field, parsed.locale)\n }, [focusPath, focusField])\n\n return { highlightedField, inspectorPaneId, setInspectorPaneId, focusField }\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
3
|
+
/** The header's live-link descriptor: an `href` (open live) + a title, or a muted
|
|
4
|
+
* reason-only title while it isn't openable (loading / publish-first / no frontendUrl). */
|
|
5
|
+
export interface HeaderLive {
|
|
6
|
+
href?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure
|
|
11
|
+
* decision. Only a routable collection shows the row; its leaf opens the live page
|
|
12
|
+
* when the doc is published AND a frontendUrl is set (an absolute `href`),
|
|
13
|
+
* otherwise it is a muted icon whose tooltip explains WHY (still loading, publish
|
|
14
|
+
* first, or set the frontendUrl). The draft-derived path itself updates as you
|
|
15
|
+
* type — only the OPEN-live action is published-gated, preserving draft/live.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveHeaderLive(input: {
|
|
18
|
+
routableCollection: boolean;
|
|
19
|
+
liveUrl: {
|
|
20
|
+
href?: string;
|
|
21
|
+
path: string;
|
|
22
|
+
} | undefined;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
published: boolean;
|
|
25
|
+
}): HeaderLive | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
|
|
28
|
+
* published snapshot + its published ancestors), never the draft, so an unpublished
|
|
29
|
+
* slug/parent edit can't leak into the live URL. Resolves the header's live-link
|
|
30
|
+
* descriptor for the composed-URL row; `undefined` for a non-routable collection.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useLiveUrl(input: {
|
|
33
|
+
source: CollectionViewSource;
|
|
34
|
+
collection: CollectionDef;
|
|
35
|
+
doc: CmsDocument;
|
|
36
|
+
locale: Locale;
|
|
37
|
+
published: boolean;
|
|
38
|
+
routableCollection: boolean;
|
|
39
|
+
}): {
|
|
40
|
+
headerLive: HeaderLive | undefined;
|
|
41
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { urlForDocument } from '@elytracms/core/cms-core';
|
|
3
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
5
|
+
import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
|
|
6
|
+
/**
|
|
7
|
+
* 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure
|
|
8
|
+
* decision. Only a routable collection shows the row; its leaf opens the live page
|
|
9
|
+
* when the doc is published AND a frontendUrl is set (an absolute `href`),
|
|
10
|
+
* otherwise it is a muted icon whose tooltip explains WHY (still loading, publish
|
|
11
|
+
* first, or set the frontendUrl). The draft-derived path itself updates as you
|
|
12
|
+
* type — only the OPEN-live action is published-gated, preserving draft/live.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveHeaderLive(input) {
|
|
15
|
+
if (!input.routableCollection)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (input.liveUrl?.href) {
|
|
18
|
+
return { href: input.liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: input.liveUrl.path }) };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
title: input.loading
|
|
22
|
+
? translate('cms.editor.loadingLiveUrl')
|
|
23
|
+
: !input.published
|
|
24
|
+
? translate('cms.editor.publishToViewLive')
|
|
25
|
+
: translate('cms.editor.setFrontendUrlHint'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
|
|
30
|
+
* published snapshot + its published ancestors), never the draft, so an unpublished
|
|
31
|
+
* slug/parent edit can't leak into the live URL. Resolves the header's live-link
|
|
32
|
+
* descriptor for the composed-URL row; `undefined` for a non-routable collection.
|
|
33
|
+
*/
|
|
34
|
+
export function useLiveUrl(input) {
|
|
35
|
+
const { source, collection, doc, locale, published, routableCollection } = input;
|
|
36
|
+
const publishedDocs = usePublishedDocuments(source.projectId, collection.id);
|
|
37
|
+
const liveUrl = useMemo(() => {
|
|
38
|
+
const publishedDoc = publishedDocs.byId.get(doc.id);
|
|
39
|
+
if (!publishedDoc)
|
|
40
|
+
return undefined;
|
|
41
|
+
return urlForDocument({
|
|
42
|
+
routes: resolveDeploymentRoutes(),
|
|
43
|
+
frontendUrl: resolveDeploymentFrontendUrl(),
|
|
44
|
+
localeConfig: source.localeConfig,
|
|
45
|
+
}, collection, publishedDoc, { siblings: [...publishedDocs.byId.values()], locale });
|
|
46
|
+
}, [publishedDocs, collection, doc.id, locale, source.localeConfig]);
|
|
47
|
+
const headerLive = resolveHeaderLive({
|
|
48
|
+
routableCollection,
|
|
49
|
+
liveUrl,
|
|
50
|
+
loading: publishedDocs.status === 'loading',
|
|
51
|
+
published,
|
|
52
|
+
});
|
|
53
|
+
return { headerLive };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=use-live-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-live-url.js","sourceRoot":"","sources":["../../../src/components/cms/use-live-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAGzD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAA;AACvG,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAA;AASrF;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAKjC;IACC,IAAI,CAAC,KAAK,CAAC,kBAAkB;QAAE,OAAO,SAAS,CAAA;IAC/C,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,wBAAwB,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;IAC/G,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,OAAO;YAClB,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC;YACxC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS;gBAChB,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC;gBAC3C,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC;KACjD,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAO1B;IACC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAA;IAChF,MAAM,aAAa,GAAG,qBAAqB,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE;QAC3B,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnD,IAAI,CAAC,YAAY;YAAE,OAAO,SAAS,CAAA;QACnC,OAAO,cAAc,CACnB;YACE,MAAM,EAAE,uBAAuB,EAAE;YACjC,WAAW,EAAE,4BAA4B,EAAE;YAC3C,YAAY,EAAE,MAAM,CAAC,YAAY;SAClC,EACD,UAAU,EACV,YAAY,EACZ,EAAE,QAAQ,EAAE,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CACvD,CAAA;IACH,CAAC,EAAE,CAAC,aAAa,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAA;IAEpE,MAAM,UAAU,GAAG,iBAAiB,CAAC;QACnC,kBAAkB;QAClB,OAAO;QACP,OAAO,EAAE,aAAa,CAAC,MAAM,KAAK,SAAS;QAC3C,SAAS;KACV,CAAC,CAAA;IACF,OAAO,EAAE,UAAU,EAAE,CAAA;AACvB,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { urlForDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents'\n\n/** The header's live-link descriptor: an `href` (open live) + a title, or a muted\n * reason-only title while it isn't openable (loading / publish-first / no frontendUrl). */\nexport interface HeaderLive {\n href?: string\n title: string\n}\n\n/**\n * 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure\n * decision. Only a routable collection shows the row; its leaf opens the live page\n * when the doc is published AND a frontendUrl is set (an absolute `href`),\n * otherwise it is a muted icon whose tooltip explains WHY (still loading, publish\n * first, or set the frontendUrl). The draft-derived path itself updates as you\n * type — only the OPEN-live action is published-gated, preserving draft/live.\n */\nexport function resolveHeaderLive(input: {\n routableCollection: boolean\n liveUrl: { href?: string; path: string } | undefined\n loading: boolean\n published: boolean\n}): HeaderLive | undefined {\n if (!input.routableCollection) return undefined\n if (input.liveUrl?.href) {\n return { href: input.liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: input.liveUrl.path }) }\n }\n return {\n title: input.loading\n ? translate('cms.editor.loadingLiveUrl')\n : !input.published\n ? translate('cms.editor.publishToViewLive')\n : translate('cms.editor.setFrontendUrlHint'),\n }\n}\n\n/**\n * EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned\n * published snapshot + its published ancestors), never the draft, so an unpublished\n * slug/parent edit can't leak into the live URL. Resolves the header's live-link\n * descriptor for the composed-URL row; `undefined` for a non-routable collection.\n */\nexport function useLiveUrl(input: {\n source: CollectionViewSource\n collection: CollectionDef\n doc: CmsDocument\n locale: Locale\n published: boolean\n routableCollection: boolean\n}): { headerLive: HeaderLive | undefined } {\n const { source, collection, doc, locale, published, routableCollection } = input\n const publishedDocs = usePublishedDocuments(source.projectId, collection.id)\n const liveUrl = useMemo(() => {\n const publishedDoc = publishedDocs.byId.get(doc.id)\n if (!publishedDoc) return undefined\n return urlForDocument(\n {\n routes: resolveDeploymentRoutes(),\n frontendUrl: resolveDeploymentFrontendUrl(),\n localeConfig: source.localeConfig,\n },\n collection,\n publishedDoc,\n { siblings: [...publishedDocs.byId.values()], locale },\n )\n }, [publishedDocs, collection, doc.id, locale, source.localeConfig])\n\n const headerLive = resolveHeaderLive({\n routableCollection,\n liveUrl,\n loading: publishedDocs.status === 'loading',\n published,\n })\n return { headerLive }\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
3
|
+
export type PublishState = 'idle' | 'pending' | 'success';
|
|
4
|
+
/**
|
|
5
|
+
* EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the
|
|
6
|
+
* primary "Publish" and the "Publish & close" item can never diverge. A pure
|
|
7
|
+
* decision (directly testable): the Published perspective is read-only (switch to
|
|
8
|
+
* Draft to publish), schema errors block (with a count-aware hint), and an
|
|
9
|
+
* in-flight publish disables re-entry.
|
|
10
|
+
*/
|
|
11
|
+
export declare function publishGate(input: {
|
|
12
|
+
hasErrors: boolean;
|
|
13
|
+
viewReadOnly: boolean;
|
|
14
|
+
publishState: PublishState;
|
|
15
|
+
errorCount: number;
|
|
16
|
+
}): {
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
title: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
export interface PublishController {
|
|
21
|
+
publishState: PublishState;
|
|
22
|
+
/** Is the confirm-through dialog (unpublished hard refs) open? */
|
|
23
|
+
publishConfirm: boolean;
|
|
24
|
+
/** Publish (opening the confirm-through dialog first if there are unpublished refs). */
|
|
25
|
+
handlePublish: () => void;
|
|
26
|
+
/** Publish and, on success, return to the rail's list (0.0.39 "Publish & close"). */
|
|
27
|
+
handlePublishAndClose: () => void;
|
|
28
|
+
/** Confirm-through: publish despite the unpublished hard references. */
|
|
29
|
+
confirmPublish: () => void;
|
|
30
|
+
/** Dismiss the confirm-through dialog (also clears a pending "& close" intent). */
|
|
31
|
+
cancelPublishConfirm: () => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* EC-268 / EC-300 / 0.0.39 — the publish controller. Owns the publish indicator
|
|
35
|
+
* state, the EC-300 confirm-through guard (unpublished hard references would render
|
|
36
|
+
* empty on the live site, so publishing confirms rather than blocks), and the
|
|
37
|
+
* 0.0.39 "Publish & close" navigation (remembered across the confirm dialog via a
|
|
38
|
+
* ref, so a successful publish returns to the rail-owned list).
|
|
39
|
+
*/
|
|
40
|
+
export declare function usePublishController(input: {
|
|
41
|
+
source: CollectionViewSource;
|
|
42
|
+
docRef: DocumentRef;
|
|
43
|
+
collection: CollectionDef;
|
|
44
|
+
projectId: string;
|
|
45
|
+
draft: CmsDocument;
|
|
46
|
+
setDraft: (doc: CmsDocument) => void;
|
|
47
|
+
dirty: boolean;
|
|
48
|
+
hasUnpublishedRefs: boolean;
|
|
49
|
+
}): PublishController;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
|
|
3
|
+
import { clearHiddenValues } from '@elytracms/core/cms-core';
|
|
4
|
+
import { toast } from '@elytracms/studio/ui';
|
|
5
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
|
+
/**
|
|
7
|
+
* EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the
|
|
8
|
+
* primary "Publish" and the "Publish & close" item can never diverge. A pure
|
|
9
|
+
* decision (directly testable): the Published perspective is read-only (switch to
|
|
10
|
+
* Draft to publish), schema errors block (with a count-aware hint), and an
|
|
11
|
+
* in-flight publish disables re-entry.
|
|
12
|
+
*/
|
|
13
|
+
export function publishGate(input) {
|
|
14
|
+
const disabled = input.hasErrors || input.viewReadOnly || input.publishState === 'pending';
|
|
15
|
+
const title = input.viewReadOnly
|
|
16
|
+
? translate('cms.editor.switchToDraftToPublish')
|
|
17
|
+
: input.hasErrors
|
|
18
|
+
? translate(input.errorCount === 1
|
|
19
|
+
? 'cms.editor.fixIssuesToPublish.one'
|
|
20
|
+
: 'cms.editor.fixIssuesToPublish.other', { count: input.errorCount })
|
|
21
|
+
: undefined;
|
|
22
|
+
return { disabled, title };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* EC-268 / EC-300 / 0.0.39 — the publish controller. Owns the publish indicator
|
|
26
|
+
* state, the EC-300 confirm-through guard (unpublished hard references would render
|
|
27
|
+
* empty on the live site, so publishing confirms rather than blocks), and the
|
|
28
|
+
* 0.0.39 "Publish & close" navigation (remembered across the confirm dialog via a
|
|
29
|
+
* ref, so a successful publish returns to the rail-owned list).
|
|
30
|
+
*/
|
|
31
|
+
export function usePublishController(input) {
|
|
32
|
+
const { source, docRef, collection, projectId, draft, setDraft, dirty, hasUnpublishedRefs } = input;
|
|
33
|
+
const navigate = useNavigate();
|
|
34
|
+
const [publishState, setPublishState] = useState('idle');
|
|
35
|
+
const [publishConfirm, setPublishConfirm] = useState(false);
|
|
36
|
+
// The split-button "Publish & close" intent — remembered across the confirm-through
|
|
37
|
+
// dialog so a successful publish returns to the list. Cleared on cancel or failure.
|
|
38
|
+
const closeAfterPublishRef = useRef(false);
|
|
39
|
+
const doPublish = useCallback(async () => {
|
|
40
|
+
// EC-273: clean conditionally-hidden values from the draft AT PUBLISH TIME —
|
|
41
|
+
// into the draft AND the snapshot, so both go clean together. Also captures the
|
|
42
|
+
// latest edit if Publish fires inside the autosave debounce window; the serial
|
|
43
|
+
// persister runs the upsert before the publish so the snapshot is this draft.
|
|
44
|
+
const cleaned = clearHiddenValues(collection, draft);
|
|
45
|
+
if (cleaned !== draft)
|
|
46
|
+
setDraft(cleaned);
|
|
47
|
+
if (cleaned !== draft || dirty)
|
|
48
|
+
source.save(cleaned);
|
|
49
|
+
setPublishState('pending');
|
|
50
|
+
try {
|
|
51
|
+
await source.publishing?.publish(docRef);
|
|
52
|
+
setPublishState('success');
|
|
53
|
+
setTimeout(() => setPublishState('idle'), 1800);
|
|
54
|
+
// "Publish & close": return to the list once the publish landed (never on a
|
|
55
|
+
// failure). Rail-owned `listPath`, falling back to the project index (AD-19).
|
|
56
|
+
if (closeAfterPublishRef.current) {
|
|
57
|
+
closeAfterPublishRef.current = false;
|
|
58
|
+
void navigate({
|
|
59
|
+
to: STUDIO_PROJECT_ROUTE,
|
|
60
|
+
params: { projectId, _splat: source.listPath ?? studioPath.index() },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
closeAfterPublishRef.current = false;
|
|
66
|
+
setPublishState('idle');
|
|
67
|
+
toast.error(error instanceof Error
|
|
68
|
+
? translate('cms.editor.publishFailedReason', { error: error.message })
|
|
69
|
+
: translate('cms.editor.publishFailed'));
|
|
70
|
+
}
|
|
71
|
+
}, [collection, draft, setDraft, dirty, source, docRef, navigate, projectId]);
|
|
72
|
+
// Gate publish on the confirm-through dialog when there are unpublished hard
|
|
73
|
+
// references; schema errors already disable the button, so they never reach here.
|
|
74
|
+
const handlePublish = useCallback(() => {
|
|
75
|
+
if (hasUnpublishedRefs) {
|
|
76
|
+
setPublishConfirm(true);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
void doPublish();
|
|
80
|
+
}, [hasUnpublishedRefs, doPublish]);
|
|
81
|
+
const handlePublishAndClose = useCallback(() => {
|
|
82
|
+
closeAfterPublishRef.current = true;
|
|
83
|
+
handlePublish();
|
|
84
|
+
}, [handlePublish]);
|
|
85
|
+
const confirmPublish = useCallback(() => {
|
|
86
|
+
setPublishConfirm(false);
|
|
87
|
+
void doPublish();
|
|
88
|
+
}, [doPublish]);
|
|
89
|
+
const cancelPublishConfirm = useCallback(() => {
|
|
90
|
+
setPublishConfirm(false);
|
|
91
|
+
closeAfterPublishRef.current = false;
|
|
92
|
+
}, []);
|
|
93
|
+
return {
|
|
94
|
+
publishState,
|
|
95
|
+
publishConfirm,
|
|
96
|
+
handlePublish,
|
|
97
|
+
handlePublishAndClose,
|
|
98
|
+
confirmPublish,
|
|
99
|
+
cancelPublishConfirm,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=use-publish-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-publish-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-publish-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { clearHiddenValues } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { toast } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\nexport type PublishState = 'idle' | 'pending' | 'success'\n\n/**\n * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the\n * primary \"Publish\" and the \"Publish & close\" item can never diverge. A pure\n * decision (directly testable): the Published perspective is read-only (switch to\n * Draft to publish), schema errors block (with a count-aware hint), and an\n * in-flight publish disables re-entry.\n */\nexport function publishGate(input: {\n hasErrors: boolean\n viewReadOnly: boolean\n publishState: PublishState\n errorCount: number\n}): { disabled: boolean; title: string | undefined } {\n const disabled = input.hasErrors || input.viewReadOnly || input.publishState === 'pending'\n const title = input.viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : input.hasErrors\n ? translate(\n input.errorCount === 1\n ? 'cms.editor.fixIssuesToPublish.one'\n : 'cms.editor.fixIssuesToPublish.other',\n { count: input.errorCount },\n )\n : undefined\n return { disabled, title }\n}\n\nexport interface PublishController {\n publishState: PublishState\n /** Is the confirm-through dialog (unpublished hard refs) open? */\n publishConfirm: boolean\n /** Publish (opening the confirm-through dialog first if there are unpublished refs). */\n handlePublish: () => void\n /** Publish and, on success, return to the rail's list (0.0.39 \"Publish & close\"). */\n handlePublishAndClose: () => void\n /** Confirm-through: publish despite the unpublished hard references. */\n confirmPublish: () => void\n /** Dismiss the confirm-through dialog (also clears a pending \"& close\" intent). */\n cancelPublishConfirm: () => void\n}\n\n/**\n * EC-268 / EC-300 / 0.0.39 — the publish controller. Owns the publish indicator\n * state, the EC-300 confirm-through guard (unpublished hard references would render\n * empty on the live site, so publishing confirms rather than blocks), and the\n * 0.0.39 \"Publish & close\" navigation (remembered across the confirm dialog via a\n * ref, so a successful publish returns to the rail-owned list).\n */\nexport function usePublishController(input: {\n source: CollectionViewSource\n docRef: DocumentRef\n collection: CollectionDef\n projectId: string\n draft: CmsDocument\n setDraft: (doc: CmsDocument) => void\n dirty: boolean\n hasUnpublishedRefs: boolean\n}): PublishController {\n const { source, docRef, collection, projectId, draft, setDraft, dirty, hasUnpublishedRefs } = input\n const navigate = useNavigate()\n const [publishState, setPublishState] = useState<PublishState>('idle')\n const [publishConfirm, setPublishConfirm] = useState(false)\n // The split-button \"Publish & close\" intent — remembered across the confirm-through\n // dialog so a successful publish returns to the list. Cleared on cancel or failure.\n const closeAfterPublishRef = useRef(false)\n\n const doPublish = useCallback(async () => {\n // EC-273: clean conditionally-hidden values from the draft AT PUBLISH TIME —\n // into the draft AND the snapshot, so both go clean together. Also captures the\n // latest edit if Publish fires inside the autosave debounce window; the serial\n // persister runs the upsert before the publish so the snapshot is this draft.\n const cleaned = clearHiddenValues(collection, draft)\n if (cleaned !== draft) setDraft(cleaned)\n if (cleaned !== draft || dirty) source.save(cleaned)\n setPublishState('pending')\n try {\n await source.publishing?.publish(docRef)\n setPublishState('success')\n setTimeout(() => setPublishState('idle'), 1800)\n // \"Publish & close\": return to the list once the publish landed (never on a\n // failure). Rail-owned `listPath`, falling back to the project index (AD-19).\n if (closeAfterPublishRef.current) {\n closeAfterPublishRef.current = false\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: source.listPath ?? studioPath.index() },\n })\n }\n } catch (error) {\n closeAfterPublishRef.current = false\n setPublishState('idle')\n toast.error(\n error instanceof Error\n ? translate('cms.editor.publishFailedReason', { error: error.message })\n : translate('cms.editor.publishFailed'),\n )\n }\n }, [collection, draft, setDraft, dirty, source, docRef, navigate, projectId])\n\n // Gate publish on the confirm-through dialog when there are unpublished hard\n // references; schema errors already disable the button, so they never reach here.\n const handlePublish = useCallback(() => {\n if (hasUnpublishedRefs) {\n setPublishConfirm(true)\n return\n }\n void doPublish()\n }, [hasUnpublishedRefs, doPublish])\n\n const handlePublishAndClose = useCallback(() => {\n closeAfterPublishRef.current = true\n handlePublish()\n }, [handlePublish])\n\n const confirmPublish = useCallback(() => {\n setPublishConfirm(false)\n void doPublish()\n }, [doPublish])\n\n const cancelPublishConfirm = useCallback(() => {\n setPublishConfirm(false)\n closeAfterPublishRef.current = false\n }, [])\n\n return {\n publishState,\n publishConfirm,\n handlePublish,\n handlePublishAndClose,\n confirmPublish,\n cancelPublishConfirm,\n }\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { QueryKey } from '@tanstack/react-query';
|
|
2
|
+
import type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core';
|
|
3
|
+
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
4
|
+
import type { DocumentVersionsView } from './cms-store';
|
|
5
|
+
import type { PublishedSnapshotState } from './versions-model';
|
|
6
|
+
export type Perspective = 'draft' | 'published';
|
|
7
|
+
/**
|
|
8
|
+
* EC-226 — which document the fieldset renders, given the active perspective and
|
|
9
|
+
* the pinned published snapshot's load state. A pure decision so "never present
|
|
10
|
+
* the draft AS published" is single-sourced and directly testable:
|
|
11
|
+
*
|
|
12
|
+
* - Draft perspective → the live draft, editable.
|
|
13
|
+
* - Published perspective, snapshot READY → the pinned snapshot, read-only.
|
|
14
|
+
* - Published perspective, snapshot NOT ready → still the draft as the fallback
|
|
15
|
+
* render target, but flagged `publishedPending` so the shell shows a placeholder
|
|
16
|
+
* instead (the draft must never be shown labelled as the published content).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveViewDoc(input: {
|
|
19
|
+
perspective: Perspective;
|
|
20
|
+
publishedSnapshot: PublishedSnapshotState;
|
|
21
|
+
draft: CmsDocument;
|
|
22
|
+
}): {
|
|
23
|
+
viewReadOnly: boolean;
|
|
24
|
+
viewDoc: CmsDocument;
|
|
25
|
+
publishedPending: boolean;
|
|
26
|
+
};
|
|
27
|
+
export interface PublishedPerspective {
|
|
28
|
+
perspective: Perspective;
|
|
29
|
+
setPerspective: (perspective: Perspective) => void;
|
|
30
|
+
/** Is the document published (a version pointer is set)? */
|
|
31
|
+
published: boolean;
|
|
32
|
+
/** A publishing AND versioned rail — gates the version-history query + Discard. */
|
|
33
|
+
canDiscard: boolean;
|
|
34
|
+
/** The recorded version history (null while loading / disabled). */
|
|
35
|
+
versionsView: DocumentVersionsView | null;
|
|
36
|
+
/** The shared version-history cache key ({@link documentVersionsKey}). */
|
|
37
|
+
versionKey: QueryKey;
|
|
38
|
+
/** The pinned published version number, or undefined while loading / none pinned. */
|
|
39
|
+
publishedVersionNumber: number | undefined;
|
|
40
|
+
/** The pinned published version's snapshot, or undefined while loading / none. */
|
|
41
|
+
publishedSnapshotDoc: CmsDocument | undefined;
|
|
42
|
+
/** The loaded state of the pinned published snapshot. */
|
|
43
|
+
publishedSnapshot: PublishedSnapshotState;
|
|
44
|
+
/** Is the editor viewing the read-only Published snapshot? */
|
|
45
|
+
viewReadOnly: boolean;
|
|
46
|
+
/** The document the fieldset renders (draft or the pinned snapshot). */
|
|
47
|
+
viewDoc: CmsDocument;
|
|
48
|
+
/** Is the Published snapshot still loading (show a placeholder, not the draft)? */
|
|
49
|
+
publishedPending: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective plus the pinned
|
|
53
|
+
* published snapshot, both derived from the ONE shared version-history query
|
|
54
|
+
* (AD-17: the single operations-backed cache, `use-document-versions.ts` — no raw
|
|
55
|
+
* `source.versioning.load` call). The Discard-draft gating and the Published
|
|
56
|
+
* perspective read exactly the same `versions`/`publishedVersion` shape, so a
|
|
57
|
+
* second bespoke fetch on perspective switch would just re-read this. Gated the
|
|
58
|
+
* same way Discard always was — a publishing + versioned rail AND a published
|
|
59
|
+
* document — which is also exactly when the Published perspective is reachable.
|
|
60
|
+
*/
|
|
61
|
+
export declare function usePublishedPerspective(input: {
|
|
62
|
+
ref: DocumentRef;
|
|
63
|
+
source: CollectionViewSource;
|
|
64
|
+
canPublish: boolean;
|
|
65
|
+
canVersions: boolean;
|
|
66
|
+
draft: CmsDocument;
|
|
67
|
+
}): PublishedPerspective;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useDocumentVersionsQuery } from './use-document-versions';
|
|
3
|
+
import { derivePublishedSnapshot, publishedVersionSnapshot } from './versions-model';
|
|
4
|
+
/**
|
|
5
|
+
* EC-226 — which document the fieldset renders, given the active perspective and
|
|
6
|
+
* the pinned published snapshot's load state. A pure decision so "never present
|
|
7
|
+
* the draft AS published" is single-sourced and directly testable:
|
|
8
|
+
*
|
|
9
|
+
* - Draft perspective → the live draft, editable.
|
|
10
|
+
* - Published perspective, snapshot READY → the pinned snapshot, read-only.
|
|
11
|
+
* - Published perspective, snapshot NOT ready → still the draft as the fallback
|
|
12
|
+
* render target, but flagged `publishedPending` so the shell shows a placeholder
|
|
13
|
+
* instead (the draft must never be shown labelled as the published content).
|
|
14
|
+
*/
|
|
15
|
+
export function resolveViewDoc(input) {
|
|
16
|
+
const viewReadOnly = input.perspective === 'published';
|
|
17
|
+
const ready = input.publishedSnapshot.status === 'ready';
|
|
18
|
+
return {
|
|
19
|
+
viewReadOnly,
|
|
20
|
+
viewDoc: viewReadOnly && input.publishedSnapshot.status === 'ready' ? input.publishedSnapshot.doc : input.draft,
|
|
21
|
+
publishedPending: viewReadOnly && !ready,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective plus the pinned
|
|
26
|
+
* published snapshot, both derived from the ONE shared version-history query
|
|
27
|
+
* (AD-17: the single operations-backed cache, `use-document-versions.ts` — no raw
|
|
28
|
+
* `source.versioning.load` call). The Discard-draft gating and the Published
|
|
29
|
+
* perspective read exactly the same `versions`/`publishedVersion` shape, so a
|
|
30
|
+
* second bespoke fetch on perspective switch would just re-read this. Gated the
|
|
31
|
+
* same way Discard always was — a publishing + versioned rail AND a published
|
|
32
|
+
* document — which is also exactly when the Published perspective is reachable.
|
|
33
|
+
*/
|
|
34
|
+
export function usePublishedPerspective(input) {
|
|
35
|
+
const { ref, source, canPublish, canVersions, draft } = input;
|
|
36
|
+
const published = source.publishing?.isPublished(ref) ?? false;
|
|
37
|
+
const [perspective, setPerspective] = useState('draft');
|
|
38
|
+
// Snap back to Draft if the document is unpublished while viewing Published.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!published)
|
|
41
|
+
setPerspective('draft');
|
|
42
|
+
}, [published]);
|
|
43
|
+
const canDiscard = canPublish && canVersions;
|
|
44
|
+
const { versionsQuery, versionKey } = useDocumentVersionsQuery(ref, {
|
|
45
|
+
enabled: canDiscard && published,
|
|
46
|
+
});
|
|
47
|
+
const versionsView = versionsQuery.data ?? null;
|
|
48
|
+
const publishedVersionNumber = versionsView?.publishedVersion;
|
|
49
|
+
const publishedSnapshotDoc = publishedVersionSnapshot(versionsView);
|
|
50
|
+
const publishedSnapshot = derivePublishedSnapshot(versionsView);
|
|
51
|
+
const { viewReadOnly, viewDoc, publishedPending } = resolveViewDoc({ perspective, publishedSnapshot, draft });
|
|
52
|
+
return {
|
|
53
|
+
perspective,
|
|
54
|
+
setPerspective,
|
|
55
|
+
published,
|
|
56
|
+
canDiscard,
|
|
57
|
+
versionsView,
|
|
58
|
+
versionKey,
|
|
59
|
+
publishedVersionNumber,
|
|
60
|
+
publishedSnapshotDoc,
|
|
61
|
+
publishedSnapshot,
|
|
62
|
+
viewReadOnly,
|
|
63
|
+
viewDoc,
|
|
64
|
+
publishedPending,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=use-published-perspective.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-published-perspective.js","sourceRoot":"","sources":["../../../src/components/cms/use-published-perspective.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAK3C,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAKpF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,KAI9B;IACC,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,KAAK,WAAW,CAAA;IACtD,MAAM,KAAK,GAAG,KAAK,CAAC,iBAAiB,CAAC,MAAM,KAAK,OAAO,CAAA;IACxD,OAAO;QACL,YAAY;QACZ,OAAO,EAAE,YAAY,IAAI,KAAK,CAAC,iBAAiB,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK;QAC/G,gBAAgB,EAAE,YAAY,IAAI,CAAC,KAAK;KACzC,CAAA;AACH,CAAC;AA2BD;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAMvC;IACC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;IAC7D,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI,KAAK,CAAA;IAC9D,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAc,OAAO,CAAC,CAAA;IACpE,6EAA6E;IAC7E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,SAAS;YAAE,cAAc,CAAC,OAAO,CAAC,CAAA;IACzC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAA;IAEf,MAAM,UAAU,GAAG,UAAU,IAAI,WAAW,CAAA;IAC5C,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,wBAAwB,CAAC,GAAG,EAAE;QAClE,OAAO,EAAE,UAAU,IAAI,SAAS;KACjC,CAAC,CAAA;IACF,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,IAAI,IAAI,CAAA;IAC/C,MAAM,sBAAsB,GAAG,YAAY,EAAE,gBAAgB,CAAA;IAC7D,MAAM,oBAAoB,GAAG,wBAAwB,CAAC,YAAY,CAAC,CAAA;IACnE,MAAM,iBAAiB,GAAG,uBAAuB,CAAC,YAAY,CAAC,CAAA;IAC/D,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,gBAAgB,EAAE,GAAG,cAAc,CAAC,EAAE,WAAW,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC,CAAA;IAE7G,OAAO;QACL,WAAW;QACX,cAAc;QACd,SAAS;QACT,UAAU;QACV,YAAY;QACZ,UAAU;QACV,sBAAsB;QACtB,oBAAoB;QACpB,iBAAiB;QACjB,YAAY;QACZ,OAAO;QACP,gBAAgB;KACjB,CAAA;AACH,CAAC","sourcesContent":["import { useEffect, useState } from 'react'\nimport type { QueryKey } from '@tanstack/react-query'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport type { DocumentVersionsView } from './cms-store'\nimport { useDocumentVersionsQuery } from './use-document-versions'\nimport { derivePublishedSnapshot, publishedVersionSnapshot } from './versions-model'\nimport type { PublishedSnapshotState } from './versions-model'\n\nexport type Perspective = 'draft' | 'published'\n\n/**\n * EC-226 — which document the fieldset renders, given the active perspective and\n * the pinned published snapshot's load state. A pure decision so \"never present\n * the draft AS published\" is single-sourced and directly testable:\n *\n * - Draft perspective → the live draft, editable.\n * - Published perspective, snapshot READY → the pinned snapshot, read-only.\n * - Published perspective, snapshot NOT ready → still the draft as the fallback\n * render target, but flagged `publishedPending` so the shell shows a placeholder\n * instead (the draft must never be shown labelled as the published content).\n */\nexport function resolveViewDoc(input: {\n perspective: Perspective\n publishedSnapshot: PublishedSnapshotState\n draft: CmsDocument\n}): { viewReadOnly: boolean; viewDoc: CmsDocument; publishedPending: boolean } {\n const viewReadOnly = input.perspective === 'published'\n const ready = input.publishedSnapshot.status === 'ready'\n return {\n viewReadOnly,\n viewDoc: viewReadOnly && input.publishedSnapshot.status === 'ready' ? input.publishedSnapshot.doc : input.draft,\n publishedPending: viewReadOnly && !ready,\n }\n}\n\nexport interface PublishedPerspective {\n perspective: Perspective\n setPerspective: (perspective: Perspective) => void\n /** Is the document published (a version pointer is set)? */\n published: boolean\n /** A publishing AND versioned rail — gates the version-history query + Discard. */\n canDiscard: boolean\n /** The recorded version history (null while loading / disabled). */\n versionsView: DocumentVersionsView | null\n /** The shared version-history cache key ({@link documentVersionsKey}). */\n versionKey: QueryKey\n /** The pinned published version number, or undefined while loading / none pinned. */\n publishedVersionNumber: number | undefined\n /** The pinned published version's snapshot, or undefined while loading / none. */\n publishedSnapshotDoc: CmsDocument | undefined\n /** The loaded state of the pinned published snapshot. */\n publishedSnapshot: PublishedSnapshotState\n /** Is the editor viewing the read-only Published snapshot? */\n viewReadOnly: boolean\n /** The document the fieldset renders (draft or the pinned snapshot). */\n viewDoc: CmsDocument\n /** Is the Published snapshot still loading (show a placeholder, not the draft)? */\n publishedPending: boolean\n}\n\n/**\n * EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective plus the pinned\n * published snapshot, both derived from the ONE shared version-history query\n * (AD-17: the single operations-backed cache, `use-document-versions.ts` — no raw\n * `source.versioning.load` call). The Discard-draft gating and the Published\n * perspective read exactly the same `versions`/`publishedVersion` shape, so a\n * second bespoke fetch on perspective switch would just re-read this. Gated the\n * same way Discard always was — a publishing + versioned rail AND a published\n * document — which is also exactly when the Published perspective is reachable.\n */\nexport function usePublishedPerspective(input: {\n ref: DocumentRef\n source: CollectionViewSource\n canPublish: boolean\n canVersions: boolean\n draft: CmsDocument\n}): PublishedPerspective {\n const { ref, source, canPublish, canVersions, draft } = input\n const published = source.publishing?.isPublished(ref) ?? false\n const [perspective, setPerspective] = useState<Perspective>('draft')\n // Snap back to Draft if the document is unpublished while viewing Published.\n useEffect(() => {\n if (!published) setPerspective('draft')\n }, [published])\n\n const canDiscard = canPublish && canVersions\n const { versionsQuery, versionKey } = useDocumentVersionsQuery(ref, {\n enabled: canDiscard && published,\n })\n const versionsView = versionsQuery.data ?? null\n const publishedVersionNumber = versionsView?.publishedVersion\n const publishedSnapshotDoc = publishedVersionSnapshot(versionsView)\n const publishedSnapshot = derivePublishedSnapshot(versionsView)\n const { viewReadOnly, viewDoc, publishedPending } = resolveViewDoc({ perspective, publishedSnapshot, draft })\n\n return {\n perspective,\n setPerspective,\n published,\n canDiscard,\n versionsView,\n versionKey,\n publishedVersionNumber,\n publishedSnapshotDoc,\n publishedSnapshot,\n viewReadOnly,\n viewDoc,\n publishedPending,\n }\n}\n"]}
|