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

308 lines
10 KiB
TypeScript

"use client"
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"
import { useRouter } from "next/navigation"
import type { Editor } from "@tiptap/react"
import { toast } from "sonner"
import { DocsDetailsDialog } from "@/components/drive/richtext/docs-details-dialog"
import { DocsOpenDialog } from "@/components/drive/richtext/docs-open-dialog"
import { DocsPageSetupDialog } from "@/components/drive/richtext/docs-page-setup-dialog"
import type { DocsFileMenuActions } from "@/components/drive/richtext/docs-file-menu"
import { DriveMoveDialog } from "@/components/drive/drive-move-dialog"
import { DriveNameDialog } from "@/components/drive/drive-name-dialog"
import { apiClient } from "@/lib/api/client"
import { useDriveList, useDriveMutations } from "@/lib/api/hooks/use-drive-queries"
import type { DriveFileInfo } from "@/lib/api/types"
import { displayFileName } from "@/lib/drive/display-file-name"
import { nextUntitledName } from "@/lib/drive/drive-default-name"
import { buildMoveDestination, parentFolderPath } from "@/lib/drive/drive-move-items"
import { buildDriveDocsEditHref } from "@/lib/drive/drive-url"
import type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot"
import {
exportDocsContent,
type DocsDownloadFormat,
} from "@/lib/drive/docs-file-menu-export"
import { printDocsDocument } from "@/lib/drive/docs-print"
import { openRichTextDocument } from "@/lib/drive/open-rich-text-document"
import type { PageFormatId } from "@/lib/drive/page-formats"
import type { DocPageSetup } from "@/lib/drive/doc-page-setup"
import { useDocsKeyboardShortcutsStore } from "@/lib/stores/docs-keyboard-shortcuts-store"
function buildCopyFileName(originalName: string, siblingNames: string[]): string {
const name = displayFileName(originalName)
const dot = name.lastIndexOf(".")
const base = dot > 0 ? name.slice(0, dot) : name
const ext = dot > 0 ? name.slice(dot) : ""
const siblings = siblingNames.map(displayFileName)
let candidate = `Copie de ${base}${ext}`
if (!siblings.includes(candidate)) return candidate
let n = 2
while (siblings.includes(`Copie de ${base} (${n})${ext}`)) {
n += 1
}
return `Copie de ${base} (${n})${ext}`
}
export function useDocsFileMenu({
file,
editor,
pageSetup,
fallbackFormatId,
getExportSnapshot,
onPageSetupApply,
onShareClick,
onRenameRequest,
onFileMoved,
onPurgeSidecarAndReimport,
disabled,
}: {
file?: DriveFileInfo
editor: Editor | null
pageSetup: DocPageSetup | null
fallbackFormatId: PageFormatId
getExportSnapshot: () => DocsExportSnapshot | null
onPageSetupApply: (setup: DocPageSetup) => void
onShareClick?: () => void
onRenameRequest?: () => void
onFileMoved?: (newPath: string) => void
onPurgeSidecarAndReimport?: () => void
disabled?: boolean
}) {
const router = useRouter()
const mutations = useDriveMutations()
const parentPath = file ? parentFolderPath(file.path) : "/"
const siblingList = useDriveList(parentPath, 1, "", false)
const siblingNames = useMemo(
() => (siblingList.data?.files ?? []).map((item) => item.name),
[siblingList.data?.files]
)
const [openDialogOpen, setOpenDialogOpen] = useState(false)
const [moveDialogOpen, setMoveDialogOpen] = useState(false)
const [newDocDialogOpen, setNewDocDialogOpen] = useState(false)
const [pageSetupOpen, setPageSetupOpen] = useState(false)
const [detailsOpen, setDetailsOpen] = useState(false)
const [newDocDefaultName, setNewDocDefaultName] = useState("")
const navigateToFile = useCallback(
async (path: string) => {
const info = await apiClient.get<DriveFileInfo>(
`/drive/files/info${path.startsWith("/") ? path : `/${path}`}`
)
if (!info.file_id) {
throw new Error("Identifiant du document introuvable")
}
router.push(buildDriveDocsEditHref(info.file_id))
},
[router]
)
const createDocument = useCallback(
async (rawName: string) => {
const name = rawName.trim().replace(/\//g, "")
if (!name) return
const fileName = name.endsWith(".docx") ? name : `${name}.docx`
try {
const { path } = await mutations.createFile.mutateAsync({
parent_path: parentPath,
name: fileName,
kind: "document",
})
toast.success("Document créé")
await navigateToFile(path)
} catch {
toast.error("Impossible de créer le document")
throw new Error("create failed")
}
},
[mutations.createFile, navigateToFile, parentPath]
)
const makeCopy = useCallback(async () => {
if (!file) return
const copyName = buildCopyFileName(file.name, siblingNames)
const destination = buildMoveDestination(parentPath, copyName)
try {
await mutations.copy.mutateAsync({ source: file.path, destination })
toast.success("Copie créée")
await navigateToFile(destination)
} catch {
toast.error("Impossible de créer la copie")
}
}, [file, mutations.copy, navigateToFile, parentPath, siblingNames])
const moveToTrash = useCallback(async () => {
if (!file) return
try {
await mutations.deleteFile.mutateAsync(file.path)
toast.success("Document déplacé dans la corbeille")
router.push("/drive")
} catch {
toast.error("Impossible de déplacer dans la corbeille")
}
}, [file, mutations.deleteFile, router])
const downloadFormat = useCallback(
async (format: DocsDownloadFormat) => {
if (!file) return
if (format === "html-zip" || format === "odt" || format === "rtf" || format === "epub") {
toast.info("Export bientôt disponible pour ce format")
return
}
const snapshot = getExportSnapshot()
const loadingToast =
format === "pdf" ? toast.loading("Génération du PDF…") : undefined
try {
const result = await exportDocsContent(format, snapshot, editor, file.name)
if (result === "unsupported") {
toast.error("Export indisponible")
} else if (format === "pdf") {
toast.success("PDF téléchargé")
}
} catch {
toast.error("Échec de l'export")
} finally {
if (loadingToast != null) toast.dismiss(loadingToast)
}
},
[editor, file, getExportSnapshot]
)
const handlePrint = useCallback(async () => {
const snapshot = getExportSnapshot()
if (!snapshot) {
window.print()
return
}
try {
await printDocsDocument(snapshot)
} catch {
toast.error("Impossible d'imprimer le document")
}
}, [getExportSnapshot])
const soon = useCallback((label: string) => {
toast.info(`${label} — bientôt disponible`)
}, [])
const menuDisabled = disabled || !file
const actions = useMemo<DocsFileMenuActions>(
() => ({
onNewDocument: () => {
setNewDocDefaultName(nextUntitledName(siblingNames, "Document", ".docx"))
setNewDocDialogOpen(true)
},
onNewFromTemplate: () => soon("Galerie de modèles"),
onOpen: () => setOpenDialogOpen(true),
onMakeCopy: () => void makeCopy(),
onShareWithUsers: () => onShareClick?.(),
onPublishToWeb: () => onShareClick?.(),
onEmailFile: () => soon("Envoi par e-mail"),
onEmailCollaborators: () => soon("Envoi aux collaborateurs"),
onEmailDraft: () => soon("Brouillon d'e-mail"),
onDownload: (format) => void downloadFormat(format),
onRename: () => onRenameRequest?.(),
onMove: () => setMoveDialogOpen(true),
onAddShortcut: () => soon("Raccourci Drive"),
onMoveToTrash: () => void moveToTrash(),
onNameCurrentVersion: () => soon("Nommer la version actuelle"),
onShowVersionHistory: () => soon("Historique des versions"),
onToggleOffline: () => soon("Disponible hors connexion"),
onDetails: () => setDetailsOpen(true),
onSecurityLimits: () => soon("Limites de sécurité"),
onPageSetup: () => setPageSetupOpen(true),
onPrint: () => void handlePrint(),
...(onPurgeSidecarAndReimport
? { onPurgeSidecarAndReimport: () => void onPurgeSidecarAndReimport() }
: {}),
}),
[
downloadFormat,
handlePrint,
makeCopy,
moveToTrash,
onPurgeSidecarAndReimport,
onRenameRequest,
onShareClick,
siblingNames,
soon,
]
)
useEffect(() => {
if (menuDisabled) return
const onKeyDown = (event: KeyboardEvent) => {
const id = useDocsKeyboardShortcutsStore.getState().matchEvent(
event,
(definition) => definition.scope === "document" && definition.handler === "custom"
)
if (id === "file.open") {
event.preventDefault()
setOpenDialogOpen(true)
return
}
if (id === "file.print") {
event.preventDefault()
void handlePrint()
}
}
window.addEventListener("keydown", onKeyDown)
return () => window.removeEventListener("keydown", onKeyDown)
}, [handlePrint, menuDisabled])
const dialogs: ReactNode = file ? (
<>
<DocsOpenDialog
open={openDialogOpen}
onOpenChange={setOpenDialogOpen}
onOpenFile={(target) =>
openRichTextDocument(target, {
push: (href) => router.push(href),
})
}
/>
<DriveMoveDialog
open={moveDialogOpen}
onOpenChange={setMoveDialogOpen}
sources={[file]}
mode="move"
onMoved={(destinationFolder) => {
if (!destinationFolder) return
onFileMoved?.(buildMoveDestination(destinationFolder, file.name))
}}
/>
<DriveNameDialog
open={newDocDialogOpen}
onOpenChange={setNewDocDialogOpen}
title="Nouveau document"
description="Nom du document à créer dans le dossier actuel."
defaultValue={newDocDefaultName}
confirmLabel="Créer"
onConfirm={createDocument}
/>
<DocsPageSetupDialog
open={pageSetupOpen}
onOpenChange={setPageSetupOpen}
pageSetup={pageSetup}
fallbackFormatId={fallbackFormatId}
onApply={onPageSetupApply}
/>
<DocsDetailsDialog open={detailsOpen} onOpenChange={setDetailsOpen} file={file} />
</>
) : null
return {
actions,
dialogs,
disabled: menuDisabled,
}
}