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.
267 lines
8.9 KiB
TypeScript
267 lines
8.9 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import Link from "next/link"
|
|
import { useRouter } from "next/navigation"
|
|
import { ChevronRight, Link2 } from "lucide-react"
|
|
import { Icon } from "@iconify/react"
|
|
import { DriveAddMountDialog } from "@/components/drive/drive-add-mount-dialog"
|
|
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 { displayFileName } from "@/lib/drive/display-file-name"
|
|
import { driveFolderHref, mountRootKey } from "@/lib/drive/drive-sidebar-tree"
|
|
import { useDriveMountList, useDriveMountMutations, useDriveMounts } from "@/lib/api/hooks/use-drive-queries"
|
|
import type { DriveMount } from "@/lib/api/types"
|
|
import { useIsMobile } from "@/hooks/use-mobile"
|
|
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
|
|
import { openDriveMountOAuthPopup, buildDriveMountOAuthRedirectURI } from "@/lib/drive/drive-mount-oauth"
|
|
|
|
const INDENT_PX = 16
|
|
|
|
function mountIcon(backendType: string) {
|
|
switch (backendType) {
|
|
case "googledrive":
|
|
case "google":
|
|
return "logos:google-drive"
|
|
case "dropbox":
|
|
return "logos:dropbox"
|
|
case "onedrive":
|
|
case "microsoft":
|
|
return "logos:microsoft-onedrive"
|
|
case "webdav":
|
|
case "dav":
|
|
return "mdi:cloud-sync"
|
|
default:
|
|
return "mdi:harddisk"
|
|
}
|
|
}
|
|
|
|
function MountConnectButton({ mount }: { mount: DriveMount }) {
|
|
const { fetchOAuthURL } = useDriveMountMutations()
|
|
const [pending, setPending] = useState(false)
|
|
|
|
if (!mount.needs_oauth && mount.status !== "pending_oauth") {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
"mr-1 flex h-7 shrink-0 cursor-pointer items-center gap-1 rounded-md px-2 text-[11px] text-primary",
|
|
DRIVE_ICON_BTN
|
|
)}
|
|
disabled={pending}
|
|
onClick={() => {
|
|
void (async () => {
|
|
setPending(true)
|
|
try {
|
|
const { oauth_url: oauthUrl } = await fetchOAuthURL(mount.id, buildDriveMountOAuthRedirectURI())
|
|
openDriveMountOAuthPopup(oauthUrl, mount.id)
|
|
} finally {
|
|
setPending(false)
|
|
}
|
|
})()
|
|
}}
|
|
>
|
|
<Link2 className="h-3.5 w-3.5" />
|
|
{pending ? "…" : "Connecter"}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
function MountTree({
|
|
mount,
|
|
active,
|
|
pathSegments,
|
|
}: {
|
|
mount: DriveMount
|
|
active: boolean
|
|
pathSegments: string[]
|
|
}) {
|
|
const router = useRouter()
|
|
const isMobile = useIsMobile()
|
|
const expandedPaths = useDriveUIStore((s) => s.expandedSidebarPaths)
|
|
const toggleSidebarPath = useDriveUIStore((s) => s.toggleSidebarPath)
|
|
const rootKey = mountRootKey(mount.id)
|
|
const isExpanded = expandedPaths.has(rootKey)
|
|
const isRootSelected = active && pathSegments.length === 0
|
|
const rootHref = driveFolderHref("mount", "/", mount.id)
|
|
const mountReady = mount.status === "active"
|
|
const list = useDriveMountList(mount.id, "/", 1, isExpanded && mountReady)
|
|
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: 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)}
|
|
disabled={!mountReady}
|
|
>
|
|
<ChevronRight className={cn("h-3.5 w-3.5 text-muted-foreground transition-transform", isExpanded && "rotate-90", !mountReady && "opacity-40")} />
|
|
</button>
|
|
<Link
|
|
href={rootHref}
|
|
className={cn(DRIVE_SIDEBAR_ROW_BODY_CLASS, "cursor-pointer", !mountReady && "pointer-events-none opacity-70")}
|
|
onClick={(event) => {
|
|
if (!mountReady) {
|
|
event.preventDefault()
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
router.push(rootHref)
|
|
if (isMobile) useDriveUIStore.getState().setSidebarCollapsed(true)
|
|
}}
|
|
>
|
|
<Icon icon={mountIcon(mount.backend_type)} className="h-4 w-4 shrink-0" aria-hidden />
|
|
<span className="truncate">{mount.display_name}</span>
|
|
{mount.status === "error" ? (
|
|
<span className="ml-auto shrink-0 text-[10px] text-destructive">!</span>
|
|
) : null}
|
|
</Link>
|
|
<MountConnectButton mount={mount} />
|
|
</div>
|
|
{isExpanded && mountReady
|
|
? directories.map((child) => (
|
|
<MountFolderNode
|
|
key={child.path}
|
|
mount={mount}
|
|
folderPath={child.path}
|
|
depth={1}
|
|
active={active}
|
|
currentPath={pathSegments.length ? `/${pathSegments.join("/")}` : "/"}
|
|
/>
|
|
))
|
|
: null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function MountFolderNode({
|
|
mount,
|
|
folderPath,
|
|
depth,
|
|
active,
|
|
currentPath,
|
|
}: {
|
|
mount: DriveMount
|
|
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("mount", folderPath, mount.id)
|
|
const list = useDriveMountList(mount.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"
|
|
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={(e) => { e.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) => (
|
|
<MountFolderNode key={child.path} mount={mount} folderPath={child.path} depth={depth + 1} active={active} currentPath={currentPath} />
|
|
)) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function DriveSidebarMounts({
|
|
active,
|
|
pathSegments,
|
|
rootId,
|
|
}: {
|
|
active: boolean
|
|
pathSegments: string[]
|
|
rootId: string | null
|
|
}) {
|
|
const mounts = useDriveMounts()
|
|
const items = mounts.data ?? []
|
|
|
|
if (items.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{items.map((mount) => (
|
|
<MountTree
|
|
key={mount.id}
|
|
mount={mount}
|
|
active={active && rootId === mount.id}
|
|
pathSegments={rootId === mount.id ? pathSegments : []}
|
|
/>
|
|
))}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export function DriveConnectMountAction() {
|
|
const [addOpen, setAddOpen] = useState(false)
|
|
const { invalidate } = useDriveMountMutations()
|
|
|
|
useEffect(() => {
|
|
const onMessage = (event: MessageEvent) => {
|
|
if (event.origin !== window.location.origin) return
|
|
if (event.data?.type === "drive-mount-oauth-complete") {
|
|
invalidate()
|
|
}
|
|
}
|
|
window.addEventListener("message", onMessage)
|
|
return () => window.removeEventListener("message", onMessage)
|
|
}, [invalidate])
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
"flex w-full cursor-pointer items-center gap-2 rounded-lg border border-dashed border-border px-3 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-mail-nav-hover hover:text-foreground"
|
|
)}
|
|
onClick={() => setAddOpen(true)}
|
|
>
|
|
<span className="flex shrink-0 items-center gap-0.5" aria-hidden>
|
|
<Icon icon="logos:google-drive" className="size-3.5" />
|
|
<Icon icon="logos:dropbox" className="size-3.5" />
|
|
<Icon icon="logos:microsoft-onedrive" className="size-3.5" />
|
|
</span>
|
|
<span className="min-w-0 truncate">Monter un volume</span>
|
|
</button>
|
|
<DriveAddMountDialog open={addOpen} onOpenChange={setAddOpen} />
|
|
</>
|
|
)
|
|
}
|