@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.
Files changed (35) hide show
  1. package/admin/src/components/StepFlowBuilder/constants.ts +5 -3
  2. package/admin/src/components/StepFlowBuilder/context/ValidationContext.tsx +33 -0
  3. package/admin/src/components/StepFlowBuilder/context/index.ts +1 -0
  4. package/admin/src/components/StepFlowBuilder/edges/LabeledEdge.tsx +7 -13
  5. package/admin/src/components/StepFlowBuilder/flow-canvas/FlowCanvas.tsx +199 -103
  6. package/admin/src/components/StepFlowBuilder/index.tsx +33 -19
  7. package/admin/src/components/StepFlowBuilder/nodes/BranchNode.tsx +57 -12
  8. package/admin/src/components/StepFlowBuilder/nodes/EntryNode.tsx +52 -7
  9. package/admin/src/components/StepFlowBuilder/nodes/ExitNode.tsx +53 -8
  10. package/admin/src/components/StepFlowBuilder/nodes/MessageNode.tsx +55 -10
  11. package/admin/src/components/StepFlowBuilder/nodes/WaitNode.tsx +55 -10
  12. package/admin/src/components/StepFlowBuilder/panels/NodeEditPanel.tsx +119 -1
  13. package/admin/src/components/StepFlowBuilder/panels/ValidationPanel.tsx +282 -0
  14. package/admin/src/components/StepFlowBuilder/panels/index.ts +1 -0
  15. package/admin/src/components/StepFlowBuilder/utils.ts +18 -1
  16. package/admin/src/components/StepFlowBuilder/validation.ts +317 -0
  17. package/admin/src/translations/en.json +14 -1
  18. package/admin/src/translations/ru.json +14 -1
  19. package/dist/_chunks/{FlowCanvas-Ktzy5E8i.mjs → FlowCanvas-BQX_vGrx.mjs} +960 -87
  20. package/dist/_chunks/{FlowCanvas-CZ2FJpk2.js → FlowCanvas-CGSaCOu9.js} +957 -84
  21. package/dist/_chunks/en-Bar3xmYk.mjs +16 -0
  22. package/dist/_chunks/en-CMBkwPq9.js +16 -0
  23. package/dist/_chunks/{index-B6wwHXMh.mjs → index-BSzRuqIy.mjs} +92 -113
  24. package/dist/_chunks/{index-DzjZLi7B.js → index-CQakgGHK.js} +30 -19
  25. package/dist/_chunks/{index-BWqO1iVx.mjs → index-CxTg0iGt.mjs} +30 -19
  26. package/dist/_chunks/{index-BFtqa7pq.js → index-DFnEvoil.js} +92 -113
  27. package/dist/_chunks/ru-9mrg61lt.js +16 -0
  28. package/dist/_chunks/ru-CJuWxs7W.mjs +16 -0
  29. package/dist/admin/index.js +3 -3
  30. package/dist/admin/index.mjs +3 -3
  31. package/package.json +1 -1
  32. package/dist/_chunks/en-CC6nrL6u.mjs +0 -8
  33. package/dist/_chunks/en-Cx5J83tP.js +0 -8
  34. package/dist/_chunks/ru-Bbls49WS.mjs +0 -8
  35. 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: 2,
59
+ strokeWidth: 3,
60
60
  },
61
61
  yes: {
62
62
  stroke: '#10b981',
63
- strokeWidth: 2,
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: 2,
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
- sourceHandle,
16
+ sourceHandleId,
22
17
  style = {},
23
18
  markerEnd,
24
19
  }) => {
25
- const [edgePath] = getSmoothStepPath({
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 = sourceHandle === 'yes';
36
- const isNo = sourceHandle === 'no';
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 + 24;
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 { FlowNode, FlowEdge, FlowNodeData, FlowStep, StepType, CampaignContext } from '../types';
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> = ({ steps, onStepsChange, disabled, error, campaignContext }) => {
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 fitViewWithTopPadding = useCallback((topOffset = 40) => {
54
- fitView({ padding: 0.2, duration: 200 });
55
- setTimeout(() => {
56
- const { x, y, zoom } = getViewport();
57
- setViewport({ x, y: y - topOffset, zoom }, { duration: 200 });
58
- }, 210);
59
- }, [fitView, getViewport, setViewport]);
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 = (internalNode as NodeWithPosition).positionAbsolute || internalNode.position;
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' || closestData.stepType === 'branch') {
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' || draggedData.stepType === 'branch') {
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
- closeEdge &&
167
- !nextEdges.find(
168
- (ne) => ne.source === closeEdge.source && ne.target === closeEdge.target
169
- )
170
- ) {
171
- nextEdges.push({ ...closeEdge, className: 'temp' });
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
- closeEdge &&
200
- !nextEdges.find(
201
- (ne) => ne.source === closeEdge.source && ne.target === closeEdge.target
202
- )
203
- ) {
204
- nextEdges.push(closeEdge);
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
- <Box
393
- style={{
394
- height: '100%',
395
- width: '100%',
396
- border: error ? '1px solid #ee5e52' : '1px solid #dcdce4',
397
- borderRadius: 4,
398
- overflow: 'hidden',
399
- position: 'relative',
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
- <Background color="#f0f0f0" gap={20} />
440
- <Controls showInteractive={false} />
441
- <MiniMap
442
- nodeColor={(node) => {
443
- switch (node.data?.stepType) {
444
- case 'entry':
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
- maskColor="rgba(0, 0, 0, 0.1)"
459
- style={{ background: '#f7f7f7' }}
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
- <FlowToolbar onAddNode={handleAddNode} onAutoLayout={handleAutoLayout} disabled={disabled} />
462
- </ReactFlow>
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
- console.log('[StepFlowBuilder] No ContentManagerContext (test page?)');
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
- <Field.Label>{displayLabel}</Field.Label>
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} step{stepCount !== 1 ? 's' : ''}
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>