@actuate-media/cms-admin 0.58.1 → 0.60.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 (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/__tests__/lib/editor-preview.test.d.ts +2 -0
  3. package/dist/__tests__/lib/editor-preview.test.d.ts.map +1 -0
  4. package/dist/__tests__/lib/editor-preview.test.js +18 -0
  5. package/dist/__tests__/lib/editor-preview.test.js.map +1 -0
  6. package/dist/__tests__/lib/post-editor-service.test.js +16 -0
  7. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
  8. package/dist/__tests__/views/dashboard.test.js +23 -0
  9. package/dist/__tests__/views/dashboard.test.js.map +1 -1
  10. package/dist/__tests__/views/use-site-preview.render.test.js +10 -1
  11. package/dist/__tests__/views/use-site-preview.render.test.js.map +1 -1
  12. package/dist/components/PlanUpgradeCallout.d.ts +7 -0
  13. package/dist/components/PlanUpgradeCallout.d.ts.map +1 -0
  14. package/dist/components/PlanUpgradeCallout.js +8 -0
  15. package/dist/components/PlanUpgradeCallout.js.map +1 -0
  16. package/dist/components/seo/SeoIssueFixPanel.d.ts +3 -1
  17. package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
  18. package/dist/components/seo/SeoIssueFixPanel.js +8 -3
  19. package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
  20. package/dist/lib/editor-preview.d.ts +4 -0
  21. package/dist/lib/editor-preview.d.ts.map +1 -0
  22. package/dist/lib/editor-preview.js +10 -0
  23. package/dist/lib/editor-preview.js.map +1 -0
  24. package/dist/lib/plan.d.ts +12 -0
  25. package/dist/lib/plan.d.ts.map +1 -0
  26. package/dist/lib/plan.js +30 -0
  27. package/dist/lib/plan.js.map +1 -0
  28. package/dist/lib/post-editor-service.d.ts.map +1 -1
  29. package/dist/lib/post-editor-service.js +4 -1
  30. package/dist/lib/post-editor-service.js.map +1 -1
  31. package/dist/lib/seo-service.d.ts.map +1 -1
  32. package/dist/lib/seo-service.js +11 -3
  33. package/dist/lib/seo-service.js.map +1 -1
  34. package/dist/views/SEO.d.ts.map +1 -1
  35. package/dist/views/SEO.js +6 -1
  36. package/dist/views/SEO.js.map +1 -1
  37. package/dist/views/Users.js +1 -1
  38. package/dist/views/Users.js.map +1 -1
  39. package/dist/views/page-editor/EditorCanvas.d.ts +2 -1
  40. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  41. package/dist/views/page-editor/EditorCanvas.js +2 -2
  42. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  43. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  44. package/dist/views/page-editor/PageSectionEditor.js +4 -1
  45. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  46. package/dist/views/page-editor/StructureNotice.d.ts +4 -3
  47. package/dist/views/page-editor/StructureNotice.d.ts.map +1 -1
  48. package/dist/views/page-editor/StructureNotice.js +9 -7
  49. package/dist/views/page-editor/StructureNotice.js.map +1 -1
  50. package/dist/views/page-editor/useSitePreview.d.ts +3 -1
  51. package/dist/views/page-editor/useSitePreview.d.ts.map +1 -1
  52. package/dist/views/page-editor/useSitePreview.js +44 -31
  53. package/dist/views/page-editor/useSitePreview.js.map +1 -1
  54. package/dist/views/post-editor/PostEditorCanvas.d.ts +3 -1
  55. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  56. package/dist/views/post-editor/PostEditorCanvas.js +2 -2
  57. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  58. package/dist/views/post-editor/PostSectionEditor.d.ts +8 -0
  59. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  60. package/dist/views/post-editor/PostSectionEditor.js +4 -1
  61. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  62. package/dist/views/seo/AuditTab.d.ts +3 -1
  63. package/dist/views/seo/AuditTab.d.ts.map +1 -1
  64. package/dist/views/seo/AuditTab.js +6 -3
  65. package/dist/views/seo/AuditTab.js.map +1 -1
  66. package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -1
  67. package/dist/views/settings/MonthlyUsageCard.js +1 -1
  68. package/dist/views/settings/MonthlyUsageCard.js.map +1 -1
  69. package/package.json +2 -2
  70. package/src/__tests__/lib/editor-preview.test.ts +21 -0
  71. package/src/__tests__/lib/post-editor-service.test.ts +17 -0
  72. package/src/__tests__/views/dashboard.test.tsx +26 -0
  73. package/src/__tests__/views/use-site-preview.render.test.tsx +12 -0
  74. package/src/components/PlanUpgradeCallout.tsx +42 -0
  75. package/src/components/seo/SeoIssueFixPanel.tsx +37 -18
  76. package/src/lib/editor-preview.ts +12 -0
  77. package/src/lib/plan.ts +44 -0
  78. package/src/lib/post-editor-service.ts +4 -1
  79. package/src/lib/seo-service.ts +14 -3
  80. package/src/views/SEO.tsx +7 -0
  81. package/src/views/Users.tsx +1 -1
  82. package/src/views/page-editor/EditorCanvas.tsx +6 -1
  83. package/src/views/page-editor/PageSectionEditor.tsx +5 -0
  84. package/src/views/page-editor/StructureNotice.tsx +42 -8
  85. package/src/views/page-editor/useSitePreview.ts +47 -31
  86. package/src/views/post-editor/PostEditorCanvas.tsx +7 -1
  87. package/src/views/post-editor/PostSectionEditor.tsx +6 -0
  88. package/src/views/seo/AuditTab.tsx +23 -10
  89. package/src/views/settings/MonthlyUsageCard.tsx +2 -1
@@ -21,6 +21,8 @@ interface UseSitePreviewOptions {
21
21
  saveState: SaveState
22
22
  /** Persist the draft; resolves `false` when the save failed. */
23
23
  saveDraft: () => Promise<boolean>
24
+ /** Open Site preview on load when the document is saved (has id + slug). */
25
+ defaultActive?: boolean
24
26
  }
25
27
 
26
28
  export interface SitePreviewState {
@@ -49,12 +51,55 @@ export function useSitePreview({
49
51
  dirty,
50
52
  saveState,
51
53
  saveDraft,
54
+ defaultActive = false,
52
55
  }: UseSitePreviewOptions): SitePreviewState {
53
56
  const [active, setActive] = useState(false)
54
57
  const [url, setUrl] = useState<string | null>(null)
55
58
  const [reloadKey, setReloadKey] = useState(0)
56
59
  const tokenRef = useRef<string | null>(null)
57
60
  const prevSaveState = useRef(saveState)
61
+ const autoOpenRef = useRef(false)
62
+
63
+ const activatePreview = useCallback(async () => {
64
+ if (!documentId || !slug) {
65
+ toast.error('Save the page before opening the site preview')
66
+ return false
67
+ }
68
+ setActive(true)
69
+ setUrl(null)
70
+ try {
71
+ if (dirty) {
72
+ const ok = await saveDraft()
73
+ if (!ok) {
74
+ setActive(false)
75
+ return false
76
+ }
77
+ }
78
+ const { token } = await createPreviewToken(collection, documentId)
79
+ tokenRef.current = token
80
+ setUrl(
81
+ buildPublicPreviewUrl({
82
+ siteUrl: resolveSiteUrl(siteUrl),
83
+ urlPrefix: urlPrefix ?? '',
84
+ slug,
85
+ token,
86
+ }),
87
+ )
88
+ return true
89
+ } catch (err) {
90
+ toast.error(err instanceof Error ? err.message : 'Failed to open site preview')
91
+ setActive(false)
92
+ return false
93
+ }
94
+ }, [collection, documentId, slug, urlPrefix, siteUrl, dirty, saveDraft])
95
+
96
+ // Opt-in default: open the iframe once a saved document is loaded.
97
+ useEffect(() => {
98
+ if (!defaultActive || autoOpenRef.current) return
99
+ if (!documentId || !slug) return
100
+ autoOpenRef.current = true
101
+ void activatePreview()
102
+ }, [defaultActive, documentId, slug, activatePreview])
58
103
 
59
104
  // After a save completes while the preview is open, rebuild the URL (the
60
105
  // slug may have changed in settings — the token is bound to the document
@@ -81,37 +126,8 @@ export function useSitePreview({
81
126
  setActive(false)
82
127
  return
83
128
  }
84
- if (!documentId || !slug) {
85
- toast.error('Save the page before opening the site preview')
86
- return
87
- }
88
- setActive(true)
89
- setUrl(null)
90
- void (async () => {
91
- try {
92
- if (dirty) {
93
- const ok = await saveDraft()
94
- if (!ok) {
95
- setActive(false)
96
- return
97
- }
98
- }
99
- const { token } = await createPreviewToken(collection, documentId)
100
- tokenRef.current = token
101
- setUrl(
102
- buildPublicPreviewUrl({
103
- siteUrl: resolveSiteUrl(siteUrl),
104
- urlPrefix: urlPrefix ?? '',
105
- slug,
106
- token,
107
- }),
108
- )
109
- } catch (err) {
110
- toast.error(err instanceof Error ? err.message : 'Failed to open site preview')
111
- setActive(false)
112
- }
113
- })()
114
- }, [active, collection, documentId, slug, urlPrefix, siteUrl, dirty, saveDraft])
129
+ void activatePreview()
130
+ }, [active, activatePreview])
115
131
 
