@prenta/admin 1.5.1 → 1.6.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/__tests__/components/request-changes-dialog.render.test.d.ts +2 -0
  3. package/dist/__tests__/components/request-changes-dialog.render.test.d.ts.map +1 -0
  4. package/dist/__tests__/components/request-changes-dialog.render.test.js +12 -0
  5. package/dist/__tests__/components/request-changes-dialog.render.test.js.map +1 -0
  6. package/dist/__tests__/lib/page-audit-input.test.js +10 -0
  7. package/dist/__tests__/lib/page-audit-input.test.js.map +1 -1
  8. package/dist/__tests__/lib/page-editor-service-api.test.js +28 -15
  9. package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -1
  10. package/dist/__tests__/lib/post-editor-service-api.test.d.ts +2 -0
  11. package/dist/__tests__/lib/post-editor-service-api.test.d.ts.map +1 -0
  12. package/dist/__tests__/lib/post-editor-service-api.test.js +68 -0
  13. package/dist/__tests__/lib/post-editor-service-api.test.js.map +1 -0
  14. package/dist/__tests__/lib/post-editor-service.test.js +27 -6
  15. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
  16. package/dist/__tests__/lib/version-history.test.js +27 -0
  17. package/dist/__tests__/lib/version-history.test.js.map +1 -1
  18. package/dist/__tests__/lib/workflow-chrome.test.d.ts +2 -0
  19. package/dist/__tests__/lib/workflow-chrome.test.d.ts.map +1 -0
  20. package/dist/__tests__/lib/workflow-chrome.test.js +108 -0
  21. package/dist/__tests__/lib/workflow-chrome.test.js.map +1 -0
  22. package/dist/__tests__/views/editor-top-bar.render.test.js +40 -33
  23. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  24. package/dist/__tests__/views/page-section-editor.test.js +8 -0
  25. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  26. package/dist/__tests__/views/page-tab.render.test.js +2 -0
  27. package/dist/__tests__/views/page-tab.render.test.js.map +1 -1
  28. package/dist/__tests__/views/post-fields-modal.render.test.js +21 -0
  29. package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
  30. package/dist/__tests__/views/post-section-editor.test.js +3 -0
  31. package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
  32. package/dist/__tests__/views/post-tab.render.test.js +3 -0
  33. package/dist/__tests__/views/post-tab.render.test.js.map +1 -1
  34. package/dist/components/RequestChangesDialog.d.ts +6 -0
  35. package/dist/components/RequestChangesDialog.d.ts.map +1 -0
  36. package/dist/components/RequestChangesDialog.js +15 -0
  37. package/dist/components/RequestChangesDialog.js.map +1 -0
  38. package/dist/lib/page-audit-input.js +1 -1
  39. package/dist/lib/page-audit-input.js.map +1 -1
  40. package/dist/lib/page-editor-service.d.ts +6 -2
  41. package/dist/lib/page-editor-service.d.ts.map +1 -1
  42. package/dist/lib/page-editor-service.js +16 -12
  43. package/dist/lib/page-editor-service.js.map +1 -1
  44. package/dist/lib/post-editor-service.d.ts +24 -4
  45. package/dist/lib/post-editor-service.d.ts.map +1 -1
  46. package/dist/lib/post-editor-service.js +49 -24
  47. package/dist/lib/post-editor-service.js.map +1 -1
  48. package/dist/lib/version-history.d.ts.map +1 -1
  49. package/dist/lib/version-history.js +3 -9
  50. package/dist/lib/version-history.js.map +1 -1
  51. package/dist/lib/workflow-chrome.d.ts +22 -0
  52. package/dist/lib/workflow-chrome.d.ts.map +1 -0
  53. package/dist/lib/workflow-chrome.js +80 -0
  54. package/dist/lib/workflow-chrome.js.map +1 -0
  55. package/dist/prenta-admin.css +1 -1
  56. package/dist/views/page-editor/EditorTopBar.d.ts +10 -5
  57. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  58. package/dist/views/page-editor/EditorTopBar.js +38 -28
  59. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  60. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  61. package/dist/views/page-editor/PageSectionEditor.js +83 -62
  62. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  63. package/dist/views/post-editor/PostFieldsForm.d.ts +3 -0
  64. package/dist/views/post-editor/PostFieldsForm.d.ts.map +1 -1
  65. package/dist/views/post-editor/PostFieldsForm.js +7 -2
  66. package/dist/views/post-editor/PostFieldsForm.js.map +1 -1
  67. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
  68. package/dist/views/post-editor/PostFieldsModal.js +15 -2
  69. package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
  70. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  71. package/dist/views/post-editor/PostSectionEditor.js +81 -65
  72. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  73. package/dist/views/post-editor/PostTab.d.ts.map +1 -1
  74. package/dist/views/post-editor/PostTab.js +1 -0
  75. package/dist/views/post-editor/PostTab.js.map +1 -1
  76. package/package.json +4 -4
  77. package/src/__tests__/components/request-changes-dialog.render.test.tsx +11 -0
  78. package/src/__tests__/lib/page-audit-input.test.ts +11 -0
  79. package/src/__tests__/lib/page-editor-service-api.test.ts +37 -18
  80. package/src/__tests__/lib/post-editor-service-api.test.ts +74 -0
  81. package/src/__tests__/lib/post-editor-service.test.ts +28 -9
  82. package/src/__tests__/lib/version-history.test.ts +29 -0
  83. package/src/__tests__/lib/workflow-chrome.test.ts +125 -0
  84. package/src/__tests__/views/editor-top-bar.render.test.tsx +42 -45
  85. package/src/__tests__/views/page-section-editor.test.tsx +18 -0
  86. package/src/__tests__/views/page-tab.render.test.tsx +2 -0
  87. package/src/__tests__/views/post-fields-modal.render.test.tsx +41 -0
  88. package/src/__tests__/views/post-section-editor.test.tsx +3 -0
  89. package/src/__tests__/views/post-tab.render.test.tsx +3 -0
  90. package/src/components/RequestChangesDialog.tsx +56 -0
  91. package/src/lib/page-audit-input.ts +1 -1
  92. package/src/lib/page-editor-service.ts +26 -11
  93. package/src/lib/post-editor-service.ts +71 -20
  94. package/src/lib/version-history.ts +3 -7
  95. package/src/lib/workflow-chrome.ts +105 -0
  96. package/src/views/page-editor/EditorTopBar.tsx +110 -48
  97. package/src/views/page-editor/PageSectionEditor.tsx +121 -92
  98. package/src/views/post-editor/PostFieldsForm.tsx +14 -5
  99. package/src/views/post-editor/PostFieldsModal.tsx +15 -2
  100. package/src/views/post-editor/PostSectionEditor.tsx +114 -91
  101. package/src/views/post-editor/PostTab.tsx +1 -0
