ultisuite-client/components/gmail/settings/automation/webhooks-panel.tsx
R3D347HR4Y 5304790ed5
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): enhance session management and identity provider settings
- Added SessionGuard component to manage session expiration and online status.
- Updated AuthProvider to streamline session fetching and handling.
- Introduced IdentityProvidersSection for managing OAuth, SAML, and LDAP identity providers.
- Implemented identity provider guides for easier configuration.
- Enhanced mail settings with infinite scroll option for improved user experience.
- Updated global styles and layout components for better consistency across the application.
2026-06-09 09:36:46 +02:00

273 lines
11 KiB
TypeScript

"use client"
import { useState } from "react"
import { Pencil, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
useMailWebhooks,
useCreateMailWebhook,
useUpdateMailWebhook,
useDeleteMailWebhook,
} from "@/lib/api/hooks/use-mail-automation-queries"
import { useAuthReady } from "@/lib/api/use-auth-ready"
import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-banner"
import { WebhookTemplateVariablesPanel } from "@/components/gmail/settings/automation/webhook-template-variables-panel"
import { AutomationTabMasonry } from "@/components/gmail/settings/automation/automation-tab-masonry"
import { WebhookEventScopeEditor } from "@/components/gmail/settings/automation/webhook-event-scope-editor"
import {
AutomationDomainFilterTab,
AutomationDomainMark,
AutomationDomainMarks,
} from "@/components/gmail/settings/automation/automation-domain-mark"
import { WEBHOOK_DEFAULT_TEMPLATES } from "@/lib/mail-automation/webhook-template-variables"
import {
AUTOMATION_DOMAIN_LABELS,
type AutomationDomain,
} from "@/lib/mail-automation/domains"
import {
createDefaultWebhookForm,
defaultEventTypesForDomain,
eventDomainsFromTypes,
type WebhookFormState,
} from "@/lib/mail-automation/webhook-config"
import { TRIGGER_LABELS } from "@/lib/mail-automation/domains"
import type { ApiWebhook } from "@/lib/api/types"
import type { TriggerType } from "@/lib/mail-automation/types"
const CREATE_DOMAINS: AutomationDomain[] = ["mail", "drive", "contacts"]
function webhookToForm(hook: ApiWebhook): WebhookFormState {
return {
name: hook.name,
url: hook.url,
template: hook.body_template ?? WEBHOOK_DEFAULT_TEMPLATES.mail,
eventTypes: (hook.event_types?.length ? hook.event_types : ["message_received"]) as TriggerType[],
mailScope: hook.mail_scope ?? { all_accounts: true, account_ids: [] },
driveScope: hook.drive_scope ?? { all_folders: true, folder_paths: [] },
contactsScope: hook.contacts_scope ?? { all_books: true, book_ids: [] },
}
}
function summarizeWebhook(hook: ApiWebhook): string {
const types = hook.event_types ?? []
if (types.length === 0) return "Aucun événement"
const labels = types.slice(0, 3).map((t) => TRIGGER_LABELS[t as TriggerType] ?? t)
const extra = types.length > 3 ? ` +${types.length - 3}` : ""
return labels.join(", ") + extra
}
function webhookDomains(hook: ApiWebhook): AutomationDomain[] {
const types = (hook.event_types ?? []) as TriggerType[]
return eventDomainsFromTypes(types.length > 0 ? types : ["message_received"])
}
export function WebhooksPanel() {
const { ready, authenticated } = useAuthReady()
const { data: webhooks = [], isFetching, isError, refetch, isPending } = useMailWebhooks()
const createWebhook = useCreateMailWebhook()
const updateWebhook = useUpdateMailWebhook()
const deleteWebhook = useDeleteMailWebhook()
const [editorOpen, setEditorOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [presetDomain, setPresetDomain] = useState<AutomationDomain>("mail")
const [form, setForm] = useState<WebhookFormState>(() => createDefaultWebhookForm("mail"))
const showInitialLoad = ready && authenticated && isPending && webhooks.length === 0
const editorTitle = editingId ? "Modifier le webhook" : "Nouveau webhook"
function openCreate(domain: AutomationDomain) {
setEditingId(null)
setPresetDomain(domain)
setForm({
...createDefaultWebhookForm(domain),
template: WEBHOOK_DEFAULT_TEMPLATES[domain],
})
setEditorOpen(true)
}
function openEdit(hook: ApiWebhook) {
setEditingId(hook.id)
setForm(webhookToForm(hook))
const domains = webhookDomains(hook)
setPresetDomain(domains[0] ?? "mail")
setEditorOpen(true)
}
async function handleSave() {
const payload = {
name: form.name.trim(),
url: form.url.trim(),
method: "POST" as const,
body_template: form.template,
event_types: form.eventTypes,
mail_scope: form.mailScope,
drive_scope: form.driveScope,
contacts_scope: form.contactsScope,
}
if (editingId) {
await updateWebhook.mutateAsync({ webhookId: editingId, ...payload })
} else {
await createWebhook.mutateAsync(payload)
}
setEditorOpen(false)
}
const canSave =
form.name.trim().length > 0 &&
form.url.trim().length > 0 &&
form.eventTypes.length > 0 &&
!createWebhook.isPending &&
!updateWebhook.isPending
return (
<div className="space-y-4">
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
<AutomationTabMasonry columns={2}>
<div className="space-y-4">
<div className="space-y-2">
<p className="text-sm font-medium">Webhooks</p>
<div className="flex flex-col gap-1.5">
{CREATE_DOMAINS.map((domain) => (
<Button
key={domain}
type="button"
variant="outline"
size="sm"
className="h-9 w-full justify-start gap-2 px-3"
onClick={() => openCreate(domain)}
>
<AutomationDomainMark domain={domain} className="size-5" alt="" />
<span>Nouveau webhook {AUTOMATION_DOMAIN_LABELS[domain]}</span>
</Button>
))}
</div>
</div>
{showInitialLoad ? (
<p className="text-sm text-muted-foreground">Chargement</p>
) : webhooks.length === 0 ? (
<p className="text-sm text-muted-foreground">Aucun webhook configuré.</p>
) : (
<ul className="divide-y divide-border rounded-lg border border-border">
{webhooks.map((hook) => {
const domains = webhookDomains(hook)
return (
<li key={hook.id} className="flex items-start gap-2 px-3 py-3">
<AutomationDomainMarks
domains={domains}
className="pt-0.5"
markClassName="size-5"
/>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">{hook.name}</p>
<p className="truncate text-xs text-muted-foreground">{hook.url}</p>
<p className="mt-0.5 text-[11px] text-muted-foreground">
{summarizeWebhook(hook)}
</p>
</div>
<div className="flex shrink-0 gap-1">
<Button type="button" variant="ghost" size="icon" onClick={() => openEdit(hook)}>
<Pencil className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => deleteWebhook.mutate(hook.id)}
>
<Trash2 className="size-4" />
</Button>
</div>
</li>
)
})}
</ul>
)}
</div>
<WebhookTemplateVariablesPanel />
</AutomationTabMasonry>
<Dialog open={editorOpen} onOpenChange={setEditorOpen}>
<DialogContent className="flex max-h-[90vh] max-w-lg flex-col gap-0 overflow-hidden p-0 sm:max-w-2xl">
<DialogHeader className="border-b border-border px-4 py-3">
<DialogTitle className="text-base">{editorTitle}</DialogTitle>
</DialogHeader>
<div className="flex-1 space-y-4 overflow-y-auto p-4">
{!editingId ? (
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Modèle de départ</Label>
<div className="flex flex-wrap gap-2">
{CREATE_DOMAINS.map((domain) => (
<AutomationDomainFilterTab
key={domain}
domain={domain}
label={AUTOMATION_DOMAIN_LABELS[domain]}
active={presetDomain === domain}
onClick={() => {
setPresetDomain(domain)
setForm((f) => ({
...f,
eventTypes: defaultEventTypesForDomain(domain),
template: WEBHOOK_DEFAULT_TEMPLATES[domain],
}))
}}
/>
))}
</div>
</div>
) : null}
<div className="space-y-2">
<Label className="text-xs">Nom</Label>
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
</div>
<div className="space-y-2">
<Label className="text-xs">URL HTTPS</Label>
<Input value={form.url} onChange={(e) => setForm({ ...form, url: e.target.value })} />
</div>
<div className="space-y-2">
<Label className="text-xs">body_template JSON</Label>
<textarea
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs"
value={form.template}
onChange={(e) => setForm({ ...form, template: e.target.value })}
/>
</div>
<WebhookEventScopeEditor
eventTypes={form.eventTypes}
onEventTypesChange={(eventTypes) => setForm({ ...form, eventTypes })}
mailScope={form.mailScope}
onMailScopeChange={(mailScope) => setForm({ ...form, mailScope })}
driveScope={form.driveScope}
onDriveScopeChange={(driveScope) => setForm({ ...form, driveScope })}
contactsScope={form.contactsScope}
onContactsScopeChange={(contactsScope) => setForm({ ...form, contactsScope })}
/>
</div>
<div className="flex justify-end gap-2 border-t border-border px-4 py-3">
<Button type="button" variant="outline" onClick={() => setEditorOpen(false)}>
Annuler
</Button>
<Button type="button" disabled={!canSave} onClick={handleSave}>
Enregistrer
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}