@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.
- package/CHANGELOG.md +12 -0
- package/dist/__tests__/components/request-changes-dialog.render.test.d.ts +2 -0
- package/dist/__tests__/components/request-changes-dialog.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/request-changes-dialog.render.test.js +12 -0
- package/dist/__tests__/components/request-changes-dialog.render.test.js.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.js +10 -0
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -1
- package/dist/__tests__/lib/page-editor-service-api.test.js +28 -15
- package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -1
- package/dist/__tests__/lib/post-editor-service-api.test.d.ts +2 -0
- package/dist/__tests__/lib/post-editor-service-api.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-editor-service-api.test.js +68 -0
- package/dist/__tests__/lib/post-editor-service-api.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +27 -6
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
- package/dist/__tests__/lib/version-history.test.js +27 -0
- package/dist/__tests__/lib/version-history.test.js.map +1 -1
- package/dist/__tests__/lib/workflow-chrome.test.d.ts +2 -0
- package/dist/__tests__/lib/workflow-chrome.test.d.ts.map +1 -0
- package/dist/__tests__/lib/workflow-chrome.test.js +108 -0
- package/dist/__tests__/lib/workflow-chrome.test.js.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +40 -33
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/page-section-editor.test.js +8 -0
- package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/page-tab.render.test.js +2 -0
- package/dist/__tests__/views/page-tab.render.test.js.map +1 -1
- package/dist/__tests__/views/post-fields-modal.render.test.js +21 -0
- package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
- package/dist/__tests__/views/post-section-editor.test.js +3 -0
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/post-tab.render.test.js +3 -0
- package/dist/__tests__/views/post-tab.render.test.js.map +1 -1
- package/dist/components/RequestChangesDialog.d.ts +6 -0
- package/dist/components/RequestChangesDialog.d.ts.map +1 -0
- package/dist/components/RequestChangesDialog.js +15 -0
- package/dist/components/RequestChangesDialog.js.map +1 -0
- package/dist/lib/page-audit-input.js +1 -1
- package/dist/lib/page-audit-input.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +6 -2
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +16 -12
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/post-editor-service.d.ts +24 -4
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +49 -24
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/lib/version-history.d.ts.map +1 -1
- package/dist/lib/version-history.js +3 -9
- package/dist/lib/version-history.js.map +1 -1
- package/dist/lib/workflow-chrome.d.ts +22 -0
- package/dist/lib/workflow-chrome.d.ts.map +1 -0
- package/dist/lib/workflow-chrome.js +80 -0
- package/dist/lib/workflow-chrome.js.map +1 -0
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +10 -5
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +38 -28
- 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 +83 -62
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostFieldsForm.d.ts +3 -0
- package/dist/views/post-editor/PostFieldsForm.d.ts.map +1 -1
- package/dist/views/post-editor/PostFieldsForm.js +7 -2
- package/dist/views/post-editor/PostFieldsForm.js.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.js +15 -2
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +81 -65
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTab.d.ts.map +1 -1
- package/dist/views/post-editor/PostTab.js +1 -0
- package/dist/views/post-editor/PostTab.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/components/request-changes-dialog.render.test.tsx +11 -0
- package/src/__tests__/lib/page-audit-input.test.ts +11 -0
- package/src/__tests__/lib/page-editor-service-api.test.ts +37 -18
- package/src/__tests__/lib/post-editor-service-api.test.ts +74 -0
- package/src/__tests__/lib/post-editor-service.test.ts +28 -9
- package/src/__tests__/lib/version-history.test.ts +29 -0
- package/src/__tests__/lib/workflow-chrome.test.ts +125 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +42 -45
- package/src/__tests__/views/page-section-editor.test.tsx +18 -0
- package/src/__tests__/views/page-tab.render.test.tsx +2 -0
- package/src/__tests__/views/post-fields-modal.render.test.tsx +41 -0
- package/src/__tests__/views/post-section-editor.test.tsx +3 -0
- package/src/__tests__/views/post-tab.render.test.tsx +3 -0
- package/src/components/RequestChangesDialog.tsx +56 -0
- package/src/lib/page-audit-input.ts +1 -1
- package/src/lib/page-editor-service.ts +26 -11
- package/src/lib/post-editor-service.ts +71 -20
- package/src/lib/version-history.ts +3 -7
- package/src/lib/workflow-chrome.ts +105 -0
- package/src/views/page-editor/EditorTopBar.tsx +110 -48
- package/src/views/page-editor/PageSectionEditor.tsx +121 -92
- package/src/views/post-editor/PostFieldsForm.tsx +14 -5
- package/src/views/post-editor/PostFieldsModal.tsx +15 -2
- package/src/views/post-editor/PostSectionEditor.tsx +114 -91
- 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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
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
|
|
220
|
-
if (typeof raw === 'string')
|
|
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
|
-
|
|
224
|
-
const url = obj.url ?? obj.publicUrl ?? obj.src
|
|
225
|
-
|
|
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:
|
|
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
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
+
}
|