Lots of changes to the API and webhooks
Some checks are pending
E2E / Playwright e2e (push) Waiting to run

This commit is contained in:
R3D347HR4Y 2026-06-07 16:02:55 +02:00
parent 20552a34ff
commit 636b8cf789
9 changed files with 260 additions and 93 deletions

View File

@ -9,6 +9,7 @@ import { useDriveList } from "@/lib/api/hooks/use-drive-queries"
import type { ApiTokenDriveScope } from "@/lib/api/types"
import { displayFileName } from "@/lib/drive/display-file-name"
import { normalizeDriveFolderPath } from "@/lib/drive/drive-sidebar-tree"
import { AutomationBorderedFieldset } from "@/components/gmail/settings/automation/automation-bordered-fieldset"
import { cn } from "@/lib/utils"
export function ApiTokenDriveScopeEditor({
@ -61,10 +62,10 @@ export function ApiTokenDriveScopeEditor({
}
return (
<fieldset className={cn("space-y-3 rounded-md border border-border p-3", className)}>
<legend className="px-1 text-sm font-medium text-foreground">
Périmètre Drive dossiers
</legend>
<AutomationBorderedFieldset
className={className}
legend="Périmètre Drive — dossiers"
>
<label className="flex items-start gap-2">
<Checkbox
checked={scope.all_folders}
@ -159,6 +160,6 @@ export function ApiTokenDriveScopeEditor({
</div>
</div>
)}
</fieldset>
</AutomationBorderedFieldset>
)
}

View File

@ -4,7 +4,7 @@ import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { useMailAccounts } from "@/lib/api/hooks/use-mail-queries"
import type { ApiTokenMailScope } from "@/lib/api/types"
import { cn } from "@/lib/utils"
import { AutomationBorderedFieldset } from "@/components/gmail/settings/automation/automation-bordered-fieldset"
export function ApiTokenMailScopeEditor({
scope,
@ -29,10 +29,10 @@ export function ApiTokenMailScopeEditor({
}
return (
<fieldset className={cn("space-y-3 rounded-md border border-border p-3", className)}>
<legend className="px-1 text-sm font-medium text-foreground">
Périmètre mail comptes
</legend>
<AutomationBorderedFieldset
className={className}
legend="Périmètre mail — comptes"
>
<label className="flex items-start gap-2">
<Checkbox
checked={scope.all_accounts}
@ -83,6 +83,6 @@ export function ApiTokenMailScopeEditor({
)}
</div>
)}
</fieldset>
</AutomationBorderedFieldset>
)
}

View File

@ -0,0 +1,34 @@
"use client"
import type { ReactNode } from "react"
import { cn } from "@/lib/utils"
export function AutomationBorderedFieldset({
legend,
children,
className,
legendClassName,
contentClassName,
}: {
legend: ReactNode
children: ReactNode
className?: string
legendClassName?: string
contentClassName?: string
}) {
return (
<fieldset
className={cn("m-0 min-w-0 rounded-md border border-border p-0", className)}
>
<legend
className={cn(
"ml-2.5 w-auto max-w-[calc(100%-1.25rem)] px-1 text-sm font-medium leading-none text-foreground",
legendClassName
)}
>
{legend}
</legend>
<div className={cn("space-y-3 px-3 pb-3 pt-2", contentClassName)}>{children}</div>
</fieldset>
)
}

View File

@ -0,0 +1,88 @@
"use client"
import { LayoutGrid } from "lucide-react"
import type { AutomationDomain } from "@/lib/mail-automation/domains"
import { AUTOMATION_DOMAIN_LABELS } from "@/lib/mail-automation/domains"
import { suitePublicAsset } from "@/lib/suite/suite-public-asset"
import { cn } from "@/lib/utils"
export const AUTOMATION_DOMAIN_MARK_SRC: Record<AutomationDomain, string> = {
mail: suitePublicAsset("/brand/ultimail-header-icon.png"),
drive: suitePublicAsset("/ultidrive-mark.svg"),
contacts: suitePublicAsset("/contacts-mark.svg"),
}
export function AutomationDomainMark({
domain,
className,
alt,
}: {
domain: AutomationDomain
className?: string
alt?: string
}) {
return (
<img
src={AUTOMATION_DOMAIN_MARK_SRC[domain]}
alt={alt ?? AUTOMATION_DOMAIN_LABELS[domain]}
className={cn("shrink-0 object-contain", className)}
draggable={false}
/>
)
}
export function AutomationDomainMarks({
domains,
className,
markClassName = "size-5",
}: {
domains: AutomationDomain[]
className?: string
markClassName?: string
}) {
if (domains.length === 0) return null
return (
<div className={cn("flex shrink-0 items-center gap-1", className)}>
{domains.map((domain) => (
<AutomationDomainMark
key={domain}
domain={domain}
className={markClassName}
alt=""
/>
))}
</div>
)
}
export function AutomationDomainFilterTab({
active,
onClick,
domain,
label,
}: {
active: boolean
onClick: () => void
domain: AutomationDomain | "all"
label: string
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-[11px] transition-colors",
active
? "border-primary bg-primary/10 text-primary"
: "border-border bg-background text-muted-foreground hover:bg-muted"
)}
>
{domain === "all" ? (
<LayoutGrid className="size-3.5 shrink-0 opacity-80" aria-hidden />
) : (
<AutomationDomainMark domain={domain} className="size-3.5" alt="" />
)}
<span>{label}</span>
</button>
)
}

