@prenta/admin 1.1.0 → 1.3.0
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/CHANGELOG.md +12 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js +1 -22
- package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -1
- package/dist/__tests__/lib/page-audit-input.test.js +81 -0
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -1
- package/dist/__tests__/lib/preview-bridge.test.js +19 -79
- package/dist/__tests__/lib/preview-bridge.test.js.map +1 -1
- package/dist/__tests__/lib/version-history.test.js +18 -0
- package/dist/__tests__/lib/version-history.test.js.map +1 -1
- package/dist/__tests__/lib/view-site-chrome.test.js +11 -1
- package/dist/__tests__/lib/view-site-chrome.test.js.map +1 -1
- package/dist/__tests__/no-server-barrel-imports.test.js +3 -4
- package/dist/__tests__/no-server-barrel-imports.test.js.map +1 -1
- package/dist/__tests__/views/canvas-review-mode.render.test.js +11 -1
- package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -1
- package/dist/__tests__/views/editor-top-bar.render.test.js +13 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/page-section-editor.test.js +23 -9
- package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/post-section-editor.test.js +38 -0
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/post-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/post-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/post-tab.render.test.js +86 -0
- package/dist/__tests__/views/post-tab.render.test.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.d.ts +9 -1
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +5 -2
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/hooks/usePageAudit.d.ts +4 -1
- package/dist/hooks/usePageAudit.d.ts.map +1 -1
- package/dist/hooks/usePageAudit.js +3 -3
- package/dist/hooks/usePageAudit.js.map +1 -1
- package/dist/lib/canvas-edit-chrome.d.ts +0 -17
- package/dist/lib/canvas-edit-chrome.d.ts.map +1 -1
- package/dist/lib/canvas-edit-chrome.js +0 -62
- package/dist/lib/canvas-edit-chrome.js.map +1 -1
- package/dist/lib/page-audit-input.d.ts +26 -1
- package/dist/lib/page-audit-input.d.ts.map +1 -1
- package/dist/lib/page-audit-input.js +48 -2
- package/dist/lib/page-audit-input.js.map +1 -1
- package/dist/lib/preview-bridge.d.ts +7 -74
- package/dist/lib/preview-bridge.d.ts.map +1 -1
- package/dist/lib/preview-bridge.js +12 -73
- package/dist/lib/preview-bridge.js.map +1 -1
- package/dist/lib/version-history.d.ts +5 -0
- package/dist/lib/version-history.d.ts.map +1 -1
- package/dist/lib/version-history.js +18 -0
- package/dist/lib/version-history.js.map +1 -1
- package/dist/lib/view-site-chrome.d.ts +3 -0
- package/dist/lib/view-site-chrome.d.ts.map +1 -1
- package/dist/lib/view-site-chrome.js +11 -2
- package/dist/lib/view-site-chrome.js.map +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +3 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +3 -2
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +8 -47
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +2 -1
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +15 -2
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +2 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostFieldsForm.d.ts +39 -0
- package/dist/views/post-editor/PostFieldsForm.d.ts.map +1 -0
- package/dist/views/post-editor/PostFieldsForm.js +85 -0
- package/dist/views/post-editor/PostFieldsForm.js.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts +2 -14
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.js +20 -81
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +128 -74
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTab.d.ts +17 -0
- package/dist/views/post-editor/PostTab.d.ts.map +1 -0
- package/dist/views/post-editor/PostTab.js +23 -0
- package/dist/views/post-editor/PostTab.js.map +1 -0
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +2 -1
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
- package/dist/views/settings/SiteInformationCard.js +3 -1
- package/dist/views/settings/SiteInformationCard.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/lib/canvas-edit-chrome.test.ts +0 -27
- package/src/__tests__/lib/page-audit-input.test.ts +99 -0
- package/src/__tests__/lib/preview-bridge.test.ts +21 -110
- package/src/__tests__/lib/version-history.test.ts +20 -0
- package/src/__tests__/lib/view-site-chrome.test.ts +16 -1
- package/src/__tests__/no-server-barrel-imports.test.ts +3 -4
- package/src/__tests__/views/canvas-review-mode.render.test.tsx +14 -1
- package/src/__tests__/views/editor-top-bar.render.test.tsx +25 -0
- package/src/__tests__/views/page-section-editor.test.tsx +23 -13
- package/src/__tests__/views/post-section-editor.test.tsx +59 -0
- package/src/__tests__/views/post-tab.render.test.tsx +116 -0
- package/src/components/seo/SeoEditorPane.tsx +16 -1
- package/src/hooks/usePageAudit.ts +9 -2
- package/src/lib/canvas-edit-chrome.ts +0 -64
- package/src/lib/page-audit-input.ts +67 -1
- package/src/lib/preview-bridge.ts +11 -157
- package/src/lib/version-history.ts +23 -0
- package/src/lib/view-site-chrome.ts +15 -3
- package/src/views/page-editor/EditorCanvas.tsx +1 -1
- package/src/views/page-editor/EditorTopBar.tsx +6 -2
- package/src/views/page-editor/PageSectionEditor.tsx +6 -63
- package/src/views/page-editor/sections/SectionRenderer.tsx +5 -2
- package/src/views/post-editor/PostEditorCanvas.tsx +28 -6
- package/src/views/post-editor/PostFieldsForm.tsx +322 -0
- package/src/views/post-editor/PostFieldsModal.tsx +30 -281
- package/src/views/post-editor/PostSectionEditor.tsx +209 -116
- package/src/views/post-editor/PostTab.tsx +46 -0
- package/src/views/settings/SeoSettingsTab.tsx +21 -0
- package/src/views/settings/SiteInformationCard.tsx +7 -0
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import { toast } from 'sonner';
|
|
6
|
-
import { normalizeSlug, validateSlug } from '@prenta/core/client';
|
|
4
|
+
import { validateSlug } from '@prenta/core/client';
|
|
7
5
|
import { Modal } from '../../components/ui/Modal.js';
|
|
8
6
|
import { Button } from '../../components/ui/Button.js';
|
|
9
7
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js';
|
|
10
|
-
import {
|
|
11
|
-
import { TagInput } from '../../components/TagInput.js';
|
|
12
|
-
import { coauthorText } from '../../lib/coauthor-client.js';
|
|
13
|
-
const LABEL = 'text-foreground mb-1 block text-xs font-medium';
|
|
14
|
-
/** Match TipTap body editor borders — `border-input` is transparent in theme.css. */
|
|
15
|
-
const INPUT = 'border-border bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none';
|
|
16
|
-
const slugify = normalizeSlug;
|
|
8
|
+
import { PostFieldsForm, } from './PostFieldsForm.js';
|
|
17
9
|
function snapshotFromPost(post) {
|
|
18
10
|
return {
|
|
19
11
|
title: post.title,
|
|
@@ -30,20 +22,6 @@ function snapshotFromFields(input) {
|
|
|
30
22
|
tags: [...input.tags].sort().join('\0'),
|
|
31
23
|
};
|
|
32
24
|
}
|
|
33
|
-
function stripHtml(html) {
|
|
34
|
-
return html
|
|
35
|
-
.replace(/<[^>]*>/g, ' ')
|
|
36
|
-
.replace(/\s+/g, ' ')
|
|
37
|
-
.trim();
|
|
38
|
-
}
|
|
39
|
-
function buildContentContext(title, body, excerpt) {
|
|
40
|
-
const plain = stripHtml(body);
|
|
41
|
-
const parts = [title.trim(), excerpt.trim(), plain].filter(Boolean);
|
|
42
|
-
return parts.join('\n\n');
|
|
43
|
-
}
|
|
44
|
-
function AiFieldButton({ label, busy, disabled, onClick, }) {
|
|
45
|
-
return (_jsxs("button", { type: "button", "aria-label": label, title: label, disabled: disabled || busy, onClick: onClick, className: "inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium text-[var(--acc)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60", children: [busy ? (_jsx(Loader2, { className: "h-3 w-3 animate-spin", "aria-hidden": true })) : (_jsx(Sparkles, { className: "h-3 w-3", "aria-hidden": true })), busy ? 'Generating…' : 'AI'] }));
|
|
46
|
-
}
|
|
47
25
|
/** Edits post metadata: title, slug, excerpt, featured image, category, and tags. SEO lives in the SEO tab. */
|
|
48
26
|
export function PostFieldsModal({ open, post, canEdit, fields, onClose, onSave, }) {
|
|
49
27
|
const [title, setTitle] = useState(post.title);
|
|
@@ -53,18 +31,10 @@ export function PostFieldsModal({ open, post, canEdit, fields, onClose, onSave,
|
|
|
53
31
|
const [category, setCategory] = useState(post.category);
|
|
54
32
|
const [tags, setTags] = useState(post.tags);
|
|
55
33
|
const [slugTouched, setSlugTouched] = useState(Boolean(post.slug));
|
|
56
|
-
const [mediaOpen, setMediaOpen] = useState(false);
|
|
57
|
-
const [aiBusy, setAiBusy] = useState(false);
|
|
58
34
|
const [confirmDiscard, setConfirmDiscard] = useState(false);
|
|
59
35
|
const baselineRef = useRef(null);
|
|
60
36
|
// Land the user in the Title field (name-it-first flow for new posts).
|
|
61
37
|
const titleRef = useRef(null);
|
|
62
|
-
const categoryField = fields?.category;
|
|
63
|
-
const categoryOptions = categoryField?.type === 'select' && Array.isArray(categoryField.options)
|
|
64
|
-
? categoryField.options
|
|
65
|
-
: null;
|
|
66
|
-
const tagsField = fields?.tags;
|
|
67
|
-
const hasTags = tagsField?.type === 'array';
|
|
68
38
|
useEffect(() => {
|
|
69
39
|
if (!open) {
|
|
70
40
|
baselineRef.current = null;
|
|
@@ -78,22 +48,14 @@ export function PostFieldsModal({ open, post, canEdit, fields, onClose, onSave,
|
|
|
78
48
|
setCategory(post.category);
|
|
79
49
|
setTags(post.tags);
|
|
80
50
|
setSlugTouched(Boolean(post.slug));
|
|
81
|
-
setAiBusy(false);
|
|
82
51
|
baselineRef.current = snapshotFromPost(post);
|
|
83
52
|
}, [open, post]);
|
|
53
|
+
const values = { title, slug, excerpt, featuredImage, category, tags };
|
|
84
54
|
const isDirty = useMemo(() => {
|
|
85
55
|
if (!open || !baselineRef.current)
|
|
86
56
|
return false;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
slug,
|
|
90
|
-
excerpt,
|
|
91
|
-
featuredImage,
|
|
92
|
-
category,
|
|
93
|
-
tags,
|
|
94
|
-
});
|
|
95
|
-
return JSON.stringify(current) !== JSON.stringify(baselineRef.current);
|
|
96
|
-
}, [open, title, slug, excerpt, featuredImage, category, tags]);
|
|
57
|
+
return JSON.stringify(snapshotFromFields(values)) !== JSON.stringify(baselineRef.current);
|
|
58
|
+
}, [open, values]);
|
|
97
59
|
function requestDismiss() {
|
|
98
60
|
if (isDirty) {
|
|
99
61
|
setConfirmDiscard(true);
|
|
@@ -101,50 +63,27 @@ export function PostFieldsModal({ open, post, canEdit, fields, onClose, onSave,
|
|
|
101
63
|
}
|
|
102
64
|
onClose();
|
|
103
65
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
66
|
+
function handleChange(patch) {
|
|
67
|
+
if (patch.title !== undefined)
|
|
68
|
+
setTitle(patch.title);
|
|
69
|
+
if (patch.slug !== undefined)
|
|
70
|
+
setSlug(patch.slug);
|
|
71
|
+
if (patch.excerpt !== undefined)
|
|
72
|
+
setExcerpt(patch.excerpt);
|
|
73
|
+
if (patch.featuredImage !== undefined)
|
|
74
|
+
setFeaturedImage(patch.featuredImage);
|
|
75
|
+
if (patch.category !== undefined)
|
|
76
|
+
setCategory(patch.category);
|
|
77
|
+
if (patch.tags !== undefined)
|
|
78
|
+
setTags(patch.tags);
|
|
108
79
|
}
|
|
109
80
|
const slugError = validateSlug(slug);
|
|
110
|
-
async function handleAiGenerateExcerpt() {
|
|
111
|
-
const context = buildContentContext(title, post.body, excerpt);
|
|
112
|
-
if (!context.trim()) {
|
|
113
|
-
toast.error('Add a title or write body content in the Article Body section before generating with AI.');
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
setAiBusy(true);
|
|
117
|
-
try {
|
|
118
|
-
const outcome = await coauthorText('compress', context, {
|
|
119
|
-
instructions: 'Write a 1–2 sentence excerpt for blog listings and cards. Return only the excerpt text.',
|
|
120
|
-
targetLength: 45,
|
|
121
|
-
});
|
|
122
|
-
if (!outcome.ok) {
|
|
123
|
-
toast.error(outcome.error);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
setExcerpt(outcome.result.text.trim());
|
|
127
|
-
toast.success('Excerpt generated — review before saving.');
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
toast.error('AI request failed.');
|
|
131
|
-
}
|
|
132
|
-
finally {
|
|
133
|
-
setAiBusy(false);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
81
|
function handleSave() {
|
|
137
82
|
if (slugError)
|
|
138
83
|
return;
|
|
139
84
|
onSave({ title, slug, excerpt, featuredImage, category, tags });
|
|
140
85
|
onClose();
|
|
141
86
|
}
|
|
142
|
-
return (_jsxs(_Fragment, { children: [_jsx(Modal, { open: open, onClose: onClose, onDismiss: requestDismiss, title: "Post details", size: "lg", initialFocusRef: titleRef, actions: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: requestDismiss, children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: handleSave, disabled: !canEdit || Boolean(slugError), children: "Done" })] }), children:
|
|
143
|
-
setSlugTouched(true);
|
|
144
|
-
setSlug(slugify(e.target.value));
|
|
145
|
-
} }), slugError && (_jsx("p", { id: "pf-slug-error", className: "text-destructive mt-1 text-xs", children: slugError }))] }), _jsxs("div", { children: [_jsx("label", { className: LABEL, htmlFor: "pf-category", children: "Category" }), categoryOptions ? (_jsxs("select", { id: "pf-category", className: INPUT, value: category, disabled: !canEdit, onChange: (e) => setCategory(e.target.value), children: [_jsx("option", { value: "", children: "No category" }), categoryOptions.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))), category && !categoryOptions.some((o) => o.value === category) && (_jsx("option", { value: category, children: category }))] })) : (_jsx("input", { id: "pf-category", className: INPUT, value: category, disabled: !canEdit, placeholder: "e.g. Product", onChange: (e) => setCategory(e.target.value) }))] })] }), hasTags && (_jsxs("div", { children: [_jsx("label", { className: LABEL, htmlFor: "pf-tags", children: "Tags" }), _jsx(TagInput, { id: "pf-tags", value: tags, onChange: setTags, collection: post.postType, field: "tags", disabled: !canEdit, maxTags: tagsField?.maxRows })] })), _jsxs("div", { children: [_jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [_jsx("label", { className: `${LABEL} mb-0`, htmlFor: "pf-excerpt", children: "Excerpt" }), _jsx(AiFieldButton, { label: "Generate excerpt from content", busy: aiBusy, disabled: !canEdit, onClick: () => void handleAiGenerateExcerpt() })] }), _jsx("textarea", { id: "pf-excerpt", className: `${INPUT} min-h-[72px] resize-y`, value: excerpt, disabled: !canEdit, placeholder: "A short summary shown in listings and the header.", onChange: (e) => setExcerpt(e.target.value) })] }), _jsxs("div", { children: [_jsx("label", { className: LABEL, htmlFor: "pf-image", children: "Featured image" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { id: "pf-image", className: `${INPUT} min-w-0 flex-1`, value: featuredImage, disabled: !canEdit, placeholder: "https://\u2026 or pick from the media library", onChange: (e) => setFeaturedImage(e.target.value) }), _jsx("button", { type: "button", disabled: !canEdit, onClick: () => setMediaOpen(true), className: "bg-accent text-foreground border-border hover:bg-accent/80 shrink-0 rounded-md border px-3 py-1.5 text-xs font-medium disabled:opacity-50", children: "Browse" })] }), featuredImage && (_jsx("div", { className: "border-border bg-muted mt-2 overflow-hidden rounded-md border", children: _jsx("img", { src: featuredImage, alt: "", className: "h-28 w-full object-cover" }) }))] })] }) }), _jsx(MediaPickerModal, { open: mediaOpen, onClose: () => setMediaOpen(false), onSelect: (url) => {
|
|
146
|
-
setFeaturedImage(url);
|
|
147
|
-
setMediaOpen(false);
|
|
148
|
-
}, accept: "image/*" }), _jsx(ConfirmDialog, { open: confirmDiscard, onClose: () => setConfirmDiscard(false), onConfirm: onClose, title: "Discard unsaved changes?", description: "You have unsaved post details. Closing now will discard them.", confirmLabel: "Discard", cancelLabel: "Keep editing", destructive: true })] }));
|
|
87
|
+
return (_jsxs(_Fragment, { children: [_jsx(Modal, { open: open, onClose: onClose, onDismiss: requestDismiss, title: "Post details", size: "lg", initialFocusRef: titleRef, actions: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: requestDismiss, children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: handleSave, disabled: !canEdit || Boolean(slugError), children: "Done" })] }), children: _jsx(PostFieldsForm, { values: values, canEdit: canEdit, fields: fields, postType: post.postType, bodyHtml: post.body, slugTouched: slugTouched, onSlugTouched: () => setSlugTouched(true), onChange: handleChange, titleRef: titleRef }) }), _jsx(ConfirmDialog, { open: confirmDiscard, onClose: () => setConfirmDiscard(false), onConfirm: onClose, title: "Discard unsaved changes?", description: "You have unsaved post details. Closing now will discard them.", confirmLabel: "Discard", cancelLabel: "Keep editing", destructive: true })] }));
|
|
149
88
|
}
|
|
150
89
|
//# sourceMappingURL=PostFieldsModal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostFieldsModal.js","sourceRoot":"","sources":["../../../src/views/post-editor/PostFieldsModal.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC5D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"PostFieldsModal.js","sourceRoot":"","sources":["../../../src/views/post-editor/PostFieldsModal.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostSectionEditor.d.ts","sourceRoot":"","sources":["../../../src/views/post-editor/PostSectionEditor.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PostSectionEditor.d.ts","sourceRoot":"","sources":["../../../src/views/post-editor/PostSectionEditor.tsx"],"names":[],"mappings":"AA0DA,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAc5E,8DAA8D;AAC9D,UAAU,YAAY;IACpB,WAAW,CAAC,EAAE,MAAM,CAClB,MAAM,EACJ;QACE,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;QAC9B,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC,CAAA;KACrD,GACD,SAAS,CACZ,CAAA;IACD,GAAG,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3B;AAED,wDAAwD;AACxD,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACxD;AAED,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC;AAgBD,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,UAAU,EACV,MAAM,EACN,OAAO,EACP,UAAU,GACX,EAAE,sBAAsB,2CAizBxB"}
|
|
@@ -10,9 +10,13 @@ import { EditorRail, RailHeader } from '../../components/EditorRail.js';
|
|
|
10
10
|
import { EditorRailTabs } from '../../components/EditorRailTabs.js';
|
|
11
11
|
import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js';
|
|
12
12
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
13
|
+
import { VersionHistoryPanel } from '../page-editor/VersionHistoryPanel.js';
|
|
14
|
+
import { ReviewBar } from '../page-editor/ReviewBar.js';
|
|
15
|
+
import { fetchPageVersions, restorePageVersion, } from '../../lib/version-history.js';
|
|
16
|
+
import { changedSectionIds, diffVersions } from '../../lib/version-diff.js';
|
|
17
|
+
import { scoreBand } from '@prenta/core/seo/page-audit';
|
|
18
|
+
import { usePageAudit } from '../../hooks/usePageAudit.js';
|
|
19
|
+
import { emptyPostFromTemplate, fetchPostForEditor, fetchTemplateForEditor, missingPostFields, publishPost, savePostDraft, toggleSectionVisibility, updateSectionContent, updateSectionMeta, validatePost, } from '../../lib/post-editor-service.js';
|
|
16
20
|
import { SharePreviewLinkDialog } from '../../components/SharePreviewLinkDialog.js';
|
|
17
21
|
import { defaultPostHeader } from '@prenta/core/page-sections';
|
|
18
22
|
import { openPublicSiteTab } from '../../lib/open-public-site-tab.js';
|
|
@@ -23,12 +27,24 @@ import { EditorTopBar } from '../page-editor/EditorTopBar.js';
|
|
|
23
27
|
import { SectionsList } from '../page-editor/SectionsPanel.js';
|
|
24
28
|
import { SectionEditForm } from '../page-editor/SectionEditForm.js';
|
|
25
29
|
import { ValidationSummary } from '../page-editor/ValidationSummary.js';
|
|
26
|
-
import { buildEditorContentHtml
|
|
30
|
+
import { buildEditorContentHtml } from '../../lib/editor-seo-content.js';
|
|
27
31
|
import { snapshotBaseline } from '../../lib/reflow-baseline.js';
|
|
28
32
|
import { ReflowContext } from '../page-editor/reflow-context.js';
|
|
29
|
-
import {
|
|
33
|
+
import { inspectorIdFromFieldPath } from '../../lib/preview-bridge.js';
|
|
30
34
|
import { PostEditorCanvas } from './PostEditorCanvas.js';
|
|
31
35
|
import { PostFieldsModal } from './PostFieldsModal.js';
|
|
36
|
+
import { PostTab } from './PostTab.js';
|
|
37
|
+
/**
|
|
38
|
+
* The lower of the two audit scores — the tab dot should reflect the half that
|
|
39
|
+
* needs work, not an average that hides it. Same three-valued contract as the
|
|
40
|
+
* page editor: undefined = not reported yet, null = gated off, number = score.
|
|
41
|
+
*/
|
|
42
|
+
function weakestAuditScore(audit) {
|
|
43
|
+
if (!audit)
|
|
44
|
+
return undefined;
|
|
45
|
+
const scores = [audit.seo.score, audit.geo.score].filter((s) => s !== null);
|
|
46
|
+
return scores.length ? Math.min(...scores) : null;
|
|
47
|
+
}
|
|
32
48
|
function deriveCan(session) {
|
|
33
49
|
const role = session?.user?.role;
|
|
34
50
|
// Default closed: when we can't identify a role, hide the edit/publish
|
|
@@ -48,10 +64,6 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
48
64
|
const [selectedId, setSelectedId] = useState(null);
|
|
49
65
|
/** Canvas field click — focused after the inspector mounts for `selectedId`. */
|
|
50
66
|
const [pendingFocusPath, setPendingFocusPath] = useState(null);
|
|
51
|
-
/** Bump to skip the next `prenta-preview:sections` echo after a canvas text edit. */
|
|
52
|
-
const [suppressSectionsEcho, setSuppressSectionsEcho] = useState(0);
|
|
53
|
-
/** Canvas image Replace — opens MediaPickerModal for this field path. */
|
|
54
|
-
const [canvasMediaTarget, setCanvasMediaTarget] = useState(null);
|
|
55
67
|
const [viewport, setViewport] = useState(DEFAULT_VIEWPORT);
|
|
56
68
|
const [zoom, setZoom] = useState('fit');
|
|
57
69
|
/** Scale the canvas measured — reported upward so the zoom chip shows a real %. */
|
|
@@ -72,6 +84,18 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
72
84
|
const [railTab, setRailTab] = useState('sections');
|
|
73
85
|
const seoOpen = railTab === 'seo';
|
|
74
86
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
87
|
+
const [versions, setVersions] = useState([]);
|
|
88
|
+
const [versionsLoading, setVersionsLoading] = useState(false);
|
|
89
|
+
const [versionsError, setVersionsError] = useState(null);
|
|
90
|
+
/** Index into `versions`; 0 is the newest, which is the current content. */
|
|
91
|
+
const [versionIndex, setVersionIndex] = useState(0);
|
|
92
|
+
const [restoring, setRestoring] = useState(false);
|
|
93
|
+
/**
|
|
94
|
+
* Live audit reported by the SEO pane, built from unsaved form state. Null
|
|
95
|
+
* until that pane has mounted — `audit` below falls back to the saved-record
|
|
96
|
+
* audit so the header chip and tab dot always have a number.
|
|
97
|
+
*/
|
|
98
|
+
const [liveAudit, setLiveAudit] = useState(null);
|
|
75
99
|
const [confirmPublish, setConfirmPublish] = useState(false);
|
|
76
100
|
const [scheduleOpen, setScheduleOpen] = useState(false);
|
|
77
101
|
const [shareOpen, setShareOpen] = useState(false);
|
|
@@ -92,7 +116,7 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
92
116
|
}, []);
|
|
93
117
|
const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session]);
|
|
94
118
|
const customRenderers = useAdminSectionRenderers();
|
|
95
|
-
const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl);
|
|
119
|
+
const { siteUrl: publicSiteUrl, matchesAdminOrigin } = usePublicSiteUrl(config?.seo?.siteUrl);
|
|
96
120
|
const typeLabel = useMemo(() => {
|
|
97
121
|
const col = config?.collections?.[postType];
|
|
98
122
|
return col?.labels?.singular ?? postType;
|
|
@@ -113,6 +137,19 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
113
137
|
}, [config, postType]);
|
|
114
138
|
const publishCheck = useMemo(() => (post ? validatePost(post, true) : { errors: [''], warnings: [] }), [post]);
|
|
115
139
|
const publishReady = Boolean(post) && publishCheck.errors.length === 0;
|
|
140
|
+
const { audit: savedAudit } = usePageAudit({
|
|
141
|
+
entityType: 'post',
|
|
142
|
+
entityId: post?.id ?? null,
|
|
143
|
+
sections: post?.sections ?? [],
|
|
144
|
+
slug: post?.slug ?? '',
|
|
145
|
+
fallback: { title: post?.seoTitle, description: post?.seoDescription },
|
|
146
|
+
post: {
|
|
147
|
+
title: post?.title,
|
|
148
|
+
bodyHtml: post?.body,
|
|
149
|
+
featuredImage: post?.featuredImage,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
const audit = liveAudit ?? savedAudit;
|
|
116
153
|
const railTabs = useMemo(() => [
|
|
117
154
|
{
|
|
118
155
|
id: 'sections',
|
|
@@ -120,12 +157,18 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
120
157
|
title: 'Post content',
|
|
121
158
|
count: post?.sections.length ?? 0,
|
|
122
159
|
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
{
|
|
161
|
+
id: 'seo',
|
|
162
|
+
label: 'SEO & GEO',
|
|
163
|
+
title: 'Search & AI readiness',
|
|
164
|
+
score: weakestAuditScore(audit),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 'page',
|
|
168
|
+
label: 'Post',
|
|
169
|
+
title: 'Post settings',
|
|
170
|
+
},
|
|
171
|
+
], [post?.sections.length, audit]);
|
|
129
172
|
const activeRailTab = railTabs.find((tab) => tab.id === railTab) ?? railTabs[0];
|
|
130
173
|
// Content the SEO pane analyses — sections plus the post body.
|
|
131
174
|
const seoContentHtml = useMemo(() => buildEditorContentHtml(post?.sections ?? [], post?.body ?? ''), [post?.sections, post?.body]);
|
|
@@ -140,18 +183,6 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
140
183
|
device: viewport,
|
|
141
184
|
onProof: setViewport,
|
|
142
185
|
}), [canvasEl, selectedId, baseline, viewport]);
|
|
143
|
-
const liveSeoScore = useMemo(() => {
|
|
144
|
-
if (!post)
|
|
145
|
-
return 0;
|
|
146
|
-
return computeLiveSeoScore({
|
|
147
|
-
title: post.title,
|
|
148
|
-
slug: post.slug,
|
|
149
|
-
seoTitle: post.seoTitle,
|
|
150
|
-
seoDescription: post.seoDescription,
|
|
151
|
-
sections: post.sections,
|
|
152
|
-
bodyHtml: post.body,
|
|
153
|
-
});
|
|
154
|
-
}, [post]);
|
|
155
186
|
const publishDisabledReason = publishCheck.errors[0];
|
|
156
187
|
// ─── Load ──────────────────────────────────────────────────────────
|
|
157
188
|
useEffect(() => {
|
|
@@ -337,24 +368,66 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
337
368
|
setPublishing(false);
|
|
338
369
|
}
|
|
339
370
|
}, [post, documentId, postType, onNavigate]);
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
371
|
+
// ─── Version history ─────────────────────────────────────────────────
|
|
372
|
+
// History is a MODE, not a dialog: it overlays the rail and drives the
|
|
373
|
+
// canvas, so the version being reviewed is editor state rather than
|
|
374
|
+
// something owned by a panel that can be unmounted.
|
|
375
|
+
const loadVersions = useCallback(async (postId) => {
|
|
376
|
+
setVersionsLoading(true);
|
|
377
|
+
setVersionsError(null);
|
|
378
|
+
try {
|
|
379
|
+
setVersions(await fetchPageVersions(postType, postId));
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
setVersions([]);
|
|
383
|
+
setVersionsError(err instanceof Error ? err.message : 'Failed to load version history');
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
setVersionsLoading(false);
|
|
387
|
+
}
|
|
388
|
+
}, [postType]);
|
|
389
|
+
const openHistory = useCallback(() => {
|
|
343
390
|
if (!post?.id)
|
|
344
391
|
return;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
392
|
+
setHistoryOpen(true);
|
|
393
|
+
setVersionIndex(0);
|
|
394
|
+
void loadVersions(post.id);
|
|
395
|
+
}, [post?.id, loadVersions]);
|
|
396
|
+
const closeHistory = useCallback(() => {
|
|
397
|
+
setHistoryOpen(false);
|
|
398
|
+
setVersionIndex(0);
|
|
399
|
+
}, []);
|
|
400
|
+
const reviewing = historyOpen && versionIndex > 0;
|
|
401
|
+
const reviewVersion = reviewing ? (versions[versionIndex] ?? null) : null;
|
|
402
|
+
const reviewChanges = useMemo(() => reviewVersion
|
|
403
|
+
? diffVersions(reviewVersion.sections, versions[versionIndex + 1]?.sections)
|
|
404
|
+
: [], [reviewVersion, versions, versionIndex]);
|
|
405
|
+
const reviewChangedIds = useMemo(() => changedSectionIds(reviewChanges), [reviewChanges]);
|
|
406
|
+
const handleRestoreVersion = useCallback(async (version) => {
|
|
407
|
+
if (!post?.id)
|
|
408
|
+
return;
|
|
409
|
+
setRestoring(true);
|
|
410
|
+
try {
|
|
411
|
+
await restorePageVersion(postType, post.id, version.id);
|
|
412
|
+
const restored = await fetchPostForEditor(postType, post.id);
|
|
413
|
+
acceptPost(restored);
|
|
348
414
|
setSelectedId(null);
|
|
349
|
-
setRailTab('sections');
|
|
350
415
|
setDirty(false);
|
|
351
416
|
setSaveState('idle');
|
|
352
417
|
setValidation(null);
|
|
353
|
-
|
|
354
|
-
.
|
|
355
|
-
toast.
|
|
356
|
-
}
|
|
357
|
-
|
|
418
|
+
closeHistory();
|
|
419
|
+
void loadVersions(post.id);
|
|
420
|
+
toast.success('Version restored');
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
toast.error(err instanceof Error ? err.message : 'Failed to restore version');
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
setRestoring(false);
|
|
427
|
+
}
|
|
428
|
+
}, [post?.id, postType, acceptPost, closeHistory, loadVersions]);
|
|
429
|
+
const effectiveCanEdit = canEdit && !reviewing;
|
|
430
|
+
const canRestoreVersion = canEdit;
|
|
358
431
|
const handleSeoSaved = useCallback(() => {
|
|
359
432
|
if (!post?.id)
|
|
360
433
|
return;
|
|
@@ -376,23 +449,10 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
376
449
|
setRailTab('sections');
|
|
377
450
|
setPendingFocusPath(fieldPath ?? null);
|
|
378
451
|
// Toggles, because selection is now disclosure — see the note on the page
|
|
379
|
-
// editor's copy. A `fieldPath` means
|
|
380
|
-
//
|
|
452
|
+
// editor's copy. A `fieldPath` means a canvas field click, which must
|
|
453
|
+
// open rather than toggle.
|
|
381
454
|
setSelectedId((current) => (fieldPath ? id : current === id ? null : id));
|
|
382
455
|
}, []);
|
|
383
|
-
const handleCanvasEdit = useCallback((edit) => {
|
|
384
|
-
if (!canEdit)
|
|
385
|
-
return;
|
|
386
|
-
setSuppressSectionsEcho((n) => n + 1);
|
|
387
|
-
mutateSections((s) => updateSectionContentAtPath(s, edit.sectionId, edit.fieldPath, edit.value));
|
|
388
|
-
setSelectedId(edit.sectionId);
|
|
389
|
-
}, [canEdit, mutateSections]);
|
|
390
|
-
const handleCanvasRequestMedia = useCallback((request) => {
|
|
391
|
-
if (!canEdit)
|
|
392
|
-
return;
|
|
393
|
-
setSelectedId(request.sectionId);
|
|
394
|
-
setCanvasMediaTarget(request);
|
|
395
|
-
}, [canEdit]);
|
|
396
456
|
// After a canvas field click, focus the matching inspector input once painted.
|
|
397
457
|
useEffect(() => {
|
|
398
458
|
if (!pendingFocusPath || !selectedId)
|
|
@@ -456,31 +516,25 @@ export function PostSectionEditor({ postType, documentId, config, session, onNav
|
|
|
456
516
|
const structuralTypes = Array.from(new Set(post.sections
|
|
457
517
|
.filter((s) => !hasCanvasRenderer(s.sectionType) && !customRenderers[s.sectionType])
|
|
458
518
|
.map((s) => s.sectionType)));
|
|
459
|
-
return (_jsxs(ErrorBoundary, { children: [_jsxs("div", { className: "bg-background flex h-full flex-col overflow-hidden", children: [_jsx(EditorTopBar, { title: post.title || `New ${typeLabel}`, status: post.status, dirty: dirty, saveState: saveState, publishing: publishing, canEdit:
|
|
519
|
+
return (_jsxs(ErrorBoundary, { children: [_jsxs("div", { className: "bg-background flex h-full flex-col overflow-hidden", children: [_jsx(EditorTopBar, { title: post.title || `New ${typeLabel}`, status: post.status, dirty: dirty, saveState: saveState, publishing: publishing, canEdit: effectiveCanEdit, canPublish: canPublish && effectiveCanEdit, publishReady: publishReady, publishDisabledReason: publishDisabledReason, viewport: viewport, backLabel: "Posts", onViewport: setViewport, zoom: zoom, canvasScale: canvasScale, onZoom: setZoom, onBack: () => guardedNavigate('/posts'), onViewOnSite: post.id && post.slug ? handleViewOnSite : undefined, onShare: post.id ? handleShare : undefined, hasPublicSiteUrl: Boolean(publicSiteUrl), matchesAdminOrigin: matchesAdminOrigin, historyOpen: historyOpen, onHistory: post.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined, onOpenSettings: () => setFieldsOpen(true), onOpenSeo: handleToggleSeo, seoOpen: seoOpen, seoScore: audit?.seo.score ?? undefined, seoScoreBand: typeof audit?.seo.score === 'number' ? scoreBand(audit.seo.score) : undefined, onSaveDraft: handleSaveDraft, onPublish: requestPublish, onSchedule: post.id ? requestSchedule : undefined, scheduleReady: Boolean(post.id), scheduledAt: post.scheduledAt, scheduledUnpublishAt: post.scheduledUnpublishAt, structuralTypes: structuralTypes }), unmanagedTypes.length > 0 && (_jsxs("div", { role: "status", className: "border-border bg-muted/50 flex items-start gap-3 border-b px-4 py-2.5", children: [_jsx(Lock, { className: "text-muted-foreground mt-0.5 shrink-0", size: 16, "aria-hidden": true }), _jsxs("p", { className: "text-muted-foreground flex-1 text-sm", children: ["This post has", ' ', _jsxs("span", { className: "text-foreground font-medium", children: [unmanagedTypes.length, " section type", unmanagedTypes.length > 1 ? 's' : ''] }), ' ', "managed outside the editor (", unmanagedTypes.join(', '), "). They are preserved on save but edited in your seed script or code, not here."] })] })), validation && (_jsx(ValidationSummary, { errors: validation.errors, warnings: validation.warnings, onDismiss: () => setValidation(null) })), _jsx(ReflowContext.Provider, { value: reflowContext, children: _jsxs("div", { className: "prenta-editor-body relative flex flex-1 overflow-hidden", children: [_jsx(EditorRail, { collapsedLabel: activeRailTab.label, children: ({ width, dragging, collapse }) => (_jsxs(_Fragment, { children: [_jsx(EditorRailTabs, { tabs: railTabs, active: railTab, onChange: setRailTab }), _jsx(RailHeader, { title: activeRailTab.title, width: width, dragging: dragging, onCollapse: collapse }), _jsx("div", { role: "tabpanel", id: `rail-panel-${railTab}`, "aria-labelledby": `rail-tab-${railTab}`, className: "flex min-h-0 flex-1 flex-col", children: railTab === 'sections' ? (_jsx(SectionsList, { sections: post.sections, selectedId: selectedId, canEdit: effectiveCanEdit, onSelect: handleSelectSection, onToggleVisibility: handleToggleVisibility, renderEditor: reviewing
|
|
520
|
+
? undefined
|
|
521
|
+
: (section) => (_jsx(SectionEditForm, { section: section, canEdit: effectiveCanEdit, onContentChange: handleContentChange, onMetaChange: handleMetaChange, postBody: post.body, onPostBodyChange: handlePostBodyChange })) })) : railTab === 'page' ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(PostTab, { post: post, canEdit: effectiveCanEdit, fields: config?.collections?.[postType]?.fields, onChange: handleFields }) })) : (_jsx(SeoEditorPane, { entityType: "post", entityId: post.id, slug: post.slug, auditSections: post.sections, onAuditChange: setLiveAudit, contentHtml: seoContentHtml, title: post.title, bodyHtml: post.body, featuredImage: post.featuredImage, draftMeta: {
|
|
460
522
|
seoTitle: post.seoTitle,
|
|
461
523
|
seoDescription: post.seoDescription,
|
|
462
|
-
}, onDraftChange: handleFields, onClose: () => setRailTab('sections'), onSaved: handleSeoSaved })) })
|
|
524
|
+
}, onDraftChange: handleFields, onClose: () => setRailTab('sections'), onSaved: handleSeoSaved })) }), historyOpen && (_jsx(VersionHistoryPanel, { versions: versions, loading: versionsLoading, error: versionsError, selectedIndex: versionIndex, onSelect: setVersionIndex, onClose: closeHistory, onRestore: handleRestoreVersion, restoring: restoring, canRestore: canRestoreVersion }))] })) }), _jsx("div", { className: "flex-1 overflow-hidden", children: _jsx(PostEditorCanvas, { sections: reviewVersion ? reviewVersion.sections : post.sections, header: header, context: reviewVersion
|
|
525
|
+
? {
|
|
526
|
+
...context,
|
|
527
|
+
title: reviewVersion.title ?? context.title,
|
|
528
|
+
excerpt: reviewVersion.excerpt ?? context.excerpt,
|
|
529
|
+
body: reviewVersion.body ?? context.body,
|
|
530
|
+
featuredImageUrl: reviewVersion.featuredImage ?? context.featuredImageUrl,
|
|
531
|
+
}
|
|
532
|
+
: context, selectedId: selectedId, viewport: viewport, zoom: zoom, onScaleChange: setCanvasScale, onCanvasElement: setCanvasEl, onSelect: handleSelectSection, structuralTypes: structuralTypes, reviewing: reviewing, changedSectionIds: reviewChangedIds, reviewBar: reviewVersion ? (_jsx(ReviewBar, { version: reviewVersion, changedCount: reviewChangedIds.size, onRestore: () => void handleRestoreVersion(reviewVersion), onBackToCurrent: () => setVersionIndex(0), restoring: restoring, canRestore: canRestoreVersion })) : null }) })] }) })] }), _jsx(PostFieldsModal, { open: fieldsOpen, post: post, canEdit: effectiveCanEdit, fields: config?.collections?.[postType]?.fields, onClose: () => setFieldsOpen(false), onSave: handleFields }), _jsx(ConfirmDialog, { open: confirmPublish, onClose: () => setConfirmPublish(false), onConfirm: doPublish, title: "Publish this post?", description: "The current content will go live and replace the published version.", confirmLabel: "Publish" }), post.id && (_jsx(SchedulePublishDialog, { collectionSlug: postType, documentId: post.id, scheduledAt: post.scheduledAt, scheduledUnpublishAt: post.scheduledUnpublishAt, open: scheduleOpen, onClose: () => setScheduleOpen(false), onScheduled: handleScheduled })), post.id && (_jsx(SharePreviewLinkDialog, { collectionSlug: postType, documentId: post.id, documentSlug: post.slug, documentPath: post.path, siteUrl: publicSiteUrl, urlPrefix: urlPrefix, open: shareOpen, onClose: () => setShareOpen(false) })), _jsx(ConfirmDialog, { open: leaveTarget !== null, onClose: () => setLeaveTarget(null), onConfirm: () => {
|
|
463
533
|
const target = leaveTarget;
|
|
464
534
|
setLeaveTarget(null);
|
|
465
535
|
setDirty(false);
|
|
466
536
|
if (target)
|
|
467
537
|
onNavigate(target);
|
|
468
|
-
}, title: "Discard unsaved changes?", description: "You have unsaved changes that will be lost if you leave now.", confirmLabel: "Leave", cancelLabel: "Stay", destructive: true })
|
|
469
|
-
if (!canvasMediaTarget)
|
|
470
|
-
return;
|
|
471
|
-
const { sectionId, fieldPath } = canvasMediaTarget;
|
|
472
|
-
const { widthPath, heightPath } = companionDimensionPaths(fieldPath);
|
|
473
|
-
mutateSections((s) => {
|
|
474
|
-
let next = updateSectionContentAtPath(s, sectionId, fieldPath, item.url);
|
|
475
|
-
if (typeof item.width === 'number') {
|
|
476
|
-
next = updateSectionContentAtPath(next, sectionId, widthPath, item.width);
|
|
477
|
-
}
|
|
478
|
-
if (typeof item.height === 'number') {
|
|
479
|
-
next = updateSectionContentAtPath(next, sectionId, heightPath, item.height);
|
|
480
|
-
}
|
|
481
|
-
return next;
|
|
482
|
-
});
|
|
483
|
-
setCanvasMediaTarget(null);
|
|
484
|
-
}, onSelect: () => setCanvasMediaTarget(null) })] }));
|
|
538
|
+
}, title: "Discard unsaved changes?", description: "You have unsaved changes that will be lost if you leave now.", confirmLabel: "Leave", cancelLabel: "Stay", destructive: true })] }));
|
|
485
539
|
}
|
|
486
540
|
//# sourceMappingURL=PostSectionEditor.js.map
|