ultisuite-client/components/drive/sidebar-folder-tree.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

301 lines
8.6 KiB
TypeScript

"use client"
import { useEffect } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import type { LucideIcon } from "lucide-react"
import { ChevronRight, HardDrive, Users } from "lucide-react"
import { DRIVE_DROP_TARGET_CLASS } from "@/components/drive/drive-file-context-menu"
import { DriveFolderIcon } from "@/lib/drive/drive-file-icon"
import {
DRIVE_ICON_BTN,
DRIVE_SIDEBAR_CARET_SLOT_CLASS,
DRIVE_SIDEBAR_ROW_BODY_CLASS,
DRIVE_SIDEBAR_ROW_CLASS,
} from "@/lib/drive/drive-chrome-classes"
import { mailNavRowClass } from "@/lib/mail-chrome-classes"
import { cn } from "@/lib/utils"
import { useDriveRouteRoot } from "@/lib/drive/drive-route-context"
import type { DriveView } from "@/lib/drive/drive-url"
import { driveRouteBase, folderPathFromSegments } from "@/lib/drive/drive-url"
import { useDriveList, useDriveSharedWithMe } from "@/lib/api/hooks/use-drive-queries"
import type { DriveFileInfo } from "@/lib/api/types"
import { displayFileName } from "@/lib/drive/display-file-name"
import {
ancestorFolderPaths,
driveFolderHref,
isSharedRootSelected,
normalizeDriveFolderPath,
selectedFolderPath,
} from "@/lib/drive/drive-sidebar-tree"
import { useIsMobile } from "@/hooks/use-mobile"
import { useDriveDropTarget } from "@/lib/hooks/use-drive-drop-target"
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
const INDENT_PX = 16
function useFolderChildren(folderPath: string, enabled: boolean, sharedRoot: boolean) {
const list = useDriveList(folderPath, 1, "", enabled && !sharedRoot)
const shared = useDriveSharedWithMe(1, "", enabled && sharedRoot)
const active = sharedRoot ? shared : list
const directories =
active.data?.files.filter((file) => file.type === "directory") ?? []
return { directories }
}
function SidebarTreeCaret({
visible,
expanded,
onToggle,
label,
}: {
visible: boolean
expanded: boolean
onToggle: () => void
label: string
}) {
if (!visible) {
return <span className={DRIVE_SIDEBAR_CARET_SLOT_CLASS} aria-hidden="true" />
}
return (
<button
type="button"
aria-label={label}
className={cn(
DRIVE_SIDEBAR_CARET_SLOT_CLASS,
"cursor-pointer rounded-md",
DRIVE_ICON_BTN
)}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onToggle()
}}
>
<ChevronRight
className={cn(
"h-3.5 w-3.5 text-muted-foreground transition-transform",
expanded && "rotate-90"
)}
/>
</button>
)
}
function SidebarFolderNode({
folder,
depth,
view,
currentPath,
active,
routeRoot,
}: {
folder: DriveFileInfo
depth: number
view: DriveView
currentPath: string
active: boolean
routeRoot: string
}) {
const router = useRouter()
const isMobile = useIsMobile()
const folderPath = normalizeDriveFolderPath(folder.path)
const expandedPaths = useDriveUIStore((s) => s.expandedSidebarPaths)
const toggleSidebarPath = useDriveUIStore((s) => s.toggleSidebarPath)
const ensureSidebarPathsExpanded = useDriveUIStore((s) => s.ensureSidebarPathsExpanded)
const isExpanded = expandedPaths.has(folderPath)
const isSelected = active && currentPath === folderPath
const { directories } = useFolderChildren(folderPath, true, false)
const hasChildFolders = directories.length > 0
const href = driveFolderHref(view, folderPath, undefined, routeRoot)
const label = displayFileName(folder.name)
const { dropProps, canDrop, isOver } = useDriveDropTarget({
folderPath,
disabled: isMobile,
hasChildFolders,
onExpandRequest: () => {
if (!isExpanded) ensureSidebarPathsExpanded([folderPath])
},
})
return (
<div className="min-w-0">
<div
className={cn(
DRIVE_SIDEBAR_ROW_CLASS,
mailNavRowClass({ isSelected }),
isOver && canDrop && DRIVE_DROP_TARGET_CLASS
)}
style={{ paddingLeft: depth * INDENT_PX }}
{...dropProps}
>
<SidebarTreeCaret
visible={hasChildFolders}
expanded={isExpanded}
label={isExpanded ? "Replier le dossier" : "Déplier le dossier"}
onToggle={() => toggleSidebarPath(folderPath)}
/>
<Link
href={href}
className={cn(DRIVE_SIDEBAR_ROW_BODY_CLASS, "cursor-pointer")}
onClick={(event) => {
event.preventDefault()
router.push(href)
if (isMobile) useDriveUIStore.getState().setSidebarCollapsed(true)
}}
>
<DriveFolderIcon file={folder} inSharedView={view === "shared"} size="sm" />
<span className="truncate">{label}</span>
</Link>
</div>
{isExpanded && hasChildFolders
? directories.map((child) => (
<SidebarFolderNode
key={child.path}
folder={child}
depth={depth + 1}
view={view}
currentPath={currentPath}
active={active}
routeRoot={routeRoot}
/>
))
: null}
</div>
)
}
function SidebarRootBranch({
view,
rootHref,
rootLabel,
rootIcon: RootIcon,
rootKey,
pathSegments,
active,
routeRoot,
}: {
view: DriveView
rootHref: string
rootLabel: string
rootIcon: LucideIcon
rootKey: string
pathSegments: string[]
active: boolean
routeRoot: string
}) {
const router = useRouter()
const isMobile = useIsMobile()
const expandedPaths = useDriveUIStore((s) => s.expandedSidebarPaths)
const toggleSidebarPath = useDriveUIStore((s) => s.toggleSidebarPath)
const ensureSidebarPathsExpanded = useDriveUIStore((s) => s.ensureSidebarPathsExpanded)
const currentPath = active ? selectedFolderPath(view, pathSegments) : ""
const isRootSelected =
active &&
(view === "shared" ? isSharedRootSelected(view, pathSegments) : currentPath === "/")
const isExpanded = expandedPaths.has(rootKey)
const sharedRoot = view === "shared"
const { directories } = useFolderChildren("/", true, sharedRoot)
const hasChildFolders = directories.length > 0
const { dropProps, canDrop, isOver } = useDriveDropTarget({
folderPath: "/",
disabled: isMobile,
hasChildFolders,
onExpandRequest: () => {
if (!isExpanded) ensureSidebarPathsExpanded([rootKey])
},
})
useEffect(() => {
if (!active) return
ensureSidebarPathsExpanded(ancestorFolderPaths(folderPathFromSegments(pathSegments)))
ensureSidebarPathsExpanded([rootKey])
}, [active, ensureSidebarPathsExpanded, pathSegments, rootKey])
return (
<div className="min-w-0">
<div
className={cn(
DRIVE_SIDEBAR_ROW_CLASS,
mailNavRowClass({ isSelected: isRootSelected }),
isOver && canDrop && DRIVE_DROP_TARGET_CLASS
)}
{...dropProps}
>
<SidebarTreeCaret
visible={hasChildFolders}
expanded={isExpanded}
label={isExpanded ? "Replier" : "Déplier"}
onToggle={() => toggleSidebarPath(rootKey)}
/>
<Link
href={rootHref}
className={cn(DRIVE_SIDEBAR_ROW_BODY_CLASS, "cursor-pointer")}
onClick={(event) => {
event.preventDefault()
router.push(rootHref)
if (isMobile) useDriveUIStore.getState().setSidebarCollapsed(true)
}}
>
<RootIcon className="h-4 w-4 shrink-0" />
<span className="truncate">{rootLabel}</span>
</Link>
</div>
{isExpanded && hasChildFolders
? directories.map((folder) => (
<SidebarFolderNode
key={folder.path}
folder={folder}
depth={1}
view={view}
currentPath={currentPath}
active={active}
routeRoot={routeRoot}
/>
))
: null}
</div>
)
}
export function DriveSidebarFolderTree({
view,
pathSegments,
active,
}: {
view: "files" | "shared"
pathSegments: string[]
active: boolean
}) {
const routeRoot = useDriveRouteRoot()
const driveBase = driveRouteBase(routeRoot)
if (view === "files") {
return (
<SidebarRootBranch
view="files"
rootHref={driveBase}
rootLabel="Mon Drive"
rootIcon={HardDrive}
rootKey="/"
pathSegments={pathSegments}
active={active}
routeRoot={routeRoot}
/>
)
}
return (
<SidebarRootBranch
view="shared"
rootHref={`${driveBase}/shared`}
rootLabel="Partagés avec moi"
rootIcon={Users}
rootKey="/__shared_root__"
pathSegments={pathSegments}
active={active}
routeRoot={routeRoot}
/>
)
}