fix(admin): resolve and display current product image
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 2m45s
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:
parent
94fdb37dc3
commit
a41dfba9e6
@ -1,29 +1,58 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useField } from '@payloadcms/ui'
|
import { useField, useForm } from '@payloadcms/ui'
|
||||||
import { useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
type MediaDoc = { id: number | string; url?: string | null; alt?: string | null }
|
type MediaDoc = { id: number | string; url?: string | null; alt?: string | null }
|
||||||
type ImageEntry = { id?: string; image?: MediaDoc | string | null } | null | undefined
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
displayName?: string
|
displayName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveFirstImage(entries: ImageEntry[] | undefined | null): { url: string; alt: string } {
|
function extractMediaId(value: unknown): string | number | null {
|
||||||
const first = entries?.[0]?.image
|
if (value == null) return null
|
||||||
if (!first) return { url: '', alt: '' }
|
if (typeof value === 'number' || typeof value === 'string') return value
|
||||||
if (typeof first === 'string') return { url: '', alt: '' }
|
if (typeof value === 'object' && 'id' in (value as object)) {
|
||||||
return { url: first.url ?? '', alt: first.alt ?? '' }
|
return (value as { id: number | string }).id ?? null
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageUploadSlot({ displayName }: Props) {
|
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 fileInput = useRef<HTMLInputElement | null>(null)
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
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()
|
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' })
|
const res = await fetch('/api/media', { method: 'POST', body: fd, credentials: 'include' })
|
||||||
if (!res.ok) throw new Error(`upload ${res.status}`)
|
if (!res.ok) throw new Error(`upload ${res.status}`)
|
||||||
const { doc } = await res.json()
|
const { doc } = await res.json()
|
||||||
const media: MediaDoc = { id: doc.id, url: doc.url, alt: doc.alt }
|
if (mediaId == null) {
|
||||||
const next = [{ image: media }, ...(images ?? []).slice(1)]
|
dispatchFields({
|
||||||
setValue(next)
|
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) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'upload failed')
|
setError(err instanceof Error ? err.message : 'upload failed')
|
||||||
} finally {
|
} finally {
|
||||||
@ -50,10 +89,13 @@ export function ImageUploadSlot({ displayName }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = resolved?.url ?? ''
|
||||||
|
const alt = resolved?.alt ?? displayName ?? 'Produit'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rebours-admin-panel__img-slot" onClick={onPick} role="button" tabIndex={0}>
|
<div className="rebours-admin-panel__img-slot" onClick={onPick} role="button" tabIndex={0}>
|
||||||
{url ? (
|
{url ? (
|
||||||
<img src={url} alt={alt || displayName || 'Produit'} />
|
<img src={url} alt={alt} />
|
||||||
) : (
|
) : (
|
||||||
<div className="rebours-admin-panel__img-placeholder">
|
<div className="rebours-admin-panel__img-placeholder">
|
||||||
Cliquez pour<br />uploader une image
|
Cliquez pour<br />uploader une image
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user