@actuate-media/cms-admin 0.40.1 → 0.42.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/AdminRoot.d.ts +10 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +5 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/views/admin-section-renderers.test.d.ts +2 -0
- package/dist/__tests__/views/admin-section-renderers.test.d.ts.map +1 -0
- package/dist/__tests__/views/admin-section-renderers.test.js +71 -0
- package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -0
- package/dist/__tests__/views/page-section-editor.test.js +64 -0
- package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.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 +29 -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 +47 -4
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.d.ts +87 -0
- package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -0
- package/dist/views/page-editor/sections/admin-renderers.js +46 -0
- package/dist/views/page-editor/sections/admin-renderers.js.map +1 -0
- package/dist/views/page-editor/sections/index.d.ts +3 -1
- package/dist/views/page-editor/sections/index.d.ts.map +1 -1
- package/dist/views/page-editor/sections/index.js +2 -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 +29 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +20 -1
- package/src/__tests__/views/admin-section-renderers.test.tsx +101 -0
- package/src/__tests__/views/page-section-editor.test.tsx +99 -0
- package/src/index.ts +11 -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 +46 -0
- package/src/views/page-editor/StructureNotice.tsx +27 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +54 -4
- package/src/views/page-editor/sections/admin-renderers.tsx +105 -0
- package/src/views/page-editor/sections/index.ts +13 -1
- package/src/views/post-editor/PostEditorCanvas.tsx +5 -0
- package/src/views/post-editor/PostSectionEditor.tsx +50 -1
|
@@ -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, useAdminSectionRenderers } 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,12 +84,14 @@ 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)
|
|
88
91
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
89
92
|
|
|
90
93
|
const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
|
|
94
|
+
const customRenderers = useAdminSectionRenderers()
|
|
91
95
|
|
|
92
96
|
// A misconfigured `pages` collection (missing the fields the editor
|
|
93
97
|
// writes) would silently drop content on save — detect it up front.
|
|
@@ -285,6 +289,23 @@ export function PageSectionEditor({
|
|
|
285
289
|
}
|
|
286
290
|
}, [page, documentId, onNavigate])
|
|
287
291
|
|
|
292
|
+
// After a version restore the server has already written the restored
|
|
293
|
+
// data — refetch so the editor reflects it instead of the stale state.
|
|
294
|
+
const handleRestored = useCallback(() => {
|
|
295
|
+
if (!page?.id) return
|
|
296
|
+
fetchPageForEditor(page.id)
|
|
297
|
+
.then((p) => {
|
|
298
|
+
setPage(p)
|
|
299
|
+
setSelectedId(null)
|
|
300
|
+
setDirty(false)
|
|
301
|
+
setSaveState('idle')
|
|
302
|
+
setValidation(null)
|
|
303
|
+
})
|
|
304
|
+
.catch(() => {
|
|
305
|
+
toast.error('Version restored — refresh the page to see it.')
|
|
306
|
+
})
|
|
307
|
+
}, [page?.id])
|
|
308
|
+
|
|
288
309
|
// ─── Navigation with unsaved guard ───────────────────────────────────
|
|
289
310
|
const guardedNavigate = useCallback(
|
|
290
311
|
(path: string) => {
|
|
@@ -398,6 +419,18 @@ export function PageSectionEditor({
|
|
|
398
419
|
new Set(page.sections.filter((s) => s.unmanaged).map((s) => s.sectionType)),
|
|
399
420
|
)
|
|
400
421
|
|
|
422
|
+
// Section types the canvas renders as placeholders — no built-in body
|
|
423
|
+
// component and no consumer-registered admin renderer. When present the
|
|
424
|
+
// editor surfaces "structure view" messaging and promotes "View on site"
|
|
425
|
+
// as the primary design preview.
|
|
426
|
+
const structuralTypes = Array.from(
|
|
427
|
+
new Set(
|
|
428
|
+
page.sections
|
|
429
|
+
.filter((s) => !hasCanvasRenderer(s.sectionType) && !customRenderers[s.sectionType])
|
|
430
|
+
.map((s) => s.sectionType),
|
|
431
|
+
),
|
|
432
|
+
)
|
|
433
|
+
|
|
401
434
|
return (
|
|
402
435
|
<ErrorBoundary>
|
|
403
436
|
<div className="bg-background flex h-full flex-col overflow-hidden">
|
|
@@ -414,9 +447,11 @@ export function PageSectionEditor({
|
|
|
414
447
|
onBack={() => guardedNavigate('/pages')}
|
|
415
448
|
onPreview={handlePreview}
|
|
416
449
|
onViewOnSite={page.id ? handleViewOnSite : undefined}
|
|
450
|
+
onHistory={page.id ? () => setHistoryOpen(true) : undefined}
|
|
417
451
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
418
452
|
onSaveDraft={handleSaveDraft}
|
|
419
453
|
onPublish={requestPublish}
|
|
454
|
+
structuralTypes={structuralTypes}
|
|
420
455
|
/>
|
|
421
456
|
|
|
422
457
|
{readOnly && (
|
|
@@ -487,6 +522,7 @@ export function PageSectionEditor({
|
|
|
487
522
|
viewport={viewport}
|
|
488
523
|
onSelect={setSelectedId}
|
|
489
524
|
onAddSection={() => setAddOpen(true)}
|
|
525
|
+
structuralTypes={structuralTypes}
|
|
490
526
|
/>
|
|
491
527
|
</div>
|
|
492
528
|
|
|
@@ -506,6 +542,16 @@ export function PageSectionEditor({
|
|
|
506
542
|
|
|
507
543
|
<AddSectionModal open={addOpen} onClose={() => setAddOpen(false)} onAdd={handleAdd} />
|
|
508
544
|
|
|
545
|
+
{page.id && (
|
|
546
|
+
<VersionHistory
|
|
547
|
+
collectionSlug="pages"
|
|
548
|
+
documentId={page.id}
|
|
549
|
+
open={historyOpen}
|
|
550
|
+
onClose={() => setHistoryOpen(false)}
|
|
551
|
+
onRestore={handleRestored}
|
|
552
|
+
/>
|
|
553
|
+
)}
|
|
554
|
+
|
|
509
555
|
<PageSettingsModal
|
|
510
556
|
open={settingsOpen}
|
|
511
557
|
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
|
+
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { Puzzle } from 'lucide-react'
|
|
4
4
|
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
5
|
+
import { ErrorBoundary } from '../../../components/ErrorBoundary.js'
|
|
5
6
|
import { getSectionType } from '../section-types.js'
|
|
7
|
+
import { ADMIN_RENDER_OPTIONS, useAdminSectionRenderers } from './admin-renderers.js'
|
|
6
8
|
import { backgroundClass, paddingClass, type PostRenderContext } from './parts.js'
|
|
7
9
|
import { HeroBannerSection } from './HeroBannerSection.js'
|
|
8
10
|
import { MissionSection } from './MissionSection.js'
|
|
@@ -15,8 +17,34 @@ import { AuthorBioSection } from './AuthorBioSection.js'
|
|
|
15
17
|
import { RelatedPostsSection } from './RelatedPostsSection.js'
|
|
16
18
|
import { GallerySection } from './GallerySection.js'
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Section types with a real canvas body component below. Anything else —
|
|
22
|
+
* config-declared custom types (`opal-*`), unmanaged sections, and the
|
|
23
|
+
* `form` embed — renders as a structural placeholder, so the editors use
|
|
24
|
+
* this set to tell authors the canvas shows structure, not design.
|
|
25
|
+
*/
|
|
26
|
+
const CANVAS_RENDERED_TYPES: ReadonlySet<string> = new Set([
|
|
27
|
+
'hero',
|
|
28
|
+
'text',
|
|
29
|
+
'feature',
|
|
30
|
+
'stats',
|
|
31
|
+
'cta',
|
|
32
|
+
'article-body',
|
|
33
|
+
'quote',
|
|
34
|
+
'author-bio',
|
|
35
|
+
'related-posts',
|
|
36
|
+
'gallery',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/** True when the canvas renders this type for real (not as a placeholder). */
|
|
40
|
+
export function hasCanvasRenderer(sectionType: string): boolean {
|
|
41
|
+
return CANVAS_RENDERED_TYPES.has(sectionType)
|
|
42
|
+
}
|
|
43
|
+
|
|
18
44
|
/** Dispatch a section's body component by type. Shared by editor + preview. */
|
|
19
45
|
function SectionBody({ section, context }: { section: PageSection; context?: PostRenderContext }) {
|
|
46
|
+
const customRenderers = useAdminSectionRenderers()
|
|
47
|
+
|
|
20
48
|
switch (section.sectionType) {
|
|
21
49
|
case 'hero':
|
|
22
50
|
return <HeroBannerSection section={section} />
|
|
@@ -38,11 +66,33 @@ function SectionBody({ section, context }: { section: PageSection; context?: Pos
|
|
|
38
66
|
return <RelatedPostsSection section={section} context={context} />
|
|
39
67
|
case 'gallery':
|
|
40
68
|
return <GallerySection section={section} />
|
|
41
|
-
default:
|
|
42
|
-
// Custom (config-declared)
|
|
43
|
-
//
|
|
44
|
-
//
|
|
69
|
+
default: {
|
|
70
|
+
// Custom (config-declared) and unmanaged types: use a consumer-supplied
|
|
71
|
+
// canvas renderer when one is registered (see admin-renderers.tsx).
|
|
72
|
+
// Consumer code can throw, so a per-section error boundary falls back
|
|
73
|
+
// to the structural placeholder instead of taking down the editor.
|
|
74
|
+
const Custom = customRenderers[section.sectionType]
|
|
75
|
+
if (Custom) {
|
|
76
|
+
// The renderer contract types `settings` as an open record (matching
|
|
77
|
+
// the sections-react wire shape); the admin's concrete
|
|
78
|
+
// `SectionSettings` interface is the same object at runtime but has
|
|
79
|
+
// no index signature, so bridge the nominal gap here.
|
|
80
|
+
const wireSection = {
|
|
81
|
+
...section,
|
|
82
|
+
settings: { ...section.settings } as Record<string, unknown>,
|
|
83
|
+
}
|
|
84
|
+
// Rendered as a component (not invoked inline) so a throw happens
|
|
85
|
+
// inside the boundary's subtree and is actually caught.
|
|
86
|
+
return (
|
|
87
|
+
<ErrorBoundary key={section.id} fallback={<PlaceholderSection section={section} />}>
|
|
88
|
+
<Custom section={wireSection} ctx={context} options={ADMIN_RENDER_OPTIONS} />
|
|
89
|
+
</ErrorBoundary>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
// No renderer registered: a neutral placeholder keeps the canvas
|
|
93
|
+
// reflecting the section's presence + order instead of a blank gap.
|
|
45
94
|
return <PlaceholderSection section={section} />
|
|
95
|
+
}
|
|
46
96
|
}
|
|
47
97
|
}
|
|
48
98
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pluggable canvas renderers for custom section types.
|
|
5
|
+
*
|
|
6
|
+
* By default the editor canvas renders custom (config-declared) and unmanaged
|
|
7
|
+
* section types as structural placeholders. Projects that want a real design
|
|
8
|
+
* preview inside the admin can pass renderers to `<AdminRoot
|
|
9
|
+
* sectionRenderers={...}>`; the canvas and the eye preview then use them in
|
|
10
|
+
* place of the placeholder.
|
|
11
|
+
*
|
|
12
|
+
* The renderer signature is a structural mirror of `SectionRenderer` from
|
|
13
|
+
* `@actuate-media/sections-react` (cms-admin depends on cms-core only, so the
|
|
14
|
+
* type is duplicated rather than imported). Because the shapes match, a
|
|
15
|
+
* consumer can hand `AdminRoot` the exact same components it registers for
|
|
16
|
+
* the public site:
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { OPAL_RENDERERS } from './sections' // SectionRendererMap
|
|
20
|
+
* <AdminRoot sectionRenderers={OPAL_RENDERERS} ... />
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Renderers run inside the admin bundle, so they must be client-safe (no
|
|
24
|
+
* server-only imports). Each one is wrapped in an error boundary — a throwing
|
|
25
|
+
* renderer falls back to the structural placeholder instead of taking down
|
|
26
|
+
* the editor.
|
|
27
|
+
*/
|
|
28
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
29
|
+
|
|
30
|
+
/** Structural mirror of `RenderImageProps` from `@actuate-media/sections-react`. */
|
|
31
|
+
export interface AdminRenderImageProps {
|
|
32
|
+
src: string
|
|
33
|
+
alt: string
|
|
34
|
+
width?: number | null
|
|
35
|
+
height?: number | null
|
|
36
|
+
className?: string
|
|
37
|
+
priority?: boolean
|
|
38
|
+
sizes?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Structural mirror of `PostContext` from `@actuate-media/sections-react`. */
|
|
42
|
+
export interface AdminSectionPostContext {
|
|
43
|
+
title?: string
|
|
44
|
+
excerpt?: string
|
|
45
|
+
body?: string
|
|
46
|
+
featuredImageUrl?: string
|
|
47
|
+
publishDate?: string | null
|
|
48
|
+
category?: string
|
|
49
|
+
author?: { name?: string; bio?: string; avatarUrl?: string }
|
|
50
|
+
relatedPosts?: Array<{ title: string; url?: string; excerpt?: string }>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Props passed to an admin section renderer (mirrors `SectionRendererProps`). */
|
|
54
|
+
export interface AdminSectionRendererProps {
|
|
55
|
+
section: {
|
|
56
|
+
id: string
|
|
57
|
+
sectionType: string
|
|
58
|
+
name?: string
|
|
59
|
+
visible?: boolean
|
|
60
|
+
content?: Record<string, unknown>
|
|
61
|
+
settings?: Record<string, unknown>
|
|
62
|
+
}
|
|
63
|
+
ctx?: AdminSectionPostContext
|
|
64
|
+
options: {
|
|
65
|
+
sanitizeHtml: (html: string) => string
|
|
66
|
+
baseUrl: string
|
|
67
|
+
renderImage: (props: AdminRenderImageProps) => ReactNode
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A canvas renderer for one custom section type. */
|
|
72
|
+
export type AdminSectionRenderer = (props: AdminSectionRendererProps) => ReactNode
|
|
73
|
+
|
|
74
|
+
/** Section-type id → canvas renderer. */
|
|
75
|
+
export type AdminSectionRendererMap = Record<string, AdminSectionRenderer>
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Render options passed to every admin renderer. Rich text is sanitized on
|
|
79
|
+
* write by cms-core, so the canvas (same trust domain as the public default)
|
|
80
|
+
* uses the identity sanitizer; images render as plain lazy `<img>` since the
|
|
81
|
+
* editor canvas has no next/image pipeline.
|
|
82
|
+
*/
|
|
83
|
+
export const ADMIN_RENDER_OPTIONS: AdminSectionRendererProps['options'] = {
|
|
84
|
+
sanitizeHtml: (html) => html,
|
|
85
|
+
baseUrl: '',
|
|
86
|
+
renderImage: ({ src, alt, className, priority, width, height }) => (
|
|
87
|
+
<img
|
|
88
|
+
src={src}
|
|
89
|
+
alt={alt}
|
|
90
|
+
className={className}
|
|
91
|
+
width={width ?? undefined}
|
|
92
|
+
height={height ?? undefined}
|
|
93
|
+
loading={priority ? undefined : 'lazy'}
|
|
94
|
+
/>
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const AdminSectionRenderersContext = createContext<AdminSectionRendererMap>({})
|
|
99
|
+
|
|
100
|
+
export const AdminSectionRenderersProvider = AdminSectionRenderersContext.Provider
|
|
101
|
+
|
|
102
|
+
/** The renderer map passed to `<AdminRoot sectionRenderers>` (empty by default). */
|
|
103
|
+
export function useAdminSectionRenderers(): AdminSectionRendererMap {
|
|
104
|
+
return useContext(AdminSectionRenderersContext)
|
|
105
|
+
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
export { SectionRenderer } from './SectionRenderer.js'
|
|
1
|
+
export { SectionRenderer, hasCanvasRenderer } from './SectionRenderer.js'
|
|
2
2
|
export type { SectionRendererProps } from './SectionRenderer.js'
|
|
3
|
+
export {
|
|
4
|
+
AdminSectionRenderersProvider,
|
|
5
|
+
useAdminSectionRenderers,
|
|
6
|
+
ADMIN_RENDER_OPTIONS,
|
|
7
|
+
} from './admin-renderers.js'
|
|
8
|
+
export type {
|
|
9
|
+
AdminSectionRenderer,
|
|
10
|
+
AdminSectionRendererMap,
|
|
11
|
+
AdminSectionRendererProps,
|
|
12
|
+
AdminSectionPostContext,
|
|
13
|
+
AdminRenderImageProps,
|
|
14
|
+
} from './admin-renderers.js'
|
|
3
15
|
export type { PostRenderContext } from './parts.js'
|
|
4
16
|
export { HeroBannerSection } from './HeroBannerSection.js'
|
|
5
17
|
export { MissionSection } from './MissionSection.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,11 @@ 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
|
|
37
|
+
import {
|
|
38
|
+
hasCanvasRenderer,
|
|
39
|
+
useAdminSectionRenderers,
|
|
40
|
+
type PostRenderContext,
|
|
41
|
+
} from '../page-editor/sections/index.js'
|
|
37
42
|
import { DEFAULT_VIEWPORT, type ViewportId } from '../page-editor/viewports.js'
|
|
38
43
|
import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
|
|
39
44
|
import { SectionsPanel } from '../page-editor/SectionsPanel.js'
|
|
@@ -104,12 +109,14 @@ export function PostSectionEditor({
|
|
|
104
109
|
|
|
105
110
|
const [addOpen, setAddOpen] = useState(false)
|
|
106
111
|
const [fieldsOpen, setFieldsOpen] = useState(false)
|
|
112
|
+
const [historyOpen, setHistoryOpen] = useState(false)
|
|
107
113
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
|
|
108
114
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
109
115
|
const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
|
|
110
116
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
111
117
|
|
|
112
118
|
const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
|
|
119
|
+
const customRenderers = useAdminSectionRenderers()
|
|
113
120
|
|
|
114
121
|
const typeLabel = useMemo(() => {
|
|
115
122
|
const col = config?.collections?.[postType]
|
|
@@ -302,6 +309,23 @@ export function PostSectionEditor({
|
|
|
302
309
|
}
|
|
303
310
|
}, [post, documentId, postType, onNavigate])
|
|
304
311
|
|
|
312
|
+
// After a version restore the server has already written the restored
|
|
313
|
+
// data — refetch so the editor reflects it instead of the stale state.
|
|
314
|
+
const handleRestored = useCallback(() => {
|
|
315
|
+
if (!post?.id) return
|
|
316
|
+
fetchPostForEditor(postType, post.id)
|
|
317
|
+
.then((p) => {
|
|
318
|
+
setPost(p)
|
|
319
|
+
setSelectedId(null)
|
|
320
|
+
setDirty(false)
|
|
321
|
+
setSaveState('idle')
|
|
322
|
+
setValidation(null)
|
|
323
|
+
})
|
|
324
|
+
.catch(() => {
|
|
325
|
+
toast.error('Version restored — refresh the page to see it.')
|
|
326
|
+
})
|
|
327
|
+
}, [postType, post?.id])
|
|
328
|
+
|
|
305
329
|
const guardedNavigate = useCallback(
|
|
306
330
|
(path: string) => {
|
|
307
331
|
if (dirty) setLeaveTarget(path)
|
|
@@ -400,6 +424,18 @@ export function PostSectionEditor({
|
|
|
400
424
|
|
|
401
425
|
const selected = post.sections.find((s) => s.id === selectedId) ?? null
|
|
402
426
|
|
|
427
|
+
// Section types the canvas renders as placeholders — no built-in body
|
|
428
|
+
// component and no consumer-registered admin renderer. When present the
|
|
429
|
+
// editor surfaces "structure view" messaging and promotes "View on site"
|
|
430
|
+
// as the primary design preview.
|
|
431
|
+
const structuralTypes = Array.from(
|
|
432
|
+
new Set(
|
|
433
|
+
post.sections
|
|
434
|
+
.filter((s) => !hasCanvasRenderer(s.sectionType) && !customRenderers[s.sectionType])
|
|
435
|
+
.map((s) => s.sectionType),
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
|
|
403
439
|
return (
|
|
404
440
|
<ErrorBoundary>
|
|
405
441
|
<div className="bg-background flex h-full flex-col overflow-hidden">
|
|
@@ -417,9 +453,11 @@ export function PostSectionEditor({
|
|
|
417
453
|
onBack={() => guardedNavigate('/posts')}
|
|
418
454
|
onPreview={handlePreview}
|
|
419
455
|
onViewOnSite={post.id ? handleViewOnSite : undefined}
|
|
456
|
+
onHistory={post.id ? () => setHistoryOpen(true) : undefined}
|
|
420
457
|
onOpenSettings={() => setFieldsOpen(true)}
|
|
421
458
|
onSaveDraft={handleSaveDraft}
|
|
422
459
|
onPublish={requestPublish}
|
|
460
|
+
structuralTypes={structuralTypes}
|
|
423
461
|
/>
|
|
424
462
|
|
|
425
463
|
{validation && (
|
|
@@ -452,6 +490,7 @@ export function PostSectionEditor({
|
|
|
452
490
|
viewport={viewport}
|
|
453
491
|
onSelect={setSelectedId}
|
|
454
492
|
onAddSection={() => setAddOpen(true)}
|
|
493
|
+
structuralTypes={structuralTypes}
|
|
455
494
|
/>
|
|
456
495
|
</div>
|
|
457
496
|
|
|
@@ -476,6 +515,16 @@ export function PostSectionEditor({
|
|
|
476
515
|
onAdd={handleAdd}
|
|
477
516
|
/>
|
|
478
517
|
|
|
518
|
+
{post.id && (
|
|
519
|
+
<VersionHistory
|
|
520
|
+
collectionSlug={postType}
|
|
521
|
+
documentId={post.id}
|
|
522
|
+
open={historyOpen}
|
|
523
|
+
onClose={() => setHistoryOpen(false)}
|
|
524
|
+
onRestore={handleRestored}
|
|
525
|
+
/>
|
|
526
|
+
)}
|
|
527
|
+
|
|
479
528
|
<PostFieldsModal
|
|
480
529
|
open={fieldsOpen}
|
|
481
530
|
post={post}
|