@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
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import { useRef, useState } from 'react'
4
+ import { Modal } from './ui/Modal.js'
5
+ import { Button } from './ui/Button.js'
6
+
7
+ export function RequestChangesDialog({
8
+ open,
9
+ onClose,
10
+ onConfirm,
11
+ }: {
12
+ open: boolean
13
+ onClose: () => void
14
+ onConfirm: (note: string) => void
15
+ }) {
16
+ const [note, setNote] = useState('')
17
+ const inputRef = useRef<HTMLTextAreaElement>(null)
18
+ return (
19
+ <Modal
20
+ open={open}
21
+ onClose={onClose}
22
+ title="Request changes"
23
+ description="Optional note for the author. The page returns to draft."
24
+ initialFocusRef={inputRef}
25
+ actions={
26
+ <>
27
+ <Button variant="ghost" onClick={onClose}>
28
+ Cancel
29
+ </Button>
30
+ <Button
31
+ variant="primary"
32
+ onClick={() => {
33
+ onConfirm(note.trim())
34
+ setNote('')
35
+ onClose()
36
+ }}
37
+ >
38
+ Send back
39
+ </Button>
40
+ </>
41
+ }
42
+ >
43
+ <label className="text-foreground block text-sm font-medium" htmlFor="review-note">
44
+ Note
45
+ </label>
46
+ <textarea
47
+ id="review-note"
48
+ ref={inputRef}
49
+ value={note}
50
+ onChange={(e) => setNote(e.target.value)}
51
+ rows={4}
52
+ className="border-border bg-input-background mt-1 w-full rounded-md border px-3 py-2 text-sm"
53
+ />
54
+ </Modal>
55
+ )
56
+ }
@@ -95,7 +95,7 @@ export function withPostAuditSlots(sections: PageSection[], post?: PostAuditExtr
95
95
  })
96
96
  }
97
97
  const image = post.featuredImage?.trim() ?? ''
