@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.
- package/admin/src/components/StepFlowBuilder/constants.ts +91 -0
- package/admin/src/components/StepFlowBuilder/edges/LabeledEdge.tsx +77 -0
- package/admin/src/components/StepFlowBuilder/edges/index.ts +8 -0
- package/admin/src/components/StepFlowBuilder/index.tsx +320 -0
- package/admin/src/components/StepFlowBuilder/nodes/BranchNode.tsx +90 -0
- package/admin/src/components/StepFlowBuilder/nodes/EntryNode.tsx +47 -0
- package/admin/src/components/StepFlowBuilder/nodes/ExitNode.tsx +47 -0
- package/admin/src/components/StepFlowBuilder/nodes/MessageNode.tsx +78 -0
- package/admin/src/components/StepFlowBuilder/nodes/WaitNode.tsx +71 -0
- package/admin/src/components/StepFlowBuilder/nodes/index.ts +16 -0
- package/admin/src/components/StepFlowBuilder/panels/BranchConfig.tsx +112 -0
- package/admin/src/components/StepFlowBuilder/panels/MessageConfig.tsx +188 -0
- package/admin/src/components/StepFlowBuilder/panels/NodeEditPanel.tsx +158 -0
- package/admin/src/components/StepFlowBuilder/panels/WaitConfig.tsx +87 -0
- package/admin/src/components/StepFlowBuilder/panels/index.ts +4 -0
- package/admin/src/components/StepFlowBuilder/toolbar/FlowToolbar.tsx +86 -0
- package/admin/src/components/StepFlowBuilder/toolbar/index.ts +1 -0
- package/admin/src/components/StepFlowBuilder/types.ts +77 -0
- package/admin/src/components/StepFlowBuilder/utils.ts +217 -0
- package/admin/src/index.ts +8 -8
- package/dist/_chunks/index-BK8649hk.mjs +1405 -0
- package/dist/_chunks/{index-CWnuAWMG.mjs → index-BeiHTAlq.mjs} +91 -112
- package/dist/_chunks/{index-Bw1mkNpv.js → index-T7-DTUJN.js} +262 -227
- package/dist/_chunks/index-aSjgyfVX.js +1408 -0
- package/dist/admin/index.js +9 -9
- package/dist/admin/index.mjs +10 -10
- package/dist/server/index.js +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +3 -1
- package/server/src/register.ts +1 -1
- package/admin/src/components/JourneyConfigField/index.tsx +0 -744
- package/dist/_chunks/index-BhNY5vYI.js +0 -591
- package/dist/_chunks/index-D0XEcc24.mjs +0 -591
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import { Handle, Position, type NodeProps } from 'reactflow';
|
|
3
|
+
import { Flex, Typography, Badge } from '@strapi/design-system';
|
|
4
|
+
import { Message } from '@strapi/icons';
|
|
5
|
+
import type { FlowNodeData } from '../types';
|
|
6
|
+
import { NODE_COLORS, NODE_DIMENSIONS, CHANNEL_OPTIONS } from '../constants';
|
|
7
|
+
|
|
8
|
+
const MessageNode: React.FC<NodeProps<FlowNodeData>> = ({ data, selected }) => {
|
|
9
|
+
const colors = NODE_COLORS.message;
|
|
10
|
+
const channel = data.config.channel || 'telegram';
|
|
11
|
+
const variantCount = data.config.variants?.length || 0;
|
|
12
|
+
const channelLabel = CHANNEL_OPTIONS.find((c) => c.value === channel)?.label || channel;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<Handle
|
|
17
|
+
type="target"
|
|
18
|
+
position={Position.Top}
|
|
19
|
+
style={{
|
|
20
|
+
background: colors.border,
|
|
21
|
+
width: 10,
|
|
22
|
+
height: 10,
|
|
23
|
+
border: '2px solid white',
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
<Flex
|
|
27
|
+
direction="column"
|
|
28
|
+
alignItems="center"
|
|
29
|
+
justifyContent="center"
|
|
30
|
+
gap={1}
|
|
31
|
+
padding={3}
|
|
32
|
+
background="neutral0"
|
|
33
|
+
hasRadius
|
|
34
|
+
style={{
|
|
35
|
+
width: NODE_DIMENSIONS.width,
|
|
36
|
+
height: NODE_DIMENSIONS.height,
|
|
37
|
+
border: `2px solid ${selected ? colors.border : colors.background}`,
|
|
38
|
+
backgroundColor: colors.background,
|
|
39
|
+
boxShadow: selected ? `0 0 0 2px ${colors.border}` : 'none',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Flex alignItems="center" gap={2}>
|
|
43
|
+
<Message style={{ color: colors.text, width: 18, height: 18 }} />
|
|
44
|
+
<Typography
|
|
45
|
+
variant="omega"
|
|
46
|
+
fontWeight="semiBold"
|
|
47
|
+
style={{ color: colors.text }}
|
|
48
|
+
ellipsis
|
|
49
|
+
>
|
|
50
|
+
{data.name}
|
|
51
|
+
</Typography>
|
|
52
|
+
</Flex>
|
|
53
|
+
<Flex gap={1}>
|
|
54
|
+
<Badge size="S" backgroundColor="neutral100" textColor="neutral700">
|
|
55
|
+
{channelLabel}
|
|
56
|
+
</Badge>
|
|
57
|
+
{variantCount > 0 && (
|
|
58
|
+
<Badge size="S" backgroundColor="primary100" textColor="primary700">
|
|
59
|
+
{variantCount} variant{variantCount !== 1 ? 's' : ''}
|
|
60
|
+
</Badge>
|
|
61
|
+
)}
|
|
62
|
+
</Flex>
|
|
63
|
+
</Flex>
|
|
64
|
+
<Handle
|
|
65
|
+
type="source"
|
|
66
|
+
position={Position.Bottom}
|
|
67
|
+
style={{
|
|
68
|
+
background: colors.border,
|
|
69
|
+
width: 10,
|
|
70
|
+
height: 10,
|
|
71
|
+
border: '2px solid white',
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default memo(MessageNode);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import { Handle, Position, type NodeProps } from 'reactflow';
|
|
3
|
+
import { Flex, Typography, Badge } from '@strapi/design-system';
|
|
4
|
+
import { Clock } from '@strapi/icons';
|
|
5
|
+
import type { FlowNodeData } from '../types';
|
|
6
|
+
import { NODE_COLORS, NODE_DIMENSIONS, DURATION_UNIT_OPTIONS } from '../constants';
|
|
7
|
+
|
|
8
|
+
const WaitNode: React.FC<NodeProps<FlowNodeData>> = ({ data, selected }) => {
|
|
9
|
+
const colors = NODE_COLORS.wait;
|
|
10
|
+
const duration = data.config.duration || 1;
|
|
11
|
+
const unit = data.config.durationUnit || 'hours';
|
|
12
|
+
const unitLabel = DURATION_UNIT_OPTIONS.find((u) => u.value === unit)?.label.toLowerCase() || unit;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<Handle
|
|
17
|
+
type="target"
|
|
18
|
+
position={Position.Top}
|
|
19
|
+
style={{
|
|
20
|
+
background: colors.border,
|
|
21
|
+
width: 10,
|
|
22
|
+
height: 10,
|
|
23
|
+
border: '2px solid white',
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
<Flex
|
|
27
|
+
direction="column"
|
|
28
|
+
alignItems="center"
|
|
29
|
+
justifyContent="center"
|
|
30
|
+
gap={1}
|
|
31
|
+
padding={3}
|
|
32
|
+
background="neutral0"
|
|
33
|
+
hasRadius
|
|
34
|
+
style={{
|
|
35
|
+
width: NODE_DIMENSIONS.width,
|
|
36
|
+
height: NODE_DIMENSIONS.height,
|
|
37
|
+
border: `2px solid ${selected ? colors.border : colors.background}`,
|
|
38
|
+
backgroundColor: colors.background,
|
|
39
|
+
boxShadow: selected ? `0 0 0 2px ${colors.border}` : 'none',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Flex alignItems="center" gap={2}>
|
|
43
|
+
<Clock style={{ color: colors.text, width: 18, height: 18 }} />
|
|
44
|
+
<Typography
|
|
45
|
+
variant="omega"
|
|
46
|
+
fontWeight="semiBold"
|
|
47
|
+
style={{ color: colors.text }}
|
|
48
|
+
ellipsis
|
|
49
|
+
>
|
|
50
|
+
{data.name}
|
|
51
|
+
</Typography>
|
|
52
|
+
</Flex>
|
|
53
|
+
<Badge size="S" backgroundColor="warning100" textColor="warning700">
|
|
54
|
+
{duration} {unitLabel}
|
|
55
|
+
</Badge>
|
|
56
|
+
</Flex>
|
|
57
|
+
<Handle
|
|
58
|
+
type="source"
|
|
59
|
+
position={Position.Bottom}
|
|
60
|
+
style={{
|
|
61
|
+
background: colors.border,
|
|
62
|
+
width: 10,
|
|
63
|
+
height: 10,
|
|
64
|
+
border: '2px solid white',
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default memo(WaitNode);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { NodeTypes } from 'reactflow';
|
|
2
|
+
import EntryNode from './EntryNode';
|
|
3
|
+
import MessageNode from './MessageNode';
|
|
4
|
+
import WaitNode from './WaitNode';
|
|
5
|
+
import BranchNode from './BranchNode';
|
|
6
|
+
import ExitNode from './ExitNode';
|
|
7
|
+
|
|
8
|
+
export { EntryNode, MessageNode, WaitNode, BranchNode, ExitNode };
|
|
9
|
+
|
|
10
|
+
export const nodeTypes: NodeTypes = {
|
|
11
|
+
entry: EntryNode,
|
|
12
|
+
message: MessageNode,
|
|
13
|
+
wait: WaitNode,
|
|
14
|
+
branch: BranchNode,
|
|
15
|
+
exit: ExitNode,
|
|
16
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Flex,
|
|
5
|
+
Typography,
|
|
6
|
+
SingleSelect,
|
|
7
|
+
SingleSelectOption,
|
|
8
|
+
Loader,
|
|
9
|
+
} from '@strapi/design-system';
|
|
10
|
+
import { useFetchClient } from '@strapi/strapi/admin';
|
|
11
|
+
import type { FlowNodeData, SegmentOption } from '../types';
|
|
12
|
+
|
|
13
|
+
interface BranchConfigProps {
|
|
14
|
+
data: FlowNodeData;
|
|
15
|
+
onUpdate: (data: Partial<FlowNodeData>) => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BranchConfig: React.FC<BranchConfigProps> = ({ data, onUpdate, disabled }) => {
|
|
20
|
+
const { get } = useFetchClient();
|
|
21
|
+
const [segments, setSegments] = useState<SegmentOption[]>([]);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
|
|
24
|
+
const selectedSegmentId = data.config.branchSegment?.id?.toString() || '';
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const fetchSegments = async () => {
|
|
28
|
+
try {
|
|
29
|
+
const response = await get('/content-manager/collection-types/api::crm-segment.crm-segment', {
|
|
30
|
+
params: {
|
|
31
|
+
pageSize: 100,
|
|
32
|
+
sort: 'name:asc',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const results = response.data?.results || [];
|
|
36
|
+
setSegments(
|
|
37
|
+
results.map((s: { id: number; name: string }) => ({
|
|
38
|
+
id: s.id,
|
|
39
|
+
name: s.name,
|
|
40
|
+
}))
|
|
41
|
+
);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Failed to fetch segments:', error);
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
fetchSegments();
|
|
49
|
+
}, [get]);
|
|
50
|
+
|
|
51
|
+
const handleSegmentChange = (value: string) => {
|
|
52
|
+
const segment = segments.find((s) => s.id.toString() === value);
|
|
53
|
+
onUpdate({
|
|
54
|
+
config: {
|
|
55
|
+
...data.config,
|
|
56
|
+
branchSegment: segment ? { id: segment.id, name: segment.name } : null,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (loading) {
|
|
62
|
+
return (
|
|
63
|
+
<Flex justifyContent="center" padding={4}>
|
|
64
|
+
<Loader small>Loading segments...</Loader>
|
|
65
|
+
</Flex>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Flex direction="column" gap={4}>
|
|
71
|
+
<Box>
|
|
72
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
73
|
+
Condition Segment
|
|
74
|
+
</Typography>
|
|
75
|
+
<Box paddingTop={1}>
|
|
76
|
+
<SingleSelect
|
|
77
|
+
value={selectedSegmentId}
|
|
78
|
+
onChange={handleSegmentChange}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
placeholder="Select a segment..."
|
|
81
|
+
>
|
|
82
|
+
{segments.map((segment) => (
|
|
83
|
+
<SingleSelectOption key={segment.id} value={segment.id.toString()}>
|
|
84
|
+
{segment.name}
|
|
85
|
+
</SingleSelectOption>
|
|
86
|
+
))}
|
|
87
|
+
</SingleSelect>
|
|
88
|
+
</Box>
|
|
89
|
+
</Box>
|
|
90
|
+
|
|
91
|
+
<Box
|
|
92
|
+
padding={3}
|
|
93
|
+
background="secondary100"
|
|
94
|
+
hasRadius
|
|
95
|
+
>
|
|
96
|
+
<Flex direction="column" gap={2}>
|
|
97
|
+
<Typography variant="pi" textColor="secondary700" fontWeight="semiBold">
|
|
98
|
+
How it works:
|
|
99
|
+
</Typography>
|
|
100
|
+
<Typography variant="pi" textColor="secondary600">
|
|
101
|
+
• Users matching the segment → <strong>Yes</strong> path
|
|
102
|
+
</Typography>
|
|
103
|
+
<Typography variant="pi" textColor="secondary600">
|
|
104
|
+
• Users not matching → <strong>No</strong> path
|
|
105
|
+
</Typography>
|
|
106
|
+
</Flex>
|
|
107
|
+
</Box>
|
|
108
|
+
</Flex>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default BranchConfig;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Flex,
|
|
5
|
+
Typography,
|
|
6
|
+
TextInput,
|
|
7
|
+
SingleSelect,
|
|
8
|
+
SingleSelectOption,
|
|
9
|
+
Button,
|
|
10
|
+
IconButton,
|
|
11
|
+
NumberInput,
|
|
12
|
+
} from '@strapi/design-system';
|
|
13
|
+
import { Plus, Trash } from '@strapi/icons';
|
|
14
|
+
import type { FlowNodeData, CampaignVariant, ChannelType } from '../types';
|
|
15
|
+
import { CHANNEL_OPTIONS } from '../constants';
|
|
16
|
+
|
|
17
|
+
interface MessageConfigProps {
|
|
18
|
+
data: FlowNodeData;
|
|
19
|
+
onUpdate: (data: Partial<FlowNodeData>) => void;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MessageConfig: React.FC<MessageConfigProps> = ({ data, onUpdate, disabled }) => {
|
|
24
|
+
const [variants, setVariants] = useState<CampaignVariant[]>(data.config.variants || []);
|
|
25
|
+
const channel = data.config.channel || 'telegram';
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setVariants(data.config.variants || []);
|
|
29
|
+
}, [data.config.variants]);
|
|
30
|
+
|
|
31
|
+
const handleChannelChange = (value: string) => {
|
|
32
|
+
onUpdate({
|
|
33
|
+
config: {
|
|
34
|
+
...data.config,
|
|
35
|
+
channel: value as ChannelType,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleAddVariant = () => {
|
|
41
|
+
const newVariant: CampaignVariant = {
|
|
42
|
+
name: `Variant ${variants.length + 1}`,
|
|
43
|
+
template: null,
|
|
44
|
+
weight: 100,
|
|
45
|
+
};
|
|
46
|
+
const updated = [...variants, newVariant];
|
|
47
|
+
setVariants(updated);
|
|
48
|
+
onUpdate({
|
|
49
|
+
config: {
|
|
50
|
+
...data.config,
|
|
51
|
+
variants: updated,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleRemoveVariant = (index: number) => {
|
|
57
|
+
const updated = variants.filter((_, i) => i !== index);
|
|
58
|
+
setVariants(updated);
|
|
59
|
+
onUpdate({
|
|
60
|
+
config: {
|
|
61
|
+
...data.config,
|
|
62
|
+
variants: updated,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleVariantChange = (index: number, field: keyof CampaignVariant, value: unknown) => {
|
|
68
|
+
const updated = variants.map((v, i) =>
|
|
69
|
+
i === index ? { ...v, [field]: value } : v
|
|
70
|
+
);
|
|
71
|
+
setVariants(updated);
|
|
72
|
+
onUpdate({
|
|
73
|
+
config: {
|
|
74
|
+
...data.config,
|
|
75
|
+
variants: updated,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Flex direction="column" gap={4}>
|
|
82
|
+
<Box>
|
|
83
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
84
|
+
Channel
|
|
85
|
+
</Typography>
|
|
86
|
+
<Box paddingTop={1}>
|
|
87
|
+
<SingleSelect
|
|
88
|
+
value={channel}
|
|
89
|
+
onChange={handleChannelChange}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
>
|
|
92
|
+
{CHANNEL_OPTIONS.map((opt) => (
|
|
93
|
+
<SingleSelectOption key={opt.value} value={opt.value}>
|
|
94
|
+
{opt.label}
|
|
95
|
+
</SingleSelectOption>
|
|
96
|
+
))}
|
|
97
|
+
</SingleSelect>
|
|
98
|
+
</Box>
|
|
99
|
+
</Box>
|
|
100
|
+
|
|
101
|
+
<Box>
|
|
102
|
+
<Flex justifyContent="space-between" alignItems="center">
|
|
103
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
104
|
+
Variants (A/B Testing)
|
|
105
|
+
</Typography>
|
|
106
|
+
<Button
|
|
107
|
+
variant="tertiary"
|
|
108
|
+
size="S"
|
|
109
|
+
startIcon={<Plus />}
|
|
110
|
+
onClick={handleAddVariant}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
>
|
|
113
|
+
Add Variant
|
|
114
|
+
</Button>
|
|
115
|
+
</Flex>
|
|
116
|
+
|
|
117
|
+
<Flex direction="column" gap={2} paddingTop={2}>
|
|
118
|
+
{variants.length === 0 ? (
|
|
119
|
+
<Box
|
|
120
|
+
padding={4}
|
|
121
|
+
background="neutral100"
|
|
122
|
+
hasRadius
|
|
123
|
+
style={{ textAlign: 'center' }}
|
|
124
|
+
>
|
|
125
|
+
<Typography variant="pi" textColor="neutral500">
|
|
126
|
+
No variants yet. Add a variant to enable A/B testing.
|
|
127
|
+
</Typography>
|
|
128
|
+
</Box>
|
|
129
|
+
) : (
|
|
130
|
+
variants.map((variant, index) => (
|
|
131
|
+
<Box
|
|
132
|
+
key={index}
|
|
133
|
+
padding={3}
|
|
134
|
+
background="neutral100"
|
|
135
|
+
hasRadius
|
|
136
|
+
style={{ border: '1px solid #dcdce4' }}
|
|
137
|
+
>
|
|
138
|
+
<Flex gap={2} alignItems="flex-end">
|
|
139
|
+
<Box style={{ flex: 1 }}>
|
|
140
|
+
<Typography variant="pi" textColor="neutral600">
|
|
141
|
+
Name
|
|
142
|
+
</Typography>
|
|
143
|
+
<TextInput
|
|
144
|
+
value={variant.name}
|
|
145
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
146
|
+
handleVariantChange(index, 'name', e.target.value)
|
|
147
|
+
}
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
size="S"
|
|
150
|
+
/>
|
|
151
|
+
</Box>
|
|
152
|
+
<Box style={{ width: 80 }}>
|
|
153
|
+
<Typography variant="pi" textColor="neutral600">
|
|
154
|
+
Weight
|
|
155
|
+
</Typography>
|
|
156
|
+
<NumberInput
|
|
157
|
+
value={variant.weight}
|
|
158
|
+
onValueChange={(value: number | undefined) =>
|
|
159
|
+
handleVariantChange(index, 'weight', value || 0)
|
|
160
|
+
}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
size="S"
|
|
163
|
+
/>
|
|
164
|
+
</Box>
|
|
165
|
+
<IconButton
|
|
166
|
+
onClick={() => handleRemoveVariant(index)}
|
|
167
|
+
label="Remove variant"
|
|
168
|
+
variant="ghost"
|
|
169
|
+
disabled={disabled}
|
|
170
|
+
>
|
|
171
|
+
<Trash />
|
|
172
|
+
</IconButton>
|
|
173
|
+
</Flex>
|
|
174
|
+
<Box paddingTop={2}>
|
|
175
|
+
<Typography variant="pi" textColor="neutral500">
|
|
176
|
+
Template: {variant.template?.name || 'Not selected'}
|
|
177
|
+
</Typography>
|
|
178
|
+
</Box>
|
|
179
|
+
</Box>
|
|
180
|
+
))
|
|
181
|
+
)}
|
|
182
|
+
</Flex>
|
|
183
|
+
</Box>
|
|
184
|
+
</Flex>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default MessageConfig;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Flex,
|
|
5
|
+
Typography,
|
|
6
|
+
TextInput,
|
|
7
|
+
Button,
|
|
8
|
+
Divider,
|
|
9
|
+
IconButton,
|
|
10
|
+
} from '@strapi/design-system';
|
|
11
|
+
import { Cross } from '@strapi/icons';
|
|
12
|
+
import type { NodeEditPanelProps, FlowNodeData } from '../types';
|
|
13
|
+
import { NODE_COLORS, STEP_TYPE_LABELS } from '../constants';
|
|
14
|
+
import MessageConfig from './MessageConfig';
|
|
15
|
+
import WaitConfig from './WaitConfig';
|
|
16
|
+
import BranchConfig from './BranchConfig';
|
|
17
|
+
|
|
18
|
+
const NodeEditPanel: React.FC<NodeEditPanelProps> = ({
|
|
19
|
+
node,
|
|
20
|
+
onClose,
|
|
21
|
+
onUpdate,
|
|
22
|
+
disabled,
|
|
23
|
+
}) => {
|
|
24
|
+
if (!node) return null;
|
|
25
|
+
|
|
26
|
+
const { stepType, name } = node.data;
|
|
27
|
+
const colors = NODE_COLORS[stepType];
|
|
28
|
+
const typeLabel = STEP_TYPE_LABELS[stepType];
|
|
29
|
+
|
|
30
|
+
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
31
|
+
onUpdate(node.id, { name: e.target.value });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleConfigUpdate = (updates: Partial<FlowNodeData>) => {
|
|
35
|
+
onUpdate(node.id, updates);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const renderConfig = () => {
|
|
39
|
+
switch (stepType) {
|
|
40
|
+
case 'message':
|
|
41
|
+
return (
|
|
42
|
+
<MessageConfig
|
|
43
|
+
data={node.data}
|
|
44
|
+
onUpdate={handleConfigUpdate}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
case 'wait':
|
|
49
|
+
return (
|
|
50
|
+
<WaitConfig
|
|
51
|
+
data={node.data}
|
|
52
|
+
onUpdate={handleConfigUpdate}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
case 'branch':
|
|
57
|
+
return (
|
|
58
|
+
<BranchConfig
|
|
59
|
+
data={node.data}
|
|
60
|
+
onUpdate={handleConfigUpdate}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
case 'entry':
|
|
65
|
+
case 'exit':
|
|
66
|
+
default:
|
|
67
|
+
return (
|
|
68
|
+
<Box
|
|
69
|
+
padding={3}
|
|
70
|
+
background="neutral100"
|
|
71
|
+
hasRadius
|
|
72
|
+
style={{ textAlign: 'center' }}
|
|
73
|
+
>
|
|
74
|
+
<Typography variant="pi" textColor="neutral500">
|
|
75
|
+
No additional configuration needed for this step type.
|
|
76
|
+
</Typography>
|
|
77
|
+
</Box>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Box
|
|
84
|
+
background="neutral0"
|
|
85
|
+
shadow="filterShadow"
|
|
86
|
+
hasRadius
|
|
87
|
+
style={{
|
|
88
|
+
position: 'absolute',
|
|
89
|
+
top: 0,
|
|
90
|
+
right: 0,
|
|
91
|
+
width: 350,
|
|
92
|
+
height: '100%',
|
|
93
|
+
zIndex: 10,
|
|
94
|
+
borderLeft: '1px solid #dcdce4',
|
|
95
|
+
overflow: 'auto',
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<Box
|
|
99
|
+
padding={4}
|
|
100
|
+
style={{
|
|
101
|
+
borderBottom: `3px solid ${colors.border}`,
|
|
102
|
+
backgroundColor: colors.background,
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<Flex justifyContent="space-between" alignItems="center">
|
|
106
|
+
<Flex alignItems="center" gap={2}>
|
|
107
|
+
<Typography variant="beta" style={{ color: colors.text }}>
|
|
108
|
+
{typeLabel}
|
|
109
|
+
</Typography>
|
|
110
|
+
</Flex>
|
|
111
|
+
<IconButton onClick={onClose} label="Close panel" variant="ghost">
|
|
112
|
+
<Cross />
|
|
113
|
+
</IconButton>
|
|
114
|
+
</Flex>
|
|
115
|
+
</Box>
|
|
116
|
+
|
|
117
|
+
<Box padding={4}>
|
|
118
|
+
<Flex direction="column" gap={4}>
|
|
119
|
+
{stepType !== 'entry' && (
|
|
120
|
+
<Box>
|
|
121
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
122
|
+
Step Name
|
|
123
|
+
</Typography>
|
|
124
|
+
<Box paddingTop={1}>
|
|
125
|
+
<TextInput
|
|
126
|
+
value={name}
|
|
127
|
+
onChange={handleNameChange}
|
|
128
|
+
disabled={disabled || stepType === 'entry'}
|
|
129
|
+
placeholder="Enter step name..."
|
|
130
|
+
/>
|
|
131
|
+
</Box>
|
|
132
|
+
</Box>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{stepType !== 'entry' && stepType !== 'exit' && (
|
|
136
|
+
<>
|
|
137
|
+
<Divider />
|
|
138
|
+
{renderConfig()}
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{stepType !== 'entry' && (
|
|
143
|
+
<>
|
|
144
|
+
<Divider />
|
|
145
|
+
<Box>
|
|
146
|
+
<Typography variant="pi" textColor="neutral500">
|
|
147
|
+
Step ID: {node.id}
|
|
148
|
+
</Typography>
|
|
149
|
+
</Box>
|
|
150
|
+
</>
|
|
151
|
+
)}
|
|
152
|
+
</Flex>
|
|
153
|
+
</Box>
|
|
154
|
+
</Box>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default NodeEditPanel;
|