Skip to content
Open
10 changes: 7 additions & 3 deletions app/components/FileUploadHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { useRef, useEffect, type InputHTMLAttributes } from "react";
import { toast } from "@/components/ui/toast";
import {
getAuthenticatedSession,
uploadFilesToContainer,
uploadFolderFilesToContainer,
FolderUploadFile,
} from "../lib/helpers";
import { useUploadConflictPrompt } from "./useUploadConflictPrompt";

type FileWithRelativePath = File & {
webkitRelativePath?: string;
Expand All @@ -28,6 +28,8 @@ export default function FileUploadHandler({
const fileInputRef = useRef<HTMLInputElement>(null);
const folderInputRef = useRef<HTMLInputElement>(null);

const { uploadFilesWithConflictPrompt, conflictDialog } = useUploadConflictPrompt();

useEffect(() => {
if (triggerUpload && triggerUpload > 0 && fileInputRef.current) {
fileInputRef.current.click();
Expand Down Expand Up @@ -58,11 +60,12 @@ export default function FileUploadHandler({
e.target.value = "";
return;
}

try {
const { uploadedFiles, failedFiles } = await uploadFilesToContainer(
const { uploadedFiles, failedFiles } = await uploadFilesWithConflictPrompt(
Array.from(files),
currentContainerUrl,
fetchFn
fetchFn,
);

if (uploadedFiles.length > 0) {
Expand Down Expand Up @@ -171,6 +174,7 @@ export default function FileUploadHandler({
className="hidden"
onChange={handleFolderChange}
/>
{conflictDialog}
</>
);
}
Expand Down
60 changes: 60 additions & 0 deletions app/components/UploadConflictDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogMedia,
} from "@/components/ui/alert-dialog";
import { FileWarning } from "lucide-react";

interface UploadConflictDialogProps {
isOpen: boolean;
fileName: string | null;
onReplace: () => void;
onKeepBoth: () => void;
onCancel: () => void;
}

export default function UploadConflictDialog({
isOpen,
fileName,
onReplace,
onKeepBoth,
onCancel,
}: UploadConflictDialogProps) {
return (
<AlertDialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onCancel();
}}
>
<AlertDialogContent size="default">
<AlertDialogHeader>
<AlertDialogMedia className="bg-primary/10 text-primary">
<FileWarning />
</AlertDialogMedia>
<AlertDialogTitle>File already exists</AlertDialogTitle>
<AlertDialogDescription>
A file named{" "}
<span className="font-medium text-foreground">&quot;{fileName}&quot;</span>{" "}
already exists in this folder. Do you want to replace it or keep both?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction variant="outline" onClick={onKeepBoth}>
Keep both
</AlertDialogAction>
<AlertDialogAction onClick={onReplace}>Replace</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
19 changes: 16 additions & 3 deletions app/components/file-manager/FileManagerContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@ import LoadingSpinner from "../shared/LoadingSpinner";
import ErrorDisplay from "../shared/ErrorDisplay";
import {
getAuthenticatedSession,
uploadFilesToContainer,
uploadFolderFilesToContainer,
processDragDropItems,
hasFiles as hasFilesInDrag,
isUnsupportedFolderDrag,
} from "@/app/lib/helpers";
import { isDialog } from "./types/fileActions";
import { useFileManagerNavigation, useFileManagerBrowse, useFileManagerActions, useFileManagerSelection, useFileManagerDialogs } from "./context/fileManagerContext";
import {
useFileManagerNavigation,
useFileManagerBrowse,
useFileManagerActions,
useFileManagerSelection,
useFileManagerDialogs
} from "./context/fileManagerContext";
import { useUploadConflictPrompt } from "../useUploadConflictPrompt";

type ContextMenuState =
| { type: "new"; position: { x: number; y: number } }
Expand Down Expand Up @@ -100,6 +106,7 @@ export default function FileManagerContent() {
const [contextMenuState, setContextMenuState] = useState<ContextMenuState | null>(null);

const closeContextMenu = () => setContextMenuState(null);
const { uploadFilesWithConflictPrompt, conflictDialog } = useUploadConflictPrompt();

/** Require a selected container before create/upload actions. */
const ensureStorageSelected = () => {
Expand Down Expand Up @@ -210,7 +217,12 @@ export default function FileManagerContent() {

if (singleFiles.length > 0) {
try {
const { uploadedFiles, failedFiles } = await uploadFilesToContainer(singleFiles, containerUrlToBrowse, fetchFn);
const { uploadedFiles, failedFiles } = await uploadFilesWithConflictPrompt(
singleFiles,
containerUrlToBrowse,
fetchFn,
)

if (uploadedFiles.length > 0) {
uploadedSomething = true;
toast.add({
Expand Down Expand Up @@ -549,6 +561,7 @@ export default function FileManagerContent() {
triggerUpload={fileUploadTrigger}
triggerFolderUpload={folderUploadTrigger}
/>
{conflictDialog}
{contextMenuState && (
<ContextMenu
position={contextMenuState.position}
Expand Down
97 changes: 97 additions & 0 deletions app/components/useUploadConflictPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use client";

import { useCallback, useRef, useState } from "react";
import UploadConflictDialog from "./UploadConflictDialog";
import {
findUploadConflicts,
uploadFilesToContainer,
uploadFileWithConflictChoice,
type UploadConflict,
type UploadConflictChoice,
type UploadResult,
} from "../lib/helpers";

export function useUploadConflictPrompt() {
const [activeConflict, setActiveConflict] = useState<UploadConflict | null>(null);
const conflictResolverRef = useRef<((choice: UploadConflictChoice) => void) | null>(null);

const resolveConflictChoice = useCallback((choice: UploadConflictChoice) => {
const resolve = conflictResolverRef.current;
conflictResolverRef.current = null;
setActiveConflict(null);
resolve?.(choice);
}, []);

const askConflictChoice = useCallback(
(conflict: UploadConflict) =>
new Promise<UploadConflictChoice>((resolve) => {
conflictResolverRef.current = resolve;
setActiveConflict(conflict);
}),
[],
);

const uploadFilesWithConflictPrompt = useCallback(
async (
files: File[],
currentContainerUrl: string,
fetchFn: typeof fetch,
): Promise<UploadResult> => {
const { newFiles, conflicts } = await findUploadConflicts(
files,
currentContainerUrl,
fetchFn,
);

const uploadedFiles: string[] = [];
const failedFiles: string[] = [];

if (newFiles.length > 0) {
const result = await uploadFilesToContainer(
newFiles,
currentContainerUrl,
fetchFn,
);
uploadedFiles.push(...result.uploadedFiles);
failedFiles.push(...result.failedFiles);
}

for (const conflict of conflicts) {
const choice = await askConflictChoice(conflict);
if (choice === "cancel") {
continue;
}

try {
const { uploadedName } = await uploadFileWithConflictChoice(
conflict,
choice,
currentContainerUrl,
fetchFn,
);
uploadedFiles.push(uploadedName);
} catch {
failedFiles.push(conflict.existingName);
}
}

return { uploadedFiles, failedFiles };
},
[askConflictChoice],
);

const conflictDialog = (
<UploadConflictDialog
isOpen={activeConflict !== null}
fileName={activeConflict?.existingName ?? null}
onReplace={() => resolveConflictChoice("replace")}
onKeepBoth={() => resolveConflictChoice("keepBoth")}
onCancel={() => resolveConflictChoice("cancel")}
/>
);

return {
uploadFilesWithConflictPrompt,
conflictDialog,
};
}
2 changes: 1 addition & 1 deletion app/lib/helpers/copyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const shouldSkipResourceCopy = (resourceUrl: string): boolean => {
};


const resourceExists = async (url: string, fetchFn: typeof fetch): Promise<boolean> => {
export const resourceExists = async (url: string, fetchFn: typeof fetch): Promise<boolean> => {
try {
const response = await fetchFn(url, { method: "HEAD" });
if (response.status === 404) {
Expand Down
Loading
Loading