@inspirer-dev/crm-dashboard 1.0.21 → 1.0.23

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-XoiSAQhK.js} +91 -112
  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
@@ -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;