@prenta/admin 1.6.0 → 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 +6 -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/post-editor-service-api.test.js +1 -0
- package/dist/__tests__/lib/post-editor-service-api.test.js.map +1 -1
- package/dist/__tests__/lib/post-editor-service.test.js +25 -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__/views/post-fields-modal.render.test.js +19 -0
- package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
- package/dist/__tests__/views/post-section-editor.test.js +1 -0
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/post-tab.render.test.js +1 -0
- package/dist/__tests__/views/post-tab.render.test.js.map +1 -1
- package/dist/lib/page-audit-input.js +1 -1
- package/dist/lib/page-audit-input.js.map +1 -1
- package/dist/lib/post-editor-service.d.ts +18 -3
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +33 -13
- 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/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.js +3 -3
- 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 +3 -3
- package/src/__tests__/lib/page-audit-input.test.ts +11 -0
- package/src/__tests__/lib/post-editor-service-api.test.ts +1 -0
- package/src/__tests__/lib/post-editor-service.test.ts +26 -9
- package/src/__tests__/lib/version-history.test.ts +29 -0
- package/src/__tests__/views/post-fields-modal.render.test.tsx +39 -0
- package/src/__tests__/views/post-section-editor.test.tsx +1 -0
- package/src/__tests__/views/post-tab.render.test.tsx +1 -0
- package/src/lib/page-audit-input.ts +1 -1
- package/src/lib/post-editor-service.ts +45 -10
- package/src/lib/version-history.ts +3 -7
- 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 +3 -3
- package/src/views/post-editor/PostTab.tsx +1 -0
|
@@ -244,6 +244,17 @@ describe('post audit extras', () => {
|
|
|
244
244
|
expect(imageSlots(withImage.sections)).toHaveLength(1)
|
|
245
245
|
expect(imageSlots(withImage.sections)[0]?.sectionId).toBe('__post-featured-image')
|
|
246
246
|
})
|
|
247
|
+
|
|
248
|
+
it('does not invent an image slot from a bare media id', () => {
|
|
249
|
+
const withId = buildPageAuditInput({
|
|
250
|
+
sections: textOnlySections,
|
|
251
|
+
meta: postMeta,
|
|
252
|
+
slug: 'a-post',
|
|
253
|
+
record: null,
|
|
254
|
+
post: { featuredImage: 'cmssbdfxo000004jrzphzt2bj' },
|
|
255
|
+
})
|
|
256
|
+
expect(imageSlots(withId.sections)).toHaveLength(0)
|
|
257
|
+
})
|
|
247
258
|
})
|
|
248
259
|
|
|
249
260
|
function builderTree(heroData: Record<string, unknown>) {
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
missingPostFields,
|
|
18
18
|
savePostDraft,
|
|
19
19
|
parseFeaturedImage,
|
|
20
|
+
parseFeaturedImageRef,
|
|
20
21
|
} = await import('../../lib/post-editor-service.js')
|
|
21
22
|
|
|
22
23
|
type EditorPostT = Awaited<ReturnType<typeof emptyPostFromTemplate>>
|
|
@@ -29,6 +30,7 @@ function basePost(over: Partial<EditorPostT> = {}): EditorPostT {
|
|
|
29
30
|
slug: 'hello',
|
|
30
31
|
excerpt: '',
|
|
31
32
|
featuredImage: '',
|
|
33
|
+
featuredImageUrl: '',
|
|
32
34
|
body: '',
|
|
33
35
|
category: '',
|
|
34
36
|
tags: [],
|
|
@@ -494,21 +496,34 @@ describe('tags round-trip', () => {
|
|
|
494
496
|
expect(post.tags).toEqual(['AI', 'Nextjs'])
|
|
495
497
|
})
|
|
496
498
|
|
|
499
|
+
it('parseFeaturedImageRef splits persist id from display url', () => {
|
|
500
|
+
const mediaId = 'cmry1z9yr000004l832th7bf2'
|
|
501
|
+
const url = 'https://cdn.example.com/hero.webp'
|
|
502
|
+
expect(parseFeaturedImageRef({ id: mediaId, url, alt: 'Hero' })).toEqual({
|
|
503
|
+
id: mediaId,
|
|
504
|
+
url,
|
|
505
|
+
})
|
|
506
|
+
expect(parseFeaturedImageRef(mediaId)).toEqual({ id: mediaId, url: '' })
|
|
507
|
+
expect(parseFeaturedImageRef(url)).toEqual({ id: url, url })
|
|
508
|
+
expect(parseFeaturedImageRef({ publicUrl: url })).toEqual({ id: url, url })
|
|
509
|
+
expect(parseFeaturedImageRef({ src: '/uploads/hero.webp' })).toEqual({
|
|
510
|
+
id: '/uploads/hero.webp',
|
|
511
|
+
url: '/uploads/hero.webp',
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
497
515
|
it('parseFeaturedImage / fetchPostForEditor keep denormalized media objects (#671)', async () => {
|
|
498
516
|
const mediaId = 'cmry1z9yr000004l832th7bf2'
|
|
517
|
+
const url = 'https://cdn.example.com/hero.webp'
|
|
499
518
|
expect(
|
|
500
519
|
parseFeaturedImage({
|
|
501
520
|
id: mediaId,
|
|
502
|
-
url
|
|
521
|
+
url,
|
|
503
522
|
alt: 'Hero',
|
|
504
523
|
}),
|
|
505
524
|
).toBe(mediaId)
|
|
506
|
-
expect(parseFeaturedImage(
|
|
507
|
-
|
|
508
|
-
)
|
|
509
|
-
expect(parseFeaturedImage({ url: 'https://cdn.example.com/hero.webp' })).toBe(
|
|
510
|
-
'https://cdn.example.com/hero.webp',
|
|
511
|
-
)
|
|
525
|
+
expect(parseFeaturedImage(url)).toBe(url)
|
|
526
|
+
expect(parseFeaturedImage({ url })).toBe(url)
|
|
512
527
|
|
|
513
528
|
cmsApiMock.mockResolvedValueOnce({
|
|
514
529
|
data: {
|
|
@@ -519,7 +534,7 @@ describe('tags round-trip', () => {
|
|
|
519
534
|
data: {
|
|
520
535
|
featuredImage: {
|
|
521
536
|
id: mediaId,
|
|
522
|
-
url
|
|
537
|
+
url,
|
|
523
538
|
alt: 'Hero',
|
|
524
539
|
},
|
|
525
540
|
},
|
|
@@ -527,16 +542,18 @@ describe('tags round-trip', () => {
|
|
|
527
542
|
})
|
|
528
543
|
const post = await fetchPostForEditor('blog', 'p1')
|
|
529
544
|
expect(post.featuredImage).toBe(mediaId)
|
|
545
|
+
expect(post.featuredImageUrl).toBe(url)
|
|
530
546
|
|
|
531
547
|
cmsApiMock.mockResolvedValueOnce({
|
|
532
548
|
data: { id: 'p1', title: 'Hello', slug: 'hello', status: 'DRAFT', data: {} },
|
|
533
549
|
})
|
|
534
|
-
await savePostDraft(basePost({ id: 'p1', featuredImage: mediaId }))
|
|
550
|
+
await savePostDraft(basePost({ id: 'p1', featuredImage: mediaId, featuredImageUrl: url }))
|
|
535
551
|
const body = JSON.parse(cmsApiMock.mock.calls[1]![1].body as string) as {
|
|
536
552
|
featuredImage: string
|
|
537
553
|
}
|
|
538
554
|
expect(body.featuredImage).toBe(mediaId)
|
|
539
555
|
expect(body.featuredImage).not.toBe('')
|
|
556
|
+
expect(body).not.toHaveProperty('featuredImageUrl')
|
|
540
557
|
})
|
|
541
558
|
})
|
|
542
559
|
|
|
@@ -60,6 +60,35 @@ describe('toPageVersion', () => {
|
|
|
60
60
|
expect(mapped.body).toBe('<p>Snapshot body</p>')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
it('does not treat a bare media id as a canvas preview URL', () => {
|
|
64
|
+
const mapped = toPageVersion({
|
|
65
|
+
id: 'v1',
|
|
66
|
+
changeType: 'UPDATE',
|
|
67
|
+
createdAt: '2026-08-02T12:00:00Z',
|
|
68
|
+
data: {
|
|
69
|
+
featuredImage: 'cmssbdfxo000004jrzphzt2bj',
|
|
70
|
+
sections: [],
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
expect(mapped.featuredImage).toBeUndefined()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('prefers the public URL over the media id on a denormalized snapshot', () => {
|
|
77
|
+
const mapped = toPageVersion({
|
|
78
|
+
id: 'v1',
|
|
79
|
+
changeType: 'UPDATE',
|
|
80
|
+
createdAt: '2026-08-02T12:00:00Z',
|
|
81
|
+
data: {
|
|
82
|
+
featuredImage: {
|
|
83
|
+
id: 'cmssbdfxo000004jrzphzt2bj',
|
|
84
|
+
url: 'https://cdn.example.com/hero.webp',
|
|
85
|
+
},
|
|
86
|
+
sections: [],
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
expect(mapped.featuredImage).toBe('https://cdn.example.com/hero.webp')
|
|
90
|
+
})
|
|
91
|
+
|
|
63
92
|
it('preserves an unmanaged section rather than dropping it from the snapshot', () => {
|
|
64
93
|
// Coercing a snapshot more strictly than the live document reads would
|
|
65
94
|
// make an externally-managed section look deleted in every version.
|
|
@@ -39,6 +39,7 @@ function basePost(over: Partial<ModalProps['post']> = {}): ModalProps['post'] {
|
|
|
39
39
|
slug: 'hello',
|
|
40
40
|
excerpt: '',
|
|
41
41
|
featuredImage: '',
|
|
42
|
+
featuredImageUrl: '',
|
|
42
43
|
body: '',
|
|
43
44
|
category: '',
|
|
44
45
|
tags: [],
|
|
@@ -175,4 +176,42 @@ describe('PostFieldsModal', () => {
|
|
|
175
176
|
expect(onClose).toHaveBeenCalledTimes(1)
|
|
176
177
|
expect(screen.queryByRole('dialog', { name: 'Discard unsaved changes?' })).toBeNull()
|
|
177
178
|
})
|
|
179
|
+
|
|
180
|
+
it('previews the featured image URL, not the stored media id', () => {
|
|
181
|
+
const mediaId = 'cmssbdfxo000004jrzphzt2bj'
|
|
182
|
+
const url = 'https://cdn.example.com/hero.webp'
|
|
183
|
+
render(
|
|
184
|
+
<PostFieldsModal
|
|
185
|
+
open
|
|
186
|
+
post={basePost({ featuredImage: mediaId, featuredImageUrl: url })}
|
|
187
|
+
canEdit
|
|
188
|
+
onClose={() => {}}
|
|
189
|
+
onSave={() => {}}
|
|
190
|
+
/>,
|
|
191
|
+
)
|
|
192
|
+
const input = screen.getByLabelText('Featured image') as HTMLInputElement
|
|
193
|
+
expect(input.value).toBe(url)
|
|
194
|
+
expect(input.value).not.toBe(mediaId)
|
|
195
|
+
const preview = document.querySelector('img')
|
|
196
|
+
expect(preview?.getAttribute('src')).toBe(url)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('saves the media id when the input is showing the public URL', () => {
|
|
200
|
+
const onSave = vi.fn()
|
|
201
|
+
const mediaId = 'cmssbdfxo000004jrzphzt2bj'
|
|
202
|
+
const url = 'https://cdn.example.com/hero.webp'
|
|
203
|
+
render(
|
|
204
|
+
<PostFieldsModal
|
|
205
|
+
open
|
|
206
|
+
post={basePost({ featuredImage: mediaId, featuredImageUrl: url })}
|
|
207
|
+
canEdit
|
|
208
|
+
onClose={() => {}}
|
|
209
|
+
onSave={onSave}
|
|
210
|
+
/>,
|
|
211
|
+
)
|
|
212
|
+
fireEvent.click(screen.getByRole('button', { name: 'Done' }))
|
|
213
|
+
expect(onSave).toHaveBeenCalledWith(
|
|
214
|
+
expect.objectContaining({ featuredImage: mediaId, featuredImageUrl: url }),
|
|
215
|
+
)
|
|
216
|
+
})
|
|
178
217
|
})
|
|
@@ -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',
|
|
@@ -74,7 +74,10 @@ export interface EditorPost {
|
|
|
74
74
|
title: string
|
|
75
75
|
slug: string
|
|
76
76
|
excerpt: string
|
|
77
|
+
/** Persist identity — media id when present so usage tracking survives (#671). */
|
|
77
78
|
featuredImage: string
|
|
79
|
+
/** Public URL for `<img src>` and the details input. Empty when only an id is known. */
|
|
80
|
+
featuredImageUrl: string
|
|
78
81
|
body: string
|
|
79
82
|
category: string
|
|
80
83
|
/** Editorial tags as plain strings (stored as `[{ tag }]` rows, see {@link parseTags}). */
|
|
@@ -216,20 +219,49 @@ function dataStr(data: Record<string, unknown>, key: string): string {
|
|
|
216
219
|
return typeof v === 'string' ? v : ''
|
|
217
220
|
}
|
|
218
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
|
+
|
|
219
241
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
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).
|
|
223
247
|
*/
|
|
224
|
-
export function
|
|
225
|
-
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
|
+
}
|
|
226
253
|
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
227
254
|
const obj = raw as Record<string, unknown>
|
|
228
|
-
|
|
229
|
-
const url = obj.url ?? obj.publicUrl ?? obj.src
|
|
230
|
-
|
|
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 }
|
|
231
258
|
}
|
|
232
|
-
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
|
|
233
265
|
}
|
|
234
266
|
|
|
235
267
|
/**
|
|
@@ -255,6 +287,7 @@ export function parseTags(raw: unknown): string[] {
|
|
|
255
287
|
function rowToEditorPost(postType: string, doc: DocumentApiRow): EditorPost {
|
|
256
288
|
const data = doc.data ?? {}
|
|
257
289
|
const dataTitle = dataStr(data, 'title')
|
|
290
|
+
const featured = parseFeaturedImageRef(data.featuredImage)
|
|
258
291
|
return {
|
|
259
292
|
id: doc.id,
|
|
260
293
|
postType,
|
|
@@ -263,7 +296,8 @@ function rowToEditorPost(postType: string, doc: DocumentApiRow): EditorPost {
|
|
|
263
296
|
title: dataTitle || (doc.title ?? ''),
|
|
264
297
|
slug: doc.slug ?? dataStr(data, 'slug'),
|
|
265
298
|
excerpt: dataStr(data, 'excerpt'),
|
|
266
|
-
featuredImage:
|
|
299
|
+
featuredImage: featured.id,
|
|
300
|
+
featuredImageUrl: featured.url,
|
|
267
301
|
body: dataStr(data, 'body'),
|
|
268
302
|
category: dataStr(data, 'category'),
|
|
269
303
|
tags: parseTags(data.tags),
|
|
@@ -457,6 +491,7 @@ export async function emptyPostFromTemplate(postType: string): Promise<EditorPos
|
|
|
457
491
|
slug: '',
|
|
458
492
|
excerpt: '',
|
|
459
493
|
featuredImage: '',
|
|
494
|
+
featuredImageUrl: '',
|
|
460
495
|
body: '',
|
|
461
496
|
category: '',
|
|
462
497
|
tags: [],
|
|
@@ -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 {
|
|
@@ -23,7 +23,10 @@ export interface PostFieldsFormValues {
|
|
|
23
23
|
title: string
|
|
24
24
|
slug: string
|
|
25
25
|
excerpt: string
|
|
26
|
+
/** Persist identity (media id when known). */
|
|
26
27
|
featuredImage: string
|
|
28
|
+
/** Public URL shown in the input and used as `<img src>`. */
|
|
29
|
+
featuredImageUrl: string
|
|
27
30
|
category: string
|
|
28
31
|
tags: string[]
|
|
29
32
|
}
|
|
@@ -286,10 +289,13 @@ export function PostFieldsForm({
|
|
|
286
289
|
<input
|
|
287
290
|
id={imageId}
|
|
288
291
|
className={`${INPUT} min-w-0 flex-1`}
|
|
289
|
-
value={values.
|
|
292
|
+
value={values.featuredImageUrl}
|
|
290
293
|
disabled={!canEdit}
|
|
291
294
|
placeholder="https://… or pick from the media library"
|
|
292
|
-
onChange={(e) =>
|
|
295
|
+
onChange={(e) => {
|
|
296
|
+
const next = e.target.value
|
|
297
|
+
onChange({ featuredImage: next, featuredImageUrl: next })
|
|
298
|
+
}}
|
|
293
299
|
/>
|
|
294
300
|
<button
|
|
295
301
|
type="button"
|
|
@@ -300,9 +306,9 @@ export function PostFieldsForm({
|
|
|
300
306
|
Browse
|
|
301
307
|
</button>
|
|
302
308
|
</div>
|
|
303
|
-
{values.
|
|
309
|
+
{values.featuredImageUrl && (
|
|
304
310
|
<div className="border-border bg-muted mt-2 overflow-hidden rounded-md border">
|
|
305
|
-
<img src={values.
|
|
311
|
+
<img src={values.featuredImageUrl} alt="" className="h-28 w-full object-cover" />
|
|
306
312
|
</div>
|
|
307
313
|
)}
|
|
308
314
|
</div>
|
|
@@ -311,8 +317,11 @@ export function PostFieldsForm({
|
|
|
311
317
|
<MediaPickerModal
|
|
312
318
|
open={mediaOpen}
|
|
313
319
|
onClose={() => setMediaOpen(false)}
|
|
320
|
+
onSelectItem={(item) => {
|
|
321
|
+
onChange({ featuredImage: item.id || item.url, featuredImageUrl: item.url })
|
|
322
|
+
}}
|
|
314
323
|
onSelect={(url) => {
|
|
315
|
-
onChange({
|
|
324
|
+
onChange({ featuredImageUrl: url })
|
|
316
325
|
setMediaOpen(false)
|
|
317
326
|
}}
|
|
318
327
|
accept="image/*"
|
|
@@ -29,6 +29,7 @@ type FormSnapshot = {
|
|
|
29
29
|
slug: string
|
|
30
30
|
excerpt: string
|
|
31
31
|
featuredImage: string
|
|
32
|
+
featuredImageUrl: string
|
|
32
33
|
category: string
|
|
33
34
|
tags: string
|
|
34
35
|
}
|
|
@@ -39,6 +40,7 @@ function snapshotFromPost(post: EditorPost): FormSnapshot {
|
|
|
39
40
|
slug: post.slug,
|
|
40
41
|
excerpt: post.excerpt,
|
|
41
42
|
featuredImage: post.featuredImage,
|
|
43
|
+
featuredImageUrl: post.featuredImageUrl,
|
|
42
44
|
category: post.category,
|
|
43
45
|
tags: [...post.tags].sort().join('\0'),
|
|
44
46
|
}
|
|
@@ -64,6 +66,7 @@ export function PostFieldsModal({
|
|
|
64
66
|
const [slug, setSlug] = useState(post.slug)
|
|
65
67
|
const [excerpt, setExcerpt] = useState(post.excerpt)
|
|
66
68
|
const [featuredImage, setFeaturedImage] = useState(post.featuredImage)
|
|
69
|
+
const [featuredImageUrl, setFeaturedImageUrl] = useState(post.featuredImageUrl)
|
|
67
70
|
const [category, setCategory] = useState(post.category)
|
|
68
71
|
const [tags, setTags] = useState<string[]>(post.tags)
|
|
69
72
|
const [slugTouched, setSlugTouched] = useState(Boolean(post.slug))
|
|
@@ -82,13 +85,22 @@ export function PostFieldsModal({
|
|
|
82
85
|
setSlug(post.slug)
|
|
83
86
|
setExcerpt(post.excerpt)
|
|
84
87
|
setFeaturedImage(post.featuredImage)
|
|
88
|
+
setFeaturedImageUrl(post.featuredImageUrl)
|
|
85
89
|
setCategory(post.category)
|
|
86
90
|
setTags(post.tags)
|
|
87
91
|
setSlugTouched(Boolean(post.slug))
|
|
88
92
|
baselineRef.current = snapshotFromPost(post)
|
|
89
93
|
}, [open, post])
|
|
90
94
|
|
|
91
|
-
const values: PostFieldsFormValues = {
|
|
95
|
+
const values: PostFieldsFormValues = {
|
|
96
|
+
title,
|
|
97
|
+
slug,
|
|
98
|
+
excerpt,
|
|
99
|
+
featuredImage,
|
|
100
|
+
featuredImageUrl,
|
|
101
|
+
category,
|
|
102
|
+
tags,
|
|
103
|
+
}
|
|
92
104
|
|
|
93
105
|
const isDirty = useMemo(() => {
|
|
94
106
|
if (!open || !baselineRef.current) return false
|
|
@@ -108,6 +120,7 @@ export function PostFieldsModal({
|
|
|
108
120
|
if (patch.slug !== undefined) setSlug(patch.slug)
|
|
109
121
|
if (patch.excerpt !== undefined) setExcerpt(patch.excerpt)
|
|
110
122
|
if (patch.featuredImage !== undefined) setFeaturedImage(patch.featuredImage)
|
|
123
|
+
if (patch.featuredImageUrl !== undefined) setFeaturedImageUrl(patch.featuredImageUrl)
|
|
111
124
|
if (patch.category !== undefined) setCategory(patch.category)
|
|
112
125
|
if (patch.tags !== undefined) setTags(patch.tags)
|
|
113
126
|
}
|
|
@@ -116,7 +129,7 @@ export function PostFieldsModal({
|
|
|
116
129
|
|
|
117
130
|
function handleSave() {
|
|
118
131
|
if (slugError) return
|
|
119
|
-
onSave({ title, slug, excerpt, featuredImage, category, tags })
|
|
132
|
+
onSave({ title, slug, excerpt, featuredImage, featuredImageUrl, category, tags })
|
|
120
133
|
onClose()
|
|
121
134
|
}
|
|
122
135
|
|
|
@@ -226,7 +226,7 @@ export function PostSectionEditor({
|
|
|
226
226
|
post: {
|
|
227
227
|
title: post?.title,
|
|
228
228
|
bodyHtml: post?.body,
|
|
229
|
-
featuredImage: post?.
|
|
229
|
+
featuredImage: post?.featuredImageUrl,
|
|
230
230
|
},
|
|
231
231
|
})
|
|
232
232
|
const audit = liveAudit ?? savedAudit
|
|
@@ -338,7 +338,7 @@ export function PostSectionEditor({
|
|
|
338
338
|
title: post?.title,
|
|
339
339
|
excerpt: post?.excerpt,
|
|
340
340
|
body: post?.body,
|
|
341
|
-
featuredImageUrl: post?.
|
|
341
|
+
featuredImageUrl: post?.featuredImageUrl,
|
|
342
342
|
publishDate: post?.publishDate ?? post?.updatedAt ?? null,
|
|
343
343
|
category: post?.category,
|
|
344
344
|
author: { name: session?.user?.name || session?.user?.email },
|
|
@@ -841,7 +841,7 @@ export function PostSectionEditor({
|
|
|
841
841
|
contentHtml={seoContentHtml}
|
|
842
842
|
title={post.title}
|
|
843
843
|
bodyHtml={post.body}
|
|
844
|
-
featuredImage={post.
|
|
844
|
+
featuredImage={post.featuredImageUrl}
|
|
845
845
|
draftMeta={{
|
|
846
846
|
seoTitle: post.seoTitle,
|
|
847
847
|
seoDescription: post.seoDescription,
|