@inspirer-dev/crm-dashboard 1.0.40 → 1.0.42
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 +198 -104
- 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-CZ2FJpk2.js → FlowCanvas-BW6aa6oF.js} +957 -85
- package/dist/_chunks/{FlowCanvas-Ktzy5E8i.mjs → FlowCanvas-BvC4CsIX.mjs} +960 -88
- package/dist/_chunks/en-Bar3xmYk.mjs +16 -0
- package/dist/_chunks/en-CMBkwPq9.js +16 -0
- package/dist/_chunks/{index-B6wwHXMh.mjs → index--y6vzvC5.mjs} +92 -113
- package/dist/_chunks/{index-DzjZLi7B.js → index-1FgA66KH.js} +30 -19
- package/dist/_chunks/{index-BFtqa7pq.js → index-pMzvnKEe.js} +237 -328
- package/dist/_chunks/{index-BWqO1iVx.mjs → index-vqLSa44X.mjs} +30 -19
- 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,7 +1,6 @@
|
|
|
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
|
-
Controls,
|
|
5
4
|
MiniMap,
|
|
6
5
|
useNodesState,
|
|
7
6
|
useEdgesState,
|
|
@@ -17,10 +16,17 @@ import ReactFlow, {
|
|
|
17
16
|
import 'reactflow/dist/style.css';
|
|
18
17
|
import { Box } from '@strapi/design-system';
|
|
19
18
|
|
|
20
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
FlowNode,
|
|
21
|
+
FlowEdge,
|
|
22
|
+
FlowNodeData,
|
|
23
|
+
FlowStep,
|
|
24
|
+
StepType,
|
|
25
|
+
CampaignContext,
|
|
26
|
+
} from '../types';
|
|
21
27
|
import { nodeTypes } from '../nodes';
|
|
22
28
|
import { edgeTypes } from '../edges';
|
|
23
|
-
import { NodeEditPanel } from '../panels';
|
|
29
|
+
import { NodeEditPanel, ValidationPanel } from '../panels';
|
|
24
30
|
import { FlowToolbar } from '../toolbar';
|
|
25
31
|
import {
|
|
26
32
|
stepsToFlow,
|
|
@@ -31,32 +37,53 @@ import {
|
|
|
31
37
|
isValidConnection,
|
|
32
38
|
} from '../utils';
|
|
33
39
|
import { ENTRY_NODE_ID, PROXIMITY_THRESHOLD, NODE_DIMENSIONS } from '../constants';
|
|
40
|
+
import { validateFlow, type ValidationResult } from '../validation';
|
|
41
|
+
import { ValidationProvider } from '../context/ValidationContext';
|
|
34
42
|
|
|
35
43
|
interface FlowCanvasProps {
|
|
36
44
|
steps: FlowStep[];
|
|
37
45
|
onStepsChange: (steps: FlowStep[]) => void;
|
|
46
|
+
onValidationChange?: (validation: ValidationResult) => void;
|
|
38
47
|
disabled?: boolean;
|
|
39
48
|
error?: string;
|
|
40
49
|
campaignContext?: CampaignContext;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
52
|
+
const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
53
|
+
steps,
|
|
54
|
+
onStepsChange,
|
|
55
|
+
onValidationChange,
|
|
56
|
+
disabled,
|
|
57
|
+
error,
|
|
58
|
+
campaignContext,
|
|
59
|
+
}) => {
|
|
44
60
|
const [nodes, setNodes, onNodesChange] = useNodesState<FlowNodeData>([]);
|
|
45
61
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
46
62
|
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
|
|
47
63
|
const isInitialMount = useRef(true);
|
|
48
64
|
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
49
65
|
const lastStepsRef = useRef<string>('');
|
|
50
|
-
const { fitView, getViewport, setViewport } = useReactFlow();
|
|
66
|
+
const { fitView, getViewport, setViewport, setCenter } = useReactFlow();
|
|
51
67
|
const store = useStoreApi();
|
|
52
68
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}, [
|
|
69
|
+
const validation = useMemo(() => {
|
|
70
|
+
return validateFlow(nodes as FlowNode[], edges as FlowEdge[]);
|
|
71
|
+
}, [nodes, edges]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
onValidationChange?.(validation);
|
|
75
|
+
}, [validation, onValidationChange]);
|
|
76
|
+
|
|
77
|
+
const fitViewWithTopPadding = useCallback(
|
|
78
|
+
(topOffset = 40) => {
|
|
79
|
+
fitView({ padding: 0.2, duration: 200 });
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
const { x, y, zoom } = getViewport();
|
|
82
|
+
setViewport({ x, y: y - topOffset, zoom }, { duration: 200 });
|
|
83
|
+
}, 210);
|
|
84
|
+
},
|
|
85
|
+
[fitView, getViewport, setViewport]
|
|
86
|
+
);
|
|
60
87
|
|
|
61
88
|
const getNodeDistance = useCallback(
|
|
62
89
|
(nodeId1: string, nodeId2: string) => {
|
|
@@ -86,7 +113,8 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
86
113
|
if (!internalNode) return null;
|
|
87
114
|
|
|
88
115
|
type NodeWithPosition = Node & { positionAbsolute?: { x: number; y: number } };
|
|
89
|
-
const draggedPos =
|
|
116
|
+
const draggedPos =
|
|
117
|
+
(internalNode as NodeWithPosition).positionAbsolute || internalNode.position;
|
|
90
118
|
|
|
91
119
|
const closestNode = Array.from(nodeInternals.values()).reduce<{
|
|
92
120
|
distance: number;
|
|
@@ -119,21 +147,45 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
119
147
|
const draggedIsBelow = draggedPos.y > closestPos.y;
|
|
120
148
|
|
|
121
149
|
if (draggedIsBelow) {
|
|
122
|
-
if (closestData.stepType === 'exit'
|
|
150
|
+
if (closestData.stepType === 'exit') {
|
|
123
151
|
return null;
|
|
124
152
|
}
|
|
153
|
+
if (closestData.stepType === 'branch') {
|
|
154
|
+
const draggedCenterX = draggedPos.x + NODE_DIMENSIONS.width / 2;
|
|
155
|
+
const closestCenterX = closestPos.x + NODE_DIMENSIONS.width / 2;
|
|
156
|
+
const isRight = draggedCenterX >= closestCenterX;
|
|
157
|
+
const handle = isRight ? 'no' : 'yes';
|
|
158
|
+
return {
|
|
159
|
+
id: `${closestNode.node.id}-${node.id}-${handle}`,
|
|
160
|
+
source: closestNode.node.id,
|
|
161
|
+
sourceHandle: handle,
|
|
162
|
+
target: node.id,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
125
165
|
return {
|
|
126
166
|
id: `${closestNode.node.id}-${node.id}`,
|
|
127
167
|
source: closestNode.node.id,
|
|
128
168
|
target: node.id,
|
|
129
169
|
};
|
|
130
170
|
} else {
|
|
131
|
-
if (draggedData.stepType === 'entry'
|
|
171
|
+
if (draggedData.stepType === 'entry') {
|
|
132
172
|
return null;
|
|
133
173
|
}
|
|
134
174
|
if (closestData.stepType === 'entry') {
|
|
135
175
|
return null;
|
|
136
176
|
}
|
|
177
|
+
if (draggedData.stepType === 'branch') {
|
|
178
|
+
const draggedCenterX = draggedPos.x + NODE_DIMENSIONS.width / 2;
|
|
179
|
+
const closestCenterX = closestPos.x + NODE_DIMENSIONS.width / 2;
|
|
180
|
+
const isRight = closestCenterX >= draggedCenterX;
|
|
181
|
+
const handle = isRight ? 'no' : 'yes';
|
|
182
|
+
return {
|
|
183
|
+
id: `${node.id}-${closestNode.node.id}-${handle}`,
|
|
184
|
+
source: node.id,
|
|
185
|
+
sourceHandle: handle,
|
|
186
|
+
target: closestNode.node.id,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
137
189
|
return {
|
|
138
190
|
id: `${node.id}-${closestNode.node.id}`,
|
|
139
191
|
source: node.id,
|
|
@@ -162,13 +214,20 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
162
214
|
return true;
|
|
163
215
|
});
|
|
164
216
|
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
217
|
+
if (closeEdge) {
|
|
218
|
+
const isDuplicate = nextEdges.find(
|
|
219
|
+
(ne) =>
|
|
220
|
+
ne.source === closeEdge.source &&
|
|
221
|
+
ne.target === closeEdge.target &&
|
|
222
|
+
ne.sourceHandle === closeEdge.sourceHandle
|
|
223
|
+
);
|
|
224
|
+
if (!isDuplicate) {
|
|
225
|
+
nextEdges.push({
|
|
226
|
+
...closeEdge,
|
|
227
|
+
className: 'temp',
|
|
228
|
+
type: closeEdge.sourceHandle ? 'labeled' : 'default',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
172
231
|
}
|
|
173
232
|
|
|
174
233
|
return nextEdges;
|
|
@@ -195,13 +254,19 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
195
254
|
return true;
|
|
196
255
|
});
|
|
197
256
|
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
257
|
+
if (closeEdge) {
|
|
258
|
+
const isDuplicate = nextEdges.find(
|
|
259
|
+
(ne) =>
|
|
260
|
+
ne.source === closeEdge.source &&
|
|
261
|
+
ne.target === closeEdge.target &&
|
|
262
|
+
ne.sourceHandle === closeEdge.sourceHandle
|
|
263
|
+
);
|
|
264
|
+
if (!isDuplicate) {
|
|
265
|
+
nextEdges.push({
|
|
266
|
+
...closeEdge,
|
|
267
|
+
type: closeEdge.sourceHandle ? 'labeled' : 'default',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
205
270
|
}
|
|
206
271
|
|
|
207
272
|
return nextEdges;
|
|
@@ -388,86 +453,115 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({ steps, onStepsChange, disabled,
|
|
|
388
453
|
setSelectedNode(null);
|
|
389
454
|
}, []);
|
|
390
455
|
|
|
456
|
+
const handleErrorClick = useCallback(
|
|
457
|
+
(nodeId: string) => {
|
|
458
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
459
|
+
if (node) {
|
|
460
|
+
setSelectedNode(node as FlowNode);
|
|
461
|
+
setCenter(
|
|
462
|
+
node.position.x + NODE_DIMENSIONS.width / 2,
|
|
463
|
+
node.position.y + NODE_DIMENSIONS.height / 2,
|
|
464
|
+
{ duration: 300, zoom: 1 }
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
[nodes, setCenter]
|
|
469
|
+
);
|
|
470
|
+
|
|
391
471
|
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,
|
|
472
|
+
<ValidationProvider nodeErrors={validation.nodeErrors}>
|
|
473
|
+
<Box
|
|
474
|
+
style={{
|
|
475
|
+
height: '100%',
|
|
476
|
+
width: '100%',
|
|
477
|
+
border: error ? '1px solid #ee5e52' : '1px solid #dcdce4',
|
|
478
|
+
borderRadius: 4,
|
|
479
|
+
overflow: 'hidden',
|
|
480
|
+
position: 'relative',
|
|
437
481
|
}}
|
|
438
482
|
>
|
|
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';
|
|
483
|
+
<style>
|
|
484
|
+
{`
|
|
485
|
+
.react-flow__edge.temp .react-flow__edge-path {
|
|
486
|
+
stroke: #94a3b8;
|
|
487
|
+
stroke-dasharray: 5 5;
|
|
488
|
+
stroke-width: 3;
|
|
456
489
|
}
|
|
490
|
+
.react-flow__edge.temp {
|
|
491
|
+
pointer-events: none;
|
|
492
|
+
}
|
|
493
|
+
`}
|
|
494
|
+
</style>
|
|
495
|
+
<ReactFlow
|
|
496
|
+
nodes={nodes}
|
|
497
|
+
edges={edges}
|
|
498
|
+
nodeTypes={nodeTypes}
|
|
499
|
+
edgeTypes={edgeTypes}
|
|
500
|
+
onNodesChange={handleNodesChange}
|
|
501
|
+
onEdgesChange={handleEdgesChange}
|
|
502
|
+
onConnect={handleConnect}
|
|
503
|
+
onNodeClick={handleNodeClick}
|
|
504
|
+
onPaneClick={handlePaneClick}
|
|
505
|
+
onNodeDrag={disabled ? undefined : handleNodeDrag}
|
|
506
|
+
onNodeDragStop={disabled ? undefined : handleNodeDragStop}
|
|
507
|
+
fitView
|
|
508
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
509
|
+
deleteKeyCode={['Backspace', 'Delete']}
|
|
510
|
+
nodesDraggable={!disabled}
|
|
511
|
+
nodesConnectable={!disabled}
|
|
512
|
+
elementsSelectable={!disabled}
|
|
513
|
+
panOnScroll
|
|
514
|
+
selectionOnDrag
|
|
515
|
+
defaultEdgeOptions={{
|
|
516
|
+
type: 'simplebezier',
|
|
517
|
+
animated: false,
|
|
518
|
+
style: {
|
|
519
|
+
stroke: '#94a3b8',
|
|
520
|
+
strokeWidth: 3,
|
|
521
|
+
},
|
|
457
522
|
}}
|
|
458
|
-
|
|
459
|
-
|
|
523
|
+
>
|
|
524
|
+
<Background color="#f0f0f0" gap={20} />
|
|
525
|
+
<MiniMap
|
|
526
|
+
nodeColor={(node) => {
|
|
527
|
+
switch (node.data?.stepType) {
|
|
528
|
+
case 'entry':
|
|
529
|
+
return '#5cb176';
|
|
530
|
+
case 'message':
|
|
531
|
+
return '#0077cc';
|
|
532
|
+
case 'wait':
|
|
533
|
+
return '#e9b200';
|
|
534
|
+
case 'branch':
|
|
535
|
+
return '#7b61ff';
|
|
536
|
+
case 'exit':
|
|
537
|
+
return '#dc2626';
|
|
538
|
+
default:
|
|
539
|
+
return '#999';
|
|
540
|
+
}
|
|
541
|
+
}}
|
|
542
|
+
maskColor="rgba(0, 0, 0, 0.1)"
|
|
543
|
+
style={{ background: '#f7f7f7' }}
|
|
544
|
+
/>
|
|
545
|
+
<FlowToolbar
|
|
546
|
+
onAddNode={handleAddNode}
|
|
547
|
+
onAutoLayout={handleAutoLayout}
|
|
548
|
+
disabled={disabled}
|
|
549
|
+
/>
|
|
550
|
+
<ValidationPanel
|
|
551
|
+
validation={validation}
|
|
552
|
+
onErrorClick={handleErrorClick}
|
|
553
|
+
/>
|
|
554
|
+
</ReactFlow>
|
|
555
|
+
<NodeEditPanel
|
|
556
|
+
node={selectedNode}
|
|
557
|
+
onClose={handleClosePanel}
|
|
558
|
+
onUpdate={handleNodeUpdate}
|
|
559
|
+
disabled={disabled}
|
|
560
|
+
campaignContext={campaignContext}
|
|
561
|
+
nodeErrors={selectedNode ? validation.nodeErrors.get(selectedNode.id) || [] : []}
|
|
460
562
|
/>
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
<NodeEditPanel
|
|
464
|
-
node={selectedNode}
|
|
465
|
-
onClose={handleClosePanel}
|
|
466
|
-
onUpdate={handleNodeUpdate}
|
|
467
|
-
disabled={disabled}
|
|
468
|
-
campaignContext={campaignContext}
|
|
469
|
-
/>
|
|
470
|
-
</Box>
|
|
563
|
+
</Box>
|
|
564
|
+
</ValidationProvider>
|
|
471
565
|
);
|
|
472
566
|
};
|
|
473
567
|
|
|
@@ -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>
|