@inspirer-dev/crm-dashboard 1.0.21 → 1.0.22

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 (33) hide show
  1. package/admin/src/components/StepFlowBuilder/constants.ts +91 -0
  2. package/admin/src/components/StepFlowBuilder/edges/LabeledEdge.tsx +77 -0
  3. package/admin/src/components/StepFlowBuilder/edges/index.ts +8 -0
  4. package/admin/src/components/StepFlowBuilder/index.tsx +320 -0
  5. package/admin/src/components/StepFlowBuilder/nodes/BranchNode.tsx +90 -0
  6. package/admin/src/components/StepFlowBuilder/nodes/EntryNode.tsx +47 -0
  7. package/admin/src/components/StepFlowBuilder/nodes/ExitNode.tsx +47 -0
  8. package/admin/src/components/StepFlowBuilder/nodes/MessageNode.tsx +78 -0
  9. package/admin/src/components/StepFlowBuilder/nodes/WaitNode.tsx +71 -0
  10. package/admin/src/components/StepFlowBuilder/nodes/index.ts +16 -0
  11. package/admin/src/components/StepFlowBuilder/panels/BranchConfig.tsx +112 -0
  12. package/admin/src/components/StepFlowBuilder/panels/MessageConfig.tsx +188 -0
  13. package/admin/src/components/StepFlowBuilder/panels/NodeEditPanel.tsx +158 -0
  14. package/admin/src/components/StepFlowBuilder/panels/WaitConfig.tsx +87 -0
  15. package/admin/src/components/StepFlowBuilder/panels/index.ts +4 -0
  16. package/admin/src/components/StepFlowBuilder/toolbar/FlowToolbar.tsx +86 -0
  17. package/admin/src/components/StepFlowBuilder/toolbar/index.ts +1 -0
  18. package/admin/src/components/StepFlowBuilder/types.ts +77 -0
  19. package/admin/src/components/StepFlowBuilder/utils.ts +217 -0
  20. package/admin/src/index.ts +8 -8
  21. package/dist/_chunks/index-BK8649hk.mjs +1405 -0
  22. package/dist/_chunks/{index-CWnuAWMG.mjs → index-BeiHTAlq.mjs} +91 -112
  23. package/dist/_chunks/{index-Bw1mkNpv.js → index-T7-DTUJN.js} +262 -227
  24. package/dist/_chunks/index-aSjgyfVX.js +1408 -0
  25. package/dist/admin/index.js +9 -9
  26. package/dist/admin/index.mjs +10 -10
  27. package/dist/server/index.js +1 -1
  28. package/dist/server/index.mjs +1 -1
  29. package/package.json +3 -1
  30. package/server/src/register.ts +1 -1
  31. package/admin/src/components/JourneyConfigField/index.tsx +0 -744
  32. package/dist/_chunks/index-BhNY5vYI.js +0 -591
  33. package/dist/_chunks/index-D0XEcc24.mjs +0 -591