116
132
  return { active, url, reloadKey, toggle }
117
133
  }
@@ -18,6 +18,8 @@ interface PostEditorCanvasProps {
18
18
  onAddSection: () => void
19
19
  /** Section types rendered as placeholders — shows the "Structure view" notice. */
20
20
  structuralTypes?: string[]
21
+ /** Site preview toggle is available — show WYSIWYG nudge on the canvas. */
22
+ sitePreviewAvailable?: boolean
21
23
  }
22
24
 
23
25
  /**
@@ -35,6 +37,7 @@ export function PostEditorCanvas({
35
37
  onSelect,
36
38
  onAddSection,
37
39
  structuralTypes = [],
40
+ sitePreviewAvailable = false,
38
41
  }: PostEditorCanvasProps) {
39
42
  const vp = getViewport(viewport)
40
43
  const containerRef = useRef<HTMLDivElement>(null)
@@ -74,7 +77,10 @@ export function PostEditorCanvas({
74
77
 
75
78
  return (
76
79
  <div ref={containerRef} className="bg-muted h-full overflow-auto p-6">
77
- <StructureNotice structuralTypes={structuralTypes} />
80
+ <StructureNotice
81
+ structuralTypes={structuralTypes}
82
+ sitePreviewAvailable={sitePreviewAvailable}
83
+ />
78
84
  <div
79
85
  className="mx-auto"
80
86
  style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
@@ -48,6 +48,7 @@ import { SectionsPanel } from '../page-editor/SectionsPanel.js'
48
48
  import { SectionInspector } from '../page-editor/SectionInspector.js'
49
49
  import { AddSectionModal } from '../page-editor/AddSectionModal.js'
50
50
  import { ValidationSummary } from '../page-editor/ValidationSummary.js'
51
+ import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
51
52
  import { PostEditorCanvas } from './PostEditorCanvas.js'
52
53
  import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'
53
54
 
@@ -63,6 +64,7 @@ interface EditorConfig {
63
64
  | undefined
64
65
  >
65
66
  seo?: { siteUrl?: string }
67
+ admin?: { editor?: { defaultSitePreview?: boolean | { pages?: boolean; posts?: boolean } } }
66
68
  }
67
69
 
68
70
  /** The slice of the admin session this editor reads. */
