mirror of
https://github.com/get-drexa/drive.git
synced 2025-12-01 05:51:39 +00:00
feat: add trash page
This commit is contained in:
1
bun.lock
1
bun.lock
@@ -13,7 +13,6 @@
|
|||||||
"name": "@fileone/convex",
|
"name": "@fileone/convex",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fileone/path": "workspace:*",
|
"@fileone/path": "workspace:*",
|
||||||
"convex-helpers": "^0.1.104",
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"convex": "^1.27.0",
|
"convex": "^1.27.0",
|
||||||
|
|||||||
2
packages/convex/_generated/api.d.ts
vendored
2
packages/convex/_generated/api.d.ts
vendored
@@ -13,7 +13,6 @@ import type {
|
|||||||
FilterApi,
|
FilterApi,
|
||||||
FunctionReference,
|
FunctionReference,
|
||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
import type * as admin from "../admin.js";
|
|
||||||
import type * as files from "../files.js";
|
import type * as files from "../files.js";
|
||||||
import type * as filesystem from "../filesystem.js";
|
import type * as filesystem from "../filesystem.js";
|
||||||
import type * as functions from "../functions.js";
|
import type * as functions from "../functions.js";
|
||||||
@@ -33,7 +32,6 @@ import type * as users from "../users.js";
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
declare const fullApi: ApiFromModules<{
|
declare const fullApi: ApiFromModules<{
|
||||||
admin: typeof admin;
|
|
||||||
files: typeof files;
|
files: typeof files;
|
||||||
filesystem: typeof filesystem;
|
filesystem: typeof filesystem;
|
||||||
functions: typeof functions;
|
functions: typeof functions;
|
||||||
|
|||||||
@@ -50,15 +50,6 @@ export const fetchDirectory = authenticatedQuery({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const fetchDirectoryContent = authenticatedQuery({
|
|
||||||
args: {
|
|
||||||
directoryId: v.optional(v.id("directories")),
|
|
||||||
},
|
|
||||||
handler: async (ctx, { directoryId }): Promise<FileSystemItem[]> => {
|
|
||||||
return await Directories.fetchContent(ctx, { directoryId })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const createDirectory = authenticatedMutation({
|
export const createDirectory = authenticatedMutation({
|
||||||
args: {
|
args: {
|
||||||
name: v.string(),
|
name: v.string(),
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { v } from "convex/values"
|
import { v } from "convex/values"
|
||||||
import { authenticatedMutation } from "./functions"
|
import { authenticatedMutation, authenticatedQuery } from "./functions"
|
||||||
import * as Directories from "./model/directories"
|
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,
|
||||||
|
FileSystemItem,
|
||||||
|
} from "./model/filesystem"
|
||||||
import {
|
import {
|
||||||
type FileSystemHandle,
|
type FileSystemHandle,
|
||||||
FileType,
|
FileType,
|
||||||
VDirectoryHandle,
|
VDirectoryHandle,
|
||||||
VFileSystemHandle,
|
VFileSystemHandle,
|
||||||
} from "./model/filesystem"
|
} from "./model/filesystem"
|
||||||
|
|
||||||
export const moveItems = authenticatedMutation({
|
export const moveItems = authenticatedMutation({
|
||||||
args: {
|
args: {
|
||||||
targetDirectory: VDirectoryHandle,
|
targetDirectory: VDirectoryHandle,
|
||||||
@@ -100,3 +103,16 @@ export const moveToTrash = authenticatedMutation({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const fetchDirectoryContent = authenticatedQuery({
|
||||||
|
args: {
|
||||||
|
directoryId: v.optional(v.id("directories")),
|
||||||
|
trashed: v.boolean(),
|
||||||
|
},
|
||||||
|
handler: async (
|
||||||
|
ctx,
|
||||||
|
{ directoryId, trashed },
|
||||||
|
): Promise<FileSystemItem[]> => {
|
||||||
|
return await Directories.fetchContent(ctx, { directoryId, trashed })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export async function fetch(
|
|||||||
|
|
||||||
export async function fetchContent(
|
export async function fetchContent(
|
||||||
ctx: AuthenticatedQueryCtx,
|
ctx: AuthenticatedQueryCtx,
|
||||||
{ directoryId }: { directoryId?: Id<"directories"> } = {},
|
{
|
||||||
|
directoryId,
|
||||||
|
trashed,
|
||||||
|
}: { directoryId?: Id<"directories">; trashed: boolean },
|
||||||
): Promise<FileSystemItem[]> {
|
): Promise<FileSystemItem[]> {
|
||||||
let dirId: Id<"directories"> | undefined
|
let dirId: Id<"directories"> | undefined
|
||||||
if (directoryId) {
|
if (directoryId) {
|
||||||
@@ -85,21 +88,33 @@ export async function fetchContent(
|
|||||||
const [files, directories] = await Promise.all([
|
const [files, directories] = await Promise.all([
|
||||||
ctx.db
|
ctx.db
|
||||||
.query("files")
|
.query("files")
|
||||||
.withIndex("byDirectoryId", (q) =>
|
.withIndex("byDirectoryId", (q) => {
|
||||||
q
|
if (trashed) {
|
||||||
|
return q
|
||||||
.eq("userId", ctx.user._id)
|
.eq("userId", ctx.user._id)
|
||||||
.eq("directoryId", dirId)
|
.eq("directoryId", dirId)
|
||||||
.eq("deletedAt", undefined),
|
.gte("deletedAt", 0)
|
||||||
)
|
}
|
||||||
|
return q
|
||||||
|
.eq("userId", ctx.user._id)
|
||||||
|
.eq("directoryId", dirId)
|
||||||
|
.eq("deletedAt", undefined)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
ctx.db
|
ctx.db
|
||||||
.query("directories")
|
.query("directories")
|
||||||
.withIndex("byParentId", (q) =>
|
.withIndex("byParentId", (q) => {
|
||||||
q
|
if (trashed) {
|
||||||
|
return q
|
||||||
.eq("userId", ctx.user._id)
|
.eq("userId", ctx.user._id)
|
||||||
.eq("parentId", dirId)
|
.eq("parentId", dirId)
|
||||||
.eq("deletedAt", undefined),
|
.gte("deletedAt", 0)
|
||||||
)
|
}
|
||||||
|
return q
|
||||||
|
.eq("userId", ctx.user._id)
|
||||||
|
.eq("parentId", dirId)
|
||||||
|
.eq("deletedAt", undefined)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -216,7 +231,10 @@ export async function move(
|
|||||||
ignoredHandles.add(handle)
|
ignoredHandles.add(handle)
|
||||||
} else {
|
} else {
|
||||||
promises.push(
|
promises.push(
|
||||||
ctx.db.patch(handle.id, { parentId: targetDirectory.id, updatedAt: Date.now() }),
|
ctx.db.patch(handle.id, {
|
||||||
|
parentId: targetDirectory.id,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
HomeIcon,
|
HomeIcon,
|
||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
|
TrashIcon,
|
||||||
User2Icon,
|
User2Icon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import {
|
import {
|
||||||
@@ -62,6 +63,7 @@ function MainSidebarMenu() {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
<AllFilesItem />
|
<AllFilesItem />
|
||||||
|
<TrashItem />
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -89,6 +91,29 @@ function AllFilesItem() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TrashItem() {
|
||||||
|
const location = useLocation()
|
||||||
|
const rootDirectory = useConvexQuery(api.files.fetchRootDirectory)
|
||||||
|
|
||||||
|
if (!rootDirectory) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
asChild
|
||||||
|
isActive={location.pathname.startsWith(
|
||||||
|
`/trash/directories/${rootDirectory._id}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link to={`/trash/directories/${rootDirectory._id}`}>
|
||||||
|
<TrashIcon />
|
||||||
|
<span>Trash</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function UserMenu() {
|
function UserMenu() {
|
||||||
const { signOut } = useAuth()
|
const { signOut } = useAuth()
|
||||||
|
|
||||||
|
|||||||
@@ -131,8 +131,6 @@ export function DirectoryContentTable() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function DirectoryContentTableContent() {
|
export function DirectoryContentTableContent() {
|
||||||
const { directoryContent } = useContext(DirectoryPageContext)
|
const { directoryContent } = useContext(DirectoryPageContext)
|
||||||
const optimisticDeletedItems = useAtomValue(optimisticDeletedItemsAtom)
|
const optimisticDeletedItems = useAtomValue(optimisticDeletedItemsAtom)
|
||||||
|
|||||||
@@ -1,252 +1,19 @@
|
|||||||
import { api } from "@fileone/convex/_generated/api"
|
|
||||||
import {
|
|
||||||
type DirectoryHandle,
|
|
||||||
FileType,
|
|
||||||
type PathComponent,
|
|
||||||
} from "@fileone/convex/model/filesystem"
|
|
||||||
import { useMutation } from "@tanstack/react-query"
|
|
||||||
import { Link } from "@tanstack/react-router"
|
|
||||||
import { useMutation as useConvexMutation } from "convex/react"
|
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
Loader2Icon,
|
|
||||||
PlusIcon,
|
|
||||||
UploadCloudIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
import React, { type ChangeEvent, Fragment, useContext, useRef } from "react"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import { ImagePreviewDialog } from "@/components/image-preview-dialog"
|
import { ImagePreviewDialog } from "@/components/image-preview-dialog"
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu"
|
|
||||||
import { DirectoryIcon } from "../../components/icons/directory-icon"
|
|
||||||
import { TextFileIcon } from "../../components/icons/text-file-icon"
|
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from "../../components/ui/breadcrumb"
|
|
||||||
import { Button } from "../../components/ui/button"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "../../components/ui/tooltip"
|
|
||||||
import { WithAtom } from "../../components/with-atom"
|
|
||||||
import { useFileDrop } from "../../files/use-file-drop"
|
|
||||||
import { cn } from "../../lib/utils"
|
|
||||||
import { DirectoryPageContext } from "./context"
|
|
||||||
import { DirectoryContentTable } from "./directory-content-table"
|
import { DirectoryContentTable } from "./directory-content-table"
|
||||||
import { NewDirectoryDialog } from "./new-directory-dialog"
|
import { openedFileAtom } from "./state"
|
||||||
import { RenameFileDialog } from "./rename-file-dialog"
|
|
||||||
import { dragInfoAtom, newFileTypeAtom, openedFileAtom } from "./state"
|
|
||||||
|
|
||||||
export function DirectoryPage() {
|
export function DirectoryPage() {
|
||||||
const { directory } = useContext(DirectoryPageContext)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="flex py-1 shrink-0 items-center gap-2 border-b px-4 w-full">
|
|
||||||
<FilePathBreadcrumb />
|
|
||||||
<div className="ml-auto flex flex-row gap-2">
|
|
||||||
<NewDirectoryItemDropdown />
|
|
||||||
<UploadFileButton />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<DirectoryContentTable />
|
<DirectoryContentTable />
|
||||||
</div>
|
</div>
|
||||||
<RenameFileDialog />
|
|
||||||
<PreviewDialog />
|
<PreviewDialog />
|
||||||
<WithAtom atom={newFileTypeAtom}>
|
|
||||||
{(newFileType, setNewFileType) => (
|
|
||||||
<NewDirectoryDialog
|
|
||||||
open={newFileType === FileType.Directory}
|
|
||||||
directoryId={directory._id}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setNewFileType(null)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</WithAtom>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilePathBreadcrumb() {
|
|
||||||
const { rootDirectory, directory } = useContext(DirectoryPageContext)
|
|
||||||
|
|
||||||
const breadcrumbItems: React.ReactNode[] = []
|
|
||||||
for (let i = 1; i < directory.path.length - 1; i++) {
|
|
||||||
breadcrumbItems.push(
|
|
||||||
<Fragment key={directory.path[i]?.handle.id}>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<FilePathBreadcrumbItem component={directory.path[i]!} />
|
|
||||||
</Fragment>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Breadcrumb>
|
|
||||||
<BreadcrumbList>
|
|
||||||
{rootDirectory._id === directory._id ? (
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>All Files</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
) : (
|
|
||||||
<FilePathBreadcrumbItem component={directory.path[0]!} />
|
|
||||||
)}
|
|
||||||
{breadcrumbItems}
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>{directory.name}</BreadcrumbPage>{" "}
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilePathBreadcrumbItem({ component }: { component: PathComponent }) {
|
|
||||||
const { isDraggedOver, dropHandlers } = useFileDrop({
|
|
||||||
destItem: component.handle as DirectoryHandle,
|
|
||||||
dragInfoAtom,
|
|
||||||
})
|
|
||||||
|
|
||||||
const dirName = component.name || "All Files"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip open={isDraggedOver}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<BreadcrumbItem
|
|
||||||
className={cn({ "bg-muted": isDraggedOver })}
|
|
||||||
{...dropHandlers}
|
|
||||||
>
|
|
||||||
<BreadcrumbLink asChild>
|
|
||||||
<Link to={`/directories/${component.handle.id}`}>
|
|
||||||
{dirName}
|
|
||||||
</Link>
|
|
||||||
</BreadcrumbLink>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Move to {dirName}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tags: upload, uploadfile, uploadfilebutton, fileupload, fileuploadbutton
|
|
||||||
function UploadFileButton() {
|
|
||||||
const { directory } = useContext(DirectoryPageContext)
|
|
||||||
const generateUploadUrl = useConvexMutation(api.files.generateUploadUrl)
|
|
||||||
const saveFile = useConvexMutation(api.files.saveFile)
|
|
||||||
const { mutate: uploadFile, isPending: isUploading } = useMutation({
|
|
||||||
mutationFn: async (file: File) => {
|
|
||||||
const uploadUrl = await generateUploadUrl()
|
|
||||||
const uploadResult = await fetch(uploadUrl, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": file.type,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const { storageId } = await uploadResult.json()
|
|
||||||
|
|
||||||
await saveFile({
|
|
||||||
storageId,
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
mimeType: file.type,
|
|
||||||
directoryId: directory._id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("File uploaded successfully.")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
fileInputRef.current?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0]
|
|
||||||
if (file) {
|
|
||||||
uploadFile(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
hidden
|
|
||||||
onChange={onFileUpload}
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
name="files"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={handleClick}
|
|
||||||
disabled={isUploading}
|
|
||||||
>
|
|
||||||
{isUploading ? (
|
|
||||||
<Loader2Icon className="animate-spin size-4" />
|
|
||||||
) : (
|
|
||||||
<UploadCloudIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
{isUploading ? "Uploading" : "Upload File"}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewDirectoryItemDropdown() {
|
|
||||||
const [newFileType, setNewFileType] = useAtom(newFileTypeAtom)
|
|
||||||
|
|
||||||
const addNewDirectory = () => {
|
|
||||||
setNewFileType(FileType.Directory)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseAutoFocus = (event: Event) => {
|
|
||||||
// If we just created a new item, prevent the dropdown from restoring focus to the trigger
|
|
||||||
if (newFileType) {
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button size="sm" type="button" variant="outline">
|
|
||||||
<PlusIcon className="size-4" />
|
|
||||||
New
|
|
||||||
<ChevronDownIcon className="pl-1 size-4 shrink-0" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent onCloseAutoFocus={handleCloseAutoFocus}>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<TextFileIcon />
|
|
||||||
Text file
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={addNewDirectory}>
|
|
||||||
<DirectoryIcon />
|
|
||||||
Directory
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PreviewDialog() {
|
function PreviewDialog() {
|
||||||
const [openedFile, setOpenedFile] = useAtom(openedFileAtom)
|
const [openedFile, setOpenedFile] = useAtom(openedFileAtom)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import type {
|
||||||
|
DirectoryHandle,
|
||||||
|
PathComponent,
|
||||||
|
} from "@fileone/convex/model/filesystem"
|
||||||
|
import { Link } from "@tanstack/react-router"
|
||||||
|
import { Fragment, useContext } from "react"
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "../../components/ui/breadcrumb"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "../../components/ui/tooltip"
|
||||||
|
import { useFileDrop } from "../../files/use-file-drop"
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
import { DirectoryPageContext } from "./context"
|
||||||
|
import { dragInfoAtom } from "./state"
|
||||||
|
|
||||||
|
export function FilePathBreadcrumb() {
|
||||||
|
const { rootDirectory, directory } = useContext(DirectoryPageContext)
|
||||||
|
|
||||||
|
const breadcrumbItems: React.ReactNode[] = []
|
||||||
|
for (let i = 1; i < directory.path.length - 1; i++) {
|
||||||
|
breadcrumbItems.push(
|
||||||
|
<Fragment key={directory.path[i]?.handle.id}>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<FilePathBreadcrumbItem component={directory.path[i]!} />
|
||||||
|
</Fragment>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
{rootDirectory._id === directory._id ? (
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>All Files</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
) : (
|
||||||
|
<FilePathBreadcrumbItem component={directory.path[0]!} />
|
||||||
|
)}
|
||||||
|
{breadcrumbItems}
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>{directory.name}</BreadcrumbPage>{" "}
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilePathBreadcrumbItem({ component }: { component: PathComponent }) {
|
||||||
|
const { isDraggedOver, dropHandlers } = useFileDrop({
|
||||||
|
destItem: component.handle as DirectoryHandle,
|
||||||
|
dragInfoAtom,
|
||||||
|
})
|
||||||
|
|
||||||
|
const dirName = component.name || "All Files"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip open={isDraggedOver}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<BreadcrumbItem
|
||||||
|
className={cn({ "bg-muted": isDraggedOver })}
|
||||||
|
{...dropHandlers}
|
||||||
|
>
|
||||||
|
<BreadcrumbLink asChild>
|
||||||
|
<Link to={`/directories/${component.handle.id}`}>
|
||||||
|
{dirName}
|
||||||
|
</Link>
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Move to {dirName}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,8 +11,6 @@ export const optimisticDeletedItemsAtom = atom(
|
|||||||
|
|
||||||
export const selectedFileRowsAtom = atom<RowSelectionState>({})
|
export const selectedFileRowsAtom = atom<RowSelectionState>({})
|
||||||
|
|
||||||
export const newFileTypeAtom = atom<FileType | null>(null)
|
|
||||||
|
|
||||||
export const itemBeingRenamedAtom = atom<{
|
export const itemBeingRenamedAtom = atom<{
|
||||||
originalItem: FileSystemItem
|
originalItem: FileSystemItem
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Route as LoginCallbackRouteImport } from './routes/login_.callback'
|
|||||||
import { Route as AuthenticatedSidebarLayoutRouteImport } from './routes/_authenticated/_sidebar-layout'
|
import { Route as AuthenticatedSidebarLayoutRouteImport } from './routes/_authenticated/_sidebar-layout'
|
||||||
import { Route as AuthenticatedSidebarLayoutHomeRouteImport } from './routes/_authenticated/_sidebar-layout/home'
|
import { Route as AuthenticatedSidebarLayoutHomeRouteImport } from './routes/_authenticated/_sidebar-layout/home'
|
||||||
import { Route as AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport } from './routes/_authenticated/_sidebar-layout/directories.$directoryId'
|
import { Route as AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport } from './routes/_authenticated/_sidebar-layout/directories.$directoryId'
|
||||||
|
import { Route as AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRouteImport } from './routes/_authenticated/_sidebar-layout/trash.directories.$directoryId'
|
||||||
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
@@ -53,6 +54,12 @@ const AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute =
|
|||||||
path: '/directories/$directoryId',
|
path: '/directories/$directoryId',
|
||||||
getParentRoute: () => AuthenticatedSidebarLayoutRoute,
|
getParentRoute: () => AuthenticatedSidebarLayoutRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute =
|
||||||
|
AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRouteImport.update({
|
||||||
|
id: '/trash/directories/$directoryId',
|
||||||
|
path: '/trash/directories/$directoryId',
|
||||||
|
getParentRoute: () => AuthenticatedSidebarLayoutRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
@@ -60,6 +67,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/': typeof AuthenticatedIndexRoute
|
'/': typeof AuthenticatedIndexRoute
|
||||||
'/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
'/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
||||||
'/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
'/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
||||||
|
'/trash/directories/$directoryId': typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
@@ -67,6 +75,7 @@ export interface FileRoutesByTo {
|
|||||||
'/': typeof AuthenticatedIndexRoute
|
'/': typeof AuthenticatedIndexRoute
|
||||||
'/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
'/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
||||||
'/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
'/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
||||||
|
'/trash/directories/$directoryId': typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -77,6 +86,7 @@ export interface FileRoutesById {
|
|||||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||||
'/_authenticated/_sidebar-layout/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
'/_authenticated/_sidebar-layout/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
||||||
'/_authenticated/_sidebar-layout/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
'/_authenticated/_sidebar-layout/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
||||||
|
'/_authenticated/_sidebar-layout/trash/directories/$directoryId': typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -86,8 +96,15 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/home'
|
| '/home'
|
||||||
| '/directories/$directoryId'
|
| '/directories/$directoryId'
|
||||||
|
| '/trash/directories/$directoryId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/login' | '/login/callback' | '/' | '/home' | '/directories/$directoryId'
|
to:
|
||||||
|
| '/login'
|
||||||
|
| '/login/callback'
|
||||||
|
| '/'
|
||||||
|
| '/home'
|
||||||
|
| '/directories/$directoryId'
|
||||||
|
| '/trash/directories/$directoryId'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/_authenticated'
|
| '/_authenticated'
|
||||||
@@ -97,6 +114,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authenticated/'
|
| '/_authenticated/'
|
||||||
| '/_authenticated/_sidebar-layout/home'
|
| '/_authenticated/_sidebar-layout/home'
|
||||||
| '/_authenticated/_sidebar-layout/directories/$directoryId'
|
| '/_authenticated/_sidebar-layout/directories/$directoryId'
|
||||||
|
| '/_authenticated/_sidebar-layout/trash/directories/$directoryId'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -156,12 +174,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport
|
preLoaderRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport
|
||||||
parentRoute: typeof AuthenticatedSidebarLayoutRoute
|
parentRoute: typeof AuthenticatedSidebarLayoutRoute
|
||||||
}
|
}
|
||||||
|
'/_authenticated/_sidebar-layout/trash/directories/$directoryId': {
|
||||||
|
id: '/_authenticated/_sidebar-layout/trash/directories/$directoryId'
|
||||||
|
path: '/trash/directories/$directoryId'
|
||||||
|
fullPath: '/trash/directories/$directoryId'
|
||||||
|
preLoaderRoute: typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedSidebarLayoutRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthenticatedSidebarLayoutRouteChildren {
|
interface AuthenticatedSidebarLayoutRouteChildren {
|
||||||
AuthenticatedSidebarLayoutHomeRoute: typeof AuthenticatedSidebarLayoutHomeRoute
|
AuthenticatedSidebarLayoutHomeRoute: typeof AuthenticatedSidebarLayoutHomeRoute
|
||||||
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
||||||
|
AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute: typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedSidebarLayoutRouteChildren: AuthenticatedSidebarLayoutRouteChildren =
|
const AuthenticatedSidebarLayoutRouteChildren: AuthenticatedSidebarLayoutRouteChildren =
|
||||||
@@ -169,6 +195,8 @@ const AuthenticatedSidebarLayoutRouteChildren: AuthenticatedSidebarLayoutRouteCh
|
|||||||
AuthenticatedSidebarLayoutHomeRoute: AuthenticatedSidebarLayoutHomeRoute,
|
AuthenticatedSidebarLayoutHomeRoute: AuthenticatedSidebarLayoutHomeRoute,
|
||||||
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute:
|
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute:
|
||||||
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute,
|
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute,
|
||||||
|
AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute:
|
||||||
|
AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedSidebarLayoutRouteWithChildren =
|
const AuthenticatedSidebarLayoutRouteWithChildren =
|
||||||
|
|||||||
@@ -1,9 +1,36 @@
|
|||||||
import { api } from "@fileone/convex/_generated/api"
|
import { api } from "@fileone/convex/_generated/api"
|
||||||
|
import { FileType } from "@fileone/convex/model/filesystem"
|
||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { useQuery as useConvexQuery } from "convex/react"
|
import {
|
||||||
|
useMutation as useConvexMutation,
|
||||||
|
useQuery as useConvexQuery,
|
||||||
|
} from "convex/react"
|
||||||
|
import { atom, useAtom } from "jotai"
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
Loader2Icon,
|
||||||
|
PlusIcon,
|
||||||
|
UploadCloudIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { type ChangeEvent, useContext, useRef } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { DirectoryIcon } from "@/components/icons/directory-icon"
|
||||||
|
import { TextFileIcon } from "@/components/icons/text-file-icon"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { WithAtom } from "@/components/with-atom"
|
||||||
import { DirectoryPageContext } from "@/directories/directory-page/context"
|
import { DirectoryPageContext } from "@/directories/directory-page/context"
|
||||||
import { DirectoryPage } from "@/directories/directory-page/directory-page"
|
import { DirectoryPage } from "@/directories/directory-page/directory-page"
|
||||||
import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton"
|
import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton"
|
||||||
|
import { FilePathBreadcrumb } from "@/directories/directory-page/file-path-breadcrumb"
|
||||||
|
import { NewDirectoryDialog } from "@/directories/directory-page/new-directory-dialog"
|
||||||
|
import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog"
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticated/_sidebar-layout/directories/$directoryId",
|
"/_authenticated/_sidebar-layout/directories/$directoryId",
|
||||||
@@ -11,15 +38,21 @@ export const Route = createFileRoute(
|
|||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const newFileTypeAtom = atom<FileType | null>(null)
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { directoryId } = Route.useParams()
|
const { directoryId } = Route.useParams()
|
||||||
const rootDirectory = useConvexQuery(api.files.fetchRootDirectory)
|
const rootDirectory = useConvexQuery(api.files.fetchRootDirectory)
|
||||||
const directory = useConvexQuery(api.files.fetchDirectory, {
|
const directory = useConvexQuery(api.files.fetchDirectory, {
|
||||||
directoryId,
|
directoryId,
|
||||||
})
|
})
|
||||||
const directoryContent = useConvexQuery(api.files.fetchDirectoryContent, {
|
const directoryContent = useConvexQuery(
|
||||||
|
api.filesystem.fetchDirectoryContent,
|
||||||
|
{
|
||||||
directoryId,
|
directoryId,
|
||||||
})
|
trashed: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (!directory || !directoryContent || !rootDirectory) {
|
if (!directory || !directoryContent || !rootDirectory) {
|
||||||
return <DirectoryPageSkeleton />
|
return <DirectoryPageSkeleton />
|
||||||
@@ -29,7 +62,137 @@ function RouteComponent() {
|
|||||||
<DirectoryPageContext
|
<DirectoryPageContext
|
||||||
value={{ rootDirectory, directory, directoryContent }}
|
value={{ rootDirectory, directory, directoryContent }}
|
||||||
>
|
>
|
||||||
|
<header className="flex py-2 shrink-0 items-center gap-2 border-b px-4 w-full">
|
||||||
|
<FilePathBreadcrumb />
|
||||||
|
<div className="ml-auto flex flex-row gap-2">
|
||||||
|
<NewDirectoryItemDropdown />
|
||||||
|
<UploadFileButton />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<DirectoryPage />
|
<DirectoryPage />
|
||||||
|
|
||||||
|
<WithAtom atom={newFileTypeAtom}>
|
||||||
|
{(newFileType, setNewFileType) => (
|
||||||
|
<NewDirectoryDialog
|
||||||
|
open={newFileType === FileType.Directory}
|
||||||
|
directoryId={directory._id}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setNewFileType(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</WithAtom>
|
||||||
|
|
||||||
|
<RenameFileDialog />
|
||||||
</DirectoryPageContext>
|
</DirectoryPageContext>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tags: upload, uploadfile, uploadfilebutton, fileupload, fileuploadbutton
|
||||||
|
function UploadFileButton() {
|
||||||
|
const { directory } = useContext(DirectoryPageContext)
|
||||||
|
const generateUploadUrl = useConvexMutation(api.files.generateUploadUrl)
|
||||||
|
const saveFile = useConvexMutation(api.files.saveFile)
|
||||||
|
const { mutate: uploadFile, isPending: isUploading } = useMutation({
|
||||||
|
mutationFn: async (file: File) => {
|
||||||
|
const uploadUrl = await generateUploadUrl()
|
||||||
|
const uploadResult = await fetch(uploadUrl, {
|
||||||
|
method: "POST",
|
||||||
|
body: file,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": file.type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { storageId } = await uploadResult.json()
|
||||||
|
|
||||||
|
await saveFile({
|
||||||
|
storageId,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mimeType: file.type,
|
||||||
|
directoryId: directory._id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("File uploaded successfully.")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
fileInputRef.current?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
uploadFile(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
onChange={onFileUpload}
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
name="files"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<Loader2Icon className="animate-spin size-4" />
|
||||||
|
) : (
|
||||||
|
<UploadCloudIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
{isUploading ? "Uploading" : "Upload File"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewDirectoryItemDropdown() {
|
||||||
|
const [newFileType, setNewFileType] = useAtom(newFileTypeAtom)
|
||||||
|
|
||||||
|
const addNewDirectory = () => {
|
||||||
|
setNewFileType(FileType.Directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseAutoFocus = (event: Event) => {
|
||||||
|
// If we just created a new item, prevent the dropdown from restoring focus to the trigger
|
||||||
|
if (newFileType) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button size="sm" type="button" variant="outline">
|
||||||
|
<PlusIcon className="size-4" />
|
||||||
|
New
|
||||||
|
<ChevronDownIcon className="pl-1 size-4 shrink-0" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent onCloseAutoFocus={handleCloseAutoFocus}>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<TextFileIcon />
|
||||||
|
Text file
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={addNewDirectory}>
|
||||||
|
<DirectoryIcon />
|
||||||
|
Directory
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { api } from "@fileone/convex/_generated/api"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { useQuery as useConvexQuery } from "convex/react"
|
||||||
|
import { TrashIcon } from "lucide-react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { DirectoryPageContext } from "@/directories/directory-page/context"
|
||||||
|
import { DirectoryPage } from "@/directories/directory-page/directory-page"
|
||||||
|
import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton"
|
||||||
|
import { FilePathBreadcrumb } from "@/directories/directory-page/file-path-breadcrumb"
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticated/_sidebar-layout/trash/directories/$directoryId",
|
||||||
|
)({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { directoryId } = Route.useParams()
|
||||||
|
const rootDirectory = useConvexQuery(api.files.fetchRootDirectory)
|
||||||
|
const directory = useConvexQuery(api.files.fetchDirectory, {
|
||||||
|
directoryId,
|
||||||
|
})
|
||||||
|
const directoryContent = useConvexQuery(
|
||||||
|
api.filesystem.fetchDirectoryContent,
|
||||||
|
{
|
||||||
|
directoryId,
|
||||||
|
trashed: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!directory || !directoryContent || !rootDirectory) {
|
||||||
|
return <DirectoryPageSkeleton />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DirectoryPageContext
|
||||||
|
value={{ rootDirectory, directory, directoryContent }}
|
||||||
|
>
|
||||||
|
<header className="flex py-2 shrink-0 items-center gap-2 border-b px-4 w-full">
|
||||||
|
<FilePathBreadcrumb />
|
||||||
|
<div className="ml-auto flex flex-row gap-2">
|
||||||
|
<EmptyTrashButton />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<DirectoryPage />
|
||||||
|
</DirectoryPageContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyTrashButton() {
|
||||||
|
return (
|
||||||
|
<Button size="sm" type="button" variant="outline">
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
Empty Trash
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user