ultisuite-client/components/drive/breadcrumb-folder-menu.tsx
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

252 lines
7.9 KiB
TypeScript

"use client"
import { useCallback, useMemo, useState } from "react"
import { useRouter } from "next/navigation"
import { MoreVertical } from "lucide-react"
import { toast } from "sonner"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ui/sheet"
import { DriveMoveDialog, type DriveFolderPickerMode } from "@/components/drive/drive-move-dialog"
import { DriveNameDialog } from "@/components/drive/drive-name-dialog"
import { DriveFileMenuActions } from "@/components/drive/drive-file-menu-actions"
import { DRIVE_MENU_SURFACE_CLASS } from "@/components/drive/drive-file-context-menu"
import { useDriveMutations } from "@/lib/api/hooks/use-drive-queries"
import type { DriveFileInfo } from "@/lib/api/types"
import type { DriveView } from "@/lib/drive/drive-url"
import { useDriveRouteRoot } from "@/lib/drive/drive-route-context"
import { buildDriveFolderHref } from "@/lib/drive/drive-url"
import { displayFileName } from "@/lib/drive/display-file-name"
import { resolveRenameName } from "@/lib/drive/drive-default-name"
import { guardDriveMenuPointer, stopDriveMenuBubble } from "@/lib/drive/drive-menu-guard"
import { useIsMobile } from "@/hooks/use-mobile"
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
import { DRIVE_MENU_BTN, DRIVE_MENU_BTN_ACTIVE } from "@/lib/drive/drive-chrome-classes"
import { cn } from "@/lib/utils"
function breadcrumbFolderTarget(folderPath: string, name: string): DriveFileInfo {
return {
path: folderPath,
name,
type: "directory",
size: 0,
mime_type: "httpd/unix-directory",
last_modified: "",
etag: "",
is_favorite: false,
}
}
export function BreadcrumbFolderMenu({
view,
segments,
folderPath,
writable = true,
allowShare = true,
renameOpen,
onRenameOpenChange,
className,
}: {
view: Extract<DriveView, "files" | "shared" | "org" | "mount">
segments: string[]
folderPath: string
writable?: boolean
allowShare?: boolean
renameOpen?: boolean
onRenameOpenChange?: (open: boolean) => void
className?: string
}) {
const isMobile = useIsMobile()
const router = useRouter()
const routeRoot = useDriveRouteRoot()
const [dropdownOpen, setDropdownOpen] = useState(false)
const [sheetOpen, setSheetOpen] = useState(false)
const [internalRenameOpen, setInternalRenameOpen] = useState(false)
const [folderPickerMode, setFolderPickerMode] = useState<DriveFolderPickerMode | null>(null)
const setSharePath = useDriveUIStore((s) => s.setSharePath)
const mutations = useDriveMutations()
const folderName = segments[segments.length - 1] ?? ""
const folder = useMemo(
() => breadcrumbFolderTarget(folderPath, folderName),
[folderPath, folderName]
)
const targets = useMemo(() => [folder], [folder])
const label = displayFileName(folderName)
const renameControlled = onRenameOpenChange != null
const renameDialogOpen = renameControlled ? (renameOpen ?? false) : internalRenameOpen
const setRenameDialogOpen = renameControlled ? onRenameOpenChange : setInternalRenameOpen
const handleRename = async (input: string) => {
const newName = resolveRenameName(folder, input)
if (displayFileName(folder.name) === newName) return
try {
await mutations.rename.mutateAsync({ path: folder.path, new_name: newName })
toast.success("Dossier renommé")
const parentSegments = segments.slice(0, -1)
router.push(
buildDriveFolderHref(view, [...parentSegments, newName], undefined, routeRoot)
)
} catch {
toast.error("Impossible de renommer ce dossier")
throw new Error("rename failed")
}
}
const closeDropdown = useCallback(() => {
setDropdownOpen(false)
guardDriveMenuPointer()
}, [])
const openRenameDialog = useCallback(() => {
guardDriveMenuPointer()
window.setTimeout(() => setRenameDialogOpen(true), 0)
}, [setRenameDialogOpen])
const openFolderPicker = (mode: DriveFolderPickerMode) => {
setSheetOpen(false)
closeDropdown()
window.setTimeout(() => setFolderPickerMode(mode), 0)
}
const menuActionsProps = {
targets,
writable,
allowShare,
hideOpen: true,
onOpen: () => {},
setSharePath,
mutations,
onRenameRequest: openRenameDialog,
onMoveRequest: writable ? () => openFolderPicker("move") : undefined,
onCopyRequest: writable ? () => openFolderPicker("copy") : undefined,
}
const dialogs = (
<>
<DriveNameDialog
open={renameDialogOpen}
onOpenChange={setRenameDialogOpen}
title="Renommer le dossier"
defaultValue={label}
confirmLabel="Renommer"
onConfirm={handleRename}
/>
<DriveMoveDialog
open={folderPickerMode !== null}
onOpenChange={(next) => {
if (!next) setFolderPickerMode(null)
}}
mode={folderPickerMode ?? "move"}
sources={targets}
/>
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent
side="bottom"
hideClose
className="gap-0 overflow-hidden rounded-t-2xl border-border px-0 pb-[max(1rem,env(safe-area-inset-bottom))] pt-2"
>
<SheetTitle className="sr-only">Actions pour {label}</SheetTitle>
<SheetDescription className="sr-only">
Actions disponibles pour {label}.
</SheetDescription>
<p className="truncate px-4 pb-2 text-sm font-medium text-muted-foreground">
{label}
</p>
<div className="flex flex-col border-t border-border">
<DriveFileMenuActions
variant="sheet"
{...menuActionsProps}
onClose={() => setSheetOpen(false)}
onRenameRequest={() => {
setSheetOpen(false)
openRenameDialog()
}}
/>
</div>
</SheetContent>
</Sheet>
</>
)
if (isMobile) {
return (
<>
<button
type="button"
data-drive-menu-btn
aria-label={`Actions pour ${label}`}
aria-expanded={sheetOpen}
aria-haspopup="dialog"
className={cn(DRIVE_MENU_BTN, className)}
onClick={(e) => {
stopDriveMenuBubble(e)
setSheetOpen(true)
}}
onPointerDown={(e) => stopDriveMenuBubble(e)}
>
<MoreVertical className="h-4 w-4" aria-hidden />
</button>
{dialogs}
</>
)
}
return (
<>
<DropdownMenu
modal
open={dropdownOpen}
onOpenChange={(next) => {
if (next) {
setDropdownOpen(true)
return
}
closeDropdown()
}}
>
<DropdownMenuTrigger asChild>
<button
type="button"
data-drive-menu-btn
aria-label={`Actions pour ${label}`}
aria-haspopup="menu"
className={cn(
DRIVE_MENU_BTN,
dropdownOpen && DRIVE_MENU_BTN_ACTIVE,
className
)}
onClick={(e) => stopDriveMenuBubble(e)}
onPointerDown={(e) => stopDriveMenuBubble(e)}
>
<MoreVertical className="h-4 w-4" aria-hidden />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
data-drive-menu-surface
className={cn(DRIVE_MENU_SURFACE_CLASS, "w-52")}
onCloseAutoFocus={(e) => e.preventDefault()}
onPointerDown={(e) => stopDriveMenuBubble(e)}
onClick={(e) => e.stopPropagation()}
>
<DriveFileMenuActions
variant="dropdown"
{...menuActionsProps}
onClose={closeDropdown}
onRenameRequest={() => {
closeDropdown()
openRenameDialog()
}}
/>
</DropdownMenuContent>
</DropdownMenu>
{dialogs}
</>
)
}