@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/__tests__/components/media-block.render.test.js +1 -1
  3. package/dist/__tests__/components/media-block.render.test.js.map +1 -1
  4. package/dist/__tests__/views/appearance-settings.render.test.js +1 -1
  5. package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -1
  6. package/dist/__tests__/views/autosave.test.d.ts +2 -0
  7. package/dist/__tests__/views/autosave.test.d.ts.map +1 -0
  8. package/dist/__tests__/views/autosave.test.js +51 -0
  9. package/dist/__tests__/views/autosave.test.js.map +1 -0
  10. package/dist/components/MediaBlock.d.ts.map +1 -1
  11. package/dist/components/MediaBlock.js +1 -1
  12. package/dist/components/MediaBlock.js.map +1 -1
  13. package/dist/components/MediaPickerModal.js +1 -1
  14. package/dist/components/MediaPickerModal.js.map +1 -1
  15. package/dist/views/MediaBrowser.d.ts.map +1 -1
  16. package/dist/views/MediaBrowser.js +1 -1
  17. package/dist/views/MediaBrowser.js.map +1 -1
  18. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  19. package/dist/views/page-editor/PageSectionEditor.js +26 -6
  20. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  21. package/dist/views/page-editor/autosave.d.ts +16 -0
  22. package/dist/views/page-editor/autosave.d.ts.map +1 -0
  23. package/dist/views/page-editor/autosave.js +23 -0
  24. package/dist/views/page-editor/autosave.js.map +1 -0
  25. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  26. package/dist/views/post-editor/PostSectionEditor.js +23 -6
  27. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  28. package/package.json +11 -11
  29. package/src/__tests__/components/media-block.render.test.tsx +1 -1
  30. package/src/__tests__/views/appearance-settings.render.test.tsx +1 -1
  31. package/src/__tests__/views/autosave.test.tsx +66 -0
  32. package/src/components/MediaBlock.tsx +2 -4
  33. package/src/components/MediaPickerModal.tsx +2 -2
  34. package/src/views/MediaBrowser.tsx +4 -5
  35. package/src/views/page-editor/PageSectionEditor.tsx +28 -7
  36. package/src/views/page-editor/autosave.ts +30 -0
  37. 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
- setValidation(result)
567
- toast.error(result.errors[0]!)
568
- setSettingsOpen(true)
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(page)
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
- setValidation(result)
445
- toast.error(result.errors[0]!)
446
- setFieldsOpen(true)
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(post)
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