- 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.
371 lines
10 KiB
TypeScript
371 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import type { ReactNode } from "react"
|
|
import { toast } from "sonner"
|
|
import {
|
|
CheckSquare,
|
|
Copy,
|
|
Download,
|
|
ExternalLink,
|
|
FolderInput,
|
|
Link2,
|
|
Pencil,
|
|
Star,
|
|
Trash2,
|
|
Undo2,
|
|
} from "lucide-react"
|
|
import {
|
|
ContextMenuItem,
|
|
} from "@/components/ui/context-menu"
|
|
import {
|
|
DropdownMenuItem,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import type { useDriveMutations } from "@/lib/api/hooks/use-drive-queries"
|
|
import type { DriveFileInfo } from "@/lib/api/types"
|
|
import {
|
|
guardDriveMenuPointer,
|
|
stopDriveMenuEvent,
|
|
} from "@/lib/drive/drive-menu-guard"
|
|
import { cn } from "@/lib/utils"
|
|
import { driveTrashItemKey } from "@/lib/drive/drive-trash"
|
|
|
|
export const DRIVE_MENU_ITEM_CLASS =
|
|
"gap-3 py-2 text-[#3c4043] focus:text-[#3c4043] dark:text-[#e8eaed] dark:focus:text-[#e8eaed] [&_svg]:text-[#3c4043] dark:[&_svg]:text-[#e8eaed]"
|
|
|
|
export const DRIVE_MENU_ITEM_DESTRUCTIVE_CLASS =
|
|
"gap-3 py-2 text-destructive focus:text-destructive [&_svg]:text-destructive"
|
|
|
|
const SHEET_ACTION_CLASS =
|
|
"flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-[#3c4043] transition-colors hover:bg-accent active:bg-accent/80 dark:text-[#e8eaed] [&_svg]:text-[#3c4043] dark:[&_svg]:text-[#e8eaed]"
|
|
|
|
export function canShareDriveItem(item: DriveFileInfo) {
|
|
return item.type === "file" || item.type === "directory"
|
|
}
|
|
|
|
function DriveMenuItemIcon({ children }: { children: ReactNode }) {
|
|
return (
|
|
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
|
|
{children}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
type Mutations = ReturnType<typeof useDriveMutations>
|
|
|
|
type DriveFileMenuActionsProps = {
|
|
variant: "dropdown" | "context" | "sheet"
|
|
targets: DriveFileInfo[]
|
|
isTrash?: boolean
|
|
allowShare?: boolean
|
|
/** When false, hide rename / move / copy / delete. */
|
|
writable?: boolean
|
|
/** When true, hide the "Ouvrir" action (e.g. current folder in breadcrumb). */
|
|
hideOpen?: boolean
|
|
/** When false, hide favorite actions (public share). */
|
|
hideFavorite?: boolean
|
|
onOpen: () => void
|
|
onClose?: () => void
|
|
setSharePath: (path: string | null, itemType?: "file" | "directory" | null) => void
|
|
mutations: Mutations
|
|
onRenameRequest: () => void
|
|
onMoveRequest?: () => void
|
|
onCopyRequest?: () => void
|
|
onDownloadRequest?: () => void
|
|
onQuickLinkRequest?: () => void
|
|
onEnterSelectionMode?: () => void
|
|
}
|
|
|
|
export function DriveFileMenuActions({
|
|
variant,
|
|
targets,
|
|
isTrash,
|
|
allowShare = true,
|
|
writable = true,
|
|
hideOpen = false,
|
|
hideFavorite = false,
|
|
onOpen,
|
|
onClose,
|
|
setSharePath,
|
|
mutations,
|
|
onRenameRequest,
|
|
onMoveRequest,
|
|
onCopyRequest,
|
|
onDownloadRequest,
|
|
onQuickLinkRequest,
|
|
onEnterSelectionMode,
|
|
}: DriveFileMenuActionsProps) {
|
|
const single = targets.length === 1 ? targets[0]! : null
|
|
const multi = targets.length > 1
|
|
|
|
const runAsync = async (fn: () => Promise<void>, ok: string, err: string) => {
|
|
onClose?.()
|
|
try {
|
|
await fn()
|
|
toast.success(ok)
|
|
} catch {
|
|
toast.error(err)
|
|
}
|
|
}
|
|
|
|
const runMenuAction = (
|
|
fn: () => void,
|
|
asyncFn?: () => Promise<void>,
|
|
ok?: string,
|
|
err?: string
|
|
) => {
|
|
guardDriveMenuPointer()
|
|
onClose?.()
|
|
if (asyncFn && ok && err) {
|
|
void runAsync(asyncFn, ok, err)
|
|
return
|
|
}
|
|
fn()
|
|
}
|
|
|
|
const selectAction = (fn: () => void) => () => {
|
|
guardDriveMenuPointer()
|
|
onClose?.()
|
|
window.setTimeout(fn, 0)
|
|
}
|
|
|
|
const favoriteLabel = targets.every((f) => f.is_favorite)
|
|
? "Retirer des favoris"
|
|
: "Ajouter aux favoris"
|
|
|
|
const favoriteAsync = async () => {
|
|
for (const f of targets) {
|
|
await mutations.favorite.mutateAsync({
|
|
path: f.path,
|
|
favorite: !f.is_favorite,
|
|
})
|
|
}
|
|
}
|
|
|
|
const deleteAsync = async () => {
|
|
for (const f of targets) {
|
|
await mutations.deleteFile.mutateAsync(f.path)
|
|
}
|
|
}
|
|
|
|
const restoreAsync = async () => {
|
|
for (const f of targets) {
|
|
await mutations.restore.mutateAsync(driveTrashItemKey(f))
|
|
}
|
|
}
|
|
|
|
const deleteTrashAsync = async () => {
|
|
for (const f of targets) {
|
|
await mutations.deleteTrash.mutateAsync(driveTrashItemKey(f))
|
|
}
|
|
}
|
|
|
|
const actions: Array<{
|
|
key: string
|
|
label: string
|
|
icon: ReactNode
|
|
destructive?: boolean
|
|
visible: boolean
|
|
onSelect: () => void
|
|
}> = [
|
|
{
|
|
key: "open",
|
|
label: "Ouvrir",
|
|
icon: <ExternalLink className="h-4 w-4" aria-hidden />,
|
|
visible: !hideOpen && !multi && Boolean(single),
|
|
onSelect: () => runMenuAction(() => window.setTimeout(() => onOpen(), 0)),
|
|
},
|
|
{
|
|
key: "select",
|
|
label: "Sélectionner",
|
|
icon: <CheckSquare className="h-4 w-4" aria-hidden />,
|
|
visible: variant === "sheet" && Boolean(!isTrash && single && onEnterSelectionMode),
|
|
onSelect: () =>
|
|
runMenuAction(() => {
|
|
window.setTimeout(() => onEnterSelectionMode?.(), 0)
|
|
}),
|
|
},
|
|
{
|
|
key: "share",
|
|
label: "Partager",
|
|
icon: <Link2 className="h-4 w-4" aria-hidden />,
|
|
visible: Boolean(!isTrash && allowShare && !multi && single && canShareDriveItem(single)),
|
|
onSelect: () => runMenuAction(() => setSharePath(single!.path, single!.type)),
|
|
},
|
|
{
|
|
key: "copy",
|
|
label: multi ? `Copier vers (${targets.length})` : "Copier vers",
|
|
icon: <Copy className="h-4 w-4" aria-hidden />,
|
|
visible: Boolean(writable && !isTrash && onCopyRequest),
|
|
onSelect: () =>
|
|
runMenuAction(() => {
|
|
window.setTimeout(() => onCopyRequest?.(), 0)
|
|
}),
|
|
},
|
|
{
|
|
key: "move",
|
|
label: multi ? `Déplacer vers (${targets.length})` : "Déplacer vers",
|
|
icon: <FolderInput className="h-4 w-4" aria-hidden />,
|
|
visible: Boolean(writable && !isTrash && onMoveRequest),
|
|
onSelect: () =>
|
|
runMenuAction(() => {
|
|
window.setTimeout(() => onMoveRequest?.(), 0)
|
|
}),
|
|
},
|
|
{
|
|
key: "download",
|
|
label: "Télécharger",
|
|
icon: <Download className="h-4 w-4" aria-hidden />,
|
|
visible: Boolean(!isTrash && onDownloadRequest),
|
|
onSelect: () =>
|
|
runMenuAction(() => {
|
|
window.setTimeout(() => onDownloadRequest?.(), 0)
|
|
}),
|
|
},
|
|
{
|
|
key: "quick-link",
|
|
label: "Obtenir le lien",
|
|
icon: <Link2 className="h-4 w-4" aria-hidden />,
|
|
visible: Boolean(!isTrash && onQuickLinkRequest && single),
|
|
onSelect: () =>
|
|
runMenuAction(() => {
|
|
window.setTimeout(() => onQuickLinkRequest?.(), 0)
|
|
}),
|
|
},
|
|
{
|
|
key: "favorite",
|
|
label: favoriteLabel,
|
|
icon: <Star className="h-4 w-4" aria-hidden />,
|
|
visible: !isTrash && !hideFavorite,
|
|
onSelect: () =>
|
|
runMenuAction(
|
|
() => {},
|
|
favoriteAsync,
|
|
favoriteLabel === "Retirer des favoris" ? "Retiré des favoris" : "Ajouté aux favoris",
|
|
"Impossible de modifier les favoris"
|
|
),
|
|
},
|
|
{
|
|
key: "restore",
|
|
label: `Restaurer${multi ? ` (${targets.length})` : ""}`,
|
|
icon: <Undo2 className="h-4 w-4" aria-hidden />,
|
|
visible: Boolean(isTrash),
|
|
onSelect: () =>
|
|
runMenuAction(
|
|
() => {},
|
|
restoreAsync,
|
|
"Élément(s) restauré(s)",
|
|
"Impossible de restaurer"
|
|
),
|
|
},
|
|
{
|
|
key: "delete-trash",
|
|
label: `Supprimer définitivement${multi ? ` (${targets.length})` : ""}`,
|
|
icon: <Trash2 className="h-4 w-4" aria-hidden />,
|
|
destructive: true,
|
|
visible: Boolean(isTrash && writable),
|
|
onSelect: () =>
|
|
runMenuAction(
|
|
() => {},
|
|
deleteTrashAsync,
|
|
"Élément(s) supprimé(s) définitivement",
|
|
"Impossible de supprimer définitivement"
|
|
),
|
|
},
|
|
{
|
|
key: "rename",
|
|
label: "Renommer",
|
|
icon: <Pencil className="h-4 w-4" aria-hidden />,
|
|
visible: Boolean(writable && !isTrash && single && !multi),
|
|
onSelect: () =>
|
|
runMenuAction(() => {
|
|
window.setTimeout(() => onRenameRequest(), 0)
|
|
}),
|
|
},
|
|
{
|
|
key: "delete",
|
|
label: `Supprimer${multi ? ` (${targets.length})` : ""}`,
|
|
icon: <Trash2 className="h-4 w-4" aria-hidden />,
|
|
destructive: true,
|
|
visible: writable && !isTrash,
|
|
onSelect: () =>
|
|
runMenuAction(() => {}, deleteAsync, "Supprimé", "Impossible de supprimer"),
|
|
},
|
|
]
|
|
|
|
if (variant === "sheet") {
|
|
return (
|
|
<>
|
|
{actions
|
|
.filter((action) => action.visible)
|
|
.map((action) => (
|
|
<button
|
|
key={action.key}
|
|
type="button"
|
|
className={cn(
|
|
SHEET_ACTION_CLASS,
|
|
action.destructive &&
|
|
"text-destructive hover:bg-destructive/10 active:bg-destructive/15 [&_svg]:text-destructive"
|
|
)}
|
|
onPointerDown={(e) => stopDriveMenuEvent(e)}
|
|
onClick={() => {
|
|
guardDriveMenuPointer()
|
|
action.onSelect()
|
|
}}
|
|
>
|
|
<DriveMenuItemIcon>{action.icon}</DriveMenuItemIcon>
|
|
{action.label}
|
|
</button>
|
|
))}
|
|
</>
|
|
)
|
|
}
|
|
|
|
if (variant === "context") {
|
|
return (
|
|
<>
|
|
{actions
|
|
.filter((action) => action.visible)
|
|
.map((action) => (
|
|
<ContextMenuItem
|
|
key={action.key}
|
|
variant={action.destructive ? "destructive" : "default"}
|
|
className={cn(
|
|
action.destructive
|
|
? DRIVE_MENU_ITEM_DESTRUCTIVE_CLASS
|
|
: DRIVE_MENU_ITEM_CLASS
|
|
)}
|
|
onPointerDown={(e) => stopDriveMenuEvent(e)}
|
|
onSelect={selectAction(action.onSelect)}
|
|
>
|
|
<DriveMenuItemIcon>{action.icon}</DriveMenuItemIcon>
|
|
{action.label}
|
|
</ContextMenuItem>
|
|
))}
|
|
</>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{actions
|
|
.filter((action) => action.visible)
|
|
.map((action) => (
|
|
<DropdownMenuItem
|
|
key={action.key}
|
|
variant={action.destructive ? "destructive" : "default"}
|
|
className={cn(
|
|
action.destructive
|
|
? DRIVE_MENU_ITEM_DESTRUCTIVE_CLASS
|
|
: DRIVE_MENU_ITEM_CLASS
|
|
)}
|
|
onPointerDown={(e) => stopDriveMenuEvent(e)}
|
|
onSelect={() => action.onSelect()}
|
|
>
|
|
<DriveMenuItemIcon>{action.icon}</DriveMenuItemIcon>
|
|
{action.label}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</>
|
|
)
|
|
}
|