diff --git a/src/components/features/products/object-browser/DirectoryList.tsx b/src/components/features/products/object-browser/DirectoryList.tsx index e3ca388a..e98dca3c 100644 --- a/src/components/features/products/object-browser/DirectoryList.tsx +++ b/src/components/features/products/object-browser/DirectoryList.tsx @@ -43,8 +43,6 @@ export function DirectoryList({ // reappear); we just keep a successfully-deleted path hidden for the lifetime // of this directory view and reset when the prefix changes (navigating away). // We only ever add on a confirmed delete, so the object really is gone. - // ponytail: re-uploading the exact same path in this same view would stay - // hidden until you navigate or refresh — rare; not worth tracking re-creates. const [deletedPaths, setDeletedPaths] = useState>(new Set()); useEffect(() => { setDeletedPaths(new Set()); // new directory view → drop stale optimistic hides @@ -57,7 +55,21 @@ export function DirectoryList({ }; const scopedUploads = getUploadsForScope(scope); - // Merge file objects with upload progress, dropping optimistically-deleted ones + // Re-uploading to a previously-deleted path re-creates the object, so stop + // hiding it. deletedPaths is client state that survives a router.refresh(), + // so without this the re-upload stays hidden until you navigate away. + const uploadKeysSig = scopedUploads.map((u) => u.key).join("\n"); + useEffect(() => { + setDeletedPaths((prev) => { + const keys = uploadKeysSig.split("\n"); + if (!keys.some((k) => prev.has(k))) return prev; + const next = new Set(prev); + keys.forEach((k) => next.delete(k)); + return next; + }); + }, [uploadKeysSig]); + + // Merge file objects with upload progress const items = mergeUploadsWithFiles( asFileNodes(objects), scopedUploads, diff --git a/src/components/features/uploader/Dropzone.tsx b/src/components/features/uploader/Dropzone.tsx index af12626c..75b0ff76 100644 --- a/src/components/features/uploader/Dropzone.tsx +++ b/src/components/features/uploader/Dropzone.tsx @@ -3,6 +3,8 @@ import { Box, Text } from "@radix-ui/themes"; import { UploadIcon } from "@radix-ui/react-icons"; import { useDropzone } from "react-dropzone"; +import { useEffect, useRef } from "react"; +import { useRouter } from "next/navigation"; import type { Product } from "@/types"; import { useS3Credentials, @@ -20,18 +22,30 @@ export function Dropzone({ children, }: React.PropsWithChildren) { const { getCredentials } = useS3Credentials(); - const { uploadFiles } = useUploadManager(); - const uploadEnabled = getCredentials({ - productId: product.product_id, + const { uploadFiles, getUploadsForScope } = useUploadManager(); + const router = useRouter(); + const scope = { accountId: product.account_id, - }); + productId: product.product_id, + }; + const uploadEnabled = getCredentials(scope); + + // Re-fetch the server render when this product's uploads drain, so a re-upload + // to an existing path shows its new content/metadata instead of the stale + // version. Lives here (always mounted for the product route) rather than in + // the directory list, which isn't rendered while viewing a single file. + const hasActiveUploads = getUploadsForScope(scope).some( + (u) => u.status === "uploading" || u.status === "queued" + ); + const wasActive = useRef(false); + useEffect(() => { + if (wasActive.current && !hasActiveUploads) router.refresh(); + wasActive.current = hasActiveUploads; + }, [hasActiveUploads, router]); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop: (files) => { if (!uploadEnabled) return; - uploadFiles(files, prefix, { - accountId: product.account_id, - productId: product.product_id, - }); + uploadFiles(files, prefix, scope); }, noClick: true, // Don't open file browser on click // noKeyboard: true,