Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
301 lines
8.6 KiB
TypeScript
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}
|
|
/>
|
|
)
|
|
}
|