ultisuite-client/components/drive/public-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

192 lines
5.8 KiB
TypeScript

"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import Link from "next/link"
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 { displayFileBaseName } from "@/lib/drive/display-file-name"
import { resolvePublicShareEditReturnTo, shouldShowPublicShareEditorBack } from "@/lib/drive/public-share-url"
import type { PublicShareRootType } from "@/lib/drive/public-share-url"
import { useDriveDocumentTitle } from "@/lib/drive/use-drive-document-title"
import { getGuestEditorIdentity } from "@/lib/drive/guest-editor-identity"
import type { RichTextSessionResponse } from "@/lib/drive/richtext-types"
import type { DocPageSetup } from "@/lib/drive/doc-page-setup"
import { fetchPublicShareBlob } from "@/lib/api/public-share"
function fileNameFromPath(filePath: string, fallback?: string): string {
const base = filePath.split("/").filter(Boolean).pop()
return base || fallback || filePath
}
export function PublicRichTextEditor({
token,
filePath,
password,
returnTo,
mode = "edit",
fileDisplayName,
shareRoot,
}: {
token: string
filePath: string
password?: string
returnTo?: string | null
mode?: "edit" | "view"
fileDisplayName?: string
shareRoot?: PublicShareRootType | null
}) {
const guest = useMemo(() => getGuestEditorIdentity(token), [token])
const [session, setSession] = useState<RichTextSessionResponse | null>(null)
const [error, setError] = useState<string | null>(null)
const [resolvedMode, setResolvedMode] = useState<"edit" | "view">(mode)
const fileName = fileDisplayName || fileNameFromPath(filePath)
const title = displayFileBaseName(fileName)
useDriveDocumentTitle(title)
const backHref = useMemo(
() => resolvePublicShareEditReturnTo(token, returnTo, filePath),
[token, returnTo, filePath]
)
const showBack = shouldShowPublicShareEditorBack(shareRoot, returnTo, filePath)
useEffect(() => {
let cancelled = false
setSession(null)
setError(null)
void (async () => {
try {
const res = await fetch(
`/api/v1/drive/public/shares/${encodeURIComponent(token)}/richtext/session`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: filePath,
mode,
password: password ?? "",
guest_id: guest.guestId,
guest_name: guest.guestName,
display_name: fileName,
}),
}
)
if (!res.ok) throw new Error("Session indisponible")
const data = (await res.json()) as RichTextSessionResponse
if (!cancelled) {
setSession(data)
setResolvedMode(data.mode === "view" ? "view" : mode)
}
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : "Impossible d'ouvrir le document")
}
})()
return () => {
cancelled = true
}
}, [token, filePath, mode, password, guest.guestId, guest.guestName, fileName])
const fetchSourceBytes = useCallback(
async (path: string) => {
const blob = await fetchPublicShareBlob(
token,
{ path, name: path.split("/").pop() ?? path },
password
)
return blob.arrayBuffer()
},
[token, password]
)
const importApi = useCallback(
async (body: {
source_path: string
content: Record<string, unknown>
pageSetup?: DocPageSetup | null
}) => {
const res = await fetch(
`/api/v1/drive/public/shares/${encodeURIComponent(token)}/richtext/import`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...body, password: password ?? "", display_name: fileName }),
}
)
if (!res.ok) throw new Error("Import impossible")
},
[token, password, fileName]
)
const chrome = useMemo(
() => ({
title,
backHref,
backLabel: "Partage",
showBack,
showShare: false,
showAccount: false,
trailing:
resolvedMode === "view" ? (
<span className="text-xs text-muted-foreground">Lecture seule</span>
) : (
<span
className="rounded px-2 py-1 text-xs font-semibold text-white"
style={{ backgroundColor: guest.color }}
>
{guest.guestName}
</span>
),
}),
[title, backHref, showBack, resolvedMode, guest.color, guest.guestName]
)
const { documentLoading, documentPhase, onDocumentLoadingChange } = useDocsEditorLoadingState(
session?.roomId ?? filePath
)
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>
{showBack ? (
<Button variant="outline" asChild>
<Link href={backHref}>
<ArrowLeft className="mr-2 h-4 w-4" />
Retour
</Link>
</Button>
) : null}
</div>
)
}
return (
<DocsEditorLoadingShell
title={title}
resolvingFile={false}
awaitingSession={!session}
documentLoading={Boolean(session) && documentLoading}
documentPhase={documentPhase}
>
{session ? (
<RichTextDocumentEditor
session={session}
mode={resolvedMode}
userName={guest.guestName}
userColor={guest.color}
fetchSourceBytes={fetchSourceBytes}
importApi={importApi}
chrome={chrome}
deferSplash
onLoadingChange={onDocumentLoadingChange}
/>
) : null}
</DocsEditorLoadingShell>
)
}