@prenta/admin 1.0.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 +12 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +5 -56
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/breakpoint-controls.render.test.js +4 -4
- package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -1
- 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/open-public-site-tab.test.d.ts +2 -0
- package/dist/__tests__/lib/open-public-site-tab.test.d.ts.map +1 -0
- package/dist/__tests__/lib/open-public-site-tab.test.js +64 -0
- package/dist/__tests__/lib/open-public-site-tab.test.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/__tests__/lib/view-site-chrome.test.d.ts.map +1 -0
- package/dist/__tests__/lib/view-site-chrome.test.js +69 -0
- package/dist/__tests__/lib/view-site-chrome.test.js.map +1 -0
- package/dist/__tests__/router/admin-routes.test.js +4 -0
- package/dist/__tests__/router/admin-routes.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/canvas-surface-shared.test.js +0 -16
- package/dist/__tests__/views/canvas-surface-shared.test.js.map +1 -1
- package/dist/__tests__/views/editor-top-bar.render.test.js +52 -22
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/page-section-editor.test.js +53 -18
- package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/post-section-editor.test.js +23 -0
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/components/BreakpointPicker.d.ts +3 -2
- package/dist/components/BreakpointPicker.d.ts.map +1 -1
- package/dist/components/BreakpointPicker.js +5 -3
- package/dist/components/BreakpointPicker.js.map +1 -1
- package/dist/components/CanvasEditTip.js +1 -1
- package/dist/components/CanvasEditTip.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/editor-preview.d.ts +4 -1
- package/dist/lib/editor-preview.d.ts.map +1 -1
- package/dist/lib/editor-preview.js +4 -1
- package/dist/lib/editor-preview.js.map +1 -1
- package/dist/lib/open-public-site-tab.d.ts +14 -0
- package/dist/lib/open-public-site-tab.d.ts.map +1 -0
- package/dist/lib/open-public-site-tab.js +46 -0
- package/dist/lib/open-public-site-tab.js.map +1 -0
- 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 +19 -0
- package/dist/lib/view-site-chrome.d.ts.map +1 -0
- package/dist/lib/view-site-chrome.js +24 -0
- package/dist/lib/view-site-chrome.js.map +1 -0
- package/dist/router/admin-routes.d.ts.map +1 -1
- package/dist/router/admin-routes.js +0 -2
- package/dist/router/admin-routes.js.map +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts +2 -3
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +2 -2
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +15 -17
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +20 -21
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts +1 -7
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +36 -141
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/StructureNotice.d.ts +2 -5
- package/dist/views/page-editor/StructureNotice.d.ts.map +1 -1
- package/dist/views/page-editor/StructureNotice.js +6 -10
- package/dist/views/page-editor/StructureNotice.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 +2 -4
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +3 -3
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts +1 -15
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +36 -128
- 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 +6 -6
- package/src/AdminRoot.tsx +0 -70
- package/src/__tests__/components/breakpoint-controls.render.test.tsx +6 -4
- package/src/__tests__/lib/canvas-edit-chrome.test.ts +0 -27
- package/src/__tests__/lib/open-public-site-tab.test.ts +74 -0
- package/src/__tests__/lib/preview-bridge.test.ts +21 -110
- package/src/__tests__/lib/view-site-chrome.test.ts +80 -0
- package/src/__tests__/router/admin-routes.test.ts +5 -0
- package/src/__tests__/views/canvas-review-mode.render.test.tsx +14 -1
- package/src/__tests__/views/canvas-surface-shared.test.ts +0 -19
- package/src/__tests__/views/editor-top-bar.render.test.tsx +104 -22
- package/src/__tests__/views/page-section-editor.test.tsx +65 -22
- package/src/__tests__/views/post-section-editor.test.tsx +35 -0
- package/src/components/BreakpointPicker.tsx +6 -3
- package/src/components/CanvasEditTip.tsx +2 -2
- package/src/lib/canvas-edit-chrome.ts +0 -64
- package/src/lib/editor-preview.ts +4 -1
- package/src/lib/open-public-site-tab.ts +57 -0
- package/src/lib/preview-bridge.ts +11 -157
- package/src/lib/view-site-chrome.ts +46 -0
- package/src/router/admin-routes.ts +0 -2
- package/src/styles/theme.css +4 -4
- package/src/views/page-editor/EditorCanvas.tsx +2 -7
- package/src/views/page-editor/EditorTopBar.tsx +85 -82
- package/src/views/page-editor/PageSectionEditor.tsx +83 -220
- package/src/views/page-editor/StructureNotice.tsx +7 -42
- package/src/views/page-editor/sections/SectionRenderer.tsx +5 -2
- package/src/views/post-editor/PostEditorCanvas.tsx +3 -9
- package/src/views/post-editor/PostSectionEditor.tsx +71 -195
- package/src/views/settings/SeoSettingsTab.tsx +21 -0
- package/src/views/settings/SiteInformationCard.tsx +7 -0
- package/dist/__tests__/views/use-site-preview.render.test.d.ts +0 -2
- package/dist/__tests__/views/use-site-preview.render.test.d.ts.map +0 -1
- package/dist/__tests__/views/use-site-preview.render.test.js +0 -120
- package/dist/__tests__/views/use-site-preview.render.test.js.map +0 -1
- package/dist/views/page-editor/PagePreview.d.ts +0 -13
- package/dist/views/page-editor/PagePreview.d.ts.map +0 -1
- package/dist/views/page-editor/PagePreview.js +0 -46
- package/dist/views/page-editor/PagePreview.js.map +0 -1
- package/dist/views/page-editor/SitePreviewFrame.d.ts +0 -43
- package/dist/views/page-editor/SitePreviewFrame.d.ts.map +0 -1
- package/dist/views/page-editor/SitePreviewFrame.js +0 -165
- package/dist/views/page-editor/SitePreviewFrame.js.map +0 -1
- package/dist/views/page-editor/useSitePreview.d.ts +0 -47
- package/dist/views/page-editor/useSitePreview.d.ts.map +0 -1
- package/dist/views/page-editor/useSitePreview.js +0 -96
- package/dist/views/page-editor/useSitePreview.js.map +0 -1
- package/dist/views/post-editor/PostPreview.d.ts +0 -14
- package/dist/views/post-editor/PostPreview.d.ts.map +0 -1
- package/dist/views/post-editor/PostPreview.js +0 -63
- package/dist/views/post-editor/PostPreview.js.map +0 -1
- package/src/__tests__/views/use-site-preview.render.test.tsx +0 -164
- package/src/views/page-editor/PagePreview.tsx +0 -105
- package/src/views/page-editor/SitePreviewFrame.tsx +0 -250
- package/src/views/page-editor/useSitePreview.ts +0 -158
- package/src/views/post-editor/PostPreview.tsx +0 -125
|
@@ -15,7 +15,6 @@ const baseProps = {
|
|
|
15
15
|
viewport: 'wide' as const,
|
|
16
16
|
onViewport: vi.fn(),
|
|
17
17
|
onBack: vi.fn(),
|
|
18
|
-
onPreview: vi.fn(),
|
|
19
18
|
onOpenSettings: vi.fn(),
|
|
20
19
|
onSaveDraft: vi.fn(),
|
|
21
20
|
onPublish: vi.fn(),
|
|
@@ -88,25 +87,112 @@ describe('EditorTopBar publish gating', () => {
|
|
|
88
87
|
})
|
|
89
88
|
})
|
|
90
89
|
|
|
91
|
-
describe('EditorTopBar view
|
|
92
|
-
it('
|
|
93
|
-
render(
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
describe('EditorTopBar view site / share', () => {
|
|
91
|
+
it('does not render the eye or the globe', () => {
|
|
92
|
+
render(
|
|
93
|
+
<EditorTopBar {...baseProps} onViewOnSite={vi.fn()} onShare={vi.fn()} hasPublicSiteUrl />,
|
|
94
|
+
)
|
|
95
|
+
expect(screen.queryByRole('button', { name: /Preview page|Preview structure/ })).toBeNull()
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
it('labels
|
|
98
|
+
it('labels View site on a draft and calls with preview-draft', () => {
|
|
99
99
|
const onViewOnSite = vi.fn()
|
|
100
|
-
render(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
render(
|
|
101
|
+
<EditorTopBar
|
|
102
|
+
{...baseProps}
|
|
103
|
+
status="DRAFT"
|
|
104
|
+
onViewOnSite={onViewOnSite}
|
|
105
|
+
onShare={vi.fn()}
|
|
106
|
+
hasPublicSiteUrl
|
|
107
|
+
/>,
|
|
108
|
+
)
|
|
109
|
+
fireEvent.click(screen.getByRole('button', { name: 'View site' }))
|
|
110
|
+
expect(onViewOnSite).toHaveBeenCalledWith('preview-draft')
|
|
111
|
+
expect(screen.getByRole('button', { name: 'Share' })).toBeTruthy()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('labels View live on a clean published document and hides Share', () => {
|
|
115
|
+
const onViewOnSite = vi.fn()
|
|
116
|
+
render(
|
|
117
|
+
<EditorTopBar
|
|
118
|
+
{...baseProps}
|
|
119
|
+
status="PUBLISHED"
|
|
120
|
+
onViewOnSite={onViewOnSite}
|
|
121
|
+
onShare={vi.fn()}
|
|
122
|
+
hasPublicSiteUrl
|
|
123
|
+
/>,
|
|
124
|
+
)
|
|
125
|
+
fireEvent.click(screen.getByRole('button', { name: 'View live' }))
|
|
126
|
+
expect(onViewOnSite).toHaveBeenCalledWith('view-live')
|
|
127
|
+
expect(screen.queryByRole('button', { name: 'Share' })).toBeNull()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('keeps View live as a second action when published and dirty', () => {
|
|
131
|
+
const onViewOnSite = vi.fn()
|
|
132
|
+
render(
|
|
133
|
+
<EditorTopBar
|
|
134
|
+
{...baseProps}
|
|
135
|
+
status="PUBLISHED"
|
|
136
|
+
dirty
|
|
137
|
+
onViewOnSite={onViewOnSite}
|
|
138
|
+
onShare={vi.fn()}
|
|
139
|
+
hasPublicSiteUrl
|
|
140
|
+
/>,
|
|
141
|
+
)
|
|
142
|
+
fireEvent.click(screen.getByRole('button', { name: 'View site' }))
|
|
143
|
+
expect(onViewOnSite).toHaveBeenCalledWith('preview-draft')
|
|
144
|
+
// Radix DropdownMenu opens on pointerdown (not click), which happy-dom
|
|
145
|
+
// doesn't synthesize from fireEvent.click — open via keyboard instead.
|
|
146
|
+
fireEvent.keyDown(screen.getByRole('button', { name: 'More view options' }), {
|
|
147
|
+
key: 'Enter',
|
|
148
|
+
})
|
|
149
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'View live' }))
|
|
150
|
+
expect(onViewOnSite).toHaveBeenCalledWith('view-live')
|
|
151
|
+
expect(screen.getByRole('button', { name: 'Share' })).toBeTruthy()
|
|
152
|
+
})
|
|
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
|
+
|
|
173
|
+
it('disables View site and Share when the public site URL is missing', () => {
|
|
174
|
+
render(
|
|
175
|
+
<EditorTopBar
|
|
176
|
+
{...baseProps}
|
|
177
|
+
onViewOnSite={vi.fn()}
|
|
178
|
+
onShare={vi.fn()}
|
|
179
|
+
hasPublicSiteUrl={false}
|
|
180
|
+
/>,
|
|
181
|
+
)
|
|
182
|
+
const view = screen.getByRole('button', { name: 'View site' }) as HTMLButtonElement
|
|
183
|
+
const share = screen.getByRole('button', { name: 'Share' }) as HTMLButtonElement
|
|
184
|
+
expect(view.disabled).toBe(true)
|
|
185
|
+
expect(share.disabled).toBe(true)
|
|
186
|
+
expect(view.getAttribute('title')).toBe(
|
|
187
|
+
'Set the public site URL in Settings → SEO to open the live design.',
|
|
188
|
+
)
|
|
105
189
|
})
|
|
106
190
|
|
|
107
|
-
it('hides
|
|
191
|
+
it('hides View site and Share when onViewOnSite is omitted (unsaved doc)', () => {
|
|
108
192
|
render(<EditorTopBar {...baseProps} status="PUBLISHED" />)
|
|
109
|
-
expect(screen.queryByRole('button', { name:
|
|
193
|
+
expect(screen.queryByRole('button', { name: 'View site' })).toBeNull()
|
|
194
|
+
expect(screen.queryByRole('button', { name: 'View live' })).toBeNull()
|
|
195
|
+
expect(screen.queryByRole('button', { name: 'Share' })).toBeNull()
|
|
110
196
|
})
|
|
111
197
|
})
|
|
112
198
|
|
|
@@ -133,19 +219,15 @@ describe('EditorTopBar history button', () => {
|
|
|
133
219
|
render(
|
|
134
220
|
<EditorTopBar
|
|
135
221
|
{...baseProps}
|
|
222
|
+
status="PUBLISHED"
|
|
223
|
+
dirty
|
|
224
|
+
hasPublicSiteUrl
|
|
136
225
|
onHistory={vi.fn()}
|
|
137
226
|
onViewOnSite={vi.fn()}
|
|
138
|
-
onToggleSitePreview={vi.fn()}
|
|
139
227
|
/>,
|
|
140
228
|
)
|
|
141
229
|
|
|
142
|
-
for (const name of [
|
|
143
|
-
'Page settings',
|
|
144
|
-
'Version history',
|
|
145
|
-
'Preview page',
|
|
146
|
-
'Inline site preview',
|
|
147
|
-
'View draft on site',
|
|
148
|
-
]) {
|
|
230
|
+
for (const name of ['Page settings', 'Version history', 'More view options']) {
|
|
149
231
|
const button = screen.getByRole('button', { name })
|
|
150
232
|
expect(button.className, `${name} has no touch target`).toContain('prenta-touch-target')
|
|
151
233
|
}
|
|
@@ -3,11 +3,20 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
3
3
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
4
|
|
|
5
5
|
const fetchPageForEditor = vi.fn()
|
|
6
|
+
const savePageDraft = vi.fn(async (page: unknown) => page)
|
|
6
7
|
vi.mock('../../lib/page-editor-service.js', async (importOriginal) => {
|
|
7
8
|
const actual = await importOriginal<typeof import('../../lib/page-editor-service.js')>()
|
|
8
9
|
return {
|
|
9
10
|
...actual,
|
|
10
|
-
fetchPageForEditor
|
|
11
|
+
fetchPageForEditor,
|
|
12
|
+
savePageDraft,
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
vi.mock('../../lib/seo-service.js', async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal<typeof import('../../lib/seo-service.js')>()
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
updateSeoFields: vi.fn(async () => ({})),
|
|
11
20
|
}
|
|
12
21
|
})
|
|
13
22
|
|
|
@@ -32,6 +41,7 @@ const validConfig = {
|
|
|
32
41
|
|
|
33
42
|
afterEach(() => {
|
|
34
43
|
fetchPageForEditor.mockReset()
|
|
44
|
+
savePageDraft.mockClear()
|
|
35
45
|
})
|
|
36
46
|
|
|
37
47
|
describe('PageSectionEditor schema guard', () => {
|
|
@@ -112,9 +122,8 @@ describe('PageSectionEditor structure mode', () => {
|
|
|
112
122
|
|
|
113
123
|
await screen.findByText('Sections')
|
|
114
124
|
expect(screen.queryByText(/Structure view/)).toBeNull()
|
|
115
|
-
|
|
116
|
-
expect(screen.getByRole('button', { name: '
|
|
117
|
-
expect(screen.getByRole('button', { name: 'Preview page' })).toBeTruthy()
|
|
125
|
+
expect(screen.getByRole('button', { name: 'View site' })).toBeTruthy()
|
|
126
|
+
expect(screen.getByRole('button', { name: 'Share' })).toBeTruthy()
|
|
118
127
|
})
|
|
119
128
|
|
|
120
129
|
it('shows the structure notice and promotes "View on site" for custom section types', async () => {
|
|
@@ -130,10 +139,7 @@ describe('PageSectionEditor structure mode', () => {
|
|
|
130
139
|
|
|
131
140
|
await screen.findByText('Sections')
|
|
132
141
|
expect(screen.getByText(/Structure view/)).toBeTruthy()
|
|
133
|
-
|
|
134
|
-
expect(screen.getByRole('button', { name: /View on site/ })).toBeTruthy()
|
|
135
|
-
// Eye preview is relabeled to set expectations.
|
|
136
|
-
expect(screen.getByRole('button', { name: /Preview structure/ })).toBeTruthy()
|
|
142
|
+
expect(screen.getByRole('button', { name: 'View site' })).toBeTruthy()
|
|
137
143
|
})
|
|
138
144
|
|
|
139
145
|
it('stays out of structure mode when a custom renderer is registered for the type', async () => {
|
|
@@ -161,7 +167,7 @@ describe('PageSectionEditor structure mode', () => {
|
|
|
161
167
|
// structural for this page.
|
|
162
168
|
expect(screen.getByTestId('opal-canvas')).toBeTruthy()
|
|
163
169
|
expect(screen.queryByText(/Structure view/)).toBeNull()
|
|
164
|
-
expect(screen.getByRole('button', { name: 'View
|
|
170
|
+
expect(screen.getByRole('button', { name: 'View site' })).toBeTruthy()
|
|
165
171
|
})
|
|
166
172
|
|
|
167
173
|
it('exposes a version history button for saved pages', async () => {
|
|
@@ -207,12 +213,22 @@ describe('PageSectionEditor section disclosure', () => {
|
|
|
207
213
|
expect(screen.queryByLabelText('Section name')).toBeNull()
|
|
208
214
|
})
|
|
209
215
|
|
|
210
|
-
it('keeps the row open when
|
|
216
|
+
it('keeps the row open when a canvas field click targets the same section', async () => {
|
|
211
217
|
// A `fieldPath` means "the editor clicked THIS field" — toggling there
|
|
212
|
-
// would close the form on the second click of a field being
|
|
213
|
-
fetchPageForEditor.mockResolvedValue(
|
|
214
|
-
loadedPage([{ id: 's1', sectionType: 'hero', name: 'Hero' }]),
|
|
215
|
-
|
|
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
|
+
})
|
|
216
232
|
render(
|
|
217
233
|
<PageSectionEditor documentId="p1" config={validConfig} onNavigate={() => {}} session={{}} />,
|
|
218
234
|
)
|
|
@@ -221,15 +237,42 @@ describe('PageSectionEditor section disclosure', () => {
|
|
|
221
237
|
fireEvent.click(screen.getByRole('button', { name: 'Edit Hero' }))
|
|
222
238
|
await screen.findByRole('button', { name: 'Close Hero' })
|
|
223
239
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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)
|
|
232
248
|
|
|
233
249
|
expect(screen.getByRole('button', { name: 'Close Hero' })).toBeTruthy()
|
|
234
250
|
})
|
|
235
251
|
})
|
|
252
|
+
|
|
253
|
+
describe('PageSectionEditor Share', () => {
|
|
254
|
+
const configWithSite = { ...validConfig, seo: { siteUrl: 'https://example.com' } }
|
|
255
|
+
|
|
256
|
+
it('saves a dirty draft before opening Share', async () => {
|
|
257
|
+
const page = loadedPage([{ id: 's1', sectionType: 'hero', name: 'Hero' }])
|
|
258
|
+
fetchPageForEditor.mockResolvedValue(page)
|
|
259
|
+
savePageDraft.mockResolvedValue(page)
|
|
260
|
+
|
|
261
|
+
render(
|
|
262
|
+
<PageSectionEditor
|
|
263
|
+
documentId="p1"
|
|
264
|
+
config={configWithSite}
|
|
265
|
+
onNavigate={() => {}}
|
|
266
|
+
session={{}}
|
|
267
|
+
/>,
|
|
268
|
+
)
|
|
269
|
+
await screen.findByText('Sections')
|
|
270
|
+
|
|
271
|
+
fireEvent.click(screen.getByRole('button', { name: 'Edit Hero' }))
|
|
272
|
+
fireEvent.change(await screen.findByLabelText('Section name'), { target: { value: 'Intro' } })
|
|
273
|
+
fireEvent.click(screen.getByRole('button', { name: 'Share' }))
|
|
274
|
+
|
|
275
|
+
await waitFor(() => expect(savePageDraft).toHaveBeenCalledOnce())
|
|
276
|
+
expect(await screen.findByRole('dialog', { name: 'Share preview link' })).toBeTruthy()
|
|
277
|
+
})
|
|
278
|
+
})
|
|
@@ -4,12 +4,14 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
|
4
4
|
|
|
5
5
|
const fetchPostForEditor = vi.fn()
|
|
6
6
|
const fetchTemplateForEditor = vi.fn()
|
|
7
|
+
const savePostDraft = vi.fn()
|
|
7
8
|
vi.mock('../../lib/post-editor-service.js', async (importOriginal) => {
|
|
8
9
|
const actual = await importOriginal<typeof import('../../lib/post-editor-service.js')>()
|
|
9
10
|
return {
|
|
10
11
|
...actual,
|
|
11
12
|
fetchPostForEditor: (...args: unknown[]) => fetchPostForEditor(...args),
|
|
12
13
|
fetchTemplateForEditor: (...args: unknown[]) => fetchTemplateForEditor(...args),
|
|
14
|
+
savePostDraft: (...args: unknown[]) => savePostDraft(...args),
|
|
13
15
|
}
|
|
14
16
|
})
|
|
15
17
|
|
|
@@ -62,6 +64,7 @@ function loadedPost(
|
|
|
62
64
|
afterEach(() => {
|
|
63
65
|
fetchPostForEditor.mockReset()
|
|
64
66
|
fetchTemplateForEditor.mockReset()
|
|
67
|
+
savePostDraft.mockReset()
|
|
65
68
|
})
|
|
66
69
|
|
|
67
70
|
describe('PostSectionEditor schema guard', () => {
|
|
@@ -126,6 +129,9 @@ describe('PostSectionEditor new post', () => {
|
|
|
126
129
|
)
|
|
127
130
|
await screen.findByText('Sections')
|
|
128
131
|
expect(screen.queryByRole('dialog', { name: 'Post details' })).toBeNull()
|
|
132
|
+
expect(screen.getByRole('button', { name: 'View site' })).toBeTruthy()
|
|
133
|
+
expect(screen.getByRole('button', { name: 'Share' })).toBeTruthy()
|
|
134
|
+
expect(screen.queryByRole('button', { name: /Preview page/ })).toBeNull()
|
|
129
135
|
})
|
|
130
136
|
})
|
|
131
137
|
|
|
@@ -201,6 +207,35 @@ describe('PostSectionEditor section disclosure', () => {
|
|
|
201
207
|
})
|
|
202
208
|
})
|
|
203
209
|
|
|
210
|
+
describe('PostSectionEditor Share', () => {
|
|
211
|
+
it('saves a dirty draft before opening Share', async () => {
|
|
212
|
+
const post = loadedPost([{ id: 's1', sectionType: 'quote', name: 'Quote' }])
|
|
213
|
+
fetchTemplateForEditor.mockRejectedValue(new Error('no template'))
|
|
214
|
+
fetchPostForEditor.mockResolvedValue(post)
|
|
215
|
+
savePostDraft.mockResolvedValue(post)
|
|
216
|
+
|
|
217
|
+
render(
|
|
218
|
+
<PostSectionEditor
|
|
219
|
+
postType="blog"
|
|
220
|
+
documentId="p1"
|
|
221
|
+
config={{ ...validConfig, seo: { siteUrl: 'https://example.com' } }}
|
|
222
|
+
onNavigate={() => {}}
|
|
223
|
+
session={{}}
|
|
224
|
+
/>,
|
|
225
|
+
)
|
|
226
|
+
await screen.findByText('Sections')
|
|
227
|
+
|
|
228
|
+
fireEvent.click(screen.getByRole('button', { name: 'Edit Quote' }))
|
|
229
|
+
fireEvent.change(await screen.findByLabelText('Section name'), {
|
|
230
|
+
target: { value: 'Pull quote' },
|
|
231
|
+
})
|
|
232
|
+
fireEvent.click(screen.getByRole('button', { name: 'Share' }))
|
|
233
|
+
|
|
234
|
+
await waitFor(() => expect(savePostDraft).toHaveBeenCalledOnce())
|
|
235
|
+
expect(await screen.findByRole('dialog', { name: 'Share preview link' })).toBeTruthy()
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
204
239
|
/**
|
|
205
240
|
* The post editor was the last screen still on the pre-handoff panel: a fixed
|
|
206
241
|
* 288px aside with no resize, no collapse and no tabs, with SEO in a
|
|
@@ -19,11 +19,12 @@ export interface BreakpointPickerProps {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Canvas-width selector for the editor header.
|
|
23
23
|
*
|
|
24
24
|
* A dropdown rather than a segmented row: six presets with names like "Tablet
|
|
25
25
|
* Landscape" do not fit a header strip, and the grouped menu is what makes the
|
|
26
|
-
* DESKTOP / TABLET / MOBILE structure legible.
|
|
26
|
+
* DESKTOP / TABLET / MOBILE structure legible. This only sizes the first-party
|
|
27
|
+
* canvas — it is not a live-site viewport.
|
|
27
28
|
*
|
|
28
29
|
* Radix owns the menu behaviour — outside-click, Escape, roving focus, and
|
|
29
30
|
* `aria-expanded` on the trigger — rather than a hand-rolled popover.
|
|
@@ -32,6 +33,7 @@ export function BreakpointPicker({ value, onChange, disabled }: BreakpointPicker
|
|
|
32
33
|
const [open, setOpen] = useState(false)
|
|
33
34
|
const active = getBreakpoint(value)
|
|
34
35
|
const ActiveIcon = breakpointIcon(active.icon)
|
|
36
|
+
const canvasWidthLabel = `Canvas width: ${active.name}, ${active.width} pixels. First-party layout only — use View site for the live design.`
|
|
35
37
|
|
|
36
38
|
return (
|
|
37
39
|
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
|
@@ -39,7 +41,8 @@ export function BreakpointPicker({ value, onChange, disabled }: BreakpointPicker
|
|
|
39
41
|
<button
|
|
40
42
|
type="button"
|
|
41
43
|
disabled={disabled}
|
|
42
|
-
aria-label={
|
|
44
|
+
aria-label={canvasWidthLabel}
|
|
45
|
+
title={canvasWidthLabel}
|
|
43
46
|
className={`border-border flex h-[34px] items-center gap-1.5 rounded-lg border px-2.5 text-[12.5px] font-medium transition-colors disabled:opacity-50 ${
|
|
44
47
|
open
|
|
45
48
|
? 'border-brand bg-accent text-accent-foreground'
|
|
@@ -27,8 +27,8 @@ export function CanvasEditTip() {
|
|
|
27
27
|
<span className="text-foreground font-medium">Editing tip</span>
|
|
28
28
|
{' — '}
|
|
29
29
|
Click a section on the canvas, or in the list, to open its copy and images in the section
|
|
30
|
-
list. Use <span className="text-foreground font-medium">
|
|
31
|
-
|
|
30
|
+
list. Use <span className="text-foreground font-medium">View site</span> for the live
|
|
31
|
+
design.
|
|
32
32
|
</p>
|
|
33
33
|
<button
|
|
34
34
|
type="button"
|
|
@@ -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,6 +1,9 @@
|
|
|
1
1
|
import type { AdminEditorConfig } from '@prenta/core'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Ignored by the editors; returns the old boolean for compatibility only.
|
|
5
|
+
* The page and post editors no longer mount an inline site-preview iframe.
|
|
6
|
+
*/
|
|
4
7
|
export function resolveDefaultSitePreview(
|
|
5
8
|
editor: AdminEditorConfig | undefined,
|
|
6
9
|
kind: 'pages' | 'posts',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { toast } from 'sonner'
|
|
2
|
+
import { buildPublicPreviewUrl, buildPublicUrl, createPreviewToken } from './preview-link.js'
|
|
3
|
+
import { NO_PUBLIC_SITE_URL_REASON, type ViewSiteIntent } from './view-site-chrome.js'
|
|
4
|
+
|
|
5
|
+
export async function openPublicSiteTab(opts: {
|
|
6
|
+
intent: ViewSiteIntent
|
|
7
|
+
siteUrl: string
|
|
8
|
+
collection: string
|
|
9
|
+
documentId: string
|
|
10
|
+
slug: string
|
|
11
|
+
path?: string | null
|
|
12
|
+
urlPrefix?: string
|
|
13
|
+
data?: Record<string, unknown> | null
|
|
14
|
+
dirty: boolean
|
|
15
|
+
saveDraft: () => Promise<boolean>
|
|
16
|
+
}): Promise<void> {
|
|
17
|
+
const siteUrl = opts.siteUrl.trim()
|
|
18
|
+
if (!siteUrl) throw new Error(NO_PUBLIC_SITE_URL_REASON)
|
|
19
|
+
|
|
20
|
+
const parts = {
|
|
21
|
+
siteUrl,
|
|
22
|
+
urlPrefix: opts.urlPrefix ?? '',
|
|
23
|
+
slug: opts.slug,
|
|
24
|
+
path: opts.path,
|
|
25
|
+
data: opts.data,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (opts.intent === 'view-live') {
|
|
29
|
+
window.open(buildPublicUrl(parts), '_blank', 'noopener')
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tab = window.open('about:blank', '_blank')
|
|
34
|
+
if (!tab) {
|
|
35
|
+
toast.error('Enable pop-ups for this site to open the preview in a new tab.')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
if (opts.dirty) {
|
|
40
|
+
const ok = await opts.saveDraft()
|
|
41
|
+
if (!ok) {
|
|
42
|
+
tab.close()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const { token } = await createPreviewToken(opts.collection, opts.documentId)
|
|
47
|
+
try {
|
|
48
|
+
tab.opener = null
|
|
49
|
+
} catch {
|
|
50
|
+
/* same-origin guard */
|
|
51
|
+
}
|
|
52
|
+
tab.location.replace(buildPublicPreviewUrl({ ...parts, token }))
|
|
53
|
+
} catch (err) {
|
|
54
|
+
tab.close()
|
|
55
|
+
throw err
|
|
56
|
+
}
|
|
57
|
+
}
|