ultisuite-client/components/drive/share-dialog.tsx
R3D347HR4Y 8f81d7aba1
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(admin-settings): refactor admin settings components for improved usability and consistency
- Replaced legacy components with new `SettingsCard`, `SettingsField`, and `SettingsToggleRow` for a unified design.
- Enhanced `AdminListControls` to support compact mode and improved pagination controls.
- Updated various sections including `AiAssistantSection`, `AuthenticationSection`, and `DriveMountOAuthSection` to utilize new components, streamlining the settings interface.
- Improved accessibility and user experience across admin settings with clearer labels and hints.
- Deprecated old components while maintaining backward compatibility for existing admin sections.
2026-06-15 11:10:17 +02:00

775 lines
26 KiB
TypeScript

"use client"
import { useEffect, useMemo, useState } from "react"
import {
Building2,
Copy,
Eye,
Globe,
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { useSearchContacts } from "@/lib/api/hooks/use-contact-queries"
import { useDriveShares, useDriveMutations } from "@/lib/api/hooks/use-drive-queries"
import type { 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,
formatShareDate,
groupSharesBySection,
shareAccessLabel,
shareLinkForCopy,
shareOwnerLabel,
sharePermissionsLabel,
shareRecipientLabel,
type ShareListSection,
} from "@/lib/drive/drive-share-types"
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
import {
DRIVE_BTN_GHOST,
DRIVE_BTN_PRIMARY,
DRIVE_DIALOG_CONTENT,
DRIVE_DIALOG_DIVIDER,
DRIVE_DIALOG_FOOTER,
DRIVE_DIALOG_HEADER,
DRIVE_DIALOG_OVERLAY,
DRIVE_FIELD_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"
type LinkAccessMode = "public" | "internal"
const PERMISSION_OPTIONS: {
id: SharePermissionMode
label: string
icon: typeof Eye
folderOnly?: boolean
}[] = [
{ id: "viewer", label: "Lecteur", icon: Eye },
{ id: "editor", label: "Éditeur", icon: Pencil },
{ id: "advanced", label: "Avancé", icon: SlidersHorizontal, folderOnly: true },
]
const LINK_ACCESS_OPTIONS: {
id: LinkAccessMode
label: string
description: string
icon: typeof Globe
}[] = [
{
id: "public",
label: "Lien public",
description: "Toute personne disposant du lien peut consulter l'élément.",
icon: Globe,
},
{
id: "internal",
label: "Lien interne",
description: "Réservé aux utilisateurs inscrits et connectés.",
icon: Users,
},
]
function shareSectionIcon(section: ShareListSection) {
if (section === "people") return UserRound
if (section === "groups") return Building2
return Link2
}
function RoleChip({ permissions }: { permissions: number }) {
return (
<span className="inline-flex shrink-0 items-center rounded-full bg-[#e8eaed] px-2 py-0.5 text-[11px] font-medium text-[#3c4043] dark:bg-[#3c4043] dark:text-[#e8eaed]">
{sharePermissionsLabel(permissions)}
</span>
)
}
function shareTooltipLines(share: DriveShare): string[] {
const lines: string[] = []
const owner = shareOwnerLabel(share)
if (owner) lines.push(`Propriétaire · ${owner}`)
const created = formatShareDate(share.created_at)
if (created) lines.push(`Créé le ${created}`)
const expires = formatShareDate(share.expires_at)
if (expires) lines.push(`Expire le ${expires}`)
if (share.has_password) lines.push("Protégé par mot de passe")
const url = shareLinkForCopy(share)
if (url) lines.push(url)
if (share.note?.trim()) lines.push(`« ${share.note.trim()} »`)
return lines
}
function ShareEntryRow({
share,
onDelete,
deleting,
}: {
share: DriveShare
onDelete: () => void
deleting: boolean
}) {
const url = shareLinkForCopy(share)
const recipient = shareRecipientLabel(share)
const accessLabel = shareAccessLabel(share)
const isLink = share.share_type === NC_SHARE_TYPE.LINK
const tooltipLines = shareTooltipLines(share)
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 primaryLabel = isLink ? accessLabel : (recipient ?? accessLabel)
return (
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"group flex items-center gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-[#f1f3f4] dark:hover:bg-[#3c4043]/50",
tooltipLines.length > 0 && "cursor-default"
)}
>
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#e8f0fe] text-[#1967d2] dark:bg-[#1a377a]/50 dark:text-[#8ab4f8]">
{isLink ? (
<Link2 className="h-3.5 w-3.5" aria-hidden />
) : share.share_type === NC_SHARE_TYPE.GROUP ? (
<Building2 className="h-3.5 w-3.5" aria-hidden />
) : (
<UserRound className="h-3.5 w-3.5" aria-hidden />
)}
</div>
<div className="min-w-0 flex-1">
<p className={cn("truncate text-sm", DRIVE_TEXT_PRIMARY)}>{primaryLabel}</p>
</div>
{share.has_password ? (
<Shield className="h-3.5 w-3.5 shrink-0 text-[#5f6368] dark:text-[#9aa0a6]" aria-label="Mot de passe" />
) : null}
<RoleChip permissions={share.permissions} />
<div className="flex shrink-0 items-center opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
{url ? (
<Button
type="button"
variant="ghost"
size="icon-sm"
className={DRIVE_BTN_GHOST}
aria-label="Copier le lien"
onClick={() => void copy()}
>
<Copy className="h-3.5 w-3.5" />
</Button>
) : null}
<Button
type="button"
variant="ghost"
size="icon-sm"
className={cn(DRIVE_TEXT_SECONDARY, "hover:bg-[#fce8e6] hover:text-[#d93025] dark:hover:bg-[#5c2b29]/50")}
aria-label="Supprimer le partage"
disabled={deleting}
onClick={onDelete}
>
{deleting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Trash2 className="h-3.5 w-3.5" />}
</Button>
</div>
</div>
</TooltipTrigger>
{tooltipLines.length > 0 ? (
<TooltipContent side="top" className="max-w-xs space-y-0.5 text-left">
{tooltipLines.map((line) => (
<p key={line} className="break-all">
{line}
</p>
))}
</TooltipContent>
) : null}
</Tooltip>
)
}
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[] = ["people", "groups", "links"]
const hasShares = shares.length > 0
if (loading) {
return (
<div className={cn("flex items-center gap-2 py-2 text-sm", DRIVE_TEXT_SECONDARY)}>
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
Chargement
</div>
)
}
if (error) {
return (
<div className="space-y-2 py-2">
<p className={cn("text-sm", DRIVE_TEXT_PRIMARY)}>Impossible de charger les partages.</p>
<Button type="button" variant="ghost" size="sm" className={cn(DRIVE_BTN_GHOST, "h-8 px-2 text-xs")} onClick={onRetry}>
<RefreshCw className="h-3.5 w-3.5" aria-hidden />
Réessayer
</Button>
</div>
)
}
if (!hasShares) return null
return (
<div className="space-y-3">
<div className="flex items-center justify-between gap-2">
<p className={cn("text-sm font-medium", DRIVE_TEXT_PRIMARY)}>Utilisateurs avec accès</p>
<Button
type="button"
variant="ghost"
size="sm"
className={cn(DRIVE_BTN_GHOST, "h-7 px-2 text-xs")}
onClick={onRetry}
>
<RefreshCw className="h-3 w-3" aria-hidden />
</Button>
</div>
<div className="max-h-44 space-y-3 overflow-y-auto pr-0.5">
{sectionOrder.map((section) => {
const items = grouped[section]
if (items.length === 0) return null
const SectionIcon = shareSectionIcon(section)
return (
<div key={section} className="space-y-0.5">
<p className={cn("flex items-center gap-1 px-2 text-[11px] font-medium uppercase tracking-wide", DRIVE_TEXT_SECONDARY)}>
<SectionIcon className="h-3 w-3" aria-hidden />
{SHARE_SECTION_LABELS[section]}
</p>
{items.map((share) => (
<ShareEntryRow
key={share.id}
share={share}
deleting={deletingShareId === share.id}
onDelete={() => onDeleteShare(share.id)}
/>
))}
</div>
)
})}
</div>
</div>
)
}
function AdvancedPermissionsPanel({
folderPermissions,
onFolderPermissionChange,
}: {
folderPermissions: FolderSharePermissions
onFolderPermissionChange: (id: FolderSharePermissionId, checked: boolean) => void
}) {
const advancedPermissionBits = folderPermissionsToBitmask(folderPermissions)
return (
<div className={cn(DRIVE_PANEL_MUTED, "mt-2 space-y-2 px-3 py-2.5")}>
{FOLDER_SHARE_PERMISSION_OPTIONS.map((option) => {
const checkboxId = `drive-share-perm-${option.id}`
return (
<div key={option.id} className="flex items-center gap-2.5 py-0.5">
<Checkbox
id={checkboxId}
checked={folderPermissions[option.id]}
onCheckedChange={(value) => onFolderPermissionChange(option.id, value === true)}
/>
<Label htmlFor={checkboxId} className={cn("cursor-pointer text-sm font-normal", DRIVE_TEXT_PRIMARY)}>
{option.label}
</Label>
</div>
)
})}
{!folderPermissions.viewContent && folderPermissions.addFiles ? (
<p className={cn("border-t pt-2 text-xs leading-relaxed", DRIVE_DIALOG_DIVIDER, DRIVE_TEXT_SECONDARY)}>
Dépôt uniquement : les visiteurs pourront ajouter des fichiers sans voir le contenu existant.
</p>
) : null}
{advancedPermissionBits === 0 ? (
<p className="text-xs text-[#d93025]" role="alert">
Sélectionnez au moins une autorisation.
</p>
) : null}
</div>
)
}
function PermissionSelect({
value,
isFolder,
onChange,
className,
}: {
value: SharePermissionMode
isFolder: boolean
onChange: (mode: SharePermissionMode) => void
className?: string
}) {
const options = PERMISSION_OPTIONS.filter((o) => isFolder || !o.folderOnly)
const selected = options.find((o) => o.id === value) ?? options[0]
return (
<Select value={value} onValueChange={(v) => onChange(v as SharePermissionMode)}>
<SelectTrigger
size="sm"
className={cn(
"h-9 min-w-[132px] shrink-0 border-[#dadce0] bg-transparent px-3 text-sm shadow-none dark:border-[#5f6368]/40",
className
)}
>
<SelectValue>{selected.label}</SelectValue>
</SelectTrigger>
<SelectContent>
{options.map((option) => {
const Icon = option.icon
return (
<SelectItem key={option.id} value={option.id}>
<span className="flex items-center gap-2">
<Icon className="h-3.5 w-3.5 shrink-0" aria-hidden />
{option.label}
</span>
</SelectItem>
)
})}
</SelectContent>
</Select>
)
}
export function ShareDialog() {
const path = useDriveUIStore((s) => s.sharePath)
const shareItemType = useDriveUIStore((s) => s.shareItemType)
const setSharePath = useDriveUIStore((s) => s.setSharePath)
const [linkAccessMode, setLinkAccessMode] = useState<LinkAccessMode>("public")
const [permissionMode, setPermissionMode] = useState<SharePermissionMode>("viewer")
const [folderPermissions, setFolderPermissions] = useState<FolderSharePermissions>(
() => folderPermissionsFromRole("viewer")
)
const [contactEmail, setContactEmail] = useState("")
const [contactQuery, setContactQuery] = useState("")
const [contactNote, setContactNote] = useState("")
const [recipientRegistered, setRecipientRegistered] = useState<boolean | null>(null)
const [deletingShareId, setDeletingShareId] = useState<string | null>(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 selectedLinkAccess = LINK_ACCESS_OPTIONS.find((o) => o.id === linkAccessMode) ?? LINK_ACCESS_OPTIONS[0]
const LinkAccessIcon = selectedLinkAccess.icon
const showContactExtras = contactEmail.trim().length > 0
const hasValidContactEmail = contactEmail.trim().includes("@")
useEffect(() => {
if (path) {
setLinkAccessMode("public")
setPermissionMode("viewer")
setFolderPermissions(folderPermissionsFromRole("viewer"))
setContactEmail("")
setContactQuery("")
setContactNote("")
setRecipientRegistered(null)
}
}, [path])
useEffect(() => {
const email = contactEmail.trim().toLowerCase()
if (!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, lookupRecipientEmail])
const advancedPermissionBits = folderPermissionsToBitmask(folderPermissions)
const canCreateLink =
!isFolder || permissionMode !== "advanced" || advancedPermissionBits > 0
const canShareWithContact = hasValidContactEmail && canCreateLink
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 onCreateLink = async () => {
if (!path || !canCreateLink) return
try {
const share = await createShare.mutateAsync({
...sharePayload(),
mode: linkAccessMode,
})
const link = shareLinkForCopy(share)
if (link) {
await navigator.clipboard.writeText(link)
toast.success(
linkAccessMode === "internal"
? "Lien interne copié"
: "Lien public copié"
)
} else {
toast.success("Partage créé")
}
void refetchShares()
} catch {
toast.error("Partage impossible")
}
}
const onShareWithContact = async () => {
if (!path || !canShareWithContact) return
try {
const share = await createShare.mutateAsync({
...sharePayload(),
mode: "contact",
share_with: contactEmail.trim().toLowerCase(),
note: contactNote.trim() || undefined,
send_mail: true,
})
if (share.access_mode === "user" || share.share_type === NC_SHARE_TYPE.USER) {
toast.success("Partagé — visible dans « Partagés avec moi »")
} else {
toast.success("Invitation envoyée par e-mail")
}
setContactEmail("")
setContactNote("")
setContactQuery("")
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 ?? []
return (
<TooltipProvider delayDuration={400}>
<Dialog open={Boolean(path)} onOpenChange={(open) => !open && close()}>
<DialogContent
overlayClassName={DRIVE_DIALOG_OVERLAY}
className={cn(DRIVE_DIALOG_CONTENT, "sm:max-w-[480px]")}
>
<DialogHeader className={cn(DRIVE_DIALOG_HEADER, "pb-4")}>
<DialogTitle className={cn("text-base font-medium", DRIVE_TEXT_TITLE)}>
Partager « {itemLabel} »
</DialogTitle>
<DialogDescription className="sr-only">
{isFolder ? "Partager le dossier" : "Partager le fichier"} {itemLabel}
</DialogDescription>
</DialogHeader>
<div className="max-h-[min(70vh,520px)] space-y-5 overflow-y-auto px-6 py-4">
{/* Ajouter des personnes */}
<div className="space-y-2">
<div className="flex items-start gap-2">
<div className="relative min-w-0 flex-1">
<Input
id="drive-share-contact-email"
type="email"
value={contactEmail}
onChange={(e) => {
setContactEmail(e.target.value)
setContactQuery(e.target.value)
}}
placeholder="Ajouter des personnes par e-mail"
autoComplete="off"
className={cn(DRIVE_FIELD_CLASS, "h-10 pr-3")}
/>
{contactQuery.length >= 2 && contactResults.length > 0 ? (
<div className="absolute top-full z-10 mt-1 w-full overflow-hidden rounded-lg border border-[#dadce0] bg-white shadow-md dark:border-[#5f6368]/40 dark:bg-[#292a2d]">
{contactResults.slice(0, 6).map((c) =>
c.email ? (
<button
key={c.uid}
type="button"
className="flex w-full flex-col items-start gap-0.5 px-3 py-2 text-left hover:bg-[#f1f3f4] dark:hover:bg-[#3c4043]/50"
onClick={() => {
setContactEmail(c.email!)
setContactQuery("")
}}
>
<span className={cn("text-sm font-medium", DRIVE_TEXT_PRIMARY)}>{c.full_name}</span>
<span className={cn("text-xs", DRIVE_TEXT_SECONDARY)}>{c.email}</span>
</button>
) : null
)}
</div>
) : null}
</div>
{showContactExtras ? (
<PermissionSelect
value={permissionMode}
isFolder={isFolder}
onChange={onPermissionModeChange}
className="min-w-[132px]"
/>
) : null}
</div>
{showContactExtras ? (
<div className="space-y-2 animate-in fade-in-0 slide-in-from-top-1 duration-150">
<Textarea
id="drive-share-contact-note"
value={contactNote}
onChange={(e) => setContactNote(e.target.value)}
placeholder="Message (optionnel)"
rows={2}
className={DRIVE_TEXTAREA_CLASS}
/>
{recipientRegistered === true ? (
<p className="flex items-center gap-1.5 text-xs text-[#188038] dark:text-[#81c995]">
<UserRound className="h-3 w-3" aria-hidden />
Compte inscrit partage direct
</p>
) : recipientRegistered === false ? (
<p className={cn("flex items-center gap-1.5 text-xs", DRIVE_TEXT_SECONDARY)}>
<Mail className="h-3 w-3" aria-hidden />
Invitation par e-mail avec lien public
</p>
) : null}
{isFolder && permissionMode === "advanced" ? (
<AdvancedPermissionsPanel
folderPermissions={folderPermissions}
onFolderPermissionChange={setFolderPermission}
/>
) : null}
</div>
) : null}
</div>
<ActiveSharesPanel
shares={existingShares}
loading={sharesLoading}
error={sharesError}
onRetry={() => void refetchShares()}
deletingShareId={deletingShareId}
onDeleteShare={(shareId) => void onDeleteShare(shareId)}
/>
{/* Accès général */}
<div className={cn("space-y-2", existingShares.length > 0 && "border-t pt-4")}>
<p className={cn("text-sm font-medium", DRIVE_TEXT_PRIMARY)}>Accès général</p>
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#e6f4ea] text-[#188038] dark:bg-[#1e3a2f]/50 dark:text-[#81c995]">
<LinkAccessIcon className="h-4 w-4" aria-hidden />
</div>
<div className="min-w-0 flex-1 space-y-0.5">
<Select value={linkAccessMode} onValueChange={(v) => setLinkAccessMode(v as LinkAccessMode)}>
<SelectTrigger
variant="ghost"
size="sm"
className="h-auto w-full justify-start gap-1 p-0 text-sm font-medium"
>
<SelectValue>{selectedLinkAccess.label}</SelectValue>
</SelectTrigger>
<SelectContent>
{LINK_ACCESS_OPTIONS.map((option) => {
const Icon = option.icon
return (
<SelectItem key={option.id} value={option.id}>
<span className="flex items-center gap-2">
<Icon className="h-3.5 w-3.5 shrink-0" aria-hidden />
{option.label}
</span>
</SelectItem>
)
})}
</SelectContent>
</Select>
<p className={cn("text-xs leading-relaxed", DRIVE_TEXT_SECONDARY)}>
{selectedLinkAccess.description}
</p>
</div>
{!showContactExtras ? (
<PermissionSelect
value={permissionMode}
isFolder={isFolder}
onChange={onPermissionModeChange}
className="min-w-[132px]"
/>
) : null}
</div>
{!showContactExtras && isFolder && permissionMode === "advanced" ? (
<AdvancedPermissionsPanel
folderPermissions={folderPermissions}
onFolderPermissionChange={setFolderPermission}
/>
) : null}
</div>
</div>
<DialogFooter className={cn(DRIVE_DIALOG_FOOTER, "justify-between sm:justify-between")}>
<Button
type="button"
variant="outline"
className={cn(
"rounded-full border-[#dadce0] bg-white text-sm font-medium text-[#1a73e8] hover:bg-[#f8f9fa] dark:border-[#5f6368]/40 dark:bg-transparent dark:text-[#8ab4f8] dark:hover:bg-[#3c4043]/50"
)}
disabled={createShare.isPending || !canCreateLink}
onClick={() => void onCreateLink()}
>
{createShare.isPending && !showContactExtras ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Link2 className="h-4 w-4" />
)}
Copier le lien
</Button>
<div className="flex gap-2">
{showContactExtras ? (
<Button
type="button"
className={cn(DRIVE_BTN_PRIMARY, "rounded-full px-6")}
disabled={createShare.isPending || !canShareWithContact}
onClick={() => void onShareWithContact()}
>
{createShare.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Envoi
</>
) : (
<>
<Mail className="h-4 w-4" />
Partager
</>
)}
</Button>
) : null}
<Button
type="button"
className={cn(DRIVE_BTN_PRIMARY, "rounded-full px-6")}
onClick={close}
>
{showContactExtras ? "Annuler" : "Terminé"}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</TooltipProvider>
)
}