feat: new directory dialog

repalces the new item table row
This commit is contained in:
2025-09-26 23:04:02 +00:00
parent ce2e3c4f26
commit 3dfcdd84cf
6 changed files with 119 additions and 119 deletions

View File

@@ -1,5 +1,10 @@
import type { Id } from "../_generated/dataModel" import type { Id } from "../_generated/dataModel"
export enum FileType {
File = "File",
Directory = "Directory",
}
export type DirectoryPathComponent = { export type DirectoryPathComponent = {
handle: DirectoryHandle handle: DirectoryHandle
name: string name: string

View File

@@ -0,0 +1,15 @@
import { type PrimitiveAtom, useAtom } from "jotai"
export function WithAtom<Value>({
atom,
children,
}: {
atom: PrimitiveAtom<Value>
children: (
value: Value,
setValue: (value: Value) => void,
) => React.ReactNode
}) {
const [value, setValue] = useAtom(atom)
return children(value, setValue)
}

View File

@@ -53,7 +53,7 @@ import {
contextMenuTargeItemAtom, contextMenuTargeItemAtom,
dragInfoAtom, dragInfoAtom,
itemBeingRenamedAtom, itemBeingRenamedAtom,
newItemKindAtom, newFileTypeAtom,
openedFileAtom, openedFileAtom,
optimisticDeletedItemsAtom, optimisticDeletedItemsAtom,
} from "./state" } from "./state"
@@ -332,7 +332,6 @@ export function DirectoryContentTableContent() {
) : ( ) : (
<NoResultsRow /> <NoResultsRow />
)} )}
<NewItemRow />
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
@@ -340,10 +339,6 @@ export function DirectoryContentTableContent() {
} }
function NoResultsRow() { function NoResultsRow() {
const newItemKind = useAtomValue(newItemKindAtom)
if (newItemKind) {
return null
}
return ( return (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="text-center"> <TableCell colSpan={columns.length} className="text-center">
@@ -353,108 +348,6 @@ function NoResultsRow() {
) )
} }
function NewItemRow() {
const { directory } = useContext(DirectoryPageContext)
const inputRef = useRef<HTMLInputElement>(null)
const newItemFormId = useId()
const [newItemKind, setNewItemKind] = useAtom(newItemKindAtom)
const { mutate: createDirectory, isPending } = useMutation({
mutationFn: useContextMutation(api.files.createDirectory),
onSuccess: () => {
setNewItemKind(null)
},
onError: withDefaultOnError(() => {
setTimeout(() => {
inputRef.current?.focus()
}, 1)
}),
})
// Auto-focus the input when newItemKind changes to a truthy value
useEffect(() => {
if (newItemKind && inputRef.current) {
// Use requestAnimationFrame to ensure the component is fully rendered
// and the dropdown has completed its close cycle
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
inputRef.current.select() // Also select the default text for better UX
}
})
}
}, [newItemKind])
if (!newItemKind) {
return null
}
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const itemName = formData.get("itemName") as string
if (itemName) {
createDirectory({ name: itemName, directoryId: directory._id })
} else {
toast.error("Please enter a name.")
}
}
const clearNewItemKind = () => {
// setItemBeingAdded(null)
setNewItemKind(null)
}
return (
<TableRow className={cn("align-middle", { "opacity-50": isPending })}>
<TableCell />
<TableCell className="p-0">
<div className="flex items-center gap-2 px-2 py-1 h-full">
{isPending ? (
<LoadingSpinner className="size-4" />
) : (
<DirectoryIcon className="size-4" />
)}
<form
className="w-full"
id={newItemFormId}
onSubmit={onSubmit}
>
<input
ref={inputRef}
type="text"
name="itemName"
defaultValue={newItemKind}
disabled={isPending}
className="w-full h-8 px-2 bg-transparent border border-input rounded-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary"
/>
</form>
</div>
</TableCell>
<TableCell />
<TableCell align="right" className="space-x-2 p-1">
{!isPending ? (
<>
<Button
type="button"
form={newItemFormId}
variant="ghost"
size="icon"
onClick={clearNewItemKind}
>
<XIcon />
</Button>
<Button type="submit" form={newItemFormId} size="icon">
<CheckIcon />
</Button>
</>
) : null}
</TableCell>
</TableRow>
)
}
function FileItemRow({ function FileItemRow({
table, table,
row, row,

View File

@@ -1,7 +1,8 @@
import { api } from "@fileone/convex/_generated/api" import { api } from "@fileone/convex/_generated/api"
import type { import {
DirectoryHandle, type DirectoryHandle,
PathComponent, FileType,
type PathComponent,
} from "@fileone/convex/model/filesystem" } from "@fileone/convex/model/filesystem"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
@@ -38,14 +39,17 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "../../components/ui/tooltip" } from "../../components/ui/tooltip"
import { WithAtom } from "../../components/with-atom"
import { useFileDrop } from "../../files/use-file-drop" import { useFileDrop } from "../../files/use-file-drop"
import { cn } from "../../lib/utils" import { cn } from "../../lib/utils"
import { DirectoryPageContext } from "./context" import { DirectoryPageContext } from "./context"
import { DirectoryContentTable } from "./directory-content-table" import { DirectoryContentTable } from "./directory-content-table"
import { NewDirectoryDialog } from "./new-directory-dialog"
import { RenameFileDialog } from "./rename-file-dialog" import { RenameFileDialog } from "./rename-file-dialog"
import { dragInfoAtom, newItemKindAtom, openedFileAtom } from "./state" 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"> <header className="flex py-1 shrink-0 items-center gap-2 border-b px-4 w-full">
@@ -60,6 +64,19 @@ export function DirectoryPage() {
</div> </div>
<RenameFileDialog /> <RenameFileDialog />
<PreviewDialog /> <PreviewDialog />
<WithAtom atom={newFileTypeAtom}>
{(newFileType, setNewFileType) => (
<NewDirectoryDialog
open={newFileType === FileType.Directory}
directoryId={directory._id}
onOpenChange={(open) => {
if (!open) {
setNewFileType(null)
}
}}
/>
)}
</WithAtom>
</> </>
) )
} }
@@ -194,16 +211,15 @@ function UploadFileButton() {
} }
function NewDirectoryItemDropdown() { function NewDirectoryItemDropdown() {
const setNewItemKind = useSetAtom(newItemKindAtom) const [newFileType, setNewFileType] = useAtom(newFileTypeAtom)
const newItemKind = useAtomValue(newItemKindAtom)
const addNewDirectory = () => { const addNewDirectory = () => {
setNewItemKind("directory") setNewFileType(FileType.Directory)
} }
const handleCloseAutoFocus = (event: Event) => { const handleCloseAutoFocus = (event: Event) => {
// If we just created a new item, prevent the dropdown from restoring focus to the trigger // If we just created a new item, prevent the dropdown from restoring focus to the trigger
if (newItemKind) { if (newFileType) {
event.preventDefault() event.preventDefault()
} }
} }
@@ -236,8 +252,6 @@ function PreviewDialog() {
if (!openedFile) return null if (!openedFile) return null
console.log("openedFile", openedFile)
switch (openedFile.mimeType) { switch (openedFile.mimeType) {
case "image/jpeg": case "image/jpeg":
case "image/png": case "image/png":

View File

@@ -0,0 +1,72 @@
import { api } from "@fileone/convex/_generated/api"
import type { Id } from "@fileone/convex/_generated/dataModel"
import { useMutation } from "@tanstack/react-query"
import { useMutation as useContextMutation } from "convex/react"
import { useId } from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
export function NewDirectoryDialog({
open,
onOpenChange,
directoryId,
}: {
open: boolean
onOpenChange: (open: boolean) => void
directoryId: Id<"directories">
}) {
const formId = useId()
const { mutate: createDirectory, isPending: isCreating } = useMutation({
mutationFn: useContextMutation(api.files.createDirectory),
onSuccess: () => {
onOpenChange(false)
toast.success("Directory created successfully")
},
})
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const name = formData.get("directoryName") as string
if (name) {
createDirectory({ name, directoryId })
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>New Directory</DialogTitle>
</DialogHeader>
<form id={formId} onSubmit={onSubmit}>
<Input name="directoryName" />
</form>
<DialogFooter>
<DialogClose asChild>
<Button loading={isCreating} variant="outline">
<span>Cancel</span>
</Button>
</DialogClose>
<Button loading={isCreating} type="submit" form={formId}>
<span>Create</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -3,6 +3,7 @@ import type {
DirectoryItem, DirectoryItem,
DirectoryItemKind, DirectoryItemKind,
} from "@fileone/convex/model/directories" } 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"
@@ -14,7 +15,7 @@ export const optimisticDeletedItemsAtom = atom(
export const selectedFileRowsAtom = atom<RowSelectionState>({}) export const selectedFileRowsAtom = atom<RowSelectionState>({})
export const newItemKindAtom = atom<DirectoryItemKind | null>(null) export const newFileTypeAtom = atom<FileType | null>(null)
export const itemBeingRenamedAtom = atom<{ export const itemBeingRenamedAtom = atom<{
kind: DirectoryItemKind kind: DirectoryItemKind