@prenta/admin 1.17.18 → 2.0.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 +14 -0
- package/dist/__tests__/components/media-block.render.test.js +1 -1
- package/dist/__tests__/components/media-block.render.test.js.map +1 -1
- package/dist/__tests__/views/appearance-settings.render.test.js +1 -1
- package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/autosave.test.d.ts +2 -0
- package/dist/__tests__/views/autosave.test.d.ts.map +1 -0
- package/dist/__tests__/views/autosave.test.js +51 -0
- package/dist/__tests__/views/autosave.test.js.map +1 -0
- package/dist/components/MediaBlock.d.ts.map +1 -1
- package/dist/components/MediaBlock.js +1 -1
- package/dist/components/MediaBlock.js.map +1 -1
- package/dist/components/MediaPickerModal.js +1 -1
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +1 -1
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +26 -6
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/autosave.d.ts +16 -0
- package/dist/views/page-editor/autosave.d.ts.map +1 -0
- package/dist/views/page-editor/autosave.js +23 -0
- package/dist/views/page-editor/autosave.js.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +23 -6
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +11 -11
- package/src/__tests__/components/media-block.render.test.tsx +1 -1
- package/src/__tests__/views/appearance-settings.render.test.tsx +1 -1
- package/src/__tests__/views/autosave.test.tsx +66 -0
- package/src/components/MediaBlock.tsx +2 -4
- package/src/components/MediaPickerModal.tsx +2 -2
- package/src/views/MediaBrowser.tsx +4 -5
- package/src/views/page-editor/PageSectionEditor.tsx +28 -7
- package/src/views/page-editor/autosave.ts +30 -0
- package/src/views/post-editor/PostSectionEditor.tsx +25 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, 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'
|
|
@@ -11,6 +11,7 @@ import { EditorRail, RailHeader } from '../../components/EditorRail.js'
|
|
|
11
11
|
import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/EditorRailTabs.js'
|
|
12
12
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
13
13
|
import { VersionHistoryPanel } from './VersionHistoryPanel.js'
|
|
14
|
+
import { useDebouncedAutosave } from './autosave.js'
|
|
14
15
|
import { ReviewBar } from './ReviewBar.js'
|
|
15
16
|
import {
|
|
16
17
|
fetchPageVersions,
|
|
@@ -558,25 +559,38 @@ export function PageSectionEditor({
|
|
|
558
559
|
[directives, reloadAudit],
|
|
559
560
|
)
|
|
560
561
|
|
|
562
|
+
const latestPage = useRef(page)
|
|
563
|
+
latestPage.current = page
|
|
564
|
+
|
|
561
565
|
const handleSaveDraft = useCallback(
|
|
562
|
-
async (options?: { navigate?: boolean }): Promise<EditorPage | false> => {
|
|
566
|
+
async (options?: { navigate?: boolean; silent?: boolean }): Promise<EditorPage | false> => {
|
|
563
567
|
if (!page) return false
|
|
564
568
|
const result = validatePage(page, false)
|
|
565
569
|
if (result.errors.length > 0) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
570
|
+
if (!options?.silent) {
|
|
571
|
+
setValidation(result)
|
|
572
|
+
toast.error(result.errors[0]!)
|
|
573
|
+
setSettingsOpen(true)
|
|
574
|
+
}
|
|
569
575
|
return false
|
|
570
576
|
}
|
|
577
|
+
const snapshot = page
|
|
571
578
|
setSaveState('saving')
|
|
572
579
|
try {
|
|
573
|
-
const saved = await savePageDraft(
|
|
580
|
+
const saved = await savePageDraft(snapshot)
|
|
581
|
+
// A keystroke landed while this request was in flight. Drop the
|
|
582
|
+
// response so it cannot replace the newer draft. The next quiet
|
|
583
|
+
// period saves that draft. This is not a concurrent-editor warning.
|
|
584
|
+
if (latestPage.current !== snapshot) {
|
|
585
|
+
setSaveState('idle')
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
574
588
|
if (saved.id) await saveDirectives(saved.id)
|
|
575
589
|
acceptPage(saved)
|
|
576
590
|
setDirty(false)
|
|
577
591
|
setSaveState('saved')
|
|
578
592
|
setValidation(null)
|
|
579
|
-
toast.success('Draft saved')
|
|
593
|
+
if (!options?.silent) toast.success('Draft saved')
|
|
580
594
|
if ((options?.navigate ?? true) && !documentId && saved.id) {
|
|
581
595
|
onNavigate(`/pages/${saved.id}/edit`)
|
|
582
596
|
}
|
|
@@ -590,6 +604,13 @@ export function PageSectionEditor({
|
|
|
590
604
|
[page, documentId, onNavigate, saveDirectives, acceptPage],
|
|
591
605
|
)
|
|
592
606
|
|
|
607
|
+
useDebouncedAutosave({
|
|
608
|
+
dirty,
|
|
609
|
+
enabled: Boolean(documentId) && effectiveCanEdit && !publishing,
|
|
610
|
+
revision: page,
|
|
611
|
+
save: () => handleSaveDraft({ navigate: false, silent: true }),
|
|
612
|
+
})
|
|
613
|
+
|
|
593
614
|
const runWorkflow = useCallback(
|
|
594
615
|
async (stage: WorkflowStage, note?: string, toastOk?: string) => {
|
|
595
616
|
if (!page) return
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Quiet period after the last edit before a draft is written. */
|
|
4
|
+
export const AUTOSAVE_DELAY_MS = 800
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Schedule `save` after `delayMs` of no changes. `revision` is the draft
|
|
8
|
+
* itself (or any value that changes with it) so typing resets the timer.
|
|
9
|
+
* A clean document, or one autosave is not allowed to touch, does not save.
|
|
10
|
+
* This does not detect a concurrent edit by someone else (#554).
|
|
11
|
+
*/
|
|
12
|
+
export function useDebouncedAutosave(options: {
|
|
13
|
+
dirty: boolean
|
|
14
|
+
enabled: boolean
|
|
15
|
+
revision: unknown
|
|
16
|
+
delayMs?: number
|
|
17
|
+
save: () => void | Promise<unknown>
|
|
18
|
+
}): void {
|
|
19
|
+
const saveRef = useRef(options.save)
|
|
20
|
+
saveRef.current = options.save
|
|
21
|
+
const { dirty, enabled, revision, delayMs = AUTOSAVE_DELAY_MS } = options
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!dirty || !enabled) return
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
void saveRef.current()
|
|
27
|
+
}, delayMs)
|
|
28
|
+
return () => clearTimeout(timer)
|
|
29
|
+
}, [dirty, enabled, revision, delayMs])
|
|
30
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, 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'
|
|
@@ -11,6 +11,7 @@ import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/E
|
|
|
11
11
|
import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js'
|
|
12
12
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
13
13
|
import { VersionHistoryPanel } from '../page-editor/VersionHistoryPanel.js'
|
|
14
|
+
import { useDebouncedAutosave } from '../page-editor/autosave.js'
|
|
14
15
|
import { ReviewBar } from '../page-editor/ReviewBar.js'
|
|
15
16
|
import {
|
|
16
17
|
fetchPageVersions,
|
|
@@ -436,24 +437,34 @@ export function PostSectionEditor({
|
|
|
436
437
|
}, [])
|
|
437
438
|
|
|
438
439
|
// ─── Save / publish ──────────────────────────────────────────────────
|
|
440
|
+
const latestPost = useRef(post)
|
|
441
|
+
latestPost.current = post
|
|
442
|
+
|
|
439
443
|
const handleSaveDraft = useCallback(
|
|
440
|
-
async (options?: { navigate?: boolean }): Promise<EditorPost | false> => {
|
|
444
|
+
async (options?: { navigate?: boolean; silent?: boolean }): Promise<EditorPost | false> => {
|
|
441
445
|
if (!post) return false
|
|
442
446
|
const result = validatePost(post, false)
|
|
443
447
|
if (result.errors.length > 0) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
448
|
+
if (!options?.silent) {
|
|
449
|
+
setValidation(result)
|
|
450
|
+
toast.error(result.errors[0]!)
|
|
451
|
+
setFieldsOpen(true)
|
|
452
|
+
}
|
|
447
453
|
return false
|
|
448
454
|
}
|
|
455
|
+
const snapshot = post
|
|
449
456
|
setSaveState('saving')
|
|
450
457
|
try {
|
|
451
|
-
const saved = await savePostDraft(
|
|
458
|
+
const saved = await savePostDraft(snapshot)
|
|
459
|
+
if (latestPost.current !== snapshot) {
|
|
460
|
+
setSaveState('idle')
|
|
461
|
+
return false
|
|
462
|
+
}
|
|
452
463
|
acceptPost(saved)
|
|
453
464
|
setDirty(false)
|
|
454
465
|
setSaveState('saved')
|
|
455
466
|
setValidation(null)
|
|
456
|
-
toast.success('Draft saved')
|
|
467
|
+
if (!options?.silent) toast.success('Draft saved')
|
|
457
468
|
if ((options?.navigate ?? true) && !documentId && saved.id) {
|
|
458
469
|
onNavigate(`/posts/${postType}/${saved.id}/edit`)
|
|
459
470
|
}
|
|
@@ -557,6 +568,13 @@ export function PostSectionEditor({
|
|
|
557
568
|
const effectiveCanEdit = canEdit && !reviewing
|
|
558
569
|
const canRestoreVersion = canEdit
|
|
559
570
|
|
|
571
|
+
useDebouncedAutosave({
|
|
572
|
+
dirty,
|
|
573
|
+
enabled: Boolean(documentId) && effectiveCanEdit && !publishing,
|
|
574
|
+
revision: post,
|
|
575
|
+
save: () => handleSaveDraft({ navigate: false, silent: true }),
|
|
576
|
+
})
|
|
577
|
+
|
|
560
578
|
const runWorkflow = useCallback(
|
|
561
579
|
async (stage: WorkflowStage, note?: string, toastOk?: string) => {
|
|
562
580
|
if (!post) return
|