Lots of changes to the API and webhooks
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
This commit is contained in:
parent
20552a34ff
commit
636b8cf789
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user