"use client" import { useEffect, useMemo, useState } from "react" import { Icon } from "@iconify/react" import { Building2, Copy, Eye, Link2, Loader2, Mail, Pencil, RefreshCw, Shield, SlidersHorizontal, Trash2, UserRound, Users, } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { useSearchContacts } from "@/lib/api/hooks/use-contact-queries" import { useDriveShares, useDriveMutations } from "@/lib/api/hooks/use-drive-queries" import type { DriveFileInfo, DriveShare } from "@/lib/api/types" import { displayFileName } from "@/lib/drive/display-file-name" import { FOLDER_SHARE_PERMISSION_OPTIONS, folderPermissionsFromRole, folderPermissionsToBitmask, type FolderSharePermissionId, type FolderSharePermissions, } from "@/lib/drive/drive-share-permissions" import { NC_SHARE_TYPE, SHARE_SECTION_LABELS, groupSharesBySection, shareAccessLabel, shareLinkForCopy, shareMetaLine, shareOwnerLabel, shareRecipientLabel, type DriveShareMode, type ShareListSection, } from "@/lib/drive/drive-share-types" import { DriveFileTypeIcon } from "@/lib/drive/drive-file-icon" import { useDriveUIStore } from "@/lib/stores/drive-ui-store" import { DRIVE_BTN_GHOST, DRIVE_BTN_PRIMARY, DRIVE_CARD_ACTIVE, DRIVE_CARD_IDLE, DRIVE_DIALOG_CONTENT, DRIVE_DIALOG_DIVIDER, DRIVE_DIALOG_FOOTER, DRIVE_DIALOG_HEADER, DRIVE_DIALOG_OVERLAY, DRIVE_FIELD_CLASS, DRIVE_LABEL_CLASS, DRIVE_PANEL_MUTED, DRIVE_TEXT_PRIMARY, DRIVE_TEXT_SECONDARY, DRIVE_TEXT_TITLE, DRIVE_TEXTAREA_CLASS, } from "@/lib/drive/drive-dialog-styles" import { cn } from "@/lib/utils" function shareItemLabel(path: string) { const trimmed = path.replace(/\/+$/, "") const base = trimmed.slice(trimmed.lastIndexOf("/") + 1) return displayFileName(base || path) } type SharePermissionMode = "viewer" | "editor" | "advanced" const PERMISSION_MODE_OPTIONS: { id: SharePermissionMode label: string description: string icon: typeof Eye folderOnly?: boolean }[] = [ { id: "viewer", label: "Lecteur", description: "Consultation uniquement", icon: Eye, }, { id: "editor", label: "Éditeur", description: "Peut modifier le contenu", icon: Pencil, }, { id: "advanced", label: "Avancé", description: "Définir chaque autorisation", icon: SlidersHorizontal, folderOnly: true, }, ] const MODE_OPTIONS: { id: DriveShareMode label: string description: string icon: typeof Link2 }[] = [ { id: "contact", label: "Personne", description: "Partage direct par e-mail ou compte", icon: UserRound, }, { id: "internal", label: "Lien interne", description: "Réservé aux utilisateurs inscrits connectés", icon: Users, }, { id: "public", label: "Lien public", description: "Accessible à toute personne disposant du lien", icon: Link2, }, ] function shareSectionIcon(section: ShareListSection) { if (section === "people") return UserRound if (section === "groups") return Building2 return Link2 } function ShareEntryRow({ share, onDelete, deleting, }: { share: DriveShare onDelete: () => void deleting: boolean }) { const url = shareLinkForCopy(share) const recipient = shareRecipientLabel(share) const owner = shareOwnerLabel(share) const meta = shareMetaLine(share) const accessLabel = shareAccessLabel(share) const isLink = share.share_type === NC_SHARE_TYPE.LINK const copy = async () => { if (!url) return try { await navigator.clipboard.writeText(url) toast.success("Lien copié") } catch { toast.error("Impossible de copier le lien") } } const primaryLine = recipient ?? (url && isLink ? url : accessLabel) return (
{accessLabel} {share.has_password ? ( Mot de passe ) : null}

{primaryLine}

{url && isLink && recipient ? (

{url}

) : null} {owner ? (

Propriétaire · {owner}

) : null}

{meta}

{share.note?.trim() ? (

« {share.note.trim()} »

) : null}
{url ? ( ) : null}
) } function ActiveSharesPanel({ shares, loading, error, onRetry, deletingShareId, onDeleteShare, }: { shares: DriveShare[] loading: boolean error: boolean onRetry: () => void deletingShareId: string | null onDeleteShare: (shareId: string) => void }) { const grouped = useMemo(() => groupSharesBySection(shares), [shares]) const sectionOrder: ShareListSection[] = ["links", "people", "groups"] const hasShares = shares.length > 0 return (

Accès existants

{!loading ? ( ) : null}
{loading ? (
Chargement des partages…
) : error ? (

Impossible de charger les partages existants.

) : !hasShares ? (

Aucun partage actif pour cet élément. Créez un lien ou invitez une personne ci-dessus.

) : (
{sectionOrder.map((section) => { const items = grouped[section] if (items.length === 0) return null const SectionIcon = shareSectionIcon(section) return (

{SHARE_SECTION_LABELS[section]} ({items.length})

{items.map((share) => ( onDeleteShare(share.id)} /> ))}
) })}
)}
) } function SharePermissionsPanel({ isFolder, permissionMode, folderPermissions, onPermissionModeChange, onFolderPermissionChange, }: { isFolder: boolean permissionMode: SharePermissionMode folderPermissions: FolderSharePermissions onPermissionModeChange: (mode: SharePermissionMode) => void onFolderPermissionChange: (id: FolderSharePermissionId, checked: boolean) => void }) { const advancedPermissionBits = folderPermissionsToBitmask(folderPermissions) const modeOptions = PERMISSION_MODE_OPTIONS.filter((option) => isFolder || !option.folderOnly) return (
{modeOptions.map((option) => { const IconComponent = option.icon const selected = permissionMode === option.id return ( ) })}
{isFolder && permissionMode === "advanced" ? (
{FOLDER_SHARE_PERMISSION_OPTIONS.map((option) => { const checked = folderPermissions[option.id] const checkboxId = `drive-share-perm-${option.id}` return (
onFolderPermissionChange(option.id, value === true) } />
) })} {!folderPermissions.viewContent && folderPermissions.addFiles ? (

Dépôt uniquement : les visiteurs pourront ajouter des fichiers sans voir le contenu existant du dossier.

) : null} {advancedPermissionBits === 0 ? (

Sélectionnez au moins une autorisation.

) : null}
) : null}
) } export function ShareDialog() { const path = useDriveUIStore((s) => s.sharePath) const shareItemType = useDriveUIStore((s) => s.shareItemType) const setSharePath = useDriveUIStore((s) => s.setSharePath) const [mode, setMode] = useState("public") const [permissionMode, setPermissionMode] = useState("viewer") const [folderPermissions, setFolderPermissions] = useState( () => folderPermissionsFromRole("viewer") ) const [contactEmail, setContactEmail] = useState("") const [contactQuery, setContactQuery] = useState("") const [contactNote, setContactNote] = useState("") const [recipientRegistered, setRecipientRegistered] = useState(null) const [deletingShareId, setDeletingShareId] = useState(null) const { data, isLoading: sharesLoading, isError: sharesError, refetch: refetchShares } = useDriveShares( path ?? "", Boolean(path) ) const { createShare, deleteShare, lookupShareRecipient } = useDriveMutations() const lookupRecipientEmail = lookupShareRecipient.mutateAsync const { data: contactResults = [] } = useSearchContacts(contactQuery) const itemLabel = useMemo(() => (path ? shareItemLabel(path) : ""), [path]) const isFolder = shareItemType === "directory" const selectedMode = MODE_OPTIONS.find((m) => m.id === mode) ?? MODE_OPTIONS[0] const filePreview = useMemo((): DriveFileInfo | null => { if (!path) return null return { path, name: itemLabel, type: shareItemType ?? "file", size: 0, mime_type: "", last_modified: "", etag: "", is_favorite: false, } }, [path, itemLabel, shareItemType]) useEffect(() => { if (path) { setMode("public") setPermissionMode("viewer") setFolderPermissions(folderPermissionsFromRole("viewer")) setContactEmail("") setContactQuery("") setContactNote("") setRecipientRegistered(null) } }, [path]) useEffect(() => { const email = contactEmail.trim().toLowerCase() if (mode !== "contact" || !email.includes("@")) { setRecipientRegistered(null) return } const timer = window.setTimeout(() => { lookupRecipientEmail(email) .then((res) => setRecipientRegistered(res.registered)) .catch(() => setRecipientRegistered(null)) }, 350) return () => window.clearTimeout(timer) }, [contactEmail, mode, lookupRecipientEmail]) const advancedPermissionBits = folderPermissionsToBitmask(folderPermissions) const canCreateShare = (mode !== "contact" || contactEmail.trim().includes("@")) && (!isFolder || permissionMode !== "advanced" || advancedPermissionBits > 0) const setFolderPermission = (id: FolderSharePermissionId, checked: boolean) => { setFolderPermissions((prev) => ({ ...prev, [id]: checked })) } const onPermissionModeChange = (nextMode: SharePermissionMode) => { setPermissionMode(nextMode) if (nextMode === "advanced") { setFolderPermissions(folderPermissionsFromRole(permissionMode === "editor" ? "editor" : "viewer")) } } const close = () => setSharePath(null, null) const sharePayload = () => { const base = isFolder && permissionMode === "advanced" ? { path: path!, permissions: advancedPermissionBits } : { path: path!, role: permissionMode === "editor" ? "editor" : "viewer" } return base } const onShare = async () => { if (!path || !canCreateShare) return try { const payload = sharePayload() const share = await createShare.mutateAsync({ ...payload, mode, ...(mode === "contact" ? { share_with: contactEmail.trim().toLowerCase(), note: contactNote.trim() || undefined, send_mail: true, } : {}), }) if (mode === "contact") { if (share.access_mode === "user" || share.share_type === NC_SHARE_TYPE.USER) { toast.success("Partagé — visible dans « Partagés avec moi » du destinataire") } else { toast.success("Invitation envoyée par e-mail avec un lien public") } setContactEmail("") setContactNote("") setContactQuery("") } else { const link = shareLinkForCopy(share) if (link) { await navigator.clipboard.writeText(link) toast.success( mode === "internal" ? "Lien interne copié dans le presse-papiers" : "Lien public copié dans le presse-papiers" ) } else { toast.success("Partage créé") } } void refetchShares() } catch { toast.error("Partage impossible") } } const onDeleteShare = async (shareId: string) => { setDeletingShareId(shareId) try { await deleteShare.mutateAsync(shareId) toast.success("Partage supprimé") void refetchShares() } catch { toast.error("Suppression impossible") } finally { setDeletingShareId(null) } } const existingShares = data?.shares ?? [] const actionLabel = mode === "contact" ? "Partager" : mode === "internal" ? "Créer le lien interne" : "Créer le lien public" return ( !open && close()}>
{filePreview ? (
) : (
)}
{isFolder ? "Partager le dossier" : "Partager le fichier"} {itemLabel}
{MODE_OPTIONS.map((option) => { const IconComponent = option.icon const selected = mode === option.id return ( ) })}

{selectedMode.label}

{mode === "public" ? `Toute personne disposant du lien pourra accéder à ${isFolder ? "ce dossier" : "ce fichier"} selon le rôle choisi.` : mode === "internal" ? `Seuls les utilisateurs inscrits et connectés pourront ouvrir ce lien vers ${isFolder ? "ce dossier" : "ce fichier"}.` : "Si le destinataire possède un compte, le fichier apparaît dans ses « Partagés avec moi ». Sinon, il reçoit un e-mail avec un lien public."}

{mode === "contact" ? (
{ setContactEmail(e.target.value) setContactQuery(e.target.value) }} placeholder="nom@exemple.com" autoComplete="off" className={DRIVE_FIELD_CLASS} /> {contactQuery.length >= 2 && contactResults.length > 0 ? (
{contactResults.slice(0, 6).map((c) => c.email ? ( ) : null )}
) : null} {recipientRegistered === true ? (

Compte inscrit — partage direct dans « Partagés avec moi »

) : null} {recipientRegistered === false ? (

Pas de compte — invitation par e-mail avec lien public

) : null}