@actuate-media/cms-admin 0.75.0 → 0.76.1
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 +34 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.js +46 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +2 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
- package/dist/__tests__/router/navigation-blocker.test.d.ts +2 -0
- package/dist/__tests__/router/navigation-blocker.test.d.ts.map +1 -0
- package/dist/__tests__/router/navigation-blocker.test.js +69 -0
- package/dist/__tests__/router/navigation-blocker.test.js.map +1 -0
- package/dist/__tests__/views/article-body-section.render.test.d.ts +2 -0
- package/dist/__tests__/views/article-body-section.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/article-body-section.render.test.js +24 -0
- package/dist/__tests__/views/article-body-section.render.test.js.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +12 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/post-fields-modal.render.test.js +2 -0
- package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +17 -0
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +2 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +4 -0
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/post-editor-service.d.ts +3 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +4 -0
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/router/index.d.ts +13 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +49 -0
- package/dist/router/index.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +4 -9
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +3 -0
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +7 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +35 -4
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +38 -13
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/ArticleBodySection.js +1 -1
- package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +38 -12
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.js +3 -10
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
- package/package.json +4 -3
- package/src/__tests__/layout/sidebar-plugin-nav.test.ts +50 -0
- package/src/__tests__/lib/post-editor-service.test.ts +2 -0
- package/src/__tests__/router/navigation-blocker.test.ts +80 -0
- package/src/__tests__/views/article-body-section.render.test.tsx +32 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +24 -0
- package/src/__tests__/views/post-fields-modal.render.test.tsx +2 -0
- package/src/layout/Sidebar.tsx +19 -0
- package/src/lib/page-editor-service.ts +8 -0
- package/src/lib/post-editor-service.ts +8 -0
- package/src/router/index.ts +61 -0
- package/src/styles/theme.css +91 -0
- package/src/views/DocumentEdit.tsx +4 -8
- package/src/views/page-builder/PageBuilder.tsx +4 -0
- package/src/views/page-editor/EditorTopBar.tsx +69 -0
- package/src/views/page-editor/PageSectionEditor.tsx +58 -10
- package/src/views/page-editor/sections/ArticleBodySection.tsx +1 -4
- package/src/views/post-editor/PostSectionEditor.tsx +58 -10
- package/src/views/post-editor/PostTemplateEditor.tsx +3 -9
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
Calendar,
|
|
4
5
|
Check,
|
|
5
6
|
ExternalLink,
|
|
6
7
|
Eye,
|
|
@@ -65,6 +66,12 @@ interface EditorTopBarProps {
|
|
|
65
66
|
seoScore?: number
|
|
66
67
|
onSaveDraft: () => void
|
|
67
68
|
onPublish: () => void
|
|
69
|
+
/** Open the schedule-publish dialog. Omitted when the document is unsaved. */
|
|
70
|
+
onSchedule?: () => void
|
|
71
|
+
/** When false, Schedule stays disabled (e.g. unsaved document). Defaults to true. */
|
|
72
|
+
scheduleReady?: boolean
|
|
73
|
+
scheduledAt?: string | null
|
|
74
|
+
scheduledUnpublishAt?: string | null
|
|
68
75
|
/**
|
|
69
76
|
* Section types the canvas renders as placeholders (custom / unmanaged).
|
|
70
77
|
* When non-empty the editor is in "structure" mode: the eye preview is
|
|
@@ -113,6 +120,10 @@ export function EditorTopBar({
|
|
|
113
120
|
seoScore = 0,
|
|
114
121
|
onSaveDraft,
|
|
115
122
|
onPublish,
|
|
123
|
+
onSchedule,
|
|
124
|
+
scheduleReady = true,
|
|
125
|
+
scheduledAt = null,
|
|
126
|
+
scheduledUnpublishAt = null,
|
|
116
127
|
structuralTypes = [],
|
|
117
128
|
}: EditorTopBarProps) {
|
|
118
129
|
const statusInfo = STATUS_TEXT[status]
|
|
@@ -135,6 +146,26 @@ export function EditorTopBar({
|
|
|
135
146
|
? 'You do not have permission to publish'
|
|
136
147
|
: undefined
|
|
137
148
|
|
|
149
|
+
const scheduleBlocked =
|
|
150
|
+
!onSchedule ||
|
|
151
|
+
!canPublish ||
|
|
152
|
+
!publishReady ||
|
|
153
|
+
!scheduleReady ||
|
|
154
|
+
publishing ||
|
|
155
|
+
saveState === 'saving'
|
|
156
|
+
const scheduleTitle =
|
|
157
|
+
saveState === 'saving'
|
|
158
|
+
? 'Wait for the current save to finish'
|
|
159
|
+
: !scheduleReady
|
|
160
|
+
? 'Save before scheduling'
|
|
161
|
+
: canPublish && !publishReady && publishDisabledReason
|
|
162
|
+
? publishDisabledReason
|
|
163
|
+
: !canPublish
|
|
164
|
+
? 'You do not have permission to schedule'
|
|
165
|
+
: scheduledAt
|
|
166
|
+
? `Reschedule — currently publishes ${formatScheduleLabel(scheduledAt)}`
|
|
167
|
+
: 'Schedule a future publish date'
|
|
168
|
+
|
|
138
169
|
return (
|
|
139
170
|
<header className="bg-card border-border flex h-14 shrink-0 items-center justify-between gap-4 border-b px-4">
|
|
140
171
|
{/* Breadcrumb + status */}
|
|
@@ -158,6 +189,18 @@ export function EditorTopBar({
|
|
|
158
189
|
>
|
|
159
190
|
{statusInfo.label}
|
|
160
191
|
</span>
|
|
192
|
+
{scheduledAt && (
|
|
193
|
+
<span
|
|
194
|
+
className="text-muted-foreground ml-1 hidden shrink-0 text-xs sm:inline"
|
|
195
|
+
title={
|
|
196
|
+
scheduledUnpublishAt
|
|
197
|
+
? `Publishes ${formatScheduleLabel(scheduledAt)} · Unpublishes ${formatScheduleLabel(scheduledUnpublishAt)}`
|
|
198
|
+
: `Publishes ${formatScheduleLabel(scheduledAt)}`
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
· {formatScheduleLabel(scheduledAt)}
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
161
204
|
<SaveStatus state={saveState} dirty={dirty} />
|
|
162
205
|
</div>
|
|
163
206
|
|
|
@@ -294,6 +337,20 @@ export function EditorTopBar({
|
|
|
294
337
|
{saveState === 'saving' ? 'Saving…' : 'Save Draft'}
|
|
295
338
|
</button>
|
|
296
339
|
|
|
340
|
+
{onSchedule && (
|
|
341
|
+
<button
|
|
342
|
+
type="button"
|
|
343
|
+
onClick={onSchedule}
|
|
344
|
+
disabled={scheduleBlocked}
|
|
345
|
+
title={scheduleTitle}
|
|
346
|
+
aria-label={scheduledAt ? 'Reschedule publish' : 'Schedule publish'}
|
|
347
|
+
className="border-border text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
348
|
+
>
|
|
349
|
+
<Calendar className="h-3.5 w-3.5" aria-hidden />
|
|
350
|
+
<span className="hidden sm:inline">{scheduledAt ? 'Reschedule' : 'Schedule'}</span>
|
|
351
|
+
</button>
|
|
352
|
+
)}
|
|
353
|
+
|
|
297
354
|
<button
|
|
298
355
|
type="button"
|
|
299
356
|
onClick={onPublish}
|
|
@@ -353,3 +410,15 @@ function SaveStatus({ state, dirty }: { state: SaveState; dirty: boolean }) {
|
|
|
353
410
|
}
|
|
354
411
|
return null
|
|
355
412
|
}
|
|
413
|
+
|
|
414
|
+
function formatScheduleLabel(iso: string): string {
|
|
415
|
+
const d = new Date(iso)
|
|
416
|
+
if (Number.isNaN(d.getTime())) return iso
|
|
417
|
+
return d.toLocaleString(undefined, {
|
|
418
|
+
month: 'short',
|
|
419
|
+
day: 'numeric',
|
|
420
|
+
year: 'numeric',
|
|
421
|
+
hour: 'numeric',
|
|
422
|
+
minute: '2-digit',
|
|
423
|
+
})
|
|
424
|
+
}
|
|
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
4
4
|
import { AlertTriangle, Blocks, Loader2, Lock } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
7
|
+
import { useNavigationBlocker } from '../../router/index.js'
|
|
8
|
+
import { SchedulePublishDialog } from '../../components/SchedulePublishDialog.js'
|
|
7
9
|
import { EditorSeoAside } from '../../components/EditorSeoAside.js'
|
|
8
10
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
9
11
|
import { VersionHistory } from '../../components/VersionHistory.js'
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
missingPageFields,
|
|
17
19
|
moveSection,
|
|
18
20
|
publishPage,
|
|
21
|
+
PAGES_COLLECTION,
|
|
19
22
|
removeSection,
|
|
20
23
|
reorderSections,
|
|
21
24
|
savePageDraft,
|
|
@@ -103,6 +106,7 @@ export function PageSectionEditor({
|
|
|
103
106
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
104
107
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
|
|
105
108
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
109
|
+
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
106
110
|
const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
|
|
107
111
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
108
112
|
|
|
@@ -187,16 +191,8 @@ export function PageSectionEditor({
|
|
|
187
191
|
}
|
|
188
192
|
}, [documentId, initialParentId, schemaError])
|
|
189
193
|
|
|
190
|
-
// ─── Unsaved-changes guard (browser unload)
|
|
191
|
-
|
|
192
|
-
if (!dirty) return
|
|
193
|
-
function onBeforeUnload(e: BeforeUnloadEvent) {
|
|
194
|
-
e.preventDefault()
|
|
195
|
-
e.returnValue = ''
|
|
196
|
-
}
|
|
197
|
-
window.addEventListener('beforeunload', onBeforeUnload)
|
|
198
|
-
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
|
199
|
-
}, [dirty])
|
|
194
|
+
// ─── Unsaved-changes guard (client-side navigation + browser unload) ──
|
|
195
|
+
useNavigationBlocker(dirty)
|
|
200
196
|
|
|
201
197
|
// ─── Section mutations ───────────────────────────────────────────────
|
|
202
198
|
const mutateSections = useCallback(
|
|
@@ -324,6 +320,42 @@ export function PageSectionEditor({
|
|
|
324
320
|
setConfirmPublish(true)
|
|
325
321
|
}, [page])
|
|
326
322
|
|
|
323
|
+
const requestSchedule = useCallback(async () => {
|
|
324
|
+
if (!page) return
|
|
325
|
+
if (!page.id) {
|
|
326
|
+
toast.error('Save the page before scheduling')
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
const result = validatePage(page, true)
|
|
330
|
+
setValidation(result.errors.length || result.warnings.length ? result : null)
|
|
331
|
+
if (result.errors.length > 0) {
|
|
332
|
+
toast.error('Resolve the listed issues before scheduling')
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
if (dirty) {
|
|
336
|
+
const ok = await handleSaveDraft()
|
|
337
|
+
if (!ok) return
|
|
338
|
+
}
|
|
339
|
+
setScheduleOpen(true)
|
|
340
|
+
}, [page, dirty, handleSaveDraft])
|
|
341
|
+
|
|
342
|
+
const handleScheduled = useCallback(
|
|
343
|
+
(next: { status: string; scheduledAt: string | null; scheduledUnpublishAt: string | null }) => {
|
|
344
|
+
setPage((prev) =>
|
|
345
|
+
prev
|
|
346
|
+
? {
|
|
347
|
+
...prev,
|
|
348
|
+
status: next.status as EditorPage['status'],
|
|
349
|
+
scheduledAt: next.scheduledAt,
|
|
350
|
+
scheduledUnpublishAt: next.scheduledUnpublishAt,
|
|
351
|
+
}
|
|
352
|
+
: prev,
|
|
353
|
+
)
|
|
354
|
+
setValidation(null)
|
|
355
|
+
},
|
|
356
|
+
[],
|
|
357
|
+
)
|
|
358
|
+
|
|
327
359
|
const doPublish = useCallback(async () => {
|
|
328
360
|
if (!page) return
|
|
329
361
|
setPublishing(true)
|
|
@@ -533,6 +565,10 @@ export function PageSectionEditor({
|
|
|
533
565
|
seoScore={liveSeoScore}
|
|
534
566
|
onSaveDraft={handleSaveDraft}
|
|
535
567
|
onPublish={requestPublish}
|
|
568
|
+
onSchedule={page.id ? requestSchedule : undefined}
|
|
569
|
+
scheduleReady={Boolean(page.id)}
|
|
570
|
+
scheduledAt={page.scheduledAt}
|
|
571
|
+
scheduledUnpublishAt={page.scheduledUnpublishAt}
|
|
536
572
|
structuralTypes={structuralTypes}
|
|
537
573
|
/>
|
|
538
574
|
|
|
@@ -687,6 +723,18 @@ export function PageSectionEditor({
|
|
|
687
723
|
confirmLabel="Publish"
|
|
688
724
|
/>
|
|
689
725
|
|
|
726
|
+
{page.id && (
|
|
727
|
+
<SchedulePublishDialog
|
|
728
|
+
collectionSlug={PAGES_COLLECTION}
|
|
729
|
+
documentId={page.id}
|
|
730
|
+
scheduledAt={page.scheduledAt}
|
|
731
|
+
scheduledUnpublishAt={page.scheduledUnpublishAt}
|
|
732
|
+
open={scheduleOpen}
|
|
733
|
+
onClose={() => setScheduleOpen(false)}
|
|
734
|
+
onScheduled={handleScheduled}
|
|
735
|
+
/>
|
|
736
|
+
)}
|
|
737
|
+
|
|
690
738
|
<ConfirmDialog
|
|
691
739
|
open={leaveTarget !== null}
|
|
692
740
|
onClose={() => setLeaveTarget(null)}
|
|
@@ -37,10 +37,7 @@ export function ArticleBodySection({
|
|
|
37
37
|
</div>
|
|
38
38
|
)}
|
|
39
39
|
{html.trim() ? (
|
|
40
|
-
<div
|
|
41
|
-
className="prose prose-zinc text-foreground max-w-none text-base leading-relaxed"
|
|
42
|
-
dangerouslySetInnerHTML={{ __html: html }}
|
|
43
|
-
/>
|
|
40
|
+
<div className="actuate-rich-text max-w-none" dangerouslySetInnerHTML={{ __html: html }} />
|
|
44
41
|
) : (
|
|
45
42
|
<p className="text-muted-foreground text-base leading-relaxed italic">
|
|
46
43
|
{source === 'field'
|
|
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
4
4
|
import { AlertTriangle, Loader2, Lock } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
7
|
+
import { useNavigationBlocker } from '../../router/index.js'
|
|
8
|
+
import { SchedulePublishDialog } from '../../components/SchedulePublishDialog.js'
|
|
7
9
|
import { EditorSeoAside } from '../../components/EditorSeoAside.js'
|
|
8
10
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
9
11
|
import { VersionHistory } from '../../components/VersionHistory.js'
|
|
@@ -128,6 +130,7 @@ export function PostSectionEditor({
|
|
|
128
130
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
129
131
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
|
|
130
132
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
133
|
+
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
131
134
|
const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
|
|
132
135
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
133
136
|
|
|
@@ -225,16 +228,8 @@ export function PostSectionEditor({
|
|
|
225
228
|
}
|
|
226
229
|
}, [postType, documentId, schemaError])
|
|
227
230
|
|
|
228
|
-
// ─── Unsaved-changes guard
|
|
229
|
-
|
|
230
|
-
if (!dirty) return
|
|
231
|
-
function onBeforeUnload(e: BeforeUnloadEvent) {
|
|
232
|
-
e.preventDefault()
|
|
233
|
-
e.returnValue = ''
|
|
234
|
-
}
|
|
235
|
-
window.addEventListener('beforeunload', onBeforeUnload)
|
|
236
|
-
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
|
237
|
-
}, [dirty])
|
|
231
|
+
// ─── Unsaved-changes guard (client-side navigation + browser unload) ──
|
|
232
|
+
useNavigationBlocker(dirty)
|
|
238
233
|
|
|
239
234
|
const context: PostRenderContext = useMemo(
|
|
240
235
|
() => ({
|
|
@@ -375,6 +370,43 @@ export function PostSectionEditor({
|
|
|
375
370
|
setConfirmPublish(true)
|
|
376
371
|
}, [post])
|
|
377
372
|
|
|
373
|
+
const requestSchedule = useCallback(async () => {
|
|
374
|
+
if (!post) return
|
|
375
|
+
if (!post.id) {
|
|
376
|
+
toast.error('Save the post before scheduling')
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
const result = validatePost(post, true)
|
|
380
|
+
setValidation(result.errors.length || result.warnings.length ? result : null)
|
|
381
|
+
if (result.errors.length > 0) {
|
|
382
|
+
toast.error('Resolve the listed issues before scheduling')
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
if (dirty) {
|
|
386
|
+
const ok = await handleSaveDraft()
|
|
387
|
+
if (!ok) return
|
|
388
|
+
}
|
|
389
|
+
setScheduleOpen(true)
|
|
390
|
+
}, [post, dirty, handleSaveDraft])
|
|
391
|
+
|
|
392
|
+
const handleScheduled = useCallback(
|
|
393
|
+
(next: { status: string; scheduledAt: string | null; scheduledUnpublishAt: string | null }) => {
|
|
394
|
+
setPost((prev) =>
|
|
395
|
+
prev
|
|
396
|
+
? {
|
|
397
|
+
...prev,
|
|
398
|
+
status: next.status as EditorPost['status'],
|
|
399
|
+
scheduledAt: next.scheduledAt,
|
|
400
|
+
scheduledUnpublishAt: next.scheduledUnpublishAt,
|
|
401
|
+
publishDate: next.scheduledAt ?? prev.publishDate,
|
|
402
|
+
}
|
|
403
|
+
: prev,
|
|
404
|
+
)
|
|
405
|
+
setValidation(null)
|
|
406
|
+
},
|
|
407
|
+
[],
|
|
408
|
+
)
|
|
409
|
+
|
|
378
410
|
const doPublish = useCallback(async () => {
|
|
379
411
|
if (!post) return
|
|
380
412
|
setPublishing(true)
|
|
@@ -576,6 +608,10 @@ export function PostSectionEditor({
|
|
|
576
608
|
seoScore={liveSeoScore}
|
|
577
609
|
onSaveDraft={handleSaveDraft}
|
|
578
610
|
onPublish={requestPublish}
|
|
611
|
+
onSchedule={post.id ? requestSchedule : undefined}
|
|
612
|
+
scheduleReady={Boolean(post.id)}
|
|
613
|
+
scheduledAt={post.scheduledAt}
|
|
614
|
+
scheduledUnpublishAt={post.scheduledUnpublishAt}
|
|
579
615
|
structuralTypes={structuralTypes}
|
|
580
616
|
/>
|
|
581
617
|
|
|
@@ -718,6 +754,18 @@ export function PostSectionEditor({
|
|
|
718
754
|
confirmLabel="Publish"
|
|
719
755
|
/>
|
|
720
756
|
|
|
757
|
+
{post.id && (
|
|
758
|
+
<SchedulePublishDialog
|
|
759
|
+
collectionSlug={postType}
|
|
760
|
+
documentId={post.id}
|
|
761
|
+
scheduledAt={post.scheduledAt}
|
|
762
|
+
scheduledUnpublishAt={post.scheduledUnpublishAt}
|
|
763
|
+
open={scheduleOpen}
|
|
764
|
+
onClose={() => setScheduleOpen(false)}
|
|
765
|
+
onScheduled={handleScheduled}
|
|
766
|
+
/>
|
|
767
|
+
)}
|
|
768
|
+
|
|
721
769
|
<ConfirmDialog
|
|
722
770
|
open={leaveTarget !== null}
|
|
723
771
|
onClose={() => setLeaveTarget(null)}
|
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
4
4
|
import { AlertTriangle, ArrowLeft, Layout, Loader2, Save } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
7
|
+
import { useNavigationBlocker } from '../../router/index.js'
|
|
7
8
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
8
9
|
import {
|
|
9
10
|
addSection,
|
|
@@ -99,15 +100,8 @@ export function PostTemplateEditor({
|
|
|
99
100
|
}
|
|
100
101
|
}, [postType])
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
function onBeforeUnload(e: BeforeUnloadEvent) {
|
|
105
|
-
e.preventDefault()
|
|
106
|
-
e.returnValue = ''
|
|
107
|
-
}
|
|
108
|
-
window.addEventListener('beforeunload', onBeforeUnload)
|
|
109
|
-
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
|
110
|
-
}, [dirty])
|
|
103
|
+
// Unsaved-changes guard (client-side navigation + browser unload).
|
|
104
|
+
useNavigationBlocker(dirty)
|
|
111
105
|
|
|
112
106
|
const mutateSections = useCallback(
|
|
113
107
|
(fn: (sections: EditorPostTemplate['sections']) => EditorPostTemplate['sections']) => {
|