273 lines
11 KiB
TypeScript
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-1">
|
|
{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>
|
|
)
|
|
}
|