@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
package/src/AdminRoot.tsx
CHANGED
|
@@ -42,7 +42,6 @@ import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js'
|
|
|
42
42
|
import { useAppearanceBootstrap } from './hooks/useAppearanceBootstrap.js'
|
|
43
43
|
import { useIdleLogout } from './hooks/useIdleLogout.js'
|
|
44
44
|
import { PageSectionEditor } from './views/page-editor/PageSectionEditor.js'
|
|
45
|
-
import { PagePreview } from './views/page-editor/PagePreview.js'
|
|
46
45
|
import {
|
|
47
46
|
AdminSectionRenderersProvider,
|
|
48
47
|
type AdminSectionRendererMap,
|
|
@@ -56,7 +55,6 @@ import { NewPostPicker } from './views/Posts/NewPostPicker.js'
|
|
|
56
55
|
import { NewPostTypeInstructions } from './views/Posts/NewPostTypeInstructions.js'
|
|
57
56
|
import { PostSectionEditor } from './views/post-editor/PostSectionEditor.js'
|
|
58
57
|
import { PostTemplateEditor } from './views/post-editor/PostTemplateEditor.js'
|
|
59
|
-
import { PostPreview } from './views/post-editor/PostPreview.js'
|
|
60
58
|
|
|
61
59
|
export interface AdminRootProps {
|
|
62
60
|
config: any
|
|
@@ -172,42 +170,6 @@ function AdminShell({
|
|
|
172
170
|
await onLogout?.()
|
|
173
171
|
}, [navigate, initialPath, onLogout])
|
|
174
172
|
|
|
175
|
-
// Open a preview route in a new browser tab so it behaves like a normal,
|
|
176
|
-
// shareable page (its own URL, no admin chrome — see the standalone branch
|
|
177
|
-
// below). The blank tab is opened synchronously inside the click gesture so
|
|
178
|
-
// popup blockers permit it; `prepare` (e.g. saving an unsaved draft) runs
|
|
179
|
-
// before we point the tab at the URL, guaranteeing the preview reflects the
|
|
180
|
-
// latest saved content. If `prepare` rejects, the tab is closed.
|
|
181
|
-
const openPreview = useCallback(
|
|
182
|
-
(path: string, prepare?: () => Promise<boolean | void> | boolean | void) => {
|
|
183
|
-
if (typeof window === 'undefined') return
|
|
184
|
-
const tab = window.open('about:blank', '_blank')
|
|
185
|
-
if (!tab) {
|
|
186
|
-
toast.error('Enable pop-ups for this site to open the preview in a new tab.')
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
const go = (ok: boolean | void) => {
|
|
190
|
-
if (ok === false) {
|
|
191
|
-
tab?.close()
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
if (tab && !tab.closed) {
|
|
195
|
-
// Sever the opener link for safety, then load the real URL.
|
|
196
|
-
try {
|
|
197
|
-
tab.opener = null
|
|
198
|
-
} catch {
|
|
199
|
-
/* cross-origin guard — never throws for same-origin admin */
|
|
200
|
-
}
|
|
201
|
-
tab.location.replace(buildHref(path))
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
Promise.resolve(prepare?.())
|
|
205
|
-
.then(go)
|
|
206
|
-
.catch(() => tab?.close())
|
|
207
|
-
},
|
|
208
|
-
[buildHref],
|
|
209
|
-
)
|
|
210
|
-
|
|
211
173
|
// Idle auto-logout (Settings → Security). The policy is read from the
|
|
212
174
|
// non-admin-gated /auth/session-policy endpoint so every member's client
|
|
213
175
|
// enforces it, not just admins. When enabled, `timeoutHours` of inactivity
|
|
@@ -459,18 +421,6 @@ function AdminShell({
|
|
|
459
421
|
config={config}
|
|
460
422
|
session={session}
|
|
461
423
|
onNavigate={navigate}
|
|
462
|
-
onOpenPreview={openPreview}
|
|
463
|
-
/>
|
|
464
|
-
)
|
|
465
|
-
}
|
|
466
|
-
const postPreview = matchRoute('/posts/:type/:id/preview')
|
|
467
|
-
if (postPreview?.type && postPreview?.id) {
|
|
468
|
-
return (
|
|
469
|
-
<PostPreview
|
|
470
|
-
postType={postPreview.type}
|
|
471
|
-
documentId={postPreview.id}
|
|
472
|
-
session={session}
|
|
473
|
-
onNavigate={navigate}
|
|
474
424
|
/>
|
|
475
425
|
)
|
|
476
426
|
}
|
|
@@ -483,7 +433,6 @@ function AdminShell({
|
|
|
483
433
|
config={config}
|
|
484
434
|
session={session}
|
|
485
435
|
onNavigate={navigate}
|
|
486
|
-
onOpenPreview={openPreview}
|
|
487
436
|
/>
|
|
488
437
|
)
|
|
489
438
|
}
|
|
@@ -514,14 +463,9 @@ function AdminShell({
|
|
|
514
463
|
config={config}
|
|
515
464
|
session={session}
|
|
516
465
|
onNavigate={navigate}
|
|
517
|
-
onOpenPreview={openPreview}
|
|
518
466
|
/>
|
|
519
467
|
)
|
|
520
468
|
}
|
|
521
|
-
const pagePreview = matchRoute('/pages/:id/preview')
|
|
522
|
-
if (pagePreview?.id) {
|
|
523
|
-
return <PagePreview documentId={pagePreview.id} onNavigate={navigate} />
|
|
524
|
-
}
|
|
525
469
|
const pageEdit = matchRoute('/pages/:id/edit')
|
|
526
470
|
if (pageEdit?.id) {
|
|
527
471
|
return (
|
|
@@ -530,7 +474,6 @@ function AdminShell({
|
|
|
530
474
|
config={config}
|
|
531
475
|
session={session}
|
|
532
476
|
onNavigate={navigate}
|
|
533
|
-
onOpenPreview={openPreview}
|
|
534
477
|
/>
|
|
535
478
|
)
|
|
536
479
|
}
|
|
@@ -542,7 +485,6 @@ function AdminShell({
|
|
|
542
485
|
config={config}
|
|
543
486
|
session={session}
|
|
544
487
|
onNavigate={navigate}
|
|
545
|
-
onOpenPreview={openPreview}
|
|
546
488
|
/>
|
|
547
489
|
)
|
|
548
490
|
}
|
|
@@ -729,18 +671,6 @@ function AdminShell({
|
|
|
729
671
|
)
|
|
730
672
|
}
|
|
731
673
|
|
|
732
|
-
// Preview routes render as standalone pages (no sidebar/header) so a
|
|
733
|
-
// new-tab preview looks like the real published page rather than an
|
|
734
|
-
// embedded admin view. Everything else gets the full admin chrome.
|
|
735
|
-
// Query string is stripped — currentPath carries it after reloads.
|
|
736
|
-
if (currentPath.split('?')[0]!.endsWith('/preview')) {
|
|
737
|
-
return (
|
|
738
|
-
<div className="bg-background h-full min-h-screen">
|
|
739
|
-
<ErrorBoundary>{renderView()}</ErrorBoundary>
|
|
740
|
-
</div>
|
|
741
|
-
)
|
|
742
|
-
}
|
|
743
|
-
|
|
744
674
|
return (
|
|
745
675
|
<Layout
|
|
746
676
|
config={config}
|
|
@@ -5,11 +5,13 @@ import { BreakpointPicker } from '../../components/BreakpointPicker.js'
|
|
|
5
5
|
import { ZoomChip } from '../../components/ZoomChip.js'
|
|
6
6
|
|
|
7
7
|
describe('BreakpointPicker', () => {
|
|
8
|
-
it('names
|
|
8
|
+
it('names canvas width, not a live preview', () => {
|
|
9
9
|
render(<BreakpointPicker value="tabletL" onChange={vi.fn()} />)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
expect(
|
|
11
|
+
screen.getByRole('button', {
|
|
12
|
+
name: 'Canvas width: Tablet Landscape, 1024 pixels. First-party layout only — use View site for the live design.',
|
|
13
|
+
}),
|
|
14
|
+
).toBeTruthy()
|
|
13
15
|
})
|
|
14
16
|
|
|
15
17
|
it('renders 4K at 2560, not the physical pixel count', () => {
|
|
@@ -3,9 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
3
3
|
import {
|
|
4
4
|
CANVAS_EDIT_TIP_STORAGE_KEY,
|
|
5
5
|
dismissCanvasEditTip,
|
|
6
|
-
injectPreviewEditChrome,
|
|
7
6
|
isCanvasEditTipDismissed,
|
|
8
|
-
PREVIEW_EDITABLE_FIELD_CHROME_CSS,
|
|
9
7
|
} from '../../lib/canvas-edit-chrome.js'
|
|
10
8
|
|
|
11
9
|
// happy-dom localStorage is non-functional in this Node setup — stub a Map.
|
|
@@ -28,29 +26,4 @@ describe('canvas-edit-chrome', () => {
|
|
|
28
26
|
expect(isCanvasEditTipDismissed()).toBe(true)
|
|
29
27
|
expect(localStorage.getItem(CANVAS_EDIT_TIP_STORAGE_KEY)).toBe('1')
|
|
30
28
|
})
|
|
31
|
-
|
|
32
|
-
it('injects editable field chrome into a same-origin iframe document', () => {
|
|
33
|
-
const iframe = document.createElement('iframe')
|
|
34
|
-
document.body.appendChild(iframe)
|
|
35
|
-
const doc = iframe.contentDocument
|
|
36
|
-
expect(doc).toBeTruthy()
|
|
37
|
-
if (!doc) return
|
|
38
|
-
|
|
39
|
-
expect(injectPreviewEditChrome(iframe)).toBe(true)
|
|
40
|
-
const style = doc.head.querySelector('style[data-prenta-edit-chrome]')
|
|
41
|
-
expect(style?.textContent).toContain('[data-cms-field]')
|
|
42
|
-
expect(style?.textContent).toContain('data-cms-field-kind')
|
|
43
|
-
expect(PREVIEW_EDITABLE_FIELD_CHROME_CSS.length).toBeGreaterThan(0)
|
|
44
|
-
expect(doc.documentElement.getAttribute('data-prenta-preview-editing')).toBe('true')
|
|
45
|
-
|
|
46
|
-
// Refresh is idempotent — one style tag.
|
|
47
|
-
expect(injectPreviewEditChrome(iframe)).toBe(true)
|
|
48
|
-
expect(doc.head.querySelectorAll('style[data-prenta-edit-chrome]')).toHaveLength(1)
|
|
49
|
-
|
|
50
|
-
iframe.remove()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('returns false when iframe has no contentDocument', () => {
|
|
54
|
-
expect(injectPreviewEditChrome(null)).toBe(false)
|
|
55
|
-
})
|
|
56
29
|
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const createPreviewToken = vi.fn()
|
|
4
|
+
vi.mock('../../lib/preview-link.js', async (importOriginal) => {
|
|
5
|
+
const actual = await importOriginal<typeof import('../../lib/preview-link.js')>()
|
|
6
|
+
return { ...actual, createPreviewToken }
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const { openPublicSiteTab } = await import('../../lib/open-public-site-tab.js')
|
|
10
|
+
const { NO_PUBLIC_SITE_URL_REASON } = await import('../../lib/view-site-chrome.js')
|
|
11
|
+
|
|
12
|
+
function mockTab() {
|
|
13
|
+
return { close: vi.fn(), location: { replace: vi.fn() }, opener: {} as unknown }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.unstubAllGlobals()
|
|
18
|
+
createPreviewToken.mockReset()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const base = {
|
|
22
|
+
siteUrl: 'https://example.com',
|
|
23
|
+
collection: 'pages',
|
|
24
|
+
documentId: 'p1',
|
|
25
|
+
slug: 'about',
|
|
26
|
+
path: '/about',
|
|
27
|
+
dirty: false,
|
|
28
|
+
saveDraft: vi.fn().mockResolvedValue(true),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('openPublicSiteTab', () => {
|
|
32
|
+
it('refuses to open the admin host when siteUrl is empty', async () => {
|
|
33
|
+
const open = vi.fn()
|
|
34
|
+
vi.stubGlobal('window', { open })
|
|
35
|
+
await expect(openPublicSiteTab({ ...base, siteUrl: '', intent: 'view-live' })).rejects.toThrow(
|
|
36
|
+
NO_PUBLIC_SITE_URL_REASON,
|
|
37
|
+
)
|
|
38
|
+
expect(open).not.toHaveBeenCalled()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('opens the live URL with no token', async () => {
|
|
42
|
+
const open = vi.fn()
|
|
43
|
+
vi.stubGlobal('window', { open })
|
|
44
|
+
await openPublicSiteTab({ ...base, intent: 'view-live' })
|
|
45
|
+
expect(open).toHaveBeenCalledWith('https://example.com/about', '_blank', 'noopener')
|
|
46
|
+
expect(createPreviewToken).not.toHaveBeenCalled()
|
|
47
|
+
expect(base.saveDraft).not.toHaveBeenCalled()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('saves a dirty draft, mints a token, and replaces about:blank', async () => {
|
|
51
|
+
const tab = mockTab()
|
|
52
|
+
const open = vi.fn().mockReturnValue(tab)
|
|
53
|
+
vi.stubGlobal('window', { open })
|
|
54
|
+
createPreviewToken.mockResolvedValue({ token: 'tok', expiresAt: 'x' })
|
|
55
|
+
await openPublicSiteTab({ ...base, intent: 'preview-draft', dirty: true })
|
|
56
|
+
expect(open).toHaveBeenCalledWith('about:blank', '_blank')
|
|
57
|
+
expect(base.saveDraft).toHaveBeenCalledOnce()
|
|
58
|
+
expect(createPreviewToken).toHaveBeenCalledWith('pages', 'p1')
|
|
59
|
+
expect(tab.location.replace).toHaveBeenCalledWith('https://example.com/about?preview=tok')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('closes the blank tab when save fails', async () => {
|
|
63
|
+
const tab = mockTab()
|
|
64
|
+
vi.stubGlobal('window', { open: vi.fn().mockReturnValue(tab) })
|
|
65
|
+
await openPublicSiteTab({
|
|
66
|
+
...base,
|
|
67
|
+
intent: 'preview-draft',
|
|
68
|
+
dirty: true,
|
|
69
|
+
saveDraft: vi.fn().mockResolvedValue(false),
|
|
70
|
+
})
|
|
71
|
+
expect(tab.close).toHaveBeenCalledOnce()
|
|
72
|
+
expect(createPreviewToken).not.toHaveBeenCalled()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -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
|
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
ADMIN_ORIGIN_SITE_URL_REASON,
|
|
4
|
+
NO_PUBLIC_SITE_URL_REASON,
|
|
5
|
+
resolveViewSiteChrome,
|
|
6
|
+
} from '../../lib/view-site-chrome.js'
|
|
7
|
+
|
|
8
|
+
const base = {
|
|
9
|
+
dirty: false,
|
|
10
|
+
hasPublicSiteUrl: true,
|
|
11
|
+
hasIdentity: true,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('resolveViewSiteChrome', () => {
|
|
15
|
+
it('previews a draft and shows Share', () => {
|
|
16
|
+
expect(resolveViewSiteChrome({ ...base, status: 'DRAFT' })).toEqual({
|
|
17
|
+
viewEnabled: true,
|
|
18
|
+
primaryIntent: 'preview-draft',
|
|
19
|
+
showViewLive: false,
|
|
20
|
+
shareVisible: true,
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('treats scheduled and archived like a draft', () => {
|
|
25
|
+
for (const status of ['SCHEDULED', 'ARCHIVED'] as const) {
|
|
26
|
+
expect(resolveViewSiteChrome({ ...base, status }).primaryIntent).toBe('preview-draft')
|
|
27
|
+
expect(resolveViewSiteChrome({ ...base, status }).shareVisible).toBe(true)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('opens live on a clean published document and hides Share', () => {
|
|
32
|
+
expect(resolveViewSiteChrome({ ...base, status: 'PUBLISHED' })).toEqual({
|
|
33
|
+
viewEnabled: true,
|
|
34
|
+
primaryIntent: 'view-live',
|
|
35
|
+
showViewLive: false,
|
|
36
|
+
shareVisible: false,
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('previews unpublished changes when published and dirty, and keeps View live + Share', () => {
|
|
41
|
+
expect(resolveViewSiteChrome({ ...base, status: 'PUBLISHED', dirty: true })).toEqual({
|
|
42
|
+
viewEnabled: true,
|
|
43
|
+
primaryIntent: 'preview-draft',
|
|
44
|
+
showViewLive: true,
|
|
45
|
+
shareVisible: true,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('disables View site and Share when the public site URL is missing', () => {
|
|
50
|
+
const chrome = resolveViewSiteChrome({
|
|
51
|
+
...base,
|
|
52
|
+
status: 'DRAFT',
|
|
53
|
+
hasPublicSiteUrl: false,
|
|
54
|
+
})
|
|
55
|
+
expect(chrome.viewEnabled).toBe(false)
|
|
56
|
+
expect(chrome.viewDisabledReason).toBe(NO_PUBLIC_SITE_URL_REASON)
|
|
57
|
+
expect(chrome.shareVisible).toBe(true)
|
|
58
|
+
})
|
|
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
|
+
|
|
71
|
+
it('hides View site and Share when the document is unsaved', () => {
|
|
72
|
+
const chrome = resolveViewSiteChrome({
|
|
73
|
+
...base,
|
|
74
|
+
status: 'DRAFT',
|
|
75
|
+
hasIdentity: false,
|
|
76
|
+
})
|
|
77
|
+
expect(chrome.viewEnabled).toBe(false)
|
|
78
|
+
expect(chrome.shareVisible).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -71,4 +71,9 @@ describe('ADMIN_STATIC_ROUTE_PATTERNS stays in sync with AdminRoot', () => {
|
|
|
71
71
|
const missing = [...new Set(literals)].filter((p) => !known.has(p))
|
|
72
72
|
expect(missing).toEqual([])
|
|
73
73
|
})
|
|
74
|
+
|
|
75
|
+
it('does not keep the retired admin-renderer preview routes', () => {
|
|
76
|
+
expect(ADMIN_STATIC_ROUTE_PATTERNS).not.toContain('/pages/:id/preview')
|
|
77
|
+
expect(ADMIN_STATIC_ROUTE_PATTERNS).not.toContain('/posts/:type/:id/preview')
|
|
78
|
+
})
|
|
74
79
|
})
|
|
@@ -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(
|
|
@@ -58,23 +58,4 @@ describe('canvas surface is shared, not copied', () => {
|
|
|
58
58
|
expect(read(rel), `${rel} re-implements the scale`).not.toMatch(/fitScale/)
|
|
59
59
|
}
|
|
60
60
|
})
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* `SitePreviewFrame` was the third copy of `clientWidth - 48` and the one
|
|
64
|
-
* followup 2 left alone, because it scales an IFRAME rather than a rendered
|
|
65
|
-
* surface and cannot render `CanvasSurface`. It can still measure the same
|
|
66
|
-
* way, and now does: the padding arithmetic is gone even though the
|
|
67
|
-
* component is not shared.
|
|
68
|
-
*
|
|
69
|
-
* Kept apart from CANVASES on purpose — the assertion here is about the
|
|
70
|
-
* measurement, not about rendering the shared surface, and folding it in
|
|
71
|
-
* would make the other two tests claim something untrue about it.
|
|
72
|
-
*/
|
|
73
|
-
it('the site preview measures from a sentinel too, though it cannot share the surface', () => {
|
|
74
|
-
const preview = read('src/views/page-editor/SitePreviewFrame.tsx')
|
|
75
|
-
expect(preview, 'SitePreviewFrame still does padding arithmetic').not.toMatch(
|
|
76
|
-
/clientWidth\s*-\s*\d/,
|
|
77
|
-
)
|
|
78
|
-
expect(preview, 'SitePreviewFrame does not measure from a sentinel').toMatch(/sentinelRef/)
|
|
79
|
-
})
|
|
80
61
|
})
|