diff --git a/web/src/lib/api/client/app/automations/createAutomation.ts b/web/src/lib/api/client/app/automations/createAutomation.ts new file mode 100644 index 00000000..9b11f729 --- /dev/null +++ b/web/src/lib/api/client/app/automations/createAutomation.ts @@ -0,0 +1,11 @@ +import type { Automation, AutomationWrite } from "@/lib/api/models/app/automations/Automation"; +import Request from "../../Request"; + +export default async function createAutomation(w: AutomationWrite): Promise<{ automation: Automation }> { + return await Request<{ automation: Automation }>({ + method: "POST", + url: "/automations", + data: w, + authorization: true, + }); +} diff --git a/web/src/lib/api/client/app/automations/deleteAutomation.ts b/web/src/lib/api/client/app/automations/deleteAutomation.ts new file mode 100644 index 00000000..cc826ae4 --- /dev/null +++ b/web/src/lib/api/client/app/automations/deleteAutomation.ts @@ -0,0 +1,9 @@ +import Request from "../../Request"; + +export default async function deleteAutomation(id: string): Promise<{ deleted: boolean }> { + return await Request<{ deleted: boolean }>({ + method: "DELETE", + url: `/automations/${id}`, + authorization: true, + }); +} diff --git a/web/src/lib/api/client/app/automations/getAutomation.ts b/web/src/lib/api/client/app/automations/getAutomation.ts new file mode 100644 index 00000000..f096e8ef --- /dev/null +++ b/web/src/lib/api/client/app/automations/getAutomation.ts @@ -0,0 +1,10 @@ +import type { Automation } from "@/lib/api/models/app/automations/Automation"; +import Request from "../../Request"; + +export default async function getAutomation(id: string): Promise<{ automation: Automation }> { + return await Request<{ automation: Automation }>({ + method: "GET", + url: `/automations/${id}`, + authorization: true, + }); +} diff --git a/web/src/lib/api/client/app/automations/listAutomations.ts b/web/src/lib/api/client/app/automations/listAutomations.ts new file mode 100644 index 00000000..d9657e93 --- /dev/null +++ b/web/src/lib/api/client/app/automations/listAutomations.ts @@ -0,0 +1,10 @@ +import type { Automation } from "@/lib/api/models/app/automations/Automation"; +import Request from "../../Request"; + +export default async function listAutomations(): Promise<{ automations: Automation[] }> { + return await Request<{ automations: Automation[] }>({ + method: "GET", + url: "/automations", + authorization: true, + }); +} diff --git a/web/src/lib/api/client/app/automations/updateAutomation.ts b/web/src/lib/api/client/app/automations/updateAutomation.ts new file mode 100644 index 00000000..4d03212f --- /dev/null +++ b/web/src/lib/api/client/app/automations/updateAutomation.ts @@ -0,0 +1,11 @@ +import type { Automation, AutomationWrite } from "@/lib/api/models/app/automations/Automation"; +import Request from "../../Request"; + +export default async function updateAutomation(id: string, w: AutomationWrite): Promise<{ automation: Automation }> { + return await Request<{ automation: Automation }>({ + method: "PATCH", + url: `/automations/${id}`, + data: w, + authorization: true, + }); +} diff --git a/web/src/lib/api/hooks/app/automations/useAutomation.ts b/web/src/lib/api/hooks/app/automations/useAutomation.ts new file mode 100644 index 00000000..d0dfdaf2 --- /dev/null +++ b/web/src/lib/api/hooks/app/automations/useAutomation.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import getAutomation from "@/lib/api/client/app/automations/getAutomation"; + +export function useAutomation(id: string, enabled = true) { + return useQuery({ + queryKey: ["automations", id], + queryFn: () => getAutomation(id), + enabled: enabled && !!id, + staleTime: 15_000, + }); +} diff --git a/web/src/lib/api/hooks/app/automations/useAutomationMutations.ts b/web/src/lib/api/hooks/app/automations/useAutomationMutations.ts new file mode 100644 index 00000000..c1144866 --- /dev/null +++ b/web/src/lib/api/hooks/app/automations/useAutomationMutations.ts @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import createAutomation from "@/lib/api/client/app/automations/createAutomation"; +import updateAutomation from "@/lib/api/client/app/automations/updateAutomation"; +import deleteAutomation from "@/lib/api/client/app/automations/deleteAutomation"; +import type { AutomationWrite } from "@/lib/api/models/app/automations/Automation"; + +export function useCreateAutomation() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (w: AutomationWrite) => createAutomation(w), + onSuccess: () => void qc.invalidateQueries({ queryKey: ["automations"] }), + }); +} + +export function useUpdateAutomation() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, w }: { id: string; w: AutomationWrite }) => updateAutomation(id, w), + onSuccess: (_res, vars) => { + void qc.invalidateQueries({ queryKey: ["automations"] }); + void qc.invalidateQueries({ queryKey: ["automations", vars.id] }); + }, + }); +} + +export function useDeleteAutomation() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => deleteAutomation(id), + onSuccess: () => void qc.invalidateQueries({ queryKey: ["automations"] }), + }); +} diff --git a/web/src/lib/api/hooks/app/automations/useAutomations.ts b/web/src/lib/api/hooks/app/automations/useAutomations.ts new file mode 100644 index 00000000..5c1acea8 --- /dev/null +++ b/web/src/lib/api/hooks/app/automations/useAutomations.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import listAutomations from "@/lib/api/client/app/automations/listAutomations"; + +export function useAutomations() { + return useQuery({ + queryKey: ["automations"], + queryFn: listAutomations, + staleTime: 15_000, + }); +} diff --git a/web/src/lib/api/models/app/automations/Automation.ts b/web/src/lib/api/models/app/automations/Automation.ts new file mode 100644 index 00000000..7f976943 --- /dev/null +++ b/web/src/lib/api/models/app/automations/Automation.ts @@ -0,0 +1,32 @@ +import type { IntegrationAction } from "@/lib/api/models/app/integrations/Integration"; + +// One action node of an automation: run `action` on `connection_id` with config. +export interface AutomationStep { + id?: string; + connection_id: string; + action: IntegrationAction; + config?: Record; +} + +// An automation = one trigger event -> a set of action steps. Execution reuses +// the integration event-subscription dispatcher (each step is a subscription). +export interface Automation { + id: string; + organization_id: string; + name: string; + enabled: boolean; + trigger_event: string; + filter?: Record; + created_at: string; + updated_at: string; + steps: AutomationStep[]; +} + +// Create/update payload from the flow builder. +export interface AutomationWrite { + name: string; + enabled: boolean; + trigger_event: string; + filter?: Record; + steps: AutomationStep[]; +} diff --git a/web/src/lib/api/models/app/automations/meta.ts b/web/src/lib/api/models/app/automations/meta.ts new file mode 100644 index 00000000..7930cfd3 --- /dev/null +++ b/web/src/lib/api/models/app/automations/meta.ts @@ -0,0 +1,53 @@ +// Trigger + action vocabulary the Automations flow builder renders from. Kept in +// sync with the backend's subscribable events (catalog crmEvents/notifyEvents) +// and the per-provider action handlers. + +import { EVENT_LABELS } from "@/lib/api/models/app/integrations/Integration"; + +// Warmbly events that can trigger an automation. Order = how they're listed. +export const TRIGGER_EVENTS: string[] = [ + "campaign.reply_received", + "meeting.booked", + "meeting.rescheduled", + "meeting.canceled", + "campaign.email_bounced", + "campaign.unsubscribed", + "warmup.health_changed", + "deliverability.complaint", +]; + +export function triggerLabel(ev: string): string { + return EVENT_LABELS[ev] ?? ev; +} + +// Human labels for the provider action handlers (the action a step performs). +export const ACTION_LABELS: Record = { + "slack.notify": "Send a Slack message", + "discord.notify": "Send a Discord message", + "hubspot.upsert_contact": "Create / update HubSpot contact", + "pipedrive.upsert_person": "Create / update Pipedrive person", + "salesforce.upsert_contact": "Create / update Salesforce contact", + "close.upsert_lead": "Create / update Close lead", + "webhook.ping": "Send a webhook", +}; + +export function actionLabel(a: string): string { + return ACTION_LABELS[a] ?? a; +} + +// Per-action config field needs, so the node editor shows the right inputs. +export function actionNeedsChannel(action: string): boolean { + return action === "slack.notify"; +} +export function actionNeedsURL(action: string): boolean { + return action === "webhook.ping"; +} +// Notify-style actions accept an optional custom message template. +export function actionSupportsTemplate(action: string): boolean { + return action === "slack.notify" || action === "discord.notify" || action === "webhook.ping"; +} + +// Only the reply trigger carries intent / confidence filters. +export function triggerSupportsIntentFilter(ev: string): boolean { + return ev === "campaign.reply_received"; +}