From 8e4e2fcac1eae36e80982b641a4b891650975353 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Mar 2026 21:26:29 +0100 Subject: [PATCH] feat: enhance asset browser panel with improved asset selection and loading states - Added state management for asset selection to prevent multiple simultaneous selections. - Implemented request sequence tracking to ensure accurate loading state handling during asset searches. - Enhanced error handling and user feedback for asset loading failures. - Updated UI elements to improve accessibility and user experience during asset browsing. --- components/canvas/asset-browser-panel.tsx | 173 ++++++++++++------ components/canvas/canvas.tsx | 3 - components/canvas/nodes/asset-node.tsx | 75 ++++---- components/canvas/nodes/base-node-wrapper.tsx | 116 +++++++++++- components/canvas/nodes/compare-node.tsx | 7 +- components/canvas/nodes/frame-node.tsx | 4 +- components/canvas/nodes/image-node.tsx | 47 +---- components/canvas/nodes/note-node.tsx | 2 +- components/canvas/nodes/text-node.tsx | 6 +- 9 files changed, 278 insertions(+), 155 deletions(-) diff --git a/components/canvas/asset-browser-panel.tsx b/components/canvas/asset-browser-panel.tsx index 5770edc..578749a 100644 --- a/components/canvas/asset-browser-panel.tsx +++ b/components/canvas/asset-browser-panel.tsx @@ -59,11 +59,15 @@ export function AssetBrowserPanel({ const [totalPages, setTotalPages] = useState(initialState?.totalPages ?? 1); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); + const [selectingAssetKey, setSelectingAssetKey] = useState(null); const searchFreepik = useAction(api.freepik.search); const updateData = useMutation(api.nodes.updateData); const resizeNode = useMutation(api.nodes.resize); const shouldSkipInitialSearchRef = useRef(Boolean(initialState?.results?.length)); + const requestSequenceRef = useRef(0); + const scrollAreaRef = useRef(null); + const isSelecting = selectingAssetKey !== null; useEffect(() => { setIsMounted(true); @@ -102,6 +106,7 @@ export function AssetBrowserPanel({ const runSearch = useCallback( async (searchTerm: string, type: AssetType, requestedPage: number) => { const cleanedTerm = searchTerm.trim(); + const requestSequence = ++requestSequenceRef.current; if (!cleanedTerm) { setResults([]); setErrorMessage(null); @@ -121,16 +126,29 @@ export function AssetBrowserPanel({ limit: 20, }); + if (requestSequence !== requestSequenceRef.current) { + return; + } + setResults(response.results); setTotalPages(response.totalPages); setPage(response.currentPage); + + if (scrollAreaRef.current) { + scrollAreaRef.current.scrollTop = 0; + } } catch (error) { + if (requestSequence !== requestSequenceRef.current) { + return; + } console.error("Freepik search error", error); setErrorMessage( error instanceof Error ? error.message : "Freepik search failed", ); } finally { - setIsLoading(false); + if (requestSequence === requestSequenceRef.current) { + setIsLoading(false); + } } }, [searchFreepik], @@ -147,38 +165,47 @@ export function AssetBrowserPanel({ const handleSelect = useCallback( async (asset: FreepikResult) => { - await updateData({ - nodeId: nodeId as Id<"nodes">, - data: { - assetId: asset.id, - assetType: asset.assetType, - title: asset.title, - previewUrl: asset.previewUrl, + if (isSelecting) return; + const assetKey = `${asset.assetType}-${asset.id}`; + setSelectingAssetKey(assetKey); + try { + await updateData({ + nodeId: nodeId as Id<"nodes">, + data: { + assetId: asset.id, + assetType: asset.assetType, + title: asset.title, + previewUrl: asset.previewUrl, + intrinsicWidth: asset.intrinsicWidth, + intrinsicHeight: asset.intrinsicHeight, + url: asset.previewUrl, + sourceUrl: asset.sourceUrl, + license: asset.license, + authorName: asset.authorName, + orientation: asset.orientation, + canvasId, + }, + }); + + const targetSize = computeMediaNodeSize("asset", { intrinsicWidth: asset.intrinsicWidth, intrinsicHeight: asset.intrinsicHeight, - url: asset.previewUrl, - sourceUrl: asset.sourceUrl, - license: asset.license, - authorName: asset.authorName, orientation: asset.orientation, - canvasId, - }, - }); + }); - const targetSize = computeMediaNodeSize("asset", { - intrinsicWidth: asset.intrinsicWidth, - intrinsicHeight: asset.intrinsicHeight, - orientation: asset.orientation, - }); - - await resizeNode({ - nodeId: nodeId as Id<"nodes">, - width: targetSize.width, - height: targetSize.height, - }); - onClose(); + await resizeNode({ + nodeId: nodeId as Id<"nodes">, + width: targetSize.width, + height: targetSize.height, + }); + onClose(); + } catch (error) { + console.error("Failed to select asset", error); + } finally { + setSelectingAssetKey(null); + } }, - [canvasId, nodeId, onClose, resizeNode, updateData], + [canvasId, isSelecting, nodeId, onClose, resizeNode, updateData], ); const handlePreviousPage = useCallback(() => { @@ -204,6 +231,9 @@ export function AssetBrowserPanel({ onClick={(event) => event.stopPropagation()} onWheelCapture={(event) => event.stopPropagation()} onPointerDownCapture={(event) => event.stopPropagation()} + role="dialog" + aria-modal="true" + aria-label="Browse Freepik assets" >

Browse Freepik Assets

@@ -244,6 +274,7 @@ export function AssetBrowserPanel({
event.stopPropagation()} > @@ -258,6 +289,14 @@ export function AssetBrowserPanel({

Search failed

{errorMessage}

+
) : results.length === 0 ? (
@@ -269,35 +308,49 @@ export function AssetBrowserPanel({ ) : ( <>
- {results.map((asset) => ( - - ))} + {results.map((asset) => { + const assetKey = `${asset.assetType}-${asset.id}`; + const isSelectingThisAsset = selectingAssetKey === assetKey; + + return ( + + ); + })}
@@ -306,7 +359,7 @@ export function AssetBrowserPanel({
{results.length > 0 ? ( -
+