@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.
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 +198 -104
  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-CZ2FJpk2.js → FlowCanvas-BW6aa6oF.js} +957 -85
  20. package/dist/_chunks/{FlowCanvas-Ktzy5E8i.mjs → FlowCanvas-BvC4CsIX.mjs} +960 -88
  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--y6vzvC5.mjs} +92 -113
  24. package/dist/_chunks/{index-DzjZLi7B.js → index-1FgA66KH.js} +30 -19
  25. package/dist/_chunks/{index-BFtqa7pq.js → index-pMzvnKEe.js} +237 -328
  26. package/dist/_chunks/{index-BWqO1iVx.mjs → index-vqLSa44X.mjs} +30 -19
  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,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 { FlowNode, FlowEdge, FlowNodeData, FlowStep, StepType, CampaignContext } from '../types';
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> = ({ steps, onStepsChange, disabled, error, campaignContext }) => {
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 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]);
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 = (internalNode as NodeWithPosition).positionAbsolute || internalNode.position;
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' || closestData.stepType === 'branch') {
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' || draggedData.stepType === 'branch') {
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
- closeEdge &&
167
- !nextEdges.find(
168
- (ne) => ne.source === closeEdge.source && ne.target === closeEdge.target
169
- )
170
- ) {
171
- nextEdges.push({ ...closeEdge, className: 'temp' });
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
- closeEdge &&
200
- !nextEdges.find(
201
- (ne) => ne.source === closeEdge.source && ne.target === closeEdge.target
202
- )
203
- ) {
204
- nextEdges.push(closeEdge);
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
- <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,
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
- <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';
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
- maskColor="rgba(0, 0, 0, 0.1)"
459
- style={{ background: '#f7f7f7' }}
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
- <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>
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
- 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>