feat(automations): add deal pipeline selectors

This commit is contained in:
bellopushon 2026-06-26 15:57:06 -04:00
parent b2ed58a41f
commit 377c1b738a

View File

@ -167,7 +167,7 @@ function blankConfig(type: AutomationStepType): Record<string, unknown> {
} }
// ------------------------------------------------------------ // ------------------------------------------------------------
// Account resources (tags, members, approved templates) // Account resources (tags, members, approved templates, pipelines)
// //
// Loaded once at the builder root and shared via context so the // Loaded once at the builder root and shared via context so the
// tag / agent / template pickers below can offer existing resources // tag / agent / template pickers below can offer existing resources
@ -181,6 +181,20 @@ interface AutomationResources {
members: AccountMember[] members: AccountMember[]
templates: MessageTemplate[] templates: MessageTemplate[]
customFields: CustomField[] customFields: CustomField[]
pipelines: PipelineOption[]
stages: PipelineStageOption[]
}
interface PipelineOption {
id: string
name: string
}
interface PipelineStageOption {
id: string
name: string
pipeline_id: string
position: number
} }
const ResourcesContext = createContext<AutomationResources>({ const ResourcesContext = createContext<AutomationResources>({
@ -188,6 +202,8 @@ const ResourcesContext = createContext<AutomationResources>({
members: [], members: [],
templates: [], templates: [],
customFields: [], customFields: [],
pipelines: [],
stages: [],
}) })
function useResources(): AutomationResources { function useResources(): AutomationResources {
@ -199,6 +215,8 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
const [members, setMembers] = useState<AccountMember[]>([]) const [members, setMembers] = useState<AccountMember[]>([])
const [templates, setTemplates] = useState<MessageTemplate[]>([]) const [templates, setTemplates] = useState<MessageTemplate[]>([])
const [customFields, setCustomFields] = useState<CustomField[]>([]) const [customFields, setCustomFields] = useState<CustomField[]>([])
const [pipelines, setPipelines] = useState<PipelineOption[]>([])
const [stages, setStages] = useState<PipelineStageOption[]>([])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@ -209,19 +227,27 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
// actually be sent (anything else 400s at send time), matching the // actually be sent (anything else 400s at send time), matching the
// broadcast picker. // broadcast picker.
void (async () => { void (async () => {
const [tagsRes, templatesRes, customFieldsRes] = await Promise.all([ const [tagsRes, templatesRes, customFieldsRes, pipelinesRes, stagesRes] =
supabase.from("tags").select("*").order("name"), await Promise.all([
supabase supabase.from("tags").select("*").order("name"),
.from("message_templates") supabase
.select("*") .from("message_templates")
.eq("status", "APPROVED") .select("*")
.order("name"), .eq("status", "APPROVED")
supabase.from("custom_fields").select("*").order("field_name"), .order("name"),
]) supabase.from("custom_fields").select("*").order("field_name"),
supabase.from("pipelines").select("id, name").order("name"),
supabase
.from("pipeline_stages")
.select("id, name, pipeline_id, position")
.order("position"),
])
if (cancelled) return if (cancelled) return
setTags((tagsRes.data as TagRecord[] | null) ?? []) setTags((tagsRes.data as TagRecord[] | null) ?? [])
setTemplates((templatesRes.data as MessageTemplate[] | null) ?? []) setTemplates((templatesRes.data as MessageTemplate[] | null) ?? [])
setCustomFields((customFieldsRes.data as CustomField[] | null) ?? []) setCustomFields((customFieldsRes.data as CustomField[] | null) ?? [])
setPipelines((pipelinesRes.data as PipelineOption[] | null) ?? [])
setStages((stagesRes.data as PipelineStageOption[] | null) ?? [])
})() })()
// Members go through the API so we inherit its email-visibility // Members go through the API so we inherit its email-visibility
@ -244,7 +270,9 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
}, []) }, [])
return ( return (
<ResourcesContext.Provider value={{ tags, members, templates, customFields }}> <ResourcesContext.Provider
value={{ tags, members, templates, customFields, pipelines, stages }}
>
{children} {children}
</ResourcesContext.Provider> </ResourcesContext.Provider>
) )
@ -382,6 +410,102 @@ function AgentSelect({
) )
} }
/** Pipeline + stage picker for Create Deal. The automation stores ids because
* the engine writes directly to deals, but authors should choose by name. */
function DealPipelineFields({
pipelineId,
stageId,
onChange,
}: {
pipelineId: string
stageId: string
onChange: (patch: { pipeline_id: string; stage_id: string }) => void
}) {
const { pipelines, stages } = useResources()
if (pipelines.length === 0) {
return (
<>
<FieldBlock label="Pipeline id">
<Input
value={pipelineId}
onChange={(e) =>
onChange({ pipeline_id: e.target.value, stage_id: stageId })
}
className="bg-muted text-foreground"
/>
</FieldBlock>
<FieldBlock label="Stage id">
<Input
value={stageId}
onChange={(e) =>
onChange({ pipeline_id: pipelineId, stage_id: e.target.value })
}
className="bg-muted text-foreground"
/>
</FieldBlock>
</>
)
}
const selectedPipeline = pipelines.find((p) => p.id === pipelineId)
const stageOptions = stages.filter((s) => s.pipeline_id === pipelineId)
const selectedStage = stageOptions.find((s) => s.id === stageId)
return (
<>
<FieldBlock label="Pipeline">
<select
value={pipelineId}
onChange={(e) => {
const nextPipelineId = e.target.value
const firstStage = stages.find(
(s) => s.pipeline_id === nextPipelineId
)
onChange({
pipeline_id: nextPipelineId,
stage_id: firstStage?.id ?? "",
})
}}
className={SELECT_CLASS}
>
<option value="">Select a pipeline…</option>
{pipelines.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
{pipelineId && !selectedPipeline && (
<option value={pipelineId}>{pipelineId} (unknown pipeline)</option>
)}
</select>
</FieldBlock>
<FieldBlock label="Stage">
<select
value={stageId}
onChange={(e) =>
onChange({ pipeline_id: pipelineId, stage_id: e.target.value })
}
className={SELECT_CLASS}
disabled={!pipelineId || stageOptions.length === 0}
>
<option value="">
{pipelineId ? "Select a stage…" : "Select a pipeline first…"}
</option>
{stageOptions.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
{stageId && pipelineId && !selectedStage && (
<option value={stageId}>{stageId} (unknown stage)</option>
)}
</select>
</FieldBlock>
</>
)
}
/** Template dropdown showing approved templates by name + language, /** Template dropdown showing approved templates by name + language,
* storing both template_name and language. Falls back to manual name + * storing both template_name and language. Falls back to manual name +
* language inputs when no approved templates are synced yet. */ * language inputs when no approved templates are synced yet. */
@ -1125,20 +1249,11 @@ function StepEditor({
case "create_deal": case "create_deal":
return ( return (
<> <>
<FieldBlock label="Pipeline id"> <DealPipelineFields
<Input pipelineId={(cfg.pipeline_id as string) ?? ""}
value={(cfg.pipeline_id as string) ?? ""} stageId={(cfg.stage_id as string) ?? ""}
onChange={(e) => set({ pipeline_id: e.target.value })} onChange={(patch) => set(patch)}
className="bg-muted text-foreground" />
/>
</FieldBlock>
<FieldBlock label="Stage id">
<Input
value={(cfg.stage_id as string) ?? ""}
onChange={(e) => set({ stage_id: e.target.value })}
className="bg-muted text-foreground"
/>
</FieldBlock>
<FieldBlock label="Title"> <FieldBlock label="Title">
<Input <Input
value={(cfg.title as string) ?? ""} value={(cfg.title as string) ?? ""}