diff --git a/package.json b/package.json index c57f24a..b074f9c 100644 --- a/package.json +++ b/package.json @@ -16,4 +16,4 @@ "@types/bun": "latest", "convex": "^1.27.0" } -} \ No newline at end of file +} diff --git a/packages/convex/_generated/api.d.ts b/packages/convex/_generated/api.d.ts index 0b24462..5fd0788 100644 --- a/packages/convex/_generated/api.d.ts +++ b/packages/convex/_generated/api.d.ts @@ -14,6 +14,7 @@ import type { FunctionReference, } from "convex/server"; import type * as files from "../files.js"; +import type * as filesystem from "../filesystem.js"; import type * as functions from "../functions.js"; import type * as model_directories from "../model/directories.js"; import type * as model_error from "../model/error.js"; @@ -32,6 +33,7 @@ import type * as users from "../users.js"; */ declare const fullApi: ApiFromModules<{ files: typeof files; + filesystem: typeof filesystem; functions: typeof functions; "model/directories": typeof model_directories; "model/error": typeof model_error; diff --git a/packages/convex/files.ts b/packages/convex/files.ts index ad186a6..05afdf4 100644 --- a/packages/convex/files.ts +++ b/packages/convex/files.ts @@ -141,13 +141,3 @@ export const moveToTrash = authenticatedMutation({ return itemId }, }) - -export const moveFiles = authenticatedMutation({ - args: { - targetDirectoryId: v.id("directories"), - items: v.array(v.id("files")), - }, - handler: async (ctx, { targetDirectoryId, items }) => { - return await Files.moveFiles(ctx, { targetDirectoryId, items }) - }, -}) diff --git a/packages/convex/filesystem.ts b/packages/convex/filesystem.ts new file mode 100644 index 0000000..f2fcbc6 --- /dev/null +++ b/packages/convex/filesystem.ts @@ -0,0 +1,62 @@ +import { v } from "convex/values" +import { authenticatedMutation } from "./functions" +import * as Directories from "./model/directories" +import * as Err from "./model/error" +import * as Files from "./model/files" +import type { DirectoryHandle, FileHandle } from "./model/filesystem" + +const VDirectoryHandle = v.object({ + kind: v.literal("directory"), + id: v.id("directories"), +}) + +const VFileHandle = v.object({ + kind: v.literal("file"), + id: v.id("files"), +}) + +const VFileSystemHandle = v.union(VFileHandle, VDirectoryHandle) + +export const moveItems = authenticatedMutation({ + args: { + targetDirectory: VDirectoryHandle, + items: v.array(VFileSystemHandle), + }, + handler: async (ctx, { targetDirectory: targetDirectoryHandle, items }) => { + const targetDirectory = await Directories.fetchHandle( + ctx, + targetDirectoryHandle, + ) + if (!targetDirectory) { + throw Err.create( + Err.Code.DirectoryNotFound, + `Directory ${targetDirectoryHandle.id} not found`, + ) + } + + const directoryHandles: DirectoryHandle[] = [] + const fileHandles: FileHandle[] = [] + for (const item of items) { + switch (item.kind) { + case "directory": + directoryHandles.push(item) + break + case "file": + fileHandles.push(item) + break + } + } + await Promise.all([ + Files.move(ctx, { + targetDirectory: targetDirectoryHandle, + items: fileHandles, + }), + Directories.move(ctx, { + targetDirectory: targetDirectoryHandle, + sourceDirectories: directoryHandles, + }), + ]) + + return { items, targetDirectory } + }, +}) diff --git a/packages/convex/model/directories.ts b/packages/convex/model/directories.ts index e6f0adf..9ee7f39 100644 --- a/packages/convex/model/directories.ts +++ b/packages/convex/model/directories.ts @@ -4,7 +4,7 @@ import type { AuthenticatedQueryCtx, } from "../functions" import * as Err from "./error" -import type { FilePath, ReverseFilePath } from "./filesystem" +import type { DirectoryHandle, FilePath, ReverseFilePath } from "./filesystem" import { newDirectoryHandle } from "./filesystem" type Directory = { @@ -31,6 +31,20 @@ export async function fetchRoot(ctx: AuthenticatedQueryCtx) { .first() } +export async function fetchHandle( + ctx: AuthenticatedQueryCtx, + handle: DirectoryHandle, +): Promise> { + const directory = await ctx.db.get(handle.id) + if (!directory || directory.userId !== ctx.user._id) { + throw Err.create( + Err.Code.DirectoryNotFound, + `Directory ${handle.id} not found`, + ) + } + return directory +} + export async function fetch( ctx: AuthenticatedQueryCtx, { directoryId }: { directoryId: Id<"directories"> }, @@ -147,6 +161,23 @@ export async function create( }) } +export async function move( + ctx: AuthenticatedMutationCtx, + { + targetDirectory, + sourceDirectories, + }: { + targetDirectory: DirectoryHandle + sourceDirectories: DirectoryHandle[] + }, +): Promise { + await Promise.all( + sourceDirectories.map((directory) => + ctx.db.patch(directory.id, { parentId: targetDirectory.id }), + ), + ) +} + export async function moveToTrashRecursive( ctx: AuthenticatedMutationCtx, directoryId: Id<"directories">, diff --git a/packages/convex/model/files.ts b/packages/convex/model/files.ts index 1d9f6b0..9fecd3a 100644 --- a/packages/convex/model/files.ts +++ b/packages/convex/model/files.ts @@ -1,6 +1,7 @@ import type { Id } from "../_generated/dataModel" import type { AuthenticatedMutationCtx } from "../functions" import * as Err from "./error" +import type { DirectoryHandle, FileHandle } from "./filesystem" export async function renameFile( ctx: AuthenticatedMutationCtx, @@ -35,27 +36,19 @@ export async function renameFile( await ctx.db.patch(itemId, { name: newName }) } -export async function moveFiles( +export async function move( ctx: AuthenticatedMutationCtx, { - targetDirectoryId, + targetDirectory: targetDirectoryHandle, items, }: { - targetDirectoryId: Id<"directories"> - items: Id<"files">[] + targetDirectory: DirectoryHandle + items: FileHandle[] }, ) { - const targetDirectory = await ctx.db.get(targetDirectoryId) - if (!targetDirectory) { - throw Err.create( - Err.Code.DirectoryNotFound, - "Target directory not found", - ) - } await Promise.all( - items.map((itemId) => - ctx.db.patch(itemId, { directoryId: targetDirectoryId }), + items.map((item) => + ctx.db.patch(item.id, { directoryId: targetDirectoryHandle.id }), ), ) - return { items, targetDirectory } } diff --git a/packages/web/package.json b/packages/web/package.json index 6e94595..f0e8be2 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "bun --hot src/server.tsx", "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'", - "start": "NODE_ENV=production bun src/index.tsx" + "start": "NODE_ENV=production bun src/index.tsx", + "format": "biome format --write" }, "dependencies": { "@convex-dev/workos": "^0.0.1", 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 247537d..9eb944f 100644 --- a/packages/web/src/directories/directory-page/directory-content-table.tsx +++ b/packages/web/src/directories/directory-page/directory-content-table.tsx @@ -2,6 +2,7 @@ import { api } from "@fileone/convex/_generated/api" import type { Doc } from "@fileone/convex/_generated/dataModel" import type { DirectoryItem } from "@fileone/convex/model/directories" import { + type FileSystemHandle, newDirectoryHandle, newFileHandle, } from "@fileone/convex/model/filesystem" @@ -12,6 +13,7 @@ import { flexRender, getCoreRowModel, type Row, + type Table as TableType, useReactTable, } from "@tanstack/react-table" import { useMutation as useContextMutation } from "convex/react" @@ -302,6 +304,7 @@ export function DirectoryContentTableContent() { table.getRowModel().rows.map((row) => ( selectRow(row)} onContextMenu={(e) => @@ -439,11 +442,13 @@ function NewItemRow() { } function FileItemRow({ + table, row, onClick, onContextMenu, onDoubleClick, }: { + table: TableType row: Row onClick: () => void onContextMenu: (e: React.MouseEvent) => void @@ -461,17 +466,30 @@ function FileItemRow({ }) const handleDragStart = (e: React.DragEvent) => { - if (row.original.kind === "file") { - e.dataTransfer.setData( - "application/x-internal", - JSON.stringify(row.original), - ) - const fileHandle = newFileHandle(row.original.doc._id) - setDragInfo({ - source: fileHandle, - items: [fileHandle], - }) + let source: FileSystemHandle + switch (row.original.kind) { + case "file": + source = newFileHandle(row.original.doc._id) + break + case "directory": + source = newDirectoryHandle(row.original.doc._id) + break } + + // biome-ignore lint/suspicious/useIterableCallbackReturn: the switch statement is exhaustive + const draggedItems = table.getSelectedRowModel().rows.map((row) => { + switch (row.original.kind) { + case "file": + return newFileHandle(row.original.doc._id) + case "directory": + return newDirectoryHandle(row.original.doc._id) + } + }) + + setDragInfo({ + source, + items: draggedItems, + }) } const handleDragEnd = () => { diff --git a/packages/web/src/files/use-file-drop.ts b/packages/web/src/files/use-file-drop.ts index e297b54..b1dff84 100644 --- a/packages/web/src/files/use-file-drop.ts +++ b/packages/web/src/files/use-file-drop.ts @@ -2,7 +2,6 @@ import { api } from "@fileone/convex/_generated/api" import type { Doc, Id } from "@fileone/convex/_generated/dataModel" import type { DirectoryHandle, - FileHandle, FileSystemHandle, } from "@fileone/convex/model/filesystem" import { useMutation } from "@tanstack/react-query" @@ -14,7 +13,7 @@ import { toast } from "sonner" export interface FileDragInfo { source: FileSystemHandle - items: FileHandle[] + items: FileSystemHandle[] } export interface UseFileDropOptions { @@ -38,33 +37,31 @@ export interface UseFileDropReturn { export function useFileDrop({ item, dragInfoAtom, - onDropSuccess, }: UseFileDropOptions): UseFileDropReturn { const [isDraggedOver, setIsDraggedOver] = useState(false) const store = useStore() - const { mutate: moveFiles } = useMutation({ - mutationFn: useContextMutation(api.files.moveFiles), + const { mutate: moveDroppedItems } = useMutation({ + mutationFn: useContextMutation(api.filesystem.moveItems), onSuccess: ({ items, targetDirectory, }: { - items: Id<"files">[] + items: FileSystemHandle[] targetDirectory: Doc<"directories"> }) => { toast.success( - `${items.length} files moved to ${targetDirectory.name}`, + `${items.length} items moved to ${targetDirectory.name}`, ) - onDropSuccess?.(items, targetDirectory) }, }) const handleDrop = (_e: React.DragEvent) => { const dragInfo = store.get(dragInfoAtom) if (dragInfo && item) { - moveFiles({ - targetDirectoryId: item.id, - items: dragInfo.items.map((item) => item.id), + moveDroppedItems({ + targetDirectory: item, + items: dragInfo.items, }) } setIsDraggedOver(false) diff --git a/platform.ts b/platform.ts index 8e105ab..feff35a 100644 --- a/platform.ts +++ b/platform.ts @@ -1,9 +1,9 @@ export enum Platform { - Windows = 'windows', - MacOS = 'macos', - Linux = 'linux', - Android = 'android', - iOS = 'ios' + Windows = "windows", + MacOS = "macos", + Linux = "linux", + Android = "android", + iOS = "ios", } // Internal global variables (computed once at module load)