fix(admin): resolve and display current product image
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 2m45s

Form state for the images array only holds the media ID, not the
populated doc. Read images.0.image via useField, then fetch
/api/media/<id> to resolve url+alt for display. On upload, add a new
row via dispatchFields if the array is empty, otherwise update in
place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-04-21 14:29:15 +02:00
parent 94fdb37dc3
commit a41dfba9e6

View File

@ -1,29 +1,58 @@
'use client'
import { useField } from '@payloadcms/ui'
import { useRef, useState } from 'react'
import { useField, useForm } from '@payloadcms/ui'
import { useEffect, useRef, useState } from 'react'
type MediaDoc = { id: number | string; url?: string | null; alt?: string | null }
type ImageEntry = { id?: string; image?: MediaDoc | string | null } | null | undefined
type Props = {
displayName?: string
}
function resolveFirstImage(entries: ImageEntry[] | undefined | null): { url: string; alt: string } {
const first = entries?.[0]?.image
if (!first) return { url: '', alt: '' }
if (typeof first === 'string') return { url: '', alt: '' }
return { url: first.url ?? '', alt: first.alt ?? '' }
function extractMediaId(value: unknown): string | number | null {
if (value == null) return null
if (typeof value === 'number' || typeof value === 'string') return value
if (typeof value === 'object' && 'id' in (value as object)) {
return (value as { id: number | string }).id ?? null
}
return null
}
export function ImageUploadSlot({ displayName }: Props) {
const { value: images, setValue } = useField<ImageEntry[]>({ path: 'images' })
const { value: mediaValue, setValue: setMedia } = useField<unknown>({ path: 'images.0.image' })
const { dispatchFields } = useForm()
const fileInput = useRef<HTMLInputElement | null>(null)
const [uploading, setUploading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [resolved, setResolved] = useState<MediaDoc | null>(null)
const { url, alt } = resolveFirstImage(images)
const mediaId = extractMediaId(mediaValue)
useEffect(() => {
let cancelled = false
if (mediaId == null) {
setResolved(null)
return
}
if (resolved && resolved.id === mediaId) return
if (mediaValue && typeof mediaValue === 'object' && 'url' in (mediaValue as object)) {
setResolved(mediaValue as MediaDoc)
return
}
;(async () => {
try {
const res = await fetch(`/api/media/${mediaId}`, { credentials: 'include' })
if (!res.ok) return
const doc = await res.json()
if (!cancelled) setResolved({ id: doc.id, url: doc.url, alt: doc.alt })
} catch {
/* ignore */
}
})()
return () => {
cancelled = true
}
}, [mediaId, mediaValue, resolved])
const onPick = () => fileInput.current?.click()
@ -40,9 +69,19 @@ export function ImageUploadSlot({ displayName }: Props) {
const res = await fetch('/api/media', { method: 'POST', body: fd, credentials: 'include' })
if (!res.ok) throw new Error(`upload ${res.status}`)
const { doc } = await res.json()
const media: MediaDoc = { id: doc.id, url: doc.url, alt: doc.alt }
const next = [{ image: media }, ...(images ?? []).slice(1)]
setValue(next)
if (mediaId == null) {
dispatchFields({
type: 'ADD_ROW',
path: 'images',
rowIndex: 0,
subFieldState: {
image: { initialValue: doc.id, valid: true, value: doc.id },
},
})
} else {
setMedia(doc.id)
}
setResolved({ id: doc.id, url: doc.url, alt: doc.alt })
} catch (err) {
setError(err instanceof Error ? err.message : 'upload failed')
} finally {
@ -50,10 +89,13 @@ export function ImageUploadSlot({ displayName }: Props) {
}
}
const url = resolved?.url ?? ''
const alt = resolved?.alt ?? displayName ?? 'Produit'
return (
<div className="rebours-admin-panel__img-slot" onClick={onPick} role="button" tabIndex={0}>
{url ? (
<img src={url} alt={alt || displayName || 'Produit'} />
<img src={url} alt={alt} />
) : (
<div className="rebours-admin-panel__img-placeholder">
Cliquez pour<br />uploader une image