202 lines
6.2 KiB
TypeScript
202 lines
6.2 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, Loader2 } from "lucide-react"
|
|
import { RichTextDocumentEditor } from "@/components/drive/richtext-document"
|
|
import { useDriveFileById, useDriveMutations, useDriveShares } from "@/lib/api/hooks/use-drive-queries"
|
|
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 setSharePath = useDriveUIStore((s) => s.setSharePath)
|
|
const { data: file, error: fileError, isLoading: fileLoading } = 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 sessionReturnTo = readDriveEditorReturnTo()
|
|
const backHref = useMemo(
|
|
() =>
|
|
resolveDriveEditReturnTo(
|
|
null,
|
|
displayPath,
|
|
(folderPath) => driveFolderHref("files", folderPath),
|
|
sessionReturnTo
|
|
),
|
|
[displayPath, sessionReturnTo]
|
|
)
|
|
|
|
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 error =
|
|
fileError instanceof Error
|
|
? fileError.message
|
|
: sessionError
|
|
|
|
if (fileLoading) {
|
|
return (
|
|
<div className="flex h-dvh items-center justify-center">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error || !file) {
|
|
return (
|
|
<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>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-dvh min-h-0 flex-col">
|
|
{!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}
|
|
chrome={chrome}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|