@inspirer-dev/crm-dashboard 1.0.40 → 1.0.41
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 +5 -3
- package/admin/src/components/StepFlowBuilder/context/ValidationContext.tsx +33 -0
- package/admin/src/components/StepFlowBuilder/context/index.ts +1 -0
- package/admin/src/components/StepFlowBuilder/edges/LabeledEdge.tsx +7 -13
- package/admin/src/components/StepFlowBuilder/flow-canvas/FlowCanvas.tsx +199 -103
- package/admin/src/components/StepFlowBuilder/index.tsx +33 -19
- package/admin/src/components/StepFlowBuilder/nodes/BranchNode.tsx +57 -12
- package/admin/src/components/StepFlowBuilder/nodes/EntryNode.tsx +52 -7
- package/admin/src/components/StepFlowBuilder/nodes/ExitNode.tsx +53 -8
- package/admin/src/components/StepFlowBuilder/nodes/MessageNode.tsx +55 -10
- package/admin/src/components/StepFlowBuilder/nodes/WaitNode.tsx +55 -10
- package/admin/src/components/StepFlowBuilder/panels/NodeEditPanel.tsx +119 -1
- package/admin/src/components/StepFlowBuilder/panels/ValidationPanel.tsx +282 -0
- package/admin/src/components/StepFlowBuilder/panels/index.ts +1 -0
- package/admin/src/components/StepFlowBuilder/utils.ts +18 -1
- package/admin/src/components/StepFlowBuilder/validation.ts +317 -0
- package/admin/src/translations/en.json +14 -1
- package/admin/src/translations/ru.json +14 -1
- package/dist/_chunks/{FlowCanvas-Ktzy5E8i.mjs → FlowCanvas-BQX_vGrx.mjs} +960 -87
- package/dist/_chunks/{FlowCanvas-CZ2FJpk2.js → FlowCanvas-CGSaCOu9.js} +957 -84
- package/dist/_chunks/en-Bar3xmYk.mjs +16 -0
- package/dist/_chunks/en-CMBkwPq9.js +16 -0
- package/dist/_chunks/{index-B6wwHXMh.mjs → index-BSzRuqIy.mjs} +92 -113
- package/dist/_chunks/{index-DzjZLi7B.js → index-CQakgGHK.js} +30 -19
- package/dist/_chunks/{index-BWqO1iVx.mjs → index-CxTg0iGt.mjs} +30 -19
- package/dist/_chunks/{index-BFtqa7pq.js → index-DFnEvoil.js} +92 -113
- package/dist/_chunks/ru-9mrg61lt.js +16 -0
- package/dist/_chunks/ru-CJuWxs7W.mjs +16 -0
- package/dist/admin/index.js +3 -3
- package/dist/admin/index.mjs +3 -3
- package/package.json +1 -1
- package/dist/_chunks/en-CC6nrL6u.mjs +0 -8
- package/dist/_chunks/en-Cx5J83tP.js +0 -8
- package/dist/_chunks/ru-Bbls49WS.mjs +0 -8
- package/dist/_chunks/ru-BjlFbstB.js +0 -8
|
@@ -56,11 +56,11 @@ export const NODE_COLORS: Record<
|
|
|
56
56
|
export const EDGE_COLORS = {
|
|
57
57
|
default: {
|
|
58
58
|
stroke: '#94a3b8',
|
|
59
|
-
strokeWidth:
|
|
59
|
+
strokeWidth: 3,
|
|
60
60
|
},
|
|
61
61
|
yes: {
|
|
62
62
|
stroke: '#10b981',
|
|
63
|
-
strokeWidth:
|
|
63
|
+
strokeWidth: 3,
|
|
64
64
|
label: {
|
|
65
65
|
background: '#d1fae5',
|
|
66
66
|
text: '#059669',
|
|
@@ -69,7 +69,7 @@ export const EDGE_COLORS = {
|
|
|
69
69
|
},
|
|
70
70
|
no: {
|
|
71
71
|
stroke: '#ef4444',
|
|
72
|
-
strokeWidth:
|
|
72
|
+
strokeWidth: 3,
|
|
73
73
|
label: {
|
|
74
74
|
background: '#fee2e2',
|
|
75
75
|
text: '#dc2626',
|
|
@@ -83,6 +83,8 @@ export const NODE_DIMENSIONS = {
|
|
|
83
83
|
height: 88,
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
export const HANDLE_SIZE = 16;
|
|
87
|
+
|
|
86
88
|
export const STEP_TYPE_LABELS: Record<StepType, string> = {
|
|
87
89
|
entry: 'Entry Point',
|
|
88
90
|
message: 'Send Message',
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import type { ValidationError } from '../validation';
|
|
3
|
+
|
|
4
|
+
interface ValidationContextValue {
|
|
5
|
+
nodeErrors: Map<string, ValidationError[]>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ValidationContext = createContext<ValidationContextValue>({
|
|
9
|
+
nodeErrors: new Map(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const ValidationProvider: React.FC<{
|
|
13
|
+
nodeErrors: Map<string, ValidationError[]>;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}> = ({ nodeErrors, children }) => {
|
|
16
|
+
return (
|
|
17
|
+
<ValidationContext.Provider value={{ nodeErrors }}>
|
|
18
|
+
{children}
|
|
19
|
+
</ValidationContext.Provider>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const useValidation = () => useContext(ValidationContext);
|
|
24
|
+
|
|
25
|
+
export const useNodeValidation = (nodeId: string) => {
|
|
26
|
+
const { nodeErrors } = useValidation();
|
|
27
|
+
const errors = nodeErrors.get(nodeId) || [];
|
|
28
|
+
return {
|
|
29
|
+
hasError: errors.some((e) => e.severity === 'error'),
|
|
30
|
+
hasWarning: errors.some((e) => e.severity === 'warning'),
|
|
31
|
+
errors,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ValidationProvider, useValidation, useNodeValidation } from './ValidationContext';
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
BaseEdge,
|
|
4
|
-
EdgeLabelRenderer,
|
|
5
|
-
getSmoothStepPath,
|
|
6
|
-
type EdgeProps,
|
|
7
|
-
} from 'reactflow';
|
|
2
|
+
import { BaseEdge, EdgeLabelRenderer, getSimpleBezierPath, type EdgeProps } from 'reactflow';
|
|
8
3
|
import { EDGE_COLORS } from '../constants';
|
|
9
4
|
|
|
10
5
|
interface LabeledEdgeData {
|
|
@@ -18,30 +13,29 @@ const LabeledEdge: React.FC<EdgeProps<LabeledEdgeData>> = ({
|
|
|
18
13
|
targetY,
|
|
19
14
|
sourcePosition,
|
|
20
15
|
targetPosition,
|
|
21
|
-
|
|
16
|
+
sourceHandleId,
|
|
22
17
|
style = {},
|
|
23
18
|
markerEnd,
|
|
24
19
|
}) => {
|
|
25
|
-
const [edgePath] =
|
|
20
|
+
const [edgePath] = getSimpleBezierPath({
|
|
26
21
|
sourceX,
|
|
27
22
|
sourceY,
|
|
28
23
|
sourcePosition,
|
|
29
24
|
targetX,
|
|
30
25
|
targetY,
|
|
31
26
|
targetPosition,
|
|
32
|
-
borderRadius: 16,
|
|
33
27
|
});
|
|
34
28
|
|
|
35
|
-
const isYes =
|
|
36
|
-
const isNo =
|
|
29
|
+
const isYes = sourceHandleId === 'yes';
|
|
30
|
+
const isNo = sourceHandleId === 'no';
|
|
37
31
|
const label = isYes ? 'YES' : isNo ? 'NO' : null;
|
|
38
32
|
|
|
39
33
|
const edgeStyle = isYes ? EDGE_COLORS.yes : isNo ? EDGE_COLORS.no : EDGE_COLORS.default;
|
|
40
34
|
|
|
41
35
|
const labelStyle = isYes ? EDGE_COLORS.yes.label : isNo ? EDGE_COLORS.no.label : null;
|
|
42
36
|
|
|
43
|
-
const labelPosX = sourceX;
|
|
44
|
-
const labelPosY = sourceY +
|
|
37
|
+
const labelPosX = (sourceX + targetX) / 2;
|
|
38
|
+
const labelPosY = (sourceY + targetY) / 2;
|
|
45
39
|
|
|
46
40
|
return (
|
|
47
41
|
<>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
2
2
|
import ReactFlow, {
|
|
3
3
|
Background,
|
|
4
4
|
Controls,
|
|
@@ -17,10 +17,17 @@ import ReactFlow, {
|
|
|
17
17
|
import 'reactflow/dist/style.css';
|
|
18
18
|
import { Box } from '@strapi/design-system';
|
|
19
19
|
|
|
20
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
FlowNode,
|
|
22
|
+
FlowEdge,
|
|
23
|
+
FlowNodeData,
|
|
24
|
+
FlowStep,
|
|
25
|
+
StepType,
|
|
26
|
+
CampaignContext,
|
|
27
|
+
} from '../types';
|
|
21
28
|
import { nodeTypes } from '../nodes';
|
|
22
29
|
import { edgeTypes } from '../edges';
|
|
23
|
-
import { NodeEditPanel } from '../panels';
|
|
30
|
+
import { NodeEditPanel, ValidationPanel } from '../panels';
|
|
24
31
|
import { FlowToolbar } from '../toolbar';
|
|
25
32
|
import {
|
|
26
33
|
stepsToFlow,
|
|
@@ -31,32 +38,53 @@ import {
|
|
|
31
38
|
isValidConnection,
|
|
32
39
|
} from '../utils';
|
|
33
40
|
import { ENTRY_NODE_ID, PROXIMITY_THRESHOLD, NODE_DIMENSIONS } from '../constants';
|
|
41
|
+
import { validateFlow, type ValidationResult } from '../validation';
|
|
42
|
+
import { ValidationProvider } from '../context/ValidationContext';
|
|
34
43
|
|
|
35
44
|
interface FlowCanvasProps {
|
|
36
45
|
steps: FlowStep[];
|
|
37
46
|
onStepsChange: (steps: FlowStep[]) => void;
|
|
47
|
+
onValidationChange?: (validation: ValidationResult) => void;
|
|
38
48
|
disabled?: boolean;
|
|
39
49
|
error?: string;
|
|
40
50
|
campaignContext?: CampaignContext;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
53
|
+
const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
54
|
+
steps,
|
|
55
|
+
onStepsChange,
|
|
56
|
+
onValidationChange,
|
|
57
|
+
disabled,
|
|
58
|
+
error,
|
|
59
|
+
campaignContext,
|
|
60
|
+
}) => {
|
|
44
61
|
const [nodes, setNodes, onNodesChange] = useNodesState<FlowNodeData>([]);
|
|
45
62
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
46
63
|
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
|
|
47
64
|
const isInitialMount = useRef(true);
|
|
48
65
|
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
49
66
|
const lastStepsRef = useRef<string>('');
|
|
50
|
-
const { fitView, getViewport, setViewport } = useReactFlow();
|
|
67
|
+
const { fitView, getViewport, setViewport, setCenter } = useReactFlow();
|
|
51
68
|
const store = useStoreApi();
|
|
52
69
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}, [
|
|
70
|
+
const validation = useMemo(() => {
|
|
71
|
+
return validateFlow(nodes as FlowNode[], edges as FlowEdge[]);
|
|
72
|
+
}, [nodes, edges]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
onValidationChange?.(validation);
|
|
76
|
+
}, [validation, onValidationChange]);
|
|
77
|
+
|
|
78
|
+
const fitViewWithTopPadding = useCallback(
|
|
79
|
+
(topOffset = 40) => {
|
|
80
|
+
fitView({ padding: 0.2, duration: 200 });
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
const { x, y, zoom } = getViewport();
|
|
83
|
+
setViewport({ x, y: y - topOffset, zoom }, { duration: 200 });
|
|
84
|
+
}, 210);
|
|
85
|
+
},
|
|
86
|
+
[fitView, getViewport, setViewport]
|
|
87
|
+
);
|
|
60
88
|
|
|
61
89
|
const getNodeDistance = useCallback(
|
|
62
90
|
(nodeId1: string, nodeId2: string) => {
|
|
@@ -86,7 +114,8 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
86
114
|
if (!internalNode) return null;
|
|
87
115
|
|
|
88
116
|
type NodeWithPosition = Node & { positionAbsolute?: { x: number; y: number } };
|
|
89
|
-
const draggedPos =
|
|
117
|
+
const draggedPos =
|
|
118
|
+
(internalNode as NodeWithPosition).positionAbsolute || internalNode.position;
|
|
90
119
|
|
|
91
120
|
const closestNode = Array.from(nodeInternals.values()).reduce<{
|
|
92
121
|
distance: number;
|
|
@@ -119,21 +148,45 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
119
148
|
const draggedIsBelow = draggedPos.y > closestPos.y;
|
|
120
149
|
|
|
121
150
|
if (draggedIsBelow) {
|
|
122
|
-
if (closestData.stepType === 'exit'
|
|
151
|
+
if (closestData.stepType === 'exit') {
|
|
123
152
|
return null;
|
|
124
153
|
}
|
|
154
|
+
if (closestData.stepType === 'branch') {
|
|
155
|
+
const draggedCenterX = draggedPos.x + NODE_DIMENSIONS.width / 2;
|
|
156
|
+
const closestCenterX = closestPos.x + NODE_DIMENSIONS.width / 2;
|
|
157
|
+
const isRight = draggedCenterX >= closestCenterX;
|
|
158
|
+
const handle = isRight ? 'no' : 'yes';
|
|
159
|
+
return {
|
|
160
|
+
id: `${closestNode.node.id}-${node.id}-${handle}`,
|
|
161
|
+
source: closestNode.node.id,
|
|
162
|
+
sourceHandle: handle,
|
|
163
|
+
target: node.id,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
125
166
|
return {
|
|
126
167
|
id: `${closestNode.node.id}-${node.id}`,
|
|
127
168
|
source: closestNode.node.id,
|
|
128
169
|
target: node.id,
|
|
129
170
|
};
|
|
130
171
|
} else {
|
|
131
|
-
if (draggedData.stepType === 'entry'
|
|
172
|
+
if (draggedData.stepType === 'entry') {
|
|
132
173
|
return null;
|
|
133
174
|
}
|
|
134
175
|
if (closestData.stepType === 'entry') {
|
|
135
176
|
return null;
|
|
136
177
|
}
|
|
178
|
+
if (draggedData.stepType === 'branch') {
|
|
179
|
+
const draggedCenterX = draggedPos.x + NODE_DIMENSIONS.width / 2;
|
|
180
|
+
const closestCenterX = closestPos.x + NODE_DIMENSIONS.width / 2;
|
|
181
|
+
const isRight = closestCenterX >= draggedCenterX;
|
|
182
|
+
const handle = isRight ? 'no' : 'yes';
|
|
183
|
+
return {
|
|
184
|
+
id: `${node.id}-${closestNode.node.id}-${handle}`,
|
|
185
|
+
source: node.id,
|
|
186
|
+
sourceHandle: handle,
|
|
187
|
+
target: closestNode.node.id,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
137
190
|
return {
|
|
138
191
|
id: `${node.id}-${closestNode.node.id}`,
|
|
139
192
|
source: node.id,
|
|
@@ -162,13 +215,20 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
162
215
|
return true;
|
|
163
216
|
});
|
|
164
217
|
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
218
|
+
if (closeEdge) {
|
|
219
|
+
const isDuplicate = nextEdges.find(
|
|
220
|
+
(ne) =>
|
|
221
|
+
ne.source === closeEdge.source &&
|
|
222
|
+
ne.target === closeEdge.target &&
|
|
223
|
+
ne.sourceHandle === closeEdge.sourceHandle
|
|
224
|
+
);
|
|
225
|
+
if (!isDuplicate) {
|
|
226
|
+
nextEdges.push({
|
|
227
|
+
...closeEdge,
|
|
228
|
+
className: 'temp',
|
|
229
|
+
type: closeEdge.sourceHandle ? 'labeled' : 'default',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
172
232
|
}
|
|
173
233
|
|
|
174
234
|
return nextEdges;
|
|
@@ -195,13 +255,19 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
195
255
|
return true;
|
|
196
256
|
});
|
|
197
257
|
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
258
|
+
if (closeEdge) {
|
|
259
|
+
const isDuplicate = nextEdges.find(
|
|
260
|
+
(ne) =>
|
|
261
|
+
ne.source === closeEdge.source &&
|
|
262
|
+
ne.target === closeEdge.target &&
|
|
263
|
+
ne.sourceHandle === closeEdge.sourceHandle
|
|
264
|
+
);
|
|
265
|
+
if (!isDuplicate) {
|
|
266
|
+
nextEdges.push({
|
|
267
|
+
...closeEdge,
|
|
268
|
+
type: closeEdge.sourceHandle ? 'labeled' : 'default',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
205
271
|
}
|
|
206
272
|
|
|
207
273
|
return nextEdges;
|
|
@@ -388,86 +454,116 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
388
454
|
setSelectedNode(null);
|
|
389
455
|
}, []);
|
|
390
456
|
|
|
457
|
+
const handleErrorClick = useCallback(
|
|
458
|
+
(nodeId: string) => {
|
|
459
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
460
|
+
if (node) {
|
|
461
|
+
setSelectedNode(node as FlowNode);
|
|
462
|
+
setCenter(
|
|
463
|
+
node.position.x + NODE_DIMENSIONS.width / 2,
|
|
464
|
+
node.position.y + NODE_DIMENSIONS.height / 2,
|
|
465
|
+
{ duration: 300, zoom: 1 }
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
[nodes, setCenter]
|
|
470
|
+
);
|
|
471
|
+
|
|
391
472
|
return (
|
|
392
|
-
<
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
>
|
|
402
|
-
<style>
|
|
403
|
-
{`
|
|
404
|
-
.react-flow__edge.temp .react-flow__edge-path {
|
|
405
|
-
stroke: #94a3b8;
|
|
406
|
-
stroke-dasharray: 5 5;
|
|
407
|
-
stroke-width: 2;
|
|
408
|
-
}
|
|
409
|
-
.react-flow__edge.temp {
|
|
410
|
-
pointer-events: none;
|
|
411
|
-
}
|
|
412
|
-
`}
|
|
413
|
-
</style>
|
|
414
|
-
<ReactFlow
|
|
415
|
-
nodes={nodes}
|
|
416
|
-
edges={edges}
|
|
417
|
-
nodeTypes={nodeTypes}
|
|
418
|
-
edgeTypes={edgeTypes}
|
|
419
|
-
onNodesChange={handleNodesChange}
|
|
420
|
-
onEdgesChange={handleEdgesChange}
|
|
421
|
-
onConnect={handleConnect}
|
|
422
|
-
onNodeClick={handleNodeClick}
|
|
423
|
-
onPaneClick={handlePaneClick}
|
|
424
|
-
onNodeDrag={disabled ? undefined : handleNodeDrag}
|
|
425
|
-
onNodeDragStop={disabled ? undefined : handleNodeDragStop}
|
|
426
|
-
fitView
|
|
427
|
-
fitViewOptions={{ padding: 0.2 }}
|
|
428
|
-
deleteKeyCode={['Backspace', 'Delete']}
|
|
429
|
-
nodesDraggable={!disabled}
|
|
430
|
-
nodesConnectable={!disabled}
|
|
431
|
-
elementsSelectable={!disabled}
|
|
432
|
-
panOnScroll
|
|
433
|
-
selectionOnDrag
|
|
434
|
-
defaultEdgeOptions={{
|
|
435
|
-
type: 'smoothstep',
|
|
436
|
-
animated: false,
|
|
473
|
+
<ValidationProvider nodeErrors={validation.nodeErrors}>
|
|
474
|
+
<Box
|
|
475
|
+
style={{
|
|
476
|
+
height: '100%',
|
|
477
|
+
width: '100%',
|
|
478
|
+
border: error ? '1px solid #ee5e52' : '1px solid #dcdce4',
|
|
479
|
+
borderRadius: 4,
|
|
480
|
+
overflow: 'hidden',
|
|
481
|
+
position: 'relative',
|
|
437
482
|
}}
|
|
438
483
|
>
|
|
439
|
-
<
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return '#5cb176';
|
|
446
|
-
case 'message':
|
|
447
|
-
return '#0077cc';
|
|
448
|
-
case 'wait':
|
|
449
|
-
return '#e9b200';
|
|
450
|
-
case 'branch':
|
|
451
|
-
return '#7b61ff';
|
|
452
|
-
case 'exit':
|
|
453
|
-
return '#dc2626';
|
|
454
|
-
default:
|
|
455
|
-
return '#999';
|
|
484
|
+
<style>
|
|
485
|
+
{`
|
|
486
|
+
.react-flow__edge.temp .react-flow__edge-path {
|
|
487
|
+
stroke: #94a3b8;
|
|
488
|
+
stroke-dasharray: 5 5;
|
|
489
|
+
stroke-width: 3;
|
|
456
490
|
}
|
|
491
|
+
.react-flow__edge.temp {
|
|
492
|
+
pointer-events: none;
|
|
493
|
+
}
|
|
494
|
+
`}
|
|
495
|
+
</style>
|
|
496
|
+
<ReactFlow
|
|
497
|
+
nodes={nodes}
|
|
498
|
+
edges={edges}
|
|
499
|
+
nodeTypes={nodeTypes}
|
|
500
|
+
edgeTypes={edgeTypes}
|
|
501
|
+
onNodesChange={handleNodesChange}
|
|
502
|
+
onEdgesChange={handleEdgesChange}
|
|
503
|
+
onConnect={handleConnect}
|
|
504
|
+
onNodeClick={handleNodeClick}
|
|
505
|
+
onPaneClick={handlePaneClick}
|
|
506
|
+
onNodeDrag={disabled ? undefined : handleNodeDrag}
|
|
507
|
+
onNodeDragStop={disabled ? undefined : handleNodeDragStop}
|
|
508
|
+
fitView
|
|
509
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
510
|
+
deleteKeyCode={['Backspace', 'Delete']}
|
|
511
|
+
nodesDraggable={!disabled}
|
|
512
|
+
nodesConnectable={!disabled}
|
|
513
|
+
elementsSelectable={!disabled}
|
|
514
|
+
panOnScroll
|
|
515
|
+
selectionOnDrag
|
|
516
|
+
defaultEdgeOptions={{
|
|
517
|
+
type: 'simplebezier',
|
|
518
|
+
animated: false,
|
|
519
|
+
style: {
|
|
520
|
+
stroke: '#94a3b8',
|
|
521
|
+
strokeWidth: 3,
|
|
522
|
+
},
|
|
457
523
|
}}
|
|
458
|
-
|
|
459
|
-
|
|
524
|
+
>
|
|
525
|
+
<Background color="#f0f0f0" gap={20} />
|
|
526
|
+
<Controls showInteractive={false} />
|
|
527
|
+
<MiniMap
|
|
528
|
+
nodeColor={(node) => {
|
|
529
|
+
switch (node.data?.stepType) {
|
|
530
|
+
case 'entry':
|
|
531
|
+
return '#5cb176';
|
|
532
|
+
case 'message':
|
|
533
|
+
return '#0077cc';
|
|
534
|
+
case 'wait':
|
|
535
|
+
return '#e9b200';
|
|
536
|
+
case 'branch':
|
|
537
|
+
return '#7b61ff';
|
|
538
|
+
case 'exit':
|
|
539
|
+
return '#dc2626';
|
|
540
|
+
default:
|
|
541
|
+
return '#999';
|
|
542
|
+
}
|
|
543
|
+
}}
|
|
544
|
+
maskColor="rgba(0, 0, 0, 0.1)"
|
|
545
|
+
style={{ background: '#f7f7f7' }}
|
|
546
|
+
/>
|
|
547
|
+
<FlowToolbar
|
|
548
|
+
onAddNode={handleAddNode}
|
|
549
|
+
onAutoLayout={handleAutoLayout}
|
|
550
|
+
disabled={disabled}
|
|
551
|
+
/>
|
|
552
|
+
<ValidationPanel
|
|
553
|
+
validation={validation}
|
|
554
|
+
onErrorClick={handleErrorClick}
|
|
555
|
+
/>
|
|
556
|
+
</ReactFlow>
|
|
557
|
+
<NodeEditPanel
|
|
558
|
+
node={selectedNode}
|
|
559
|
+
onClose={handleClosePanel}
|
|
560
|
+
onUpdate={handleNodeUpdate}
|
|
561
|
+
disabled={disabled}
|
|
562
|
+
campaignContext={campaignContext}
|
|
563
|
+
nodeErrors={selectedNode ? validation.nodeErrors.get(selectedNode.id) || [] : []}
|
|
460
564
|
/>
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
<NodeEditPanel
|
|
464
|
-
node={selectedNode}
|
|
465
|
-
onClose={handleClosePanel}
|
|
466
|
-
onUpdate={handleNodeUpdate}
|
|
467
|
-
disabled={disabled}
|
|
468
|
-
campaignContext={campaignContext}
|
|
469
|
-
/>
|
|
470
|
-
</Box>
|
|
565
|
+
</Box>
|
|
566
|
+
</ValidationProvider>
|
|
471
567
|
);
|
|
472
568
|
};
|
|
473
569
|
|
|
@@ -5,6 +5,7 @@ import { Box, Field, Flex, Typography, Badge } from '@strapi/design-system';
|
|
|
5
5
|
import { useForm, unstable_useContentManagerContext as useContentManagerContext } from '@strapi/strapi/admin';
|
|
6
6
|
|
|
7
7
|
import type { StepFlowBuilderProps, FlowStep, CampaignContext, EntrySegment } from './types';
|
|
8
|
+
import type { ValidationResult } from './validation';
|
|
8
9
|
import { parseValue } from './utils';
|
|
9
10
|
import { FlowCanvas } from './flow-canvas';
|
|
10
11
|
|
|
@@ -12,10 +13,11 @@ type SegmentItem = { id?: number; documentId?: string; name?: string };
|
|
|
12
13
|
type ConnectDisconnect = { connect?: SegmentItem[]; disconnect?: SegmentItem[] };
|
|
13
14
|
|
|
14
15
|
const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
15
|
-
({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
|
|
16
|
+
({ name, value, onChange, intlLabel, disabled, error: externalError, required, hint }, ref) => {
|
|
16
17
|
const [steps, setSteps] = useState<FlowStep[]>([]);
|
|
17
18
|
const [initialSegment, setInitialSegment] = useState<EntrySegment | null>(null);
|
|
18
19
|
const [hasFetched, setHasFetched] = useState(false);
|
|
20
|
+
const [validation, setValidation] = useState<ValidationResult | null>(null);
|
|
19
21
|
const isInitialMount = useRef(true);
|
|
20
22
|
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
21
23
|
|
|
@@ -27,28 +29,23 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
27
29
|
const ctx = useContentManagerContext();
|
|
28
30
|
documentId = ctx?.id;
|
|
29
31
|
model = ctx?.model;
|
|
30
|
-
console.log('[StepFlowBuilder] ContentManagerContext:', { documentId, model });
|
|
31
32
|
} catch (err) {
|
|
32
|
-
|
|
33
|
+
// No ContentManagerContext (test page?)
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
useEffect(() => {
|
|
36
37
|
if (!documentId || !model || hasFetched) {
|
|
37
|
-
console.log('[StepFlowBuilder] Skip fetch:', { documentId, model, hasFetched });
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const fetchCampaignData = async () => {
|
|
42
|
-
console.log('[StepFlowBuilder] Fetching campaign data...');
|
|
43
42
|
try {
|
|
44
43
|
const { useFetchClient } = await import('@strapi/strapi/admin');
|
|
45
44
|
const { get } = useFetchClient();
|
|
46
45
|
|
|
47
46
|
const url = `/content-manager/collection-types/${model}/${documentId}`;
|
|
48
|
-
console.log('[StepFlowBuilder] Fetch URL:', url);
|
|
49
47
|
|
|
50
48
|
const response = await get(url, { params: { populate: 'entrySegment' } });
|
|
51
|
-
console.log('[StepFlowBuilder] Campaign response:', response);
|
|
52
49
|
|
|
53
50
|
const data = response?.data as { entrySegment?: { id: number; name: string } } | undefined;
|
|
54
51
|
if (data?.entrySegment?.id) {
|
|
@@ -56,10 +53,7 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
56
53
|
id: data.entrySegment.id,
|
|
57
54
|
name: data.entrySegment.name || `Segment #${data.entrySegment.id}`,
|
|
58
55
|
};
|
|
59
|
-
console.log('[StepFlowBuilder] Initial segment from API:', seg);
|
|
60
56
|
setInitialSegment(seg);
|
|
61
|
-
} else {
|
|
62
|
-
console.log('[StepFlowBuilder] No entrySegment in response');
|
|
63
57
|
}
|
|
64
58
|
} catch (err) {
|
|
65
59
|
console.error('[StepFlowBuilder] Fetch error:', err);
|
|
@@ -75,8 +69,6 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
75
69
|
const values = form?.values as Record<string, unknown> | undefined;
|
|
76
70
|
const entrySegmentValue = values?.entrySegment as ConnectDisconnect | undefined;
|
|
77
71
|
|
|
78
|
-
console.log('[StepFlowBuilder] form.values.entrySegment:', entrySegmentValue);
|
|
79
|
-
|
|
80
72
|
if (!entrySegmentValue) {
|
|
81
73
|
return initialSegment;
|
|
82
74
|
}
|
|
@@ -85,14 +77,12 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
85
77
|
|
|
86
78
|
if (connect.length > 0) {
|
|
87
79
|
const seg = connect[0];
|
|
88
|
-
console.log('[StepFlowBuilder] Using connect segment:', seg);
|
|
89
80
|
return { id: seg.id!, name: seg.name || `Segment #${seg.id}` };
|
|
90
81
|
}
|
|
91
82
|
|
|
92
83
|
if (disconnect.length > 0 && initialSegment) {
|
|
93
84
|
const disconnectedId = disconnect[0]?.id;
|
|
94
85
|
if (disconnectedId === initialSegment.id) {
|
|
95
|
-
console.log('[StepFlowBuilder] Segment disconnected');
|
|
96
86
|
return null;
|
|
97
87
|
}
|
|
98
88
|
}
|
|
@@ -104,8 +94,6 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
104
94
|
const values = form?.values as Record<string, unknown> | undefined;
|
|
105
95
|
if (!values) return {};
|
|
106
96
|
|
|
107
|
-
console.log('[StepFlowBuilder] Final segment for context:', currentSegment);
|
|
108
|
-
|
|
109
97
|
return {
|
|
110
98
|
triggerConfig: values.triggerConfig as CampaignContext['triggerConfig'],
|
|
111
99
|
entrySegment: currentSegment,
|
|
@@ -142,6 +130,10 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
142
130
|
[syncToStrapi]
|
|
143
131
|
);
|
|
144
132
|
|
|
133
|
+
const handleValidationChange = useCallback((newValidation: ValidationResult) => {
|
|
134
|
+
setValidation(newValidation);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
145
137
|
useEffect(() => {
|
|
146
138
|
return () => {
|
|
147
139
|
if (syncTimeoutRef.current) {
|
|
@@ -159,12 +151,33 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
159
151
|
|
|
160
152
|
const stepCount = steps.length;
|
|
161
153
|
|
|
154
|
+
const validationError = useMemo(() => {
|
|
155
|
+
if (!validation || validation.isValid) return null;
|
|
156
|
+
const errorCount = validation.errors.length;
|
|
157
|
+
const errorWord = errorCount === 1 ? 'ошибка' : errorCount < 5 ? 'ошибки' : 'ошибок';
|
|
158
|
+
return `Поток содержит ${errorCount} ${errorWord} валидации. Исправьте все ошибки перед публикацией.`;
|
|
159
|
+
}, [validation]);
|
|
160
|
+
|
|
161
|
+
const error = externalError || validationError;
|
|
162
|
+
|
|
162
163
|
return (
|
|
163
164
|
<Field.Root name={name} error={error} hint={hint} required={required}>
|
|
164
165
|
<Flex justifyContent="space-between" alignItems="center" paddingBottom={2}>
|
|
165
|
-
<
|
|
166
|
+
<Flex gap={2} alignItems="center">
|
|
167
|
+
<Field.Label>{displayLabel}</Field.Label>
|
|
168
|
+
{validation && !validation.isValid && (
|
|
169
|
+
<Badge backgroundColor="danger100" textColor="danger600">
|
|
170
|
+
{validation.errors.length} {validation.errors.length === 1 ? 'ошибка' : validation.errors.length < 5 ? 'ошибки' : 'ошибок'}
|
|
171
|
+
</Badge>
|
|
172
|
+
)}
|
|
173
|
+
{validation && validation.isValid && validation.warnings.length > 0 && (
|
|
174
|
+
<Badge backgroundColor="warning100" textColor="warning600">
|
|
175
|
+
{validation.warnings.length} {validation.warnings.length === 1 ? 'предупр.' : 'предупр.'}
|
|
176
|
+
</Badge>
|
|
177
|
+
)}
|
|
178
|
+
</Flex>
|
|
166
179
|
<Badge backgroundColor="neutral100" textColor="neutral600">
|
|
167
|
-
{stepCount}
|
|
180
|
+
{stepCount} {stepCount === 1 ? 'шаг' : stepCount < 5 ? 'шага' : 'шагов'}
|
|
168
181
|
</Badge>
|
|
169
182
|
</Flex>
|
|
170
183
|
|
|
@@ -172,8 +185,9 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
172
185
|
<FlowCanvas
|
|
173
186
|
steps={steps}
|
|
174
187
|
onStepsChange={handleStepsChange}
|
|
188
|
+
onValidationChange={handleValidationChange}
|
|
175
189
|
disabled={disabled}
|
|
176
|
-
error={error}
|
|
190
|
+
error={error || undefined}
|
|
177
191
|
campaignContext={campaignContext}
|
|
178
192
|
/>
|
|
179
193
|
</Box>
|