ultisuite-client/components/drive/drive-sidebar-org-folders.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

231 lines
7.5 KiB
TypeScript

"use client"
import { useEffect } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { Building2, ChevronRight } from "lucide-react"
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 { folderPathFromSegments } from "@/lib/drive/drive-url"
import { displayFileName } from "@/lib/drive/display-file-name"
import {
ancestorFolderPaths,
driveFolderHref,
orgRootKey,
selectedFolderPath,
} from "@/lib/drive/drive-sidebar-tree"
import { useDriveOrgFolders, useDriveOrgList } from "@/lib/api/hooks/use-drive-queries"
import type { DriveOrgFolder } from "@/lib/api/types"
import { useIsMobile } from "@/hooks/use-mobile"
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
const INDENT_PX = 16
function OrgFolderTree({
folder,
active,
pathSegments,
}: {
folder: DriveOrgFolder
active: boolean
pathSegments: 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 rootKey = orgRootKey(folder.id)
const isExpanded = expandedPaths.has(rootKey)
const currentPath = active ? selectedFolderPath("org", pathSegments) : ""
const isRootSelected = active && pathSegments.length === 0
const rootHref = driveFolderHref("org", "/", folder.id)
const list = useDriveOrgList(folder.id, "/", 1, isExpanded)
const directories = list.data?.files.filter((f) => f.type === "directory") ?? []
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 }))}>
<button
type="button"
aria-label={isExpanded ? "Replier" : "Déplier"}
className={cn(DRIVE_SIDEBAR_CARET_SLOT_CLASS, "cursor-pointer rounded-md", DRIVE_ICON_BTN)}
onClick={() => toggleSidebarPath(rootKey)}
>
<ChevronRight className={cn("h-3.5 w-3.5 text-muted-foreground transition-transform", isExpanded && "rotate-90")} />
</button>
<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)
}}
>
<Building2 className="h-4 w-4 shrink-0" />
<span className="truncate">{folder.mount_point}</span>
</Link>
</div>
{isExpanded
? directories.map((child) => (
<OrgFolderNode
key={child.path}
orgFolder={folder}
folderPath={child.path}
depth={1}
active={active}
currentPath={currentPath}
/>
))
: null}
</div>
)
}
function OrgFolderNode({
orgFolder,
folderPath,
depth,
active,
currentPath,
}: {
orgFolder: DriveOrgFolder
folderPath: string
depth: number
active: boolean
currentPath: string
}) {
const router = useRouter()
const isMobile = useIsMobile()
const expandedPaths = useDriveUIStore((s) => s.expandedSidebarPaths)
const toggleSidebarPath = useDriveUIStore((s) => s.toggleSidebarPath)
const isExpanded = expandedPaths.has(folderPath)
const isSelected = active && currentPath === folderPath
const href = driveFolderHref("org", folderPath, orgFolder.id)
const list = useDriveOrgList(orgFolder.id, folderPath, 1, isExpanded)
const directories = list.data?.files.filter((f) => f.type === "directory") ?? []
return (
<div className="min-w-0">
<div
className={cn(DRIVE_SIDEBAR_ROW_CLASS, mailNavRowClass({ isSelected }))}
style={{ paddingLeft: depth * INDENT_PX }}
>
<button
type="button"
aria-label={isExpanded ? "Replier" : "Déplier"}
className={cn(
DRIVE_SIDEBAR_CARET_SLOT_CLASS,
"cursor-pointer rounded-md",
directories.length > 0 ? DRIVE_ICON_BTN : "invisible"
)}
onClick={() => toggleSidebarPath(folderPath)}
>
<ChevronRight className={cn("h-3.5 w-3.5 text-muted-foreground transition-transform", isExpanded && "rotate-90")} />
</button>
<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={{ path: folderPath, name: displayFileName(folderPath.split("/").pop() ?? ""), type: "directory", size: 0, mime_type: "", last_modified: "", etag: "", is_favorite: false }} size="sm" />
<span className="truncate">{displayFileName(folderPath.split("/").pop() ?? folderPath)}</span>
</Link>
</div>
{isExpanded
? directories.map((child) => (
<OrgFolderNode
key={child.path}
orgFolder={orgFolder}
folderPath={child.path}
depth={depth + 1}
active={active}
currentPath={currentPath}
/>
))
: null}
</div>
)
}
export function DriveSidebarOrgFolders({
active,
pathSegments,
rootId,
}: {
active: boolean
pathSegments: string[]
rootId: string | null
}) {
const orgFolders = useDriveOrgFolders()
const folders = orgFolders.data ?? []
const expandedPaths = useDriveUIStore((s) => s.expandedSidebarPaths)
const toggleSidebarPath = useDriveUIStore((s) => s.toggleSidebarPath)
const sectionKey = "/__org_section__"
const isSectionExpanded = expandedPaths.has(sectionKey)
if (orgFolders.isLoading) {
return (
<div className="px-3 py-1.5 text-xs text-muted-foreground">Dossiers d&apos;organisation</div>
)
}
if (folders.length === 0) {
return null
}
return (
<div className="min-w-0 pb-1">
<button
type="button"
className={cn(
DRIVE_SIDEBAR_ROW_CLASS,
"w-full cursor-pointer",
mailNavRowClass({ isSelected: false })
)}
onClick={() => toggleSidebarPath(sectionKey)}
>
<span className={DRIVE_SIDEBAR_CARET_SLOT_CLASS}>
<ChevronRight
className={cn(
"h-3.5 w-3.5 text-muted-foreground transition-transform",
isSectionExpanded && "rotate-90"
)}
/>
</span>
<span className={DRIVE_SIDEBAR_ROW_BODY_CLASS}>
<Building2 className="h-4 w-4 shrink-0" />
<span className="truncate">Dossiers d&apos;organisation</span>
</span>
</button>
{isSectionExpanded
? folders.map((folder) => (
<OrgFolderTree
key={folder.id}
folder={folder}
active={active && rootId === folder.id}
pathSegments={rootId === folder.id ? pathSegments : []}
/>
))
: null}
</div>
)
}