208 lines
7.0 KiB
TypeScript
208 lines
7.0 KiB
TypeScript
"use client"
|
|
|
|
import { useMemo, useState } from "react"
|
|
import { ChevronRight, Folder } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { useDriveList, useDriveMutations } from "@/lib/api/hooks/use-drive-queries"
|
|
import type { DriveFileInfo } from "@/lib/api/types"
|
|
import { displayFileName } from "@/lib/drive/display-file-name"
|
|
import {
|
|
DRIVE_BTN_GHOST,
|
|
DRIVE_BTN_PRIMARY,
|
|
DRIVE_DIALOG_CONTENT,
|
|
DRIVE_DIALOG_DIVIDER,
|
|
DRIVE_DIALOG_FOOTER,
|
|
DRIVE_DIALOG_OVERLAY,
|
|
DRIVE_TEXT_PRIMARY,
|
|
DRIVE_TEXT_SECONDARY,
|
|
DRIVE_TEXT_TITLE,
|
|
} from "@/lib/drive/drive-dialog-styles"
|
|
import {
|
|
copyDriveItemsToFolder,
|
|
isMoveDestinationBlocked,
|
|
moveDriveItemsToFolder,
|
|
} from "@/lib/drive/drive-move-items"
|
|
import { normalizeDriveFolderPath } from "@/lib/drive/drive-sidebar-tree"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export type DriveFolderPickerMode = "move" | "copy"
|
|
|
|
export function DriveMoveDialog({
|
|
open,
|
|
onOpenChange,
|
|
sources,
|
|
onMoved,
|
|
mode = "move",
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
sources: DriveFileInfo[]
|
|
onMoved?: (destinationFolder?: string) => void
|
|
mode?: DriveFolderPickerMode
|
|
}) {
|
|
const [browsePath, setBrowsePath] = useState("/")
|
|
const mutations = useDriveMutations()
|
|
const list = useDriveList(browsePath, 1, "", open)
|
|
const sourcePaths = useMemo(() => new Set(sources.map((s) => s.path)), [sources])
|
|
const isCopy = mode === "copy"
|
|
|
|
const folders = useMemo(
|
|
() =>
|
|
(list.data?.files ?? []).filter(
|
|
(f) => f.type === "directory" && !sourcePaths.has(f.path)
|
|
),
|
|
[list.data?.files, sourcePaths]
|
|
)
|
|
|
|
const crumbs = useMemo(() => {
|
|
const normalized = normalizeDriveFolderPath(browsePath)
|
|
if (normalized === "/") return [{ path: "/", label: "Mon Drive" }]
|
|
const parts = normalized.slice(1).split("/")
|
|
const out: { path: string; label: string }[] = [{ path: "/", label: "Mon Drive" }]
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const path = "/" + parts.slice(0, i + 1).join("/")
|
|
out.push({ path, label: displayFileName(parts[i]!) })
|
|
}
|
|
return out
|
|
}, [browsePath])
|
|
|
|
const blockedDestination = isMoveDestinationBlocked(sources, browsePath)
|
|
|
|
const confirm = async () => {
|
|
if (blockedDestination) {
|
|
toast.error(
|
|
isCopy
|
|
? "Impossible de copier un dossier dans lui-même"
|
|
: "Impossible de déplacer un dossier dans lui-même"
|
|
)
|
|
return
|
|
}
|
|
try {
|
|
if (isCopy) {
|
|
await copyDriveItemsToFolder(sources, browsePath, (body) =>
|
|
mutations.copy.mutateAsync(body)
|
|
)
|
|
toast.success(sources.length > 1 ? "Éléments copiés" : "Élément copié")
|
|
} else {
|
|
await moveDriveItemsToFolder(sources, browsePath, (body) =>
|
|
mutations.move.mutateAsync(body)
|
|
)
|
|
toast.success(sources.length > 1 ? "Éléments déplacés" : "Élément déplacé")
|
|
}
|
|
onOpenChange(false)
|
|
onMoved?.(browsePath)
|
|
} catch {
|
|
toast.error(isCopy ? "Impossible de copier" : "Impossible de déplacer")
|
|
}
|
|
}
|
|
|
|
const pending = isCopy ? mutations.copy.isPending : mutations.move.isPending
|
|
const titleVerb = isCopy ? "Copier" : "Déplacer"
|
|
const countLabel = sources.length > 1 ? `${sources.length} éléments` : "l'élément"
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(next) => {
|
|
if (next) setBrowsePath("/")
|
|
onOpenChange(next)
|
|
}}
|
|
>
|
|
<DialogContent
|
|
overlayClassName={DRIVE_DIALOG_OVERLAY}
|
|
className={cn(DRIVE_DIALOG_CONTENT, "sm:max-w-[420px]")}
|
|
>
|
|
<DialogHeader className={cn("border-b px-5 py-4 text-left", DRIVE_DIALOG_DIVIDER)}>
|
|
<DialogTitle className={cn("text-base font-medium", DRIVE_TEXT_TITLE)}>
|
|
{titleVerb} {countLabel}
|
|
</DialogTitle>
|
|
<DialogDescription className="sr-only">
|
|
{isCopy
|
|
? `Choisir le dossier de destination pour copier ${countLabel}.`
|
|
: `Choisir le dossier de destination pour déplacer ${countLabel}.`}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="flex min-h-[280px] flex-col">
|
|
<div
|
|
className={cn(
|
|
"flex flex-wrap items-center gap-1 border-b px-4 py-2 text-sm",
|
|
DRIVE_DIALOG_DIVIDER
|
|
)}
|
|
>
|
|
{crumbs.map((crumb, i) => (
|
|
<span key={crumb.path} className="flex min-w-0 items-center gap-1">
|
|
{i > 0 ? (
|
|
<ChevronRight className={cn("h-3.5 w-3.5 shrink-0", DRIVE_TEXT_SECONDARY)} />
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
"truncate rounded px-1 py-0.5 hover:bg-[#f1f3f4] dark:hover:bg-[#3c4043]/50",
|
|
i === crumbs.length - 1
|
|
? cn("font-medium", DRIVE_TEXT_PRIMARY)
|
|
: DRIVE_TEXT_SECONDARY
|
|
)}
|
|
onClick={() => setBrowsePath(crumb.path)}
|
|
>
|
|
{crumb.label}
|
|
</button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="min-h-0 flex-1 overflow-y-auto py-1">
|
|
{list.isLoading ? (
|
|
<p className={cn("px-4 py-6 text-sm", DRIVE_TEXT_SECONDARY)}>Chargement…</p>
|
|
) : folders.length === 0 ? (
|
|
<p className={cn("px-4 py-6 text-sm", DRIVE_TEXT_SECONDARY)}>Aucun sous-dossier</p>
|
|
) : (
|
|
folders.map((folder) => (
|
|
<button
|
|
key={folder.path}
|
|
type="button"
|
|
className={cn(
|
|
"flex w-full items-center gap-3 px-4 py-2.5 text-left text-sm hover:bg-[#f1f3f4] dark:hover:bg-[#3c4043]/50",
|
|
DRIVE_TEXT_PRIMARY
|
|
)}
|
|
onClick={() => setBrowsePath(normalizeDriveFolderPath(folder.path))}
|
|
>
|
|
<Folder className={cn("h-4 w-4 shrink-0", DRIVE_TEXT_SECONDARY)} />
|
|
<span className="min-w-0 flex-1 truncate">{displayFileName(folder.name)}</span>
|
|
<ChevronRight className={cn("h-4 w-4 shrink-0", DRIVE_TEXT_SECONDARY)} />
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
<DialogFooter className={cn(DRIVE_DIALOG_FOOTER, "px-4 py-3")}>
|
|
<Button type="button" variant="ghost" className={DRIVE_BTN_GHOST} onClick={() => onOpenChange(false)}>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
className={DRIVE_BTN_PRIMARY}
|
|
disabled={pending || blockedDestination}
|
|
onClick={() => void confirm()}
|
|
>
|
|
{pending
|
|
? isCopy
|
|
? "Copie…"
|
|
: "Déplacement…"
|
|
: isCopy
|
|
? "Copier ici"
|
|
: "Déplacer ici"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|