@@ -1,744 +0,0 @@
1
- import React, { forwardRef, useEffect, useState } from 'react';
2
- import {
3
- Box,
4
- Button,
5
- Card,
6
- CardContent,
7
- Field,
8
- Flex,
9
- IconButton,
10
- SingleSelect,
11
- SingleSelectOption,
12
- TextInput,
13
- Typography,
14
- Tooltip,
15
- Badge,
16
- Divider,
17
- } from '@strapi/design-system';
18
- import { Plus, Trash, ArrowUp, ArrowDown, Mail, Clock, ArrowRight, Play, Cross } from '@strapi/icons';
19
- import { generateId } from '../RulesBuilder/utils';
20
-
21
- type JourneyNodeType = 'entrance' | 'message' | 'wait' | 'branch' | 'exit';
22
- type JourneyChannel = 'telegram' | 'email' | 'push' | 'sms';
23
- type JourneyWaitUnit = 'minutes' | 'hours' | 'days';
24
- type JourneyBranchType = 'default' | 'yes' | 'no';
25
-
26
- interface JourneyMessageVariant {
27
- id: string;
28
- name: string;
29
- templateId: string;
30
- weight: number;
31
- }
32
-
33
- interface JourneyMessageConfig {
34
- channel: JourneyChannel;
35
- variants: JourneyMessageVariant[];
36
- }
37
-
38
- interface JourneyWaitConfig {
39
- durationValue: number;
40
- durationUnit: JourneyWaitUnit;
41
- }
42
-
43
- interface JourneyBranchConfig {
44
- segmentId: string;
45
- useStepSegment: boolean;
46
- }
47
-
48
- interface JourneyStep {
49
- id: string;
50
- stepKey: string;
51
- name: string;
52
- nodeType: JourneyNodeType;
53
- messageConfig?: JourneyMessageConfig;
54
- waitConfig?: JourneyWaitConfig;
55
- branchConfig?: JourneyBranchConfig;
56
- }
57
-
58
- interface JourneyTransition {
59
- id: string;
60
- sourceStepKey: string;
61
- targetStepKey: string;
62
- branchType: JourneyBranchType;
63
- }
64
-
65
- interface JourneyConfig {
66
- steps: JourneyStep[];
67
- transitions: JourneyTransition[];
68
- entryOncePerUser: boolean;
69
- reEntryDelayDays: number;
70
- }
71
-
72
- interface JourneyConfigFieldProps {
73
- name: string;
74
- value?: string | JourneyConfig | null;
75
- onChange: (event: { target: { name: string; value: string } }) => void;
76
- intlLabel: { id: string; defaultMessage: string };
77
- attribute?: unknown;
78
- disabled?: boolean;
79
- error?: string;
80
- required?: boolean;
81
- hint?: string;
82
- }
83
-
84
- const NODE_TYPES: { value: JourneyNodeType; label: string; icon: React.ReactNode; description: string }[] = [
85
- { value: 'message', label: 'Send Message', icon: <Mail />, description: 'Send a message to user' },
86
- { value: 'wait', label: 'Wait', icon: <Clock />, description: 'Wait for a period of time' },
87
- { value: 'branch', label: 'Branch', icon: <ArrowRight />, description: 'Split flow based on segment' },
88
- { value: 'exit', label: 'Exit', icon: <Cross />, description: 'End the journey' },
89
- ];
90
-
91
- const CHANNELS: { value: JourneyChannel; label: string }[] = [
92
- { value: 'telegram', label: 'Telegram' },
93
- { value: 'email', label: 'Email' },
94
- { value: 'push', label: 'Push Notification' },
95
- { value: 'sms', label: 'SMS' },
96
- ];
97
-
98
- const WAIT_UNITS: { value: JourneyWaitUnit; label: string }[] = [
99
- { value: 'minutes', label: 'Minutes' },
100
- { value: 'hours', label: 'Hours' },
101
- { value: 'days', label: 'Days' },
102
- ];
103
-
104
- const DEFAULT_CONFIG: JourneyConfig = {
105
- steps: [
106
- { id: generateId(), stepKey: 'entrance', name: 'Entry Point', nodeType: 'entrance' },
107
- ],
108
- transitions: [],
109
- entryOncePerUser: true,
110
- reEntryDelayDays: 30,
111
- };
112
-
113
- const parseConfig = (value: string | JourneyConfig | null | undefined): JourneyConfig => {
114
- if (!value) return { ...DEFAULT_CONFIG, steps: [{ ...DEFAULT_CONFIG.steps[0], id: generateId() }] };
115
- if (typeof value === 'string') {
116
- try {
117
- const parsed = JSON.parse(value);
118
- if (parsed.steps && Array.isArray(parsed.steps)) {
119
- return parsed as JourneyConfig;
120
- }
121
- } catch {
122
- return { ...DEFAULT_CONFIG, steps: [{ ...DEFAULT_CONFIG.steps[0], id: generateId() }] };
123
- }
124
- }
125
- if (typeof value === 'object' && value.steps) {
126
- return value as JourneyConfig;
127
- }
128
- return { ...DEFAULT_CONFIG, steps: [{ ...DEFAULT_CONFIG.steps[0], id: generateId() }] };
129
- };
130
-
131
- const serializeConfig = (config: JourneyConfig): string => JSON.stringify(config);
132
-
133
- const getStepIcon = (nodeType: JourneyNodeType) => {
134
- switch (nodeType) {
135
- case 'entrance': return <Play />;
136
- case 'message': return <Mail />;
137
- case 'wait': return <Clock />;
138
- case 'branch': return <ArrowRight />;
139
- case 'exit': return <Cross />;
140
- default: return null;
141
- }
142
- };
143
-
144
- const getStepColor = (nodeType: JourneyNodeType) => {
145
- switch (nodeType) {
146
- case 'entrance': return { bg: 'success100', border: '#328048', text: 'success600' };
147
- case 'message': return { bg: 'primary100', border: '#4945ff', text: 'primary600' };
148
- case 'wait': return { bg: 'warning100', border: '#d9822f', text: 'warning600' };
149
- case 'branch': return { bg: 'secondary100', border: '#8e44ad', text: 'secondary600' };
150
- case 'exit': return { bg: 'neutral200', border: '#666687', text: 'neutral600' };
151
- default: return { bg: 'neutral100', border: '#dcdce4', text: 'neutral600' };
152
- }
153
- };
154
-
155
- const StepCard: React.FC<{
156
- step: JourneyStep;
157
- index: number;
158
- totalSteps: number;
159
- disabled?: boolean;
160
- expanded: boolean;
161
- onToggleExpand: () => void;
162
- onUpdate: (updates: Partial<JourneyStep>) => void;
163
- onDelete: () => void;
164
- onMove: (direction: 'up' | 'down') => void;
165
- transitions: JourneyTransition[];
166
- allSteps: JourneyStep[];
167
- onUpdateTransition: (stepKey: string, branchType: JourneyBranchType, targetStepKey: string) => void;
168
- }> = ({ step, index, totalSteps, disabled, expanded, onToggleExpand, onUpdate, onDelete, onMove, transitions, allSteps, onUpdateTransition }) => {
169
- const colors = getStepColor(step.nodeType);
170
- const isEntrance = step.nodeType === 'entrance';
171
-
172
- const availableTargets = allSteps.filter(s => s.stepKey !== step.stepKey && s.nodeType !== 'entrance');
173
-
174
- const getNextStep = (branchType: JourneyBranchType) => {
175
- const t = transitions.find(tr => tr.sourceStepKey === step.stepKey && tr.branchType === branchType);
176
- return t?.targetStepKey || '';
177
- };
178
-
179
- const updateMessageConfig = (updates: Partial<JourneyMessageConfig>) => {
180
- onUpdate({
181
- messageConfig: {
182
- channel: step.messageConfig?.channel || 'telegram',
183
- variants: step.messageConfig?.variants || [],
184
- ...step.messageConfig,
185
- ...updates,
186
- },
187
- });
188
- };
189
-
190
- const updateWaitConfig = (updates: Partial<JourneyWaitConfig>) => {
191
- onUpdate({
192
- waitConfig: {
193
- durationValue: step.waitConfig?.durationValue || 30,
194
- durationUnit: step.waitConfig?.durationUnit || 'minutes',
195
- ...step.waitConfig,
196
- ...updates,
197
- },
198
- });
199
- };
200
-
201
- const updateBranchConfig = (updates: Partial<JourneyBranchConfig>) => {
202
- onUpdate({
203
- branchConfig: {
204
- segmentId: step.branchConfig?.segmentId || '',
205
- useStepSegment: step.branchConfig?.useStepSegment || false,
206
- ...step.branchConfig,
207
- ...updates,
208
- },
209
- });
210
- };
211
-
212
- const addVariant = () => {
213
- const variants = step.messageConfig?.variants || [];
214
- updateMessageConfig({
215
- variants: [
216
- ...variants,
217
- { id: generateId(), name: `Variant ${variants.length + 1}`, templateId: '', weight: 100 },
218
- ],
219
- });
220
- };
221
-
222
- const updateVariant = (variantId: string, updates: Partial<JourneyMessageVariant>) => {
223
- const variants = step.messageConfig?.variants || [];
224
- updateMessageConfig({
225
- variants: variants.map(v => v.id === variantId ? { ...v, ...updates } : v),
226
- });
227
- };
228
-
229
- const deleteVariant = (variantId: string) => {
230
- const variants = step.messageConfig?.variants || [];
231
- updateMessageConfig({
232
- variants: variants.filter(v => v.id !== variantId),
233
- });
234
- };
235
-
236
- return (
237
- <Card
238
- background={colors.bg}
239
- style={{ border: `2px solid ${colors.border}` }}
240
- >
241
- <CardContent>
242
- <Flex direction="column" gap={3} padding={3}>
243
- <Flex justifyContent="space-between" alignItems="center">
244
- <Flex gap={2} alignItems="center">
245
- <Box style={{ color: colors.border }}>{getStepIcon(step.nodeType)}</Box>
246
- <Badge backgroundColor={colors.bg} textColor={colors.text}>
247
- {step.nodeType.toUpperCase()}
248
- </Badge>
249
- <Typography variant="omega" fontWeight="bold">{step.name}</Typography>
250
- </Flex>
251
-
252
- <Flex gap={1}>
253
- {!isEntrance && (
254
- <>
255
- <Tooltip label="Move up">
256
- <IconButton
257
- onClick={() => onMove('up')}
258
- label="Move up"
259
- variant="ghost"
260
- disabled={disabled || index <= 1}
261
- >
262
- <ArrowUp />
263
- </IconButton>
264
- </Tooltip>
265
- <Tooltip label="Move down">
266
- <IconButton
267
- onClick={() => onMove('down')}
268
- label="Move down"
269
- variant="ghost"
270
- disabled={disabled || index >= totalSteps - 1}
271
- >
272
- <ArrowDown />
273
- </IconButton>
274
- </Tooltip>
275
- <Tooltip label="Delete step">
276
- <IconButton
277
- onClick={onDelete}
278
- label="Delete"
279
- variant="ghost"
280
- disabled={disabled}
281
- >
282
- <Trash />
283
- </IconButton>
284
- </Tooltip>
285
- </>
286
- )}
287
- <Button variant="ghost" onClick={onToggleExpand} size="S">
288
- {expanded ? 'Collapse' : 'Expand'}
289
- </Button>
290
- </Flex>
291
- </Flex>
292
-
293
- {expanded && (
294
- <Box paddingTop={2}>
295
- <Divider />
296
- <Box paddingTop={3}>
297
- <Flex direction="column" gap={3}>
298
- <Field.Root>
299
- <Field.Label>Step Name</Field.Label>
300
- <TextInput
301
- value={step.name}
302
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
303
- onUpdate({ name: e.target.value })
304
- }
305
- disabled={disabled || isEntrance}
306
- />
307
- </Field.Root>
308
-
309
- {!isEntrance && (
310
- <Field.Root>
311
- <Field.Label>Step Key (unique identifier)</Field.Label>
312
- <TextInput
313
- value={step.stepKey}
314
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
315
- onUpdate({ stepKey: e.target.value.toLowerCase().replace(/\s+/g, '_') })
316
- }
317
- disabled={disabled}
318
- placeholder="e.g., welcome_message"
319
- />
320
- </Field.Root>
321
- )}
322
-
323
- {step.nodeType === 'message' && (
324
- <>
325
- <Field.Root>
326
- <Field.Label>Channel</Field.Label>
327
- <SingleSelect
328
- value={step.messageConfig?.channel || 'telegram'}
329
- onChange={(val: string) => updateMessageConfig({ channel: val as JourneyChannel })}
330
- disabled={disabled}
331
- >
332
- {CHANNELS.map(ch => (
333
- <SingleSelectOption key={ch.value} value={ch.value}>
334
- {ch.label}
335
- </SingleSelectOption>
336
- ))}
337
- </SingleSelect>
338
- </Field.Root>
339
-
340
- <Box>
341
- <Typography variant="pi" fontWeight="bold" style={{ marginBottom: 8, display: 'block' }}>
342
- Message Variants (A/B Testing)
343
- </Typography>
344
- <Flex direction="column" gap={2}>
345
- {(step.messageConfig?.variants || []).map((variant, vIdx) => (
346
- <Flex key={variant.id} gap={2} alignItems="flex-end" background="neutral0" padding={2} hasRadius>
347
- <Box style={{ flex: 1 }}>
348
- <Field.Root>
349
- <Field.Label>Name</Field.Label>
350
- <TextInput
351
- value={variant.name}
352
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
353
- updateVariant(variant.id, { name: e.target.value })
354
- }
355
- disabled={disabled}
356
- />
357
- </Field.Root>
358
- </Box>
359
- <Box style={{ flex: 1 }}>
360
- <Field.Root>
361
- <Field.Label>Template ID</Field.Label>
362
- <TextInput
363
- value={variant.templateId}
364
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
365
- updateVariant(variant.id, { templateId: e.target.value })
366
- }
367
- disabled={disabled}
368
- placeholder="Enter template documentId"
369
- />
370
- </Field.Root>
371
- </Box>
372
- <Box style={{ width: 80 }}>
373
- <Field.Root>
374
- <Field.Label>Weight %</Field.Label>
375
- <TextInput
376
- type="number"
377
- value={String(variant.weight)}
378
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
379
- updateVariant(variant.id, { weight: parseInt(e.target.value, 10) || 0 })
380
- }
381
- disabled={disabled}
382
- />
383
- </Field.Root>
384
- </Box>
385
- <IconButton
386
- onClick={() => deleteVariant(variant.id)}
387
- label="Delete variant"
388
- variant="ghost"
389
- disabled={disabled}
390
- >
391
- <Trash />
392
- </IconButton>
393
- </Flex>
394
- ))}
395
- <Button
396
- startIcon={<Plus />}
397
- onClick={addVariant}
398
- variant="secondary"
399
- size="S"
400
- disabled={disabled}
401
- >
402
- Add Variant
403
- </Button>
404
- </Flex>
405
- </Box>
406
- </>
407
- )}
408
-
409
- {step.nodeType === 'wait' && (
410
- <Flex gap={2}>
411
- <Box style={{ width: 120 }}>
412
- <Field.Root>
413
- <Field.Label>Duration</Field.Label>
414
- <TextInput
415
- type="number"
416
- value={String(step.waitConfig?.durationValue || 30)}
417
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
418
- updateWaitConfig({ durationValue: parseInt(e.target.value, 10) || 0 })
419
- }
420
- disabled={disabled}
421
- />
422
- </Field.Root>
423
- </Box>
424
- <Box style={{ width: 150 }}>
425
- <Field.Root>
426
- <Field.Label>Unit</Field.Label>
427
- <SingleSelect
428
- value={step.waitConfig?.durationUnit || 'minutes'}
429
- onChange={(val: string) => updateWaitConfig({ durationUnit: val as JourneyWaitUnit })}
430
- disabled={disabled}
431
- >
432
- {WAIT_UNITS.map(u => (
433
- <SingleSelectOption key={u.value} value={u.value}>
434
- {u.label}
435
- </SingleSelectOption>
436
- ))}
437
- </SingleSelect>
438
- </Field.Root>
439
- </Box>
440
- </Flex>
441
- )}
442
-
443
- {step.nodeType === 'branch' && (
444
- <>
445
- <Field.Root>
446
- <Field.Label>Segment ID (optional)</Field.Label>
447
- <TextInput
448
- value={step.branchConfig?.segmentId || ''}
449
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
450
- updateBranchConfig({ segmentId: e.target.value })
451
- }
452
- disabled={disabled}
453
- placeholder="Leave empty to use campaign's segment"
454
- />
455
- <Field.Hint>
456
- If empty, uses campaign's segment for evaluation
457
- </Field.Hint>
458
- </Field.Root>
459
-
460
- <Box background="neutral0" padding={3} hasRadius>
461
- <Typography variant="pi" fontWeight="bold" style={{ marginBottom: 12, display: 'block' }}>
462
- Branch Paths
463
- </Typography>
464
- <Flex direction="column" gap={2}>
465
- <Flex gap={2} alignItems="center">
466
- <Badge backgroundColor="success100" textColor="success600">YES</Badge>
467
- <Box style={{ flex: 1 }}>
468
- <SingleSelect
469
- value={getNextStep('yes')}
470
- onChange={(val: string) => onUpdateTransition(step.stepKey, 'yes', val)}
471
- disabled={disabled}
472
- placeholder="Select next step..."
473
- >
474
- <SingleSelectOption value="">-- None --</SingleSelectOption>
475
- {availableTargets.map(s => (
476
- <SingleSelectOption key={s.stepKey} value={s.stepKey}>
477
- {s.name} ({s.nodeType})
478
- </SingleSelectOption>
479
- ))}
480
- </SingleSelect>
481
- </Box>
482
- </Flex>
483
- <Flex gap={2} alignItems="center">
484
- <Badge backgroundColor="danger100" textColor="danger600">NO</Badge>
485
- <Box style={{ flex: 1 }}>
486
- <SingleSelect
487
- value={getNextStep('no')}
488
- onChange={(val: string) => onUpdateTransition(step.stepKey, 'no', val)}
489
- disabled={disabled}
490
- placeholder="Select next step..."
491
- >
492
- <SingleSelectOption value="">-- None --</SingleSelectOption>
493
- {availableTargets.map(s => (
494
- <SingleSelectOption key={s.stepKey} value={s.stepKey}>
495
- {s.name} ({s.nodeType})
496
- </SingleSelectOption>
497
- ))}
498
- </SingleSelect>
499
- </Box>
500
- </Flex>
501
- </Flex>
502
- </Box>
503
- </>
504
- )}
505
-
506
- {step.nodeType !== 'branch' && step.nodeType !== 'exit' && (
507
- <Field.Root>
508
- <Field.Label>Next Step</Field.Label>
509
- <SingleSelect
510
- value={getNextStep('default')}
511
- onChange={(val: string) => onUpdateTransition(step.stepKey, 'default', val)}
512
- disabled={disabled}
513
- placeholder="Select next step..."
514
- >
515
- <SingleSelectOption value="">-- None (end) --</SingleSelectOption>
516
- {availableTargets.map(s => (
517
- <SingleSelectOption key={s.stepKey} value={s.stepKey}>
518
- {s.name} ({s.nodeType})
519
- </SingleSelectOption>
520
- ))}
521
- </SingleSelect>
522
- </Field.Root>
523
- )}
524
- </Flex>
525
- </Box>
526
- </Box>
527
- )}
528
- </Flex>
529
- </CardContent>
530
- </Card>
531
- );
532
- };
533
-
534
- const JourneyConfigField = forwardRef<HTMLDivElement, JourneyConfigFieldProps>(
535
- ({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
536
- const [config, setConfig] = useState<JourneyConfig>(() => parseConfig(value));
537
- const [expandedSteps, setExpandedSteps] = useState<Set<string>>(new Set());
538
- const [showAddStep, setShowAddStep] = useState(false);
539
-
540
- useEffect(() => {
541
- setConfig(parseConfig(value));
542
- }, [value]);
543
-
544
- const update = (newConfig: JourneyConfig) => {
545
- setConfig(newConfig);
546
- onChange({ target: { name, value: serializeConfig(newConfig) } });
547
- };
548
-
549
- const toggleExpand = (stepId: string) => {
550
- setExpandedSteps(prev => {
551
- const next = new Set(prev);
552
- if (next.has(stepId)) {
553
- next.delete(stepId);
554
- } else {
555
- next.add(stepId);
556
- }
557
- return next;
558
- });
559
- };
560
-
561
- const addStep = (nodeType: JourneyNodeType) => {
562
- const newStep: JourneyStep = {
563
- id: generateId(),
564
- stepKey: `${nodeType}_${Date.now()}`,
565
- name: NODE_TYPES.find(n => n.value === nodeType)?.label || nodeType,
566
- nodeType,
567
- ...(nodeType === 'message' && {
568
- messageConfig: { channel: 'telegram', variants: [] },
569
- }),
570
- ...(nodeType === 'wait' && {
571
- waitConfig: { durationValue: 30, durationUnit: 'minutes' },
572
- }),
573
- ...(nodeType === 'branch' && {
574
- branchConfig: { segmentId: '', useStepSegment: false },
575
- }),
576
- };
577
- update({ ...config, steps: [...config.steps, newStep] });
578
- setExpandedSteps(prev => new Set(prev).add(newStep.id));
579
- setShowAddStep(false);
580
- };
581
-
582
- const updateStep = (stepId: string, updates: Partial<JourneyStep>) => {
583
- update({
584
- ...config,
585
- steps: config.steps.map(s => s.id === stepId ? { ...s, ...updates } : s),
586
- });
587
- };
588
-
589
- const deleteStep = (stepId: string) => {
590
- const step = config.steps.find(s => s.id === stepId);
591
- if (!step) return;
592
- update({
593
- ...config,
594
- steps: config.steps.filter(s => s.id !== stepId),
595
- transitions: config.transitions.filter(
596
- t => t.sourceStepKey !== step.stepKey && t.targetStepKey !== step.stepKey
597
- ),
598
- });
599
- };
600
-
601
- const moveStep = (stepId: string, direction: 'up' | 'down') => {
602
- const idx = config.steps.findIndex(s => s.id === stepId);
603
- if (idx <= 0 && direction === 'up') return;
604
- if (idx >= config.steps.length - 1 && direction === 'down') return;
605
-
606
- const newSteps = [...config.steps];
607
- const targetIdx = direction === 'up' ? idx - 1 : idx + 1;
608
- if (targetIdx === 0) return;
609
-
610
- [newSteps[idx], newSteps[targetIdx]] = [newSteps[targetIdx], newSteps[idx]];
611
- update({ ...config, steps: newSteps });
612
- };
613
-
614
- const updateTransition = (sourceStepKey: string, branchType: JourneyBranchType, targetStepKey: string) => {
615
- let transitions = config.transitions.filter(
616
- t => !(t.sourceStepKey === sourceStepKey && t.branchType === branchType)
617
- );
618
- if (targetStepKey) {
619
- transitions.push({
620
- id: generateId(),
621
- sourceStepKey,
622
- targetStepKey,
623
- branchType,
624
- });
625
- }
626
- update({ ...config, transitions });
627
- };
628
-
629
- const updateGlobalConfig = (updates: Partial<JourneyConfig>) => {
630
- update({ ...config, ...updates });
631
- };
632
-
633
- return (
634
- <Field.Root name={name} error={error} required={required} hint={hint} ref={ref}>
635
- <Flex direction="column" gap={4}>
636
- <Field.Label>{intlLabel?.defaultMessage || 'Journey Configuration'}</Field.Label>
637
-
638
- <Card background="neutral100">
639
- <CardContent>
640
- <Flex direction="column" gap={3} padding={3}>
641
- <Typography variant="delta">Journey Settings</Typography>
642
- <Flex gap={4}>
643
- <Field.Root>
644
- <Field.Label>Entry once per user</Field.Label>
645
- <SingleSelect
646
- value={config.entryOncePerUser ? 'yes' : 'no'}
647
- onChange={(val: string) => updateGlobalConfig({ entryOncePerUser: val === 'yes' })}
648
- disabled={disabled}
649
- >
650
- <SingleSelectOption value="yes">Yes</SingleSelectOption>
651
- <SingleSelectOption value="no">No (allow re-entry)</SingleSelectOption>
652
- </SingleSelect>
653
- </Field.Root>
654
- <Field.Root>
655
- <Field.Label>Re-entry delay (days)</Field.Label>
656
- <TextInput
657
- type="number"
658
- value={String(config.reEntryDelayDays || 30)}
659
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
660
- updateGlobalConfig({ reEntryDelayDays: parseInt(e.target.value, 10) || 0 })
661
- }
662
- disabled={disabled}
663
- />
664
- </Field.Root>
665
- </Flex>
666
- </Flex>
667
- </CardContent>
668
- </Card>
669
-
670
- <Box>
671
- <Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: 12 }}>
672
- <Typography variant="delta">Steps ({config.steps.length})</Typography>
673
- <Button
674
- startIcon={<Plus />}
675
- onClick={() => setShowAddStep(!showAddStep)}
676
- variant="secondary"
677
- disabled={disabled}
678
- >
679
- Add Step
680
- </Button>
681
- </Flex>
682
-
683
- {showAddStep && (
684
- <Card background="primary100" style={{ marginBottom: 16, border: '2px dashed #4945ff' }}>
685
- <CardContent>
686
- <Flex direction="column" gap={3} padding={3}>
687
- <Typography variant="omega" fontWeight="bold">Select step type:</Typography>
688
- <Flex gap={2} wrap="wrap">
689
- {NODE_TYPES.map(nt => (
690
- <Box
691
- key={nt.value}
692
- padding={3}
693
- background="neutral0"
694
- hasRadius
695
- style={{ cursor: 'pointer', border: '1px solid #dcdce4', minWidth: 140 }}
696
- onClick={() => addStep(nt.value)}
697
- >
698
- <Flex direction="column" alignItems="center" gap={1}>
699
- {nt.icon}
700
- <Typography variant="omega" fontWeight="bold">{nt.label}</Typography>
701
- <Typography variant="pi" textColor="neutral500" style={{ textAlign: 'center' }}>
702
- {nt.description}
703
- </Typography>
704
- </Flex>
705
- </Box>
706
- ))}
707
- </Flex>
708
- </Flex>
709
- </CardContent>
710
- </Card>
711
- )}
712
-
713
- <Flex direction="column" gap={2}>
714
- {config.steps.map((step, idx) => (
715
- <StepCard
716
- key={step.id}
717
- step={step}
718
- index={idx}
719
- totalSteps={config.steps.length}
720
- disabled={disabled}
721
- expanded={expandedSteps.has(step.id)}
722
- onToggleExpand={() => toggleExpand(step.id)}
723
- onUpdate={(updates) => updateStep(step.id, updates)}
724
- onDelete={() => deleteStep(step.id)}
725
- onMove={(dir) => moveStep(step.id, dir)}
726
- transitions={config.transitions}
727
- allSteps={config.steps}
728
- onUpdateTransition={updateTransition}
729
- />
730
- ))}
731
- </Flex>
732
- </Box>
733
-
734
- {error && <Field.Error>{error}</Field.Error>}
735
- {hint && <Field.Hint>{hint}</Field.Hint>}
736
- </Flex>
737
- </Field.Root>
738
- );
739
- }
740
- );
741
-
742
- JourneyConfigField.displayName = 'JourneyConfigField';
743
-
744
- export default JourneyConfigField;