@actuate-media/cms-admin 0.59.0 → 0.61.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 +16 -0
- package/dist/__tests__/lib/editor-preview.test.d.ts +2 -0
- package/dist/__tests__/lib/editor-preview.test.d.ts.map +1 -0
- package/dist/__tests__/lib/editor-preview.test.js +18 -0
- package/dist/__tests__/lib/editor-preview.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +16 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
- package/dist/__tests__/views/dashboard.test.js +23 -0
- package/dist/__tests__/views/dashboard.test.js.map +1 -1
- package/dist/__tests__/views/editor-top-bar.render.test.d.ts +2 -0
- package/dist/__tests__/views/editor-top-bar.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +46 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -0
- package/dist/__tests__/views/use-site-preview.render.test.js +10 -1
- package/dist/__tests__/views/use-site-preview.render.test.js.map +1 -1
- package/dist/lib/editor-preview.d.ts +4 -0
- package/dist/lib/editor-preview.d.ts.map +1 -0
- package/dist/lib/editor-preview.js +10 -0
- package/dist/lib/editor-preview.js.map +1 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +4 -1
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +11 -3
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts +2 -1
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +2 -2
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +5 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +12 -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 +7 -1
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/StructureNotice.d.ts +4 -3
- package/dist/views/page-editor/StructureNotice.d.ts.map +1 -1
- package/dist/views/page-editor/StructureNotice.js +9 -7
- package/dist/views/page-editor/StructureNotice.js.map +1 -1
- package/dist/views/page-editor/useSitePreview.d.ts +3 -1
- package/dist/views/page-editor/useSitePreview.d.ts.map +1 -1
- package/dist/views/page-editor/useSitePreview.js +44 -31
- package/dist/views/page-editor/useSitePreview.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 +2 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts +8 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +7 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/lib/editor-preview.test.ts +21 -0
- package/src/__tests__/lib/post-editor-service.test.ts +17 -0
- package/src/__tests__/views/dashboard.test.tsx +26 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +65 -0
- package/src/__tests__/views/use-site-preview.render.test.tsx +12 -0
- package/src/lib/editor-preview.ts +12 -0
- package/src/lib/post-editor-service.ts +4 -1
- package/src/lib/seo-service.ts +14 -3
- package/src/views/Users.tsx +1 -1
- package/src/views/page-editor/EditorCanvas.tsx +6 -1
- package/src/views/page-editor/EditorTopBar.tsx +20 -1
- package/src/views/page-editor/PageSectionEditor.tsx +14 -0
- package/src/views/page-editor/StructureNotice.tsx +42 -8
- package/src/views/page-editor/useSitePreview.ts +47 -31
- package/src/views/post-editor/PostEditorCanvas.tsx +7 -1
- package/src/views/post-editor/PostSectionEditor.tsx +15 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AdminEditorConfig } from '@actuate-media/cms-core'
|
|
2
|
+
|
|
3
|
+
/** Resolve whether a section editor should open with Site preview active. */
|
|
4
|
+
export function resolveDefaultSitePreview(
|
|
5
|
+
editor: AdminEditorConfig | undefined,
|
|
6
|
+
kind: 'pages' | 'posts',
|
|
7
|
+
): boolean {
|
|
8
|
+
const setting = editor?.defaultSitePreview
|
|
9
|
+
if (setting === true) return true
|
|
10
|
+
if (setting == null || setting === false) return false
|
|
11
|
+
return kind === 'pages' ? setting.pages === true : setting.posts === true
|
|
12
|
+
}
|
|
@@ -220,10 +220,13 @@ export function parseTags(raw: unknown): string[] {
|
|
|
220
220
|
|
|
221
221
|
function rowToEditorPost(postType: string, doc: DocumentApiRow): EditorPost {
|
|
222
222
|
const data = doc.data ?? {}
|
|
223
|
+
const dataTitle = dataStr(data, 'title')
|
|
223
224
|
return {
|
|
224
225
|
id: doc.id,
|
|
225
226
|
postType,
|
|
226
|
-
|
|
227
|
+
// Prefer the JSON field — the denormalized `doc.title` column can lag or
|
|
228
|
+
// reflect slug-derived fallbacks on imported/legacy rows.
|
|
229
|
+
title: dataTitle || (doc.title ?? ''),
|
|
227
230
|
slug: doc.slug ?? dataStr(data, 'slug'),
|
|
228
231
|
excerpt: dataStr(data, 'excerpt'),
|
|
229
232
|
featuredImage: dataStr(data, 'featuredImage'),
|
package/src/lib/seo-service.ts
CHANGED
|
@@ -817,9 +817,20 @@ export async function fetchRedirectOverview(): Promise<RedirectOverview> {
|
|
|
817
817
|
}
|
|
818
818
|
|
|
819
819
|
export async function fetchRedirectRules(): Promise<RedirectRule[]> {
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
820
|
+
const rules: RedirectRule[] = []
|
|
821
|
+
let page = 1
|
|
822
|
+
let totalPages = 1
|
|
823
|
+
do {
|
|
824
|
+
const res = await cmsApi<{
|
|
825
|
+
rules: RedirectRule[]
|
|
826
|
+
totalPages?: number
|
|
827
|
+
}>(`/seo/redirects?page=${page}&pageSize=500`)
|
|
828
|
+
throwIfError(res)
|
|
829
|
+
rules.push(...(res.data?.rules ?? []))
|
|
830
|
+
totalPages = res.data?.totalPages ?? 1
|
|
831
|
+
page += 1
|
|
832
|
+
} while (page <= totalPages)
|
|
833
|
+
return rules
|
|
823
834
|
}
|
|
824
835
|
|
|
825
836
|
export interface RedirectRulePayload {
|
package/src/views/Users.tsx
CHANGED
|
@@ -677,7 +677,7 @@ export interface UsersProps {
|
|
|
677
677
|
}
|
|
678
678
|
|
|
679
679
|
export function Users({ embedded = false }: UsersProps = {}) {
|
|
680
|
-
const members = useApiData<ApiMember[]>('/users')
|
|
680
|
+
const members = useApiData<ApiMember[]>('/users?pageSize=200')
|
|
681
681
|
const invites = useApiData<ApiInvite[]>('/invites')
|
|
682
682
|
const stats = useApiData<ApiStats>('/users/stats')
|
|
683
683
|
const review = useApiData<ApiRecommendation[]>('/access-review')
|
|
@@ -15,6 +15,7 @@ interface EditorCanvasProps {
|
|
|
15
15
|
onAddSection: () => void
|
|
16
16
|
/** Section types rendered as placeholders — shows the "Structure view" notice. */
|
|
17
17
|
structuralTypes?: string[]
|
|
18
|
+
sitePreviewAvailable?: boolean
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -30,6 +31,7 @@ export function EditorCanvas({
|
|
|
30
31
|
onSelect,
|
|
31
32
|
onAddSection,
|
|
32
33
|
structuralTypes = [],
|
|
34
|
+
sitePreviewAvailable = false,
|
|
33
35
|
}: EditorCanvasProps) {
|
|
34
36
|
const vp = getViewport(viewport)
|
|
35
37
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -78,7 +80,10 @@ export function EditorCanvas({
|
|
|
78
80
|
<EmptyCanvas onAddSection={onAddSection} />
|
|
79
81
|
) : (
|
|
80
82
|
<>
|
|
81
|
-
<StructureNotice
|
|
83
|
+
<StructureNotice
|
|
84
|
+
structuralTypes={structuralTypes}
|
|
85
|
+
sitePreviewAvailable={sitePreviewAvailable}
|
|
86
|
+
/>
|
|
82
87
|
<div
|
|
83
88
|
className="mx-auto"
|
|
84
89
|
style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
|
|
@@ -28,6 +28,10 @@ interface EditorTopBarProps {
|
|
|
28
28
|
publishing: boolean
|
|
29
29
|
canEdit: boolean
|
|
30
30
|
canPublish: boolean
|
|
31
|
+
/** When false, Publish stays disabled even if the user has publish permission. */
|
|
32
|
+
publishReady?: boolean
|
|
33
|
+
/** Shown on hover/focus when publish is blocked by validation (not role). */
|
|
34
|
+
publishDisabledReason?: string
|
|
31
35
|
viewport: ViewportId
|
|
32
36
|
/** Breadcrumb root label + back-button text (e.g. "Pages" or "Posts"). */
|
|
33
37
|
backLabel?: string
|
|
@@ -86,6 +90,8 @@ export function EditorTopBar({
|
|
|
86
90
|
publishing,
|
|
87
91
|
canEdit,
|
|
88
92
|
canPublish,
|
|
93
|
+
publishReady = true,
|
|
94
|
+
publishDisabledReason,
|
|
89
95
|
viewport,
|
|
90
96
|
backLabel = 'Pages',
|
|
91
97
|
onViewport,
|
|
@@ -109,6 +115,17 @@ export function EditorTopBar({
|
|
|
109
115
|
? 'Preview structure (custom sections show placeholders)'
|
|
110
116
|
: 'Preview page'
|
|
111
117
|
|
|
118
|
+
const publishBlocked = !canPublish || !publishReady || publishing || saveState === 'saving'
|
|
119
|
+
const publishTitle = publishing
|
|
120
|
+
? 'Publishing…'
|
|
121
|
+
: saveState === 'saving'
|
|
122
|
+
? 'Wait for the current save to finish'
|
|
123
|
+
: canPublish && !publishReady && publishDisabledReason
|
|
124
|
+
? publishDisabledReason
|
|
125
|
+
: !canPublish
|
|
126
|
+
? 'You do not have permission to publish'
|
|
127
|
+
: undefined
|
|
128
|
+
|
|
112
129
|
return (
|
|
113
130
|
<header className="bg-card border-border flex h-14 shrink-0 items-center justify-between gap-4 border-b px-4">
|
|
114
131
|
{/* Breadcrumb + status */}
|
|
@@ -253,7 +270,9 @@ export function EditorTopBar({
|
|
|
253
270
|
<button
|
|
254
271
|
type="button"
|
|
255
272
|
onClick={onPublish}
|
|
256
|
-
disabled={
|
|
273
|
+
disabled={publishBlocked}
|
|
274
|
+
title={publishTitle}
|
|
275
|
+
aria-disabled={publishBlocked}
|
|
257
276
|
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
258
277
|
>
|
|
259
278
|
{publishing && <Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />}
|
|
@@ -43,6 +43,7 @@ import { SectionInspector } from './SectionInspector.js'
|
|
|
43
43
|
import { AddSectionModal } from './AddSectionModal.js'
|
|
44
44
|
import { PageSettingsModal } from './PageSettingsModal.js'
|
|
45
45
|
import { ValidationSummary } from './ValidationSummary.js'
|
|
46
|
+
import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
|
|
46
47
|
|
|
47
48
|
interface PageSectionEditorProps {
|
|
48
49
|
/** undefined when creating a new page (`/pages/new`) */
|
|
@@ -113,6 +114,13 @@ export function PageSectionEditor({
|
|
|
113
114
|
)
|
|
114
115
|
}, [config])
|
|
115
116
|
|
|
117
|
+
const publishCheck = useMemo(
|
|
118
|
+
() => (page ? validatePage(page, true) : { errors: [''], warnings: [] }),
|
|
119
|
+
[page],
|
|
120
|
+
)
|
|
121
|
+
const publishReady = Boolean(page) && publishCheck.errors.length === 0
|
|
122
|
+
const publishDisabledReason = publishCheck.errors[0]
|
|
123
|
+
|
|
116
124
|
// ─── Load ──────────────────────────────────────────────────────────
|
|
117
125
|
useEffect(() => {
|
|
118
126
|
let cancelled = false
|
|
@@ -275,8 +283,11 @@ export function PageSectionEditor({
|
|
|
275
283
|
dirty,
|
|
276
284
|
saveState,
|
|
277
285
|
saveDraft: handleSaveDraft,
|
|
286
|
+
defaultActive: resolveDefaultSitePreview(config?.admin?.editor, 'pages'),
|
|
278
287
|
})
|
|
279
288
|
|
|
289
|
+
const sitePreviewAvailable = Boolean(page?.id && page?.slug)
|
|
290
|
+
|
|
280
291
|
const requestPublish = useCallback(() => {
|
|
281
292
|
if (!page) return
|
|
282
293
|
const result = validatePage(page, true)
|
|
@@ -459,6 +470,8 @@ export function PageSectionEditor({
|
|
|
459
470
|
publishing={publishing}
|
|
460
471
|
canEdit={effectiveCanEdit}
|
|
461
472
|
canPublish={canPublish && effectiveCanEdit}
|
|
473
|
+
publishReady={publishReady}
|
|
474
|
+
publishDisabledReason={publishDisabledReason}
|
|
462
475
|
viewport={viewport}
|
|
463
476
|
onViewport={setViewport}
|
|
464
477
|
onBack={() => guardedNavigate('/pages')}
|
|
@@ -550,6 +563,7 @@ export function PageSectionEditor({
|
|
|
550
563
|
onSelect={setSelectedId}
|
|
551
564
|
onAddSection={() => setAddOpen(true)}
|
|
552
565
|
structuralTypes={structuralTypes}
|
|
566
|
+
sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
|
|
553
567
|
/>
|
|
554
568
|
)}
|
|
555
569
|
</div>
|
|
@@ -5,12 +5,47 @@ import { Puzzle } from 'lucide-react'
|
|
|
5
5
|
/**
|
|
6
6
|
* Banner shown above the canvas surface when the document contains section
|
|
7
7
|
* types the canvas can only render as placeholders (custom / unmanaged
|
|
8
|
-
* types)
|
|
9
|
-
* lives on the public site behind "View on site".
|
|
8
|
+
* types), or when Site preview is available for accurate styling.
|
|
10
9
|
*/
|
|
11
|
-
export function StructureNotice({
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
export function StructureNotice({
|
|
11
|
+
structuralTypes,
|
|
12
|
+
sitePreviewAvailable = false,
|
|
13
|
+
}: {
|
|
14
|
+
structuralTypes: string[]
|
|
15
|
+
/** When true, nudge editors toward the Site preview toggle for WYSIWYG. */
|
|
16
|
+
sitePreviewAvailable?: boolean
|
|
17
|
+
}) {
|
|
18
|
+
if (structuralTypes.length === 0 && !sitePreviewAvailable) return null
|
|
19
|
+
|
|
20
|
+
if (structuralTypes.length > 0) {
|
|
21
|
+
const count = structuralTypes.length
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
role="note"
|
|
25
|
+
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"
|
|
26
|
+
>
|
|
27
|
+
<Puzzle className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
28
|
+
<span>
|
|
29
|
+
<span className="text-foreground font-medium">Structure view</span> — {count} custom
|
|
30
|
+
section type{count > 1 ? 's' : ''} shown as placeholders.
|
|
31
|
+
{sitePreviewAvailable ? (
|
|
32
|
+
<>
|
|
33
|
+
{' '}
|
|
34
|
+
Use <span className="text-foreground font-medium">Site preview</span> for accurate
|
|
35
|
+
styling.
|
|
36
|
+
</>
|
|
37
|
+
) : (
|
|
38
|
+
<>
|
|
39
|
+
{' '}
|
|
40
|
+
Use <span className="text-foreground font-medium">View on site</span> for the real
|
|
41
|
+
design.
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
return (
|
|
15
50
|
<div
|
|
16
51
|
role="note"
|
|
@@ -18,9 +53,8 @@ export function StructureNotice({ structuralTypes }: { structuralTypes: string[]
|
|
|
18
53
|
>
|
|
19
54
|
<Puzzle className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
20
55
|
<span>
|
|
21
|
-
<span className="text-foreground font-medium">Structure
|
|
22
|
-
|
|
23
|
-
<span className="text-foreground font-medium">View on site</span> for the real design.
|
|
56
|
+
<span className="text-foreground font-medium">Structure preview</span> — use{' '}
|
|
57
|
+
<span className="text-foreground font-medium">Site preview</span> for accurate styling.
|
|
24
58
|
</span>
|
|
25
59
|
</div>
|
|
26
60
|
)
|
|
@@ -21,6 +21,8 @@ interface UseSitePreviewOptions {
|
|
|
21
21
|
saveState: SaveState
|
|
22
22
|
/** Persist the draft; resolves `false` when the save failed. */
|
|
23
23
|
saveDraft: () => Promise<boolean>
|
|
24
|
+
/** Open Site preview on load when the document is saved (has id + slug). */
|
|
25
|
+
defaultActive?: boolean
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface SitePreviewState {
|
|
@@ -49,12 +51,55 @@ export function useSitePreview({
|
|
|
49
51
|
dirty,
|
|
50
52
|
saveState,
|
|
51
53
|
saveDraft,
|
|
54
|
+
defaultActive = false,
|
|
52
55
|
}: UseSitePreviewOptions): SitePreviewState {
|
|
53
56
|
const [active, setActive] = useState(false)
|
|
54
57
|
const [url, setUrl] = useState<string | null>(null)
|
|
55
58
|
const [reloadKey, setReloadKey] = useState(0)
|
|
56
59
|
const tokenRef = useRef<string | null>(null)
|
|
57
60
|
const prevSaveState = useRef(saveState)
|
|
61
|
+
const autoOpenRef = useRef(false)
|
|
62
|
+
|
|
63
|
+
const activatePreview = useCallback(async () => {
|
|
64
|
+
if (!documentId || !slug) {
|
|
65
|
+
toast.error('Save the page before opening the site preview')
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
setActive(true)
|
|
69
|
+
setUrl(null)
|
|
70
|
+
try {
|
|
71
|
+
if (dirty) {
|
|
72
|
+
const ok = await saveDraft()
|
|
73
|
+
if (!ok) {
|
|
74
|
+
setActive(false)
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const { token } = await createPreviewToken(collection, documentId)
|
|
79
|
+
tokenRef.current = token
|
|
80
|
+
setUrl(
|
|
81
|
+
buildPublicPreviewUrl({
|
|
82
|
+
siteUrl: resolveSiteUrl(siteUrl),
|
|
83
|
+
urlPrefix: urlPrefix ?? '',
|
|
84
|
+
slug,
|
|
85
|
+
token,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
return true
|
|
89
|
+
} catch (err) {
|
|
90
|
+
toast.error(err instanceof Error ? err.message : 'Failed to open site preview')
|
|
91
|
+
setActive(false)
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
}, [collection, documentId, slug, urlPrefix, siteUrl, dirty, saveDraft])
|
|
95
|
+
|
|
96
|
+
// Opt-in default: open the iframe once a saved document is loaded.
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!defaultActive || autoOpenRef.current) return
|
|
99
|
+
if (!documentId || !slug) return
|
|
100
|
+
autoOpenRef.current = true
|
|
101
|
+
void activatePreview()
|
|
102
|
+
}, [defaultActive, documentId, slug, activatePreview])
|
|
58
103
|
|
|
59
104
|
// After a save completes while the preview is open, rebuild the URL (the
|
|
60
105
|
// slug may have changed in settings — the token is bound to the document
|
|
@@ -81,37 +126,8 @@ export function useSitePreview({
|
|
|
81
126
|
setActive(false)
|
|
82
127
|
return
|
|
83
128
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
setActive(true)
|
|
89
|
-
setUrl(null)
|
|
90
|
-
void (async () => {
|
|
91
|
-
try {
|
|
92
|
-
if (dirty) {
|
|
93
|
-
const ok = await saveDraft()
|
|
94
|
-
if (!ok) {
|
|
95
|
-
setActive(false)
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const { token } = await createPreviewToken(collection, documentId)
|
|
100
|
-
tokenRef.current = token
|
|
101
|
-
setUrl(
|
|
102
|
-
buildPublicPreviewUrl({
|
|
103
|
-
siteUrl: resolveSiteUrl(siteUrl),
|
|
104
|
-
urlPrefix: urlPrefix ?? '',
|
|
105
|
-
slug,
|
|
106
|
-
token,
|
|
107
|
-
}),
|
|
108
|
-
)
|
|
109
|
-
} catch (err) {
|
|
110
|
-
toast.error(err instanceof Error ? err.message : 'Failed to open site preview')
|
|
111
|
-
setActive(false)
|
|
112
|
-
}
|
|
113
|
-
})()
|
|
114
|
-
}, [active, collection, documentId, slug, urlPrefix, siteUrl, dirty, saveDraft])
|
|
129
|
+
void activatePreview()
|
|
130
|
+
}, [active, activatePreview])
|
|
115
131
|
|
|
116
132
|
return { active, url, reloadKey, toggle }
|
|
117
133
|
}
|
|
@@ -18,6 +18,8 @@ interface PostEditorCanvasProps {
|
|
|
18
18
|
onAddSection: () => void
|
|
19
19
|
/** Section types rendered as placeholders — shows the "Structure view" notice. */
|
|
20
20
|
structuralTypes?: string[]
|
|
21
|
+
/** Site preview toggle is available — show WYSIWYG nudge on the canvas. */
|
|
22
|
+
sitePreviewAvailable?: boolean
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -35,6 +37,7 @@ export function PostEditorCanvas({
|
|
|
35
37
|
onSelect,
|
|
36
38
|
onAddSection,
|
|
37
39
|
structuralTypes = [],
|
|
40
|
+
sitePreviewAvailable = false,
|
|
38
41
|
}: PostEditorCanvasProps) {
|
|
39
42
|
const vp = getViewport(viewport)
|
|
40
43
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -74,7 +77,10 @@ export function PostEditorCanvas({
|
|
|
74
77
|
|
|
75
78
|
return (
|
|
76
79
|
<div ref={containerRef} className="bg-muted h-full overflow-auto p-6">
|
|
77
|
-
<StructureNotice
|
|
80
|
+
<StructureNotice
|
|
81
|
+
structuralTypes={structuralTypes}
|
|
82
|
+
sitePreviewAvailable={sitePreviewAvailable}
|
|
83
|
+
/>
|
|
78
84
|
<div
|
|
79
85
|
className="mx-auto"
|
|
80
86
|
style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
|
|
@@ -48,6 +48,7 @@ import { SectionsPanel } from '../page-editor/SectionsPanel.js'
|
|
|
48
48
|
import { SectionInspector } from '../page-editor/SectionInspector.js'
|
|
49
49
|
import { AddSectionModal } from '../page-editor/AddSectionModal.js'
|
|
50
50
|
import { ValidationSummary } from '../page-editor/ValidationSummary.js'
|
|
51
|
+
import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
|
|
51
52
|
import { PostEditorCanvas } from './PostEditorCanvas.js'
|
|
52
53
|
import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'
|
|
53
54
|
|
|
@@ -63,6 +64,7 @@ interface EditorConfig {
|
|
|
63
64
|
| undefined
|
|
64
65
|
>
|
|
65
66
|
seo?: { siteUrl?: string }
|
|
67
|
+
admin?: { editor?: { defaultSitePreview?: boolean | { pages?: boolean; posts?: boolean } } }
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/** The slice of the admin session this editor reads. */
|
|
@@ -149,6 +151,13 @@ export function PostSectionEditor({
|
|
|
149
151
|
)
|
|
150
152
|
}, [config, postType])
|
|
151
153
|
|
|
154
|
+
const publishCheck = useMemo(
|
|
155
|
+
() => (post ? validatePost(post, true) : { errors: [''], warnings: [] }),
|
|
156
|
+
[post],
|
|
157
|
+
)
|
|
158
|
+
const publishReady = Boolean(post) && publishCheck.errors.length === 0
|
|
159
|
+
const publishDisabledReason = publishCheck.errors[0]
|
|
160
|
+
|
|
152
161
|
// ─── Load ──────────────────────────────────────────────────────────
|
|
153
162
|
useEffect(() => {
|
|
154
163
|
let cancelled = false
|
|
@@ -324,8 +333,11 @@ export function PostSectionEditor({
|
|
|
324
333
|
dirty,
|
|
325
334
|
saveState,
|
|
326
335
|
saveDraft: handleSaveDraft,
|
|
336
|
+
defaultActive: resolveDefaultSitePreview(config?.admin?.editor, 'posts'),
|
|
327
337
|
})
|
|
328
338
|
|
|
339
|
+
const sitePreviewAvailable = Boolean(post?.id && post?.slug)
|
|
340
|
+
|
|
329
341
|
const requestPublish = useCallback(() => {
|
|
330
342
|
if (!post) return
|
|
331
343
|
const result = validatePost(post, true)
|
|
@@ -499,6 +511,8 @@ export function PostSectionEditor({
|
|
|
499
511
|
publishing={publishing}
|
|
500
512
|
canEdit={canEdit}
|
|
501
513
|
canPublish={canPublish && canEdit}
|
|
514
|
+
publishReady={publishReady}
|
|
515
|
+
publishDisabledReason={publishDisabledReason}
|
|
502
516
|
viewport={viewport}
|
|
503
517
|
backLabel="Posts"
|
|
504
518
|
onViewport={setViewport}
|
|
@@ -570,6 +584,7 @@ export function PostSectionEditor({
|
|
|
570
584
|
onSelect={setSelectedId}
|
|
571
585
|
onAddSection={() => setAddOpen(true)}
|
|
572
586
|
structuralTypes={structuralTypes}
|
|
587
|
+
sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
|
|
573
588
|
/>
|
|
574
589
|
)}
|
|
575
590
|
</div>
|