@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
|
@@ -33,6 +33,20 @@ export interface AuditMetaSource {
|
|
|
33
33
|
hasOgImage: boolean
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Post identity the public template renders outside the section list.
|
|
38
|
+
*
|
|
39
|
+
* The audit engine only reads `sections`. Posts put the H1, opening prose, and
|
|
40
|
+
* featured image on the document itself, so both audit callers (saved record
|
|
41
|
+
* and live SEO pane) fold these into synthetic sections here — one mapping,
|
|
42
|
+
* so the header chip and the rings cannot disagree.
|
|
43
|
+
*/
|
|
44
|
+
export interface PostAuditExtras {
|
|
45
|
+
title?: string
|
|
46
|
+
bodyHtml?: string
|
|
47
|
+
featuredImage?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
export interface BuildAuditInputArgs {
|
|
37
51
|
sections: PageSection[]
|
|
38
52
|
meta: AuditMetaSource
|
|
@@ -41,6 +55,57 @@ export interface BuildAuditInputArgs {
|
|
|
41
55
|
record: Pick<SeoContentRecord, 'title' | 'siteName' | 'titleTemplate' | 'url'> | null
|
|
42
56
|
/** Fallback title when there is no saved record yet. */
|
|
43
57
|
fallbackTitle?: string
|
|
58
|
+
/** Post-only slots prepended so the same engine sees title / body / image. */
|
|
59
|
+
post?: PostAuditExtras
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const POST_TITLE_SECTION_ID = '__post-title'
|
|
63
|
+
const POST_IMAGE_SECTION_ID = '__post-featured-image'
|
|
64
|
+
|
|
65
|
+
function stripHtml(html: string): string {
|
|
66
|
+
return html
|
|
67
|
+
.replace(/<[^>]*>/g, ' ')
|
|
68
|
+
.replace(/\s+/g, ' ')
|
|
69
|
+
.trim()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Prepend the post's own title, body, and featured image as sections the
|
|
74
|
+
* audit already knows how to read (H1, opening prose, image slot).
|
|
75
|
+
*
|
|
76
|
+
* A `text` section carries title + body so we do not invent a phantom hero
|
|
77
|
+
* image slot. The featured image is a separate `feature` section, and only
|
|
78
|
+
* when one is set — otherwise `imageSlots` would count an empty media field
|
|
79
|
+
* and fail alt-text for an image that is not on the page.
|
|
80
|
+
*/
|
|
81
|
+
export function withPostAuditSlots(sections: PageSection[], post?: PostAuditExtras): PageSection[] {
|
|
82
|
+
if (!post) return sections
|
|
83
|
+
|
|
84
|
+
const extras: PageSection[] = []
|
|
85
|
+
const title = post.title?.trim() ?? ''
|
|
86
|
+
const body = post.bodyHtml ? stripHtml(post.bodyHtml) : ''
|
|
87
|
+
if (title || body) {
|
|
88
|
+
extras.push({
|
|
89
|
+
id: POST_TITLE_SECTION_ID,
|
|
90
|
+
sectionType: 'text',
|
|
91
|
+
name: 'Post',
|
|
92
|
+
visible: true,
|
|
93
|
+
content: { heading: title, body },
|
|
94
|
+
settings: {},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
const image = post.featuredImage?.trim() ?? ''
|
|
98
|
+
if (image) {
|
|
99
|
+
extras.push({
|
|
100
|
+
id: POST_IMAGE_SECTION_ID,
|
|
101
|
+
sectionType: 'feature',
|
|
102
|
+
name: 'Featured image',
|
|
103
|
+
visible: true,
|
|
104
|
+
content: { imageUrl: image, imageAlt: '' },
|
|
105
|
+
settings: {},
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
return extras.length ? [...extras, ...sections] : sections
|
|
44
109
|
}
|
|
45
110
|
|
|
46
111
|
export function buildPageAuditInput({
|
|
@@ -49,6 +114,7 @@ export function buildPageAuditInput({
|
|
|
49
114
|
slug,
|
|
50
115
|
record,
|
|
51
116
|
fallbackTitle,
|
|
117
|
+
post,
|
|
52
118
|
}: BuildAuditInputArgs): PageAuditInput {
|
|
53
119
|
// Measure the title a searcher actually sees — the page meta title with the
|
|
54
120
|
// site's template applied — not the raw field. Checking the raw field would
|
|
@@ -65,7 +131,7 @@ export function buildPageAuditInput({
|
|
|
65
131
|
: meta.metaTitle.trim() || fallbackTitle || ''
|
|
66
132
|
|
|
67
133
|
return {
|
|
68
|
-
sections,
|
|
134
|
+
sections: withPostAuditSlots(sections, post),
|
|
69
135
|
seo: {
|
|
70
136
|
title: renderedTitle,
|
|
71
137
|
description: meta.metaDescription,
|
|
@@ -1,140 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Next editing canvas (iframe). Namespaced with colons so it does not collide
|
|
4
|
-
* with the legacy `prenta-preview-update` DocumentEdit bus.
|
|
2
|
+
* Field-path helpers shared by the editor canvas and the inspector.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
4
|
+
* The old `prenta-preview:*` postMessage protocol is gone — View site opens a
|
|
5
|
+
* normal tab and does not talk back. `data-cms-field` on first-party section
|
|
6
|
+
* renderers still feeds the reflow guard and canvas field-focus.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export const PREVIEW_BRIDGE_VERSION = 1 as const
|
|
10
|
-
|
|
11
|
-
/** Debounce for live section pushes while typing in the inspector. */
|
|
12
|
-
export const PREVIEW_SECTIONS_DEBOUNCE_MS = 120
|
|
13
|
-
|
|
14
|
-
export type PreviewBridgeMessageType =
|
|
15
|
-
| 'prenta-preview:ready'
|
|
16
|
-
| 'prenta-preview:select'
|
|
17
|
-
| 'prenta-preview:highlight'
|
|
18
|
-
| 'prenta-preview:sections'
|
|
19
|
-
| 'prenta-preview:mode'
|
|
20
|
-
| 'prenta-preview:edit'
|
|
21
|
-
| 'prenta-preview:request-media'
|
|
22
|
-
|
|
23
|
-
export interface PreviewBridgeEnvelope {
|
|
24
|
-
type: PreviewBridgeMessageType
|
|
25
|
-
v: typeof PREVIEW_BRIDGE_VERSION
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface PreviewReadyMessage extends PreviewBridgeEnvelope {
|
|
29
|
-
type: 'prenta-preview:ready'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface PreviewSelectMessage extends PreviewBridgeEnvelope {
|
|
33
|
-
type: 'prenta-preview:select'
|
|
34
|
-
sectionId: string
|
|
35
|
-
fieldPath?: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface PreviewHighlightMessage extends PreviewBridgeEnvelope {
|
|
39
|
-
type: 'prenta-preview:highlight'
|
|
40
|
-
sectionId: string | null
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Opaque JSON object on the wire. Uses `object` (not `Record<string, unknown>`)
|
|
45
|
-
* so concrete editor interfaces like `SectionSettings` remain assignable.
|
|
46
|
-
*/
|
|
47
|
-
export type PreviewObjectPayload = object
|
|
48
|
-
|
|
49
|
-
/** Wire shape for live canvas updates — matches `PublicSection` / editor sections. */
|
|
50
|
-
export interface PreviewSectionPayload {
|
|
51
|
-
id: string
|
|
52
|
-
sectionType: string
|
|
53
|
-
name?: string
|
|
54
|
-
visible?: boolean
|
|
55
|
-
content?: PreviewObjectPayload
|
|
56
|
-
settings?: PreviewObjectPayload
|
|
57
|
-
unmanaged?: boolean
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface PreviewSectionsMessage extends PreviewBridgeEnvelope {
|
|
61
|
-
type: 'prenta-preview:sections'
|
|
62
|
-
sections: PreviewSectionPayload[]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Admin → canvas: enable/disable inline editing affordances. */
|
|
66
|
-
export interface PreviewModeMessage extends PreviewBridgeEnvelope {
|
|
67
|
-
type: 'prenta-preview:mode'
|
|
68
|
-
editing: boolean
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Canvas → admin: text field edit from contenteditable. */
|
|
72
|
-
export interface PreviewEditMessage extends PreviewBridgeEnvelope {
|
|
73
|
-
type: 'prenta-preview:edit'
|
|
74
|
-
sectionId: string
|
|
75
|
-
fieldPath: string
|
|
76
|
-
value: string
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Canvas → admin: open media picker for an image field. */
|
|
80
|
-
export interface PreviewRequestMediaMessage extends PreviewBridgeEnvelope {
|
|
81
|
-
type: 'prenta-preview:request-media'
|
|
82
|
-
sectionId: string
|
|
83
|
-
fieldPath: string
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export type PreviewBridgeMessage =
|
|
87
|
-
| PreviewReadyMessage
|
|
88
|
-
| PreviewSelectMessage
|
|
89
|
-
| PreviewHighlightMessage
|
|
90
|
-
| PreviewSectionsMessage
|
|
91
|
-
| PreviewModeMessage
|
|
92
|
-
| PreviewEditMessage
|
|
93
|
-
| PreviewRequestMediaMessage
|
|
94
|
-
|
|
95
|
-
export function isPreviewBridgeMessage(data: unknown): data is PreviewBridgeMessage {
|
|
96
|
-
if (!data || typeof data !== 'object') return false
|
|
97
|
-
const msg = data as Record<string, unknown>
|
|
98
|
-
if (msg.v !== PREVIEW_BRIDGE_VERSION) return false
|
|
99
|
-
if (typeof msg.type !== 'string' || !msg.type.startsWith('prenta-preview:')) return false
|
|
100
|
-
return true
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function isPreviewEditMessage(data: unknown): data is PreviewEditMessage {
|
|
104
|
-
if (!isPreviewBridgeMessage(data) || data.type !== 'prenta-preview:edit') return false
|
|
105
|
-
return (
|
|
106
|
-
typeof data.sectionId === 'string' &&
|
|
107
|
-
data.sectionId.length > 0 &&
|
|
108
|
-
typeof data.fieldPath === 'string' &&
|
|
109
|
-
data.fieldPath.length > 0 &&
|
|
110
|
-
typeof data.value === 'string'
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function isPreviewRequestMediaMessage(data: unknown): data is PreviewRequestMediaMessage {
|
|
115
|
-
if (!isPreviewBridgeMessage(data) || data.type !== 'prenta-preview:request-media') return false
|
|
116
|
-
return (
|
|
117
|
-
typeof data.sectionId === 'string' &&
|
|
118
|
-
data.sectionId.length > 0 &&
|
|
119
|
-
typeof data.fieldPath === 'string' &&
|
|
120
|
-
data.fieldPath.length > 0
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function isPreviewModeMessage(data: unknown): data is PreviewModeMessage {
|
|
125
|
-
if (!isPreviewBridgeMessage(data) || data.type !== 'prenta-preview:mode') return false
|
|
126
|
-
return typeof data.editing === 'boolean'
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function canvasOriginFromSrc(src: string | null | undefined): string | null {
|
|
130
|
-
if (!src) return null
|
|
131
|
-
try {
|
|
132
|
-
return new URL(src, typeof window !== 'undefined' ? window.location.origin : undefined).origin
|
|
133
|
-
} catch {
|
|
134
|
-
return null
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
9
|
/**
|
|
139
10
|
* Map a dotted content field path (`lead`, `stats.0.num`) to the inspector
|
|
140
11
|
* input id (`insp-field-lead`, `insp-field-stats-0-num`). Same string shape
|
|
@@ -181,29 +52,12 @@ export function companionDimensionPaths(fieldPath: string): {
|
|
|
181
52
|
}
|
|
182
53
|
|
|
183
54
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
55
|
+
* Read `data-cms-field` from a canvas click target (or its ancestor).
|
|
56
|
+
* Used so a field click opens the accordion on the matching inspector input.
|
|
186
57
|
*/
|
|
187
|
-
export function
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
// Gallery rows use `images.N.url` (not link CTAs).
|
|
193
|
-
return leaf === 'url' && fieldPath.includes('images.')
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Strip editor chrome to the public wire shape before posting to the canvas. */
|
|
197
|
-
export function toPreviewSectionPayload(
|
|
198
|
-
section: PreviewSectionPayload & { internalLabel?: string },
|
|
199
|
-
): PreviewSectionPayload {
|
|
200
|
-
return {
|
|
201
|
-
id: section.id,
|
|
202
|
-
sectionType: section.sectionType,
|
|
203
|
-
name: section.name,
|
|
204
|
-
visible: section.visible,
|
|
205
|
-
content: section.content ?? {},
|
|
206
|
-
settings: section.settings ?? {},
|
|
207
|
-
unmanaged: section.unmanaged,
|
|
208
|
-
}
|
|
58
|
+
export function fieldPathFromEventTarget(target: EventTarget | null): string | undefined {
|
|
59
|
+
if (!(target instanceof Element)) return undefined
|
|
60
|
+
const field = target.closest('[data-cms-field]')
|
|
61
|
+
const path = field?.getAttribute('data-cms-field')?.trim()
|
|
62
|
+
return path || undefined
|
|
209
63
|
}
|
|
@@ -35,6 +35,11 @@ export interface PageVersion {
|
|
|
35
35
|
author: VersionAuthor
|
|
36
36
|
/** Full page content as of this version — what the canvas renders. */
|
|
37
37
|
sections: PageSection[]
|
|
38
|
+
/** Post identity fields, when the snapshot is a post. */
|
|
39
|
+
title?: string
|
|
40
|
+
excerpt?: string
|
|
41
|
+
featuredImage?: string
|
|
42
|
+
body?: string
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/** Versions sharing a calendar day, newest day first. */
|
|
@@ -87,6 +92,20 @@ function toAuthor(row: VersionApiRow): VersionAuthor {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
function optionalString(value: unknown): string | undefined {
|
|
96
|
+
return typeof value === 'string' ? value : undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function optionalFeaturedImage(value: unknown): string | undefined {
|
|
100
|
+
if (typeof value === 'string') return value
|
|
101
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
102
|
+
const obj = value as Record<string, unknown>
|
|
103
|
+
const url = obj.url ?? obj.publicUrl ?? obj.src
|
|
104
|
+
if (typeof url === 'string') return url
|
|
105
|
+
}
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
export function toPageVersion(row: VersionApiRow): PageVersion {
|
|
91
110
|
const data = row.data ?? {}
|
|
92
111
|
return {
|
|
@@ -98,6 +117,10 @@ export function toPageVersion(row: VersionApiRow): PageVersion {
|
|
|
98
117
|
// snapshot more strictly than the current page would make an unmanaged
|
|
99
118
|
// section look like it was deleted in every version that contains one.
|
|
100
119
|
sections: coerceSections(data.sections, { onUnknown: 'preserve' }),
|
|
120
|
+
title: optionalString(data.title),
|
|
121
|
+
excerpt: optionalString(data.excerpt),
|
|
122
|
+
featuredImage: optionalFeaturedImage(data.featuredImage),
|
|
123
|
+
body: optionalString(data.body),
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
|
|
@@ -5,11 +5,16 @@ export type ViewSiteIntent = 'preview-draft' | 'view-live'
|
|
|
5
5
|
export const NO_PUBLIC_SITE_URL_REASON =
|
|
6
6
|
'Set the public site URL in Settings → SEO to open the live design.'
|
|
7
7
|
|
|
8
|
+
export const ADMIN_ORIGIN_SITE_URL_REASON =
|
|
9
|
+
'This origin is the admin, not the public site. Set the public site URL in Settings → SEO.'
|
|
10
|
+
|
|
8
11
|
export function resolveViewSiteChrome(input: {
|
|
9
12
|
status: PageStatus
|
|
10
13
|
dirty: boolean
|
|
11
14
|
hasPublicSiteUrl: boolean
|
|
12
15
|
hasIdentity: boolean
|
|
16
|
+
/** True when the configured site URL is the CMS host (Astro-split misconfig). */
|
|
17
|
+
matchesAdminOrigin?: boolean
|
|
13
18
|
}): {
|
|
14
19
|
viewEnabled: boolean
|
|
15
20
|
viewDisabledReason?: string
|
|
@@ -21,12 +26,19 @@ export function resolveViewSiteChrome(input: {
|
|
|
21
26
|
const primaryIntent: ViewSiteIntent =
|
|
22
27
|
input.status === 'PUBLISHED' && !input.dirty ? 'view-live' : 'preview-draft'
|
|
23
28
|
const shareVisible = input.hasIdentity && unpublished
|
|
24
|
-
const
|
|
29
|
+
const usablePublicUrl = input.hasPublicSiteUrl && !input.matchesAdminOrigin
|
|
30
|
+
const viewEnabled = input.hasIdentity && usablePublicUrl
|
|
31
|
+
|
|
32
|
+
let viewDisabledReason: string | undefined
|
|
33
|
+
if (input.hasIdentity && !input.hasPublicSiteUrl) {
|
|
34
|
+
viewDisabledReason = NO_PUBLIC_SITE_URL_REASON
|
|
35
|
+
} else if (input.hasIdentity && input.matchesAdminOrigin) {
|
|
36
|
+
viewDisabledReason = ADMIN_ORIGIN_SITE_URL_REASON
|
|
37
|
+
}
|
|
25
38
|
|
|
26
39
|
return {
|
|
27
40
|
viewEnabled,
|
|
28
|
-
viewDisabledReason
|
|
29
|
-
input.hasIdentity && !input.hasPublicSiteUrl ? NO_PUBLIC_SITE_URL_REASON : undefined,
|
|
41
|
+
viewDisabledReason,
|
|
30
42
|
primaryIntent,
|
|
31
43
|
showViewLive: input.hasIdentity && input.status === 'PUBLISHED' && input.dirty,
|
|
32
44
|
shareVisible,
|
|
@@ -13,7 +13,7 @@ interface EditorCanvasProps {
|
|
|
13
13
|
selectedId: string | null
|
|
14
14
|
breakpoint: BreakpointId
|
|
15
15
|
zoom?: Zoom
|
|
16
|
-
onSelect: (id: string) => void
|
|
16
|
+
onSelect: (id: string, fieldPath?: string) => void
|
|
17
17
|
onScaleChange?: (scale: number) => void
|
|
18
18
|
onCanvasElement?: (el: HTMLElement | null) => void
|
|
19
19
|
/** Section types rendered as placeholders — shows the "Structure view" notice. */
|
|
@@ -55,6 +55,8 @@ interface EditorTopBarProps {
|
|
|
55
55
|
onShare?: () => void
|
|
56
56
|
/** When false, View site and Share are disabled with the SEO settings reason. */
|
|
57
57
|
hasPublicSiteUrl?: boolean
|
|
58
|
+
/** When true, View site and Share stay disabled — the URL is the CMS host. */
|
|
59
|
+
matchesAdminOrigin?: boolean
|
|
58
60
|
/**
|
|
59
61
|
* Toggle the version history panel. When omitted the History button is
|
|
60
62
|
* hidden — e.g. for unsaved documents that have no versions yet.
|
|
@@ -122,6 +124,7 @@ export function EditorTopBar({
|
|
|
122
124
|
onViewOnSite,
|
|
123
125
|
onShare,
|
|
124
126
|
hasPublicSiteUrl,
|
|
127
|
+
matchesAdminOrigin,
|
|
125
128
|
onHistory,
|
|
126
129
|
historyOpen = false,
|
|
127
130
|
onOpenSettings,
|
|
@@ -142,6 +145,7 @@ export function EditorTopBar({
|
|
|
142
145
|
status,
|
|
143
146
|
dirty,
|
|
144
147
|
hasPublicSiteUrl: Boolean(hasPublicSiteUrl),
|
|
148
|
+
matchesAdminOrigin: Boolean(matchesAdminOrigin),
|
|
145
149
|
hasIdentity: true,
|
|
146
150
|
})
|
|
147
151
|
: null
|
|
@@ -227,8 +231,8 @@ export function EditorTopBar({
|
|
|
227
231
|
<button
|
|
228
232
|
type="button"
|
|
229
233
|
onClick={onOpenSettings}
|
|
230
|
-
aria-label=
|
|
231
|
-
title=
|
|
234
|
+
aria-label={backLabel === 'Posts' ? 'Post settings' : 'Page settings'}
|
|
235
|
+
title={backLabel === 'Posts' ? 'Post settings' : 'Page settings'}
|
|
232
236
|
className="prenta-touch-target text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
|
|
233
237
|
>
|
|
234
238
|
<Settings2 className="h-4 w-4" aria-hidden />
|
|
@@ -27,13 +27,11 @@ import {
|
|
|
27
27
|
savePageDraft,
|
|
28
28
|
toggleSectionVisibility,
|
|
29
29
|
updateSectionContent,
|
|
30
|
-
updateSectionContentAtPath,
|
|
31
30
|
updateSectionMeta,
|
|
32
31
|
validatePage,
|
|
33
32
|
type EditorPage,
|
|
34
33
|
type ValidationResult,
|
|
35
34
|
} from '../../lib/page-editor-service.js'
|
|
36
|
-
import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
37
35
|
import { SharePreviewLinkDialog } from '../../components/SharePreviewLinkDialog.js'
|
|
38
36
|
import { openPublicSiteTab } from '../../lib/open-public-site-tab.js'
|
|
39
37
|
import type { ViewSiteIntent } from '../../lib/view-site-chrome.js'
|
|
@@ -61,7 +59,7 @@ import { SectionEditForm } from './SectionEditForm.js'
|
|
|
61
59
|
import { PageSettingsModal } from './PageSettingsModal.js'
|
|
62
60
|
import { ValidationSummary } from './ValidationSummary.js'
|
|
63
61
|
import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
|
|
64
|
-
import {
|
|
62
|
+
import { inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
|
|
65
63
|
|
|
66
64
|
interface PageSectionEditorProps {
|
|
67
65
|
/** undefined when creating a new page (`/pages/new`) */
|
|
@@ -134,13 +132,6 @@ export function PageSectionEditor({
|
|
|
134
132
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
135
133
|
/** Canvas field click — focused after the inspector mounts for `selectedId`. */
|
|
136
134
|
const [pendingFocusPath, setPendingFocusPath] = useState<string | null>(null)
|
|
137
|
-
/** Bump to skip the next `prenta-preview:sections` echo after a canvas text edit. */
|
|
138
|
-
const [suppressSectionsEcho, setSuppressSectionsEcho] = useState(0)
|
|
139
|
-
/** Canvas image Replace — opens MediaPickerModal for this field path. */
|
|
140
|
-
const [canvasMediaTarget, setCanvasMediaTarget] = useState<{
|
|
141
|
-
sectionId: string
|
|
142
|
-
fieldPath: string
|
|
143
|
-
} | null>(null)
|
|
144
135
|
const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
|
|
145
136
|
const [zoom, setZoom] = useState<Zoom>('fit')
|
|
146
137
|
/**
|
|
@@ -227,7 +218,7 @@ export function PageSectionEditor({
|
|
|
227
218
|
|
|
228
219
|
const { canEdit, canPublish, canCreateRedirect } = useMemo(() => deriveCan(session), [session])
|
|
229
220
|
const customRenderers = useAdminSectionRenderers()
|
|
230
|
-
const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl)
|
|
221
|
+
const { siteUrl: publicSiteUrl, matchesAdminOrigin } = usePublicSiteUrl(config?.seo?.siteUrl)
|
|
231
222
|
|
|
232
223
|
// A misconfigured `pages` collection (missing the fields the editor
|
|
233
224
|
// writes) would silently drop content on save — detect it up front.
|
|
@@ -716,39 +707,14 @@ export function PageSectionEditor({
|
|
|
716
707
|
// rail has no close affordance at all — the chevron and the row both
|
|
717
708
|
// re-select what is already selected and nothing happens.
|
|
718
709
|
//
|
|
719
|
-
// Except when a fieldPath came with it:
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
// user is trying to edit.
|
|
710
|
+
// Except when a fieldPath came with it: a canvas field click must open
|
|
711
|
+
// and focus that input. Toggling would close the form on the second
|
|
712
|
+
// click of a field the user is trying to inspect.
|
|
723
713
|
if (fieldPath) return id
|
|
724
714
|
return current === id ? null : id
|
|
725
715
|
})
|
|
726
716
|
}, [])
|
|
727
717
|
|
|
728
|
-
// `effectiveCanEdit`, not `canEdit`: the site preview is a second editing
|
|
729
|
-
// surface, so a role check alone let it write through both the version-review
|
|
730
|
-
// lock and the builder-owned lock the rest of the editor honours.
|
|
731
|
-
const handleCanvasEdit = useCallback(
|
|
732
|
-
(edit: { sectionId: string; fieldPath: string; value: string }) => {
|
|
733
|
-
if (!effectiveCanEdit) return
|
|
734
|
-
setSuppressSectionsEcho((n) => n + 1)
|
|
735
|
-
mutateSections((s) =>
|
|
736
|
-
updateSectionContentAtPath(s, edit.sectionId, edit.fieldPath, edit.value),
|
|
737
|
-
)
|
|
738
|
-
setSelectedId(edit.sectionId)
|
|
739
|
-
},
|
|
740
|
-
[effectiveCanEdit, mutateSections],
|
|
741
|
-
)
|
|
742
|
-
|
|
743
|
-
const handleCanvasRequestMedia = useCallback(
|
|
744
|
-
(request: { sectionId: string; fieldPath: string }) => {
|
|
745
|
-
if (!effectiveCanEdit) return
|
|
746
|
-
setSelectedId(request.sectionId)
|
|
747
|
-
setCanvasMediaTarget(request)
|
|
748
|
-
},
|
|
749
|
-
[effectiveCanEdit],
|
|
750
|
-
)
|
|
751
|
-
|
|
752
718
|
// After a canvas field click, focus the matching inspector input once painted.
|
|
753
719
|
useEffect(() => {
|
|
754
720
|
if (!pendingFocusPath || !selectedId) return
|
|
@@ -878,6 +844,7 @@ export function PageSectionEditor({
|
|
|
878
844
|
onViewOnSite={page.id && page.slug ? handleViewOnSite : undefined}
|
|
879
845
|
onShare={page.id ? handleShare : undefined}
|
|
880
846
|
hasPublicSiteUrl={Boolean(publicSiteUrl)}
|
|
847
|
+
matchesAdminOrigin={matchesAdminOrigin}
|
|
881
848
|
historyOpen={historyOpen}
|
|
882
849
|
onHistory={page.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined}
|
|
883
850
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
@@ -1114,30 +1081,6 @@ export function PageSectionEditor({
|
|
|
1114
1081
|
cancelLabel="Stay"
|
|
1115
1082
|
destructive
|
|
1116
1083
|
/>
|
|
1117
|
-
|
|
1118
|
-
<MediaPickerModal
|
|
1119
|
-
open={canvasMediaTarget !== null}
|
|
1120
|
-
onClose={() => setCanvasMediaTarget(null)}
|
|
1121
|
-
accept="image/*"
|
|
1122
|
-
onSelectItem={(item) => {
|
|
1123
|
-
if (!canvasMediaTarget) return
|
|
1124
|
-
const { sectionId, fieldPath } = canvasMediaTarget
|
|
1125
|
-
const { widthPath, heightPath } = companionDimensionPaths(fieldPath)
|
|
1126
|
-
// Media Replace must echo sections so the canvas image updates.
|
|
1127
|
-
mutateSections((s) => {
|
|
1128
|
-
let next = updateSectionContentAtPath(s, sectionId, fieldPath, item.url)
|
|
1129
|
-
if (typeof item.width === 'number') {
|
|
1130
|
-
next = updateSectionContentAtPath(next, sectionId, widthPath, item.width)
|
|
1131
|
-
}
|
|
1132
|
-
if (typeof item.height === 'number') {
|
|
1133
|
-
next = updateSectionContentAtPath(next, sectionId, heightPath, item.height)
|
|
1134
|
-
}
|
|
1135
|
-
return next
|
|
1136
|
-
})
|
|
1137
|
-
setCanvasMediaTarget(null)
|
|
1138
|
-
}}
|
|
1139
|
-
onSelect={() => setCanvasMediaTarget(null)}
|
|
1140
|
-
/>
|
|
1141
1084
|
</ErrorBoundary>
|
|
1142
1085
|
)
|
|
1143
1086
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Puzzle } from 'lucide-react'
|
|
4
|
+
import { fieldPathFromEventTarget } from '../../../lib/preview-bridge.js'
|
|
4
5
|
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
5
6
|
import { ErrorBoundary } from '../../../components/ErrorBoundary.js'
|
|
6
7
|
import { getSectionType } from '../section-types.js'
|
|
@@ -163,7 +164,7 @@ export interface SectionRendererProps {
|
|
|
163
164
|
* renders hidden sections muted instead of removing them. */
|
|
164
165
|
editable?: boolean
|
|
165
166
|
selected?: boolean
|
|
166
|
-
onSelect?: (id: string) => void
|
|
167
|
+
onSelect?: (id: string, fieldPath?: string) => void
|
|
167
168
|
/** Lets the canvas register the DOM node for scroll-to-section. */
|
|
168
169
|
registerRef?: (id: string, el: HTMLElement | null) => void
|
|
169
170
|
/** Live post data for post-oriented section renderers (pages omit this). */
|
|
@@ -234,7 +235,9 @@ export function SectionRenderer({
|
|
|
234
235
|
? 'outline-transparent hover:outline hover:outline-[color-mix(in_oklab,var(--acc)_40%,transparent)]'
|
|
235
236
|
: 'outline-transparent'
|
|
236
237
|
}`}
|
|
237
|
-
onClick={
|
|
238
|
+
onClick={
|
|
239
|
+
interactive ? (e) => onSelect?.(section.id, fieldPathFromEventTarget(e.target)) : undefined
|
|
240
|
+
}
|
|
238
241
|
// Selection is a convenience in the editor; the section list is the
|
|
239
242
|
// primary, fully-accessible control surface, so this stays a region.
|
|
240
243
|
aria-label={
|
|
@@ -17,13 +17,26 @@ interface PostEditorCanvasProps {
|
|
|
17
17
|
selectedId: string | null
|
|
18
18
|
viewport: ViewportId
|
|
19
19
|
zoom?: Zoom
|
|
20
|
-
onSelect: (id: string) => void
|
|
20
|
+
onSelect: (id: string, fieldPath?: string) => void
|
|
21
21
|
onScaleChange?: (scale: number) => void
|
|
22
22
|
onCanvasElement?: (el: HTMLElement | null) => void
|
|
23
23
|
/** Add-section CTA on the empty canvas. Omitted by design-owned editors. */
|
|
24
24
|
onAddSection?: () => void
|
|
25
25
|
/** Section types rendered as placeholders — shows the "Structure view" notice. */
|
|
26
26
|
structuralTypes?: string[]
|
|
27
|
+
/**
|
|
28
|
+
* Read-only render of a past version. Suppresses selection and the editing
|
|
29
|
+
* affordances, and turns on the changed-section outlines.
|
|
30
|
+
*/
|
|
31
|
+
reviewing?: boolean
|
|
32
|
+
/** Sections that differ from the version before the one being reviewed. */
|
|
33
|
+
changedSectionIds?: ReadonlySet<string>
|
|
34
|
+
/**
|
|
35
|
+
* Rendered in place of the editing tip / structure notice while reviewing.
|
|
36
|
+
* Passed in rather than built here because the review bar's actions belong
|
|
37
|
+
* to the editor's history state, which the canvas knows nothing about.
|
|
38
|
+
*/
|
|
39
|
+
reviewBar?: React.ReactNode
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
/**
|
|
@@ -51,6 +64,9 @@ export function PostEditorCanvas({
|
|
|
51
64
|
onCanvasElement,
|
|
52
65
|
onAddSection,
|
|
53
66
|
structuralTypes = [],
|
|
67
|
+
reviewing = false,
|
|
68
|
+
changedSectionIds,
|
|
69
|
+
reviewBar,
|
|
54
70
|
}: PostEditorCanvasProps) {
|
|
55
71
|
const registerRef = useSectionRefs(selectedId)
|
|
56
72
|
|
|
@@ -62,15 +78,19 @@ export function PostEditorCanvas({
|
|
|
62
78
|
onScaleChange={onScaleChange}
|
|
63
79
|
onCanvasElement={onCanvasElement}
|
|
64
80
|
chrome={
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
reviewing ? (
|
|
82
|
+
reviewBar
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
{sections.length > 0 && <CanvasEditTip />}
|
|
86
|
+
<StructureNotice structuralTypes={structuralTypes} />
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
69
89
|
}
|
|
70
90
|
>
|
|
71
91
|
<PostHeader config={header} context={context} />
|
|
72
92
|
{sections.length === 0 ? (
|
|
73
|
-
<EmptyBody onAddSection={onAddSection} />
|
|
93
|
+
<EmptyBody onAddSection={reviewing ? undefined : onAddSection} />
|
|
74
94
|
) : (
|
|
75
95
|
sections.map((section) => (
|
|
76
96
|
<SectionRenderer
|
|
@@ -78,6 +98,8 @@ export function PostEditorCanvas({
|
|
|
78
98
|
section={section}
|
|
79
99
|
context={context}
|
|
80
100
|
editable
|
|
101
|
+
reviewing={reviewing}
|
|
102
|
+
changed={changedSectionIds?.has(section.id) ?? false}
|
|
81
103
|
selected={section.id === selectedId}
|
|
82
104
|
onSelect={onSelect}
|
|
83
105
|
registerRef={registerRef}
|