Merge pull request #310 from bellopushon/feat/automation-deal-selectors

feat(automations): add deal pipeline selectors
This commit is contained in:
Arnas Donauskas 2026-07-01 11:46:13 +03:00 committed by GitHub
commit 427b4612da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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
// tag / agent / template pickers below can offer existing resources
@ -181,6 +181,20 @@ interface AutomationResources {
members: AccountMember[]
templates: MessageTemplate[]
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>({
@ -188,6 +202,8 @@ const ResourcesContext = createContext<AutomationResources>({
members: [],
templates: [],
customFields: [],
pipelines: [],
stages: [],
})
function useResources(): AutomationResources {
@ -199,6 +215,8 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
const [members, setMembers] = useState<AccountMember[]>([])
const [templates, setTemplates] = useState<MessageTemplate[]>([])
const [customFields, setCustomFields] = useState<CustomField[]>([])
const [pipelines, setPipelines] = useState<PipelineOption[]>([])
const [stages, setStages] = useState<PipelineStageOption[]>([])
useEffect(() => {
let cancelled = false
@ -209,7 +227,8 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
// actually be sent (anything else 400s at send time), matching the
// broadcast picker.
void (async () => {
const [tagsRes, templatesRes, customFieldsRes] = await Promise.all([
const [tagsRes, templatesRes, customFieldsRes, pipelinesRes, stagesRes] =
await Promise.all([
supabase.from("tags").select("*").order("name"),
supabase
.from("message_templates")
@ -217,11 +236,18 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
.eq("status", "APPROVED")
.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
setTags((tagsRes.data as TagRecord[] | null) ?? [])
setTemplates((templatesRes.data as MessageTemplate[] | 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
@ -244,7 +270,9 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
}, [])
return (
<ResourcesContext.Provider value={{ tags, members, templates, customFields }}>
<ResourcesContext.Provider
value={{ tags, members, templates, customFields, pipelines, stages }}
>
{children}
</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,
* storing both template_name and language. Falls back to manual name +
* language inputs when no approved templates are synced yet. */
@ -1125,20 +1249,11 @@ function StepEditor({
case "create_deal":
return (
<>
<FieldBlock label="Pipeline id">
<Input
value={(cfg.pipeline_id as string) ?? ""}
onChange={(e) => set({ pipeline_id: e.target.value })}
className="bg-muted text-foreground"
<DealPipelineFields
pipelineId={(cfg.pipeline_id as string) ?? ""}
stageId={(cfg.stage_id as string) ?? ""}
onChange={(patch) => set(patch)}
/>
</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">
<Input
value={(cfg.title as string) ?? ""}