- 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.
216 lines
6.0 KiB
TypeScript
216 lines
6.0 KiB
TypeScript
"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>
|
||
)
|
||
}
|