- 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.
352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import {
|
|
Copy,
|
|
Download,
|
|
FolderInput,
|
|
Link2,
|
|
MoreVertical,
|
|
Trash2,
|
|
Undo2,
|
|
UserPlus,
|
|
X,
|
|
} from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { DriveMoveDialog, type DriveFolderPickerMode } from "@/components/drive/drive-move-dialog"
|
|
import {
|
|
canShareDriveItem,
|
|
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 { downloadDriveFile } from "@/lib/api/drive-download"
|
|
import type { DriveFileInfo } from "@/lib/api/types"
|
|
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
|
|
import { DRIVE_CARD_PAD_X } from "@/lib/drive/drive-chrome-classes"
|
|
import { DRIVE_ICON_BTN } from "@/lib/drive/drive-chrome-classes"
|
|
import { cn } from "@/lib/utils"
|
|
import { driveTrashItemKey } from "@/lib/drive/drive-trash"
|
|
|
|
function BulkIconButton({
|
|
label,
|
|
onClick,
|
|
disabled,
|
|
children,
|
|
className,
|
|
}: {
|
|
label: string
|
|
onClick: () => void
|
|
disabled?: boolean
|
|
children: React.ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className={cn("h-8 w-8 shrink-0 rounded-full", DRIVE_ICON_BTN, className)}
|
|
aria-label={label}
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
export function DriveBulkToolbar({
|
|
targets,
|
|
isTrash,
|
|
allowShare = true,
|
|
allowMove = true,
|
|
allowCopy = true,
|
|
allowQuickLink = true,
|
|
allowDelete = true,
|
|
mutations: mutationsProp,
|
|
onDownloadBulk,
|
|
}: {
|
|
targets: DriveFileInfo[]
|
|
isTrash?: boolean
|
|
allowShare?: boolean
|
|
allowMove?: boolean
|
|
allowCopy?: boolean
|
|
allowQuickLink?: boolean
|
|
allowDelete?: boolean
|
|
mutations?: ReturnType<typeof useDriveMutations>
|
|
onDownloadBulk?: (files: DriveFileInfo[]) => Promise<void>
|
|
}) {
|
|
const clearSelection = useDriveUIStore((s) => s.clearSelection)
|
|
const setSharePath = useDriveUIStore((s) => s.setSharePath)
|
|
const mutationsDefault = useDriveMutations()
|
|
const mutations = mutationsProp ?? mutationsDefault
|
|
const [folderPickerMode, setFolderPickerMode] = useState<DriveFolderPickerMode | null>(null)
|
|
const [moreOpen, setMoreOpen] = useState(false)
|
|
const n = targets.length
|
|
|
|
if (n === 0) return null
|
|
|
|
const single = n === 1 ? targets[0]! : null
|
|
const canShare = Boolean(
|
|
!isTrash && allowShare && single && canShareDriveItem(single)
|
|
)
|
|
|
|
const run = async (fn: () => Promise<void>, ok: string, err: string) => {
|
|
try {
|
|
await fn()
|
|
toast.success(ok)
|
|
clearSelection()
|
|
} catch {
|
|
toast.error(err)
|
|
}
|
|
}
|
|
|
|
const onShare = () => {
|
|
if (!canShare || !single) {
|
|
toast.error("Sélectionnez un seul élément pour partager")
|
|
return
|
|
}
|
|
setSharePath(single.path, single.type)
|
|
}
|
|
|
|
const onDownload = async () => {
|
|
const files = targets.filter((t) => t.type === "file")
|
|
if (files.length === 0) {
|
|
toast.error("Aucun fichier à télécharger")
|
|
return
|
|
}
|
|
try {
|
|
if (onDownloadBulk) {
|
|
await onDownloadBulk(files)
|
|
} else {
|
|
for (const file of files) {
|
|
await downloadDriveFile(file.path, file.name, file.name)
|
|
}
|
|
}
|
|
toast.success(
|
|
files.length > 1 ? `${files.length} fichiers téléchargés` : "Fichier téléchargé"
|
|
)
|
|
} catch {
|
|
toast.error("Impossible de télécharger")
|
|
}
|
|
}
|
|
|
|
const onQuickLink = async () => {
|
|
if (!single) {
|
|
toast.error("Sélectionnez un seul élément pour obtenir un lien")
|
|
return
|
|
}
|
|
try {
|
|
const share = await mutations.createShare.mutateAsync({
|
|
path: single.path,
|
|
role: "viewer",
|
|
mode: "public",
|
|
})
|
|
if (share.url) {
|
|
await navigator.clipboard.writeText(share.url)
|
|
toast.success("Lien copié")
|
|
} else {
|
|
toast.success("Lien de partage créé")
|
|
}
|
|
} catch {
|
|
toast.error("Impossible de créer le lien")
|
|
}
|
|
}
|
|
|
|
const onDelete = () =>
|
|
void run(
|
|
async () => {
|
|
for (const f of targets) {
|
|
await mutations.deleteFile.mutateAsync(f.path)
|
|
}
|
|
},
|
|
n > 1 ? "Éléments supprimés" : "Élément supprimé",
|
|
"Impossible de supprimer"
|
|
)
|
|
|
|
const onPermanentDelete = () =>
|
|
void run(
|
|
async () => {
|
|
for (const f of targets) {
|
|
await mutations.deleteTrash.mutateAsync(driveTrashItemKey(f))
|
|
}
|
|
},
|
|
n > 1 ? "Éléments supprimés définitivement" : "Élément supprimé définitivement",
|
|
"Impossible de supprimer définitivement"
|
|
)
|
|
|
|
const onRestore = () =>
|
|
void run(
|
|
async () => {
|
|
for (const f of targets) {
|
|
await mutations.restore.mutateAsync(driveTrashItemKey(f))
|
|
}
|
|
},
|
|
n > 1 ? "Éléments restaurés" : "Élément restauré",
|
|
"Impossible de restaurer"
|
|
)
|
|
|
|
const noopOpen = () => {}
|
|
|
|
return (
|
|
<>
|
|
<DriveMoveDialog
|
|
open={folderPickerMode !== null && (allowMove || allowCopy)}
|
|
onOpenChange={(open) => {
|
|
if (!open) setFolderPickerMode(null)
|
|
}}
|
|
mode={folderPickerMode ?? "move"}
|
|
sources={targets}
|
|
onMoved={clearSelection}
|
|
/>
|
|
<div className={cn("py-1.5", DRIVE_CARD_PAD_X)}>
|
|
<div
|
|
role="toolbar"
|
|
aria-label="Actions de sélection"
|
|
className="flex h-8 shrink-0 items-center gap-2 rounded-full bg-[#edf2fc] dark:bg-primary/15"
|
|
>
|
|
<div className="flex min-w-0 shrink-0 items-center gap-0.5">
|
|
<BulkIconButton label="Désélectionner" onClick={clearSelection}>
|
|
<X className="h-5 w-5" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
<span className="whitespace-nowrap px-1 text-sm font-medium text-[#444746] dark:text-[#e8eaed]">
|
|
{n} sélectionné{n > 1 ? "s" : ""}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="ml-auto flex shrink-0 items-center gap-0.5">
|
|
{isTrash ? (
|
|
<>
|
|
<BulkIconButton label="Restaurer" onClick={onRestore}>
|
|
<Undo2 className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
<BulkIconButton
|
|
label="Supprimer définitivement"
|
|
onClick={onPermanentDelete}
|
|
className="text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
</>
|
|
) : (
|
|
<>
|
|
{allowShare ? (
|
|
<BulkIconButton
|
|
label="Partager"
|
|
onClick={onShare}
|
|
disabled={!canShare}
|
|
>
|
|
<UserPlus className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
) : null}
|
|
<BulkIconButton label="Télécharger" onClick={() => void onDownload()}>
|
|
<Download className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
{allowCopy ? (
|
|
<BulkIconButton
|
|
label="Copier vers"
|
|
className="max-sm:hidden"
|
|
onClick={() => setFolderPickerMode("copy")}
|
|
>
|
|
<Copy className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
) : null}
|
|
{allowMove ? (
|
|
<BulkIconButton
|
|
label="Déplacer vers"
|
|
onClick={() => setFolderPickerMode("move")}
|
|
>
|
|
<FolderInput className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
) : null}
|
|
{allowDelete ? (
|
|
<BulkIconButton
|
|
label="Supprimer"
|
|
onClick={onDelete}
|
|
className="text-[#444746] hover:text-destructive dark:text-[#e8eaed]"
|
|
>
|
|
<Trash2 className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
) : null}
|
|
{allowQuickLink ? (
|
|
<BulkIconButton
|
|
label="Obtenir le lien"
|
|
className="max-sm:hidden"
|
|
onClick={() => void onQuickLink()}
|
|
disabled={!single}
|
|
>
|
|
<Link2 className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</BulkIconButton>
|
|
) : null}
|
|
<DropdownMenu open={moreOpen} onOpenChange={setMoreOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className={cn("h-8 w-8 shrink-0 rounded-full", DRIVE_ICON_BTN)}
|
|
aria-label="Plus d'actions"
|
|
>
|
|
<MoreVertical className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="end"
|
|
sideOffset={8}
|
|
data-drive-menu-surface
|
|
className={cn(DRIVE_MENU_SURFACE_CLASS, "w-52")}
|
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
>
|
|
<DriveFileMenuActions
|
|
variant="dropdown"
|
|
targets={targets}
|
|
isTrash={isTrash}
|
|
allowShare={allowShare}
|
|
onOpen={noopOpen}
|
|
onClose={() => setMoreOpen(false)}
|
|
setSharePath={setSharePath}
|
|
mutations={mutations}
|
|
onRenameRequest={() => setMoreOpen(false)}
|
|
onMoveRequest={
|
|
isTrash || !allowMove
|
|
? undefined
|
|
: () => {
|
|
setMoreOpen(false)
|
|
window.setTimeout(() => setFolderPickerMode("move"), 0)
|
|
}
|
|
}
|
|
onCopyRequest={
|
|
isTrash || !allowCopy
|
|
? undefined
|
|
: () => {
|
|
setMoreOpen(false)
|
|
window.setTimeout(() => setFolderPickerMode("copy"), 0)
|
|
}
|
|
}
|
|
onQuickLinkRequest={
|
|
isTrash || !allowQuickLink || !single
|
|
? undefined
|
|
: () => {
|
|
setMoreOpen(false)
|
|
window.setTimeout(() => void onQuickLink(), 0)
|
|
}
|
|
}
|
|
/>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|