From a6290748e6eb43e2e310e29b3091d28e7d346bfb Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Mon, 8 Jun 2026 06:55:37 +0200 Subject: [PATCH] feat: move automation editing out of integrations --- .../_components/ConnectionDetail.tsx | 281 ------------------ 1 file changed, 281 deletions(-) diff --git a/web/src/app/app/integrations/_components/ConnectionDetail.tsx b/web/src/app/app/integrations/_components/ConnectionDetail.tsx index 39a71593e..6d97ad443 100644 --- a/web/src/app/app/integrations/_components/ConnectionDetail.tsx +++ b/web/src/app/app/integrations/_components/ConnectionDetail.tsx @@ -30,7 +30,6 @@ import { import toast from "react-hot-toast"; import { Label, TextInput } from "@/components/ui/field"; -import { SelectMenu, type SelectOption } from "@/components/ui/select-menu"; import { useConfirm } from "@/hooks/context/confirm"; import useConnectionDetail from "@/lib/api/hooks/app/integrations/useConnectionDetail"; import useDisconnectIntegration from "@/lib/api/hooks/app/integrations/useDisconnectIntegration"; @@ -38,19 +37,11 @@ import { useFinishIntegrationOAuth, useReauthIntegration, } from "@/lib/api/hooks/app/integrations/useIntegrationOAuth"; -import { - useCreateConnectionEvent, - useDeleteConnectionEvent, -} from "@/lib/api/hooks/app/integrations/useConnectionEvents"; import { openOAuthPopup } from "@/lib/integrations/oauthPopup"; import { - defaultActionForProvider, - EVENT_LABELS, - REPLY_INTENT_OPTIONS, type CapabilityObject, type IntegrationCatalogEntry, type IntegrationConnection, - type IntegrationEventSubscription, } from "@/lib/api/models/app/integrations/Integration"; import { useFieldMappings, useUpdateConnectionConfig } from "@/lib/api/hooks/app/integrations/useFieldMappings"; import { useRevealWebhookSecret, useTestConnection } from "@/lib/api/hooks/app/integrations/useConnectionWebhookTools"; @@ -60,8 +51,6 @@ import { Drawer, SectionLabel } from "./ConnectDrawer"; import FieldMapEditor from "./FieldMapEditor"; import StatusPill, { HealthDot } from "./StatusPill"; -const REPLY_EVENT = "campaign.reply_received"; - // Providers whose deliveries we can test (notify + generic webhook). Automation // tools additionally expose an HMAC signing secret for verification. const WEBHOOK_TOOL_PROVIDERS = ["slack", "discord", "zapier", "make", "n8n"]; @@ -80,10 +69,7 @@ export default function ConnectionDetail({ const disconnect = useDisconnectIntegration(); const reauth = useReauthIntegration(); const finishOAuth = useFinishIntegrationOAuth(); - const createEvent = useCreateConnectionEvent(); - const deleteEvent = useDeleteConnectionEvent(); - const [adding, setAdding] = React.useState(false); const [busy, setBusy] = React.useState(false); const confirm = useConfirm(); @@ -91,9 +77,6 @@ export default function ConnectionDetail({ const events = detail.data?.events ?? []; const runs = detail.data?.runs ?? []; - // Only offer events the provider actually has a handler for; an empty list - // means this connection has no automations (e.g. a meeting provider). - const availableEvents = entry?.events ?? []; const capability = entry?.capability; const crmObject = capability?.objects?.[0]; const isOAuth = conn.auth_method === "oauth"; @@ -126,22 +109,6 @@ export default function ConnectionDetail({ }); } - async function addAutomation(eventType: string, config: Record) { - try { - await createEvent.mutateAsync({ - connectionId: conn.id, - event_type: eventType, - action: defaultActionForProvider(conn.provider), - config, - enabled: true, - }); - toast.success("Automation added"); - setAdding(false); - detail.refetch(); - } catch (err: unknown) { - toast.error(msg(err) ?? "Could not add automation"); - } - } return ( @@ -197,54 +164,6 @@ export default function ConnectionDetail({ )} - {/* Automations — the core "how you use it" surface */} - {(availableEvents.length > 0 || events.length > 0) && ( -
-
- Automations - {!adding && availableEvents.length > 0 && ( - - )} -
- - {events.length === 0 && !adding && ( -

- No automations yet. Add a rule to push Warmbly events into {entry?.name ?? conn.label} — - e.g. ping a channel when a prospect replies, or upsert a contact in your CRM. -

- )} - - {events.map((ev) => ( - - deleteEvent - .mutateAsync({ connectionId: conn.id, eventId: ev.id }) - .then(() => detail.refetch()) - } - /> - ))} - - {adding && ( - setAdding(false)} - onAdd={addAutomation} - busy={createEvent.isPending} - /> - )} -
- )} - {/* Field mapping — control exactly what each CRM record gets */} {crmObject && (
@@ -328,206 +247,6 @@ export default function ConnectionDetail({ ); } -// AutomationRow renders one configured rule with a human summary of its -// trigger, filters, destination, and custom message. -function AutomationRow({ sub, onDelete }: { sub: IntegrationEventSubscription; onDelete: () => void }) { - const cfg = (sub.config ?? {}) as Record; - const intents = Array.isArray(cfg.intents) ? (cfg.intents as string[]) : []; - const minConf = typeof cfg.min_confidence === "number" ? cfg.min_confidence : undefined; - const dest = - (cfg.channel as string) || (cfg.url as string) || (cfg.webhook_url as string) || ""; - const tmpl = (cfg.message_template as string) || ""; - - const filters: string[] = []; - if (intents.length) filters.push(intents.join(", ")); - if (minConf) filters.push(`≥${Math.round(minConf * 100)}%`); - - return ( -
-
- -
-
- {EVENT_LABELS[sub.event_type] ?? sub.event_type} -
-
- {dest || sub.action} - {filters.length > 0 && ` · ${filters.join(" · ")}`} -
-
- -
- {tmpl && ( -

“{tmpl}”

- )} -
- ); -} - -// AddAutomation is the rule builder. Everything a user needs to customize a -// behaviour lives here: trigger, reply-intent + confidence filters, the -// destination, and a custom message template. -function AddAutomation({ - provider, - availableEvents, - onAdd, - onCancel, - busy, -}: { - provider: string; - availableEvents: string[]; - onAdd: (eventType: string, config: Record) => void; - onCancel: () => void; - busy: boolean; -}) { - const [eventType, setEventType] = React.useState( - availableEvents.includes(REPLY_EVENT) ? REPLY_EVENT : availableEvents[0] ?? REPLY_EVENT, - ); - const eventOptions = React.useMemo( - () => availableEvents.map((ev) => ({ value: ev, label: EVENT_LABELS[ev] ?? ev })), - [availableEvents], - ); - const [dest, setDest] = React.useState(""); - const [intents, setIntents] = React.useState([]); - const [minConf, setMinConf] = React.useState(0); - const [template, setTemplate] = React.useState(""); - - const needsChannel = provider === "slack"; - const needsURL = - provider === "discord" || provider === "zapier" || provider === "make" || provider === "n8n"; - const isReplyTrigger = eventType === REPLY_EVENT; - const destRequired = needsChannel || needsURL; - - function toggleIntent(v: string) { - setIntents((cur) => (cur.includes(v) ? cur.filter((x) => x !== v) : [...cur, v])); - } - - function buildConfig(): Record { - const cfg: Record = {}; - if (needsChannel && dest.trim()) cfg.channel = dest.trim(); - if (needsURL && dest.trim()) cfg.url = dest.trim(); - if (isReplyTrigger && intents.length) cfg.intents = intents; - if (isReplyTrigger && minConf > 0) cfg.min_confidence = minConf; - if (template.trim()) cfg.message_template = template.trim(); - return cfg; - } - - const destLabel = needsChannel ? "Channel" : "Destination URL"; - const destPlaceholder = needsChannel ? "#sales" : "https://…"; - const canSubmit = !busy && !(destRequired && !dest.trim()); - - return ( -
-
- - -
- - {/* Reply-only filters */} - {isReplyTrigger && ( -
- -
- {REPLY_INTENT_OPTIONS.map((opt) => { - const on = intents.includes(opt.value); - return ( - - ); - })} -
-
- - setMinConf(Number(e.target.value) / 100)} - className="w-full accent-sky-600" - /> -
-
- )} - - {(needsChannel || needsURL) && ( -
- - -
- )} - - {/* Custom message (for notification-style actions) */} - {(needsChannel || provider === "discord" || needsURL) && ( -
- -