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:
@@ -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
|
||||||
|
|||||||
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,
|
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,
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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,
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user