ultisuite-client/components/drive/office-editor.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- Updated .env.example to include configuration for OnlyOffice Document Server.
- Modified the workspace configuration to remove the drive-suite path.
- Adjusted TypeScript environment imports for consistency.
- Enhanced Next.js configuration to disable canvas in Webpack.
- Updated package.json to include new dependencies for OnlyOffice and PDF.js.
- Added global styles for OnlyOffice theme integration in the CSS.
- Created new layout and page components for the Drive feature, including public sharing and editing functionalities.
- Updated metadata handling across various layouts to reflect the new app structure.
2026-06-07 15:49:21 +02:00

216 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { apiClient } from "@/lib/api/client"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { ArrowLeft } from "lucide-react"
import { driveFolderHref } from "@/lib/drive/drive-sidebar-tree"
import { resolveDriveEditReturnTo } from "@/lib/drive/drive-url"
type DocEditorInstance = { destroyEditor: () => void }
declare global {
interface Window {
DocsAPI?: {
DocEditor: new (id: string, config: Record<string, unknown>) => DocEditorInstance
}
DocEditor?: { instances: Record<string, DocEditorInstance | undefined> }
}
}
let docsApiLoad: Promise<void> | null = null
function loadDocsApi(documentServerUrl: string): Promise<void> {
if (window.DocsAPI) return Promise.resolve()
if (docsApiLoad) return docsApiLoad
const base = documentServerUrl.replace(/\/$/, "") + "/"
docsApiLoad = new Promise((resolve, reject) => {
const script = document.createElement("script")
script.id = "onlyoffice-docs-api"
script.src = `${base}web-apps/apps/api/documents/api.js`
script.async = true
script.onload = () => resolve()
script.onerror = () => {
docsApiLoad = null
reject(new Error(`Error load DocsAPI from ${base}`))
}
document.body.appendChild(script)
})
return docsApiLoad
}
function destroyDocEditor(id: string) {
const inst = window.DocEditor?.instances?.[id]
if (inst) {
try {
inst.destroyEditor()
} catch {
/* ignore */
}
delete window.DocEditor!.instances[id]
}
document.getElementById(id)?.replaceChildren()
}
function OnlyOfficeMount({
editorId,
documentServerUrl,
config,
onError,
}: {
editorId: string
documentServerUrl: string
config: Record<string, unknown>
onError: (message: string) => void
}) {
const configJson = JSON.stringify(config)
const onErrorRef = useRef(onError)
onErrorRef.current = onError
useEffect(() => {
let cancelled = false
const id = editorId
const parsed = JSON.parse(configJson) as Record<string, unknown>
const editorConfig: Record<string, unknown> = {
type: "desktop",
width: "100%",
height: "100%",
events: {
onDocumentReady: () => {
/* loaded */
},
onError: (event: { data?: { errorCode?: number; errorDescription?: string } }) => {
const code = event?.data?.errorCode
const desc = event?.data?.errorDescription
const msg =
desc ||
(code != null ? `OnlyOffice error ${code}` : "Erreur OnlyOffice.")
onErrorRef.current(msg)
},
},
...parsed,
}
void loadDocsApi(documentServerUrl)
.then(() => {
if (cancelled) return
if (!window.DocsAPI) throw new Error("DocsAPI is not defined")
destroyDocEditor(id)
if (!window.DocEditor) window.DocEditor = { instances: {} }
const editor = new window.DocsAPI.DocEditor(id, editorConfig)
window.DocEditor.instances[id] = editor
})
.catch((err: unknown) => {
if (!cancelled) {
onErrorRef.current(
err instanceof Error ? err.message : "Impossible de charger OnlyOffice.",
)
}
})
return () => {
cancelled = true
destroyDocEditor(id)
}
}, [editorId, documentServerUrl, configJson])
return <div id={editorId} className="h-full w-full min-h-0" />
}
export function OfficeEditor({
filePath,
returnTo,
}: {
filePath: string
returnTo?: string | null
}) {
const instanceSeq = useRef(0)
const [config, setConfig] = useState<Record<string, unknown> | null>(null)
const [serverUrl, setServerUrl] = useState("")
const [editorId, setEditorId] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const backHref = useMemo(
() =>
resolveDriveEditReturnTo(returnTo, filePath, (folderPath) =>
driveFolderHref("files", folderPath)
),
[returnTo, filePath]
)
const handleEditorError = useCallback((message: string) => {
setError(message)
}, [])
useEffect(() => {
let cancelled = false
setConfig(null)
setServerUrl("")
setEditorId(null)
setError(null)
void (async () => {
try {
const res = await apiClient.post<{
config: Record<string, unknown>
serverUrl: string
}>("/office/session", { path: filePath, mode: "edit" })
if (cancelled) return
instanceSeq.current += 1
setConfig(res.config)
setServerUrl(res.serverUrl || process.env.NEXT_PUBLIC_ONLYOFFICE_URL || "")
setEditorId(`ultidrive-editor-${instanceSeq.current}`)
} catch {
if (!cancelled) setError("Impossible de charger léditeur.")
}
})()
return () => {
cancelled = true
}
}, [filePath])
if (error) {
return (
<div className="flex h-dvh flex-col items-center justify-center gap-4">
<p className="text-destructive">{error}</p>
<Button asChild variant="outline">
<Link href={backHref}>
<ArrowLeft className="mr-2 h-4 w-4" />
Retour
</Link>
</Button>
</div>
)
}
if (!config || !editorId || !serverUrl) {
return <p className="p-8 text-center text-muted-foreground">Ouverture du document</p>
}
const docServer = serverUrl.replace(/\/$/, "")
return (
<div className="flex h-dvh flex-col">
<div className="flex h-12 shrink-0 items-center gap-2 border-b border-border px-3 ultidrive-editor-chrome">
<Button variant="ghost" size="sm" asChild>
<Link href={backHref}>
<ArrowLeft className="mr-1 h-4 w-4" />
Drive
</Link>
</Button>
<span className="truncate text-sm font-medium">{filePath.split("/").pop()}</span>
</div>
<div className="relative min-h-0 flex-1">
<OnlyOfficeMount
editorId={editorId}
documentServerUrl={docServer}
config={config}
onError={handleEditorError}
/>
</div>
</div>
)
}