@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
|
@@ -10,7 +10,16 @@ import { EditorRail, RailHeader } from '../../components/EditorRail.js'
|
|
|
10
10
|
import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/EditorRailTabs.js'
|
|
11
11
|
import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js'
|
|
12
12
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
13
|
-
import {
|
|
13
|
+
import { VersionHistoryPanel } from '../page-editor/VersionHistoryPanel.js'
|
|
14
|
+
import { ReviewBar } from '../page-editor/ReviewBar.js'
|
|
15
|
+
import {
|
|
16
|
+
fetchPageVersions,
|
|
17
|
+
restorePageVersion,
|
|
18
|
+
type PageVersion,
|
|
19
|
+
} from '../../lib/version-history.js'
|
|
20
|
+
import { changedSectionIds, diffVersions } from '../../lib/version-diff.js'
|
|
21
|
+
import { scoreBand, type PageAuditResult } from '@prenta/core/seo/page-audit'
|
|
22
|
+
import { usePageAudit } from '../../hooks/usePageAudit.js'
|
|
14
23
|
import {
|
|
15
24
|
emptyPostFromTemplate,
|
|
16
25
|
fetchPostForEditor,
|
|
@@ -20,14 +29,12 @@ import {
|
|
|
20
29
|
savePostDraft,
|
|
21
30
|
toggleSectionVisibility,
|
|
22
31
|
updateSectionContent,
|
|
23
|
-
updateSectionContentAtPath,
|
|
24
32
|
updateSectionMeta,
|
|
25
33
|
validatePost,
|
|
26
34
|
type EditorPost,
|
|
27
35
|
type PostHeaderConfig,
|
|
28
36
|
type ValidationResult,
|
|
29
37
|
} from '../../lib/post-editor-service.js'
|
|
30
|
-
import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
31
38
|
import { SharePreviewLinkDialog } from '../../components/SharePreviewLinkDialog.js'
|
|
32
39
|
import { defaultPostHeader } from '@prenta/core/page-sections'
|
|
33
40
|
import { openPublicSiteTab } from '../../lib/open-public-site-tab.js'
|
|
@@ -43,13 +50,25 @@ import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
|
|
|
43
50
|
import { SectionsList } from '../page-editor/SectionsPanel.js'
|
|
44
51
|
import { SectionEditForm } from '../page-editor/SectionEditForm.js'
|
|
45
52
|
import { ValidationSummary } from '../page-editor/ValidationSummary.js'
|
|
46
|
-
import { buildEditorContentHtml
|
|
53
|
+
import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
|
|
47
54
|
import { snapshotBaseline, type ReflowBaseline } from '../../lib/reflow-baseline.js'
|
|
48
55
|
import { ReflowContext } from '../page-editor/reflow-context.js'
|
|
49
56
|
import type { Zoom } from '../../lib/breakpoints.js'
|
|
50
|
-
import {
|
|
57
|
+
import { inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
|
|
51
58
|
import { PostEditorCanvas } from './PostEditorCanvas.js'
|
|
52
59
|
import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'
|
|
60
|
+
import { PostTab } from './PostTab.js'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The lower of the two audit scores — the tab dot should reflect the half that
|
|
64
|
+
* needs work, not an average that hides it. Same three-valued contract as the
|
|
65
|
+
* page editor: undefined = not reported yet, null = gated off, number = score.
|
|
66
|
+
*/
|
|
67
|
+
function weakestAuditScore(audit: PageAuditResult | null): number | null | undefined {
|
|
68
|
+
if (!audit) return undefined
|
|
69
|
+
const scores = [audit.seo.score, audit.geo.score].filter((s): s is number => s !== null)
|
|
70
|
+
return scores.length ? Math.min(...scores) : null
|
|
71
|
+
}
|
|
53
72
|
|
|
54
73
|
/** The slice of the resolved CMS config this editor reads. */
|
|
55
74
|
interface EditorConfig {
|
|
@@ -108,13 +127,6 @@ export function PostSectionEditor({
|
|
|
108
127
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
109
128
|
/** Canvas field click — focused after the inspector mounts for `selectedId`. */
|
|
110
129
|
const [pendingFocusPath, setPendingFocusPath] = useState<string | null>(null)
|
|
111
|
-
/** Bump to skip the next `prenta-preview:sections` echo after a canvas text edit. */
|
|
112
|
-
const [suppressSectionsEcho, setSuppressSectionsEcho] = useState(0)
|
|
113
|
-
/** Canvas image Replace — opens MediaPickerModal for this field path. */
|
|
114
|
-
const [canvasMediaTarget, setCanvasMediaTarget] = useState<{
|
|
115
|
-
sectionId: string
|
|
116
|
-
fieldPath: string
|
|
117
|
-
} | null>(null)
|
|
118
130
|
const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
|
|
119
131
|
const [zoom, setZoom] = useState<Zoom>('fit')
|
|
120
132
|
/** Scale the canvas measured — reported upward so the zoom chip shows a real %. */
|
|
@@ -136,6 +148,18 @@ export function PostSectionEditor({
|
|
|
136
148
|
const [railTab, setRailTab] = useState<RailTabId>('sections')
|
|
137
149
|
const seoOpen = railTab === 'seo'
|
|
138
150
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
151
|
+
const [versions, setVersions] = useState<PageVersion[]>([])
|
|
152
|
+
const [versionsLoading, setVersionsLoading] = useState(false)
|
|
153
|
+
const [versionsError, setVersionsError] = useState<string | null>(null)
|
|
154
|
+
/** Index into `versions`; 0 is the newest, which is the current content. */
|
|
155
|
+
const [versionIndex, setVersionIndex] = useState(0)
|
|
156
|
+
const [restoring, setRestoring] = useState(false)
|
|
157
|
+
/**
|
|
158
|
+
* Live audit reported by the SEO pane, built from unsaved form state. Null
|
|
159
|
+
* until that pane has mounted — `audit` below falls back to the saved-record
|
|
160
|
+
* audit so the header chip and tab dot always have a number.
|
|
161
|
+
*/
|
|
162
|
+
const [liveAudit, setLiveAudit] = useState<PageAuditResult | null>(null)
|
|
139
163
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
140
164
|
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
141
165
|
const [shareOpen, setShareOpen] = useState(false)
|
|
@@ -158,7 +182,7 @@ export function PostSectionEditor({
|
|
|
158
182
|
|
|
159
183
|
const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
|
|
160
184
|
const customRenderers = useAdminSectionRenderers()
|
|
161
|
-
const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl)
|
|
185
|
+
const { siteUrl: publicSiteUrl, matchesAdminOrigin } = usePublicSiteUrl(config?.seo?.siteUrl)
|
|
162
186
|
|
|
163
187
|
const typeLabel = useMemo(() => {
|
|
164
188
|
const col = config?.collections?.[postType]
|
|
@@ -188,6 +212,20 @@ export function PostSectionEditor({
|
|
|
188
212
|
)
|
|
189
213
|
const publishReady = Boolean(post) && publishCheck.errors.length === 0
|
|
190
214
|
|
|
215
|
+
const { audit: savedAudit } = usePageAudit({
|
|
216
|
+
entityType: 'post',
|
|
217
|
+
entityId: post?.id ?? null,
|
|
218
|
+
sections: post?.sections ?? [],
|
|
219
|
+
slug: post?.slug ?? '',
|
|
220
|
+
fallback: { title: post?.seoTitle, description: post?.seoDescription },
|
|
221
|
+
post: {
|
|
222
|
+
title: post?.title,
|
|
223
|
+
bodyHtml: post?.body,
|
|
224
|
+
featuredImage: post?.featuredImage,
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
const audit = liveAudit ?? savedAudit
|
|
228
|
+
|
|
191
229
|
const railTabs = useMemo<RailTab[]>(
|
|
192
230
|
() => [
|
|
193
231
|
{
|
|
@@ -196,13 +234,19 @@ export function PostSectionEditor({
|
|
|
196
234
|
title: 'Post content',
|
|
197
235
|
count: post?.sections.length ?? 0,
|
|
198
236
|
},
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
237
|
+
{
|
|
238
|
+
id: 'seo',
|
|
239
|
+
label: 'SEO & GEO',
|
|
240
|
+
title: 'Search & AI readiness',
|
|
241
|
+
score: weakestAuditScore(audit),
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'page',
|
|
245
|
+
label: 'Post',
|
|
246
|
+
title: 'Post settings',
|
|
247
|
+
},
|
|
204
248
|
],
|
|
205
|
-
[post?.sections.length],
|
|
249
|
+
[post?.sections.length, audit],
|
|
206
250
|
)
|
|
207
251
|
const activeRailTab = railTabs.find((tab) => tab.id === railTab) ?? railTabs[0]!
|
|
208
252
|
|
|
@@ -227,17 +271,6 @@ export function PostSectionEditor({
|
|
|
227
271
|
[canvasEl, selectedId, baseline, viewport],
|
|
228
272
|
)
|
|
229
273
|
|
|
230
|
-
const liveSeoScore = useMemo(() => {
|
|
231
|
-
if (!post) return 0
|
|
232
|
-
return computeLiveSeoScore({
|
|
233
|
-
title: post.title,
|
|
234
|
-
slug: post.slug,
|
|
235
|
-
seoTitle: post.seoTitle,
|
|
236
|
-
seoDescription: post.seoDescription,
|
|
237
|
-
sections: post.sections,
|
|
238
|
-
bodyHtml: post.body,
|
|
239
|
-
})
|
|
240
|
-
}, [post])
|
|
241
274
|
const publishDisabledReason = publishCheck.errors[0]
|
|
242
275
|
|
|
243
276
|
// ─── Load ──────────────────────────────────────────────────────────
|
|
@@ -446,23 +479,77 @@ export function PostSectionEditor({
|
|
|
446
479
|
}
|
|
447
480
|
}, [post, documentId, postType, onNavigate])
|
|
448
481
|
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
|
|
482
|
+
// ─── Version history ─────────────────────────────────────────────────
|
|
483
|
+
// History is a MODE, not a dialog: it overlays the rail and drives the
|
|
484
|
+
// canvas, so the version being reviewed is editor state rather than
|
|
485
|
+
// something owned by a panel that can be unmounted.
|
|
486
|
+
|
|
487
|
+
const loadVersions = useCallback(
|
|
488
|
+
async (postId: string) => {
|
|
489
|
+
setVersionsLoading(true)
|
|
490
|
+
setVersionsError(null)
|
|
491
|
+
try {
|
|
492
|
+
setVersions(await fetchPageVersions(postType, postId))
|
|
493
|
+
} catch (err) {
|
|
494
|
+
setVersions([])
|
|
495
|
+
setVersionsError(err instanceof Error ? err.message : 'Failed to load version history')
|
|
496
|
+
} finally {
|
|
497
|
+
setVersionsLoading(false)
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
[postType],
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
const openHistory = useCallback(() => {
|
|
452
504
|
if (!post?.id) return
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
505
|
+
setHistoryOpen(true)
|
|
506
|
+
setVersionIndex(0)
|
|
507
|
+
void loadVersions(post.id)
|
|
508
|
+
}, [post?.id, loadVersions])
|
|
509
|
+
|
|
510
|
+
const closeHistory = useCallback(() => {
|
|
511
|
+
setHistoryOpen(false)
|
|
512
|
+
setVersionIndex(0)
|
|
513
|
+
}, [])
|
|
514
|
+
|
|
515
|
+
const reviewing = historyOpen && versionIndex > 0
|
|
516
|
+
const reviewVersion = reviewing ? (versions[versionIndex] ?? null) : null
|
|
517
|
+
|
|
518
|
+
const reviewChanges = useMemo(
|
|
519
|
+
() =>
|
|
520
|
+
reviewVersion
|
|
521
|
+
? diffVersions(reviewVersion.sections, versions[versionIndex + 1]?.sections)
|
|
522
|
+
: [],
|
|
523
|
+
[reviewVersion, versions, versionIndex],
|
|
524
|
+
)
|
|
525
|
+
const reviewChangedIds = useMemo(() => changedSectionIds(reviewChanges), [reviewChanges])
|
|
526
|
+
|
|
527
|
+
const handleRestoreVersion = useCallback(
|
|
528
|
+
async (version: PageVersion) => {
|
|
529
|
+
if (!post?.id) return
|
|
530
|
+
setRestoring(true)
|
|
531
|
+
try {
|
|
532
|
+
await restorePageVersion(postType, post.id, version.id)
|
|
533
|
+
const restored = await fetchPostForEditor(postType, post.id)
|
|
534
|
+
acceptPost(restored)
|
|
456
535
|
setSelectedId(null)
|
|
457
|
-
setRailTab('sections')
|
|
458
536
|
setDirty(false)
|
|
459
537
|
setSaveState('idle')
|
|
460
538
|
setValidation(null)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
toast.
|
|
464
|
-
})
|
|
465
|
-
|
|
539
|
+
closeHistory()
|
|
540
|
+
void loadVersions(post.id)
|
|
541
|
+
toast.success('Version restored')
|
|
542
|
+
} catch (err) {
|
|
543
|
+
toast.error(err instanceof Error ? err.message : 'Failed to restore version')
|
|
544
|
+
} finally {
|
|
545
|
+
setRestoring(false)
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
[post?.id, postType, acceptPost, closeHistory, loadVersions],
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
const effectiveCanEdit = canEdit && !reviewing
|
|
552
|
+
const canRestoreVersion = canEdit
|
|
466
553
|
|
|
467
554
|
const handleSeoSaved = useCallback(() => {
|
|
468
555
|
if (!post?.id) return
|
|
@@ -485,32 +572,11 @@ export function PostSectionEditor({
|
|
|
485
572
|
setRailTab('sections')
|
|
486
573
|
setPendingFocusPath(fieldPath ?? null)
|
|
487
574
|
// Toggles, because selection is now disclosure — see the note on the page
|
|
488
|
-
// editor's copy. A `fieldPath` means
|
|
489
|
-
//
|
|
575
|
+
// editor's copy. A `fieldPath` means a canvas field click, which must
|
|
576
|
+
// open rather than toggle.
|
|
490
577
|
setSelectedId((current) => (fieldPath ? id : current === id ? null : id))
|
|
491
578
|
}, [])
|
|
492
579
|
|
|
493
|
-
const handleCanvasEdit = useCallback(
|
|
494
|
-
(edit: { sectionId: string; fieldPath: string; value: string }) => {
|
|
495
|
-
if (!canEdit) return
|
|
496
|
-
setSuppressSectionsEcho((n) => n + 1)
|
|
497
|
-
mutateSections((s) =>
|
|
498
|
-
updateSectionContentAtPath(s, edit.sectionId, edit.fieldPath, edit.value),
|
|
499
|
-
)
|
|
500
|
-
setSelectedId(edit.sectionId)
|
|
501
|
-
},
|
|
502
|
-
[canEdit, mutateSections],
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
const handleCanvasRequestMedia = useCallback(
|
|
506
|
-
(request: { sectionId: string; fieldPath: string }) => {
|
|
507
|
-
if (!canEdit) return
|
|
508
|
-
setSelectedId(request.sectionId)
|
|
509
|
-
setCanvasMediaTarget(request)
|
|
510
|
-
},
|
|
511
|
-
[canEdit],
|
|
512
|
-
)
|
|
513
|
-
|
|
514
580
|
// After a canvas field click, focus the matching inspector input once painted.
|
|
515
581
|
useEffect(() => {
|
|
516
582
|
if (!pendingFocusPath || !selectedId) return
|
|
@@ -620,8 +686,8 @@ export function PostSectionEditor({
|
|
|
620
686
|
dirty={dirty}
|
|
621
687
|
saveState={saveState}
|
|
622
688
|
publishing={publishing}
|
|
623
|
-
canEdit={
|
|
624
|
-
canPublish={canPublish &&
|
|
689
|
+
canEdit={effectiveCanEdit}
|
|
690
|
+
canPublish={canPublish && effectiveCanEdit}
|
|
625
691
|
publishReady={publishReady}
|
|
626
692
|
publishDisabledReason={publishDisabledReason}
|
|
627
693
|
viewport={viewport}
|
|
@@ -634,11 +700,16 @@ export function PostSectionEditor({
|
|
|
634
700
|
onViewOnSite={post.id && post.slug ? handleViewOnSite : undefined}
|
|
635
701
|
onShare={post.id ? handleShare : undefined}
|
|
636
702
|
hasPublicSiteUrl={Boolean(publicSiteUrl)}
|
|
637
|
-
|
|
703
|
+
matchesAdminOrigin={matchesAdminOrigin}
|
|
704
|
+
historyOpen={historyOpen}
|
|
705
|
+
onHistory={post.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined}
|
|
638
706
|
onOpenSettings={() => setFieldsOpen(true)}
|
|
639
707
|
onOpenSeo={handleToggleSeo}
|
|
640
708
|
seoOpen={seoOpen}
|
|
641
|
-
seoScore={
|
|
709
|
+
seoScore={audit?.seo.score ?? undefined}
|
|
710
|
+
seoScoreBand={
|
|
711
|
+
typeof audit?.seo.score === 'number' ? scoreBand(audit.seo.score) : undefined
|
|
712
|
+
}
|
|
642
713
|
onSaveDraft={handleSaveDraft}
|
|
643
714
|
onPublish={requestPublish}
|
|
644
715
|
onSchedule={post.id ? requestSchedule : undefined}
|
|
@@ -696,27 +767,44 @@ export function PostSectionEditor({
|
|
|
696
767
|
<SectionsList
|
|
697
768
|
sections={post.sections}
|
|
698
769
|
selectedId={selectedId}
|
|
699
|
-
canEdit={
|
|
770
|
+
canEdit={effectiveCanEdit}
|
|
700
771
|
onSelect={handleSelectSection}
|
|
701
772
|
onToggleVisibility={handleToggleVisibility}
|
|
702
|
-
renderEditor={
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
773
|
+
renderEditor={
|
|
774
|
+
reviewing
|
|
775
|
+
? undefined
|
|
776
|
+
: (section) => (
|
|
777
|
+
<SectionEditForm
|
|
778
|
+
section={section}
|
|
779
|
+
canEdit={effectiveCanEdit}
|
|
780
|
+
onContentChange={handleContentChange}
|
|
781
|
+
onMetaChange={handleMetaChange}
|
|
782
|
+
postBody={post.body}
|
|
783
|
+
onPostBodyChange={handlePostBodyChange}
|
|
784
|
+
/>
|
|
785
|
+
)
|
|
786
|
+
}
|
|
712
787
|
/>
|
|
788
|
+
) : railTab === 'page' ? (
|
|
789
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
790
|
+
<PostTab
|
|
791
|
+
post={post}
|
|
792
|
+
canEdit={effectiveCanEdit}
|
|
793
|
+
fields={config?.collections?.[postType]?.fields}
|
|
794
|
+
onChange={handleFields}
|
|
795
|
+
/>
|
|
796
|
+
</div>
|
|
713
797
|
) : (
|
|
714
798
|
<SeoEditorPane
|
|
715
799
|
entityType="post"
|
|
716
800
|
entityId={post.id}
|
|
717
801
|
slug={post.slug}
|
|
718
802
|
auditSections={post.sections}
|
|
803
|
+
onAuditChange={setLiveAudit}
|
|
719
804
|
contentHtml={seoContentHtml}
|
|
805
|
+
title={post.title}
|
|
806
|
+
bodyHtml={post.body}
|
|
807
|
+
featuredImage={post.featuredImage}
|
|
720
808
|
draftMeta={{
|
|
721
809
|
seoTitle: post.seoTitle,
|
|
722
810
|
seoDescription: post.seoDescription,
|
|
@@ -727,15 +815,39 @@ export function PostSectionEditor({
|
|
|
727
815
|
/>
|
|
728
816
|
)}
|
|
729
817
|
</div>
|
|
818
|
+
|
|
819
|
+
{historyOpen && (
|
|
820
|
+
<VersionHistoryPanel
|
|
821
|
+
versions={versions}
|
|
822
|
+
loading={versionsLoading}
|
|
823
|
+
error={versionsError}
|
|
824
|
+
selectedIndex={versionIndex}
|
|
825
|
+
onSelect={setVersionIndex}
|
|
826
|
+
onClose={closeHistory}
|
|
827
|
+
onRestore={handleRestoreVersion}
|
|
828
|
+
restoring={restoring}
|
|
829
|
+
canRestore={canRestoreVersion}
|
|
830
|
+
/>
|
|
831
|
+
)}
|
|
730
832
|
</>
|
|
731
833
|
)}
|
|
732
834
|
</EditorRail>
|
|
733
835
|
|
|
734
836
|
<div className="flex-1 overflow-hidden">
|
|
735
837
|
<PostEditorCanvas
|
|
736
|
-
sections={post.sections}
|
|
838
|
+
sections={reviewVersion ? reviewVersion.sections : post.sections}
|
|
737
839
|
header={header}
|
|
738
|
-
context={
|
|
840
|
+
context={
|
|
841
|
+
reviewVersion
|
|
842
|
+
? {
|
|
843
|
+
...context,
|
|
844
|
+
title: reviewVersion.title ?? context.title,
|
|
845
|
+
excerpt: reviewVersion.excerpt ?? context.excerpt,
|
|
846
|
+
body: reviewVersion.body ?? context.body,
|
|
847
|
+
featuredImageUrl: reviewVersion.featuredImage ?? context.featuredImageUrl,
|
|
848
|
+
}
|
|
849
|
+
: context
|
|
850
|
+
}
|
|
739
851
|
selectedId={selectedId}
|
|
740
852
|
viewport={viewport}
|
|
741
853
|
zoom={zoom}
|
|
@@ -743,26 +855,30 @@ export function PostSectionEditor({
|
|
|
743
855
|
onCanvasElement={setCanvasEl}
|
|
744
856
|
onSelect={handleSelectSection}
|
|
745
857
|
structuralTypes={structuralTypes}
|
|
858
|
+
reviewing={reviewing}
|
|
859
|
+
changedSectionIds={reviewChangedIds}
|
|
860
|
+
reviewBar={
|
|
861
|
+
reviewVersion ? (
|
|
862
|
+
<ReviewBar
|
|
863
|
+
version={reviewVersion}
|
|
864
|
+
changedCount={reviewChangedIds.size}
|
|
865
|
+
onRestore={() => void handleRestoreVersion(reviewVersion)}
|
|
866
|
+
onBackToCurrent={() => setVersionIndex(0)}
|
|
867
|
+
restoring={restoring}
|
|
868
|
+
canRestore={canRestoreVersion}
|
|
869
|
+
/>
|
|
870
|
+
) : null
|
|
871
|
+
}
|
|
746
872
|
/>
|
|
747
873
|
</div>
|
|
748
874
|
</div>
|
|
749
875
|
</ReflowContext.Provider>
|
|
750
876
|
</div>
|
|
751
877
|
|
|
752
|
-
{post.id && (
|
|
753
|
-
<VersionHistory
|
|
754
|
-
collectionSlug={postType}
|
|
755
|
-
documentId={post.id}
|
|
756
|
-
open={historyOpen}
|
|
757
|
-
onClose={() => setHistoryOpen(false)}
|
|
758
|
-
onRestore={handleRestored}
|
|
759
|
-
/>
|
|
760
|
-
)}
|
|
761
|
-
|
|
762
878
|
<PostFieldsModal
|
|
763
879
|
open={fieldsOpen}
|
|
764
880
|
post={post}
|
|
765
|
-
canEdit={
|
|
881
|
+
canEdit={effectiveCanEdit}
|
|
766
882
|
fields={config?.collections?.[postType]?.fields}
|
|
767
883
|
onClose={() => setFieldsOpen(false)}
|
|
768
884
|
onSave={handleFields}
|
|
@@ -817,29 +933,6 @@ export function PostSectionEditor({
|
|
|
817
933
|
cancelLabel="Stay"
|
|
818
934
|
destructive
|
|
819
935
|
/>
|
|
820
|
-
|
|
821
|
-
<MediaPickerModal
|
|
822
|
-
open={canvasMediaTarget !== null}
|
|
823
|
-
onClose={() => setCanvasMediaTarget(null)}
|
|
824
|
-
accept="image/*"
|
|
825
|
-
onSelectItem={(item) => {
|
|
826
|
-
if (!canvasMediaTarget) return
|
|
827
|
-
const { sectionId, fieldPath } = canvasMediaTarget
|
|
828
|
-
const { widthPath, heightPath } = companionDimensionPaths(fieldPath)
|
|
829
|
-
mutateSections((s) => {
|
|
830
|
-
let next = updateSectionContentAtPath(s, sectionId, fieldPath, item.url)
|
|
831
|
-
if (typeof item.width === 'number') {
|
|
832
|
-
next = updateSectionContentAtPath(next, sectionId, widthPath, item.width)
|
|
833
|
-
}
|
|
834
|
-
if (typeof item.height === 'number') {
|
|
835
|
-
next = updateSectionContentAtPath(next, sectionId, heightPath, item.height)
|
|
836
|
-
}
|
|
837
|
-
return next
|
|
838
|
-
})
|
|
839
|
-
setCanvasMediaTarget(null)
|
|
840
|
-
}}
|
|
841
|
-
onSelect={() => setCanvasMediaTarget(null)}
|
|
842
|
-
/>
|
|
843
936
|
</ErrorBoundary>
|
|
844
937
|
)
|
|
845
938
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { EditorPost } from '../../lib/post-editor-service.js'
|
|
5
|
+
import { PostFieldsForm, type PostFieldConfig } from './PostFieldsForm.js'
|
|
6
|
+
|
|
7
|
+
export interface PostTabProps {
|
|
8
|
+
post: EditorPost
|
|
9
|
+
canEdit: boolean
|
|
10
|
+
fields?: Record<string, PostFieldConfig | undefined>
|
|
11
|
+
onChange: (patch: Partial<EditorPost>) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rail tab 3 — post identity fields only.
|
|
16
|
+
*
|
|
17
|
+
* Title, slug, excerpt, featured image, category, and tags. Layout, header
|
|
18
|
+
* chrome, and template controls stay design-owned on the type template.
|
|
19
|
+
* Address / 301 / robots / author / danger-zone belong to pages, not posts.
|
|
20
|
+
*/
|
|
21
|
+
export function PostTab({ post, canEdit, fields, onChange }: PostTabProps) {
|
|
22
|
+
const [slugTouched, setSlugTouched] = useState(Boolean(post.slug))
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="p-3">
|
|
26
|
+
<PostFieldsForm
|
|
27
|
+
idPrefix="post"
|
|
28
|
+
values={{
|
|
29
|
+
title: post.title,
|
|
30
|
+
slug: post.slug,
|
|
31
|
+
excerpt: post.excerpt,
|
|
32
|
+
featuredImage: post.featuredImage,
|
|
33
|
+
category: post.category,
|
|
34
|
+
tags: post.tags,
|
|
35
|
+
}}
|
|
36
|
+
canEdit={canEdit}
|
|
37
|
+
fields={fields}
|
|
38
|
+
postType={post.postType}
|
|
39
|
+
bodyHtml={post.body}
|
|
40
|
+
slugTouched={slugTouched}
|
|
41
|
+
onSlugTouched={() => setSlugTouched(true)}
|
|
42
|
+
onChange={(patch) => onChange(patch)}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Upload,
|
|
14
14
|
X,
|
|
15
15
|
} from 'lucide-react'
|
|
16
|
+
import { siteUrlMatchesAdminOrigin } from '../../lib/preview-link.js'
|
|
16
17
|
import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
17
18
|
import { SEOConfigPanel } from '../../components/SEOConfigPanel.js'
|
|
18
19
|
import {
|
|
@@ -125,6 +126,26 @@ export function SeoSettingsTab({ onNavigate, role }: SeoSettingsTabProps) {
|
|
|
125
126
|
|
|
126
127
|
<SettingsValidationSummary issues={seo.issues} />
|
|
127
128
|
|
|
129
|
+
{!seo.siteUrl ? (
|
|
130
|
+
<div
|
|
131
|
+
role="status"
|
|
132
|
+
className="border-warning/30 bg-warning/10 text-warning flex items-start gap-2 rounded-lg border p-4 text-sm"
|
|
133
|
+
>
|
|
134
|
+
<AlertTriangle size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
|
|
135
|
+
Set the public site URL in Settings → General. View site and Share stay disabled until it
|
|
136
|
+
is set.
|
|
137
|
+
</div>
|
|
138
|
+
) : siteUrlMatchesAdminOrigin(seo.siteUrl) ? (
|
|
139
|
+
<div
|
|
140
|
+
role="status"
|
|
141
|
+
className="border-warning/30 bg-warning/10 text-warning flex items-start gap-2 rounded-lg border p-4 text-sm"
|
|
142
|
+
>
|
|
143
|
+
<AlertTriangle size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
|
|
144
|
+
This origin is the admin, not the public site. Set the public site URL to the live apex —
|
|
145
|
+
View site and Share stay disabled while it matches this CMS host.
|
|
146
|
+
</div>
|
|
147
|
+
) : null}
|
|
148
|
+
|
|
128
149
|
<MetaDefaultsCard seo={seo} canEdit={canEdit} />
|
|
129
150
|
<OrganizationCard seo={seo} canEdit={canEdit} />
|
|
130
151
|
<SearchEngineVerificationCard seo={seo} canEdit={canEdit} />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useId } from 'react'
|
|
4
4
|
import { CheckCircle2 } from 'lucide-react'
|
|
5
|
+
import { siteUrlMatchesAdminOrigin } from '../../lib/preview-link.js'
|
|
5
6
|
import { SettingsCard, SettingsSourceBadge } from './components.js'
|
|
6
7
|
import type {
|
|
7
8
|
GeneralSettingsForm,
|
|
@@ -37,6 +38,7 @@ export function SiteInformationCard({
|
|
|
37
38
|
const urlIssue = issues.find((i) => i.field === 'siteUrl')
|
|
38
39
|
const urlEditable = sources.siteUrl?.editable !== false && canEdit
|
|
39
40
|
const urlValid = !urlIssue && form.siteUrl.trim().length > 0
|
|
41
|
+
const urlIsAdminOrigin = siteUrlMatchesAdminOrigin(form.siteUrl)
|
|
40
42
|
|
|
41
43
|
return (
|
|
42
44
|
<SettingsCard title="Site Information" description="Your site's name, tagline, and public URL.">
|
|
@@ -99,6 +101,11 @@ export function SiteInformationCard({
|
|
|
99
101
|
>
|
|
100
102
|
{urlIssue.message}
|
|
101
103
|
</p>
|
|
104
|
+
) : urlIsAdminOrigin ? (
|
|
105
|
+
<p role="status" className="text-warning mt-1 text-sm">
|
|
106
|
+
This origin is the admin, not the public site. Set the public apex (Astro or the live
|
|
107
|
+
Next site), not this CMS host — View site and Share will stay disabled until you do.
|
|
108
|
+
</p>
|
|
102
109
|
) : urlValid ? (
|
|
103
110
|
<p className="text-muted-foreground mt-1 flex items-center gap-1.5 text-sm">
|
|
104
111
|
<CheckCircle2 size={13} className="text-success" aria-hidden="true" />
|