From 3e3117e4a7fbb5f2b029de0bd2ad837928675878 Mon Sep 17 00:00:00 2001 From: Hana Chang Date: Tue, 24 Mar 2026 16:44:45 +0800 Subject: [PATCH 1/2] fix: replace Select/datalist with styled Combobox for event name inputs Replace the conditional Select/Input pattern with a unified Combobox (Input + Command dropdown) that always allows free-text input while offering autocomplete suggestions from previously tracked events. Applied to both workflow creation dialog and edit page (trigger event, add WAIT_FOR_EVENT step, edit WAIT_FOR_EVENT step). Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/src/pages/workflows/[id].tsx | 182 +++++++++++++++---------- apps/web/src/pages/workflows/index.tsx | 77 +++++++---- 2 files changed, 163 insertions(+), 96 deletions(-) diff --git a/apps/web/src/pages/workflows/[id].tsx b/apps/web/src/pages/workflows/[id].tsx index 14b2f957..70b18767 100644 --- a/apps/web/src/pages/workflows/[id].tsx +++ b/apps/web/src/pages/workflows/[id].tsx @@ -26,6 +26,11 @@ import { SelectItemWithDescription, SelectTrigger, SelectValue, + Command, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, Switch, } from '@plunk/ui'; import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db'; @@ -793,6 +798,7 @@ function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogPr const [description, setDescription] = useState(workflow.description ?? ''); const [allowReentry, setAllowReentry] = useState(workflow.allowReentry ?? false); const [eventName, setEventName] = useState(triggerConfig?.eventName ?? ''); + const [eventPopoverOpen, setEventPopoverOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); // Sync state when workflow changes or dialog opens @@ -852,29 +858,44 @@ function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogPr
- {eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( - - ) : ( +
setEventName(e.target.value)} + onChange={e => { + setEventName(e.target.value); + setEventPopoverOpen(true); + }} + onFocus={() => setEventPopoverOpen(true)} + onBlur={() => { + setTimeout(() => setEventPopoverOpen(false), 150); + }} placeholder="e.g., contact.created, email.opened" required + autoComplete="off" /> - )} + {eventPopoverOpen && eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 && ( +
+ + + + No matching events + + + {eventNamesData.eventNames + .filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) + .map(n => ( + { setEventName(n); setEventPopoverOpen(false); }}> + {n} + + ))} + + + +
+ )} +

The event that triggers this workflow to start for a contact

@@ -961,6 +982,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo // WAIT_FOR_EVENT fields const [eventName, setEventName] = useState(''); + const [eventPopoverOpen, setEventPopoverOpen] = useState(false); const [eventTimeoutAmount, setEventTimeoutAmount] = useState('1'); const [eventTimeoutUnit, setEventTimeoutUnit] = useState<'minutes' | 'hours' | 'days'>('days'); @@ -1611,34 +1633,47 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo - {eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( - - ) : ( +
setEventName(e.target.value)} + onChange={e => { + setEventName(e.target.value); + setEventPopoverOpen(true); + }} + onFocus={() => setEventPopoverOpen(true)} + onBlur={() => { + setTimeout(() => setEventPopoverOpen(false), 150); + }} required placeholder="e.g., email.clicked, user.upgraded" className="mt-1.5" + autoComplete="off" /> - )} + {eventPopoverOpen && eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 && ( +
+ + + + No matching events + + + {eventNamesData.eventNames + .filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) + .map(n => ( + { setEventName(n); setEventPopoverOpen(false); }}> + {n} + + ))} + + + +
+ )} +

- {eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 - ? 'The workflow will pause until this event occurs' - : 'Enter the event name to wait for'} + Enter the event name to wait for, or select from previously tracked events

@@ -1991,6 +2026,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS // WAIT_FOR_EVENT fields const [eventName, setEventName] = useState(String(config?.eventName || '')); + const [eventPopoverOpen, setEventPopoverOpen] = useState(false); const [eventTimeoutAmount, setEventTimeoutAmount] = useState(() => { const timeoutSeconds = Number(config?.timeout) || 86400; // Convert seconds to most appropriate unit @@ -2793,40 +2829,48 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
- {eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( - <> - -

- Select from previously tracked events in your project -

- - ) : ( - <> - setEventName(e.target.value)} - required - placeholder="e.g., email.clicked, user.upgraded" - className="mt-1.5" - /> -

- The workflow will pause until this event is triggered by the contact -

- - )} +
+ { + setEventName(e.target.value); + setEventPopoverOpen(true); + }} + onFocus={() => setEventPopoverOpen(true)} + onBlur={() => { + setTimeout(() => setEventPopoverOpen(false), 150); + }} + required + placeholder="e.g., email.clicked, user.upgraded" + className="mt-1.5" + autoComplete="off" + /> + {eventPopoverOpen && eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 && ( +
+ + + + No matching events + + + {eventNamesData.eventNames + .filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) + .map(n => ( + { setEventName(n); setEventPopoverOpen(false); }}> + {n} + + ))} + + + +
+ )} +
+

+ Enter the event name to wait for, or select from previously tracked events +

