Merge pull request #320 from hanamizuki/fix/wait-for-event-combobox

This commit is contained in:
Dries Augustyns
2026-04-01 18:56:00 +02:00
committed by GitHub
2 changed files with 188 additions and 96 deletions
+130 -69
View File
@@ -26,6 +26,10 @@ import {
SelectItemWithDescription, SelectItemWithDescription,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
Command,
CommandGroup,
CommandItem,
CommandList,
Switch, Switch,
} from '@plunk/ui'; } from '@plunk/ui';
import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db'; import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db';
@@ -793,6 +797,7 @@ function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogPr
const [description, setDescription] = useState(workflow.description ?? ''); const [description, setDescription] = useState(workflow.description ?? '');
const [allowReentry, setAllowReentry] = useState(workflow.allowReentry ?? false); const [allowReentry, setAllowReentry] = useState(workflow.allowReentry ?? false);
const [eventName, setEventName] = useState(triggerConfig?.eventName ?? ''); const [eventName, setEventName] = useState(triggerConfig?.eventName ?? '');
const [eventPopoverOpen, setEventPopoverOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// Sync state when workflow changes or dialog opens // Sync state when workflow changes or dialog opens
@@ -852,29 +857,50 @@ function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogPr
<div> <div>
<Label htmlFor="eventName">Trigger Event *</Label> <Label htmlFor="eventName">Trigger Event *</Label>
{eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( <div className="relative">
<Select value={eventName} onValueChange={setEventName} required>
<SelectTrigger id="eventName">
<SelectValue placeholder="Select an event" />
</SelectTrigger>
<SelectContent>
{eventNamesData.eventNames.map(name => (
<SelectItem key={name} value={name}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input <Input
id="eventName" id="eventName"
type="text" type="text"
value={eventName} value={eventName}
onChange={e => 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" placeholder="e.g., contact.created, email.opened"
required required
autoComplete="off"
/> />
)} {eventPopoverOpen && ((eventNamesData?.eventNames?.length ?? 0) > 0 || eventName?.trim()) && (
<div className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md">
<Command>
<CommandList>
<CommandGroup>
{eventNamesData?.eventNames
?.filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase()))
.map(n => (
<CommandItem key={n} value={n} onSelect={() => { setEventName(n); setEventPopoverOpen(false); }}>
{n}
</CommandItem>
))}
{eventName?.trim() && !eventNamesData?.eventNames?.some(n => n === eventName.trim()) && (
<CommandItem
key="__custom__"
value={eventName.trim()}
onSelect={() => { setEventName(eventName.trim()); setEventPopoverOpen(false); }}
>
Use &ldquo;{eventName.trim()}&rdquo;
</CommandItem>
)}
</CommandGroup>
</CommandList>
</Command>
</div>
)}
</div>
<p className="text-xs text-neutral-500 mt-1"> <p className="text-xs text-neutral-500 mt-1">
The event that triggers this workflow to start for a contact The event that triggers this workflow to start for a contact
</p> </p>
@@ -961,6 +987,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
// WAIT_FOR_EVENT fields // WAIT_FOR_EVENT fields
const [eventName, setEventName] = useState(''); const [eventName, setEventName] = useState('');
const [eventPopoverOpen, setEventPopoverOpen] = useState(false);
const [eventTimeoutAmount, setEventTimeoutAmount] = useState('1'); const [eventTimeoutAmount, setEventTimeoutAmount] = useState('1');
const [eventTimeoutUnit, setEventTimeoutUnit] = useState<'minutes' | 'hours' | 'days'>('days'); const [eventTimeoutUnit, setEventTimeoutUnit] = useState<'minutes' | 'hours' | 'days'>('days');
@@ -1611,34 +1638,53 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
<Label htmlFor="eventName" className="text-sm font-medium"> <Label htmlFor="eventName" className="text-sm font-medium">
Event Name * Event Name *
</Label> </Label>
{eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( <div className="relative">
<Select value={eventName} onValueChange={setEventName} required>
<SelectTrigger id="eventName" className="mt-1.5">
<SelectValue placeholder="Select an event..." />
</SelectTrigger>
<SelectContent>
{eventNamesData.eventNames.map(name => (
<SelectItem key={name} value={name}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input <Input
id="eventName" id="eventName"
type="text" type="text"
value={eventName} value={eventName}
onChange={e => setEventName(e.target.value)} onChange={e => {
setEventName(e.target.value);
setEventPopoverOpen(true);
}}
onFocus={() => setEventPopoverOpen(true)}
onBlur={() => {
setTimeout(() => setEventPopoverOpen(false), 150);
}}
required required
placeholder="e.g., email.clicked, user.upgraded" placeholder="e.g., email.clicked, user.upgraded"
className="mt-1.5" className="mt-1.5"
autoComplete="off"
/> />
)} {eventPopoverOpen && ((eventNamesData?.eventNames?.length ?? 0) > 0 || eventName?.trim()) && (
<div className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md">
<Command>
<CommandList>
<CommandGroup>
{eventNamesData?.eventNames
?.filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase()))
.map(n => (
<CommandItem key={n} value={n} onSelect={() => { setEventName(n); setEventPopoverOpen(false); }}>
{n}
</CommandItem>
))}
{eventName?.trim() && !eventNamesData?.eventNames?.some(n => n === eventName.trim()) && (
<CommandItem
key="__custom__"
value={eventName.trim()}
onSelect={() => { setEventName(eventName.trim()); setEventPopoverOpen(false); }}
>
Use &ldquo;{eventName.trim()}&rdquo;
</CommandItem>
)}
</CommandGroup>
</CommandList>
</Command>
</div>
)}
</div>
<p className="text-xs text-neutral-500 mt-1.5"> <p className="text-xs text-neutral-500 mt-1.5">
{eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 Enter the event name to wait for, or select from previously tracked events
? 'The workflow will pause until this event occurs'
: 'Enter the event name to wait for'}
</p> </p>
</div> </div>
@@ -1991,6 +2037,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
// WAIT_FOR_EVENT fields // WAIT_FOR_EVENT fields
const [eventName, setEventName] = useState(String(config?.eventName || '')); const [eventName, setEventName] = useState(String(config?.eventName || ''));
const [eventPopoverOpen, setEventPopoverOpen] = useState(false);
const [eventTimeoutAmount, setEventTimeoutAmount] = useState<string>(() => { const [eventTimeoutAmount, setEventTimeoutAmount] = useState<string>(() => {
const timeoutSeconds = Number(config?.timeout) || 86400; const timeoutSeconds = Number(config?.timeout) || 86400;
// Convert seconds to most appropriate unit // Convert seconds to most appropriate unit
@@ -2793,40 +2840,54 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
<div className="space-y-4 pl-3"> <div className="space-y-4 pl-3">
<div> <div>
<Label htmlFor="editEventName">Event Name *</Label> <Label htmlFor="editEventName">Event Name *</Label>
{eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( <div className="relative">
<> <Input
<Select value={eventName} onValueChange={setEventName} required> id="editEventName"
<SelectTrigger id="editEventName" className="mt-1.5"> type="text"
<SelectValue placeholder="Select an event..." /> value={eventName}
</SelectTrigger> onChange={e => {
<SelectContent> setEventName(e.target.value);
{eventNamesData.eventNames.map(name => ( setEventPopoverOpen(true);
<SelectItem key={name} value={name}> }}
{name} onFocus={() => setEventPopoverOpen(true)}
</SelectItem> onBlur={() => {
))} setTimeout(() => setEventPopoverOpen(false), 150);
</SelectContent> }}
</Select> required
<p className="text-xs text-neutral-500 mt-1.5"> placeholder="e.g., email.clicked, user.upgraded"
Select from previously tracked events in your project className="mt-1.5"
</p> autoComplete="off"
</> />
) : ( {eventPopoverOpen && ((eventNamesData?.eventNames?.length ?? 0) > 0 || eventName?.trim()) && (
<> <div className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md">
<Input <Command>
id="editEventName" <CommandList>
type="text" <CommandGroup>
value={eventName} {eventNamesData?.eventNames
onChange={e => setEventName(e.target.value)} ?.filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase()))
required .map(n => (
placeholder="e.g., email.clicked, user.upgraded" <CommandItem key={n} value={n} onSelect={() => { setEventName(n); setEventPopoverOpen(false); }}>
className="mt-1.5" {n}
/> </CommandItem>
<p className="text-xs text-neutral-500 mt-1.5"> ))}
The workflow will pause until this event is triggered by the contact {eventName?.trim() && !eventNamesData?.eventNames?.some(n => n === eventName.trim()) && (
</p> <CommandItem
</> key="__custom__"
)} value={eventName.trim()}
onSelect={() => { setEventName(eventName.trim()); setEventPopoverOpen(false); }}
>
Use &ldquo;{eventName.trim()}&rdquo;
</CommandItem>
)}
</CommandGroup>
</CommandList>
</Command>
</div>
)}
</div>
<p className="text-xs text-neutral-500 mt-1.5">
Enter the event name to wait for, or select from previously tracked events
</p>
</div> </div>
<div> <div>
+58 -27
View File
@@ -6,6 +6,10 @@ import {
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
Command,
CommandGroup,
CommandItem,
CommandList,
ConfirmDialog, ConfirmDialog,
Dialog, Dialog,
DialogContent, DialogContent,
@@ -14,11 +18,6 @@ import {
DialogTitle, DialogTitle,
Input, Input,
Label, Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@plunk/ui'; } from '@plunk/ui';
import type {Workflow} from '@plunk/db'; import type {Workflow} from '@plunk/db';
import type {PaginatedResponse} from '@plunk/types'; import type {PaginatedResponse} from '@plunk/types';
@@ -321,6 +320,7 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia
const [name, setName] = useState(''); const [name, setName] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [eventName, setEventName] = useState(''); const [eventName, setEventName] = useState('');
const [eventPopoverOpen, setEventPopoverOpen] = useState(false);
const [allowReentry, setAllowReentry] = useState(false); const [allowReentry, setAllowReentry] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -391,34 +391,65 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia
</div> </div>
<div> <div>
<Label htmlFor="eventName">Trigger Event *</Label> <Label htmlFor="createEventName">Trigger Event *</Label>
{eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( {/* Combobox: 可自由輸入 event name,同時提供已追蹤 event 的下拉建議 */}
<Select value={eventName} onValueChange={setEventName} required> <div className="relative">
<SelectTrigger id="eventName">
<SelectValue placeholder="Select an event..." />
</SelectTrigger>
<SelectContent>
{eventNamesData.eventNames.map(name => (
<SelectItem key={name} value={name}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input <Input
id="eventName" id="createEventName"
type="text" type="text"
value={eventName} value={eventName}
onChange={e => setEventName(e.target.value)} onChange={e => {
required setEventName(e.target.value);
setEventPopoverOpen(true);
}}
onFocus={() => setEventPopoverOpen(true)}
onBlur={() => {
// 延遲關閉,讓 CommandItem 的 onSelect 有時間觸發
setTimeout(() => setEventPopoverOpen(false), 150);
}}
placeholder="e.g., contact.created, email.opened" placeholder="e.g., contact.created, email.opened"
required
autoComplete="off"
/> />
)} {eventPopoverOpen && ((eventNamesData?.eventNames?.length ?? 0) > 0 || eventName?.trim()) && (
<div className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md">
<Command>
<CommandList>
<CommandGroup>
{eventNamesData?.eventNames
?.filter(n => !eventName || n.toLowerCase().includes(eventName.toLowerCase()))
.map(n => (
<CommandItem
key={n}
value={n}
onSelect={() => {
setEventName(n);
setEventPopoverOpen(false);
}}
>
{n}
</CommandItem>
))}
{eventName?.trim() && !eventNamesData?.eventNames?.some(n => n === eventName.trim()) && (
<CommandItem
key="__custom__"
value={eventName.trim()}
onSelect={() => {
setEventName(eventName.trim());
setEventPopoverOpen(false);
}}
>
Use &ldquo;{eventName.trim()}&rdquo;
</CommandItem>
)}
</CommandGroup>
</CommandList>
</Command>
</div>
)}
</div>
<p className="text-xs text-neutral-500 mt-1"> <p className="text-xs text-neutral-500 mt-1">
{eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 The event that triggers this workflow to start for a contact
? 'Select from previously tracked events'
: 'No events tracked yet. Enter the event name that will trigger this workflow.'}
</p> </p>
</div> </div>