@prenta/admin 1.1.0 → 1.2.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 +6 -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/preview-bridge.test.js +19 -79
- package/dist/__tests__/lib/preview-bridge.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__/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 +8 -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/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/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/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 +2 -1
- 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 +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +7 -41
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- 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 +3 -3
- package/src/__tests__/lib/canvas-edit-chrome.test.ts +0 -27
- package/src/__tests__/lib/preview-bridge.test.ts +21 -110
- package/src/__tests__/lib/view-site-chrome.test.ts +16 -1
- package/src/__tests__/views/canvas-review-mode.render.test.tsx +14 -1
- package/src/__tests__/views/editor-top-bar.render.test.tsx +19 -0
- package/src/__tests__/views/page-section-editor.test.tsx +23 -13
- package/src/lib/canvas-edit-chrome.ts +0 -64
- package/src/lib/preview-bridge.ts +11 -157
- 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 +4 -0
- 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 +1 -1
- package/src/views/post-editor/PostSectionEditor.tsx +5 -57
- package/src/views/settings/SeoSettingsTab.tsx +21 -0
- package/src/views/settings/SiteInformationCard.tsx +7 -0
|
@@ -1,128 +1,39 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
1
2
|
import { describe, expect, it } from 'vitest'
|
|
2
3
|
import {
|
|
3
|
-
canvasOriginFromSrc,
|
|
4
4
|
companionDimensionPaths,
|
|
5
|
+
fieldPathFromEventTarget,
|
|
6
|
+
fieldPathLeaf,
|
|
5
7
|
inspectorIdFromFieldPath,
|
|
6
|
-
isMediaFieldPath,
|
|
7
|
-
isPreviewBridgeMessage,
|
|
8
|
-
isPreviewEditMessage,
|
|
9
|
-
isPreviewModeMessage,
|
|
10
|
-
isPreviewRequestMediaMessage,
|
|
11
|
-
PREVIEW_BRIDGE_VERSION,
|
|
12
|
-
toPreviewSectionPayload,
|
|
13
8
|
} from '../../lib/preview-bridge.js'
|
|
14
9
|
|
|
15
|
-
describe('preview-bridge', () => {
|
|
16
|
-
it('accepts versioned prenta-preview messages', () => {
|
|
17
|
-
expect(
|
|
18
|
-
isPreviewBridgeMessage({ type: 'prenta-preview:ready', v: PREVIEW_BRIDGE_VERSION }),
|
|
19
|
-
).toBe(true)
|
|
20
|
-
expect(
|
|
21
|
-
isPreviewBridgeMessage({
|
|
22
|
-
type: 'prenta-preview:select',
|
|
23
|
-
v: PREVIEW_BRIDGE_VERSION,
|
|
24
|
-
sectionId: 'sec-1',
|
|
25
|
-
}),
|
|
26
|
-
).toBe(true)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('rejects legacy or wrong-version payloads', () => {
|
|
30
|
-
expect(isPreviewBridgeMessage({ type: 'prenta-preview-update', data: {} })).toBe(false)
|
|
31
|
-
expect(isPreviewBridgeMessage({ type: 'prenta-preview:ready', v: 99 })).toBe(false)
|
|
32
|
-
expect(isPreviewBridgeMessage(null)).toBe(false)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('parses canvas origin from iframe src', () => {
|
|
36
|
-
expect(canvasOriginFromSrc('https://cms.example.com/about/?preview=tok')).toBe(
|
|
37
|
-
'https://cms.example.com',
|
|
38
|
-
)
|
|
39
|
-
expect(canvasOriginFromSrc(null)).toBeNull()
|
|
40
|
-
})
|
|
41
|
-
|
|
10
|
+
describe('preview-bridge field paths', () => {
|
|
42
11
|
it('maps dotted field paths to unified insp-field ids', () => {
|
|
43
12
|
expect(inspectorIdFromFieldPath('lead')).toBe('insp-field-lead')
|
|
44
13
|
expect(inspectorIdFromFieldPath('stats.0.num')).toBe('insp-field-stats-0-num')
|
|
45
14
|
expect(inspectorIdFromFieldPath('rotateWords.2.word')).toBe('insp-field-rotateWords-2-word')
|
|
46
15
|
})
|
|
47
16
|
|
|
48
|
-
it('
|
|
49
|
-
expect(
|
|
50
|
-
isPreviewBridgeMessage({
|
|
51
|
-
type: 'prenta-preview:sections',
|
|
52
|
-
v: PREVIEW_BRIDGE_VERSION,
|
|
53
|
-
sections: [{ id: 's1', sectionType: 'hero', content: { heading: 'Hi' } }],
|
|
54
|
-
}),
|
|
55
|
-
).toBe(true)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('strips editor-only fields from preview section payloads', () => {
|
|
59
|
-
expect(
|
|
60
|
-
toPreviewSectionPayload({
|
|
61
|
-
id: 's1',
|
|
62
|
-
sectionType: 'hero',
|
|
63
|
-
name: 'Hero',
|
|
64
|
-
visible: true,
|
|
65
|
-
content: { heading: 'Hi' },
|
|
66
|
-
settings: {},
|
|
67
|
-
internalLabel: 'team note',
|
|
68
|
-
}),
|
|
69
|
-
).toEqual({
|
|
70
|
-
id: 's1',
|
|
71
|
-
sectionType: 'hero',
|
|
72
|
-
name: 'Hero',
|
|
73
|
-
visible: true,
|
|
74
|
-
content: { heading: 'Hi' },
|
|
75
|
-
settings: {},
|
|
76
|
-
unmanaged: undefined,
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('validates edit / mode / request-media payloads', () => {
|
|
81
|
-
expect(
|
|
82
|
-
isPreviewEditMessage({
|
|
83
|
-
type: 'prenta-preview:edit',
|
|
84
|
-
v: PREVIEW_BRIDGE_VERSION,
|
|
85
|
-
sectionId: 's1',
|
|
86
|
-
fieldPath: 'lead',
|
|
87
|
-
value: 'Hello',
|
|
88
|
-
}),
|
|
89
|
-
).toBe(true)
|
|
90
|
-
expect(
|
|
91
|
-
isPreviewEditMessage({
|
|
92
|
-
type: 'prenta-preview:edit',
|
|
93
|
-
v: PREVIEW_BRIDGE_VERSION,
|
|
94
|
-
sectionId: 's1',
|
|
95
|
-
fieldPath: 'lead',
|
|
96
|
-
}),
|
|
97
|
-
).toBe(false)
|
|
98
|
-
expect(
|
|
99
|
-
isPreviewModeMessage({
|
|
100
|
-
type: 'prenta-preview:mode',
|
|
101
|
-
v: PREVIEW_BRIDGE_VERSION,
|
|
102
|
-
editing: true,
|
|
103
|
-
}),
|
|
104
|
-
).toBe(true)
|
|
105
|
-
expect(
|
|
106
|
-
isPreviewRequestMediaMessage({
|
|
107
|
-
type: 'prenta-preview:request-media',
|
|
108
|
-
v: PREVIEW_BRIDGE_VERSION,
|
|
109
|
-
sectionId: 's1',
|
|
110
|
-
fieldPath: 'awards.0.imageUrl',
|
|
111
|
-
}),
|
|
112
|
-
).toBe(true)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('detects media field paths and companion dimension paths', () => {
|
|
116
|
-
expect(isMediaFieldPath('imageUrl')).toBe(true)
|
|
117
|
-
expect(isMediaFieldPath('awards.0.imageUrl')).toBe(true)
|
|
118
|
-
expect(isMediaFieldPath('lead')).toBe(false)
|
|
119
|
-
expect(isMediaFieldPath('ctaUrl')).toBe(false)
|
|
120
|
-
expect(isMediaFieldPath('url', 'media')).toBe(true)
|
|
121
|
-
expect(isMediaFieldPath('images.0.url')).toBe(true)
|
|
122
|
-
expect(isMediaFieldPath('buttonUrl')).toBe(false)
|
|
17
|
+
it('reads companion dimension paths for media URL fields', () => {
|
|
123
18
|
expect(companionDimensionPaths('awards.0.imageUrl')).toEqual({
|
|
124
19
|
widthPath: 'awards.0.imageWidth',
|
|
125
20
|
heightPath: 'awards.0.imageHeight',
|
|
126
21
|
})
|
|
22
|
+
expect(companionDimensionPaths('images.0.url')).toEqual({
|
|
23
|
+
widthPath: 'images.0.width',
|
|
24
|
+
heightPath: 'images.0.height',
|
|
25
|
+
})
|
|
26
|
+
expect(fieldPathLeaf('awards.0.imageUrl')).toBe('imageUrl')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('reads data-cms-field from a click target or its ancestor', () => {
|
|
30
|
+
const wrap = document.createElement('div')
|
|
31
|
+
wrap.setAttribute('data-cms-field', 'heading')
|
|
32
|
+
const inner = document.createElement('span')
|
|
33
|
+
wrap.appendChild(inner)
|
|
34
|
+
expect(fieldPathFromEventTarget(inner)).toBe('heading')
|
|
35
|
+
expect(fieldPathFromEventTarget(wrap)).toBe('heading')
|
|
36
|
+
expect(fieldPathFromEventTarget(document.createElement('div'))).toBeUndefined()
|
|
37
|
+
expect(fieldPathFromEventTarget(null)).toBeUndefined()
|
|
127
38
|
})
|
|
128
39
|
})
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ADMIN_ORIGIN_SITE_URL_REASON,
|
|
4
|
+
NO_PUBLIC_SITE_URL_REASON,
|
|
5
|
+
resolveViewSiteChrome,
|
|
6
|
+
} from '../../lib/view-site-chrome.js'
|
|
3
7
|
|
|
4
8
|
const base = {
|
|
5
9
|
dirty: false,
|
|
@@ -53,6 +57,17 @@ describe('resolveViewSiteChrome', () => {
|
|
|
53
57
|
expect(chrome.shareVisible).toBe(true)
|
|
54
58
|
})
|
|
55
59
|
|
|
60
|
+
it('disables View site and Share when the site URL is the admin origin', () => {
|
|
61
|
+
const chrome = resolveViewSiteChrome({
|
|
62
|
+
...base,
|
|
63
|
+
status: 'DRAFT',
|
|
64
|
+
matchesAdminOrigin: true,
|
|
65
|
+
})
|
|
66
|
+
expect(chrome.viewEnabled).toBe(false)
|
|
67
|
+
expect(chrome.viewDisabledReason).toBe(ADMIN_ORIGIN_SITE_URL_REASON)
|
|
68
|
+
expect(chrome.shareVisible).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
56
71
|
it('hides View site and Share when the document is unsaved', () => {
|
|
57
72
|
const chrome = resolveViewSiteChrome({
|
|
58
73
|
...base,
|
|
@@ -68,10 +68,23 @@ describe('SectionRenderer — review mode', () => {
|
|
|
68
68
|
|
|
69
69
|
fireEvent.click(sectionEl())
|
|
70
70
|
|
|
71
|
-
expect(onSelect).toHaveBeenCalledWith('s1')
|
|
71
|
+
expect(onSelect).toHaveBeenCalledWith('s1', undefined)
|
|
72
72
|
expect(sectionEl().getAttribute('data-editable')).toBe('true')
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
it('passes the data-cms-field path when a field inside the section is clicked', () => {
|
|
76
|
+
const onSelect = vi.fn()
|
|
77
|
+
render(<SectionRenderer section={SECTION} editable onSelect={onSelect} />)
|
|
78
|
+
|
|
79
|
+
const field = document.createElement('h2')
|
|
80
|
+
field.setAttribute('data-cms-field', 'heading')
|
|
81
|
+
field.textContent = 'A headline'
|
|
82
|
+
sectionEl().appendChild(field)
|
|
83
|
+
fireEvent.click(field)
|
|
84
|
+
|
|
85
|
+
expect(onSelect).toHaveBeenCalledWith('s1', 'heading')
|
|
86
|
+
})
|
|
87
|
+
|
|
75
88
|
it('names the change in the accessible name, not only in colour and a dashed border', () => {
|
|
76
89
|
render(<SectionRenderer section={SECTION} editable reviewing changed />)
|
|
77
90
|
expect(sectionEl().getAttribute('aria-label')).toBe(
|
|
@@ -151,6 +151,25 @@ describe('EditorTopBar view site / share', () => {
|
|
|
151
151
|
expect(screen.getByRole('button', { name: 'Share' })).toBeTruthy()
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
+
it('disables View site and Share when the site URL is the admin origin', () => {
|
|
155
|
+
render(
|
|
156
|
+
<EditorTopBar
|
|
157
|
+
{...baseProps}
|
|
158
|
+
onViewOnSite={vi.fn()}
|
|
159
|
+
onShare={vi.fn()}
|
|
160
|
+
hasPublicSiteUrl
|
|
161
|
+
matchesAdminOrigin
|
|
162
|
+
/>,
|
|
163
|
+
)
|
|
164
|
+
const view = screen.getByRole('button', { name: 'View site' }) as HTMLButtonElement
|
|
165
|
+
const share = screen.getByRole('button', { name: 'Share' }) as HTMLButtonElement
|
|
166
|
+
expect(view.disabled).toBe(true)
|
|
167
|
+
expect(share.disabled).toBe(true)
|
|
168
|
+
expect(view.getAttribute('title')).toBe(
|
|
169
|
+
'This origin is the admin, not the public site. Set the public site URL in Settings → SEO.',
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
154
173
|
it('disables View site and Share when the public site URL is missing', () => {
|
|
155
174
|
render(
|
|
156
175
|
<EditorTopBar
|
|
@@ -213,12 +213,22 @@ describe('PageSectionEditor section disclosure', () => {
|
|
|
213
213
|
expect(screen.queryByLabelText('Section name')).toBeNull()
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
-
it('keeps the row open when
|
|
216
|
+
it('keeps the row open when a canvas field click targets the same section', async () => {
|
|
217
217
|
// A `fieldPath` means "the editor clicked THIS field" — toggling there
|
|
218
|
-
// would close the form on the second click of a field being
|
|
219
|
-
fetchPageForEditor.mockResolvedValue(
|
|
220
|
-
loadedPage([{ id: 's1', sectionType: 'hero', name: 'Hero' }]),
|
|
221
|
-
|
|
218
|
+
// would close the form on the second click of a field being inspected.
|
|
219
|
+
fetchPageForEditor.mockResolvedValue({
|
|
220
|
+
...loadedPage([{ id: 's1', sectionType: 'hero', name: 'Hero' }]),
|
|
221
|
+
sections: [
|
|
222
|
+
{
|
|
223
|
+
id: 's1',
|
|
224
|
+
sectionType: 'hero',
|
|
225
|
+
name: 'Hero',
|
|
226
|
+
visible: true,
|
|
227
|
+
content: { heading: 'Welcome' },
|
|
228
|
+
settings: {},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
})
|
|
222
232
|
render(
|
|
223
233
|
<PageSectionEditor documentId="p1" config={validConfig} onNavigate={() => {}} session={{}} />,
|
|
224
234
|
)
|
|
@@ -227,14 +237,14 @@ describe('PageSectionEditor section disclosure', () => {
|
|
|
227
237
|
fireEvent.click(screen.getByRole('button', { name: 'Edit Hero' }))
|
|
228
238
|
await screen.findByRole('button', { name: 'Close Hero' })
|
|
229
239
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
const section = document.querySelector('[data-section-id="s1"]')
|
|
241
|
+
expect(section).toBeTruthy()
|
|
242
|
+
const field = document.createElement('h2')
|
|
243
|
+
field.setAttribute('data-cms-field', 'heading')
|
|
244
|
+
field.textContent = 'Welcome'
|
|
245
|
+
section?.appendChild(field)
|
|
246
|
+
fireEvent.click(field)
|
|
247
|
+
fireEvent.click(field)
|
|
238
248
|
|
|
239
249
|
expect(screen.getByRole('button', { name: 'Close Hero' })).toBeTruthy()
|
|
240
250
|
})
|
|
@@ -1,70 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared edit affordances for the page/post canvas and Site preview iframe.
|
|
3
|
-
*
|
|
4
|
-
* Structure canvas: section hover is styled in `theme.css` under `.prenta-canvas`.
|
|
5
|
-
* Site preview: the same field chrome is injected into the iframe document when
|
|
6
|
-
* same-origin (CMS-host preview) so consumers get hover borders without shipping CSS.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
/** localStorage key for the dismissible canvas tip. */
|
|
10
2
|
export const CANVAS_EDIT_TIP_STORAGE_KEY = 'prenta.canvasEditTip.dismissed'
|
|
11
3
|
|
|
12
|
-
/**
|
|
13
|
-
* CSS injected into the Site preview iframe while editing is enabled.
|
|
14
|
-
* Uses system colors so it works without admin theme tokens in the iframe.
|
|
15
|
-
*/
|
|
16
|
-
export const PREVIEW_EDITABLE_FIELD_CHROME_CSS = `
|
|
17
|
-
/* Prenta Site preview — editable field hover chrome (injected by @prenta/admin) */
|
|
18
|
-
[data-cms-field] {
|
|
19
|
-
outline: 1px solid transparent;
|
|
20
|
-
outline-offset: 2px;
|
|
21
|
-
border-radius: 2px;
|
|
22
|
-
transition: outline-color 120ms ease;
|
|
23
|
-
cursor: text;
|
|
24
|
-
}
|
|
25
|
-
[data-cms-field]:hover,
|
|
26
|
-
[data-cms-field]:focus,
|
|
27
|
-
[data-cms-field]:focus-within {
|
|
28
|
-
outline-color: color-mix(in oklab, CanvasText 24%, transparent);
|
|
29
|
-
}
|
|
30
|
-
[data-cms-field][data-cms-field-kind="media"] {
|
|
31
|
-
cursor: pointer;
|
|
32
|
-
}
|
|
33
|
-
[data-cms-field][data-cms-field-kind="media"]:hover,
|
|
34
|
-
[data-cms-field][data-cms-field-kind="media"]:focus {
|
|
35
|
-
outline-color: color-mix(in oklab, CanvasText 32%, transparent);
|
|
36
|
-
}
|
|
37
|
-
@media (prefers-reduced-motion: reduce) {
|
|
38
|
-
[data-cms-field] {
|
|
39
|
-
transition: none;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
`.trim()
|
|
43
|
-
|
|
44
|
-
const STYLE_ATTR = 'data-prenta-edit-chrome'
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Inject (or refresh) editable-field hover chrome into a same-origin preview
|
|
48
|
-
* iframe. No-ops on cross-origin frames (browser blocks contentDocument).
|
|
49
|
-
*/
|
|
50
|
-
export function injectPreviewEditChrome(iframe: HTMLIFrameElement | null): boolean {
|
|
51
|
-
try {
|
|
52
|
-
const doc = iframe?.contentDocument
|
|
53
|
-
if (!doc?.head) return false
|
|
54
|
-
let style = doc.head.querySelector<HTMLStyleElement>(`style[${STYLE_ATTR}]`)
|
|
55
|
-
if (!style) {
|
|
56
|
-
style = doc.createElement('style')
|
|
57
|
-
style.setAttribute(STYLE_ATTR, '1')
|
|
58
|
-
doc.head.appendChild(style)
|
|
59
|
-
}
|
|
60
|
-
style.textContent = PREVIEW_EDITABLE_FIELD_CHROME_CSS
|
|
61
|
-
doc.documentElement.setAttribute('data-prenta-preview-editing', 'true')
|
|
62
|
-
return true
|
|
63
|
-
} catch {
|
|
64
|
-
return false
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
4
|
export function isCanvasEditTipDismissed(): boolean {
|
|
69
5
|
try {
|
|
70
6
|
return localStorage.getItem(CANVAS_EDIT_TIP_STORAGE_KEY) === '1'
|
|
@@ -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
|
}
|
|
@@ -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
|