- 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.
248 lines
7.7 KiB
TypeScript
248 lines
7.7 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useMemo, useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import { MoreVertical } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ui/sheet"
|
|
import { DriveMoveDialog, type DriveFolderPickerMode } from "@/components/drive/drive-move-dialog"
|
|
import { DriveNameDialog } from "@/components/drive/drive-name-dialog"
|
|
import { DriveFileMenuActions } from "@/components/drive/drive-file-menu-actions"
|
|
import { DRIVE_MENU_SURFACE_CLASS } from "@/components/drive/drive-file-context-menu"
|
|
import { useDriveMutations } from "@/lib/api/hooks/use-drive-queries"
|
|
import type { DriveFileInfo } from "@/lib/api/types"
|
|
import type { DriveView } from "@/lib/drive/drive-url"
|
|
import { buildDriveFolderHref } from "@/lib/drive/drive-url"
|
|
import { displayFileName } from "@/lib/drive/display-file-name"
|
|
import { resolveRenameName } from "@/lib/drive/drive-default-name"
|
|
import { guardDriveMenuPointer, stopDriveMenuBubble } from "@/lib/drive/drive-menu-guard"
|
|
import { useIsMobile } from "@/hooks/use-mobile"
|
|
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
|
|
import { DRIVE_MENU_BTN, DRIVE_MENU_BTN_ACTIVE } from "@/lib/drive/drive-chrome-classes"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
function breadcrumbFolderTarget(folderPath: string, name: string): DriveFileInfo {
|
|
return {
|
|
path: folderPath,
|
|
name,
|
|
type: "directory",
|
|
size: 0,
|
|
mime_type: "httpd/unix-directory",
|
|
last_modified: "",
|
|
etag: "",
|
|
is_favorite: false,
|
|
}
|
|
}
|
|
|
|
export function BreadcrumbFolderMenu({
|
|
view,
|
|
segments,
|
|
folderPath,
|
|
writable = true,
|
|
allowShare = true,
|
|
renameOpen,
|
|
onRenameOpenChange,
|
|
className,
|
|
}: {
|
|
view: Extract<DriveView, "files" | "shared">
|
|
segments: string[]
|
|
folderPath: string
|
|
writable?: boolean
|
|
allowShare?: boolean
|
|
renameOpen?: boolean
|
|
onRenameOpenChange?: (open: boolean) => void
|
|
className?: string
|
|
}) {
|
|
const isMobile = useIsMobile()
|
|
const router = useRouter()
|
|
const [dropdownOpen, setDropdownOpen] = useState(false)
|
|
const [sheetOpen, setSheetOpen] = useState(false)
|
|
const [internalRenameOpen, setInternalRenameOpen] = useState(false)
|
|
const [folderPickerMode, setFolderPickerMode] = useState<DriveFolderPickerMode | null>(null)
|
|
const setSharePath = useDriveUIStore((s) => s.setSharePath)
|
|
const mutations = useDriveMutations()
|
|
|
|
const folderName = segments[segments.length - 1] ?? ""
|
|
const folder = useMemo(
|
|
() => breadcrumbFolderTarget(folderPath, folderName),
|
|
[folderPath, folderName]
|
|
)
|
|
const targets = useMemo(() => [folder], [folder])
|
|
const label = displayFileName(folderName)
|
|
|
|
const renameControlled = onRenameOpenChange != null
|
|
const renameDialogOpen = renameControlled ? (renameOpen ?? false) : internalRenameOpen
|
|
const setRenameDialogOpen = renameControlled ? onRenameOpenChange : setInternalRenameOpen
|
|
|
|
const handleRename = async (input: string) => {
|
|
const newName = resolveRenameName(folder, input)
|
|
if (displayFileName(folder.name) === newName) return
|
|
try {
|
|
await mutations.rename.mutateAsync({ path: folder.path, new_name: newName })
|
|
toast.success("Dossier renommé")
|
|
const parentSegments = segments.slice(0, -1)
|
|
router.push(buildDriveFolderHref(view, [...parentSegments, newName]))
|
|
} catch {
|
|
toast.error("Impossible de renommer ce dossier")
|
|
throw new Error("rename failed")
|
|
}
|
|
}
|
|
|
|
const closeDropdown = useCallback(() => {
|
|
setDropdownOpen(false)
|
|
guardDriveMenuPointer()
|
|
}, [])
|
|
|
|
const openRenameDialog = useCallback(() => {
|
|
guardDriveMenuPointer()
|
|
window.setTimeout(() => setRenameDialogOpen(true), 0)
|
|
}, [setRenameDialogOpen])
|
|
|
|
const openFolderPicker = (mode: DriveFolderPickerMode) => {
|
|
setSheetOpen(false)
|
|
closeDropdown()
|
|
window.setTimeout(() => setFolderPickerMode(mode), 0)
|
|
}
|
|
|
|
const menuActionsProps = {
|
|
targets,
|
|
writable,
|
|
allowShare,
|
|
hideOpen: true,
|
|
onOpen: () => {},
|
|
setSharePath,
|
|
mutations,
|
|
onRenameRequest: openRenameDialog,
|
|
onMoveRequest: writable ? () => openFolderPicker("move") : undefined,
|
|
onCopyRequest: writable ? () => openFolderPicker("copy") : undefined,
|
|
}
|
|
|
|
const dialogs = (
|
|
<>
|
|
<DriveNameDialog
|
|
open={renameDialogOpen}
|
|
onOpenChange={setRenameDialogOpen}
|
|
title="Renommer le dossier"
|
|
defaultValue={label}
|
|
confirmLabel="Renommer"
|
|
onConfirm={handleRename}
|
|
/>
|
|
<DriveMoveDialog
|
|
open={folderPickerMode !== null}
|
|
onOpenChange={(next) => {
|
|
if (!next) setFolderPickerMode(null)
|
|
}}
|
|
mode={folderPickerMode ?? "move"}
|
|
sources={targets}
|
|
/>
|
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
<SheetContent
|
|
side="bottom"
|
|
hideClose
|
|
className="gap-0 overflow-hidden rounded-t-2xl border-border px-0 pb-[max(1rem,env(safe-area-inset-bottom))] pt-2"
|
|
>
|
|
<SheetTitle className="sr-only">Actions pour {label}</SheetTitle>
|
|
<SheetDescription className="sr-only">
|
|
Actions disponibles pour {label}.
|
|
</SheetDescription>
|
|
<p className="truncate px-4 pb-2 text-sm font-medium text-muted-foreground">
|
|
{label}
|
|
</p>
|
|
<div className="flex flex-col border-t border-border">
|
|
<DriveFileMenuActions
|
|
variant="sheet"
|
|
{...menuActionsProps}
|
|
onClose={() => setSheetOpen(false)}
|
|
onRenameRequest={() => {
|
|
setSheetOpen(false)
|
|
openRenameDialog()
|
|
}}
|
|
/>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</>
|
|
)
|
|
|
|
if (isMobile) {
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
data-drive-menu-btn
|
|
aria-label={`Actions pour ${label}`}
|
|
aria-expanded={sheetOpen}
|
|
aria-haspopup="dialog"
|
|
className={cn(DRIVE_MENU_BTN, className)}
|
|
onClick={(e) => {
|
|
stopDriveMenuBubble(e)
|
|
setSheetOpen(true)
|
|
}}
|
|
onPointerDown={(e) => stopDriveMenuBubble(e)}
|
|
>
|
|
<MoreVertical className="h-4 w-4" aria-hidden />
|
|
</button>
|
|
{dialogs}
|
|
</>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<DropdownMenu
|
|
modal
|
|
open={dropdownOpen}
|
|
onOpenChange={(next) => {
|
|
if (next) {
|
|
setDropdownOpen(true)
|
|
return
|
|
}
|
|
closeDropdown()
|
|
}}
|
|
>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
data-drive-menu-btn
|
|
aria-label={`Actions pour ${label}`}
|
|
aria-haspopup="menu"
|
|
className={cn(
|
|
DRIVE_MENU_BTN,
|
|
dropdownOpen && DRIVE_MENU_BTN_ACTIVE,
|
|
className
|
|
)}
|
|
onClick={(e) => stopDriveMenuBubble(e)}
|
|
onPointerDown={(e) => stopDriveMenuBubble(e)}
|
|
>
|
|
<MoreVertical className="h-4 w-4" aria-hidden />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="start"
|
|
data-drive-menu-surface
|
|
className={cn(DRIVE_MENU_SURFACE_CLASS, "w-52")}
|
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
onPointerDown={(e) => stopDriveMenuBubble(e)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<DriveFileMenuActions
|
|
variant="dropdown"
|
|
{...menuActionsProps}
|
|
onClose={closeDropdown}
|
|
onRenameRequest={() => {
|
|
closeDropdown()
|
|
openRenameDialog()
|
|
}}
|
|
/>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
{dialogs}
|
|
</>
|
|
)
|
|
}
|