diff --git a/apps/web/src/components/sections-editor/fields/enum-field.tsx b/apps/web/src/components/sections-editor/fields/enum-field.tsx index cd097eda59..65931c0ccd 100644 --- a/apps/web/src/components/sections-editor/fields/enum-field.tsx +++ b/apps/web/src/components/sections-editor/fields/enum-field.tsx @@ -40,7 +40,9 @@ export function EnumField({ virtualMcpId={sandbox?.virtualMcpId} /> { + void handleFiles(e.target.files); + e.target.value = ""; + }} + /> + ) : ( + onChange(url)} + lockedConfigId={lockedConfig?.id ?? null} + /> + )} ); } diff --git a/apps/web/src/components/sections-editor/fields/image-field.tsx b/apps/web/src/components/sections-editor/fields/image-field.tsx index 12a9ace1fc..a95eefbfa1 100644 --- a/apps/web/src/components/sections-editor/fields/image-field.tsx +++ b/apps/web/src/components/sections-editor/fields/image-field.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import { Image01, Trash01, Upload01 } from "@untitledui/icons"; import { Button } from "@decocms/ui/components/button.tsx"; import { Input } from "@decocms/ui/components/input.tsx"; @@ -12,7 +12,14 @@ import type { FieldProps } from "./field-props"; import { basename, extension } from "./media-filename"; import { MediaTransformControls } from "./media-transform-controls"; import { isSafeImageUrl, safeImageSrc } from "./safe-image-url"; -import { useImageUpload } from "./use-image-upload"; +import { ACCEPTED_IMAGE_TYPES, useImageUpload } from "./use-image-upload"; +import { + useServeAssetSrc, + useServeAssetUpload, +} from "./use-serve-asset-upload"; + +/** How many times a thumbnail served by the site's dev app is retried. */ +const SERVED_SRC_RETRIES = 3; export function ImageField({ schema, @@ -30,7 +37,18 @@ export function ImageField({ const [imageErrored, setImageErrored] = useState(false); // An author types this field, so the value is untrusted until it has been // through `safeImageSrc`; an unsafe scheme reduces to "". - const src = safeImageSrc(strValue); + // On a connected `deco serve` (v8), `/assets/*` loads from the site's dev + // app; everywhere else this is `strValue` unchanged. + const servedSrc = useServeAssetSrc(sandbox, strValue); + // A file just uploaded to `deco serve` can 404 for a moment, until the dev + // app's file watcher sees it: a dev-app thumbnail is retried a few times + // before it shows as unavailable. + const [loadRetry, setLoadRetry] = useState(0); + const src = safeImageSrc( + loadRetry > 0 + ? `${servedSrc}${servedSrc.includes("?") ? "&" : "?"}retry=${loadRetry}` + : servedSrc, + ); const unsafe = !!strValue && !src; const fileName = strValue ? basename(strValue) : ""; const ext = fileName ? extension(fileName) : ""; @@ -46,6 +64,7 @@ export function ImageField({ function setValue(next: string) { setImageLoaded(false); setImageErrored(false); + setLoadRetry(0); onChange(next); } @@ -87,11 +106,19 @@ export function ImageField({ if (urlDraft.trim() && !isSafeImageUrl(urlDraft)) setUrlDraft(strValue); } - const { isDragging, isPending, lockedConfigId, dropProps } = useImageUpload({ - siteSlug: sandbox?.siteSlug, - onUploaded: setValue, - onNeedsPicker: () => setPickerOpen(true), - }); + const serveUpload = useServeAssetUpload(sandbox); + const fileInputRef = useRef(null); + /** Choose a file: the system file dialog for `deco serve`, else the picker. */ + const browse = () => + serveUpload ? fileInputRef.current?.click() : setPickerOpen(true); + + const { isDragging, isPending, lockedConfigId, dropProps, handleFiles } = + useImageUpload({ + siteSlug: sandbox?.siteSlug, + onUploaded: setValue, + onNeedsPicker: () => setPickerOpen(true), + serveUpload, + }); return ( // grid-cols-[minmax(0,1fr)] forces every child to be at most 100% of @@ -124,7 +151,7 @@ export function ImageField({ <>