286 lines
9.4 KiB
TypeScript
286 lines
9.4 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 {
|
|
exportDocsContent,
|
|
type DocsDownloadFormat,
|
|
} from "@/lib/drive/docs-file-menu-export"
|
|
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,
|
|
onPageSetupApply,
|
|
onShareClick,
|
|
onRenameRequest,
|
|
onFileMoved,
|
|
onPurgeSidecarAndReimport,
|
|
disabled,
|
|
}: {
|
|
file?: DriveFileInfo
|
|
editor: Editor | null
|
|
pageSetup: DocPageSetup | null
|
|
fallbackFormatId: PageFormatId
|
|
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.name, file.path, 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.path, mutations.deleteFile, router])
|
|
|
|
const downloadFormat = useCallback(
|
|
async (format: DocsDownloadFormat) => {
|
|
if (!file) return
|
|
if (format === "pdf") {
|
|
window.print()
|
|
return
|
|
}
|
|
|
|
if (format === "html-zip" || format === "odt" || format === "rtf" || format === "epub") {
|
|
toast.info("Export bientôt disponible pour ce format")
|
|
return
|
|
}
|
|
|
|
try {
|
|
const result = await exportDocsContent(format, editor, file.name)
|
|
if (result === "unsupported") {
|
|
toast.error("Export indisponible")
|
|
}
|
|
} catch {
|
|
toast.error("Échec de l'export")
|
|
}
|
|
},
|
|
[editor, file.name]
|
|
)
|
|
|
|
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: () => window.print(),
|
|
...(onPurgeSidecarAndReimport
|
|
? { onPurgeSidecarAndReimport: () => void onPurgeSidecarAndReimport() }
|
|
: {}),
|
|
}),
|
|
[
|
|
downloadFormat,
|
|
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()
|
|
window.print()
|
|
}
|
|
}
|
|
|
|
window.addEventListener("keydown", onKeyDown)
|
|
return () => window.removeEventListener("keydown", onKeyDown)
|
|
}, [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,
|
|
}
|
|
}
|