@inspirer-dev/crm-dashboard 1.0.10 → 1.0.12
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/ButtonsBuilder/index.tsx +259 -0
- package/admin/src/components/CancelConditionsField/constants.ts +104 -0
- package/admin/src/components/CancelConditionsField/index.tsx +381 -0
- package/admin/src/components/TriggerConfigField/index.tsx +333 -0
- package/admin/src/index.ts +76 -2
- package/dist/_chunks/{index-CNvqpnNt.mjs → index-BSDO36Pf.mjs} +1 -468
- package/dist/_chunks/index-Bnjm_sYk.mjs +339 -0
- package/dist/_chunks/index-Cf8DZYT6.js +341 -0
- package/dist/_chunks/index-CiwOzO0B.js +377 -0
- package/dist/_chunks/index-DFqEb9sm.mjs +253 -0
- package/dist/_chunks/index-DRJ5o0cz.js +253 -0
- package/dist/_chunks/index-XE6toVNT.mjs +170 -0
- package/dist/_chunks/index-d16UivTb.js +170 -0
- package/dist/_chunks/utils-C6_ndVAZ.mjs +491 -0
- package/dist/_chunks/utils-CmonL0io.js +490 -0
- package/dist/admin/index.js +73 -2
- package/dist/admin/index.mjs +74 -3
- package/dist/server/index.js +15 -0
- package/dist/server/index.mjs +15 -0
- package/package.json +1 -1
- package/server/src/register.ts +18 -0
- package/dist/_chunks/index-DIwnSzER.js +0 -844
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Field,
|
|
6
|
+
Flex,
|
|
7
|
+
IconButton,
|
|
8
|
+
SingleSelect,
|
|
9
|
+
SingleSelectOption,
|
|
10
|
+
TextInput,
|
|
11
|
+
Typography,
|
|
12
|
+
Tooltip,
|
|
13
|
+
Switch,
|
|
14
|
+
Card,
|
|
15
|
+
CardContent,
|
|
16
|
+
} from '@strapi/design-system';
|
|
17
|
+
import { Plus, Trash } from '@strapi/icons';
|
|
18
|
+
import { useTheme } from 'styled-components';
|
|
19
|
+
|
|
20
|
+
import type { RuleOperator } from '../RulesBuilder/types';
|
|
21
|
+
import { CANCEL_METRICS, CANCEL_TIME_UNITS, CancelMetricDefinition } from './constants';
|
|
22
|
+
|
|
23
|
+
interface CancelRule {
|
|
24
|
+
id: string;
|
|
25
|
+
field: string;
|
|
26
|
+
operator: RuleOperator;
|
|
27
|
+
value: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CancelConfig {
|
|
31
|
+
logic: '$or' | '$and';
|
|
32
|
+
rules: CancelRule[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface CancelConditionsFieldProps {
|
|
36
|
+
name: string;
|
|
37
|
+
value?: string | null;
|
|
38
|
+
onChange: (event: { target: { name: string; value: string } }) => void;
|
|
39
|
+
intlLabel: {
|
|
40
|
+
id: string;
|
|
41
|
+
defaultMessage: string;
|
|
42
|
+
};
|
|
43
|
+
attribute?: unknown;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
error?: string;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
hint?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const generateId = () => Math.random().toString(36).substring(2, 11);
|
|
51
|
+
|
|
52
|
+
const DEFAULT_CONFIG: CancelConfig = {
|
|
53
|
+
logic: '$or',
|
|
54
|
+
rules: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const parseConfig = (value: string | null | undefined): CancelConfig => {
|
|
58
|
+
if (!value) return DEFAULT_CONFIG;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(value);
|
|
61
|
+
if (parsed.logic && Array.isArray(parsed.rules)) {
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
return DEFAULT_CONFIG;
|
|
65
|
+
} catch {
|
|
66
|
+
return DEFAULT_CONFIG;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const serializeConfig = (config: CancelConfig): string => {
|
|
71
|
+
return JSON.stringify(config);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getMetric = (key: string): CancelMetricDefinition | undefined => {
|
|
75
|
+
return CANCEL_METRICS.find((m) => m.key === key);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const OPERATORS: { value: RuleOperator; label: string }[] = [
|
|
79
|
+
{ value: '$eq', label: '=' },
|
|
80
|
+
{ value: '$ne', label: '≠' },
|
|
81
|
+
{ value: '$gt', label: '>' },
|
|
82
|
+
{ value: '$lt', label: '<' },
|
|
83
|
+
{ value: '$gte', label: '≥' },
|
|
84
|
+
{ value: '$lte', label: '≤' },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
interface ValueInputProps {
|
|
88
|
+
metric: CancelMetricDefinition;
|
|
89
|
+
value: unknown;
|
|
90
|
+
operator: RuleOperator;
|
|
91
|
+
onChange: (value: unknown) => void;
|
|
92
|
+
disabled?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ValueInput: React.FC<ValueInputProps> = ({ metric, value, operator, onChange, disabled }) => {
|
|
96
|
+
if (metric.valueType === 'boolean') {
|
|
97
|
+
const boolVal = value === true || value === 'true';
|
|
98
|
+
return (
|
|
99
|
+
<Flex gap={2} alignItems="center">
|
|
100
|
+
<Switch
|
|
101
|
+
label=""
|
|
102
|
+
checked={boolVal}
|
|
103
|
+
onChange={() => onChange(!boolVal)}
|
|
104
|
+
disabled={disabled}
|
|
105
|
+
/>
|
|
106
|
+
<Typography variant="omega" textColor="neutral600">
|
|
107
|
+
{boolVal ? 'true' : 'false'}
|
|
108
|
+
</Typography>
|
|
109
|
+
</Flex>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (metric.valueType === 'number') {
|
|
114
|
+
return (
|
|
115
|
+
<TextInput
|
|
116
|
+
type="number"
|
|
117
|
+
value={String(value ?? 0)}
|
|
118
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(parseInt(e.target.value, 10) || 0)}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
aria-label="Value"
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (metric.valueType === 'time_ago') {
|
|
126
|
+
const timeValue = (value as Record<string, number>) || { minutes_ago: 10 };
|
|
127
|
+
const unit = Object.keys(timeValue)[0] || 'minutes_ago';
|
|
128
|
+
const numValue = timeValue[unit] || 10;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Flex gap={2}>
|
|
132
|
+
<Box style={{ width: '80px' }}>
|
|
133
|
+
<TextInput
|
|
134
|
+
type="number"
|
|
135
|
+
value={String(numValue)}
|
|
136
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
137
|
+
onChange({ [unit]: parseInt(e.target.value, 10) || 0 })
|
|
138
|
+
}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
aria-label="Time value"
|
|
141
|
+
/>
|
|
142
|
+
</Box>
|
|
143
|
+
<Box style={{ width: '120px' }}>
|
|
144
|
+
<SingleSelect
|
|
145
|
+
value={unit}
|
|
146
|
+
onChange={(newUnit: string) => onChange({ [newUnit]: numValue })}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
>
|
|
149
|
+
{CANCEL_TIME_UNITS.map((u) => (
|
|
150
|
+
<SingleSelectOption key={u.value} value={u.value}>
|
|
151
|
+
{u.label}
|
|
152
|
+
</SingleSelectOption>
|
|
153
|
+
))}
|
|
154
|
+
</SingleSelect>
|
|
155
|
+
</Box>
|
|
156
|
+
</Flex>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
interface RuleRowProps {
|
|
164
|
+
rule: CancelRule;
|
|
165
|
+
onUpdate: (rule: CancelRule) => void;
|
|
166
|
+
onDelete: () => void;
|
|
167
|
+
disabled?: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const RuleRow: React.FC<RuleRowProps> = ({ rule, onUpdate, onDelete, disabled }) => {
|
|
171
|
+
const theme = useTheme();
|
|
172
|
+
const colors = (theme as Record<string, unknown>)?.colors as Record<string, string> | undefined;
|
|
173
|
+
const metric = getMetric(rule.field);
|
|
174
|
+
const availableOperators = metric?.operators || ['$eq'];
|
|
175
|
+
|
|
176
|
+
const handleFieldChange = (fieldKey: string) => {
|
|
177
|
+
const newMetric = getMetric(fieldKey);
|
|
178
|
+
let newValue: unknown = true;
|
|
179
|
+
if (newMetric?.valueType === 'number') {
|
|
180
|
+
newValue = 0;
|
|
181
|
+
} else if (newMetric?.valueType === 'time_ago') {
|
|
182
|
+
newValue = { minutes_ago: 10 };
|
|
183
|
+
}
|
|
184
|
+
const newOp = newMetric?.operators[0] || '$eq';
|
|
185
|
+
onUpdate({ ...rule, field: fieldKey, operator: newOp, value: newValue });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<Flex
|
|
190
|
+
gap={2}
|
|
191
|
+
alignItems="center"
|
|
192
|
+
padding={2}
|
|
193
|
+
background="neutral0"
|
|
194
|
+
hasRadius
|
|
195
|
+
style={{ border: `1px solid ${colors?.neutral200 || '#dcdce4'}` }}
|
|
196
|
+
>
|
|
197
|
+
<Box style={{ flex: '1 1 200px', minWidth: '180px' }}>
|
|
198
|
+
<SingleSelect
|
|
199
|
+
value={rule.field}
|
|
200
|
+
onChange={(val: string) => handleFieldChange(val)}
|
|
201
|
+
disabled={disabled}
|
|
202
|
+
size="S"
|
|
203
|
+
>
|
|
204
|
+
{CANCEL_METRICS.map((m) => (
|
|
205
|
+
<SingleSelectOption key={m.key} value={m.key}>
|
|
206
|
+
{m.label}
|
|
207
|
+
</SingleSelectOption>
|
|
208
|
+
))}
|
|
209
|
+
</SingleSelect>
|
|
210
|
+
</Box>
|
|
211
|
+
|
|
212
|
+
<Box style={{ width: '80px' }}>
|
|
213
|
+
<SingleSelect
|
|
214
|
+
value={rule.operator}
|
|
215
|
+
onChange={(val: string) => onUpdate({ ...rule, operator: val as RuleOperator })}
|
|
216
|
+
disabled={disabled}
|
|
217
|
+
size="S"
|
|
218
|
+
>
|
|
219
|
+
{OPERATORS.filter((op) => availableOperators.includes(op.value)).map((op) => (
|
|
220
|
+
<SingleSelectOption key={op.value} value={op.value}>
|
|
221
|
+
{op.label}
|
|
222
|
+
</SingleSelectOption>
|
|
223
|
+
))}
|
|
224
|
+
</SingleSelect>
|
|
225
|
+
</Box>
|
|
226
|
+
|
|
227
|
+
<Box style={{ flex: '1 1 150px', minWidth: '120px' }}>
|
|
228
|
+
{metric && (
|
|
229
|
+
<ValueInput
|
|
230
|
+
metric={metric}
|
|
231
|
+
value={rule.value}
|
|
232
|
+
operator={rule.operator}
|
|
233
|
+
onChange={(val) => onUpdate({ ...rule, value: val })}
|
|
234
|
+
disabled={disabled}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
</Box>
|
|
238
|
+
|
|
239
|
+
<Tooltip label="Delete condition">
|
|
240
|
+
<IconButton onClick={onDelete} label="Delete condition" variant="ghost" disabled={disabled}>
|
|
241
|
+
<Trash />
|
|
242
|
+
</IconButton>
|
|
243
|
+
</Tooltip>
|
|
244
|
+
</Flex>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const CancelConditionsField = forwardRef<HTMLDivElement, CancelConditionsFieldProps>(
|
|
249
|
+
({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
|
|
250
|
+
const [config, setConfig] = useState<CancelConfig>(() => parseConfig(value));
|
|
251
|
+
const theme = useTheme();
|
|
252
|
+
const colors = (theme as Record<string, unknown>)?.colors as Record<string, string> | undefined;
|
|
253
|
+
|
|
254
|
+
React.useEffect(() => {
|
|
255
|
+
const parsed = parseConfig(value);
|
|
256
|
+
setConfig(parsed);
|
|
257
|
+
}, [value]);
|
|
258
|
+
|
|
259
|
+
const handleUpdate = useCallback((newConfig: CancelConfig) => {
|
|
260
|
+
setConfig(newConfig);
|
|
261
|
+
onChange({
|
|
262
|
+
target: {
|
|
263
|
+
name,
|
|
264
|
+
value: serializeConfig(newConfig),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}, [name, onChange]);
|
|
268
|
+
|
|
269
|
+
const handleAddRule = () => {
|
|
270
|
+
const newRule: CancelRule = {
|
|
271
|
+
id: generateId(),
|
|
272
|
+
field: 'has_deposit_since_trigger',
|
|
273
|
+
operator: '$eq',
|
|
274
|
+
value: true,
|
|
275
|
+
};
|
|
276
|
+
handleUpdate({
|
|
277
|
+
...config,
|
|
278
|
+
rules: [...config.rules, newRule],
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const handleUpdateRule = (ruleId: string, updatedRule: CancelRule) => {
|
|
283
|
+
handleUpdate({
|
|
284
|
+
...config,
|
|
285
|
+
rules: config.rules.map((r) => (r.id === ruleId ? updatedRule : r)),
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const handleDeleteRule = (ruleId: string) => {
|
|
290
|
+
handleUpdate({
|
|
291
|
+
...config,
|
|
292
|
+
rules: config.rules.filter((r) => r.id !== ruleId),
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const handleLogicToggle = () => {
|
|
297
|
+
handleUpdate({
|
|
298
|
+
...config,
|
|
299
|
+
logic: config.logic === '$or' ? '$and' : '$or',
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const hasRules = config.rules.length > 0;
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<Field.Root name={name} error={error} required={required} hint={hint} ref={ref}>
|
|
307
|
+
<Flex direction="column" gap={3}>
|
|
308
|
+
<Field.Label>{intlLabel?.defaultMessage || 'Cancel Conditions'}</Field.Label>
|
|
309
|
+
<Typography variant="pi" textColor="neutral600">
|
|
310
|
+
Skip sending if any of these conditions are true (checked right before send)
|
|
311
|
+
</Typography>
|
|
312
|
+
|
|
313
|
+
<Card background="neutral100">
|
|
314
|
+
<CardContent>
|
|
315
|
+
<Box padding={4}>
|
|
316
|
+
{hasRules && (
|
|
317
|
+
<Flex justifyContent="space-between" alignItems="center" marginBottom={3}>
|
|
318
|
+
<Flex gap={2} alignItems="center">
|
|
319
|
+
<Button
|
|
320
|
+
variant={config.logic === '$or' ? 'default' : 'secondary'}
|
|
321
|
+
size="S"
|
|
322
|
+
onClick={handleLogicToggle}
|
|
323
|
+
disabled={disabled}
|
|
324
|
+
>
|
|
325
|
+
{config.logic === '$or' ? 'OR' : 'AND'}
|
|
326
|
+
</Button>
|
|
327
|
+
<Typography variant="omega" textColor="neutral600">
|
|
328
|
+
{config.logic === '$or'
|
|
329
|
+
? 'Cancel if ANY condition is true'
|
|
330
|
+
: 'Cancel if ALL conditions are true'}
|
|
331
|
+
</Typography>
|
|
332
|
+
</Flex>
|
|
333
|
+
</Flex>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
<Flex direction="column" gap={2}>
|
|
337
|
+
{config.rules.map((rule) => (
|
|
338
|
+
<RuleRow
|
|
339
|
+
key={rule.id}
|
|
340
|
+
rule={rule}
|
|
341
|
+
onUpdate={(updated) => handleUpdateRule(rule.id, updated)}
|
|
342
|
+
onDelete={() => handleDeleteRule(rule.id)}
|
|
343
|
+
disabled={disabled}
|
|
344
|
+
/>
|
|
345
|
+
))}
|
|
346
|
+
|
|
347
|
+
{!hasRules && (
|
|
348
|
+
<Box padding={4} background="neutral0" hasRadius style={{ textAlign: 'center' }}>
|
|
349
|
+
<Typography variant="omega" textColor="neutral500">
|
|
350
|
+
No cancel conditions defined. Message will be sent if segment rules match.
|
|
351
|
+
</Typography>
|
|
352
|
+
</Box>
|
|
353
|
+
)}
|
|
354
|
+
</Flex>
|
|
355
|
+
|
|
356
|
+
<Box marginTop={3}>
|
|
357
|
+
<Button
|
|
358
|
+
variant="secondary"
|
|
359
|
+
startIcon={<Plus />}
|
|
360
|
+
onClick={handleAddRule}
|
|
361
|
+
disabled={disabled}
|
|
362
|
+
size="S"
|
|
363
|
+
>
|
|
364
|
+
Add Cancel Condition
|
|
365
|
+
</Button>
|
|
366
|
+
</Box>
|
|
367
|
+
</Box>
|
|
368
|
+
</CardContent>
|
|
369
|
+
</Card>
|
|
370
|
+
|
|
371
|
+
{error && <Field.Error>{error}</Field.Error>}
|
|
372
|
+
{hint && <Field.Hint>{hint}</Field.Hint>}
|
|
373
|
+
</Flex>
|
|
374
|
+
</Field.Root>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
CancelConditionsField.displayName = 'CancelConditionsField';
|
|
380
|
+
|
|
381
|
+
export default CancelConditionsField;
|