ultisuite-client/components/drive/drive-bulk-toolbar.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- 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.
2026-06-07 15:49:21 +02:00

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>
</>
)
}