ultisuite-client/components/admin/settings/sections/mail-domains-section.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

267 lines
8.9 KiB
TypeScript

"use client"
import { useCallback, useState } from "react"
import { Check, Copy } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Switch } from "@/components/ui/switch"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { AutomationTabMasonry } from "@/components/gmail/settings/automation/automation-tab-masonry"
import {
SettingsCard,
SettingsField,
SettingsGrid,
} from "@/components/settings/settings-kit"
import { OrgSettingsSection } from "@/components/admin/settings/org-settings-form"
import { MigrationProjectsPanel } from "@/components/admin/settings/sections/migration-projects-panel"
import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store"
import {
useCreateMailDomain,
useMailDomains,
useVerifyMailDomainMX,
useVerifyMailDomainTXT,
} from "@/lib/api/hooks/use-hosted-mail"
export function MailDomainsSection() {
const domainsQuery = useMailDomains()
const createDomain = useCreateMailDomain()
const [domainName, setDomainName] = useState("")
const mailing = useOrgSettingsStore((s) => s.mailing)
const setMailing = useOrgSettingsStore((s) => s.setMailing)
const domains = domainsQuery.data?.domains ?? []
return (
<div className="space-y-8">
<OrgSettingsSection
title="Mail"
description="Domaines hébergés Stalwart, relais SMTP des notifications suite et migration."
policySection="mailing"
>
<AutomationTabMasonry columns={2}>
<SettingsCard
title="Domaines hébergés"
description="Vérification DNS, DKIM et provisioning des boîtes @domaine."
>
<div className="grid gap-4 md:grid-cols-[1fr_auto] md:items-end">
<SettingsField label="Nouveau domaine" htmlFor="new-domain">
<Input
id="new-domain"
className="h-9"
value={domainName}
onChange={(e) => setDomainName(e.target.value)}
placeholder="entreprise.com"
/>
</SettingsField>
<Button
disabled={!domainName || createDomain.isPending}
onClick={() => {
void createDomain.mutateAsync({ name: domainName }).then(() => setDomainName(""))
}}
>
Ajouter
</Button>
</div>
<ul className="space-y-3">
{domains.map((domain) => (
<DomainRow key={domain.id} domain={domain} />
))}
</ul>
</SettingsCard>
<SettingsCard
title="Notifications suite (SMTP)"
description="Partages de fichiers, mentions, invitations — distinct des comptes mail utilisateur."
action={
<Switch
checked={mailing.enabled}
onCheckedChange={(enabled) => setMailing({ enabled })}
/>
}
>
<SettingsGrid columns={2}>
<SettingsField label="Hôte SMTP">
<Input
className="h-9"
value={mailing.smtp_host}
onChange={(e) => setMailing({ smtp_host: e.target.value })}
placeholder="smtp.example.com"
/>
</SettingsField>
<SettingsField label="Port">
<Input
className="h-9"
type="number"
value={mailing.smtp_port}
onChange={(e) => setMailing({ smtp_port: Number(e.target.value) || 587 })}
/>
</SettingsField>
<SettingsField label="Utilisateur">
<Input
className="h-9"
value={mailing.smtp_user}
onChange={(e) => setMailing({ smtp_user: e.target.value })}
/>
</SettingsField>
<SettingsField label="Mot de passe">
<Input
className="h-9"
type="password"
value={mailing.smtp_password}
onChange={(e) => setMailing({ smtp_password: e.target.value })}
/>
</SettingsField>
<SettingsField label="Chiffrement">
<Select
value={mailing.tls_mode}
onValueChange={(tls_mode) =>
setMailing({ tls_mode: tls_mode as typeof mailing.tls_mode })
}
>
<SelectTrigger className="h-9 w-full min-w-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="starttls">STARTTLS</SelectItem>
<SelectItem value="ssl">SSL/TLS</SelectItem>
<SelectItem value="none">Aucun</SelectItem>
</SelectContent>
</Select>
</SettingsField>
<SettingsField label="Adresse d'expédition">
<Input
className="h-9"
type="email"
value={mailing.from_email}
onChange={(e) => setMailing({ from_email: e.target.value })}
/>
</SettingsField>
<SettingsField label="Nom affiché">
<Input
className="h-9"
value={mailing.from_name}
onChange={(e) => setMailing({ from_name: e.target.value })}
/>
</SettingsField>
<SettingsField label="Reply-To (optionnel)" className="sm:col-span-2">
<Input
className="h-9"
type="email"
value={mailing.reply_to ?? ""}
onChange={(e) => setMailing({ reply_to: e.target.value })}
/>
</SettingsField>
</SettingsGrid>
</SettingsCard>
</AutomationTabMasonry>
</OrgSettingsSection>
<MigrationProjectsPanel domains={domains} />
</div>
)
}
function CopyTxtButton({
label,
copied,
onCopy,
}: {
label: string
copied: boolean
onCopy: () => void
}) {
return (
<Button
type="button"
size="icon"
variant="ghost"
className="h-6 w-6 shrink-0"
aria-label={label}
title={label}
onClick={onCopy}
>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5 opacity-60" />}
</Button>
)
}
function DomainRow({
domain,
}: {
domain: {
id: string
name: string
status: string
verification_token?: string
is_platform_domain: boolean
}
}) {
const verifyTxt = useVerifyMailDomainTXT(domain.id)
const verifyMx = useVerifyMailDomainMX(domain.id)
const [copiedField, setCopiedField] = useState<"name" | "value" | null>(null)
const txtName = `_ultisuite-verify.${domain.name}`
const copyTxtField = useCallback(async (text: string, field: "name" | "value", successLabel: string) => {
try {
await navigator.clipboard.writeText(text)
setCopiedField(field)
toast.success(successLabel)
window.setTimeout(() => setCopiedField(null), 2000)
} catch {
toast.error("Impossible de copier")
}
}, [])
return (
<li className="rounded-lg border border-mail-border bg-mail-surface-muted/40 p-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="font-medium">
{domain.name}
{domain.is_platform_domain ? " (plateforme)" : ""}
</p>
<p className="text-sm text-muted-foreground">Statut : {domain.status}</p>
{domain.verification_token && (
<div className="mt-2 space-y-1 text-xs text-muted-foreground">
<p>Enregistrement TXT :</p>
<div className="flex flex-wrap items-center gap-1">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono">{txtName}</code>
<CopyTxtButton
label="Copier le nom TXT"
copied={copiedField === "name"}
onCopy={() => void copyTxtField(txtName, "name", "Nom TXT copié")}
/>
<span className="px-0.5">=</span>
<code className="rounded bg-muted px-1.5 py-0.5 font-mono">{domain.verification_token}</code>
<CopyTxtButton
label="Copier la valeur TXT"
copied={copiedField === "value"}
onCopy={() =>
void copyTxtField(domain.verification_token!, "value", "Valeur TXT copiée")
}
/>
</div>
</div>
)}
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={() => void verifyTxt.mutateAsync()}>
Vérifier TXT
</Button>
<Button size="sm" variant="secondary" onClick={() => void verifyMx.mutateAsync()}>
Vérifier MX
</Button>
</div>
</div>
</li>
)
}