View File

@ -2,9 +2,9 @@
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { AutomationBorderedFieldset } from "@/components/gmail/settings/automation/automation-bordered-fieldset"
import { useContactBooks } from "@/lib/api/hooks/use-contact-queries"
import type { WebhookContactsScope } from "@/lib/mail-automation/webhook-config"
import { cn } from "@/lib/utils"
export function WebhookContactsScopeEditor({
scope,
@ -34,10 +34,10 @@ export function WebhookContactsScopeEditor({
}
return (
<fieldset className={cn("space-y-3 rounded-md border border-border p-3", className)}>
<legend className="px-1 text-sm font-medium text-foreground">
Périmètre contacts carnets
</legend>
<AutomationBorderedFieldset
className={className}
legend="Périmètre contacts — carnets"
>
<label className="flex items-start gap-2">
<Checkbox
checked={scope.all_books}
@ -81,6 +81,6 @@ export function WebhookContactsScopeEditor({
)}
</div>
)}
</fieldset>
</AutomationBorderedFieldset>
)
}

View File

@ -9,6 +9,8 @@ import {
AUTOMATION_DOMAIN_LABELS,
type AutomationDomain,
} from "@/lib/mail-automation/domains"
import { AutomationBorderedFieldset } from "@/components/gmail/settings/automation/automation-bordered-fieldset"
import { AutomationDomainMark } from "@/components/gmail/settings/automation/automation-domain-mark"
import type { TriggerType } from "@/lib/mail-automation/types"
import {
hasContactsWebhookEvents,
@ -58,14 +60,21 @@ export function WebhookEventScopeEditor({
<p className="text-xs text-muted-foreground">
Le webhook part uniquement pour les événements cochés, dans le périmètre défini ci-dessous.
</p>
<div className="space-y-3">
{DOMAINS.map((domain) => {
const options = WEBHOOK_EVENT_OPTIONS.filter((o) => o.domain === domain)
return (
<fieldset key={domain} className="rounded-md border border-border p-3">
<legend className="px-1 text-xs font-medium text-foreground">
<AutomationBorderedFieldset
key={domain}
legend={
<>
<AutomationDomainMark domain={domain} className="size-3.5" alt="" />
{AUTOMATION_DOMAIN_LABELS[domain]}
</legend>
<ul className="mt-2 space-y-1.5">
</>
}
legendClassName="flex items-center gap-1.5 text-xs"
>
<ul className="space-y-1.5">
{options.map((opt) => (
<li key={opt.value}>
<label className="flex cursor-pointer items-center gap-2 text-sm">
@ -78,10 +87,11 @@ export function WebhookEventScopeEditor({
</li>
))}
</ul>
</fieldset>
</AutomationBorderedFieldset>
)
})}
</div>
</div>
<ApiTokenMailScopeEditor
enabled={hasMailWebhookEvents(eventTypes)}

View File

@ -11,6 +11,7 @@ import {
AUTOMATION_DOMAIN_LABELS,
type AutomationDomain,
} from "@/lib/mail-automation/domains"
import { AutomationDomainFilterTab } from "@/components/gmail/settings/automation/automation-domain-mark"
import { cn } from "@/lib/utils"
const DOMAIN_TABS: Array<AutomationDomain | "all"> = ["all", "mail", "drive", "contacts"]
@ -79,19 +80,13 @@ export function WebhookTemplateVariablesPanel() {
</p>
<div className="flex flex-wrap gap-1">
{DOMAIN_TABS.map((tab) => (
<button
<AutomationDomainFilterTab
key={tab}
type="button"
domain={tab}
label={tab === "all" ? "Tout" : AUTOMATION_DOMAIN_LABELS[tab]}
active={filter === tab}
onClick={() => setFilter(tab)}
className={cn(
"rounded-md border px-2 py-0.5 text-[11px] transition-colors",
filter === tab
? "border-primary bg-primary/10 text-primary"
: "border-border bg-background text-muted-foreground hover:bg-muted"
)}
>
{tab === "all" ? "Tout" : AUTOMATION_DOMAIN_LABELS[tab]}
</button>
/>
))}
</div>
</div>

View File

@ -22,6 +22,11 @@ import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-ba
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,
@ -30,12 +35,14 @@ import {
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"
import { cn } from "@/lib/utils"
const CREATE_DOMAINS: AutomationDomain[] = ["mail", "drive", "contacts"]
function webhookToForm(hook: ApiWebhook): WebhookFormState {
return {
@ -57,6 +64,11 @@ function summarizeWebhook(hook: ApiWebhook): string {
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()
@ -86,6 +98,8 @@ export function WebhooksPanel() {
function openEdit(hook: ApiWebhook) {
setEditingId(hook.id)
setForm(webhookToForm(hook))
const domains = webhookDomains(hook)
setPresetDomain(domains[0] ?? "mail")
setEditorOpen(true)
}
@ -119,42 +133,70 @@ export function WebhooksPanel() {
<div className="space-y-4">
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
<div className="flex flex-wrap gap-2">
{(["mail", "drive", "contacts"] as AutomationDomain[]).map((domain) => (
<Button key={domain} type="button" size="sm" variant="outline" onClick={() => openCreate(domain)}>
Webhook {AUTOMATION_DOMAIN_LABELS[domain]}
<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>
<AutomationTabMasonry columns={2}>
<WebhookTemplateVariablesPanel />
</div>
{showInitialLoad ? (
<p className="text-sm text-muted-foreground">Chargement</p>
) : webhooks.length === 0 ? (
<p className="text-sm text-muted-foreground">Aucun webhook.</p>
<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) => (
<li key={hook.id} className="flex items-start justify-between gap-2 px-3 py-3">
<div className="min-w-0">
{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>
<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)}>
<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}>
@ -164,11 +206,15 @@ export function WebhooksPanel() {
</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">
{(["mail", "drive", "contacts"] as AutomationDomain[]).map((domain) => (
<button
{CREATE_DOMAINS.map((domain) => (
<AutomationDomainFilterTab
key={domain}
type="button"
domain={domain}
label={AUTOMATION_DOMAIN_LABELS[domain]}
active={presetDomain === domain}
onClick={() => {
setPresetDomain(domain)
setForm((f) => ({
@ -177,17 +223,10 @@ export function WebhooksPanel() {
template: WEBHOOK_DEFAULT_TEMPLATES[domain],
}))
}}
className={cn(
"rounded-md border px-2 py-0.5 text-[11px] transition-colors",
presetDomain === domain
? "border-primary bg-primary/10 text-primary"
: "border-border bg-background text-muted-foreground hover:bg-muted"
)}
>
Modèle {AUTOMATION_DOMAIN_LABELS[domain]}
</button>
/>
))}
</div>
</div>
) : null}
<div className="space-y-2">

File diff suppressed because one or more lines are too long