ui: Clean up empty states, modals, animations
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db';
|
||||
import type {PaginatedResponse} from '@plunk/types';
|
||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||
import {EmptyState} from '../../components/EmptyState';
|
||||
import {network} from '../../lib/network';
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -597,13 +598,11 @@ export default function WorkflowEditorPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!executionsData?.executions.length ? (
|
||||
<div className="text-center py-12">
|
||||
<Users className="h-12 w-12 text-neutral-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-neutral-900 mb-2">No executions yet</h3>
|
||||
<p className="text-neutral-500 mb-6">
|
||||
This workflow hasn't been executed yet. Enable it to start processing contacts.
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="No executions yet"
|
||||
description="This workflow hasn't been executed yet. Enable it to start processing contacts."
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
@@ -1254,8 +1253,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Basic Information Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Basic Information</h3>
|
||||
</div>
|
||||
|
||||
@@ -1330,12 +1328,11 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
{/* SEND_EMAIL Configuration */}
|
||||
{type === 'SEND_EMAIL' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Email Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="template" className="text-sm font-medium">
|
||||
Email Template *
|
||||
@@ -1386,7 +1383,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
</div>
|
||||
|
||||
{recipientType === 'CUSTOM' && (
|
||||
<div className="pl-3 border-l-2 border-blue-200 bg-blue-50/50 -ml-3 py-3 pr-3">
|
||||
<div className="p-3 bg-neutral-50 rounded-lg border border-neutral-200">
|
||||
<Label htmlFor="customEmail" className="text-sm font-medium">
|
||||
Email Address *
|
||||
</Label>
|
||||
@@ -1411,12 +1408,11 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
{/* DELAY Configuration */}
|
||||
{type === 'DELAY' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Delay Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 pl-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="delayAmount" className="text-sm font-medium">
|
||||
Amount *
|
||||
@@ -1459,22 +1455,21 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 pl-3">Maximum delay: 365 days</p>
|
||||
<p className="text-xs text-neutral-500">Maximum delay: 365 days</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CONDITION Configuration */}
|
||||
{type === 'CONDITION' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Condition Configuration</h3>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-600 pl-3">
|
||||
<p className="text-sm text-neutral-600">
|
||||
Define the condition that determines which path contacts will follow
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="conditionField" className="text-sm font-medium">
|
||||
Field to Check *
|
||||
@@ -1628,12 +1623,11 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
{/* WAIT_FOR_EVENT Configuration */}
|
||||
{type === 'WAIT_FOR_EVENT' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Wait for Event Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="eventName" className="text-sm font-medium">
|
||||
Event Name *
|
||||
@@ -1736,12 +1730,11 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
{/* WEBHOOK Configuration */}
|
||||
{type === 'WEBHOOK' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Webhook Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="webhookUrl" className="text-sm font-medium">
|
||||
Webhook URL *
|
||||
@@ -1799,12 +1792,11 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
{/* UPDATE_CONTACT Configuration */}
|
||||
{type === 'UPDATE_CONTACT' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Contact Update Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="pl-3">
|
||||
<div>
|
||||
<Label htmlFor="contactUpdates" className="text-sm font-medium">
|
||||
Contact Data Updates (JSON) *
|
||||
</Label>
|
||||
@@ -1827,12 +1819,11 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
{/* EXIT Configuration */}
|
||||
{type === 'EXIT' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Exit Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="pl-3">
|
||||
<div>
|
||||
<Label htmlFor="exitReason" className="text-sm font-medium">
|
||||
Exit Reason
|
||||
</Label>
|
||||
@@ -2322,12 +2313,11 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Basic Information */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Basic Information</h3>
|
||||
</div>
|
||||
|
||||
<div className="pl-3">
|
||||
<div>
|
||||
<Label htmlFor="editStepName" className="text-sm font-medium">
|
||||
Step Name *
|
||||
</Label>
|
||||
@@ -2349,12 +2339,11 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
{/* SEND_EMAIL Configuration */}
|
||||
{step.type === 'SEND_EMAIL' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Email Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="editTemplate" className="text-sm font-medium">
|
||||
Email Template *
|
||||
@@ -2405,7 +2394,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
</div>
|
||||
|
||||
{recipientType === 'CUSTOM' && (
|
||||
<div className="pl-3 border-l-2 border-blue-200 bg-blue-50/50 -ml-3 py-3 pr-3">
|
||||
<div className="p-3 bg-neutral-50 rounded-lg border border-neutral-200">
|
||||
<Label htmlFor="editCustomEmail" className="text-sm font-medium">
|
||||
Email Address *
|
||||
</Label>
|
||||
@@ -2430,12 +2419,11 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
{/* DELAY Configuration */}
|
||||
{step.type === 'DELAY' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Delay Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 pl-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="editDelayAmount" className="text-sm font-medium">
|
||||
Amount *
|
||||
@@ -2478,19 +2466,18 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 pl-3">Maximum delay: 365 days</p>
|
||||
<p className="text-xs text-neutral-500">Maximum delay: 365 days</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CONDITION Configuration */}
|
||||
{step.type === 'CONDITION' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Condition Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
{/* Mode toggle */}
|
||||
<div>
|
||||
<Label className="text-sm font-medium mb-2 block">Condition Mode</Label>
|
||||
@@ -2501,10 +2488,10 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
disabled={conditionMode === 'multi' && hasMultiBranchConnections()}
|
||||
className={`flex-1 px-3 py-2 rounded-lg border-2 text-sm font-medium transition-all ${
|
||||
conditionMode === 'binary'
|
||||
? 'border-purple-500 bg-purple-50 text-purple-700'
|
||||
? 'border-neutral-900 bg-neutral-900 text-white'
|
||||
: conditionMode === 'multi' && hasMultiBranchConnections()
|
||||
? 'border-neutral-200 text-neutral-400 bg-neutral-50 cursor-not-allowed opacity-50'
|
||||
: 'border-neutral-200 text-neutral-600 hover:border-neutral-300'
|
||||
: 'border-neutral-200 text-neutral-700 hover:border-neutral-400 hover:bg-neutral-50'
|
||||
}`}
|
||||
>
|
||||
Simple (If/Else)
|
||||
@@ -2515,10 +2502,10 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
disabled={conditionMode === 'binary' && hasBinaryConnections()}
|
||||
className={`flex-1 px-3 py-2 rounded-lg border-2 text-sm font-medium transition-all ${
|
||||
conditionMode === 'multi'
|
||||
? 'border-purple-500 bg-purple-50 text-purple-700'
|
||||
? 'border-neutral-900 bg-neutral-900 text-white'
|
||||
: conditionMode === 'binary' && hasBinaryConnections()
|
||||
? 'border-neutral-200 text-neutral-400 bg-neutral-50 cursor-not-allowed opacity-50'
|
||||
: 'border-neutral-200 text-neutral-600 hover:border-neutral-300'
|
||||
: 'border-neutral-200 text-neutral-700 hover:border-neutral-400 hover:bg-neutral-50'
|
||||
}`}
|
||||
>
|
||||
Multi-branch (Switch)
|
||||
@@ -2832,12 +2819,11 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
{/* WAIT_FOR_EVENT Configuration */}
|
||||
{step.type === 'WAIT_FOR_EVENT' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Event Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="editEventName">Event Name *</Label>
|
||||
<div className="relative">
|
||||
@@ -2937,12 +2923,11 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
{/* WEBHOOK Configuration */}
|
||||
{step.type === 'WEBHOOK' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Webhook Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
{/* Info Alert about webhook body */}
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
@@ -3097,12 +3082,11 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
{/* UPDATE_CONTACT Configuration */}
|
||||
{step.type === 'UPDATE_CONTACT' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Contact Updates</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="editContactUpdates">Contact Data Updates (JSON) *</Label>
|
||||
<textarea
|
||||
@@ -3136,12 +3120,11 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
||||
{/* EXIT Configuration */}
|
||||
{step.type === 'EXIT' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
||||
<div className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<div className="pb-2 border-b border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Exit Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pl-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="editExitReason">Exit Reason (optional)</Label>
|
||||
<Select value={exitReason} onValueChange={setExitReason}>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import type {Workflow} from '@plunk/db';
|
||||
import type {PaginatedResponse} from '@plunk/types';
|
||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||
import {EmptyState} from '../../components/EmptyState';
|
||||
import {network} from '../../lib/network';
|
||||
import {formatRelativeTime} from '../../lib/dateUtils';
|
||||
import {Calendar, Edit, Plus, Power, PowerOff, Search, Trash2, Workflow as WorkflowIcon} from 'lucide-react';
|
||||
@@ -157,20 +158,20 @@ export default function WorkflowsPage() {
|
||||
</Card>
|
||||
) : data?.data.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center py-12">
|
||||
<WorkflowIcon className="h-12 w-12 text-neutral-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-neutral-900 mb-2">No workflows found</h3>
|
||||
<p className="text-neutral-500 mb-6">
|
||||
{search ? 'Try adjusting your search terms' : 'Get started by creating your first workflow'}
|
||||
</p>
|
||||
{!search && (
|
||||
<Button onClick={() => setShowCreateDialog(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Workflow
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<CardContent>
|
||||
<EmptyState
|
||||
icon={WorkflowIcon}
|
||||
title={search ? 'No workflows match' : 'No workflows yet'}
|
||||
description={search ? 'Try a different search term.' : 'Automate emails triggered by contact events.'}
|
||||
action={
|
||||
!search ? (
|
||||
<Button onClick={() => setShowCreateDialog(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Workflow
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -366,7 +367,7 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia
|
||||
<DialogTitle>Create New Workflow</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="name">Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
@@ -378,19 +379,19 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Send a series of welcome emails to new subscribers"
|
||||
className="w-full px-3 py-2 border border-neutral-200 rounded-lg text-sm"
|
||||
className="w-full px-3 py-2 border border-neutral-200 rounded-md text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="createEventName">Trigger Event *</Label>
|
||||
{/* Combobox: 可自由輸入 event name,同時提供已追蹤 event 的下拉建議 */}
|
||||
<div className="relative">
|
||||
@@ -453,21 +454,20 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-4 bg-neutral-50 rounded-lg border border-neutral-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
id="allowReentry"
|
||||
type="checkbox"
|
||||
checked={allowReentry}
|
||||
onChange={e => setAllowReentry(e.target.checked)}
|
||||
className="mt-1 h-4 w-4 text-neutral-900 focus:ring-neutral-900 border-neutral-300 rounded"
|
||||
className="mt-0.5 h-4 w-4 text-neutral-900 focus:ring-neutral-900 border-neutral-300 rounded"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="allowReentry" className="font-medium cursor-pointer">
|
||||
Allow Re-entry
|
||||
</Label>
|
||||
<p className="text-xs text-neutral-500 mt-1">
|
||||
When enabled, contacts can enter this workflow multiple times. When disabled, contacts can only enter
|
||||
once, ever.
|
||||
<p className="text-xs text-neutral-500 mt-0.5">
|
||||
When enabled, contacts can enter this workflow multiple times.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user