diff --git a/apps/web/src/pages/workflows/index.tsx b/apps/web/src/pages/workflows/index.tsx index 8e825d98..80887865 100644 --- a/apps/web/src/pages/workflows/index.tsx +++ b/apps/web/src/pages/workflows/index.tsx @@ -6,6 +6,11 @@ import { CardDescription, CardHeader, CardTitle, + Command, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, ConfirmDialog, Dialog, DialogContent, @@ -14,11 +19,6 @@ import { DialogTitle, Input, Label, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, } from '@plunk/ui'; import type {Workflow} from '@plunk/db'; import type {PaginatedResponse} from '@plunk/types'; @@ -321,6 +321,7 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [eventName, setEventName] = useState(''); + const [eventPopoverOpen, setEventPopoverOpen] = useState(false); const [allowReentry, setAllowReentry] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); @@ -391,34 +392,56 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia
- - {eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( - - ) : ( + + {/* Combobox: 可自由輸入 event name,同時提供已追蹤 event 的下拉建議 */} +
setEventName(e.target.value)} - required + onChange={e => { + setEventName(e.target.value); + setEventPopoverOpen(true); + }} + onFocus={() => setEventPopoverOpen(true)} + onBlur={() => { + // 延遲關閉,讓 CommandItem 的 onSelect 有時間觸發 + setTimeout(() => setEventPopoverOpen(false), 150); + }} placeholder="e.g., contact.created, email.opened" + required + autoComplete="off" /> - )} + {eventPopoverOpen && eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 && ( +
+ + + + No matching events + + + {eventNamesData.eventNames + .filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) + .map(n => ( + { + setEventName(n); + setEventPopoverOpen(false); + }} + > + {n} + + ))} + + + +
+ )} +

- {eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 - ? 'Select from previously tracked events' - : 'No events tracked yet. Enter the event name that will trigger this workflow.'} + The event that triggers this workflow to start for a contact

From 3214f6c42d4ca6ab628e905ac69ed82648ccf47c Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Wed, 1 Apr 2026 18:55:32 +0200 Subject: [PATCH 2/2] fix: Hint custom event names in combobox when no matches are found --- apps/web/src/pages/workflows/[id].tsx | 55 +++++++++++++++++--------- apps/web/src/pages/workflows/index.tsx | 22 +++++++---- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/apps/web/src/pages/workflows/[id].tsx b/apps/web/src/pages/workflows/[id].tsx index 70b18767..4288b89a 100644 --- a/apps/web/src/pages/workflows/[id].tsx +++ b/apps/web/src/pages/workflows/[id].tsx @@ -27,7 +27,6 @@ import { SelectTrigger, SelectValue, Command, - CommandEmpty, CommandGroup, CommandItem, CommandList, @@ -875,21 +874,27 @@ function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogPr required autoComplete="off" /> - {eventPopoverOpen && eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 && ( + {eventPopoverOpen && ((eventNamesData?.eventNames?.length ?? 0) > 0 || eventName?.trim()) && (
- - No matching events - - {eventNamesData.eventNames - .filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) + {eventNamesData?.eventNames + ?.filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) .map(n => ( { setEventName(n); setEventPopoverOpen(false); }}> {n} ))} + {eventName?.trim() && !eventNamesData?.eventNames?.some(n => n === eventName.trim()) && ( + { setEventName(eventName.trim()); setEventPopoverOpen(false); }} + > + Use “{eventName.trim()}” + + )} @@ -1651,21 +1656,27 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo className="mt-1.5" autoComplete="off" /> - {eventPopoverOpen && eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 && ( + {eventPopoverOpen && ((eventNamesData?.eventNames?.length ?? 0) > 0 || eventName?.trim()) && (
- - No matching events - - {eventNamesData.eventNames - .filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) + {eventNamesData?.eventNames + ?.filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) .map(n => ( { setEventName(n); setEventPopoverOpen(false); }}> {n} ))} + {eventName?.trim() && !eventNamesData?.eventNames?.some(n => n === eventName.trim()) && ( + { setEventName(eventName.trim()); setEventPopoverOpen(false); }} + > + Use “{eventName.trim()}” + + )} @@ -2847,21 +2858,27 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS className="mt-1.5" autoComplete="off" /> - {eventPopoverOpen && eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 && ( + {eventPopoverOpen && ((eventNamesData?.eventNames?.length ?? 0) > 0 || eventName?.trim()) && (
- - No matching events - - {eventNamesData.eventNames - .filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) + {eventNamesData?.eventNames + ?.filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) .map(n => ( { setEventName(n); setEventPopoverOpen(false); }}> {n} ))} + {eventName?.trim() && !eventNamesData?.eventNames?.some(n => n === eventName.trim()) && ( + { setEventName(eventName.trim()); setEventPopoverOpen(false); }} + > + Use “{eventName.trim()}” + + )} diff --git a/apps/web/src/pages/workflows/index.tsx b/apps/web/src/pages/workflows/index.tsx index 80887865..e225b32a 100644 --- a/apps/web/src/pages/workflows/index.tsx +++ b/apps/web/src/pages/workflows/index.tsx @@ -7,7 +7,6 @@ import { CardHeader, CardTitle, Command, - CommandEmpty, CommandGroup, CommandItem, CommandList, @@ -412,16 +411,13 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia required autoComplete="off" /> - {eventPopoverOpen && eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 && ( + {eventPopoverOpen && ((eventNamesData?.eventNames?.length ?? 0) > 0 || eventName?.trim()) && (
- - No matching events - - {eventNamesData.eventNames - .filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) + {eventNamesData?.eventNames + ?.filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase())) .map(n => ( ))} + {eventName?.trim() && !eventNamesData?.eventNames?.some(n => n === eventName.trim()) && ( + { + setEventName(eventName.trim()); + setEventPopoverOpen(false); + }} + > + Use “{eventName.trim()}” + + )}