@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.
- package/CHANGELOG.md +16 -0
- package/dist/__tests__/lib/editor-preview.test.d.ts +2 -0
- package/dist/__tests__/lib/editor-preview.test.d.ts.map +1 -0
- package/dist/__tests__/lib/editor-preview.test.js +18 -0
- package/dist/__tests__/lib/editor-preview.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +16 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
- package/dist/__tests__/views/dashboard.test.js +23 -0
- package/dist/__tests__/views/dashboard.test.js.map +1 -1
- package/dist/__tests__/views/use-site-preview.render.test.js +10 -1
- package/dist/__tests__/views/use-site-preview.render.test.js.map +1 -1
- package/dist/components/PlanUpgradeCallout.d.ts +7 -0
- package/dist/components/PlanUpgradeCallout.d.ts.map +1 -0
- package/dist/components/PlanUpgradeCallout.js +8 -0
- package/dist/components/PlanUpgradeCallout.js.map +1 -0
- package/dist/components/seo/SeoIssueFixPanel.d.ts +3 -1
- package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +8 -3
- package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
- package/dist/lib/editor-preview.d.ts +4 -0
- package/dist/lib/editor-preview.d.ts.map +1 -0
- package/dist/lib/editor-preview.js +10 -0
- package/dist/lib/editor-preview.js.map +1 -0
- package/dist/lib/plan.d.ts +12 -0
- package/dist/lib/plan.d.ts.map +1 -0
- package/dist/lib/plan.js +30 -0
- package/dist/lib/plan.js.map +1 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +4 -1
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +11 -3
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +6 -1
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts +2 -1
- 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/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +4 -1
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/StructureNotice.d.ts +4 -3
- package/dist/views/page-editor/StructureNotice.d.ts.map +1 -1
- package/dist/views/page-editor/StructureNotice.js +9 -7
- package/dist/views/page-editor/StructureNotice.js.map +1 -1
- package/dist/views/page-editor/useSitePreview.d.ts +3 -1
- package/dist/views/page-editor/useSitePreview.d.ts.map +1 -1
- package/dist/views/page-editor/useSitePreview.js +44 -31
- package/dist/views/page-editor/useSitePreview.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +3 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +2 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts +8 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +4 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/seo/AuditTab.d.ts +3 -1
- package/dist/views/seo/AuditTab.d.ts.map +1 -1
- package/dist/views/seo/AuditTab.js +6 -3
- package/dist/views/seo/AuditTab.js.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.js +1 -1
- package/dist/views/settings/MonthlyUsageCard.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/lib/editor-preview.test.ts +21 -0
- package/src/__tests__/lib/post-editor-service.test.ts +17 -0
- package/src/__tests__/views/dashboard.test.tsx +26 -0
- package/src/__tests__/views/use-site-preview.render.test.tsx +12 -0
- package/src/components/PlanUpgradeCallout.tsx +42 -0
- package/src/components/seo/SeoIssueFixPanel.tsx +37 -18
- package/src/lib/editor-preview.ts +12 -0
- package/src/lib/plan.ts +44 -0
- package/src/lib/post-editor-service.ts +4 -1
- package/src/lib/seo-service.ts +14 -3
- package/src/views/SEO.tsx +7 -0
- package/src/views/Users.tsx +1 -1
- package/src/views/page-editor/EditorCanvas.tsx +6 -1
- package/src/views/page-editor/PageSectionEditor.tsx +5 -0
- package/src/views/page-editor/StructureNotice.tsx +42 -8
- package/src/views/page-editor/useSitePreview.ts +47 -31
- package/src/views/post-editor/PostEditorCanvas.tsx +7 -1
- package/src/views/post-editor/PostSectionEditor.tsx +6 -0
- package/src/views/seo/AuditTab.tsx +23 -10
- 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
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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.
|
|
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>
|