@inspirer-dev/crm-dashboard 1.0.72 → 1.0.73
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 +0 -25
- package/admin/src/components/StepFlowBuilder/flow-canvas/FlowCanvas.tsx +36 -51
- package/admin/src/components/StepFlowBuilder/hooks/useFlowHistory.ts +3 -0
- package/admin/src/components/StepFlowBuilder/hooks/useFlowTheme.ts +11 -13
- package/admin/src/components/StepFlowBuilder/index.tsx +49 -56
- package/admin/src/components/StepFlowBuilder/nodes/BranchNode.tsx +6 -82
- package/admin/src/components/StepFlowBuilder/nodes/EntryNode.tsx +4 -46
- package/admin/src/components/StepFlowBuilder/nodes/EventTriggerNode.tsx +6 -83
- package/admin/src/components/StepFlowBuilder/nodes/ExitNode.tsx +6 -82
- package/admin/src/components/StepFlowBuilder/nodes/MessageNode.tsx +6 -82
- package/admin/src/components/StepFlowBuilder/nodes/WaitNode.tsx +6 -82
- package/admin/src/components/StepFlowBuilder/nodes/shared.tsx +105 -0
- package/admin/src/components/StepFlowBuilder/panels/BranchConfig.tsx +9 -13
- package/admin/src/components/StepFlowBuilder/panels/EntryConfig.tsx +19 -33
- package/admin/src/components/StepFlowBuilder/panels/EventTriggerConfig.tsx +28 -17
- package/admin/src/components/StepFlowBuilder/panels/MessageConfig.tsx +40 -68
- package/admin/src/components/StepFlowBuilder/panels/NodeEditPanel.tsx +9 -27
- package/admin/src/components/StepFlowBuilder/panels/ValidationPanel.tsx +16 -42
- package/admin/src/components/StepFlowBuilder/panels/WaitConfig.tsx +3 -5
- package/admin/src/components/StepFlowBuilder/panels/shared-icons.tsx +31 -0
- package/admin/src/components/StepFlowBuilder/toolbar/FlowToolbar.tsx +7 -19
- package/admin/src/components/StepFlowBuilder/types.ts +35 -12
- package/admin/src/components/StepFlowBuilder/utils.ts +14 -3
- package/admin/src/components/StepFlowBuilder/validation.ts +20 -37
- package/admin/tsconfig.tsbuildinfo +1 -1
- package/dist/_chunks/{index-DPbRPHfu.mjs → index-Bo1xlxu3.mjs} +376 -690
- package/dist/_chunks/{index-DXq4OgEg.js → index-DgSik-4_.js} +374 -688
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/package.json +1 -1
- package/admin/src/components/StepFlowBuilder/edges/_backup_20260206/LabeledEdge.tsx +0 -78
- package/admin/src/components/StepFlowBuilder/nodes/_backup_20260206/BranchNode.tsx +0 -205
- package/admin/src/components/StepFlowBuilder/nodes/_backup_20260206/EntryNode.tsx +0 -133
- package/admin/src/components/StepFlowBuilder/nodes/_backup_20260206/ExitNode.tsx +0 -165
- package/admin/src/components/StepFlowBuilder/nodes/_backup_20260206/MessageNode.tsx +0 -215
- package/admin/src/components/StepFlowBuilder/nodes/_backup_20260206/WaitNode.tsx +0 -195
|
@@ -110,16 +110,12 @@ export const CHANNEL_OPTIONS: { value: ChannelType; label: string }[] = [
|
|
|
110
110
|
{ value: 'sms', label: 'SMS' },
|
|
111
111
|
];
|
|
112
112
|
|
|
113
|
-
export const CHANNELS = CHANNEL_OPTIONS;
|
|
114
|
-
|
|
115
113
|
export const DURATION_UNIT_OPTIONS: { value: DurationUnit; label: string }[] = [
|
|
116
114
|
{ value: 'minutes', label: 'Minutes' },
|
|
117
115
|
{ value: 'hours', label: 'Hours' },
|
|
118
116
|
{ value: 'days', label: 'Days' },
|
|
119
117
|
];
|
|
120
118
|
|
|
121
|
-
export const DURATION_UNITS = DURATION_UNIT_OPTIONS;
|
|
122
|
-
|
|
123
119
|
export const DEFAULT_NODE_DATA: Record<StepType, { name: string; config: object }> = {
|
|
124
120
|
entry: {
|
|
125
121
|
name: 'Entry',
|
|
@@ -166,24 +162,3 @@ export const PROXIMITY_THRESHOLD = 350;
|
|
|
166
162
|
|
|
167
163
|
export const AUTO_DISCONNECT_ENABLED = false;
|
|
168
164
|
|
|
169
|
-
export const DEFAULT_STEP_CONFIG: Record<StepType, object> = {
|
|
170
|
-
entry: {},
|
|
171
|
-
message: {
|
|
172
|
-
channel: 'telegram' as ChannelType,
|
|
173
|
-
variants: [],
|
|
174
|
-
},
|
|
175
|
-
wait: {
|
|
176
|
-
duration: 1,
|
|
177
|
-
durationUnit: 'hours' as DurationUnit,
|
|
178
|
-
},
|
|
179
|
-
branch: {
|
|
180
|
-
branchSegmentId: undefined,
|
|
181
|
-
},
|
|
182
|
-
exit: {},
|
|
183
|
-
event_trigger: {
|
|
184
|
-
eventTrigger: {
|
|
185
|
-
events: [],
|
|
186
|
-
logic: 'or',
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import ReactFlow, {
|
|
3
3
|
Background,
|
|
4
4
|
MiniMap,
|
|
@@ -52,7 +52,6 @@ interface FlowCanvasProps {
|
|
|
52
52
|
onStepsChange: (steps: FlowStep[]) => void;
|
|
53
53
|
onValidationChange?: (validation: ValidationResult) => void;
|
|
54
54
|
disabled?: boolean;
|
|
55
|
-
error?: string;
|
|
56
55
|
campaignContext?: CampaignContext;
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -61,29 +60,43 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
|
61
60
|
onStepsChange,
|
|
62
61
|
onValidationChange,
|
|
63
62
|
disabled,
|
|
64
|
-
error,
|
|
65
63
|
campaignContext,
|
|
66
64
|
}) => {
|
|
67
65
|
const [nodes, setNodes, onNodesChange] = useNodesState<FlowNodeData>([]);
|
|
68
66
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
69
67
|
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
|
|
70
68
|
const [, setHistoryTick] = useState(0);
|
|
69
|
+
const [validation, setValidation] = useState<ValidationResult>(() =>
|
|
70
|
+
validateFlow([] as FlowNode[], [] as FlowEdge[])
|
|
71
|
+
);
|
|
71
72
|
const isInitialMount = useRef(true);
|
|
72
73
|
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
74
|
+
const validationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
73
75
|
const lastStepsRef = useRef<string>('');
|
|
76
|
+
const lastValidationJsonRef = useRef<string>('');
|
|
74
77
|
const isRestoringRef = useRef(false);
|
|
75
78
|
const history = useFlowHistory();
|
|
76
79
|
const { fitView, getViewport, setViewport, setCenter } = useReactFlow();
|
|
77
80
|
const store = useStoreApi();
|
|
78
81
|
const updateNodeInternals = useUpdateNodeInternals();
|
|
79
82
|
|
|
80
|
-
const validation = useMemo(() => {
|
|
81
|
-
return validateFlow(nodes as FlowNode[], edges as FlowEdge[]);
|
|
82
|
-
}, [nodes, edges]);
|
|
83
|
-
|
|
84
83
|
useEffect(() => {
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
if (validationTimeoutRef.current) {
|
|
85
|
+
clearTimeout(validationTimeoutRef.current);
|
|
86
|
+
}
|
|
87
|
+
validationTimeoutRef.current = setTimeout(() => {
|
|
88
|
+
const result = validateFlow(nodes as FlowNode[], edges as FlowEdge[]);
|
|
89
|
+
const json = JSON.stringify({ e: result.errors.map((e) => e.id), w: result.warnings.map((w) => w.id) });
|
|
90
|
+
if (json !== lastValidationJsonRef.current) {
|
|
91
|
+
lastValidationJsonRef.current = json;
|
|
92
|
+
setValidation(result);
|
|
93
|
+
onValidationChange?.(result);
|
|
94
|
+
}
|
|
95
|
+
}, 200);
|
|
96
|
+
return () => {
|
|
97
|
+
if (validationTimeoutRef.current) clearTimeout(validationTimeoutRef.current);
|
|
98
|
+
};
|
|
99
|
+
}, [nodes, edges, onValidationChange]);
|
|
87
100
|
|
|
88
101
|
const fitViewWithOffset = useCallback(
|
|
89
102
|
({ topOffset = 40, rightOffset = 150, targetZoom = 0.5 } = {}) => {
|
|
@@ -220,8 +233,8 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
|
220
233
|
[store]
|
|
221
234
|
);
|
|
222
235
|
|
|
223
|
-
const
|
|
224
|
-
(
|
|
236
|
+
const applyProximityEdges = useCallback(
|
|
237
|
+
(node: Node<FlowNodeData>, isPreview: boolean) => {
|
|
225
238
|
const closeEdge = getClosestEdge(node);
|
|
226
239
|
|
|
227
240
|
setEdges((es) => {
|
|
@@ -252,7 +265,7 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
|
252
265
|
if (!sourceHandleAlreadyUsed && !targetAlreadyHasInput) {
|
|
253
266
|
nextEdges.push({
|
|
254
267
|
...closeEdge,
|
|
255
|
-
className: 'temp',
|
|
268
|
+
...(isPreview ? { className: 'temp' } : {}),
|
|
256
269
|
type: closeEdge.sourceHandle ? 'labeled' : 'default',
|
|
257
270
|
});
|
|
258
271
|
}
|
|
@@ -264,47 +277,18 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
|
264
277
|
[getClosestEdge, getNodeDistance, setEdges]
|
|
265
278
|
);
|
|
266
279
|
|
|
267
|
-
const
|
|
280
|
+
const handleNodeDrag = useCallback(
|
|
268
281
|
(_: React.MouseEvent, node: Node<FlowNodeData>) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (e.className === 'temp') return false;
|
|
274
|
-
if (AUTO_DISCONNECT_ENABLED) {
|
|
275
|
-
const isConnectedToDragged = e.source === node.id || e.target === node.id;
|
|
276
|
-
if (isConnectedToDragged) {
|
|
277
|
-
const otherNodeId = e.source === node.id ? e.target : e.source;
|
|
278
|
-
const distance = getNodeDistance(node.id, otherNodeId);
|
|
279
|
-
if (distance > PROXIMITY_THRESHOLD) {
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return true;
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
if (closeEdge) {
|
|
288
|
-
const sourceHandleAlreadyUsed = nextEdges.find(
|
|
289
|
-
(ne) =>
|
|
290
|
-
ne.source === closeEdge.source &&
|
|
291
|
-
ne.sourceHandle === closeEdge.sourceHandle
|
|
292
|
-
);
|
|
293
|
-
const targetAlreadyHasInput = nextEdges.find(
|
|
294
|
-
(ne) => ne.target === closeEdge.target
|
|
295
|
-
);
|
|
296
|
-
if (!sourceHandleAlreadyUsed && !targetAlreadyHasInput) {
|
|
297
|
-
nextEdges.push({
|
|
298
|
-
...closeEdge,
|
|
299
|
-
type: closeEdge.sourceHandle ? 'labeled' : 'default',
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
282
|
+
applyProximityEdges(node, true);
|
|
283
|
+
},
|
|
284
|
+
[applyProximityEdges]
|
|
285
|
+
);
|
|
303
286
|
|
|
304
|
-
|
|
305
|
-
|
|
287
|
+
const handleNodeDragStop = useCallback(
|
|
288
|
+
(_: React.MouseEvent, node: Node<FlowNodeData>) => {
|
|
289
|
+
applyProximityEdges(node, false);
|
|
306
290
|
},
|
|
307
|
-
[
|
|
291
|
+
[applyProximityEdges]
|
|
308
292
|
);
|
|
309
293
|
|
|
310
294
|
useEffect(() => {
|
|
@@ -564,7 +548,7 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
|
564
548
|
style={{
|
|
565
549
|
height: '100%',
|
|
566
550
|
width: '100%',
|
|
567
|
-
border:
|
|
551
|
+
border: 'none',
|
|
568
552
|
borderRadius: 0,
|
|
569
553
|
overflow: 'hidden',
|
|
570
554
|
position: 'relative',
|
|
@@ -609,6 +593,7 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|
|
609
593
|
wait: '#f59e0b',
|
|
610
594
|
branch: '#8b5cf6',
|
|
611
595
|
exit: '#6b7280',
|
|
596
|
+
event_trigger: '#f43f5e',
|
|
612
597
|
};
|
|
613
598
|
return colors[(node.data as FlowNodeData)?.stepType] || '#4a4a6a';
|
|
614
599
|
}}
|
|
@@ -24,6 +24,9 @@ export const useFlowHistory = () => {
|
|
|
24
24
|
const redo = useCallback((currentStepsJson: string): string | null => {
|
|
25
25
|
if (redoStack.current.length === 0) return null;
|
|
26
26
|
undoStack.current.push(currentStepsJson);
|
|
27
|
+
if (undoStack.current.length > MAX_HISTORY) {
|
|
28
|
+
undoStack.current.shift();
|
|
29
|
+
}
|
|
27
30
|
return redoStack.current.pop()!;
|
|
28
31
|
}, []);
|
|
29
32
|
|
|
@@ -7,19 +7,17 @@ export interface FlowThemeColors {
|
|
|
7
7
|
text: string;
|
|
8
8
|
textMuted: string;
|
|
9
9
|
textSubtle: string;
|
|
10
|
-
isDark: boolean;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
textSubtle: 'rgba(255, 255, 255, 0.4)',
|
|
23
|
-
isDark: true,
|
|
24
|
-
};
|
|
12
|
+
const THEME: FlowThemeColors = {
|
|
13
|
+
background: '#1a1a2e',
|
|
14
|
+
surface: '#252540',
|
|
15
|
+
surfaceHover: '#2f2f4a',
|
|
16
|
+
border: 'rgba(255, 255, 255, 0.1)',
|
|
17
|
+
borderLight: 'rgba(255, 255, 255, 0.06)',
|
|
18
|
+
text: '#ffffff',
|
|
19
|
+
textMuted: 'rgba(255, 255, 255, 0.6)',
|
|
20
|
+
textSubtle: 'rgba(255, 255, 255, 0.4)',
|
|
25
21
|
};
|
|
22
|
+
|
|
23
|
+
export const useFlowTheme = (): FlowThemeColors => THEME;
|
|
@@ -11,12 +11,20 @@ import {
|
|
|
11
11
|
|
|
12
12
|
import type { StepFlowBuilderProps, FlowStep, CampaignContext, EntrySegment } from './types';
|
|
13
13
|
import type { ValidationResult } from './validation';
|
|
14
|
-
import { parseValue } from './utils';
|
|
14
|
+
import { parseValue, pluralize } from './utils';
|
|
15
15
|
import { FlowCanvas } from './flow-canvas';
|
|
16
16
|
|
|
17
17
|
type SegmentItem = { id?: number; documentId?: string; name?: string };
|
|
18
18
|
type ConnectDisconnect = { connect?: SegmentItem[]; disconnect?: SegmentItem[] };
|
|
19
19
|
|
|
20
|
+
const useContentManagerContextSafe = () => {
|
|
21
|
+
try {
|
|
22
|
+
return useContentManagerContext();
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
20
28
|
interface StepFlowModalProps {
|
|
21
29
|
isOpen: boolean;
|
|
22
30
|
onClose: () => void;
|
|
@@ -27,6 +35,15 @@ interface StepFlowModalProps {
|
|
|
27
35
|
onValidationChange: (validation: ValidationResult) => void;
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
const SPIN_STYLE_ID = 'crm-spin-keyframes';
|
|
39
|
+
const ensureSpinKeyframes = () => {
|
|
40
|
+
if (document.getElementById(SPIN_STYLE_ID)) return;
|
|
41
|
+
const style = document.createElement('style');
|
|
42
|
+
style.id = SPIN_STYLE_ID;
|
|
43
|
+
style.textContent = `@keyframes crm-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.crm-spin{animation:crm-spin 1s linear infinite}`;
|
|
44
|
+
document.head.appendChild(style);
|
|
45
|
+
};
|
|
46
|
+
|
|
30
47
|
const StepFlowModal: React.FC<StepFlowModalProps> = ({
|
|
31
48
|
isOpen,
|
|
32
49
|
onClose,
|
|
@@ -36,11 +53,13 @@ const StepFlowModal: React.FC<StepFlowModalProps> = ({
|
|
|
36
53
|
disabled,
|
|
37
54
|
onValidationChange,
|
|
38
55
|
}) => {
|
|
56
|
+
useEffect(() => { ensureSpinKeyframes(); }, []);
|
|
39
57
|
const [steps, setSteps] = useState<FlowStep[]>(initialSteps);
|
|
40
58
|
const [validation, setValidation] = useState<ValidationResult | null>(null);
|
|
41
59
|
const [saveLabel, setSaveLabel] = useState<'save' | 'saved' | 'saving'>('save');
|
|
42
60
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
43
61
|
const savedStepsRef = useRef<string>(JSON.stringify(initialSteps));
|
|
62
|
+
const saveTimersRef = useRef<NodeJS.Timeout[]>([]);
|
|
44
63
|
|
|
45
64
|
useEffect(() => {
|
|
46
65
|
setSteps(initialSteps);
|
|
@@ -61,15 +80,26 @@ const StepFlowModal: React.FC<StepFlowModalProps> = ({
|
|
|
61
80
|
[onValidationChange]
|
|
62
81
|
);
|
|
63
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
return () => {
|
|
85
|
+
saveTimersRef.current.forEach(clearTimeout);
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
64
89
|
const handleSave = useCallback(() => {
|
|
90
|
+
saveTimersRef.current.forEach(clearTimeout);
|
|
91
|
+
saveTimersRef.current = [];
|
|
92
|
+
|
|
65
93
|
setSaveLabel('saving');
|
|
66
|
-
setTimeout(() => {
|
|
94
|
+
const t1 = setTimeout(() => {
|
|
67
95
|
onSave(steps);
|
|
68
96
|
savedStepsRef.current = JSON.stringify(steps);
|
|
69
97
|
setHasUnsavedChanges(false);
|
|
70
98
|
setSaveLabel('saved');
|
|
71
|
-
setTimeout(() => setSaveLabel('save'), 1500);
|
|
99
|
+
const t2 = setTimeout(() => setSaveLabel('save'), 1500);
|
|
100
|
+
saveTimersRef.current.push(t2);
|
|
72
101
|
}, 150);
|
|
102
|
+
saveTimersRef.current.push(t1);
|
|
73
103
|
}, [steps, onSave]);
|
|
74
104
|
|
|
75
105
|
if (!isOpen) return null;
|
|
@@ -78,23 +108,9 @@ const StepFlowModal: React.FC<StepFlowModalProps> = ({
|
|
|
78
108
|
const errorCount = validation?.errors.length || 0;
|
|
79
109
|
const warningCount = validation?.warnings.length || 0;
|
|
80
110
|
|
|
81
|
-
const getStepLabel = (count: number) =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return 'шагов';
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const getErrorLabel = (count: number) => {
|
|
88
|
-
if (count === 1) return 'ошибка';
|
|
89
|
-
if (count >= 2 && count <= 4) return 'ошибки';
|
|
90
|
-
return 'ошибок';
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const getWarningLabel = (count: number) => {
|
|
94
|
-
if (count === 1) return 'предупреждение';
|
|
95
|
-
if (count >= 2 && count <= 4) return 'предупреждения';
|
|
96
|
-
return 'предупреждений';
|
|
97
|
-
};
|
|
111
|
+
const getStepLabel = (count: number) => pluralize(count, 'шаг', 'шага', 'шагов');
|
|
112
|
+
const getErrorLabel = (count: number) => pluralize(count, 'ошибка', 'ошибки', 'ошибок');
|
|
113
|
+
const getWarningLabel = (count: number) => pluralize(count, 'предупреждение', 'предупреждения', 'предупреждений');
|
|
98
114
|
|
|
99
115
|
return (
|
|
100
116
|
<Modal.Root open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
@@ -275,7 +291,7 @@ const StepFlowModal: React.FC<StepFlowModalProps> = ({
|
|
|
275
291
|
height="14"
|
|
276
292
|
viewBox="0 0 24 24"
|
|
277
293
|
fill="none"
|
|
278
|
-
|
|
294
|
+
className="crm-spin"
|
|
279
295
|
>
|
|
280
296
|
<circle
|
|
281
297
|
cx="12"
|
|
@@ -306,14 +322,6 @@ const StepFlowModal: React.FC<StepFlowModalProps> = ({
|
|
|
306
322
|
position: 'relative',
|
|
307
323
|
}}
|
|
308
324
|
>
|
|
309
|
-
<style>
|
|
310
|
-
{`
|
|
311
|
-
@keyframes spin {
|
|
312
|
-
from { transform: rotate(0deg); }
|
|
313
|
-
to { transform: rotate(360deg); }
|
|
314
|
-
}
|
|
315
|
-
`}
|
|
316
|
-
</style>
|
|
317
325
|
<ReactFlowProvider>
|
|
318
326
|
<div style={{ position: 'absolute', inset: 0 }}>
|
|
319
327
|
<FlowCanvas
|
|
@@ -342,16 +350,9 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
342
350
|
|
|
343
351
|
const form = useForm('StepFlowBuilder', (state) => state);
|
|
344
352
|
const { get } = useFetchClient();
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
const ctx = useContentManagerContext();
|
|
350
|
-
documentId = ctx?.id;
|
|
351
|
-
model = ctx?.model;
|
|
352
|
-
} catch (err) {
|
|
353
|
-
// No ContentManagerContext (test page?)
|
|
354
|
-
}
|
|
353
|
+
const ctx = useContentManagerContextSafe();
|
|
354
|
+
const documentId = ctx?.id;
|
|
355
|
+
const model = ctx?.model;
|
|
355
356
|
|
|
356
357
|
useEffect(() => {
|
|
357
358
|
if (!documentId || !model || hasFetched) {
|
|
@@ -402,7 +403,9 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
402
403
|
|
|
403
404
|
if (connect.length > 0) {
|
|
404
405
|
const seg = connect[0];
|
|
405
|
-
|
|
406
|
+
if (seg.id != null) {
|
|
407
|
+
return { id: seg.id, name: seg.name || `Segment #${seg.id}` };
|
|
408
|
+
}
|
|
406
409
|
}
|
|
407
410
|
|
|
408
411
|
if (disconnect.length > 0 && initialSegment) {
|
|
@@ -438,7 +441,7 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
438
441
|
useEffect(() => {
|
|
439
442
|
const parsed = parseValue(value);
|
|
440
443
|
setSteps(parsed);
|
|
441
|
-
}, []);
|
|
444
|
+
}, [value]);
|
|
442
445
|
|
|
443
446
|
const handleValidationChange = useCallback((newValidation: ValidationResult) => {
|
|
444
447
|
setValidation(newValidation);
|
|
@@ -473,7 +476,7 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
473
476
|
const validationError = useMemo(() => {
|
|
474
477
|
if (!validation || validation.isValid) return null;
|
|
475
478
|
const errorCount = validation.errors.length;
|
|
476
|
-
const errorWord = errorCount
|
|
479
|
+
const errorWord = pluralize(errorCount, 'ошибка', 'ошибки', 'ошибок');
|
|
477
480
|
return `Поток содержит ${errorCount} ${errorWord} валидации. Исправьте все ошибки перед публикацией.`;
|
|
478
481
|
}, [validation]);
|
|
479
482
|
|
|
@@ -487,11 +490,7 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
487
490
|
{validation && !validation.isValid && (
|
|
488
491
|
<Badge backgroundColor="danger100" textColor="danger600">
|
|
489
492
|
{validation.errors.length}{' '}
|
|
490
|
-
{validation.errors.length
|
|
491
|
-
? 'ошибка'
|
|
492
|
-
: validation.errors.length < 5
|
|
493
|
-
? 'ошибки'
|
|
494
|
-
: 'ошибок'}
|
|
493
|
+
{pluralize(validation.errors.length, 'ошибка', 'ошибки', 'ошибок')}
|
|
495
494
|
</Badge>
|
|
496
495
|
)}
|
|
497
496
|
{validation && validation.isValid && validation.warnings.length > 0 && (
|
|
@@ -502,7 +501,7 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
502
501
|
)}
|
|
503
502
|
</Flex>
|
|
504
503
|
<Badge backgroundColor="neutral100" textColor="neutral600">
|
|
505
|
-
{stepCount} {stepCount
|
|
504
|
+
{stepCount} {pluralize(stepCount, 'шаг', 'шага', 'шагов')}
|
|
506
505
|
</Badge>
|
|
507
506
|
</Flex>
|
|
508
507
|
|
|
@@ -552,12 +551,6 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
|
|
|
552
551
|
}
|
|
553
552
|
);
|
|
554
553
|
|
|
555
|
-
StepFlowBuilderInner.displayName = '
|
|
556
|
-
|
|
557
|
-
const StepFlowBuilder = forwardRef<HTMLDivElement, StepFlowBuilderProps>((props, ref) => {
|
|
558
|
-
return <StepFlowBuilderInner {...props} ref={ref} />;
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
StepFlowBuilder.displayName = 'StepFlowBuilder';
|
|
554
|
+
StepFlowBuilderInner.displayName = 'StepFlowBuilder';
|
|
562
555
|
|
|
563
|
-
export default
|
|
556
|
+
export default StepFlowBuilderInner;
|
|
@@ -1,44 +1,16 @@
|
|
|
1
1
|
import React, { memo, useState } from 'react';
|
|
2
|
-
import { Handle, Position,
|
|
2
|
+
import { Handle, Position, type NodeProps } from 'reactflow';
|
|
3
3
|
import type { FlowNodeData } from '../types';
|
|
4
4
|
import { NODE_COLORS, NODE_DIMENSIONS, HANDLE_SIZE } from '../constants';
|
|
5
5
|
import { useNodeValidation } from '../context/ValidationContext';
|
|
6
|
+
import { getBorderStyle, getBoxShadow, ValidationBadge, DeleteButton } from './shared';
|
|
6
7
|
|
|
7
8
|
const BranchNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) => {
|
|
8
9
|
const [isHovered, setIsHovered] = useState(false);
|
|
9
|
-
const { deleteElements } = useReactFlow();
|
|
10
10
|
const colors = NODE_COLORS.branch;
|
|
11
11
|
const segmentName = data.config.branchSegment?.name || 'No segment';
|
|
12
12
|
const { hasError, hasWarning } = useNodeValidation(id);
|
|
13
13
|
|
|
14
|
-
const handleDelete = (e: React.MouseEvent) => {
|
|
15
|
-
e.stopPropagation();
|
|
16
|
-
deleteElements({ nodes: [{ id }] });
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const getBorderStyle = () => {
|
|
20
|
-
if (hasError) return '2px solid #ef4444';
|
|
21
|
-
if (hasWarning) return '2px solid #f59e0b';
|
|
22
|
-
if (selected) return `2px solid ${colors.border}`;
|
|
23
|
-
return '2px solid transparent';
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const getBoxShadow = () => {
|
|
27
|
-
if (hasError) {
|
|
28
|
-
return selected
|
|
29
|
-
? '0 0 0 3px rgba(239, 68, 68, 0.25), 0 10px 25px -5px rgba(0, 0, 0, 0.1)'
|
|
30
|
-
: '0 0 0 2px rgba(239, 68, 68, 0.15), 0 4px 12px -2px rgba(0, 0, 0, 0.08)';
|
|
31
|
-
}
|
|
32
|
-
if (hasWarning) {
|
|
33
|
-
return selected
|
|
34
|
-
? '0 0 0 3px rgba(245, 158, 11, 0.25), 0 10px 25px -5px rgba(0, 0, 0, 0.1)'
|
|
35
|
-
: '0 0 0 2px rgba(245, 158, 11, 0.15), 0 4px 12px -2px rgba(0, 0, 0, 0.08)';
|
|
36
|
-
}
|
|
37
|
-
return selected
|
|
38
|
-
? `0 0 0 3px ${colors.glow}, 0 10px 25px -5px rgba(0, 0, 0, 0.1)`
|
|
39
|
-
: '0 4px 12px -2px rgba(0, 0, 0, 0.08)';
|
|
40
|
-
};
|
|
41
|
-
|
|
42
14
|
return (
|
|
43
15
|
<div
|
|
44
16
|
style={{
|
|
@@ -47,8 +19,8 @@ const BranchNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) =
|
|
|
47
19
|
boxSizing: 'border-box',
|
|
48
20
|
background: colors.background,
|
|
49
21
|
borderRadius: 12,
|
|
50
|
-
border: getBorderStyle(),
|
|
51
|
-
boxShadow: getBoxShadow(),
|
|
22
|
+
border: getBorderStyle(hasError, hasWarning, !!selected, colors),
|
|
23
|
+
boxShadow: getBoxShadow(hasError, hasWarning, !!selected, colors),
|
|
52
24
|
display: 'flex',
|
|
53
25
|
alignItems: 'center',
|
|
54
26
|
padding: '0 16px',
|
|
@@ -72,56 +44,8 @@ const BranchNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) =
|
|
|
72
44
|
boxShadow: '0 0 0 3px white, 0 2px 4px rgba(0,0,0,0.1)',
|
|
73
45
|
}}
|
|
74
46
|
/>
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
style={{
|
|
78
|
-
position: 'absolute',
|
|
79
|
-
top: -6,
|
|
80
|
-
right: -6,
|
|
81
|
-
width: 18,
|
|
82
|
-
height: 18,
|
|
83
|
-
borderRadius: '50%',
|
|
84
|
-
background: hasError ? '#ef4444' : '#f59e0b',
|
|
85
|
-
display: 'flex',
|
|
86
|
-
alignItems: 'center',
|
|
87
|
-
justifyContent: 'center',
|
|
88
|
-
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
|
89
|
-
}}
|
|
90
|
-
>
|
|
91
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none">
|
|
92
|
-
<path d="M12 8v4M12 16h.01" stroke="white" strokeWidth="3" strokeLinecap="round" />
|
|
93
|
-
</svg>
|
|
94
|
-
</div>
|
|
95
|
-
)}
|
|
96
|
-
{isHovered && (
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
onClick={handleDelete}
|
|
100
|
-
style={{
|
|
101
|
-
position: 'absolute',
|
|
102
|
-
top: -8,
|
|
103
|
-
right: -8,
|
|
104
|
-
width: 22,
|
|
105
|
-
height: 22,
|
|
106
|
-
borderRadius: '50%',
|
|
107
|
-
background: '#ef4444',
|
|
108
|
-
border: '2px solid white',
|
|
109
|
-
display: 'flex',
|
|
110
|
-
alignItems: 'center',
|
|
111
|
-
justifyContent: 'center',
|
|
112
|
-
cursor: 'pointer',
|
|
113
|
-
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
|
114
|
-
transition: 'transform 0.15s ease',
|
|
115
|
-
zIndex: 10,
|
|
116
|
-
}}
|
|
117
|
-
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.1)'; }}
|
|
118
|
-
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
|
|
119
|
-
>
|
|
120
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none">
|
|
121
|
-
<path d="M18 6L6 18M6 6l12 12" stroke="white" strokeWidth="3" strokeLinecap="round" />
|
|
122
|
-
</svg>
|
|
123
|
-
</button>
|
|
124
|
-
)}
|
|
47
|
+
{!isHovered && <ValidationBadge hasError={hasError} hasWarning={hasWarning} />}
|
|
48
|
+
{isHovered && <DeleteButton nodeId={id} />}
|
|
125
49
|
<div
|
|
126
50
|
style={{
|
|
127
51
|
width: 36,
|
|
@@ -3,34 +3,12 @@ import { Handle, Position, type NodeProps } from 'reactflow';
|
|
|
3
3
|
import type { FlowNodeData } from '../types';
|
|
4
4
|
import { NODE_COLORS, NODE_DIMENSIONS, HANDLE_SIZE, ENTRY_NODE_ID } from '../constants';
|
|
5
5
|
import { useNodeValidation } from '../context/ValidationContext';
|
|
6
|
+
import { getBorderStyle, getBoxShadow, ValidationBadge } from './shared';
|
|
6
7
|
|
|
7
8
|
const EntryNode: React.FC<NodeProps<FlowNodeData>> = ({ selected }) => {
|
|
8
9
|
const colors = NODE_COLORS.entry;
|
|
9
10
|
const { hasError, hasWarning } = useNodeValidation(ENTRY_NODE_ID);
|
|
10
11
|
|
|
11
|
-
const getBorderStyle = () => {
|
|
12
|
-
if (hasError) return '2px solid #ef4444';
|
|
13
|
-
if (hasWarning) return '2px solid #f59e0b';
|
|
14
|
-
if (selected) return `2px solid ${colors.border}`;
|
|
15
|
-
return '2px solid transparent';
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const getBoxShadow = () => {
|
|
19
|
-
if (hasError) {
|
|
20
|
-
return selected
|
|
21
|
-
? '0 0 0 3px rgba(239, 68, 68, 0.25), 0 10px 25px -5px rgba(0, 0, 0, 0.1)'
|
|
22
|
-
: '0 0 0 2px rgba(239, 68, 68, 0.15), 0 4px 12px -2px rgba(0, 0, 0, 0.08)';
|
|
23
|
-
}
|
|
24
|
-
if (hasWarning) {
|
|
25
|
-
return selected
|
|
26
|
-
? '0 0 0 3px rgba(245, 158, 11, 0.25), 0 10px 25px -5px rgba(0, 0, 0, 0.1)'
|
|
27
|
-
: '0 0 0 2px rgba(245, 158, 11, 0.15), 0 4px 12px -2px rgba(0, 0, 0, 0.08)';
|
|
28
|
-
}
|
|
29
|
-
return selected
|
|
30
|
-
? `0 0 0 3px ${colors.glow}, 0 10px 25px -5px rgba(0, 0, 0, 0.1)`
|
|
31
|
-
: '0 4px 12px -2px rgba(0, 0, 0, 0.08)';
|
|
32
|
-
};
|
|
33
|
-
|
|
34
12
|
return (
|
|
35
13
|
<div
|
|
36
14
|
style={{
|
|
@@ -39,8 +17,8 @@ const EntryNode: React.FC<NodeProps<FlowNodeData>> = ({ selected }) => {
|
|
|
39
17
|
boxSizing: 'border-box',
|
|
40
18
|
background: colors.background,
|
|
41
19
|
borderRadius: 12,
|
|
42
|
-
border: getBorderStyle(),
|
|
43
|
-
boxShadow: getBoxShadow(),
|
|
20
|
+
border: getBorderStyle(hasError, hasWarning, !!selected, colors),
|
|
21
|
+
boxShadow: getBoxShadow(hasError, hasWarning, !!selected, colors),
|
|
44
22
|
display: 'flex',
|
|
45
23
|
alignItems: 'center',
|
|
46
24
|
justifyContent: 'center',
|
|
@@ -50,27 +28,7 @@ const EntryNode: React.FC<NodeProps<FlowNodeData>> = ({ selected }) => {
|
|
|
50
28
|
position: 'relative',
|
|
51
29
|
}}
|
|
52
30
|
>
|
|
53
|
-
{
|
|
54
|
-
<div
|
|
55
|
-
style={{
|
|
56
|
-
position: 'absolute',
|
|
57
|
-
top: -6,
|
|
58
|
-
right: -6,
|
|
59
|
-
width: 18,
|
|
60
|
-
height: 18,
|
|
61
|
-
borderRadius: '50%',
|
|
62
|
-
background: hasError ? '#ef4444' : '#f59e0b',
|
|
63
|
-
display: 'flex',
|
|
64
|
-
alignItems: 'center',
|
|
65
|
-
justifyContent: 'center',
|
|
66
|
-
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
|
67
|
-
}}
|
|
68
|
-
>
|
|
69
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none">
|
|
70
|
-
<path d="M12 8v4M12 16h.01" stroke="white" strokeWidth="3" strokeLinecap="round" />
|
|
71
|
-
</svg>
|
|
72
|
-
</div>
|
|
73
|
-
)}
|
|
31
|
+
<ValidationBadge hasError={hasError} hasWarning={hasWarning} />
|
|
74
32
|
<div
|
|
75
33
|
style={{
|
|
76
34
|
width: 36,
|