Merge pull request #320 from hanamizuki/fix/wait-for-event-combobox
This commit is contained in:
@@ -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 “{eventName.trim()}”
|
||||||
|
</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 “{eventName.trim()}”
|
||||||
|
</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 “{eventName.trim()}”
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -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 “{eventName.trim()}”
|
||||||
|
</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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user