ultisuite-client/components/drive/richtext/docs-drive-image-picker-dialog.tsx
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

176 lines
6.3 KiB
TypeScript

"use client"
import { useMemo, useState } from "react"
import { ChevronRight, Folder, ImageIcon, Loader2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { useDriveList } from "@/lib/api/hooks/use-drive-queries"
import { fetchDrivePreviewBlob } from "@/lib/api/drive-download"
import type { DriveFileInfo } from "@/lib/api/types"
import { displayFileName } from "@/lib/drive/display-file-name"
import { drivePreviewKind } from "@/lib/drive/drive-preview"
import {
DRIVE_BTN_GHOST,
DRIVE_DIALOG_CONTENT,
DRIVE_DIALOG_DIVIDER,
DRIVE_DIALOG_OVERLAY,
DRIVE_TEXT_PRIMARY,
DRIVE_TEXT_SECONDARY,
DRIVE_TEXT_TITLE,
} from "@/lib/drive/drive-dialog-styles"
import { normalizeDriveFolderPath } from "@/lib/drive/drive-sidebar-tree"
import { cn } from "@/lib/utils"
async function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = () => reject(reader.error)
reader.readAsDataURL(blob)
})
}
export function DocsDriveImagePickerDialog({
open,
onOpenChange,
onPickImage,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onPickImage: (src: string, file: DriveFileInfo) => void | Promise<void>
}) {
const [browsePath, setBrowsePath] = useState("/")
const [loadingPath, setLoadingPath] = useState<string | null>(null)
const list = useDriveList(browsePath, 1, "", open)
const folders = useMemo(
() => (list.data?.files ?? []).filter((f) => f.type === "directory"),
[list.data?.files]
)
const images = useMemo(
() =>
(list.data?.files ?? []).filter(
(f) => f.type === "file" && drivePreviewKind(f) === "image"
),
[list.data?.files]
)
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 pickImage = async (file: DriveFileInfo) => {
setLoadingPath(file.path)
try {
const blob = await fetchDrivePreviewBlob(file)
const src = await blobToDataUrl(blob)
await onPickImage(src, file)
onOpenChange(false)
} catch {
window.alert("Impossible de charger cette image depuis le Drive.")
} finally {
setLoadingPath(null)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
overlayClassName={DRIVE_DIALOG_OVERLAY}
className={cn(DRIVE_DIALOG_CONTENT, "flex max-h-[min(80vh,560px)] flex-col gap-0 sm:max-w-[480px]")}
>
<DialogHeader className="shrink-0 px-6 pb-3 pt-6">
<DialogTitle className={cn("text-base font-medium", DRIVE_TEXT_TITLE)}>
Image depuis Drive
</DialogTitle>
<DialogDescription className={cn("text-sm", DRIVE_TEXT_SECONDARY)}>
Choisissez une image dans votre UltiDrive.
</DialogDescription>
</DialogHeader>
<div className={cn("shrink-0 px-6 pb-2", DRIVE_DIALOG_DIVIDER)}>
<div className="flex flex-wrap items-center gap-1 text-sm">
{crumbs.map((crumb, index) => (
<span key={crumb.path} className="inline-flex items-center gap-1">
{index > 0 ? <ChevronRight className="size-3.5 text-muted-foreground" /> : null}
<Button
type="button"
variant="ghost"
size="sm"
className={cn(DRIVE_BTN_GHOST, "h-7 px-2 text-sm")}
onClick={() => setBrowsePath(crumb.path)}
>
{crumb.label}
</Button>
</span>
))}
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-2 py-2">
{list.isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
) : folders.length === 0 && images.length === 0 ? (
<p className="px-4 py-8 text-center text-sm text-muted-foreground">
Aucune image dans ce dossier.
</p>
) : (
<ul className="space-y-0.5">
{folders.map((folder) => (
<li key={folder.path}>
<button
type="button"
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-accent"
onClick={() => setBrowsePath(folder.path)}
>
<Folder className="size-4 shrink-0 text-[#5f6368]" />
<span className={cn("truncate", DRIVE_TEXT_PRIMARY)}>
{displayFileName(folder.name)}
</span>
<ChevronRight className="ml-auto size-4 shrink-0 text-muted-foreground" />
</button>
</li>
))}
{images.map((file) => (
<li key={file.path}>
<button
type="button"
disabled={loadingPath === file.path}
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-accent disabled:opacity-60"
onClick={() => void pickImage(file)}
>
<ImageIcon className="size-4 shrink-0 text-[#1967d2]" />
<span className={cn("truncate", DRIVE_TEXT_PRIMARY)}>
{displayFileName(file.name)}
</span>
{loadingPath === file.path ? (
<Loader2 className="ml-auto size-4 shrink-0 animate-spin text-muted-foreground" />
) : null}
</button>
</li>
))}
</ul>
)}
</div>
</DialogContent>
</Dialog>
)
}