diff --git a/packages/convex/filesystem.ts b/packages/convex/filesystem.ts index f2fcbc6..9d6d297 100644 --- a/packages/convex/filesystem.ts +++ b/packages/convex/filesystem.ts @@ -46,7 +46,8 @@ export const moveItems = authenticatedMutation({ break } } - await Promise.all([ + + const [fileMoveResult, directoryMoveResult] = await Promise.all([ Files.move(ctx, { targetDirectory: targetDirectoryHandle, items: fileHandles, @@ -57,6 +58,9 @@ export const moveItems = authenticatedMutation({ }), ]) - return { items, targetDirectory } + return { + moved: [...directoryMoveResult.moved, ...fileMoveResult.moved], + errors: [...fileMoveResult.errors, ...directoryMoveResult.errors], + } }, }) diff --git a/packages/convex/model/directories.ts b/packages/convex/model/directories.ts index a007989..b89b6b1 100644 --- a/packages/convex/model/directories.ts +++ b/packages/convex/model/directories.ts @@ -170,7 +170,7 @@ export async function move( targetDirectory: DirectoryHandle sourceDirectories: DirectoryHandle[] }, -): Promise { +) { const conflictCheckResults = await Promise.allSettled( sourceDirectories.map((directory) => ctx.db.get(directory.id).then((d) => { @@ -194,25 +194,40 @@ export async function move( ), ) - const errors: Err.ApplicationError[] = [] - for (const result of conflictCheckResults) { - if (result.status === "fulfilled" && result.value) { - errors.push( - Err.create( - Err.Code.Conflict, - `Directory ${targetDirectory.id} already contains a directory with name ${result.value.name}`, - ), - ) + console.log(sourceDirectories) + + const errors: Err.ApplicationErrorData[] = [] + const okDirectories: DirectoryHandle[] = [] + conflictCheckResults.forEach((result, i) => { + if (result.status === "fulfilled") { + if (result.value) { + errors.push( + Err.createJson( + Err.Code.Conflict, + `Directory ${targetDirectory.id} already contains a directory with name ${result.value.name}`, + ), + ) + } else { + okDirectories.push(sourceDirectories[i]) + } } else if (result.status === "rejected") { - errors.push(Err.create(Err.Code.Internal)) + errors.push(Err.createJson(Err.Code.Internal)) + } + }) + + const results = await Promise.allSettled( + okDirectories.map((handle) => + ctx.db.patch(handle.id, { parentId: targetDirectory.id }), + ), + ) + + for (const updateResult of results) { + if (updateResult.status === "rejected") { + errors.push(Err.createJson(Err.Code.Internal)) } } - await Promise.all( - sourceDirectories.map((directory) => - ctx.db.patch(directory.id, { parentId: targetDirectory.id }), - ), - ) + return { moved: okDirectories, errors } } export async function moveToTrashRecursive( diff --git a/packages/convex/model/error.ts b/packages/convex/model/error.ts index 6347bc8..5555354 100644 --- a/packages/convex/model/error.ts +++ b/packages/convex/model/error.ts @@ -1,15 +1,17 @@ import { ConvexError } from "convex/values" -export enum Code { + export enum Code { Conflict = "Conflict", DirectoryExists = "DirectoryExists", DirectoryNotFound = "DirectoryNotFound", FileExists = "FileExists", + FileNotFound = "FileNotFound", Internal = "Internal", Unauthenticated = "Unauthenticated", } -export type ApplicationError = ConvexError<{ code: Code; message?: string }> +export type ApplicationErrorData = { code: Code; message?: string } +export type ApplicationError = ConvexError export function isApplicationError(error: unknown): error is ApplicationError { return error instanceof ConvexError && "code" in error.data @@ -22,3 +24,11 @@ export function create(code: Code, message?: string): ApplicationError { code === Code.Internal ? "Internal application error" : message, }) } + +export function createJson(code: Code, message?: string): ApplicationErrorData { + return { + code, + message: + code === Code.Internal ? "Internal application error" : message, + } +} diff --git a/packages/convex/model/files.ts b/packages/convex/model/files.ts index 9fecd3a..85ec5b0 100644 --- a/packages/convex/model/files.ts +++ b/packages/convex/model/files.ts @@ -46,9 +46,59 @@ export async function move( items: FileHandle[] }, ) { - await Promise.all( - items.map((item) => - ctx.db.patch(item.id, { directoryId: targetDirectoryHandle.id }), + const conflictCheckResults = await Promise.allSettled( + items.map((fileHandle) => + ctx.db.get(fileHandle.id).then((f) => { + if (!f) { + throw Err.create( + Err.Code.FileNotFound, + `File ${fileHandle.id} not found`, + ) + } + return ctx.db + .query("files") + .withIndex("uniqueFileInDirectory", (q) => + q + .eq("userId", ctx.user._id) + .eq("directoryId", targetDirectoryHandle.id) + .eq("name", f.name) + .eq("deletedAt", undefined), + ) + .first() + }), ), ) + + const errors: Err.ApplicationErrorData[] = [] + const okFiles: FileHandle[] = [] + conflictCheckResults.forEach((result, i) => { + if (result.status === "fulfilled") { + if (result.value) { + errors.push( + Err.createJson( + Err.Code.Conflict, + `Directory ${targetDirectoryHandle.id} already contains a file with name ${result.value.name}`, + ), + ) + } else { + okFiles.push(items[i]) + } + } else if (result.status === "rejected") { + errors.push(Err.createJson(Err.Code.Internal)) + } + }) + + const results = await Promise.allSettled( + okFiles.map((handle) => + ctx.db.patch(handle.id, { directoryId: targetDirectoryHandle.id }), + ), + ) + + for (const updateResult of results) { + if (updateResult.status === "rejected") { + errors.push(Err.createJson(Err.Code.Internal)) + } + } + + return { moved: okFiles, errors } } diff --git a/packages/web/src/directories/directory-page/directory-content-table.tsx b/packages/web/src/directories/directory-page/directory-content-table.tsx index 9eb944f..a54b40b 100644 --- a/packages/web/src/directories/directory-page/directory-content-table.tsx +++ b/packages/web/src/directories/directory-page/directory-content-table.tsx @@ -485,6 +485,7 @@ function FileItemRow({ return newDirectoryHandle(row.original.doc._id) } }) + draggedItems.push(source) setDragInfo({ source, diff --git a/packages/web/src/files/use-file-drop.ts b/packages/web/src/files/use-file-drop.ts index b1dff84..4353ca2 100644 --- a/packages/web/src/files/use-file-drop.ts +++ b/packages/web/src/files/use-file-drop.ts @@ -1,5 +1,6 @@ import { api } from "@fileone/convex/_generated/api" import type { Doc, Id } from "@fileone/convex/_generated/dataModel" +import * as Err from "@fileone/convex/model/error" import type { DirectoryHandle, FileSystemHandle, @@ -44,20 +45,31 @@ export function useFileDrop({ const { mutate: moveDroppedItems } = useMutation({ mutationFn: useContextMutation(api.filesystem.moveItems), onSuccess: ({ - items, - targetDirectory, + moved, + errors, }: { - items: FileSystemHandle[] - targetDirectory: Doc<"directories"> + moved: FileSystemHandle[] + errors: Err.ApplicationErrorData[] }) => { - toast.success( - `${items.length} items moved to ${targetDirectory.name}`, - ) + const conflictCount = errors.reduce((acc, error) => { + if (error.code === Err.Code.Conflict) { + return acc + 1 + } + return acc + }, 0) + if (conflictCount > 0) { + toast.warning( + `${moved.length} items moved${conflictCount > 0 ? `, ${conflictCount} conflicts` : ""}`, + ) + } else { + toast.success(`${moved.length} items moved!`) + } }, }) const handleDrop = (_e: React.DragEvent) => { const dragInfo = store.get(dragInfoAtom) + console.log("dragInfo", dragInfo) if (dragInfo && item) { moveDroppedItems({ targetDirectory: item, diff --git a/packages/web/src/lib/error.ts b/packages/web/src/lib/error.ts index ff576aa..d60390c 100644 --- a/packages/web/src/lib/error.ts +++ b/packages/web/src/lib/error.ts @@ -8,6 +8,10 @@ const ERROR_MESSAGE = { [ErrorCode.DirectoryExists]: "Directory already exists", [ErrorCode.FileExists]: "File already exists", [ErrorCode.Internal]: "Internal application error", + [ErrorCode.Conflict]: "Conflict", + [ErrorCode.DirectoryNotFound]: "Directory not found", + [ErrorCode.FileNotFound]: "File not found", + [ErrorCode.Unauthenticated]: "Unauthenticated", } as const export function formatError(error: unknown): string {