@actuate-media/cms-admin 0.40.0 → 0.41.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/dist/__tests__/views/general-settings.render.test.js +23 -2
- package/dist/__tests__/views/general-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/page-section-editor.test.js +50 -0
- package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts +3 -1
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +8 -7
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +13 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +11 -3
- 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 +25 -1
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/StructureNotice.d.ts +10 -0
- package/dist/views/page-editor/StructureNotice.d.ts.map +1 -0
- package/dist/views/page-editor/StructureNotice.js +16 -0
- package/dist/views/page-editor/StructureNotice.js.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +2 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +22 -0
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/page-editor/sections/index.d.ts +1 -1
- package/dist/views/page-editor/sections/index.d.ts.map +1 -1
- package/dist/views/page-editor/sections/index.js +1 -1
- package/dist/views/page-editor/sections/index.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +3 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +3 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +25 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.d.ts.map +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.js +2 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.js.map +1 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +1 -1
- package/dist/views/settings/components.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/views/general-settings.render.test.tsx +33 -2
- package/src/__tests__/views/page-section-editor.test.tsx +70 -0
- package/src/views/page-editor/EditorCanvas.tsx +29 -22
- package/src/views/page-editor/EditorTopBar.tsx +58 -13
- package/src/views/page-editor/PageSectionEditor.tsx +42 -0
- package/src/views/page-editor/StructureNotice.tsx +27 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +24 -0
- package/src/views/page-editor/sections/index.ts +1 -1
- package/src/views/post-editor/PostEditorCanvas.tsx +5 -0
- package/src/views/post-editor/PostSectionEditor.tsx +42 -1
- package/src/views/settings/SeoRobotsDefaultsCard.tsx +9 -0
- package/src/views/settings/components.tsx +5 -0
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
|
|
|
4
4
|
import { LayoutTemplate, Plus } from 'lucide-react'
|
|
5
5
|
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
6
6
|
import { SectionRenderer } from './sections/index.js'
|
|
7
|
+
import { StructureNotice } from './StructureNotice.js'
|
|
7
8
|
import { getViewport, type ViewportId } from './viewports.js'
|
|
8
9
|
|
|
9
10
|
interface EditorCanvasProps {
|
|
@@ -12,6 +13,8 @@ interface EditorCanvasProps {
|
|
|
12
13
|
viewport: ViewportId
|
|
13
14
|
onSelect: (id: string) => void
|
|
14
15
|
onAddSection: () => void
|
|
16
|
+
/** Section types rendered as placeholders — shows the "Structure view" notice. */
|
|
17
|
+
structuralTypes?: string[]
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -26,6 +29,7 @@ export function EditorCanvas({
|
|
|
26
29
|
viewport,
|
|
27
30
|
onSelect,
|
|
28
31
|
onAddSection,
|
|
32
|
+
structuralTypes = [],
|
|
29
33
|
}: EditorCanvasProps) {
|
|
30
34
|
const vp = getViewport(viewport)
|
|
31
35
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -73,31 +77,34 @@ export function EditorCanvas({
|
|
|
73
77
|
{sections.length === 0 ? (
|
|
74
78
|
<EmptyCanvas onAddSection={onAddSection} />
|
|
75
79
|
) : (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
|
|
79
|
-
>
|
|
80
|
+
<>
|
|
81
|
+
<StructureNotice structuralTypes={structuralTypes} />
|
|
80
82
|
<div
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
style={{
|
|
84
|
-
width: vp.width,
|
|
85
|
-
transform: `scale(${scale})`,
|
|
86
|
-
transformOrigin: 'top left',
|
|
87
|
-
}}
|
|
83
|
+
className="mx-auto"
|
|
84
|
+
style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
|
|
88
85
|
>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
<div
|
|
87
|
+
ref={surfaceRef}
|
|
88
|
+
className="border-border origin-top overflow-hidden rounded-xl border bg-white shadow-sm"
|
|
89
|
+
style={{
|
|
90
|
+
width: vp.width,
|
|
91
|
+
transform: `scale(${scale})`,
|
|
92
|
+
transformOrigin: 'top left',
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{sections.map((section) => (
|
|
96
|
+
<SectionRenderer
|
|
97
|
+
key={section.id}
|
|
98
|
+
section={section}
|
|
99
|
+
editable
|
|
100
|
+
selected={section.id === selectedId}
|
|
101
|
+
onSelect={onSelect}
|
|
102
|
+
registerRef={registerRef}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
99
106
|
</div>
|
|
100
|
-
|
|
107
|
+
</>
|
|
101
108
|
)}
|
|
102
109
|
</div>
|
|
103
110
|
)
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Check,
|
|
5
5
|
ExternalLink,
|
|
6
6
|
Eye,
|
|
7
|
+
History,
|
|
7
8
|
Laptop,
|
|
8
9
|
Loader2,
|
|
9
10
|
type LucideIcon,
|
|
@@ -38,9 +39,21 @@ interface EditorTopBarProps {
|
|
|
38
39
|
* or installs without a public URL.
|
|
39
40
|
*/
|
|
40
41
|
onViewOnSite?: () => void
|
|
42
|
+
/**
|
|
43
|
+
* Open the version history panel. When omitted the History button is
|
|
44
|
+
* hidden — e.g. for unsaved documents that have no versions yet.
|
|
45
|
+
*/
|
|
46
|
+
onHistory?: () => void
|
|
41
47
|
onOpenSettings: () => void
|
|
42
48
|
onSaveDraft: () => void
|
|
43
49
|
onPublish: () => void
|
|
50
|
+
/**
|
|
51
|
+
* Section types the canvas renders as placeholders (custom / unmanaged).
|
|
52
|
+
* When non-empty the editor is in "structure" mode: the eye preview is
|
|
53
|
+
* labeled as a structure preview and "View on site" is promoted to a
|
|
54
|
+
* labeled button as the primary design preview.
|
|
55
|
+
*/
|
|
56
|
+
structuralTypes?: string[]
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
const VIEWPORT_ICONS: Record<string, LucideIcon> = {
|
|
@@ -71,11 +84,20 @@ export function EditorTopBar({
|
|
|
71
84
|
onBack,
|
|
72
85
|
onPreview,
|
|
73
86
|
onViewOnSite,
|
|
87
|
+
onHistory,
|
|
74
88
|
onOpenSettings,
|
|
75
89
|
onSaveDraft,
|
|
76
90
|
onPublish,
|
|
91
|
+
structuralTypes = [],
|
|
77
92
|
}: EditorTopBarProps) {
|
|
78
93
|
const statusInfo = STATUS_TEXT[status]
|
|
94
|
+
// With custom section types on the page the canvas (and the eye preview,
|
|
95
|
+
// which reuses the admin renderers) only shows structure — the real design
|
|
96
|
+
// preview is the public site, so promote "View on site" to a labeled button.
|
|
97
|
+
const structural = structuralTypes.length > 0
|
|
98
|
+
const previewLabel = structural
|
|
99
|
+
? 'Preview structure (custom sections show placeholders)'
|
|
100
|
+
: 'Preview page'
|
|
79
101
|
|
|
80
102
|
return (
|
|
81
103
|
<header className="bg-card border-border flex h-14 shrink-0 items-center justify-between gap-4 border-b px-4">
|
|
@@ -143,27 +165,50 @@ export function EditorTopBar({
|
|
|
143
165
|
<Settings2 className="h-4 w-4" aria-hidden />
|
|
144
166
|
</button>
|
|
145
167
|
|
|
168
|
+
{onHistory && (
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={onHistory}
|
|
172
|
+
aria-label="Version history"
|
|
173
|
+
title="Version history"
|
|
174
|
+
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
|
|
175
|
+
>
|
|
176
|
+
<History className="h-4 w-4" aria-hidden />
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
|
|
146
180
|
<button
|
|
147
181
|
type="button"
|
|
148
182
|
onClick={onPreview}
|
|
149
|
-
aria-label=
|
|
150
|
-
title=
|
|
183
|
+
aria-label={previewLabel}
|
|
184
|
+
title={previewLabel}
|
|
151
185
|
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
|
|
152
186
|
>
|
|
153
187
|
<Eye className="h-4 w-4" aria-hidden />
|
|
154
188
|
</button>
|
|
155
189
|
|
|
156
|
-
{onViewOnSite &&
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
{onViewOnSite &&
|
|
191
|
+
(structural ? (
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={onViewOnSite}
|
|
195
|
+
title="Open the draft on your public site — the real design preview"
|
|
196
|
+
className="border-border text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors"
|
|
197
|
+
>
|
|
198
|
+
<ExternalLink className="h-3.5 w-3.5" aria-hidden />
|
|
199
|
+
View on site
|
|
200
|
+
</button>
|
|
201
|
+
) : (
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={onViewOnSite}
|
|
205
|
+
aria-label="View draft on site"
|
|
206
|
+
title="View draft on site"
|
|
207
|
+
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
|
|
208
|
+
>
|
|
209
|
+
<ExternalLink className="h-4 w-4" aria-hidden />
|
|
210
|
+
</button>
|
|
211
|
+
))}
|
|
167
212
|
|
|
168
213
|
<button
|
|
169
214
|
type="button"
|
|
@@ -5,6 +5,7 @@ import { AlertTriangle, Blocks, Loader2, Lock } from 'lucide-react'
|
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
7
7
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
8
|
+
import { VersionHistory } from '../../components/VersionHistory.js'
|
|
8
9
|
import {
|
|
9
10
|
addSection,
|
|
10
11
|
createSection,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
} from '../../lib/preview-link.js'
|
|
33
34
|
import type { SectionSettings, SectionTypeId } from './section-types.js'
|
|
34
35
|
import { DEFAULT_VIEWPORT, type ViewportId } from './viewports.js'
|
|
36
|
+
import { hasCanvasRenderer } from './sections/index.js'
|
|
35
37
|
import { EditorTopBar, type SaveState } from './EditorTopBar.js'
|
|
36
38
|
import { SectionsPanel } from './SectionsPanel.js'
|
|
37
39
|
import { EditorCanvas } from './EditorCanvas.js'
|
|
@@ -82,6 +84,7 @@ export function PageSectionEditor({
|
|
|
82
84
|
|
|
83
85
|
const [addOpen, setAddOpen] = useState(false)
|
|
84
86
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
87
|
+
const [historyOpen, setHistoryOpen] = useState(false)
|
|
85
88
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
|
|
86
89
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
87
90
|
const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
|
|
@@ -285,6 +288,23 @@ export function PageSectionEditor({
|
|
|
285
288
|
}
|
|
286
289
|
}, [page, documentId, onNavigate])
|
|
287
290
|
|
|
291
|
+
// After a version restore the server has already written the restored
|
|
292
|
+
// data — refetch so the editor reflects it instead of the stale state.
|
|
293
|
+
const handleRestored = useCallback(() => {
|
|
294
|
+
if (!page?.id) return
|
|
295
|
+
fetchPageForEditor(page.id)
|
|
296
|
+
.then((p) => {
|
|
297
|
+
setPage(p)
|
|
298
|
+
setSelectedId(null)
|
|
299
|
+
setDirty(false)
|
|
300
|
+
setSaveState('idle')
|
|
301
|
+
setValidation(null)
|
|
302
|
+
})
|
|
303
|
+
.catch(() => {
|
|
304
|
+
toast.error('Version restored — refresh the page to see it.')
|
|
305
|
+
})
|
|
306
|
+
}, [page?.id])
|
|
307
|
+
|
|
288
308
|
// ─── Navigation with unsaved guard ───────────────────────────────────
|
|
289
309
|
const guardedNavigate = useCallback(
|
|
290
310
|
(path: string) => {
|
|
@@ -398,6 +418,15 @@ export function PageSectionEditor({
|
|
|
398
418
|
new Set(page.sections.filter((s) => s.unmanaged).map((s) => s.sectionType)),
|
|
399
419
|
)
|
|
400
420
|
|
|
421
|
+
// Section types the canvas renders as placeholders (custom + unmanaged).
|
|
422
|
+
// When present the editor surfaces "structure view" messaging and promotes
|
|
423
|
+
// "View on site" as the primary design preview.
|
|
424
|
+
const structuralTypes = Array.from(
|
|
425
|
+
new Set(
|
|
426
|
+
page.sections.filter((s) => !hasCanvasRenderer(s.sectionType)).map((s) => s.sectionType),
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
|
|
401
430
|
return (
|
|
402
431
|
<ErrorBoundary>
|
|
403
432
|
<div className="bg-background flex h-full flex-col overflow-hidden">
|
|
@@ -414,9 +443,11 @@ export function PageSectionEditor({
|
|
|
414
443
|
onBack={() => guardedNavigate('/pages')}
|
|
415
444
|
onPreview={handlePreview}
|
|
416
445
|
onViewOnSite={page.id ? handleViewOnSite : undefined}
|
|
446
|
+
onHistory={page.id ? () => setHistoryOpen(true) : undefined}
|
|
417
447
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
418
448
|
onSaveDraft={handleSaveDraft}
|
|
419
449
|
onPublish={requestPublish}
|
|
450
|
+
structuralTypes={structuralTypes}
|
|
420
451
|
/>
|
|
421
452
|
|
|
422
453
|
{readOnly && (
|
|
@@ -487,6 +518,7 @@ export function PageSectionEditor({
|
|
|
487
518
|
viewport={viewport}
|
|
488
519
|
onSelect={setSelectedId}
|
|
489
520
|
onAddSection={() => setAddOpen(true)}
|
|
521
|
+
structuralTypes={structuralTypes}
|
|
490
522
|
/>
|
|
491
523
|
</div>
|
|
492
524
|
|
|
@@ -506,6 +538,16 @@ export function PageSectionEditor({
|
|
|
506
538
|
|
|
507
539
|
<AddSectionModal open={addOpen} onClose={() => setAddOpen(false)} onAdd={handleAdd} />
|
|
508
540
|
|
|
541
|
+
{page.id && (
|
|
542
|
+
<VersionHistory
|
|
543
|
+
collectionSlug="pages"
|
|
544
|
+
documentId={page.id}
|
|
545
|
+
open={historyOpen}
|
|
546
|
+
onClose={() => setHistoryOpen(false)}
|
|
547
|
+
onRestore={handleRestored}
|
|
548
|
+
/>
|
|
549
|
+
)}
|
|
550
|
+
|
|
509
551
|
<PageSettingsModal
|
|
510
552
|
open={settingsOpen}
|
|
511
553
|
page={page}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Puzzle } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Banner shown above the canvas surface when the document contains section
|
|
7
|
+
* types the canvas can only render as placeholders (custom / unmanaged
|
|
8
|
+
* types). Tells authors the canvas is a structural view — the real design
|
|
9
|
+
* lives on the public site behind "View on site".
|
|
10
|
+
*/
|
|
11
|
+
export function StructureNotice({ structuralTypes }: { structuralTypes: string[] }) {
|
|
12
|
+
if (structuralTypes.length === 0) return null
|
|
13
|
+
const count = structuralTypes.length
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
role="note"
|
|
17
|
+
className="border-border bg-card text-muted-foreground mx-auto mb-4 flex w-fit max-w-full items-center gap-2 rounded-lg border px-3 py-1.5 text-xs"
|
|
18
|
+
>
|
|
19
|
+
<Puzzle className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
20
|
+
<span>
|
|
21
|
+
<span className="text-foreground font-medium">Structure view</span> — {count} custom section{' '}
|
|
22
|
+
type{count > 1 ? 's' : ''} shown as placeholders. Use{' '}
|
|
23
|
+
<span className="text-foreground font-medium">View on site</span> for the real design.
|
|
24
|
+
</span>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -15,6 +15,30 @@ import { AuthorBioSection } from './AuthorBioSection.js'
|
|
|
15
15
|
import { RelatedPostsSection } from './RelatedPostsSection.js'
|
|
16
16
|
import { GallerySection } from './GallerySection.js'
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Section types with a real canvas body component below. Anything else —
|
|
20
|
+
* config-declared custom types (`opal-*`), unmanaged sections, and the
|
|
21
|
+
* `form` embed — renders as a structural placeholder, so the editors use
|
|
22
|
+
* this set to tell authors the canvas shows structure, not design.
|
|
23
|
+
*/
|
|
24
|
+
const CANVAS_RENDERED_TYPES: ReadonlySet<string> = new Set([
|
|
25
|
+
'hero',
|
|
26
|
+
'text',
|
|
27
|
+
'feature',
|
|
28
|
+
'stats',
|
|
29
|
+
'cta',
|
|
30
|
+
'article-body',
|
|
31
|
+
'quote',
|
|
32
|
+
'author-bio',
|
|
33
|
+
'related-posts',
|
|
34
|
+
'gallery',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
/** True when the canvas renders this type for real (not as a placeholder). */
|
|
38
|
+
export function hasCanvasRenderer(sectionType: string): boolean {
|
|
39
|
+
return CANVAS_RENDERED_TYPES.has(sectionType)
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
/** Dispatch a section's body component by type. Shared by editor + preview. */
|
|
19
43
|
function SectionBody({ section, context }: { section: PageSection; context?: PostRenderContext }) {
|
|
20
44
|
switch (section.sectionType) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SectionRenderer } from './SectionRenderer.js'
|
|
1
|
+
export { SectionRenderer, hasCanvasRenderer } from './SectionRenderer.js'
|
|
2
2
|
export type { SectionRendererProps } from './SectionRenderer.js'
|
|
3
3
|
export type { PostRenderContext } from './parts.js'
|
|
4
4
|
export { HeroBannerSection } from './HeroBannerSection.js'
|
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
|
|
|
4
4
|
import { FileText, Plus } from 'lucide-react'
|
|
5
5
|
import type { PageSection, PostHeaderConfig } from '../../lib/post-editor-service.js'
|
|
6
6
|
import { SectionRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
|
|
7
|
+
import { StructureNotice } from '../page-editor/StructureNotice.js'
|
|
7
8
|
import { getViewport, type ViewportId } from '../page-editor/viewports.js'
|
|
8
9
|
import { PostHeader } from './PostHeader.js'
|
|
9
10
|
|
|
@@ -15,6 +16,8 @@ interface PostEditorCanvasProps {
|
|
|
15
16
|
viewport: ViewportId
|
|
16
17
|
onSelect: (id: string) => void
|
|
17
18
|
onAddSection: () => void
|
|
19
|
+
/** Section types rendered as placeholders — shows the "Structure view" notice. */
|
|
20
|
+
structuralTypes?: string[]
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -31,6 +34,7 @@ export function PostEditorCanvas({
|
|
|
31
34
|
viewport,
|
|
32
35
|
onSelect,
|
|
33
36
|
onAddSection,
|
|
37
|
+
structuralTypes = [],
|
|
34
38
|
}: PostEditorCanvasProps) {
|
|
35
39
|
const vp = getViewport(viewport)
|
|
36
40
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -70,6 +74,7 @@ export function PostEditorCanvas({
|
|
|
70
74
|
|
|
71
75
|
return (
|
|
72
76
|
<div ref={containerRef} className="bg-muted h-full overflow-auto p-6">
|
|
77
|
+
<StructureNotice structuralTypes={structuralTypes} />
|
|
73
78
|
<div
|
|
74
79
|
className="mx-auto"
|
|
75
80
|
style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
|
|
@@ -5,6 +5,7 @@ import { AlertTriangle, Loader2 } from 'lucide-react'
|
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
7
7
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
8
|
+
import { VersionHistory } from '../../components/VersionHistory.js'
|
|
8
9
|
import {
|
|
9
10
|
addSection,
|
|
10
11
|
createSection,
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
resolveSiteUrl,
|
|
34
35
|
} from '../../lib/preview-link.js'
|
|
35
36
|
import type { SectionSettings, SectionTypeId } from '../page-editor/section-types.js'
|
|
36
|
-
import type
|
|
37
|
+
import { hasCanvasRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
|
|
37
38
|
import { DEFAULT_VIEWPORT, type ViewportId } from '../page-editor/viewports.js'
|
|
38
39
|
import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
|
|
39
40
|
import { SectionsPanel } from '../page-editor/SectionsPanel.js'
|
|
@@ -104,6 +105,7 @@ export function PostSectionEditor({
|
|
|
104
105
|
|
|
105
106
|
const [addOpen, setAddOpen] = useState(false)
|
|
106
107
|
const [fieldsOpen, setFieldsOpen] = useState(false)
|
|
108
|
+
const [historyOpen, setHistoryOpen] = useState(false)
|
|
107
109
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
|
|
108
110
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
109
111
|
const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
|
|
@@ -302,6 +304,23 @@ export function PostSectionEditor({
|
|
|
302
304
|
}
|
|
303
305
|
}, [post, documentId, postType, onNavigate])
|
|
304
306
|
|
|
307
|
+
// After a version restore the server has already written the restored
|
|
308
|
+
// data — refetch so the editor reflects it instead of the stale state.
|
|
309
|
+
const handleRestored = useCallback(() => {
|
|
310
|
+
if (!post?.id) return
|
|
311
|
+
fetchPostForEditor(postType, post.id)
|
|
312
|
+
.then((p) => {
|
|
313
|
+
setPost(p)
|
|
314
|
+
setSelectedId(null)
|
|
315
|
+
setDirty(false)
|
|
316
|
+
setSaveState('idle')
|
|
317
|
+
setValidation(null)
|
|
318
|
+
})
|
|
319
|
+
.catch(() => {
|
|
320
|
+
toast.error('Version restored — refresh the page to see it.')
|
|
321
|
+
})
|
|
322
|
+
}, [postType, post?.id])
|
|
323
|
+
|
|
305
324
|
const guardedNavigate = useCallback(
|
|
306
325
|
(path: string) => {
|
|
307
326
|
if (dirty) setLeaveTarget(path)
|
|
@@ -400,6 +419,15 @@ export function PostSectionEditor({
|
|
|
400
419
|
|
|
401
420
|
const selected = post.sections.find((s) => s.id === selectedId) ?? null
|
|
402
421
|
|
|
422
|
+
// Section types the canvas renders as placeholders (custom + unmanaged).
|
|
423
|
+
// When present the editor surfaces "structure view" messaging and promotes
|
|
424
|
+
// "View on site" as the primary design preview.
|
|
425
|
+
const structuralTypes = Array.from(
|
|
426
|
+
new Set(
|
|
427
|
+
post.sections.filter((s) => !hasCanvasRenderer(s.sectionType)).map((s) => s.sectionType),
|
|
428
|
+
),
|
|
429
|
+
)
|
|
430
|
+
|
|
403
431
|
return (
|
|
404
432
|
<ErrorBoundary>
|
|
405
433
|
<div className="bg-background flex h-full flex-col overflow-hidden">
|
|
@@ -417,9 +445,11 @@ export function PostSectionEditor({
|
|
|
417
445
|
onBack={() => guardedNavigate('/posts')}
|
|
418
446
|
onPreview={handlePreview}
|
|
419
447
|
onViewOnSite={post.id ? handleViewOnSite : undefined}
|
|
448
|
+
onHistory={post.id ? () => setHistoryOpen(true) : undefined}
|
|
420
449
|
onOpenSettings={() => setFieldsOpen(true)}
|
|
421
450
|
onSaveDraft={handleSaveDraft}
|
|
422
451
|
onPublish={requestPublish}
|
|
452
|
+
structuralTypes={structuralTypes}
|
|
423
453
|
/>
|
|
424
454
|
|
|
425
455
|
{validation && (
|
|
@@ -452,6 +482,7 @@ export function PostSectionEditor({
|
|
|
452
482
|
viewport={viewport}
|
|
453
483
|
onSelect={setSelectedId}
|
|
454
484
|
onAddSection={() => setAddOpen(true)}
|
|
485
|
+
structuralTypes={structuralTypes}
|
|
455
486
|
/>
|
|
456
487
|
</div>
|
|
457
488
|
|
|
@@ -476,6 +507,16 @@ export function PostSectionEditor({
|
|
|
476
507
|
onAdd={handleAdd}
|
|
477
508
|
/>
|
|
478
509
|
|
|
510
|
+
{post.id && (
|
|
511
|
+
<VersionHistory
|
|
512
|
+
collectionSlug={postType}
|
|
513
|
+
documentId={post.id}
|
|
514
|
+
open={historyOpen}
|
|
515
|
+
onClose={() => setHistoryOpen(false)}
|
|
516
|
+
onRestore={handleRestored}
|
|
517
|
+
/>
|
|
518
|
+
)}
|
|
519
|
+
|
|
479
520
|
<PostFieldsModal
|
|
480
521
|
open={fieldsOpen}
|
|
481
522
|
post={post}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
|
+
import { Info } from 'lucide-react'
|
|
4
5
|
import { ConfirmDangerousSettingDialog, SettingsCard, SettingsToggleRow } from './components.js'
|
|
5
6
|
import type { GeneralSettingsForm, SiteIndexStatus } from './useGeneralSettings.js'
|
|
6
7
|
|
|
@@ -90,6 +91,14 @@ export function SeoRobotsDefaultsCard({
|
|
|
90
91
|
disabled={!canEdit}
|
|
91
92
|
onChange={(v) => requestToggle('noIndexNonProduction', v)}
|
|
92
93
|
/>
|
|
94
|
+
{isProduction && (
|
|
95
|
+
<p className="text-muted-foreground flex items-start gap-2 text-sm">
|
|
96
|
+
<Info size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
|
|
97
|
+
This deployment is running in the production environment, so this setting does not
|
|
98
|
+
change pages served here — it forces noindex only on preview, staging, and development
|
|
99
|
+
deployments. To block search engines from this site, use Default No Index.
|
|
100
|
+
</p>
|
|
101
|
+
)}
|
|
93
102
|
</div>
|
|
94
103
|
|
|
95
104
|
<div className="border-border mt-4 space-y-2 border-t pt-4">
|
|
@@ -246,6 +246,11 @@ export function SettingsSaveBar({
|
|
|
246
246
|
<Check size={16} aria-hidden="true" /> Saved
|
|
247
247
|
</span>
|
|
248
248
|
)}
|
|
249
|
+
{dirty && !saving && (
|
|
250
|
+
<span className="text-muted-foreground text-sm" aria-live="polite">
|
|
251
|
+
Unsaved changes
|
|
252
|
+
</span>
|
|
253
|
+
)}
|
|
249
254
|
<button
|
|
250
255
|
type="button"
|
|
251
256
|
onClick={onReset}
|