mirror of
https://github.com/get-drexa/drive.git
synced 2025-12-01 05:51:39 +00:00
feat: new directory dialog
repalces the new item table row
This commit is contained in:
15
packages/web/src/components/with-atom.tsx
Normal file
15
packages/web/src/components/with-atom.tsx
Normal 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)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
contextMenuTargeItemAtom,
|
||||
dragInfoAtom,
|
||||
itemBeingRenamedAtom,
|
||||
newItemKindAtom,
|
||||
newFileTypeAtom,
|
||||
openedFileAtom,
|
||||
optimisticDeletedItemsAtom,
|
||||
} from "./state"
|
||||
@@ -332,7 +332,6 @@ export function DirectoryContentTableContent() {
|
||||
) : (
|
||||
<NoResultsRow />
|
||||
)}
|
||||
<NewItemRow />
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -340,10 +339,6 @@ export function DirectoryContentTableContent() {
|
||||
}
|
||||
|
||||
function NoResultsRow() {
|
||||
const newItemKind = useAtomValue(newItemKindAtom)
|
||||
if (newItemKind) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<TableRow>
|
||||
<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({
|
||||
table,
|
||||
row,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import type {
|
||||
DirectoryHandle,
|
||||
PathComponent,
|
||||
import {
|
||||
type DirectoryHandle,
|
||||
FileType,
|
||||
type PathComponent,
|
||||
} from "@fileone/convex/model/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
@@ -38,14 +39,17 @@ import {
|
||||
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 { NewDirectoryDialog } from "./new-directory-dialog"
|
||||
import { RenameFileDialog } from "./rename-file-dialog"
|
||||
import { dragInfoAtom, newItemKindAtom, openedFileAtom } from "./state"
|
||||
import { dragInfoAtom, newFileTypeAtom, openedFileAtom } from "./state"
|
||||
|
||||
export function DirectoryPage() {
|
||||
const { directory } = useContext(DirectoryPageContext)
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<RenameFileDialog />
|
||||
<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() {
|
||||
const setNewItemKind = useSetAtom(newItemKindAtom)
|
||||
const newItemKind = useAtomValue(newItemKindAtom)
|
||||
const [newFileType, setNewFileType] = useAtom(newFileTypeAtom)
|
||||
|
||||
const addNewDirectory = () => {
|
||||
setNewItemKind("directory")
|
||||
setNewFileType(FileType.Directory)
|
||||
}
|
||||
|
||||
const handleCloseAutoFocus = (event: Event) => {
|
||||
// If we just created a new item, prevent the dropdown from restoring focus to the trigger
|
||||
if (newItemKind) {
|
||||
if (newFileType) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
@@ -236,8 +252,6 @@ function PreviewDialog() {
|
||||
|
||||
if (!openedFile) return null
|
||||
|
||||
console.log("openedFile", openedFile)
|
||||
|
||||
switch (openedFile.mimeType) {
|
||||
case "image/jpeg":
|
||||
case "image/png":
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
DirectoryItem,
|
||||
DirectoryItemKind,
|
||||
} from "@fileone/convex/model/directories"
|
||||
import type { FileType } from "@fileone/convex/model/filesystem"
|
||||
import type { RowSelectionState } from "@tanstack/react-table"
|
||||
import { atom } from "jotai"
|
||||
import type { FileDragInfo } from "../../files/use-file-drop"
|
||||
@@ -14,7 +15,7 @@ export const optimisticDeletedItemsAtom = atom(
|
||||
|
||||
export const selectedFileRowsAtom = atom<RowSelectionState>({})
|
||||
|
||||
export const newItemKindAtom = atom<DirectoryItemKind | null>(null)
|
||||
export const newFileTypeAtom = atom<FileType | null>(null)
|
||||
|
||||
export const itemBeingRenamedAtom = atom<{
|
||||
kind: DirectoryItemKind
|
||||
|
||||
Reference in New Issue
Block a user