Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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