ultisuite-client/components/drive/richtext-editor.tsx
R3D347HR4Y cdff12490a
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
hocuspocus
2026-06-09 14:31:07 +02:00

177 lines
5.7 KiB
TypeScript

"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { apiClient } from "@/lib/api/client"
import { Button } from "@/components/ui/button"
import { ArrowLeft, Loader2 } from "lucide-react"
import { OfficeEditorChrome } from "@/components/drive/office-editor-chrome"
import { RichTextDocumentEditor } from "@/components/drive/richtext-document"
import { useDriveMutations, useDriveShares } from "@/lib/api/hooks/use-drive-queries"
import { displayFileBaseName } from "@/lib/drive/display-file-name"
import { resolveRenameName } from "@/lib/drive/drive-default-name"
import { driveFolderHref } from "@/lib/drive/drive-sidebar-tree"
import { buildDriveEditHref, resolveDriveEditReturnTo } from "@/lib/drive/drive-url"
import { useDriveDocumentTitle } from "@/lib/drive/use-drive-document-title"
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
import { colorForGuestId } from "@/lib/drive/guest-editor-identity"
import type { RichTextSaveStatus, RichTextSessionResponse } from "@/lib/drive/richtext-types"
import { useChromeIdentity } from "@/lib/hooks/use-chrome-identity"
import { cn } from "@/lib/utils"
function fileNameFromPath(filePath: string): string {
return filePath.split("/").filter(Boolean).pop() ?? filePath
}
function renameTargetPath(filePath: string, newName: string): string {
const parent = filePath.replace(/\/[^/]+$/, "") || "/"
const base = parent === "/" ? "" : parent
return `${base}/${newName}`.replace(/\/+/g, "/") || `/${newName}`
}
function saveStatusLabel(status: RichTextSaveStatus): string {
switch (status) {
case "saving":
return "Enregistrement…"
case "saved":
return "Enregistré"
case "error":
return "Erreur d'enregistrement"
default:
return ""
}
}
export function RichTextEditor({
filePath,
returnTo,
}: {
filePath: string
returnTo?: string | null
}) {
const router = useRouter()
const identity = useChromeIdentity()
const setSharePath = useDriveUIStore((s) => s.setSharePath)
const [session, setSession] = useState<RichTextSessionResponse | null>(null)
const [error, setError] = useState<string | null>(null)
const [saveStatus, setSaveStatus] = useState<RichTextSaveStatus>("idle")
const [displayPath, setDisplayPath] = useState(filePath)
useEffect(() => {
setDisplayPath(filePath)
}, [filePath])
const fileName = fileNameFromPath(displayPath)
const title = displayFileBaseName(fileName)
useDriveDocumentTitle(title)
const backHref = useMemo(
() =>
resolveDriveEditReturnTo(returnTo, displayPath, (folderPath) =>
driveFolderHref("files", folderPath)
),
[returnTo, displayPath]
)
const { data: sharesData } = useDriveShares(displayPath, Boolean(displayPath))
const { rename } = useDriveMutations()
useEffect(() => {
let cancelled = false
setSession(null)
setError(null)
void (async () => {
try {
const res = await apiClient.post<RichTextSessionResponse>("/richtext/session", {
path: displayPath,
mode: "edit",
})
if (!cancelled) setSession(res)
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : "Impossible d'ouvrir le document")
}
})()
return () => {
cancelled = true
}
}, [displayPath])
const handleRename = useCallback(
async (input: string) => {
const newName = resolveRenameName({ name: fileName, type: "file" }, input)
if (displayFileBaseName(fileName) === input.trim()) return
await rename.mutateAsync({ path: displayPath, new_name: newName })
const nextPath = renameTargetPath(displayPath, newName)
setDisplayPath(nextPath)
router.replace(buildDriveEditHref(nextPath, returnTo ?? undefined, "richtext"))
},
[displayPath, fileName, rename, returnTo, router]
)
const openShare = useCallback(() => {
setSharePath(displayPath)
}, [displayPath, setSharePath])
const statusText = saveStatusLabel(saveStatus)
const collabUserName = identity?.name?.trim() || identity?.email || "Utilisateur"
const collabUserColor = colorForGuestId(identity?.email ?? collabUserName)
if (error) {
return (
<div className="flex h-full flex-col items-center justify-center gap-4 p-8">
<p className="text-sm text-muted-foreground">{error}</p>
<Button variant="outline" asChild>
<Link href={backHref}>
<ArrowLeft className="mr-2 h-4 w-4" />
Retour
</Link>
</Button>
</div>
)
}
return (
<div className="flex h-full min-h-0 flex-col bg-background">
<OfficeEditorChrome
backHref={backHref}
backLabel="Drive"
title={title}
onRename={handleRename}
renameDisabled={rename.isPending}
shares={sharesData?.shares ?? []}
onShareClick={openShare}
showShare
showAccount
trailing={
statusText ? (
<span
className={cn(
"text-xs text-muted-foreground",
saveStatus === "error" && "text-destructive"
)}
>
{statusText}
</span>
) : null
}
/>
<div className="min-h-0 flex-1">
{!session ? (
<div className="flex h-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<RichTextDocumentEditor
session={session}
mode="edit"
userName={collabUserName}
userColor={collabUserColor}
onSaveStatus={setSaveStatus}
/>
)}
</div>
</div>
)
}