@inspirer-dev/crm-dashboard 1.0.23 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/src/components/StepFlowBuilder/constants.ts +20 -0
- package/admin/src/components/StepFlowBuilder/flow-canvas/FlowCanvas.tsx +285 -0
- package/admin/src/components/StepFlowBuilder/flow-canvas/index.ts +1 -0
- package/admin/src/components/StepFlowBuilder/form-builder/FormBuilder.tsx +198 -0
- package/admin/src/components/StepFlowBuilder/form-builder/StepFormItem.tsx +313 -0
- package/admin/src/components/StepFlowBuilder/form-builder/index.ts +2 -0
- package/admin/src/components/StepFlowBuilder/index.tsx +80 -264
- package/admin/src/components/StepFlowBuilder/types.ts +2 -0
- package/dist/_chunks/{index-XoiSAQhK.js → index-BO_qHciA.js} +327 -236
- package/dist/_chunks/{index-BeiHTAlq.mjs → index-CWnuAWMG.mjs} +112 -91
- package/dist/_chunks/{index-aSjgyfVX.js → index-DTxf6tZR.js} +1029 -580
- package/dist/_chunks/{index-BK8649hk.mjs → index-TNIWToZD.mjs} +1034 -585
- package/dist/admin/index.js +2 -2
- package/dist/admin/index.mjs +2 -2
- package/package.json +1 -1
|
@@ -51,12 +51,16 @@ export const CHANNEL_OPTIONS: { value: ChannelType; label: string }[] = [
|
|
|
51
51
|
{ value: 'sms', label: 'SMS' },
|
|
52
52
|
];
|
|
53
53
|
|
|
54
|
+
export const CHANNELS = CHANNEL_OPTIONS;
|
|
55
|
+
|
|
54
56
|
export const DURATION_UNIT_OPTIONS: { value: DurationUnit; label: string }[] = [
|
|
55
57
|
{ value: 'minutes', label: 'Minutes' },
|
|
56
58
|
{ value: 'hours', label: 'Hours' },
|
|
57
59
|
{ value: 'days', label: 'Days' },
|
|
58
60
|
];
|
|
59
61
|
|
|
62
|
+
export const DURATION_UNITS = DURATION_UNIT_OPTIONS;
|
|
63
|
+
|
|
60
64
|
export const DEFAULT_NODE_DATA: Record<StepType, { name: string; config: object }> = {
|
|
61
65
|
entry: {
|
|
62
66
|
name: 'Entry',
|
|
@@ -89,3 +93,19 @@ export const DEFAULT_NODE_DATA: Record<StepType, { name: string; config: object
|
|
|
89
93
|
};
|
|
90
94
|
|
|
91
95
|
export const ENTRY_NODE_ID = 'entry';
|
|
96
|
+
|
|
97
|
+
export const DEFAULT_STEP_CONFIG: Record<StepType, object> = {
|
|
98
|
+
entry: {},
|
|
99
|
+
message: {
|
|
100
|
+
channel: 'telegram' as ChannelType,
|
|
101
|
+
variants: [],
|
|
102
|
+
},
|
|
103
|
+
wait: {
|
|
104
|
+
duration: 1,
|
|
105
|
+
durationUnit: 'hours' as DurationUnit,
|
|
106
|
+
},
|
|
107
|
+
branch: {
|
|
108
|
+
branchSegmentId: undefined,
|
|
109
|
+
},
|
|
110
|
+
exit: {},
|
|
111
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import ReactFlow, {
|
|
3
|
+
Background,
|
|
4
|
+
Controls,
|
|
5
|
+
MiniMap,
|
|
6
|
+
useNodesState,
|
|
7
|
+
useEdgesState,
|
|
8
|
+
addEdge,
|
|
9
|
+
type Connection,
|
|
10
|
+
type OnNodesChange,
|
|
11
|
+
type OnEdgesChange,
|
|
12
|
+
type OnConnect,
|
|
13
|
+
} from 'reactflow';
|
|
14
|
+
import 'reactflow/dist/style.css';
|
|
15
|
+
import { Box } from '@strapi/design-system';
|
|
16
|
+
|
|
17
|
+
import type { FlowNode, FlowEdge, FlowNodeData, FlowStep, StepType } from '../types';
|
|
18
|
+
import { nodeTypes } from '../nodes';
|
|
19
|
+
import { edgeTypes } from '../edges';
|
|
20
|
+
import { NodeEditPanel } from '../panels';
|
|
21
|
+
import { FlowToolbar } from '../toolbar';
|
|
22
|
+
import {
|
|
23
|
+
stepsToFlow,
|
|
24
|
+
flowToSteps,
|
|
25
|
+
applyAutoLayout,
|
|
26
|
+
createNode,
|
|
27
|
+
getNextNodePosition,
|
|
28
|
+
isValidConnection,
|
|
29
|
+
} from '../utils';
|
|
30
|
+
import { ENTRY_NODE_ID } from '../constants';
|
|
31
|
+
|
|
32
|
+
interface FlowCanvasProps {
|
|
33
|
+
steps: FlowStep[];
|
|
34
|
+
onStepsChange: (steps: FlowStep[]) => void;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled, error }) => {
|
|
40
|
+
const [nodes, setNodes, onNodesChange] = useNodesState<FlowNodeData>([]);
|
|
41
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
42
|
+
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
|
|
43
|
+
const isInitialMount = useRef(true);
|
|
44
|
+
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
45
|
+
const lastStepsRef = useRef<string>('');
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const stepsJson = JSON.stringify(steps);
|
|
49
|
+
if (stepsJson === lastStepsRef.current) return;
|
|
50
|
+
lastStepsRef.current = stepsJson;
|
|
51
|
+
|
|
52
|
+
const { nodes: n, edges: e } = stepsToFlow(steps);
|
|
53
|
+
|
|
54
|
+
if (n.length === 0) {
|
|
55
|
+
const entryNode = createNode('entry', { x: 250, y: 0 });
|
|
56
|
+
setNodes([entryNode]);
|
|
57
|
+
setEdges([]);
|
|
58
|
+
} else {
|
|
59
|
+
const layoutedNodes = applyAutoLayout(n, e);
|
|
60
|
+
setNodes(layoutedNodes);
|
|
61
|
+
setEdges(e);
|
|
62
|
+
}
|
|
63
|
+
isInitialMount.current = false;
|
|
64
|
+
}, [steps, setNodes, setEdges]);
|
|
65
|
+
|
|
66
|
+
const syncToParent = useCallback(() => {
|
|
67
|
+
if (isInitialMount.current) return;
|
|
68
|
+
|
|
69
|
+
if (syncTimeoutRef.current) {
|
|
70
|
+
clearTimeout(syncTimeoutRef.current);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
syncTimeoutRef.current = setTimeout(() => {
|
|
74
|
+
const newSteps = flowToSteps(nodes, edges);
|
|
75
|
+
const newStepsJson = JSON.stringify(newSteps);
|
|
76
|
+
if (newStepsJson !== lastStepsRef.current) {
|
|
77
|
+
lastStepsRef.current = newStepsJson;
|
|
78
|
+
onStepsChange(newSteps);
|
|
79
|
+
}
|
|
80
|
+
}, 300);
|
|
81
|
+
}, [nodes, edges, onStepsChange]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
syncToParent();
|
|
85
|
+
}, [nodes, edges, syncToParent]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
return () => {
|
|
89
|
+
if (syncTimeoutRef.current) {
|
|
90
|
+
clearTimeout(syncTimeoutRef.current);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const handleNodesChange: OnNodesChange = useCallback(
|
|
96
|
+
(changes) => {
|
|
97
|
+
const filteredChanges = changes.filter((change) => {
|
|
98
|
+
if (change.type === 'remove' && change.id === ENTRY_NODE_ID) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
});
|
|
103
|
+
onNodesChange(filteredChanges);
|
|
104
|
+
},
|
|
105
|
+
[onNodesChange]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const handleEdgesChange: OnEdgesChange = useCallback(
|
|
109
|
+
(changes) => {
|
|
110
|
+
onEdgesChange(changes);
|
|
111
|
+
},
|
|
112
|
+
[onEdgesChange]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const handleConnect: OnConnect = useCallback(
|
|
116
|
+
(connection: Connection) => {
|
|
117
|
+
if (!connection.source || !connection.target) return;
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
!isValidConnection(
|
|
121
|
+
connection.source,
|
|
122
|
+
connection.sourceHandle || null,
|
|
123
|
+
connection.target,
|
|
124
|
+
nodes,
|
|
125
|
+
edges
|
|
126
|
+
)
|
|
127
|
+
) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const sourceNode = nodes.find((n) => n.id === connection.source);
|
|
132
|
+
const isBranch = sourceNode?.data.stepType === 'branch';
|
|
133
|
+
|
|
134
|
+
const newEdge: FlowEdge = {
|
|
135
|
+
id: `${connection.source}-${connection.target}${
|
|
136
|
+
connection.sourceHandle ? `-${connection.sourceHandle}` : ''
|
|
137
|
+
}`,
|
|
138
|
+
source: connection.source,
|
|
139
|
+
target: connection.target,
|
|
140
|
+
sourceHandle: connection.sourceHandle || undefined,
|
|
141
|
+
targetHandle: connection.targetHandle || undefined,
|
|
142
|
+
type: isBranch ? 'labeled' : 'default',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
setEdges((eds) => addEdge(newEdge, eds));
|
|
146
|
+
},
|
|
147
|
+
[nodes, edges, setEdges]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const handleNodeClick = useCallback((_: React.MouseEvent, node: FlowNode) => {
|
|
151
|
+
setSelectedNode(node);
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
const handlePaneClick = useCallback(() => {
|
|
155
|
+
setSelectedNode(null);
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
const handleAddNode = useCallback(
|
|
159
|
+
(type: StepType) => {
|
|
160
|
+
const position = getNextNodePosition(nodes);
|
|
161
|
+
const newNode = createNode(type, position);
|
|
162
|
+
setNodes((nds) => [...nds, newNode]);
|
|
163
|
+
},
|
|
164
|
+
[nodes, setNodes]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const handleAutoLayout = useCallback(() => {
|
|
168
|
+
const layoutedNodes = applyAutoLayout(nodes, edges);
|
|
169
|
+
setNodes(layoutedNodes);
|
|
170
|
+
}, [nodes, edges, setNodes]);
|
|
171
|
+
|
|
172
|
+
const handleNodeUpdate = useCallback(
|
|
173
|
+
(nodeId: string, data: Partial<FlowNodeData>) => {
|
|
174
|
+
setNodes((nds) =>
|
|
175
|
+
nds.map((node) => {
|
|
176
|
+
if (node.id === nodeId) {
|
|
177
|
+
return {
|
|
178
|
+
...node,
|
|
179
|
+
data: {
|
|
180
|
+
...node.data,
|
|
181
|
+
...data,
|
|
182
|
+
config: {
|
|
183
|
+
...node.data.config,
|
|
184
|
+
...(data.config || {}),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return node;
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (selectedNode && selectedNode.id === nodeId) {
|
|
194
|
+
setSelectedNode((prev) =>
|
|
195
|
+
prev
|
|
196
|
+
? {
|
|
197
|
+
...prev,
|
|
198
|
+
data: {
|
|
199
|
+
...prev.data,
|
|
200
|
+
...data,
|
|
201
|
+
config: {
|
|
202
|
+
...prev.data.config,
|
|
203
|
+
...(data.config || {}),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
: null
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
[selectedNode, setNodes]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const handleClosePanel = useCallback(() => {
|
|
215
|
+
setSelectedNode(null);
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<Box
|
|
220
|
+
style={{
|
|
221
|
+
height: '100%',
|
|
222
|
+
border: error ? '1px solid #ee5e52' : '1px solid #dcdce4',
|
|
223
|
+
borderRadius: 4,
|
|
224
|
+
overflow: 'hidden',
|
|
225
|
+
position: 'relative',
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
<ReactFlow
|
|
229
|
+
nodes={nodes}
|
|
230
|
+
edges={edges}
|
|
231
|
+
nodeTypes={nodeTypes}
|
|
232
|
+
edgeTypes={edgeTypes}
|
|
233
|
+
onNodesChange={handleNodesChange}
|
|
234
|
+
onEdgesChange={handleEdgesChange}
|
|
235
|
+
onConnect={handleConnect}
|
|
236
|
+
onNodeClick={handleNodeClick}
|
|
237
|
+
onPaneClick={handlePaneClick}
|
|
238
|
+
fitView
|
|
239
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
240
|
+
deleteKeyCode={['Backspace', 'Delete']}
|
|
241
|
+
nodesDraggable={!disabled}
|
|
242
|
+
nodesConnectable={!disabled}
|
|
243
|
+
elementsSelectable={!disabled}
|
|
244
|
+
panOnScroll
|
|
245
|
+
selectionOnDrag
|
|
246
|
+
defaultEdgeOptions={{
|
|
247
|
+
type: 'smoothstep',
|
|
248
|
+
animated: false,
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
<Background color="#f0f0f0" gap={20} />
|
|
252
|
+
<Controls showInteractive={false} />
|
|
253
|
+
<MiniMap
|
|
254
|
+
nodeColor={(node) => {
|
|
255
|
+
switch (node.data?.stepType) {
|
|
256
|
+
case 'entry':
|
|
257
|
+
return '#5cb176';
|
|
258
|
+
case 'message':
|
|
259
|
+
return '#0077cc';
|
|
260
|
+
case 'wait':
|
|
261
|
+
return '#e9b200';
|
|
262
|
+
case 'branch':
|
|
263
|
+
return '#7b61ff';
|
|
264
|
+
case 'exit':
|
|
265
|
+
return '#dc2626';
|
|
266
|
+
default:
|
|
267
|
+
return '#999';
|
|
268
|
+
}
|
|
269
|
+
}}
|
|
270
|
+
maskColor="rgba(0, 0, 0, 0.1)"
|
|
271
|
+
style={{ background: '#f7f7f7' }}
|
|
272
|
+
/>
|
|
273
|
+
<FlowToolbar onAddNode={handleAddNode} onAutoLayout={handleAutoLayout} disabled={disabled} />
|
|
274
|
+
</ReactFlow>
|
|
275
|
+
<NodeEditPanel
|
|
276
|
+
node={selectedNode}
|
|
277
|
+
onClose={handleClosePanel}
|
|
278
|
+
onUpdate={handleNodeUpdate}
|
|
279
|
+
disabled={disabled}
|
|
280
|
+
/>
|
|
281
|
+
</Box>
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export default FlowCanvas;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FlowCanvas } from './FlowCanvas';
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Flex,
|
|
5
|
+
Typography,
|
|
6
|
+
Button,
|
|
7
|
+
SingleSelect,
|
|
8
|
+
SingleSelectOption,
|
|
9
|
+
EmptyStateLayout,
|
|
10
|
+
} from '@strapi/design-system';
|
|
11
|
+
import { Plus, Message, Clock, ArrowRight, Cross } from '@strapi/icons';
|
|
12
|
+
import type { FlowStep, StepType } from '../types';
|
|
13
|
+
import { NODE_COLORS, STEP_TYPE_LABELS, DEFAULT_STEP_CONFIG } from '../constants';
|
|
14
|
+
import { generateStepKey } from '../utils';
|
|
15
|
+
import StepFormItem from './StepFormItem';
|
|
16
|
+
|
|
17
|
+
interface FormBuilderProps {
|
|
18
|
+
steps: FlowStep[];
|
|
19
|
+
onStepsChange: (steps: FlowStep[]) => void;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const STEP_TYPE_OPTIONS: { type: StepType; label: string; icon: React.ReactNode }[] = [
|
|
24
|
+
{ type: 'message', label: 'Message', icon: <Message style={{ width: 16, height: 16 }} /> },
|
|
25
|
+
{ type: 'wait', label: 'Wait', icon: <Clock style={{ width: 16, height: 16 }} /> },
|
|
26
|
+
{ type: 'branch', label: 'Branch', icon: <ArrowRight style={{ width: 16, height: 16 }} /> },
|
|
27
|
+
{ type: 'exit', label: 'Exit', icon: <Cross style={{ width: 16, height: 16 }} /> },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const FormBuilder: React.FC<FormBuilderProps> = ({ steps, onStepsChange, disabled }) => {
|
|
31
|
+
const handleAddStep = useCallback(
|
|
32
|
+
(type: StepType) => {
|
|
33
|
+
const stepKey = generateStepKey();
|
|
34
|
+
const defaultConfig = DEFAULT_STEP_CONFIG[type];
|
|
35
|
+
const newStep: FlowStep = {
|
|
36
|
+
stepKey,
|
|
37
|
+
name: `${STEP_TYPE_LABELS[type]} ${steps.length + 1}`,
|
|
38
|
+
stepType: type,
|
|
39
|
+
order: steps.length,
|
|
40
|
+
...defaultConfig,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const updatedSteps = [...steps, newStep];
|
|
44
|
+
|
|
45
|
+
if (steps.length > 0) {
|
|
46
|
+
const lastStep = steps[steps.length - 1];
|
|
47
|
+
if (lastStep.stepType !== 'branch' && lastStep.stepType !== 'exit' && !lastStep.nextStep) {
|
|
48
|
+
updatedSteps[steps.length - 1] = {
|
|
49
|
+
...lastStep,
|
|
50
|
+
nextStep: stepKey,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onStepsChange(updatedSteps);
|
|
56
|
+
},
|
|
57
|
+
[steps, onStepsChange]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleUpdateStep = useCallback(
|
|
61
|
+
(stepKey: string, updates: Partial<FlowStep>) => {
|
|
62
|
+
const updatedSteps = steps.map((step) =>
|
|
63
|
+
step.stepKey === stepKey ? { ...step, ...updates } : step
|
|
64
|
+
);
|
|
65
|
+
onStepsChange(updatedSteps);
|
|
66
|
+
},
|
|
67
|
+
[steps, onStepsChange]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const handleDeleteStep = useCallback(
|
|
71
|
+
(stepKey: string) => {
|
|
72
|
+
const updatedSteps = steps
|
|
73
|
+
.filter((step) => step.stepKey !== stepKey)
|
|
74
|
+
.map((step, idx) => {
|
|
75
|
+
const updated = { ...step, order: idx };
|
|
76
|
+
if (step.nextStep === stepKey) {
|
|
77
|
+
updated.nextStep = undefined;
|
|
78
|
+
}
|
|
79
|
+
if (step.yesNextStep === stepKey) {
|
|
80
|
+
updated.yesNextStep = undefined;
|
|
81
|
+
}
|
|
82
|
+
if (step.noNextStep === stepKey) {
|
|
83
|
+
updated.noNextStep = undefined;
|
|
84
|
+
}
|
|
85
|
+
return updated;
|
|
86
|
+
});
|
|
87
|
+
onStepsChange(updatedSteps);
|
|
88
|
+
},
|
|
89
|
+
[steps, onStepsChange]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const handleMoveStep = useCallback(
|
|
93
|
+
(stepKey: string, direction: 'up' | 'down') => {
|
|
94
|
+
const currentIndex = steps.findIndex((s) => s.stepKey === stepKey);
|
|
95
|
+
if (currentIndex === -1) return;
|
|
96
|
+
|
|
97
|
+
const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
|
98
|
+
if (newIndex < 0 || newIndex >= steps.length) return;
|
|
99
|
+
|
|
100
|
+
const updatedSteps = [...steps];
|
|
101
|
+
const [moved] = updatedSteps.splice(currentIndex, 1);
|
|
102
|
+
updatedSteps.splice(newIndex, 0, moved);
|
|
103
|
+
|
|
104
|
+
const reordered = updatedSteps.map((step, idx) => ({
|
|
105
|
+
...step,
|
|
106
|
+
order: idx,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
onStepsChange(reordered);
|
|
110
|
+
},
|
|
111
|
+
[steps, onStepsChange]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Box>
|
|
116
|
+
<Box padding={3} background="neutral100" hasRadius style={{ marginBottom: 16 }}>
|
|
117
|
+
<Flex justifyContent="space-between" alignItems="center" wrap="wrap" gap={2}>
|
|
118
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
119
|
+
Add Step:
|
|
120
|
+
</Typography>
|
|
121
|
+
<Flex gap={2} wrap="wrap">
|
|
122
|
+
{STEP_TYPE_OPTIONS.map((option) => {
|
|
123
|
+
const colors = NODE_COLORS[option.type];
|
|
124
|
+
return (
|
|
125
|
+
<Button
|
|
126
|
+
key={option.type}
|
|
127
|
+
variant="tertiary"
|
|
128
|
+
size="S"
|
|
129
|
+
startIcon={option.icon}
|
|
130
|
+
onClick={() => handleAddStep(option.type)}
|
|
131
|
+
disabled={disabled}
|
|
132
|
+
style={{
|
|
133
|
+
backgroundColor: colors.background,
|
|
134
|
+
color: colors.text,
|
|
135
|
+
borderColor: colors.border,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{option.label}
|
|
139
|
+
</Button>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</Flex>
|
|
143
|
+
</Flex>
|
|
144
|
+
</Box>
|
|
145
|
+
|
|
146
|
+
{steps.length === 0 ? (
|
|
147
|
+
<Box padding={6} background="neutral100" hasRadius>
|
|
148
|
+
<EmptyStateLayout
|
|
149
|
+
icon={<Message style={{ width: 48, height: 48, color: '#666687' }} />}
|
|
150
|
+
content="No steps configured yet. Add your first step to build the campaign journey."
|
|
151
|
+
action={
|
|
152
|
+
<Button
|
|
153
|
+
variant="secondary"
|
|
154
|
+
startIcon={<Plus />}
|
|
155
|
+
onClick={() => handleAddStep('message')}
|
|
156
|
+
disabled={disabled}
|
|
157
|
+
>
|
|
158
|
+
Add Message Step
|
|
159
|
+
</Button>
|
|
160
|
+
}
|
|
161
|
+
/>
|
|
162
|
+
</Box>
|
|
163
|
+
) : (
|
|
164
|
+
<Flex direction="column" gap={3}>
|
|
165
|
+
{steps.map((step, index) => (
|
|
166
|
+
<StepFormItem
|
|
167
|
+
key={step.stepKey}
|
|
168
|
+
step={step}
|
|
169
|
+
index={index}
|
|
170
|
+
steps={steps}
|
|
171
|
+
onUpdate={handleUpdateStep}
|
|
172
|
+
onDelete={handleDeleteStep}
|
|
173
|
+
onMove={handleMoveStep}
|
|
174
|
+
disabled={disabled}
|
|
175
|
+
/>
|
|
176
|
+
))}
|
|
177
|
+
</Flex>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{steps.length > 0 && (
|
|
181
|
+
<Box paddingTop={4}>
|
|
182
|
+
<Flex justifyContent="center">
|
|
183
|
+
<Button
|
|
184
|
+
variant="tertiary"
|
|
185
|
+
startIcon={<Plus />}
|
|
186
|
+
onClick={() => handleAddStep('message')}
|
|
187
|
+
disabled={disabled}
|
|
188
|
+
>
|
|
189
|
+
Add Another Step
|
|
190
|
+
</Button>
|
|
191
|
+
</Flex>
|
|
192
|
+
</Box>
|
|
193
|
+
)}
|
|
194
|
+
</Box>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export default FormBuilder;
|