@prenta/admin 1.17.11 → 1.17.13
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__/lib/post-canvas-context.test.d.ts +2 -0
- package/dist/__tests__/lib/post-canvas-context.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-canvas-context.test.js +40 -0
- package/dist/__tests__/lib/post-canvas-context.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service-api.test.js +2 -0
- package/dist/__tests__/lib/post-editor-service-api.test.js.map +1 -1
- package/dist/__tests__/lib/post-editor-service.test.js +21 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
- package/dist/__tests__/views/post-fields-modal.render.test.js +2 -0
- package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
- package/dist/__tests__/views/post-section-editor.test.js +2 -0
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/post-tab.render.test.js +2 -0
- package/dist/__tests__/views/post-tab.render.test.js.map +1 -1
- package/dist/__tests__/views/section-edit-form.render.test.js +36 -0
- package/dist/__tests__/views/section-edit-form.render.test.js.map +1 -1
- package/dist/lib/post-canvas-context.d.ts +38 -0
- package/dist/lib/post-canvas-context.d.ts.map +1 -0
- package/dist/lib/post-canvas-context.js +65 -0
- package/dist/lib/post-canvas-context.js.map +1 -0
- package/dist/lib/post-editor-service.d.ts +8 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +5 -0
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/views/page-editor/SectionEditForm.js +1 -1
- package/dist/views/page-editor/SectionEditForm.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +1 -1
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostHeader.d.ts +1 -1
- package/dist/views/post-editor/PostHeader.d.ts.map +1 -1
- package/dist/views/post-editor/PostHeader.js +2 -0
- package/dist/views/post-editor/PostHeader.js.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.js +2 -2
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +31 -2
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/lib/post-canvas-context.test.ts +56 -0
- package/src/__tests__/lib/post-editor-service-api.test.ts +2 -0
- package/src/__tests__/lib/post-editor-service.test.ts +22 -0
- package/src/__tests__/views/post-fields-modal.render.test.tsx +2 -0
- package/src/__tests__/views/post-section-editor.test.tsx +2 -0
- package/src/__tests__/views/post-tab.render.test.tsx +2 -0
- package/src/__tests__/views/section-edit-form.render.test.tsx +55 -0
- package/src/lib/post-canvas-context.ts +80 -0
- package/src/lib/post-editor-service.ts +12 -0
- package/src/views/page-editor/SectionEditForm.tsx +2 -2
- package/src/views/page-editor/SectionInspector.tsx +2 -2
- package/src/views/post-editor/PostEditorCanvas.tsx +1 -1
- package/src/views/post-editor/PostHeader.tsx +1 -0
- package/src/views/post-editor/PostHeaderPanel.tsx +30 -22
- package/src/views/post-editor/PostSectionEditor.tsx +46 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
canvasAuthorName,
|
|
5
|
+
relatedPostsFromField,
|
|
6
|
+
relatedPostsFromSiblings,
|
|
7
|
+
} from '../../lib/post-canvas-context.js'
|
|
8
|
+
|
|
9
|
+
describe('canvasAuthorName', () => {
|
|
10
|
+
it('prefers the document author over the logged-in editor', () => {
|
|
11
|
+
expect(canvasAuthorName('Ada Lovelace', { name: 'Editor', email: 'ed@example.com' })).toBe(
|
|
12
|
+
'Ada Lovelace',
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('falls back to the session when the document author is blank', () => {
|
|
17
|
+
expect(canvasAuthorName(' ', { name: '', email: 'ed@example.com' })).toBe('ed@example.com')
|
|
18
|
+
expect(canvasAuthorName(undefined, { name: 'Editor' })).toBe('Editor')
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('related posts for the canvas', () => {
|
|
23
|
+
it('uses hand-picked posts when they already have titles', () => {
|
|
24
|
+
expect(
|
|
25
|
+
relatedPostsFromField([{ title: 'Keep going', excerpt: 'A note', url: '/blog/keep-going' }]),
|
|
26
|
+
).toEqual([{ title: 'Keep going', excerpt: 'A note', url: '/blog/keep-going' }])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns null for id-only relations so the editor can load siblings', () => {
|
|
30
|
+
expect(relatedPostsFromField(['post_1', 'post_2'])).toBeNull()
|
|
31
|
+
expect(relatedPostsFromField(undefined)).toBeNull()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('builds sibling cards from data.title and the collection prefix', () => {
|
|
35
|
+
const cards = relatedPostsFromSiblings(
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
id: 'self',
|
|
39
|
+
title: 'Slug-Style-Title',
|
|
40
|
+
slug: 'self',
|
|
41
|
+
data: { title: 'This post' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'other',
|
|
45
|
+
title: 'Foster-Independence',
|
|
46
|
+
slug: 'foster-independence',
|
|
47
|
+
data: { title: 'Foster independence', excerpt: 'A summary' },
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
{ excludeId: 'self', urlPrefix: 'blog', collection: 'posts' },
|
|
51
|
+
)
|
|
52
|
+
expect(cards).toEqual([
|
|
53
|
+
{ title: 'Foster independence', excerpt: 'A summary', url: '/blog/foster-independence' },
|
|
54
|
+
])
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -33,6 +33,8 @@ function basePost(over: Partial<EditorPostT> = {}): EditorPostT {
|
|
|
33
33
|
featuredImageUrl: '',
|
|
34
34
|
body: '',
|
|
35
35
|
category: '',
|
|
36
|
+
authorName: '',
|
|
37
|
+
canvasRelatedPosts: null,
|
|
36
38
|
tags: [],
|
|
37
39
|
status: 'DRAFT',
|
|
38
40
|
workflowStage: 'DRAFT',
|
|
@@ -314,6 +316,26 @@ describe('unmanaged section round-trip (data-loss regression)', () => {
|
|
|
314
316
|
expect(post.title).toBe('Foster independence in children with autism')
|
|
315
317
|
})
|
|
316
318
|
|
|
319
|
+
it('fetchPostForEditor reads authorName and expanded related posts', async () => {
|
|
320
|
+
cmsApiMock.mockResolvedValueOnce({
|
|
321
|
+
data: {
|
|
322
|
+
id: 'p1',
|
|
323
|
+
title: 'Logan',
|
|
324
|
+
slug: 'logan',
|
|
325
|
+
status: 'DRAFT',
|
|
326
|
+
data: {
|
|
327
|
+
title: 'Logan',
|
|
328
|
+
authorName: 'Ada Lovelace',
|
|
329
|
+
relatedPosts: [{ title: 'Next', url: '/blog/next' }],
|
|
330
|
+
sections: [],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
const post = await fetchPostForEditor('blog', 'p1')
|
|
335
|
+
expect(post.authorName).toBe('Ada Lovelace')
|
|
336
|
+
expect(post.canvasRelatedPosts).toEqual([{ title: 'Next', url: '/blog/next' }])
|
|
337
|
+
})
|
|
338
|
+
|
|
317
339
|
it('fetchPostForEditor preserves unknown section types as unmanaged', async () => {
|
|
318
340
|
cmsApiMock.mockResolvedValueOnce({
|
|
319
341
|
data: {
|
|
@@ -5,6 +5,26 @@ import { SectionEditForm } from '../../views/page-editor/SectionEditForm.js'
|
|
|
5
5
|
import { SectionsList } from '../../views/page-editor/SectionsPanel.js'
|
|
6
6
|
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
7
7
|
|
|
8
|
+
vi.mock('../../fields/RichTextField.js', () => ({
|
|
9
|
+
RichTextField: ({
|
|
10
|
+
label,
|
|
11
|
+
value,
|
|
12
|
+
onChange,
|
|
13
|
+
helpText,
|
|
14
|
+
}: {
|
|
15
|
+
label: string
|
|
16
|
+
value?: string
|
|
17
|
+
onChange: (value: string) => void
|
|
18
|
+
helpText?: string
|
|
19
|
+
}) => (
|
|
20
|
+
<label>
|
|
21
|
+
{label}
|
|
22
|
+
<textarea aria-label={label} value={value ?? ''} onChange={(e) => onChange(e.target.value)} />
|
|
23
|
+
{helpText ? <span>{helpText}</span> : null}
|
|
24
|
+
</label>
|
|
25
|
+
),
|
|
26
|
+
}))
|
|
27
|
+
|
|
8
28
|
afterEach(cleanup)
|
|
9
29
|
|
|
10
30
|
function section(over: Partial<PageSection> = {}): PageSection {
|
|
@@ -85,6 +105,41 @@ describe('SectionEditForm — content only', () => {
|
|
|
85
105
|
expect(screen.queryByLabelText('Section name')).toBeNull()
|
|
86
106
|
})
|
|
87
107
|
|
|
108
|
+
it('edits the post Body field when an article body uses that source (#519)', () => {
|
|
109
|
+
const onPostBodyChange = vi.fn()
|
|
110
|
+
const { onContentChange } = renderForm({
|
|
111
|
+
section: section({
|
|
112
|
+
sectionType: 'article-body',
|
|
113
|
+
name: 'Article Body',
|
|
114
|
+
content: { source: 'field', body: '<p>custom</p>', width: 'normal', showToc: false },
|
|
115
|
+
}),
|
|
116
|
+
postBody: '<p>Hello</p>',
|
|
117
|
+
onPostBodyChange,
|
|
118
|
+
})
|
|
119
|
+
expect(screen.getByLabelText('Post body field')).toBeTruthy()
|
|
120
|
+
expect(screen.getByText(/Shared with post settings/i)).toBeTruthy()
|
|
121
|
+
expect(screen.queryByLabelText('Custom body')).toBeNull()
|
|
122
|
+
fireEvent.change(screen.getByLabelText('Post body field'), {
|
|
123
|
+
target: { value: '<p>Next</p>' },
|
|
124
|
+
})
|
|
125
|
+
expect(onPostBodyChange).toHaveBeenCalledWith('<p>Next</p>')
|
|
126
|
+
expect(onContentChange).not.toHaveBeenCalled()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('keeps Custom body when the article body source is custom copy', () => {
|
|
130
|
+
renderForm({
|
|
131
|
+
section: section({
|
|
132
|
+
sectionType: 'article-body',
|
|
133
|
+
name: 'Article Body',
|
|
134
|
+
content: { source: 'override', body: '<p>Only here</p>' },
|
|
135
|
+
}),
|
|
136
|
+
postBody: '<p>Hello</p>',
|
|
137
|
+
onPostBodyChange: vi.fn(),
|
|
138
|
+
})
|
|
139
|
+
expect(screen.queryByLabelText('Post body field')).toBeNull()
|
|
140
|
+
expect(screen.getByLabelText('Custom body')).toBeTruthy()
|
|
141
|
+
})
|
|
142
|
+
|
|
88
143
|
it('disables every control for a read-only user', () => {
|
|
89
144
|
renderForm({ canEdit: false })
|
|
90
145
|
// Asserted on the fieldset, not the input: a disabled ancestor fieldset is
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { publicPathFor } from './public-path.js'
|
|
2
|
+
|
|
3
|
+
/** A related-post card the canvas and public resolve endpoint both understand. */
|
|
4
|
+
export interface CanvasRelatedPost {
|
|
5
|
+
title: string
|
|
6
|
+
url?: string
|
|
7
|
+
excerpt?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Byline for the canvas. The document's `authorName` is the post's author.
|
|
12
|
+
* The logged-in editor is only a fallback when that field is empty (#473).
|
|
13
|
+
*/
|
|
14
|
+
export function canvasAuthorName(
|
|
15
|
+
authorName: string | undefined,
|
|
16
|
+
session?: { name?: string; email?: string },
|
|
17
|
+
): string | undefined {
|
|
18
|
+
const fromDoc = authorName?.trim()
|
|
19
|
+
if (fromDoc) return fromDoc
|
|
20
|
+
const fallback = session?.name?.trim() || session?.email?.trim()
|
|
21
|
+
return fallback || undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hand-picked related posts already expanded to objects with titles.
|
|
26
|
+
* Returns null when the field is missing or still a list of ids, so the
|
|
27
|
+
* editor can fall back to recent published siblings.
|
|
28
|
+
*/
|
|
29
|
+
export function relatedPostsFromField(raw: unknown): CanvasRelatedPost[] | null {
|
|
30
|
+
if (!Array.isArray(raw) || raw.length === 0) return null
|
|
31
|
+
const cards: CanvasRelatedPost[] = []
|
|
32
|
+
for (const item of raw) {
|
|
33
|
+
if (!item || typeof item !== 'object') return null
|
|
34
|
+
const row = item as Record<string, unknown>
|
|
35
|
+
const title = typeof row.title === 'string' ? row.title.trim() : ''
|
|
36
|
+
if (!title) return null
|
|
37
|
+
const excerpt = typeof row.excerpt === 'string' ? row.excerpt : undefined
|
|
38
|
+
const url = typeof row.url === 'string' ? row.url : undefined
|
|
39
|
+
cards.push({
|
|
40
|
+
title,
|
|
41
|
+
...(excerpt !== undefined ? { excerpt } : {}),
|
|
42
|
+
...(url !== undefined ? { url } : {}),
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
return cards
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SiblingDoc {
|
|
49
|
+
id: string
|
|
50
|
+
title?: string | null
|
|
51
|
+
slug?: string | null
|
|
52
|
+
data?: Record<string, unknown> | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recent published siblings, matching the public resolve payload: `data.title`
|
|
57
|
+
* wins over the denormalized column, and the URL uses the collection prefix.
|
|
58
|
+
*/
|
|
59
|
+
export function relatedPostsFromSiblings(
|
|
60
|
+
docs: readonly SiblingDoc[],
|
|
61
|
+
opts: { excludeId?: string | null; urlPrefix?: string; collection: string; limit?: number },
|
|
62
|
+
): CanvasRelatedPost[] {
|
|
63
|
+
const limit = opts.limit ?? 3
|
|
64
|
+
const out: CanvasRelatedPost[] = []
|
|
65
|
+
for (const doc of docs) {
|
|
66
|
+
if (opts.excludeId && doc.id === opts.excludeId) continue
|
|
67
|
+
const data = doc.data ?? {}
|
|
68
|
+
const title = (typeof data.title === 'string' && data.title.trim()) || doc.title?.trim() || ''
|
|
69
|
+
if (!title) continue
|
|
70
|
+
const slug = doc.slug ?? (typeof data.slug === 'string' ? data.slug : '')
|
|
71
|
+
const excerpt = typeof data.excerpt === 'string' ? data.excerpt : undefined
|
|
72
|
+
out.push({
|
|
73
|
+
title,
|
|
74
|
+
...(excerpt !== undefined ? { excerpt } : {}),
|
|
75
|
+
...(slug ? { url: publicPathFor(opts.urlPrefix, opts.collection, slug) } : {}),
|
|
76
|
+
})
|
|
77
|
+
if (out.length >= limit) break
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* so the field-access layer doesn't strip them on write.
|
|
21
21
|
*/
|
|
22
22
|
import { cmsApi } from './api.js'
|
|
23
|
+
import { relatedPostsFromField, type CanvasRelatedPost } from './post-canvas-context.js'
|
|
23
24
|
import { missingCollectionFields } from './collection-schema.js'
|
|
24
25
|
import { SUBMIT_BEFORE_PUBLISH_ERROR, type WorkflowStage } from './workflow-chrome.js'
|
|
25
26
|
import {
|
|
@@ -75,6 +76,13 @@ export interface EditorPost {
|
|
|
75
76
|
featuredImageUrl: string
|
|
76
77
|
body: string
|
|
77
78
|
category: string
|
|
79
|
+
/** Display byline. Empty when the document has no `authorName`. */
|
|
80
|
+
authorName: string
|
|
81
|
+
/**
|
|
82
|
+
* Hand-picked related posts ready for the canvas. Null when the field is
|
|
83
|
+
* absent or only ids, so the editor loads recent published siblings instead.
|
|
84
|
+
*/
|
|
85
|
+
canvasRelatedPosts: CanvasRelatedPost[] | null
|
|
78
86
|
/** Editorial tags as plain strings (stored as `[{ tag }]` rows, see {@link parseTags}). */
|
|
79
87
|
tags: string[]
|
|
80
88
|
status: PostStatus
|
|
@@ -297,6 +305,8 @@ function rowToEditorPost(postType: string, doc: DocumentApiRow): EditorPost {
|
|
|
297
305
|
featuredImageUrl: featured.url,
|
|
298
306
|
body: dataStr(data, 'body'),
|
|
299
307
|
category: dataStr(data, 'category'),
|
|
308
|
+
authorName: dataStr(data, 'authorName'),
|
|
309
|
+
canvasRelatedPosts: relatedPostsFromField(data.relatedPosts),
|
|
300
310
|
tags: parseTags(data.tags),
|
|
301
311
|
status: doc.status ?? 'DRAFT',
|
|
302
312
|
workflowStage: (doc.workflowStage as WorkflowStage | undefined) ?? 'DRAFT',
|
|
@@ -491,6 +501,8 @@ export async function emptyPostFromTemplate(postType: string): Promise<EditorPos
|
|
|
491
501
|
featuredImageUrl: '',
|
|
492
502
|
body: '',
|
|
493
503
|
category: '',
|
|
504
|
+
authorName: '',
|
|
505
|
+
canvasRelatedPosts: null,
|
|
494
506
|
tags: [],
|
|
495
507
|
status: 'DRAFT',
|
|
496
508
|
workflowStage: 'DRAFT',
|
|
@@ -90,11 +90,11 @@ export function SectionEditForm({
|
|
|
90
90
|
<fieldset disabled={!canEdit} className="space-y-3">
|
|
91
91
|
{editPostBodyField && (
|
|
92
92
|
<RichTextField
|
|
93
|
-
label="
|
|
93
|
+
label="Post body field"
|
|
94
94
|
value={postBody ?? ''}
|
|
95
95
|
onChange={onPostBodyChange}
|
|
96
96
|
className="[&_.ProseMirror]:min-h-[240px]"
|
|
97
|
-
helpText="
|
|
97
|
+
helpText="Shared with post settings. Edits here change the post's Body field, which the canvas and the live site both render."
|
|
98
98
|
/>
|
|
99
99
|
)}
|
|
100
100
|
|
|
@@ -148,11 +148,11 @@ export function SectionInspector({
|
|
|
148
148
|
</legend>
|
|
149
149
|
{editPostBodyField && (
|
|
150
150
|
<RichTextField
|
|
151
|
-
label="
|
|
151
|
+
label="Post body field"
|
|
152
152
|
value={postBody ?? ''}
|
|
153
153
|
onChange={onPostBodyChange}
|
|
154
154
|
className="[&_.ProseMirror]:min-h-[240px]"
|
|
155
|
-
helpText="
|
|
155
|
+
helpText="Shared with post settings. Edits here change the post's Body field, which the canvas and the live site both render."
|
|
156
156
|
/>
|
|
157
157
|
)}
|
|
158
158
|
{contentFields.map((field) => (
|
|
@@ -125,7 +125,7 @@ function EmptyBody({ onAddSection }: { onAddSection?: () => void }) {
|
|
|
125
125
|
</span>
|
|
126
126
|
<p className="text-muted-foreground text-sm">
|
|
127
127
|
{onAddSection
|
|
128
|
-
? '
|
|
128
|
+
? 'Add body sections to build out this post. Hover sections for a light outline, click to open its editor in the section list.'
|
|
129
129
|
: 'This post’s template renders no body sections. Layout is design-owned — sections come from the template in code. Pick a different template in Post settings, or edit the template itself.'}
|
|
130
130
|
</p>
|
|
131
131
|
{onAddSection && (
|
|
@@ -21,6 +21,7 @@ function formatDate(value?: string | null): string {
|
|
|
21
21
|
* public preview match.
|
|
22
22
|
*/
|
|
23
23
|
export function PostHeader({ config, context }: PostHeaderProps) {
|
|
24
|
+
if (config.layout === 'none') return null
|
|
24
25
|
const title = context.title || 'Untitled post'
|
|
25
26
|
const date = config.showDate ? formatDate(context.publishDate) : ''
|
|
26
27
|
const overlay =
|
|
@@ -69,32 +69,40 @@ export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHead
|
|
|
69
69
|
<option value="centered">Centered</option>
|
|
70
70
|
<option value="left">Left-aligned</option>
|
|
71
71
|
<option value="overlay">Image overlay</option>
|
|
72
|
+
<option value="none">None</option>
|
|
72
73
|
</select>
|
|
74
|
+
{header.layout === 'none' && (
|
|
75
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
76
|
+
The built-in header is hidden. Put the title in a section instead.
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
73
79
|
</div>
|
|
74
80
|
</fieldset>
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
{header.layout !== 'none' && (
|
|
83
|
+
<fieldset disabled={!canEdit} className="space-y-2">
|
|
84
|
+
<legend className="text-muted-foreground mb-2 text-[10px] font-medium tracking-wider uppercase">
|
|
85
|
+
Show in header
|
|
86
|
+
</legend>
|
|
87
|
+
{TOGGLES.map((t) => {
|
|
88
|
+
const value = header[t.key] === true
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
key={t.key}
|
|
92
|
+
className="border-border text-foreground flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm"
|
|
93
|
+
>
|
|
94
|
+
<span>{t.label}</span>
|
|
95
|
+
<Toggle
|
|
96
|
+
checked={value}
|
|
97
|
+
disabled={!canEdit}
|
|
98
|
+
onCheckedChange={(v) => onChange({ [t.key]: v } as Partial<PostHeaderConfig>)}
|
|
99
|
+
aria-label={t.label}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
})}
|
|
104
|
+
</fieldset>
|
|
105
|
+
)}
|
|
98
106
|
</div>
|
|
99
107
|
</ResizableAside>
|
|
100
108
|
)
|
|
@@ -55,6 +55,12 @@ import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
|
|
|
55
55
|
import { snapshotBaseline, type ReflowBaseline } from '../../lib/reflow-baseline.js'
|
|
56
56
|
import { ReflowContext } from '../page-editor/reflow-context.js'
|
|
57
57
|
import type { Zoom } from '../../lib/breakpoints.js'
|
|
58
|
+
import { cmsApi } from '../../lib/api.js'
|
|
59
|
+
import {
|
|
60
|
+
canvasAuthorName,
|
|
61
|
+
relatedPostsFromSiblings,
|
|
62
|
+
type CanvasRelatedPost,
|
|
63
|
+
} from '../../lib/post-canvas-context.js'
|
|
58
64
|
import { inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
|
|
59
65
|
import { PostEditorCanvas } from './PostEditorCanvas.js'
|
|
60
66
|
import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'
|
|
@@ -333,6 +339,43 @@ export function PostSectionEditor({
|
|
|
333
339
|
// ─── Unsaved-changes guard (client-side navigation + browser unload) ──
|
|
334
340
|
useNavigationBlocker(dirty)
|
|
335
341
|
|
|
342
|
+
const [siblingPosts, setSiblingPosts] = useState<CanvasRelatedPost[]>([])
|
|
343
|
+
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
if (!post?.id || post.canvasRelatedPosts !== null) {
|
|
346
|
+
setSiblingPosts([])
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
let cancelled = false
|
|
350
|
+
const urlPrefix = config?.collections?.[postType]?.urlPrefix
|
|
351
|
+
cmsApi<{
|
|
352
|
+
docs?: Array<{
|
|
353
|
+
id: string
|
|
354
|
+
title?: string | null
|
|
355
|
+
slug?: string | null
|
|
356
|
+
data?: Record<string, unknown> | null
|
|
357
|
+
}>
|
|
358
|
+
}>(
|
|
359
|
+
`/collections/${encodeURIComponent(postType)}?status=PUBLISHED&pageSize=4&sort=publishedAt&order=desc`,
|
|
360
|
+
)
|
|
361
|
+
.then((res) => {
|
|
362
|
+
if (cancelled) return
|
|
363
|
+
setSiblingPosts(
|
|
364
|
+
relatedPostsFromSiblings(res.data?.docs ?? [], {
|
|
365
|
+
excludeId: post.id,
|
|
366
|
+
urlPrefix,
|
|
367
|
+
collection: postType,
|
|
368
|
+
}),
|
|
369
|
+
)
|
|
370
|
+
})
|
|
371
|
+
.catch(() => {
|
|
372
|
+
if (!cancelled) setSiblingPosts([])
|
|
373
|
+
})
|
|
374
|
+
return () => {
|
|
375
|
+
cancelled = true
|
|
376
|
+
}
|
|
377
|
+
}, [post?.id, post?.canvasRelatedPosts, postType, config])
|
|
378
|
+
|
|
336
379
|
const context: PostRenderContext = useMemo(
|
|
337
380
|
() => ({
|
|
338
381
|
title: post?.title,
|
|
@@ -341,9 +384,10 @@ export function PostSectionEditor({
|
|
|
341
384
|
featuredImageUrl: post?.featuredImageUrl,
|
|
342
385
|
publishDate: post?.publishDate ?? post?.updatedAt ?? null,
|
|
343
386
|
category: post?.category,
|
|
344
|
-
author: { name:
|
|
387
|
+
author: { name: canvasAuthorName(post?.authorName, session?.user) },
|
|
388
|
+
relatedPosts: post?.canvasRelatedPosts ?? siblingPosts,
|
|
345
389
|
}),
|
|
346
|
-
[post, session],
|
|
390
|
+
[post, session, siblingPosts],
|
|
347
391
|
)
|
|
348
392
|
|
|
349
393
|
// ─── Section mutations ───────────────────────────────────────────────
|