98
- if (image) {
98
+ if (image && (image.startsWith('/') || /^https?:\/\//i.test(image))) {
99
99
  extras.push({
100
100
  id: POST_IMAGE_SECTION_ID,
101
101
  sectionType: 'feature',
@@ -39,6 +39,7 @@ import {
39
39
  import { validateSlug } from '@prenta/core/client'
40
40
 
41
41
  import { missingCollectionFields } from './collection-schema.js'
42
+ import { SUBMIT_BEFORE_PUBLISH_ERROR, type WorkflowStage } from './workflow-chrome.js'
42
43
 
43
44
  export type { PageSection } from '@prenta/core/page-sections'
44
45
 
@@ -68,6 +69,8 @@ export interface EditorPage {
68
69
  slug: string
69
70
  path: string
70
71
  status: PageStatus
72
+ workflowStage: WorkflowStage
73
+ reviewNote: string | null
71
74
  /**
72
75
  * Parent page for the tree hierarchy (`data.parentPageId`), or null for
73
76
  * a top-level page. Preselected when arriving via "Add sub-page"
@@ -186,6 +189,8 @@ interface DocumentApiRow {
186
189
  title: string | null
187
190
  slug: string | null
188
191
  status: PageStatus
192
+ workflowStage?: WorkflowStage
193
+ reviewNote?: string | null
189
194
  publishedAt: string | null
190
195
  scheduledAt?: string | null
191
196
  scheduledUnpublishAt?: string | null
@@ -211,6 +216,8 @@ function rowToEditorPage(doc: DocumentApiRow): EditorPage {
211
216
  slug,
212
217
  path: typeof data.path === 'string' && data.path ? data.path : slug ? `/${slug}` : '/',
213
218
  status: doc.status ?? 'DRAFT',
219
+ workflowStage: (doc.workflowStage as WorkflowStage | undefined) ?? 'DRAFT',
220
+ reviewNote: typeof doc.reviewNote === 'string' ? doc.reviewNote : null,
214
221
  parentPageId:
215
222
  typeof data.parentPageId === 'string' && data.parentPageId ? data.parentPageId : null,
216
223
  // Preserve unknown (externally-managed) sections so the editor can show
@@ -290,17 +297,23 @@ export async function savePageDraft(page: EditorPage): Promise<EditorPage> {
290
297
  return rowToEditorPage(res.data)
291
298
  }
292
299
 
293
- /** Publish: persist the latest content and flip status to PUBLISHED. */
294
- export async function publishPage(page: EditorPage): Promise<EditorPage> {
295
- const endpoint = page.id
296
- ? `/collections/${PAGES_COLLECTION}/${page.id}`
297
- : `/collections/${PAGES_COLLECTION}`
298
- const res = await cmsApi<DocumentApiRow>(endpoint, {
299
- method: page.id ? 'PUT' : 'POST',
300
- body: toWriteBody(page, { status: 'PUBLISHED' }),
301
- })
302
- if (res.error || !res.data) throw new Error(res.error ?? 'Failed to publish page')
303
- return rowToEditorPage(res.data)
300
+ export async function transitionPageWorkflow(
301
+ page: EditorPage,
302
+ stage: WorkflowStage,
303
+ note?: string,
304
+ ): Promise<EditorPage> {
305
+ if (!page.id) throw new Error('Save the page before changing review state')
306
+ const res = await cmsApi<{ success: boolean }>(
307
+ `/collections/${PAGES_COLLECTION}/${page.id}/workflow`,
308
+ { method: 'POST', body: JSON.stringify({ stage, note }) },
309
+ )
310
+ if (res.error) throw new Error(res.error)
311
+ return fetchPageForEditor(page.id)
312
+ }
313
+
314
+ /** Leftover callers must not skip review — use `transitionPageWorkflow`. */
315
+ export async function publishPage(_page: EditorPage): Promise<EditorPage> {
316
+ throw new Error(SUBMIT_BEFORE_PUBLISH_ERROR)
304
317
  }
305
318
 
306
319
  /**
@@ -314,6 +327,8 @@ export function emptyPage(parentPageId: string | null = null): EditorPage {
314
327
  slug: '',
315
328
  path: '',
316
329
  status: 'DRAFT',
330
+ workflowStage: 'DRAFT',
331
+ reviewNote: null,
317
332
  parentPageId,
318
333
  sections: [],
319
334
  seoTitle: '',
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { cmsApi } from './api.js'
23
23
  import { missingCollectionFields } from './collection-schema.js'
24
+ import { SUBMIT_BEFORE_PUBLISH_ERROR, type WorkflowStage } from './workflow-chrome.js'
24
25
  import {
25
26
  getSectionType,
26
27
  generateSectionId,
@@ -73,12 +74,17 @@ export interface EditorPost {
73
74
  title: string
74
75
  slug: string
75
76
  excerpt: string
77
+ /** Persist identity — media id when present so usage tracking survives (#671). */
76
78
  featuredImage: string
79
+ /** Public URL for `<img src>` and the details input. Empty when only an id is known. */
80
+ featuredImageUrl: string
77
81
  body: string
78
82
  category: string
79
83
  /** Editorial tags as plain strings (stored as `[{ tag }]` rows, see {@link parseTags}). */
80
84
  tags: string[]
81
85
  status: PostStatus
86
+ workflowStage: WorkflowStage
87
+ reviewNote: string | null
82
88
  publishDate: string | null
83
89
  /** When set and status is SCHEDULED, the cron publishes at this time. */
84
90
  scheduledAt: string | null
@@ -199,6 +205,8 @@ interface DocumentApiRow {
199
205
  title: string | null
200
206
  slug: string | null
201
207
  status: PostStatus
208
+ workflowStage?: WorkflowStage
209
+ reviewNote?: string | null
202
210
  publishedAt: string | null
203
211
  scheduledAt?: string | null
204
212
  scheduledUnpublishAt?: string | null
@@ -211,20 +219,49 @@ function dataStr(data: Record<string, unknown>, key: string): string {
211
219
  return typeof v === 'string' ? v : ''
212
220
  }
213
221
 
222
+ export interface FeaturedImageRef {
223
+ /** Persist this — media id when present so usage tracking survives (#671). */
224
+ id: string
225
+ /** Use this for `<img src>` and the details input. Empty when only an id is known. */
226
+ url: string
227
+ }
228
+
229
+ /** True for paths and http(s) URLs — never a bare media cuid. */
230
+ export function isDisplayableImageSrc(value: string): boolean {
231
+ const v = value.trim()
232
+ return v.startsWith('/') || /^https?:\/\//i.test(v)
233
+ }
234
+
235
+ function asDisplayUrl(value: unknown): string {
236
+ if (typeof value !== 'string') return ''
237
+ const v = value.trim()
238
+ return isDisplayableImageSrc(v) ? v : ''
239
+ }
240
+
214
241
  /**
215
- * Media fields are stored denormalized as `{ id, url, alt, }` after headless
216
- * writes / admin picker. The editor state is a string (id or URL). Never coerce
217
- * an object to `""` that wiped featured images on the next save (#671).
242
+ * Split a stored featured-image value into persist identity and display URL.
243
+ *
244
+ * Media fields are denormalized as `{ id, url, alt, }` after headless writes
245
+ * / the admin picker. Never coerce an object to `""` — that wiped featured
246
+ * images on the next save (#671).
218
247
  */
219
- export function parseFeaturedImage(raw: unknown): string {
220
- if (typeof raw === 'string') return raw.trim()
248
+ export function parseFeaturedImageRef(raw: unknown): FeaturedImageRef {
249
+ if (typeof raw === 'string') {
250
+ const value = raw.trim()
251
+ return { id: value, url: asDisplayUrl(value) }
252
+ }
221
253
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
222
254
  const obj = raw as Record<string, unknown>
223
- if (typeof obj.id === 'string' && obj.id.trim()) return obj.id.trim()
224
- const url = obj.url ?? obj.publicUrl ?? obj.src
225
- if (typeof url === 'string' && url.trim()) return url.trim()
255
+ const id = typeof obj.id === 'string' ? obj.id.trim() : ''
256
+ const url = asDisplayUrl(obj.url ?? obj.publicUrl ?? obj.src)
257
+ return { id: id || url, url }
226
258
  }
227
- return ''
259
+ return { id: '', url: '' }
260
+ }
261
+
262
+ /** Persist identity only — prefer `id` so a save does not drop the media link. */
263
+ export function parseFeaturedImage(raw: unknown): string {
264
+ return parseFeaturedImageRef(raw).id
228
265
  }
229
266
 
230
267
  /**
@@ -250,6 +287,7 @@ export function parseTags(raw: unknown): string[] {
250
287
  function rowToEditorPost(postType: string, doc: DocumentApiRow): EditorPost {
251
288
  const data = doc.data ?? {}
252
289
  const dataTitle = dataStr(data, 'title')
290
+ const featured = parseFeaturedImageRef(data.featuredImage)
253
291
  return {
254
292
  id: doc.id,
255
293
  postType,
@@ -258,11 +296,14 @@ function rowToEditorPost(postType: string, doc: DocumentApiRow): EditorPost {
258
296
  title: dataTitle || (doc.title ?? ''),
259
297
  slug: doc.slug ?? dataStr(data, 'slug'),
260
298
  excerpt: dataStr(data, 'excerpt'),
261
- featuredImage: parseFeaturedImage(data.featuredImage),
299
+ featuredImage: featured.id,
300
+ featuredImageUrl: featured.url,
262
301
  body: dataStr(data, 'body'),
263
302
  category: dataStr(data, 'category'),
264
303
  tags: parseTags(data.tags),
265
304
  status: doc.status ?? 'DRAFT',
305
+ workflowStage: (doc.workflowStage as WorkflowStage | undefined) ?? 'DRAFT',
306
+ reviewNote: typeof doc.reviewNote === 'string' ? doc.reviewNote : null,
266
307
  publishDate: (data.publishDate as string) ?? doc.scheduledAt ?? null,
267
308
  scheduledAt: doc.scheduledAt ?? null,
268
309
  scheduledUnpublishAt: doc.scheduledUnpublishAt ?? null,
@@ -410,16 +451,23 @@ export async function savePostDraft(post: EditorPost): Promise<EditorPost> {
410
451
  return rowToEditorPost(post.postType, res.data)
411
452
  }
412
453
 
413
- export async function publishPost(post: EditorPost): Promise<EditorPost> {
414
- const endpoint = post.id
415
- ? `/collections/${encodeURIComponent(post.postType)}/${post.id}`
416
- : `/collections/${encodeURIComponent(post.postType)}`
417
- const res = await cmsApi<DocumentApiRow>(endpoint, {
418
- method: post.id ? 'PUT' : 'POST',
419
- body: toWriteBody(post, { status: 'PUBLISHED' }),
420
- })
421
- if (res.error || !res.data) throw new Error(res.error ?? 'Failed to publish post')
422
- return rowToEditorPost(post.postType, res.data)
454
+ export async function transitionPostWorkflow(
455
+ post: EditorPost,
456
+ stage: WorkflowStage,
457
+ note?: string,
458
+ ): Promise<EditorPost> {
459
+ if (!post.id) throw new Error('Save the post before changing review state')
460
+ const res = await cmsApi<{ success: boolean }>(
461
+ `/collections/${encodeURIComponent(post.postType)}/${post.id}/workflow`,
462
+ { method: 'POST', body: JSON.stringify({ stage, note }) },
463
+ )
464
+ if (res.error) throw new Error(res.error)
465
+ return fetchPostForEditor(post.postType, post.id)
466
+ }
467
+
468
+ /** Leftover callers must not skip review — use `transitionPostWorkflow`. */
469
+ export async function publishPost(_post: EditorPost): Promise<EditorPost> {
470
+ throw new Error(SUBMIT_BEFORE_PUBLISH_ERROR)
423
471
  }
424
472
 
425
473
  /** A blank post scaffold seeded with the post type's template sections. */
@@ -443,10 +491,13 @@ export async function emptyPostFromTemplate(postType: string): Promise<EditorPos
443
491
  slug: '',
444
492
  excerpt: '',
445
493
  featuredImage: '',
494
+ featuredImageUrl: '',
446
495
  body: '',
447
496
  category: '',
448
497
  tags: [],
449
498
  status: 'DRAFT',
499
+ workflowStage: 'DRAFT',
500
+ reviewNote: null,
450
501
  publishDate: null,
451
502
  scheduledAt: null,
452
503
  scheduledUnpublishAt: null,
@@ -9,6 +9,7 @@
9
9
  * response and no second request per row is needed.
10
10
  */
11
11
  import { cmsApi } from './api.js'
12
+ import { parseFeaturedImageRef } from './post-editor-service.js'
12
13
  import { coerceSections, type PageSection } from '@prenta/core/page-sections'
13
14
 
14
15
  /**
@@ -97,13 +98,8 @@ function optionalString(value: unknown): string | undefined {
97
98
  }
98
99
 
99
100
  function optionalFeaturedImage(value: unknown): string | undefined {
100
- if (typeof value === 'string') return value
101
- if (value && typeof value === 'object' && !Array.isArray(value)) {
102
- const obj = value as Record<string, unknown>
103
- const url = obj.url ?? obj.publicUrl ?? obj.src
104
- if (typeof url === 'string') return url
105
- }
106
- return undefined
101
+ const url = parseFeaturedImageRef(value).url
102
+ return url || undefined
107
103
  }
108
104
 
109
105
  export function toPageVersion(row: VersionApiRow): PageVersion {
@@ -0,0 +1,105 @@
1
+ export type WorkflowStage = 'DRAFT' | 'IN_REVIEW' | 'APPROVED' | 'ARCHIVED' | 'PUBLISHED'
2
+
3
+ export type WorkflowChromeAction = 'submit' | 'approve' | 'waiting'
4
+
5
+ export const SUBMIT_FOR_REVIEW_LABEL = 'Submit for review'
6
+ export const APPROVE_LABEL = 'Approve'
7
+ export const REQUEST_CHANGES_LABEL = 'Request changes'
8
+ export const WAITING_FOR_REVIEW_LABEL = 'Waiting for review.'
9
+ export const REVIEWER_NOTE_PREFIX = 'Reviewer note:'
10
+ export const SUBMIT_BEFORE_PUBLISH_ERROR = 'Submit for review before publishing.'
11
+
12
+ const REVIEWER_ROLES = new Set(['ADMIN', 'EDITOR'])
13
+ const SUBMIT_ROLES = new Set(['ADMIN', 'EDITOR', 'AUTHOR'])
14
+
15
+ const STATUS_PILL: Record<WorkflowStage | 'SCHEDULED', { label: string; className: string }> = {
16
+ DRAFT: { label: 'Draft', className: 'text-warning' },
17
+ IN_REVIEW: { label: 'In review', className: 'text-warning' },
18
+ APPROVED: { label: 'Approved', className: 'text-warning' },
19
+ PUBLISHED: { label: 'Published', className: 'text-success' },
20
+ ARCHIVED: { label: 'Archived', className: 'text-muted-foreground' },
21
+ SCHEDULED: { label: 'Scheduled', className: 'text-warning' },
22
+ }
23
+
24
+ export function resolveWorkflowChrome(input: {
25
+ stage: WorkflowStage
26
+ status: 'DRAFT' | 'PUBLISHED' | 'SCHEDULED' | 'ARCHIVED'
27
+ role: string
28
+ canEdit: boolean
29
+ }): {
30
+ primary: WorkflowChromeAction | null
31
+ showRequestChanges: boolean
32
+ showSchedule: boolean
33
+ waitingLabel?: string
34
+ statusLabel: string
35
+ statusClassName: string
36
+ } {
37
+ const role = input.role.toUpperCase()
38
+ const published = input.status === 'PUBLISHED'
39
+ const scheduled = input.status === 'SCHEDULED'
40
+ const pillKey = published ? 'PUBLISHED' : scheduled ? 'SCHEDULED' : input.stage
41
+ const pill = STATUS_PILL[pillKey]
42
+
43
+ if (!input.canEdit || published) {
44
+ return {
45
+ primary: null,
46
+ showRequestChanges: false,
47
+ showSchedule: false,
48
+ statusLabel: pill.label,
49
+ statusClassName: pill.className,
50
+ }
51
+ }
52
+
53
+ const reviewer = REVIEWER_ROLES.has(role)
54
+ const canSubmit = SUBMIT_ROLES.has(role)
55
+ const inReview = input.stage === 'IN_REVIEW' || input.stage === 'APPROVED'
56
+
57
+ if (scheduled) {
58
+ return {
59
+ primary: null,
60
+ showRequestChanges: reviewer,
61
+ showSchedule: reviewer,
62
+ statusLabel: pill.label,
63
+ statusClassName: pill.className,
64
+ }
65
+ }
66
+
67
+ if (input.stage === 'DRAFT' && canSubmit) {
68
+ return {
69
+ primary: 'submit',
70
+ showRequestChanges: false,
71
+ showSchedule: false,
72
+ statusLabel: pill.label,
73
+ statusClassName: pill.className,
74
+ }
75
+ }
76
+
77
+ if (inReview && reviewer) {
78
+ return {
79
+ primary: 'approve',
80
+ showRequestChanges: true,
81
+ showSchedule: true,
82
+ statusLabel: pill.label,
83
+ statusClassName: pill.className,
84
+ }
85
+ }
86
+
87
+ if (inReview && canSubmit) {
88
+ return {
89
+ primary: 'waiting',
90
+ showRequestChanges: false,
91
+ showSchedule: false,
92
+ waitingLabel: WAITING_FOR_REVIEW_LABEL,
93
+ statusLabel: pill.label,
94
+ statusClassName: pill.className,
95
+ }
96
+ }
97
+
98
+ return {
99
+ primary: null,
100
+ showRequestChanges: false,
101
+ showSchedule: false,
102
+ statusLabel: pill.label,
103
+ statusClassName: pill.className,
104
+ }
105
+ }