mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-06 16:01:28 +00:00
feat: surface the inbound webhook trigger in the automations builder with a copyable per-automation webhook URL and caller-defined payload conditions
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
CheckCircle2Icon,
|
||||
CheckIcon,
|
||||
CheckSquareIcon,
|
||||
CopyIcon,
|
||||
GitBranchIcon,
|
||||
GlobeIcon,
|
||||
HistoryIcon,
|
||||
@@ -101,8 +102,10 @@ import {
|
||||
isNativeAction,
|
||||
nativeActionNeeds,
|
||||
triggerCarriesThread,
|
||||
triggerIsInboundWebhook,
|
||||
type TriggerFieldDef,
|
||||
} from "@/lib/api/models/app/automations/meta";
|
||||
import { API_URL } from "@/lib/information";
|
||||
import CategoryPicker from "@/components/app/contacts/CategoryPicker";
|
||||
import { ExpressionReference } from "@/components/app/automations/ExpressionReference";
|
||||
import DealStagePicker from "@/components/app/crm/DealStagePicker";
|
||||
@@ -1083,6 +1086,7 @@ export default function AutomationFlow({
|
||||
setTrigger(ev);
|
||||
updateNodeData("trigger", { label: triggerLabel(ev) });
|
||||
}}
|
||||
inboundUrl={automation.inbound_url}
|
||||
// condition
|
||||
onCondition={(cond) => updateNodeData(selectedNode.id, { condition: cond, label: conditionLabel(cond) })}
|
||||
// action
|
||||
@@ -1257,6 +1261,56 @@ function InsightsPanel({
|
||||
);
|
||||
}
|
||||
|
||||
// InboundUrlField shows the automation's unique inbound-webhook URL (read-only)
|
||||
// with a copy button. The stored value is a path; we prefix the API origin so
|
||||
// the copied value is the full URL an external system POSTs to.
|
||||
function InboundUrlField({ inboundUrl }: { inboundUrl?: string }) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
if (!inboundUrl) {
|
||||
return (
|
||||
<p className="text-[11.5px] text-slate-400 leading-relaxed">
|
||||
Save this automation to generate its unique webhook URL. An external system POSTs JSON to that URL and
|
||||
the body becomes the event payload (reference any field with <code>{"{{.field}}"}</code>).
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const full = `${API_URL}${inboundUrl}`;
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(full);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch {
|
||||
/* clipboard blocked — the value is still selectable in the field */
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Label>Your webhook URL</Label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
readOnly
|
||||
value={full}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
className="flex-1 h-7 rounded-md border border-slate-200 bg-slate-50 px-2 text-[11.5px] font-mono text-slate-700 focus:border-sky-400 focus:ring-2 focus:ring-sky-100 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="h-7 px-2 rounded-md border border-slate-200 inline-flex items-center gap-1 text-[11.5px] text-slate-600 hover:bg-slate-50 shrink-0"
|
||||
>
|
||||
{copied ? <CheckIcon className="w-3.5 h-3.5 text-emerald-600" /> : <CopyIcon className="w-3.5 h-3.5" />}
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11.5px] text-slate-400 mt-1.5 leading-relaxed">
|
||||
POST JSON here to run this automation. The body becomes the event payload, so reference its keys with{" "}
|
||||
<code>{"{{.field}}"}</code> in actions and conditions.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Editor panel ─────────────────────────────────────────────────────────────
|
||||
function NodeEditor({
|
||||
node,
|
||||
@@ -1264,6 +1318,7 @@ function NodeEditor({
|
||||
selfId,
|
||||
trigger,
|
||||
onTrigger,
|
||||
inboundUrl,
|
||||
onCondition,
|
||||
targets,
|
||||
connLabel,
|
||||
@@ -1276,6 +1331,7 @@ function NodeEditor({
|
||||
selfId: string;
|
||||
trigger: string;
|
||||
onTrigger: (ev: string) => void;
|
||||
inboundUrl?: string;
|
||||
onCondition: (c: AutomationCondition) => void;
|
||||
targets: IntegrationConnection[];
|
||||
connLabel: (id?: string) => string;
|
||||
@@ -1316,9 +1372,13 @@ function NodeEditor({
|
||||
<Label>When this happens</Label>
|
||||
<SelectMenu value={trigger} onChange={onTrigger} options={triggerOptions} className="w-full" />
|
||||
</div>
|
||||
<p className="text-[11.5px] text-slate-400 leading-relaxed">
|
||||
Add a condition (IF) below the trigger to branch on the event — e.g. only positive replies, or by source.
|
||||
</p>
|
||||
{triggerIsInboundWebhook(trigger) ? (
|
||||
<InboundUrlField inboundUrl={inboundUrl} />
|
||||
) : (
|
||||
<p className="text-[11.5px] text-slate-400 leading-relaxed">
|
||||
Add a condition (IF) below the trigger to branch on the event — e.g. only positive replies, or by source.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : isCondition ? (
|
||||
<ConditionEditor
|
||||
|
||||
@@ -49,6 +49,9 @@ export interface Automation {
|
||||
graph: AutomationGraph;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
// Public POST path that fires this automation, set only when its trigger is
|
||||
// the inbound webhook. Append to the API origin for the full URL.
|
||||
inbound_url?: string;
|
||||
}
|
||||
|
||||
export interface AutomationWrite {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const TRIGGER_EVENTS: string[] = [
|
||||
"campaign.unsubscribed",
|
||||
"warmup.health_changed",
|
||||
"deliverability.complaint",
|
||||
"inbound.webhook",
|
||||
"campaign.action",
|
||||
];
|
||||
|
||||
@@ -24,6 +25,13 @@ export function triggerLabel(ev: string): string {
|
||||
return EVENT_LABELS[ev] ?? ev;
|
||||
}
|
||||
|
||||
// The inbound-webhook trigger fires when an external system POSTs to this
|
||||
// automation's unique URL; the JSON body becomes the event payload, so its
|
||||
// fields are caller-defined rather than fixed.
|
||||
export function triggerIsInboundWebhook(ev: string): boolean {
|
||||
return ev === "inbound.webhook";
|
||||
}
|
||||
|
||||
// Human labels for the provider action handlers (the action a step performs).
|
||||
export const ACTION_LABELS: Record<string, string> = {
|
||||
"slack.notify": "Send a Slack message",
|
||||
@@ -302,6 +310,9 @@ export const TRIGGER_VARIABLES: Record<string, string[]> = {
|
||||
"campaign.unsubscribed": ["contact_email", "contact_id", "campaign_id", "source"],
|
||||
"warmup.health_changed": ["email", "new_state", "previous_state", "reason"],
|
||||
"campaign.action": ["contact_email", "contact_id", "campaign_id", "campaign_name", "first_name", "last_name", "company", "phone"],
|
||||
// Inbound webhook payloads are caller-defined, so there are no fixed
|
||||
// variables to offer; the user references their own JSON keys directly.
|
||||
"inbound.webhook": [],
|
||||
};
|
||||
|
||||
export function triggerVariables(triggerEvent: string): string[] {
|
||||
|
||||
@@ -305,6 +305,7 @@ export const EVENT_LABELS: Record<string, string> = {
|
||||
"meeting.rescheduled": "Meeting rescheduled",
|
||||
"meeting.canceled": "Meeting canceled",
|
||||
"campaign.action": "Launched by a campaign step",
|
||||
"inbound.webhook": "Inbound webhook",
|
||||
};
|
||||
|
||||
// Which action a provider performs for an event subscription.
|
||||
|
||||
Reference in New Issue
Block a user