@@ -25,9 +25,9 @@ import {
25
25
  fetchPostForEditor,
26
26
  fetchTemplateForEditor,
27
27
  missingPostFields,
28
- publishPost,
29
28
  savePostDraft,
30
29
  toggleSectionVisibility,
30
+ transitionPostWorkflow,
31
31
  updateSectionContent,
32
32
  updateSectionMeta,
33
33
  validatePost,
@@ -35,6 +35,7 @@ import {
35
35
  type PostHeaderConfig,
36
36
  type ValidationResult,
37
37
  } from '../../lib/post-editor-service.js'
38
+ import { resolveWorkflowChrome, type WorkflowStage } from '../../lib/workflow-chrome.js'
38
39
  import { SharePreviewLinkDialog } from '../../components/SharePreviewLinkDialog.js'
39
40
  import { defaultPostHeader } from '@prenta/core/page-sections'
40
41
  import { openPublicSiteTab } from '../../lib/open-public-site-tab.js'
@@ -160,7 +161,6 @@ export function PostSectionEditor({
160
161
  * audit so the header chip and tab dot always have a number.
161
162
  */
162
163
  const [liveAudit, setLiveAudit] = useState<PageAuditResult | null>(null)
163
- const [confirmPublish, setConfirmPublish] = useState(false)
164
164
  const [scheduleOpen, setScheduleOpen] = useState(false)
165
165
  const [shareOpen, setShareOpen] = useState(false)
166
166
  const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
@@ -180,7 +180,8 @@ export function PostSectionEditor({
180
180
  setBaseline(snapshotBaseline(p.sections, p.status))
181
181
  }, [])
182
182
 
183
- const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
183
+ const { canEdit } = useMemo(() => deriveCan(session), [session])
184
+ const sessionRole = String(session?.user?.role ?? '')
184
185
  const customRenderers = useAdminSectionRenderers()
185
186
  const {
186
187
  siteUrl: publicSiteUrl,
@@ -225,7 +226,7 @@ export function PostSectionEditor({
225
226
  post: {
226
227
  title: post?.title,
227
228
  bodyHtml: post?.body,
228
- featuredImage: post?.featuredImage,
229
+ featuredImage: post?.featuredImageUrl,
229
230
  },
230
231
  })
231
232
  const audit = liveAudit ?? savedAudit
@@ -337,7 +338,7 @@ export function PostSectionEditor({
337
338
  title: post?.title,
338
339
  excerpt: post?.excerpt,
339
340
  body: post?.body,
340
- featuredImageUrl: post?.featuredImage,
341
+ featuredImageUrl: post?.featuredImageUrl,
341
342
  publishDate: post?.publishDate ?? post?.updatedAt ?? null,
342
343
  category: post?.category,
343
344
  author: { name: session?.user?.name || session?.user?.email },
@@ -391,61 +392,36 @@ export function PostSectionEditor({
391
392
  }, [])
392
393
 
393
394
  // ─── Save / publish ──────────────────────────────────────────────────
394
- const handleSaveDraft = useCallback(async (): Promise<boolean> => {
395
- if (!post) return false
396
- const result = validatePost(post, false)
397
- if (result.errors.length > 0) {
398
- setValidation(result)
399
- toast.error(result.errors[0]!)
400
- setFieldsOpen(true)
401
- return false
402
- }
403
- setSaveState('saving')
404
- try {
405
- const saved = await savePostDraft(post)
406
- acceptPost(saved)
407
- setDirty(false)
408
- setSaveState('saved')
409
- setValidation(null)
410
- toast.success('Draft saved')
411
- if (!documentId && saved.id) onNavigate(`/posts/${postType}/${saved.id}/edit`)
412
- return true
413
- } catch (err) {
414
- setSaveState('error')
415
- toast.error(err instanceof Error ? err.message : 'Failed to save draft')
416
- return false
417
- }
418
- }, [post, documentId, postType, onNavigate])
419
-
420
- const requestPublish = useCallback(() => {
421
- if (!post) return
422
- const result = validatePost(post, true)
423
- setValidation(result.errors.length || result.warnings.length ? result : null)
424
- if (result.errors.length > 0) {
425
- toast.error('Resolve the listed issues before publishing')
426
- return
427
- }
428
- setConfirmPublish(true)
429
- }, [post])
430
-
431
- const requestSchedule = useCallback(async () => {
432
- if (!post) return
433
- if (!post.id) {
434
- toast.error('Save the post before scheduling')
435
- return
436
- }
437
- const result = validatePost(post, true)
438
- setValidation(result.errors.length || result.warnings.length ? result : null)
439
- if (result.errors.length > 0) {
440
- toast.error('Resolve the listed issues before scheduling')
441
- return
442
- }
443
- if (dirty) {
444
- const ok = await handleSaveDraft()
445
- if (!ok) return
446
- }
447
- setScheduleOpen(true)
448
- }, [post, dirty, handleSaveDraft])
395
+ const handleSaveDraft = useCallback(
396
+ async (options?: { navigate?: boolean }): Promise<EditorPost | false> => {
397
+ if (!post) return false
398
+ const result = validatePost(post, false)
399
+ if (result.errors.length > 0) {
400
+ setValidation(result)
401
+ toast.error(result.errors[0]!)
402
+ setFieldsOpen(true)
403
+ return false
404
+ }
405
+ setSaveState('saving')
406
+ try {
407
+ const saved = await savePostDraft(post)
408
+ acceptPost(saved)
409
+ setDirty(false)
410
+ setSaveState('saved')
411
+ setValidation(null)
412
+ toast.success('Draft saved')
413
+ if ((options?.navigate ?? true) && !documentId && saved.id) {
414
+ onNavigate(`/posts/${postType}/${saved.id}/edit`)
415
+ }
416
+ return saved
417
+ } catch (err) {
418
+ setSaveState('error')
419
+ toast.error(err instanceof Error ? err.message : 'Failed to save draft')
420
+ return false
421
+ }
422
+ },
423
+ [post, documentId, postType, onNavigate, acceptPost],
424
+ )
449
425
 
450
426
  const handleScheduled = useCallback(
451
427
  (next: { status: string; scheduledAt: string | null; scheduledUnpublishAt: string | null }) => {
@@ -465,24 +441,6 @@ export function PostSectionEditor({
465
441
  [],
466
442
  )
467
443
 
468
- const doPublish = useCallback(async () => {
469
- if (!post) return
470
- setPublishing(true)
471
- try {
472
- const published = await publishPost(post)
473
- acceptPost(published)
474
- setDirty(false)
475
- setSaveState('saved')
476
- setValidation(null)
477
- toast.success('Post published')
478
- if (!documentId && published.id) onNavigate(`/posts/${postType}/${published.id}/edit`)
479
- } catch (err) {
480
- toast.error(err instanceof Error ? err.message : 'Failed to publish post')
481
- } finally {
482
- setPublishing(false)
483
- }
484
- }, [post, documentId, postType, onNavigate])
485
-
486
444
  // ─── Version history ─────────────────────────────────────────────────
487
445
  // History is a MODE, not a dialog: it overlays the rail and drives the
488
446
  // canvas, so the version being reviewed is editor state rather than
@@ -555,6 +513,76 @@ export function PostSectionEditor({
555
513
  const effectiveCanEdit = canEdit && !reviewing
556
514
  const canRestoreVersion = canEdit
557
515
 
516
+ const runWorkflow = useCallback(
517
+ async (stage: WorkflowStage, note?: string, toastOk?: string) => {
518
+ if (!post) return
519
+ let current = post
520
+ if (dirty || !current.id) {
521
+ const saved = await handleSaveDraft({ navigate: false })
522
+ if (!saved) return
523
+ current = saved
524
+ }
525
+ setPublishing(true)
526
+ try {
527
+ const next = await transitionPostWorkflow(current, stage, note)
528
+ acceptPost(next)
529
+ if (!documentId && next.id) onNavigate(`/posts/${postType}/${next.id}/edit`)
530
+ if (toastOk) toast.success(toastOk)
531
+ } catch (err) {
532
+ toast.error(err instanceof Error ? err.message : 'Transition failed')
533
+ } finally {
534
+ setPublishing(false)
535
+ }
536
+ },
537
+ [post, dirty, documentId, postType, handleSaveDraft, acceptPost, onNavigate],
538
+ )
539
+
540
+ const handleSubmitForReview = useCallback(
541
+ () => runWorkflow('IN_REVIEW', undefined, 'Submitted for review.'),
542
+ [runWorkflow],
543
+ )
544
+
545
+ const handleApprove = useCallback(() => {
546
+ if (!post) return
547
+ const result = validatePost(post, true)
548
+ if (result.errors.length > 0) {
549
+ toast.error('Resolve the listed issues before publishing')
550
+ return
551
+ }
552
+ void runWorkflow('PUBLISHED', undefined, 'Published.')
553
+ }, [post, runWorkflow])
554
+
555
+ const handleRequestChanges = useCallback(
556
+ (note: string) => runWorkflow('DRAFT', note || undefined, 'Sent back to draft.'),
557
+ [runWorkflow],
558
+ )
559
+
560
+ const requestSchedule = useCallback(async () => {
561
+ if (!post) return
562
+ if (!post.id) {
563
+ toast.error('Save the post before scheduling')
564
+ return
565
+ }
566
+ const result = validatePost(post, true)
567
+ setValidation(result.errors.length || result.warnings.length ? result : null)
568
+ if (result.errors.length > 0) {
569
+ toast.error('Resolve the listed issues before scheduling')
570
+ return
571
+ }
572
+ if (dirty) {
573
+ const ok = await handleSaveDraft()
574
+ if (!ok) return
575
+ }
576
+ const chrome = resolveWorkflowChrome({
577
+ stage: post.workflowStage,
578
+ status: post.status,
579
+ role: sessionRole,
580
+ canEdit: effectiveCanEdit,
581
+ })
582
+ if (!chrome.showSchedule) return
583
+ setScheduleOpen(true)
584
+ }, [post, dirty, handleSaveDraft, sessionRole, effectiveCanEdit])
585
+
558
586
  const handleSeoSaved = useCallback(() => {
559
587
  if (!post?.id) return
560
588
  fetchPostForEditor(postType, post.id)
@@ -616,7 +644,7 @@ export function PostSectionEditor({
616
644
  data: { path: (post as { path?: string }).path },
617
645
  trailingSlash,
618
646
  dirty,
619
- saveDraft: handleSaveDraft,
647
+ saveDraft: async () => Boolean(await handleSaveDraft()),
620
648
  }).catch((err) => {
621
649
  toast.error(err instanceof Error ? err.message : 'Failed to open the site')
622
650
  })
@@ -688,11 +716,13 @@ export function PostSectionEditor({
688
716
  <EditorTopBar
689
717
  title={post.title || `New ${typeLabel}`}
690
718
  status={post.status}
719
+ workflowStage={post.workflowStage}
720
+ reviewNote={post.reviewNote}
721
+ role={sessionRole}
691
722
  dirty={dirty}
692
723
  saveState={saveState}
693
724
  publishing={publishing}
694
725
  canEdit={effectiveCanEdit}
695
- canPublish={canPublish && effectiveCanEdit}
696
726
  publishReady={publishReady}
697
727
  publishDisabledReason={publishDisabledReason}
698
728
  viewport={viewport}
@@ -716,7 +746,9 @@ export function PostSectionEditor({
716
746
  typeof audit?.seo.score === 'number' ? scoreBand(audit.seo.score) : undefined
717
747
  }
718
748
  onSaveDraft={handleSaveDraft}
719
- onPublish={requestPublish}
749
+ onSubmitForReview={handleSubmitForReview}
750
+ onApprove={handleApprove}
751
+ onRequestChanges={handleRequestChanges}
720
752
  onSchedule={post.id ? requestSchedule : undefined}
721
753
  scheduleReady={Boolean(post.id)}
722
754
  scheduledAt={post.scheduledAt}
@@ -809,7 +841,7 @@ export function PostSectionEditor({
809
841
  contentHtml={seoContentHtml}
810
842
  title={post.title}
811
843
  bodyHtml={post.body}
812
- featuredImage={post.featuredImage}
844
+ featuredImage={post.featuredImageUrl}
813
845
  draftMeta={{
814
846
  seoTitle: post.seoTitle,
815
847
  seoDescription: post.seoDescription,
@@ -889,15 +921,6 @@ export function PostSectionEditor({
889
921
  onSave={handleFields}
890
922
  />
891
923
 
892
- <ConfirmDialog
893
- open={confirmPublish}
894
- onClose={() => setConfirmPublish(false)}
895
- onConfirm={doPublish}
896
- title="Publish this post?"
897
- description="The current content will go live and replace the published version."
898
- confirmLabel="Publish"
899
- />
900
-
901
924
  {post.id && (
902
925
  <SchedulePublishDialog
903
926
  collectionSlug={postType}
@@ -30,6 +30,7 @@ export function PostTab({ post, canEdit, fields, onChange }: PostTabProps) {
30
30
  slug: post.slug,
31
31
  excerpt: post.excerpt,
32
32
  featuredImage: post.featuredImage,
33
+ featuredImageUrl: post.featuredImageUrl,
33
34
  category: post.category,
34
35
  tags: post.tags,
35
36
  }}