@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.
Files changed (149) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +5 -56
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/components/breakpoint-controls.render.test.js +4 -4
  6. package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -1
  7. package/dist/__tests__/lib/canvas-edit-chrome.test.js +1 -22
  8. package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -1
  9. package/dist/__tests__/lib/open-public-site-tab.test.d.ts +2 -0
  10. package/dist/__tests__/lib/open-public-site-tab.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/open-public-site-tab.test.js +64 -0
  12. package/dist/__tests__/lib/open-public-site-tab.test.js.map +1 -0
  13. package/dist/__tests__/lib/preview-bridge.test.js +19 -79
  14. package/dist/__tests__/lib/preview-bridge.test.js.map +1 -1
  15. package/dist/__tests__/lib/view-site-chrome.test.d.ts +2 -0
  16. package/dist/__tests__/lib/view-site-chrome.test.d.ts.map +1 -0
  17. package/dist/__tests__/lib/view-site-chrome.test.js +69 -0
  18. package/dist/__tests__/lib/view-site-chrome.test.js.map +1 -0
  19. package/dist/__tests__/router/admin-routes.test.js +4 -0
  20. package/dist/__tests__/router/admin-routes.test.js.map +1 -1
  21. package/dist/__tests__/views/canvas-review-mode.render.test.js +11 -1
  22. package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -1
  23. package/dist/__tests__/views/canvas-surface-shared.test.js +0 -16
  24. package/dist/__tests__/views/canvas-surface-shared.test.js.map +1 -1
  25. package/dist/__tests__/views/editor-top-bar.render.test.js +52 -22
  26. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  27. package/dist/__tests__/views/page-section-editor.test.js +53 -18
  28. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  29. package/dist/__tests__/views/post-section-editor.test.js +23 -0
  30. package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
  31. package/dist/components/BreakpointPicker.d.ts +3 -2
  32. package/dist/components/BreakpointPicker.d.ts.map +1 -1
  33. package/dist/components/BreakpointPicker.js +5 -3
  34. package/dist/components/BreakpointPicker.js.map +1 -1
  35. package/dist/components/CanvasEditTip.js +1 -1
  36. package/dist/components/CanvasEditTip.js.map +1 -1
  37. package/dist/lib/canvas-edit-chrome.d.ts +0 -17
  38. package/dist/lib/canvas-edit-chrome.d.ts.map +1 -1
  39. package/dist/lib/canvas-edit-chrome.js +0 -62
  40. package/dist/lib/canvas-edit-chrome.js.map +1 -1
  41. package/dist/lib/editor-preview.d.ts +4 -1
  42. package/dist/lib/editor-preview.d.ts.map +1 -1
  43. package/dist/lib/editor-preview.js +4 -1
  44. package/dist/lib/editor-preview.js.map +1 -1
  45. package/dist/lib/open-public-site-tab.d.ts +14 -0
  46. package/dist/lib/open-public-site-tab.d.ts.map +1 -0
  47. package/dist/lib/open-public-site-tab.js +46 -0
  48. package/dist/lib/open-public-site-tab.js.map +1 -0
  49. package/dist/lib/preview-bridge.d.ts +7 -74
  50. package/dist/lib/preview-bridge.d.ts.map +1 -1
  51. package/dist/lib/preview-bridge.js +12 -73
  52. package/dist/lib/preview-bridge.js.map +1 -1
  53. package/dist/lib/view-site-chrome.d.ts +19 -0
  54. package/dist/lib/view-site-chrome.d.ts.map +1 -0
  55. package/dist/lib/view-site-chrome.js +24 -0
  56. package/dist/lib/view-site-chrome.js.map +1 -0
  57. package/dist/router/admin-routes.d.ts.map +1 -1
  58. package/dist/router/admin-routes.js +0 -2
  59. package/dist/router/admin-routes.js.map +1 -1
  60. package/dist/views/page-editor/EditorCanvas.d.ts +2 -3
  61. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  62. package/dist/views/page-editor/EditorCanvas.js +2 -2
  63. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  64. package/dist/views/page-editor/EditorTopBar.d.ts +15 -17
  65. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  66. package/dist/views/page-editor/EditorTopBar.js +20 -21
  67. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  68. package/dist/views/page-editor/PageSectionEditor.d.ts +1 -7
  69. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  70. package/dist/views/page-editor/PageSectionEditor.js +36 -141
  71. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  72. package/dist/views/page-editor/StructureNotice.d.ts +2 -5
  73. package/dist/views/page-editor/StructureNotice.d.ts.map +1 -1
  74. package/dist/views/page-editor/StructureNotice.js +6 -10
  75. package/dist/views/page-editor/StructureNotice.js.map +1 -1
  76. package/dist/views/page-editor/sections/SectionRenderer.d.ts +1 -1
  77. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  78. package/dist/views/page-editor/sections/SectionRenderer.js +2 -1
  79. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  80. package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -4
  81. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  82. package/dist/views/post-editor/PostEditorCanvas.js +3 -3
  83. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  84. package/dist/views/post-editor/PostSectionEditor.d.ts +1 -15
  85. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  86. package/dist/views/post-editor/PostSectionEditor.js +36 -128
  87. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  88. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  89. package/dist/views/settings/SeoSettingsTab.js +2 -1
  90. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  91. package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
  92. package/dist/views/settings/SiteInformationCard.js +3 -1
  93. package/dist/views/settings/SiteInformationCard.js.map +1 -1
  94. package/package.json +6 -6
  95. package/src/AdminRoot.tsx +0 -70
  96. package/src/__tests__/components/breakpoint-controls.render.test.tsx +6 -4
  97. package/src/__tests__/lib/canvas-edit-chrome.test.ts +0 -27
  98. package/src/__tests__/lib/open-public-site-tab.test.ts +74 -0
  99. package/src/__tests__/lib/preview-bridge.test.ts +21 -110
  100. package/src/__tests__/lib/view-site-chrome.test.ts +80 -0
  101. package/src/__tests__/router/admin-routes.test.ts +5 -0
  102. package/src/__tests__/views/canvas-review-mode.render.test.tsx +14 -1
  103. package/src/__tests__/views/canvas-surface-shared.test.ts +0 -19
  104. package/src/__tests__/views/editor-top-bar.render.test.tsx +104 -22
  105. package/src/__tests__/views/page-section-editor.test.tsx +65 -22
  106. package/src/__tests__/views/post-section-editor.test.tsx +35 -0
  107. package/src/components/BreakpointPicker.tsx +6 -3
  108. package/src/components/CanvasEditTip.tsx +2 -2
  109. package/src/lib/canvas-edit-chrome.ts +0 -64
  110. package/src/lib/editor-preview.ts +4 -1
  111. package/src/lib/open-public-site-tab.ts +57 -0
  112. package/src/lib/preview-bridge.ts +11 -157
  113. package/src/lib/view-site-chrome.ts +46 -0
  114. package/src/router/admin-routes.ts +0 -2
  115. package/src/styles/theme.css +4 -4
  116. package/src/views/page-editor/EditorCanvas.tsx +2 -7
  117. package/src/views/page-editor/EditorTopBar.tsx +85 -82
  118. package/src/views/page-editor/PageSectionEditor.tsx +83 -220
  119. package/src/views/page-editor/StructureNotice.tsx +7 -42
  120. package/src/views/page-editor/sections/SectionRenderer.tsx +5 -2
  121. package/src/views/post-editor/PostEditorCanvas.tsx +3 -9
  122. package/src/views/post-editor/PostSectionEditor.tsx +71 -195
  123. package/src/views/settings/SeoSettingsTab.tsx +21 -0
  124. package/src/views/settings/SiteInformationCard.tsx +7 -0
  125. package/dist/__tests__/views/use-site-preview.render.test.d.ts +0 -2
  126. package/dist/__tests__/views/use-site-preview.render.test.d.ts.map +0 -1
  127. package/dist/__tests__/views/use-site-preview.render.test.js +0 -120
  128. package/dist/__tests__/views/use-site-preview.render.test.js.map +0 -1
  129. package/dist/views/page-editor/PagePreview.d.ts +0 -13
  130. package/dist/views/page-editor/PagePreview.d.ts.map +0 -1
  131. package/dist/views/page-editor/PagePreview.js +0 -46
  132. package/dist/views/page-editor/PagePreview.js.map +0 -1
  133. package/dist/views/page-editor/SitePreviewFrame.d.ts +0 -43
  134. package/dist/views/page-editor/SitePreviewFrame.d.ts.map +0 -1
  135. package/dist/views/page-editor/SitePreviewFrame.js +0 -165
  136. package/dist/views/page-editor/SitePreviewFrame.js.map +0 -1
  137. package/dist/views/page-editor/useSitePreview.d.ts +0 -47
  138. package/dist/views/page-editor/useSitePreview.d.ts.map +0 -1
  139. package/dist/views/page-editor/useSitePreview.js +0 -96
  140. package/dist/views/page-editor/useSitePreview.js.map +0 -1
  141. package/dist/views/post-editor/PostPreview.d.ts +0 -14
  142. package/dist/views/post-editor/PostPreview.d.ts.map +0 -1
  143. package/dist/views/post-editor/PostPreview.js +0 -63
  144. package/dist/views/post-editor/PostPreview.js.map +0 -1
  145. package/src/__tests__/views/use-site-preview.render.test.tsx +0 -164
  146. package/src/views/page-editor/PagePreview.tsx +0 -105
  147. package/src/views/page-editor/SitePreviewFrame.tsx +0 -250
  148. package/src/views/page-editor/useSitePreview.ts +0 -158
  149. 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 the current breakpoint and its width to assistive tech', () => {
8
+ it('names canvas width, not a live preview', () => {
9
9
  render(<BreakpointPicker value="tabletL" onChange={vi.fn()} />)
10
- // The visible label collapses to an abbreviation at narrow header widths,
11
- // so the accessible name has to carry the full fact on its own.
12
- expect(screen.getByRole('button', { name: /Tablet Landscape, 1024 pixels/i })).toBeTruthy()
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('accepts prenta-preview:sections payloads', () => {
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
  })