mirror of
https://github.com/get-drexa/drive.git
synced 2025-12-01 05:51:39 +00:00
feat: impl multi file deletion support
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
import type { Id } from "@fileone/convex/_generated/dataModel"
|
|
||||||
import { v } from "convex/values"
|
import { v } from "convex/values"
|
||||||
|
import type { Id } from "./_generated/dataModel"
|
||||||
import { authenticatedMutation, authenticatedQuery } from "./functions"
|
import { authenticatedMutation, authenticatedQuery } from "./functions"
|
||||||
import type { DirectoryItem } from "./model/directories"
|
|
||||||
import * as Directories from "./model/directories"
|
import * as Directories from "./model/directories"
|
||||||
import * as Err from "./model/error"
|
|
||||||
import * as Files from "./model/files"
|
import * as Files from "./model/files"
|
||||||
|
import type { FileSystemItem } from "./model/filesystem"
|
||||||
|
|
||||||
export const generateUploadUrl = authenticatedMutation({
|
export const generateUploadUrl = authenticatedMutation({
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
@@ -55,7 +54,7 @@ export const fetchDirectoryContent = authenticatedQuery({
|
|||||||
args: {
|
args: {
|
||||||
directoryId: v.optional(v.id("directories")),
|
directoryId: v.optional(v.id("directories")),
|
||||||
},
|
},
|
||||||
handler: async (ctx, { directoryId }): Promise<DirectoryItem[]> => {
|
handler: async (ctx, { directoryId }): Promise<FileSystemItem[]> => {
|
||||||
return await Directories.fetchContent(ctx, { directoryId })
|
return await Directories.fetchContent(ctx, { directoryId })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -107,37 +106,3 @@ export const renameFile = authenticatedMutation({
|
|||||||
await Files.renameFile(ctx, { directoryId, itemId, newName })
|
await Files.renameFile(ctx, { directoryId, itemId, newName })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const moveToTrash = authenticatedMutation({
|
|
||||||
args: {
|
|
||||||
kind: v.union(v.literal("file"), v.literal("directory")),
|
|
||||||
itemId: v.union(v.id("files"), v.id("directories")),
|
|
||||||
},
|
|
||||||
handler: async (ctx, { itemId, kind }) => {
|
|
||||||
switch (kind) {
|
|
||||||
case "file": {
|
|
||||||
const file = await ctx.db.get(itemId as Id<"files">)
|
|
||||||
if (!file || file.userId !== ctx.user._id) {
|
|
||||||
throw new Error("File not found or access denied")
|
|
||||||
}
|
|
||||||
await ctx.db.patch(itemId, {
|
|
||||||
deletedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "directory": {
|
|
||||||
const directory = await ctx.db.get(itemId as Id<"directories">)
|
|
||||||
if (!directory || directory.userId !== ctx.user._id) {
|
|
||||||
throw new Error("Directory not found or access denied")
|
|
||||||
}
|
|
||||||
await Directories.moveToTrashRecursive(
|
|
||||||
ctx,
|
|
||||||
itemId as Id<"directories">,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return itemId
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -4,18 +4,12 @@ import * as Directories from "./model/directories"
|
|||||||
import * as Err from "./model/error"
|
import * as Err from "./model/error"
|
||||||
import * as Files from "./model/files"
|
import * as Files from "./model/files"
|
||||||
import type { DirectoryHandle, FileHandle } from "./model/filesystem"
|
import type { DirectoryHandle, FileHandle } from "./model/filesystem"
|
||||||
|
import {
|
||||||
const VDirectoryHandle = v.object({
|
type FileSystemHandle,
|
||||||
kind: v.literal("directory"),
|
FileType,
|
||||||
id: v.id("directories"),
|
VDirectoryHandle,
|
||||||
})
|
VFileSystemHandle,
|
||||||
|
} from "./model/filesystem"
|
||||||
const VFileHandle = v.object({
|
|
||||||
kind: v.literal("file"),
|
|
||||||
id: v.id("files"),
|
|
||||||
})
|
|
||||||
|
|
||||||
const VFileSystemHandle = v.union(VFileHandle, VDirectoryHandle)
|
|
||||||
|
|
||||||
export const moveItems = authenticatedMutation({
|
export const moveItems = authenticatedMutation({
|
||||||
args: {
|
args: {
|
||||||
@@ -38,10 +32,10 @@ export const moveItems = authenticatedMutation({
|
|||||||
const fileHandles: FileHandle[] = []
|
const fileHandles: FileHandle[] = []
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
switch (item.kind) {
|
switch (item.kind) {
|
||||||
case "directory":
|
case FileType.Directory:
|
||||||
directoryHandles.push(item)
|
directoryHandles.push(item)
|
||||||
break
|
break
|
||||||
case "file":
|
case FileType.File:
|
||||||
fileHandles.push(item)
|
fileHandles.push(item)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -64,3 +58,45 @@ export const moveItems = authenticatedMutation({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const moveToTrash = authenticatedMutation({
|
||||||
|
args: {
|
||||||
|
handles: v.array(VFileSystemHandle),
|
||||||
|
},
|
||||||
|
handler: async (ctx, { handles }) => {
|
||||||
|
// biome-ignore lint/suspicious/useIterableCallbackReturn: switch statement is exhaustive
|
||||||
|
const promises = handles.map((handle) => {
|
||||||
|
switch (handle.kind) {
|
||||||
|
case FileType.File:
|
||||||
|
return ctx.db
|
||||||
|
.patch(handle.id, {
|
||||||
|
deletedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.then(() => handle)
|
||||||
|
case FileType.Directory:
|
||||||
|
return Directories.moveToTrashRecursive(ctx, handle).then(
|
||||||
|
() => handle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises)
|
||||||
|
const errors: Err.ApplicationErrorData[] = []
|
||||||
|
const okHandles: FileSystemHandle[] = []
|
||||||
|
for (const result of results) {
|
||||||
|
switch (result.status) {
|
||||||
|
case "fulfilled":
|
||||||
|
okHandles.push(result.value)
|
||||||
|
break
|
||||||
|
case "rejected":
|
||||||
|
errors.push(Err.createJson(Err.Code.Internal))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleted: okHandles,
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -4,21 +4,14 @@ import type {
|
|||||||
AuthenticatedQueryCtx,
|
AuthenticatedQueryCtx,
|
||||||
} from "../functions"
|
} from "../functions"
|
||||||
import * as Err from "./error"
|
import * as Err from "./error"
|
||||||
import type { DirectoryHandle, FilePath, ReverseFilePath } from "./filesystem"
|
import {
|
||||||
import { newDirectoryHandle } from "./filesystem"
|
type DirectoryHandle,
|
||||||
|
type FilePath,
|
||||||
type Directory = {
|
type FileSystemItem,
|
||||||
kind: "directory"
|
FileType,
|
||||||
doc: Doc<"directories">
|
newDirectoryHandle,
|
||||||
}
|
type ReverseFilePath,
|
||||||
|
} from "./filesystem"
|
||||||
type File = {
|
|
||||||
kind: "file"
|
|
||||||
doc: Doc<"files">
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DirectoryItem = Directory | File
|
|
||||||
export type DirectoryItemKind = DirectoryItem["kind"]
|
|
||||||
|
|
||||||
export type DirectoryInfo = Doc<"directories"> & { path: FilePath }
|
export type DirectoryInfo = Doc<"directories"> & { path: FilePath }
|
||||||
|
|
||||||
@@ -83,7 +76,7 @@ export async function fetch(
|
|||||||
export async function fetchContent(
|
export async function fetchContent(
|
||||||
ctx: AuthenticatedQueryCtx,
|
ctx: AuthenticatedQueryCtx,
|
||||||
{ directoryId }: { directoryId?: Id<"directories"> } = {},
|
{ directoryId }: { directoryId?: Id<"directories"> } = {},
|
||||||
): Promise<DirectoryItem[]> {
|
): Promise<FileSystemItem[]> {
|
||||||
let dirId: Id<"directories"> | undefined
|
let dirId: Id<"directories"> | undefined
|
||||||
if (directoryId) {
|
if (directoryId) {
|
||||||
dirId = directoryId
|
dirId = directoryId
|
||||||
@@ -110,12 +103,12 @@ export async function fetchContent(
|
|||||||
.collect(),
|
.collect(),
|
||||||
])
|
])
|
||||||
|
|
||||||
const items: DirectoryItem[] = []
|
const items: FileSystemItem[] = []
|
||||||
for (const directory of directories) {
|
for (const directory of directories) {
|
||||||
items.push({ kind: "directory", doc: directory })
|
items.push({ kind: FileType.Directory, doc: directory })
|
||||||
}
|
}
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
items.push({ kind: "file", doc: file })
|
items.push({ kind: FileType.File, doc: file })
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
@@ -206,7 +199,7 @@ export async function move(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
okDirectories.push(sourceDirectories[i])
|
okDirectories.push(sourceDirectories[i]!)
|
||||||
}
|
}
|
||||||
} else if (result.status === "rejected") {
|
} else if (result.status === "rejected") {
|
||||||
errors.push(Err.createJson(Err.Code.Internal))
|
errors.push(Err.createJson(Err.Code.Internal))
|
||||||
@@ -244,14 +237,14 @@ export async function move(
|
|||||||
|
|
||||||
export async function moveToTrashRecursive(
|
export async function moveToTrashRecursive(
|
||||||
ctx: AuthenticatedMutationCtx,
|
ctx: AuthenticatedMutationCtx,
|
||||||
directoryId: Id<"directories">,
|
handle: DirectoryHandle,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
const filesToDelete: Id<"files">[] = []
|
const filesToDelete: Id<"files">[] = []
|
||||||
const directoriesToDelete: Id<"directories">[] = []
|
const directoriesToDelete: Id<"directories">[] = []
|
||||||
|
|
||||||
const directoryQueue: Id<"directories">[] = [directoryId]
|
const directoryQueue: Id<"directories">[] = [handle.id]
|
||||||
|
|
||||||
while (directoryQueue.length > 0) {
|
while (directoryQueue.length > 0) {
|
||||||
const currentDirectoryId = directoryQueue.shift()!
|
const currentDirectoryId = directoryQueue.shift()!
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import type { Id } from "../_generated/dataModel"
|
import { v } from "convex/values"
|
||||||
|
import type { Doc, Id } from "../_generated/dataModel"
|
||||||
|
|
||||||
export enum FileType {
|
export enum FileType {
|
||||||
File = "File",
|
File = "File",
|
||||||
Directory = "Directory",
|
Directory = "Directory",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Directory = {
|
||||||
|
kind: FileType.Directory
|
||||||
|
doc: Doc<"directories">
|
||||||
|
}
|
||||||
|
export type File = {
|
||||||
|
kind: FileType.File
|
||||||
|
doc: Doc<"files">
|
||||||
|
}
|
||||||
|
export type FileSystemItem = Directory | File
|
||||||
|
|
||||||
export type DirectoryPathComponent = {
|
export type DirectoryPathComponent = {
|
||||||
handle: DirectoryHandle
|
handle: DirectoryHandle
|
||||||
name: string
|
name: string
|
||||||
@@ -14,31 +25,28 @@ export type FilePathComponent = {
|
|||||||
handle: FileHandle
|
handle: FileHandle
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PathComponent = FilePathComponent | DirectoryPathComponent
|
export type PathComponent = FilePathComponent | DirectoryPathComponent
|
||||||
|
|
||||||
export type FilePath = [...DirectoryPathComponent[], PathComponent]
|
export type FilePath = [...DirectoryPathComponent[], PathComponent]
|
||||||
|
|
||||||
export type ReverseFilePath = [PathComponent, ...DirectoryPathComponent[]]
|
export type ReverseFilePath = [PathComponent, ...DirectoryPathComponent[]]
|
||||||
|
|
||||||
export type FileHandle = {
|
|
||||||
kind: "file"
|
|
||||||
id: Id<"files">
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DirectoryHandle = {
|
export type DirectoryHandle = {
|
||||||
kind: "directory"
|
kind: FileType.Directory
|
||||||
id: Id<"directories">
|
id: Id<"directories">
|
||||||
}
|
}
|
||||||
|
export type FileHandle = {
|
||||||
|
kind: FileType.File
|
||||||
|
id: Id<"files">
|
||||||
|
}
|
||||||
export type FileSystemHandle = DirectoryHandle | FileHandle
|
export type FileSystemHandle = DirectoryHandle | FileHandle
|
||||||
|
|
||||||
export function newDirectoryHandle(id: Id<"directories">): DirectoryHandle {
|
export function newFileSystemHandle(item: FileSystemItem): FileSystemHandle {
|
||||||
return { kind: "directory", id }
|
console.log("item", item)
|
||||||
}
|
switch (item.kind) {
|
||||||
|
case FileType.File:
|
||||||
export function newFileHandle(id: Id<"files">): FileHandle {
|
return { kind: item.kind, id: item.doc._id }
|
||||||
return { kind: "file", id }
|
case FileType.Directory:
|
||||||
|
return { kind: item.kind, id: item.doc._id }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSameHandle(
|
export function isSameHandle(
|
||||||
@@ -47,3 +55,21 @@ export function isSameHandle(
|
|||||||
): boolean {
|
): boolean {
|
||||||
return handle1.kind === handle2.kind && handle1.id === handle2.id
|
return handle1.kind === handle2.kind && handle1.id === handle2.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function newDirectoryHandle(id: Id<"directories">): DirectoryHandle {
|
||||||
|
return { kind: FileType.Directory, id }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newFileHandle(id: Id<"files">): FileHandle {
|
||||||
|
return { kind: FileType.File, id }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VDirectoryHandle = v.object({
|
||||||
|
kind: v.literal(FileType.Directory),
|
||||||
|
id: v.id("directories"),
|
||||||
|
})
|
||||||
|
export const VFileHandle = v.object({
|
||||||
|
kind: v.literal(FileType.File),
|
||||||
|
id: v.id("files"),
|
||||||
|
})
|
||||||
|
export const VFileSystemHandle = v.union(VFileHandle, VDirectoryHandle)
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import type { Doc } from "@fileone/convex/_generated/dataModel"
|
import type { Doc } from "@fileone/convex/_generated/dataModel"
|
||||||
import type {
|
import type { DirectoryInfo } from "@fileone/convex/model/directories"
|
||||||
DirectoryInfo,
|
import type { FileSystemItem } from "@fileone/convex/model/filesystem"
|
||||||
DirectoryItem,
|
|
||||||
} from "@fileone/convex/model/directories"
|
|
||||||
import { createContext } from "react"
|
import { createContext } from "react"
|
||||||
|
|
||||||
type DirectoryPageContextType = {
|
type DirectoryPageContextType = {
|
||||||
rootDirectory: Doc<"directories">
|
rootDirectory: Doc<"directories">
|
||||||
directory: DirectoryInfo
|
directory: DirectoryInfo
|
||||||
directoryContent: DirectoryItem[]
|
directoryContent: FileSystemItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DirectoryPageContext = createContext<DirectoryPageContextType>(
|
export const DirectoryPageContext = createContext<DirectoryPageContextType>(
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { api } from "@fileone/convex/_generated/api"
|
import { api } from "@fileone/convex/_generated/api"
|
||||||
import type { Doc } from "@fileone/convex/_generated/dataModel"
|
import type { Doc } from "@fileone/convex/_generated/dataModel"
|
||||||
import type { DirectoryItem } from "@fileone/convex/model/directories"
|
|
||||||
import {
|
import {
|
||||||
|
type DirectoryHandle,
|
||||||
|
type FileHandle,
|
||||||
type FileSystemHandle,
|
type FileSystemHandle,
|
||||||
|
type FileSystemItem,
|
||||||
|
FileType,
|
||||||
isSameHandle,
|
isSameHandle,
|
||||||
newDirectoryHandle,
|
newDirectoryHandle,
|
||||||
newFileHandle,
|
newFileHandle,
|
||||||
|
newFileSystemHandle,
|
||||||
} from "@fileone/convex/model/filesystem"
|
} from "@fileone/convex/model/filesystem"
|
||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import { Link, useNavigate } from "@tanstack/react-router"
|
import { Link, useNavigate } from "@tanstack/react-router"
|
||||||
@@ -19,8 +23,8 @@ import {
|
|||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import { useMutation as useContextMutation } from "convex/react"
|
import { useMutation as useContextMutation } from "convex/react"
|
||||||
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||||
import { CheckIcon, TextCursorInputIcon, TrashIcon, XIcon } from "lucide-react"
|
import { TextCursorInputIcon, TrashIcon } from "lucide-react"
|
||||||
import { useContext, useEffect, useId, useRef } from "react"
|
import { useContext, useEffect, useRef } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { DirectoryIcon } from "@/components/icons/directory-icon"
|
import { DirectoryIcon } from "@/components/icons/directory-icon"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
@@ -43,17 +47,13 @@ import {
|
|||||||
keyboardModifierAtom,
|
keyboardModifierAtom,
|
||||||
} from "@/lib/keyboard"
|
} from "@/lib/keyboard"
|
||||||
import { TextFileIcon } from "../../components/icons/text-file-icon"
|
import { TextFileIcon } from "../../components/icons/text-file-icon"
|
||||||
import { Button } from "../../components/ui/button"
|
|
||||||
import { LoadingSpinner } from "../../components/ui/loading-spinner"
|
|
||||||
import { useFileDrop } from "../../files/use-file-drop"
|
import { useFileDrop } from "../../files/use-file-drop"
|
||||||
import { withDefaultOnError } from "../../lib/error"
|
|
||||||
import { cn } from "../../lib/utils"
|
import { cn } from "../../lib/utils"
|
||||||
import { DirectoryPageContext } from "./context"
|
import { DirectoryPageContext } from "./context"
|
||||||
import {
|
import {
|
||||||
contextMenuTargeItemAtom,
|
contextMenuTargeItemsAtom,
|
||||||
dragInfoAtom,
|
dragInfoAtom,
|
||||||
itemBeingRenamedAtom,
|
itemBeingRenamedAtom,
|
||||||
newFileTypeAtom,
|
|
||||||
openedFileAtom,
|
openedFileAtom,
|
||||||
optimisticDeletedItemsAtom,
|
optimisticDeletedItemsAtom,
|
||||||
} from "./state"
|
} from "./state"
|
||||||
@@ -68,7 +68,7 @@ function formatFileSize(bytes: number): string {
|
|||||||
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnDef<DirectoryItem>[] = [
|
const columns: ColumnDef<FileSystemItem>[] = [
|
||||||
{
|
{
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@@ -99,9 +99,9 @@ const columns: ColumnDef<DirectoryItem>[] = [
|
|||||||
accessorKey: "doc.name",
|
accessorKey: "doc.name",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
switch (row.original.kind) {
|
switch (row.original.kind) {
|
||||||
case "file":
|
case FileType.File:
|
||||||
return <FileNameCell file={row.original.doc} />
|
return <FileNameCell file={row.original.doc} />
|
||||||
case "directory":
|
case FileType.Directory:
|
||||||
return <DirectoryNameCell directory={row.original.doc} />
|
return <DirectoryNameCell directory={row.original.doc} />
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -112,9 +112,9 @@ const columns: ColumnDef<DirectoryItem>[] = [
|
|||||||
accessorKey: "size",
|
accessorKey: "size",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
switch (row.original.kind) {
|
switch (row.original.kind) {
|
||||||
case "file":
|
case FileType.File:
|
||||||
return <div>{formatFileSize(row.original.doc.size)}</div>
|
return <div>{formatFileSize(row.original.doc.size)}</div>
|
||||||
case "directory":
|
case FileType.Directory:
|
||||||
return <div className="font-mono">-</div>
|
return <div className="font-mono">-</div>
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -148,31 +148,44 @@ export function DirectoryContentTableContextMenu({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const target = useAtomValue(contextMenuTargeItemAtom)
|
const [target, setTarget] = useAtom(contextMenuTargeItemsAtom)
|
||||||
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
|
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
|
||||||
const moveToTrashMutation = useContextMutation(api.files.moveToTrash)
|
const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash)
|
||||||
const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom)
|
const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom)
|
||||||
const setContextMenuTargetItem = useSetAtom(contextMenuTargeItemAtom)
|
|
||||||
const { mutate: moveToTrash } = useMutation({
|
const { mutate: moveToTrash } = useMutation({
|
||||||
mutationFn: moveToTrashMutation,
|
mutationFn: moveToTrashMutation,
|
||||||
onMutate: ({ itemId }) => {
|
onMutate: ({ handles }) => {
|
||||||
setOptimisticDeletedItems((prev) => new Set([...prev, itemId]))
|
setOptimisticDeletedItems(
|
||||||
|
(prev) =>
|
||||||
|
new Set([...prev, ...handles.map((handle) => handle.id)]),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onSuccess: (itemId) => {
|
onSuccess: ({ deleted, errors }, { handles }) => {
|
||||||
setOptimisticDeletedItems((prev) => {
|
setOptimisticDeletedItems((prev) => {
|
||||||
const newSet = new Set(prev)
|
const newSet = new Set(prev)
|
||||||
newSet.delete(itemId)
|
for (const handle of handles) {
|
||||||
|
newSet.delete(handle.id)
|
||||||
|
}
|
||||||
return newSet
|
return newSet
|
||||||
})
|
})
|
||||||
toast.success("Moved to trash")
|
if (errors.length === 0 && deleted.length === handles.length) {
|
||||||
|
toast.success(`Moved ${handles.length} items to trash`)
|
||||||
|
} else if (errors.length === handles.length) {
|
||||||
|
toast.error("Failed to move to trash")
|
||||||
|
} else {
|
||||||
|
toast.info(
|
||||||
|
`Moved ${deleted.length} items to trash; failed to move ${errors.length} items`,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleRename = () => {
|
const handleRename = () => {
|
||||||
const selectedItem = store.get(contextMenuTargeItemAtom)
|
const selectedItems = store.get(contextMenuTargeItemsAtom)
|
||||||
if (selectedItem) {
|
if (selectedItems.length === 1) {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: length is checked
|
||||||
|
const selectedItem = selectedItems[0]!
|
||||||
setItemBeingRenamed({
|
setItemBeingRenamed({
|
||||||
kind: selectedItem.kind,
|
|
||||||
originalItem: selectedItem,
|
originalItem: selectedItem,
|
||||||
name: selectedItem.doc.name,
|
name: selectedItem.doc.name,
|
||||||
})
|
})
|
||||||
@@ -180,11 +193,10 @@ export function DirectoryContentTableContextMenu({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
const selectedItem = store.get(contextMenuTargeItemAtom)
|
const selectedItems = store.get(contextMenuTargeItemsAtom)
|
||||||
if (selectedItem) {
|
if (selectedItems.length > 0) {
|
||||||
moveToTrash({
|
moveToTrash({
|
||||||
kind: selectedItem.kind,
|
handles: selectedItems.map(newFileSystemHandle),
|
||||||
itemId: selectedItem.doc._id,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +205,7 @@ export function DirectoryContentTableContextMenu({
|
|||||||
<ContextMenu
|
<ContextMenu
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setContextMenuTargetItem(null)
|
setTarget([])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -217,7 +229,7 @@ export function DirectoryContentTableContextMenu({
|
|||||||
export function DirectoryContentTableContent() {
|
export function DirectoryContentTableContent() {
|
||||||
const { directoryContent } = useContext(DirectoryPageContext)
|
const { directoryContent } = useContext(DirectoryPageContext)
|
||||||
const optimisticDeletedItems = useAtomValue(optimisticDeletedItemsAtom)
|
const optimisticDeletedItems = useAtomValue(optimisticDeletedItemsAtom)
|
||||||
const setContextMenuTargetItem = useSetAtom(contextMenuTargeItemAtom)
|
const setContextMenuTargetItem = useSetAtom(contextMenuTargeItemsAtom)
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@@ -247,23 +259,26 @@ export function DirectoryContentTableContent() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleRowContextMenu = (
|
const handleRowContextMenu = (
|
||||||
row: Row<DirectoryItem>,
|
row: Row<FileSystemItem>,
|
||||||
_event: React.MouseEvent,
|
_event: React.MouseEvent,
|
||||||
) => {
|
) => {
|
||||||
const target = store.get(contextMenuTargeItemAtom)
|
const target = store.get(contextMenuTargeItemsAtom)
|
||||||
if (target === row.original) {
|
if (target.length > 0) {
|
||||||
setContextMenuTargetItem(null)
|
setContextMenuTargetItem([])
|
||||||
|
} else if (row.getIsSelected()) {
|
||||||
|
setContextMenuTargetItem(
|
||||||
|
table.getSelectedRowModel().rows.map((row) => row.original),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
selectRow(row)
|
selectRow(row)
|
||||||
setContextMenuTargetItem(row.original)
|
setContextMenuTargetItem([row.original])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectRow = (row: Row<DirectoryItem>) => {
|
const selectRow = (row: Row<FileSystemItem>) => {
|
||||||
const keyboardModifiers = store.get(keyboardModifierAtom)
|
const keyboardModifiers = store.get(keyboardModifierAtom)
|
||||||
const isMultiSelectMode = isControlOrCommandKeyActive(keyboardModifiers)
|
const isMultiSelectMode = isControlOrCommandKeyActive(keyboardModifiers)
|
||||||
const isRowSelected = row.getIsSelected()
|
const isRowSelected = row.getIsSelected()
|
||||||
console.log({ isMultiSelectMode, isRowSelected })
|
|
||||||
if (isRowSelected && isMultiSelectMode) {
|
if (isRowSelected && isMultiSelectMode) {
|
||||||
row.toggleSelected(false)
|
row.toggleSelected(false)
|
||||||
} else if (isRowSelected && !isMultiSelectMode) {
|
} else if (isRowSelected && !isMultiSelectMode) {
|
||||||
@@ -282,8 +297,8 @@ export function DirectoryContentTableContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRowDoubleClick = (row: Row<DirectoryItem>) => {
|
const handleRowDoubleClick = (row: Row<FileSystemItem>) => {
|
||||||
if (row.original.kind === "directory") {
|
if (row.original.kind === FileType.Directory) {
|
||||||
navigate({
|
navigate({
|
||||||
to: `/directories/${row.original.doc._id}`,
|
to: `/directories/${row.original.doc._id}`,
|
||||||
})
|
})
|
||||||
@@ -355,8 +370,8 @@ function FileItemRow({
|
|||||||
onContextMenu,
|
onContextMenu,
|
||||||
onDoubleClick,
|
onDoubleClick,
|
||||||
}: {
|
}: {
|
||||||
table: TableType<DirectoryItem>
|
table: TableType<FileSystemItem>
|
||||||
row: Row<DirectoryItem>
|
row: Row<FileSystemItem>
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
onContextMenu: (e: React.MouseEvent) => void
|
onContextMenu: (e: React.MouseEvent) => void
|
||||||
onDoubleClick: () => void
|
onDoubleClick: () => void
|
||||||
@@ -366,19 +381,19 @@ function FileItemRow({
|
|||||||
|
|
||||||
const { isDraggedOver, dropHandlers } = useFileDrop({
|
const { isDraggedOver, dropHandlers } = useFileDrop({
|
||||||
destItem:
|
destItem:
|
||||||
row.original.kind === "directory"
|
row.original.kind === FileType.Directory
|
||||||
? newDirectoryHandle(row.original.doc._id)
|
? newDirectoryHandle(row.original.doc._id)
|
||||||
: null,
|
: null,
|
||||||
dragInfoAtom,
|
dragInfoAtom,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleDragStart = (e: React.DragEvent) => {
|
const handleDragStart = (_e: React.DragEvent) => {
|
||||||
let source: FileSystemHandle
|
let source: DirectoryHandle | FileHandle
|
||||||
switch (row.original.kind) {
|
switch (row.original.kind) {
|
||||||
case "file":
|
case FileType.File:
|
||||||
source = newFileHandle(row.original.doc._id)
|
source = newFileHandle(row.original.doc._id)
|
||||||
break
|
break
|
||||||
case "directory":
|
case FileType.Directory:
|
||||||
source = newDirectoryHandle(row.original.doc._id)
|
source = newDirectoryHandle(row.original.doc._id)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -386,15 +401,9 @@ function FileItemRow({
|
|||||||
let draggedItems: FileSystemHandle[]
|
let draggedItems: FileSystemHandle[]
|
||||||
// drag all selections, but only if the currently dragged row is also selected
|
// drag all selections, but only if the currently dragged row is also selected
|
||||||
if (row.getIsSelected()) {
|
if (row.getIsSelected()) {
|
||||||
// biome-ignore lint/suspicious/useIterableCallbackReturn: the switch statement is exhaustive
|
draggedItems = table
|
||||||
draggedItems = table.getSelectedRowModel().rows.map((row) => {
|
.getSelectedRowModel()
|
||||||
switch (row.original.kind) {
|
.rows.map((row) => newFileSystemHandle(row.original))
|
||||||
case "file":
|
|
||||||
return newFileHandle(row.original.doc._id)
|
|
||||||
case "directory":
|
|
||||||
return newDirectoryHandle(row.original.doc._id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!draggedItems.some((item) => isSameHandle(item, source))) {
|
if (!draggedItems.some((item) => isSameHandle(item, source))) {
|
||||||
draggedItems.push(source)
|
draggedItems.push(source)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
|
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
|
||||||
import type {
|
import type { DirectoryItemKind } from "@fileone/convex/model/directories"
|
||||||
DirectoryItem,
|
import type { FileSystemItem, FileType } from "@fileone/convex/model/filesystem"
|
||||||
DirectoryItemKind,
|
|
||||||
} from "@fileone/convex/model/directories"
|
|
||||||
import type { FileType } from "@fileone/convex/model/filesystem"
|
|
||||||
import type { RowSelectionState } from "@tanstack/react-table"
|
import type { RowSelectionState } from "@tanstack/react-table"
|
||||||
import { atom } from "jotai"
|
import { atom } from "jotai"
|
||||||
import type { FileDragInfo } from "../../files/use-file-drop"
|
import type { FileDragInfo } from "../../files/use-file-drop"
|
||||||
|
|
||||||
export const contextMenuTargeItemAtom = atom<DirectoryItem | null>(null)
|
export const contextMenuTargeItemsAtom = atom<FileSystemItem[]>([])
|
||||||
export const optimisticDeletedItemsAtom = atom(
|
export const optimisticDeletedItemsAtom = atom(
|
||||||
new Set<Id<"files"> | Id<"directories">>(),
|
new Set<Id<"files"> | Id<"directories">>(),
|
||||||
)
|
)
|
||||||
@@ -18,8 +15,7 @@ export const selectedFileRowsAtom = atom<RowSelectionState>({})
|
|||||||
export const newFileTypeAtom = atom<FileType | null>(null)
|
export const newFileTypeAtom = atom<FileType | null>(null)
|
||||||
|
|
||||||
export const itemBeingRenamedAtom = atom<{
|
export const itemBeingRenamedAtom = atom<{
|
||||||
kind: DirectoryItemKind
|
originalItem: FileSystemItem
|
||||||
originalItem: DirectoryItem
|
|
||||||
name: string
|
name: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user