@@ -324,8 +326,11 @@ export function PostSectionEditor({
324
326
  dirty,
325
327
  saveState,
326
328
  saveDraft: handleSaveDraft,
329
+ defaultActive: resolveDefaultSitePreview(config?.admin?.editor, 'posts'),
327
330
  })
328
331
 
332
+ const sitePreviewAvailable = Boolean(post?.id && post?.slug)
333
+
329
334
  const requestPublish = useCallback(() => {
330
335
  if (!post) return
331
336
  const result = validatePost(post, true)
@@ -570,6 +575,7 @@ export function PostSectionEditor({
570
575
  onSelect={setSelectedId}
571
576
  onAddSection={() => setAddOpen(true)}
572
577
  structuralTypes={structuralTypes}
578
+ sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
573
579
  />
574
580
  )}
575
581
  </div>
@@ -26,6 +26,8 @@ import {
26
26
  } from '../../components/seo/primitives.js'
27
27
  import { SeoIssueFixDrawer } from '../../components/seo/SeoIssueFixDrawer.js'
28
28
  import { SeoIssueFixPanel, SeoIssueFixToggle } from '../../components/seo/SeoIssueFixPanel.js'
29
+ import { PlanUpgradeCallout } from '../../components/PlanUpgradeCallout.js'
30
+ import { hasPlanFeature, type PlanInfo } from '../../lib/plan.js'
29
31
  import { useSeoResource } from './useSeoResource.js'
30
32
 
31
33
  const SEVERITY_FILTERS: { id: SeoSeverity | 'all'; label: string }[] = [
@@ -95,10 +97,12 @@ export function AuditTab({
95
97
  refetchKey = 0,
96
98
  initialIssueId,
97
99
  onNavigate,
100
+ planInfo,
98
101
  }: {
99
102
  refetchKey?: number
100
103
  initialIssueId?: string | null
101
104
  onNavigate?: (path: string) => void
105
+ planInfo?: PlanInfo | null
102
106
  }) {
103
107
  const [severity, setSeverity] = useState<SeoSeverity | 'all'>('all')
104
108
  const [status, setStatus] = useState<SeoIssueStatus | 'all'>('open')
@@ -119,6 +123,7 @@ export function AuditTab({
119
123
  () => issues.filter((i) => supportsSeoContentBrief(i.type)).length,
120
124
  [issues],
121
125
  )
126
+ const batchBriefsEnabled = hasPlanFeature(planInfo, 'seo.batchContentBriefs')
122
127
 
123
128
  function toggleExpanded(issueId: string) {
124
129
  setExpandedIssueId((current) => (current === issueId ? null : issueId))
@@ -231,16 +236,23 @@ export function AuditTab({
231
236
  </div>
232
237
  </div>
233
238
  <div className="flex flex-wrap gap-2">
234
- <button
235
- type="button"
236
- className={btnSecondary}
237
- onClick={handleBatchGenerateBriefs}
238
- disabled={batchGenerating || loading || briefEligibleCount === 0}
239
- aria-busy={batchGenerating}
240
- >
241
- <Sparkles className="h-4 w-4" aria-hidden />
242
- {batchGenerating ? 'Generating…' : 'Generate briefs'}
243
- </button>
239
+ {batchBriefsEnabled ? (
240
+ <button
241
+ type="button"
242
+ className={btnSecondary}
243
+ onClick={handleBatchGenerateBriefs}
244
+ disabled={batchGenerating || loading || briefEligibleCount === 0}
245
+ aria-busy={batchGenerating}
246
+ >
247
+ <Sparkles className="h-4 w-4" aria-hidden />
248
+ {batchGenerating ? 'Generating…' : 'Generate briefs'}
249
+ </button>
250
+ ) : (
251
+ <PlanUpgradeCallout
252
+ feature="seo.batchContentBriefs"
253
+ upgradeUrl={planInfo?.upgradeUrl}
254
+ />
255
+ )}
244
256
  <button
245
257
  type="button"
246
258
  className={btnSecondary}
@@ -330,6 +342,7 @@ export function AuditTab({
330
342
  onChanged={handleChanged}
331
343
  onNavigate={onNavigate}
332
344
  onOpenDetails={() => setDetailIssueId(issue.id)}
345
+ planInfo={planInfo}
333
346
  />
334
347
  </Fragment>
335
348
  )
@@ -104,7 +104,8 @@ export function MonthlyUsageCard({
104
104
  </p>
105
105
  ) : (
106
106
  <p className="text-muted-foreground mt-1 text-sm">
107
- Leave blank for no limit. Enforcement and alerts arrive with usage metering.
107
+ Leave blank for no limit. When set, new AI requests are blocked with HTTP 429 after
108
+ the limit is reached.
108
109
  </p>
109
110
  )}
110
111
  </div>