ultisuite-client/components/drive/richtext-editor.tsx
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

217 lines
6.8 KiB
TypeScript

"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import Link from "next/link"
import { useQueryClient } from "@tanstack/react-query"
import { apiClient } from "@/lib/api/client"
import { Button } from "@/components/ui/button"
import { ArrowLeft } from "lucide-react"
import { RichTextDocumentEditor } from "@/components/drive/richtext-document"
import {
DocsEditorLoadingShell,
useDocsEditorLoadingState,
} from "@/components/drive/richtext/docs-editor-loading-shell"
import { useDriveFileById, useDriveMutations, useDriveShares } from "@/lib/api/hooks/use-drive-queries"
import { useAuthReady } from "@/lib/api/use-auth-ready"
import { displayFileBaseName } from "@/lib/drive/display-file-name"
import { readDriveEditorReturnTo } from "@/lib/drive/drive-editor-return"
import { resolveRenameName } from "@/lib/drive/drive-default-name"
import { driveFolderHref } from "@/lib/drive/drive-sidebar-tree"
import { 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 { RichTextSessionResponse } from "@/lib/drive/richtext-types"
import type { DriveFileInfo } from "@/lib/api/types"
import { useChromeIdentity } from "@/lib/hooks/use-chrome-identity"
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}`
}
export function RichTextEditor({ fileId }: { fileId: string }) {
const queryClient = useQueryClient()
const identity = useChromeIdentity()
const { ready } = useAuthReady()
const setSharePath = useDriveUIStore((s) => s.setSharePath)
const {
data: file,
error: fileError,
isPending: filePending,
isFetching: fileFetching,
} = useDriveFileById(fileId)
const displayPath = file?.path ?? ""
const [session, setSession] = useState<RichTextSessionResponse | null>(null)
const [sessionError, setSessionError] = useState<string | null>(null)
const [renameSignal, setRenameSignal] = useState(0)
const fileName = file?.name ?? fileNameFromPath(displayPath)
const title = displayFileBaseName(fileName)
useDriveDocumentTitle(title)
const [backHref, setBackHref] = useState("/drive")
useEffect(() => {
if (!displayPath) return
setBackHref(
resolveDriveEditReturnTo(
null,
displayPath,
(folderPath) => driveFolderHref("files", folderPath),
readDriveEditorReturnTo()
)
)
}, [displayPath])
const { data: sharesData } = useDriveShares(displayPath, Boolean(displayPath))
const { rename } = useDriveMutations()
const refreshFile = useCallback(async () => {
await queryClient.invalidateQueries({ queryKey: ["drive", "file", fileId] })
}, [fileId, queryClient])
useEffect(() => {
if (!displayPath) return
let cancelled = false
setSession(null)
setSessionError(null)
void (async () => {
try {
const res = await apiClient.post<RichTextSessionResponse>("/richtext/session", {
path: displayPath,
mode: "edit",
})
if (!cancelled) setSession(res)
} catch (e) {
if (!cancelled) {
setSessionError(e instanceof Error ? e.message : "Impossible d'ouvrir le document")
}
}
})()
return () => {
cancelled = true
}
}, [displayPath])
const handleRename = useCallback(
async (input: string) => {
if (!displayPath) return
const newName = resolveRenameName({ name: fileName, type: "file" }, input)
if (displayFileBaseName(fileName) === input.trim()) return
await rename.mutateAsync({ path: displayPath, new_name: newName })
await refreshFile()
},
[displayPath, fileName, refreshFile, rename]
)
const openShare = useCallback(() => {
if (displayPath) setSharePath(displayPath)
}, [displayPath, setSharePath])
const moveFile = useMemo((): DriveFileInfo | undefined => {
if (!file || !displayPath) return undefined
return {
...file,
path: displayPath,
name: fileName,
type: "file",
}
}, [displayPath, file, fileName])
const handleFileMoved = useCallback(
async (_newPath: string) => {
await refreshFile()
},
[refreshFile]
)
const collabUserName = identity?.name?.trim() || identity?.email || "Utilisateur"
const collabUserColor = colorForGuestId(identity?.email ?? collabUserName)
const chrome = useMemo(
() => ({
title,
onRename: handleRename,
renameDisabled: rename.isPending,
backHref,
backLabel: "Drive",
showBack: true,
shares: sharesData?.shares ?? [],
onShareClick: openShare,
showShare: true,
showAccount: true,
moveFile,
onFileMoved: handleFileMoved,
file: moveFile,
onRenameRequest: () => setRenameSignal((value) => value + 1),
renameSignal,
}),
[
title,
handleRename,
rename.isPending,
backHref,
sharesData?.shares,
openShare,
moveFile,
handleFileMoved,
renameSignal,
]
)
const resolvingFile = !ready || filePending || fileFetching
const awaitingSession = Boolean(displayPath) && !session && !sessionError
const { documentLoading, documentPhase, onDocumentLoadingChange } = useDocsEditorLoadingState(
session?.roomId ?? displayPath ?? fileId
)
const error =
fileError instanceof Error
? fileError.message
: sessionError
const errorView =
error || !file ? (
<div className="flex h-full flex-col items-center justify-center gap-4 p-8">
<p className="text-sm text-muted-foreground">
{error ?? "Document introuvable"}
</p>
<Button variant="outline" asChild>
<Link href={backHref}>
<ArrowLeft className="mr-2 h-4 w-4" />
Retour
</Link>
</Button>
</div>
) : null
return (
<DocsEditorLoadingShell
title={title || undefined}
resolvingFile={resolvingFile}
awaitingSession={awaitingSession}
documentLoading={Boolean(session) && documentLoading}
documentPhase={documentPhase}
error={!resolvingFile ? errorView : null}
>
{session && file && !error ? (
<RichTextDocumentEditor
session={session}
mode="edit"
userName={collabUserName}
userColor={collabUserColor}
chrome={chrome}
deferSplash
onLoadingChange={onDocumentLoadingChange}
/>
) : null}
</DocsEditorLoadingShell>
)
}