@inspirer-dev/crm-dashboard 1.0.85 → 1.0.86

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.
@@ -263,7 +263,10 @@ const ButtonsBuilder = forwardRef<HTMLDivElement, ButtonsBuilderProps>(
263
263
  <SingleSelect
264
264
  value={btn.type || 'url'}
265
265
  onChange={(val: string) =>
266
- updateButton(btn.id, { type: val as 'url' | 'screen' })
266
+ updateButton(btn.id, {
267
+ type: val as 'url' | 'screen',
268
+ ...(val === 'url' ? { screenSlug: '' } : { url: '' }),
269
+ })
267
270
  }
268
271
  disabled={disabled}
269
272
  size="S"
@@ -17,11 +17,12 @@ import { Plus, Trash } from '@strapi/icons';
17
17
  import { useTheme } from 'styled-components';
18
18
  import { TRIGGER_PARAMS, STAGE_TYPE_LABELS, type TriggerParamDef } from './constants';
19
19
 
20
- type TriggerParamsValue = Record<string, number | null>;
20
+ type ParamGroup = Record<string, number | null>;
21
+ type TriggerParamsGroups = ParamGroup[];
21
22
 
22
23
  interface TriggerParamsFieldProps {
23
24
  name: string;
24
- value?: string | TriggerParamsValue | null;
25
+ value?: string | ParamGroup | TriggerParamsGroups | null;
25
26
  onChange: (event: { target: { name: string; value: string } }) => void;
26
27
  intlLabel: {
27
28
  id: string;
@@ -34,30 +35,42 @@ interface TriggerParamsFieldProps {
34
35
  hint?: string;
35
36
  }
36
37
 
37
- const parseValue = (value: string | TriggerParamsValue | null | undefined): TriggerParamsValue => {
38
- if (!value) return {};
39
- if (typeof value === 'object' && !Array.isArray(value)) return value as TriggerParamsValue;
38
+ const parseValue = (
39
+ value: string | ParamGroup | TriggerParamsGroups | null | undefined
40
+ ): TriggerParamsGroups => {
41
+ if (!value) return [{}];
40
42
  if (typeof value === 'string') {
41
43
  try {
42
44
  const parsed = JSON.parse(value);
43
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) return parsed;
45
+ if (Array.isArray(parsed)) return parsed.length > 0 ? parsed : [{}];
46
+ if (typeof parsed === 'object' && parsed !== null) return [parsed];
44
47
  } catch {}
48
+ return [{}];
45
49
  }
46
- return {};
50
+ if (Array.isArray(value)) return value.length > 0 ? value : [{}];
51
+ if (typeof value === 'object') return [value as ParamGroup];
52
+ return [{}];
47
53
  };
48
54
 
49
- const serialize = (params: TriggerParamsValue): string => {
50
- const out: Record<string, number> = {};
51
- for (const [key, val] of Object.entries(params)) {
52
- if (typeof val === 'number') out[key] = val;
53
- }
54
- return JSON.stringify(out);
55
+ const serialize = (groups: TriggerParamsGroups): string => {
56
+ const cleaned = groups
57
+ .map((group) => {
58
+ const out: Record<string, number> = {};
59
+ for (const [key, val] of Object.entries(group)) {
60
+ if (typeof val === 'number') out[key] = val;
61
+ }
62
+ return out;
63
+ })
64
+ .filter((group) => Object.keys(group).length > 0);
65
+ return JSON.stringify(cleaned.length > 0 ? cleaned : []);
55
66
  };
56
67
 
57
68
  interface StrapiTheme {
58
69
  colors?: Record<string, string>;
59
70
  }
60
71
 
72
+ const DEPTH_BORDERS = ['#7b79ff', '#ee5e52', '#0c75af', '#328048'];
73
+
61
74
  const useThemeColors = () => {
62
75
  const theme = useTheme() as unknown as StrapiTheme;
63
76
  const isDark = theme?.colors?.neutral0 === '#212134';
@@ -68,44 +81,302 @@ const useThemeColors = () => {
68
81
  emptyBorder: isDark ? '#32324d' : '#dcdce4',
69
82
  cardBorder: isDark ? '#32324d' : '#eaeaef',
70
83
  tagBg: isDark ? '#2d2d4a' : '#f0f0ff',
84
+ groupBg: isDark ? '#1a1a2e' : '#f6f6f9',
85
+ orDividerLine: isDark ? '#32324d' : '#dcdce4',
86
+ andLabelColor: isDark ? '#a5a5ba' : '#666687',
87
+ depthBorders: DEPTH_BORDERS,
71
88
  }),
72
89
  [isDark]
73
90
  );
74
91
  };
75
92
 
93
+ const getParamDef = (key: string): TriggerParamDef | undefined =>
94
+ TRIGGER_PARAMS.find((p) => p.key === key);
95
+
96
+ interface ParamCardProps {
97
+ paramKey: string;
98
+ value: number | null;
99
+ onValueChange: (val: number | undefined) => void;
100
+ onRemove: () => void;
101
+ disabled?: boolean;
102
+ colors: ReturnType<typeof useThemeColors>;
103
+ }
104
+
105
+ const ParamCard: React.FC<ParamCardProps> = ({
106
+ paramKey,
107
+ value,
108
+ onValueChange,
109
+ onRemove,
110
+ disabled,
111
+ colors,
112
+ }) => {
113
+ const def = getParamDef(paramKey);
114
+ if (!def) return null;
115
+
116
+ return (
117
+ <Card background="neutral0" style={{ border: `1px solid ${colors.cardBorder}` }}>
118
+ <CardContent>
119
+ <Flex gap={3} alignItems="center" padding={3}>
120
+ <Box style={{ flex: 1, minWidth: 0 }}>
121
+ <Flex gap={2} alignItems="center" marginBottom={1}>
122
+ <Typography variant="omega" fontWeight="semiBold">
123
+ {def.label}
124
+ </Typography>
125
+ {def.unit && (
126
+ <Typography variant="pi" textColor="neutral500">
127
+ ({def.unit})
128
+ </Typography>
129
+ )}
130
+ </Flex>
131
+ <Typography variant="pi" textColor="neutral500">
132
+ {def.description}
133
+ </Typography>
134
+ <Flex gap={1} marginTop={1} wrap="wrap">
135
+ {def.stageTypes.map((st) => (
136
+ <Box
137
+ key={st}
138
+ style={{
139
+ padding: '1px 6px',
140
+ borderRadius: '4px',
141
+ backgroundColor: colors.tagBg,
142
+ fontSize: '11px',
143
+ }}
144
+ >
145
+ <Typography variant="pi" textColor="primary600">
146
+ {STAGE_TYPE_LABELS[st] || st}
147
+ </Typography>
148
+ </Box>
149
+ ))}
150
+ </Flex>
151
+ </Box>
152
+
153
+ <Box style={{ width: 120, flexShrink: 0 }}>
154
+ <NumberInput
155
+ placeholder={def.placeholder}
156
+ value={value ?? ''}
157
+ onValueChange={onValueChange}
158
+ disabled={disabled}
159
+ size="S"
160
+ step={paramKey === 'balanceThresholdMultiplier' ? 0.1 : 1}
161
+ />
162
+ </Box>
163
+
164
+ <Tooltip label="Удалить параметр">
165
+ <IconButton
166
+ onClick={onRemove}
167
+ label="Delete"
168
+ variant="ghost"
169
+ size="S"
170
+ disabled={disabled}
171
+ style={{ color: '#d02b20', flexShrink: 0 }}
172
+ >
173
+ <Trash width={16} height={16} />
174
+ </IconButton>
175
+ </Tooltip>
176
+ </Flex>
177
+ </CardContent>
178
+ </Card>
179
+ );
180
+ };
181
+
182
+ interface AndLabelProps {
183
+ colors: ReturnType<typeof useThemeColors>;
184
+ }
185
+
186
+ const AndLabel: React.FC<AndLabelProps> = ({ colors }) => (
187
+ <Flex justifyContent="center" paddingTop={1} paddingBottom={1}>
188
+ <Typography
189
+ variant="sigma"
190
+ textColor="neutral600"
191
+ style={{
192
+ fontSize: '11px',
193
+ fontWeight: 700,
194
+ letterSpacing: '0.5px',
195
+ color: colors.andLabelColor,
196
+ textTransform: 'uppercase',
197
+ }}
198
+ >
199
+ И
200
+ </Typography>
201
+ </Flex>
202
+ );
203
+
204
+ interface OrDividerProps {
205
+ colors: ReturnType<typeof useThemeColors>;
206
+ }
207
+
208
+ const OrDivider: React.FC<OrDividerProps> = ({ colors }) => (
209
+ <Flex alignItems="center" gap={3} paddingTop={2} paddingBottom={2}>
210
+ <Box style={{ flex: 1, height: '1px', backgroundColor: colors.orDividerLine }} />
211
+ <Box
212
+ style={{
213
+ padding: '2px 12px',
214
+ borderRadius: '12px',
215
+ border: `1px solid ${colors.orDividerLine}`,
216
+ backgroundColor: colors.isDark ? '#212134' : '#ffffff',
217
+ }}
218
+ >
219
+ <Typography
220
+ variant="sigma"
221
+ style={{
222
+ fontSize: '11px',
223
+ fontWeight: 700,
224
+ letterSpacing: '0.5px',
225
+ color: colors.andLabelColor,
226
+ textTransform: 'uppercase',
227
+ }}
228
+ >
229
+ ИЛИ
230
+ </Typography>
231
+ </Box>
232
+ <Box style={{ flex: 1, height: '1px', backgroundColor: colors.orDividerLine }} />
233
+ </Flex>
234
+ );
235
+
236
+ interface ParamGroupCardProps {
237
+ group: ParamGroup;
238
+ groupIndex: number;
239
+ totalGroups: number;
240
+ onAddParam: (key: string) => void;
241
+ onRemoveParam: (key: string) => void;
242
+ onSetValue: (key: string, val: number | undefined) => void;
243
+ onRemoveGroup: () => void;
244
+ disabled?: boolean;
245
+ colors: ReturnType<typeof useThemeColors>;
246
+ }
247
+
248
+ const ParamGroupCard: React.FC<ParamGroupCardProps> = ({
249
+ group,
250
+ groupIndex,
251
+ totalGroups,
252
+ onAddParam,
253
+ onRemoveParam,
254
+ onSetValue,
255
+ onRemoveGroup,
256
+ disabled,
257
+ colors,
258
+ }) => {
259
+ const activeKeys = Object.keys(group);
260
+ const availableParams = TRIGGER_PARAMS.filter((p) => !activeKeys.includes(p.key));
261
+ const borderColor = colors.depthBorders[groupIndex % colors.depthBorders.length];
262
+
263
+ return (
264
+ <Box
265
+ padding={4}
266
+ hasRadius
267
+ style={{
268
+ backgroundColor: colors.groupBg,
269
+ borderLeft: `3px solid ${borderColor}`,
270
+ border: `1px solid ${colors.cardBorder}`,
271
+ borderLeftWidth: '3px',
272
+ borderLeftColor: borderColor,
273
+ }}
274
+ >
275
+ <Flex direction="column" gap={2}>
276
+ <Flex justifyContent="space-between" alignItems="center">
277
+ <Typography
278
+ variant="sigma"
279
+ textColor="neutral700"
280
+ style={{ textTransform: 'uppercase', fontSize: '11px', letterSpacing: '0.5px' }}
281
+ >
282
+ Группа {groupIndex + 1}
283
+ </Typography>
284
+ {totalGroups > 1 && (
285
+ <Tooltip label="Удалить группу">
286
+ <IconButton
287
+ onClick={onRemoveGroup}
288
+ label="Удалить группу"
289
+ variant="ghost"
290
+ size="S"
291
+ disabled={disabled}
292
+ style={{ color: '#d02b20' }}
293
+ >
294
+ <Trash width={14} height={14} />
295
+ </IconButton>
296
+ </Tooltip>
297
+ )}
298
+ </Flex>
299
+
300
+ {activeKeys.length === 0 ? (
301
+ <Box
302
+ padding={3}
303
+ hasRadius
304
+ style={{
305
+ border: `1px dashed ${colors.emptyBorder}`,
306
+ textAlign: 'center',
307
+ }}
308
+ >
309
+ <Typography variant="pi" textColor="neutral500">
310
+ Добавьте параметры в группу
311
+ </Typography>
312
+ </Box>
313
+ ) : (
314
+ activeKeys.map((key, i) => (
315
+ <React.Fragment key={key}>
316
+ {i > 0 && <AndLabel colors={colors} />}
317
+ <ParamCard
318
+ paramKey={key}
319
+ value={group[key]}
320
+ onValueChange={(val) => onSetValue(key, val)}
321
+ onRemove={() => onRemoveParam(key)}
322
+ disabled={disabled}
323
+ colors={colors}
324
+ />
325
+ </React.Fragment>
326
+ ))
327
+ )}
328
+
329
+ {availableParams.length > 0 && (
330
+ <Box paddingTop={1}>
331
+ <AddParamSelect available={availableParams} onAdd={onAddParam} disabled={disabled} />
332
+ </Box>
333
+ )}
334
+ </Flex>
335
+ </Box>
336
+ );
337
+ };
338
+
76
339
  const TriggerParamsField = forwardRef<HTMLDivElement, TriggerParamsFieldProps>(
77
340
  ({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
78
341
  const initialRef = useRef(value);
79
- const [params, setParams] = useState<TriggerParamsValue>(() => parseValue(initialRef.current));
342
+ const [groups, setGroups] = useState<TriggerParamsGroups>(() => parseValue(initialRef.current));
80
343
  const colors = useThemeColors();
81
344
 
82
- const update = (next: TriggerParamsValue) => {
83
- setParams(next);
345
+ const update = (next: TriggerParamsGroups) => {
346
+ setGroups(next);
84
347
  onChange({ target: { name, value: serialize(next) } });
85
348
  };
86
349
 
87
- const activeKeys = Object.keys(params);
350
+ const addGroup = () => {
351
+ update([...groups, {}]);
352
+ };
88
353
 
89
- const availableParams = TRIGGER_PARAMS.filter((p) => !activeKeys.includes(p.key));
354
+ const removeGroup = (idx: number) => {
355
+ const next = groups.filter((_, i) => i !== idx);
356
+ update(next.length > 0 ? next : [{}]);
357
+ };
90
358
 
91
- const addParam = (key: string) => {
92
- const def = TRIGGER_PARAMS.find((p) => p.key === key);
93
- if (!def) return;
94
- update({ ...params, [key]: null });
359
+ const addParamToGroup = (idx: number, key: string) => {
360
+ const next = groups.map((g, i) => (i === idx ? { ...g, [key]: null } : g));
361
+ update(next);
95
362
  };
96
363
 
97
- const removeParam = (key: string) => {
98
- const next = { ...params };
99
- delete next[key];
364
+ const removeParamFromGroup = (idx: number, key: string) => {
365
+ const next = groups.map((g, i) => {
366
+ if (i !== idx) return g;
367
+ const copy = { ...g };
368
+ delete copy[key];
369
+ return copy;
370
+ });
100
371
  update(next);
101
372
  };
102
373
 
103
- const setParamValue = (key: string, val: number | undefined) => {
104
- update({ ...params, [key]: val ?? null });
374
+ const setParamValue = (idx: number, key: string, val: number | undefined) => {
375
+ const next = groups.map((g, i) => (i === idx ? { ...g, [key]: val ?? null } : g));
376
+ update(next);
105
377
  };
106
378
 
107
- const getParamDef = (key: string): TriggerParamDef | undefined =>
108
- TRIGGER_PARAMS.find((p) => p.key === key);
379
+ const allEmpty = groups.every((g) => Object.keys(g).length === 0);
109
380
 
110
381
  return (
111
382
  <Field.Root name={name} error={error} required={required} hint={hint} ref={ref}>
@@ -119,7 +390,7 @@ const TriggerParamsField = forwardRef<HTMLDivElement, TriggerParamsFieldProps>(
119
390
  </Box>
120
391
  </Flex>
121
392
 
122
- {activeKeys.length === 0 ? (
393
+ {groups.length === 1 && allEmpty ? (
123
394
  <Box
124
395
  padding={5}
125
396
  background="neutral100"
@@ -141,91 +412,42 @@ const TriggerParamsField = forwardRef<HTMLDivElement, TriggerParamsFieldProps>(
141
412
  </Typography>
142
413
  </Box>
143
414
  ) : (
144
- <Flex direction="column" gap={2}>
145
- {activeKeys.map((key) => {
146
- const def = getParamDef(key);
147
- if (!def) return null;
148
-
149
- return (
150
- <Card
151
- key={key}
152
- background="neutral0"
153
- style={{
154
- border: `1px solid ${colors.cardBorder}`,
155
- }}
156
- >
157
- <CardContent>
158
- <Flex gap={3} alignItems="center" padding={3}>
159
- <Box style={{ flex: 1, minWidth: 0 }}>
160
- <Flex gap={2} alignItems="center" marginBottom={1}>
161
- <Typography variant="omega" fontWeight="semiBold">
162
- {def.label}
163
- </Typography>
164
- {def.unit && (
165
- <Typography variant="pi" textColor="neutral500">
166
- ({def.unit})
167
- </Typography>
168
- )}
169
- </Flex>
170
- <Typography variant="pi" textColor="neutral500">
171
- {def.description}
172
- </Typography>
173
- <Flex gap={1} marginTop={1} wrap="wrap">
174
- {def.stageTypes.map((st) => (
175
- <Box
176
- key={st}
177
- style={{
178
- padding: '1px 6px',
179
- borderRadius: '4px',
180
- backgroundColor: colors.tagBg,
181
- fontSize: '11px',
182
- }}
183
- >
184
- <Typography variant="pi" textColor="primary600">
185
- {STAGE_TYPE_LABELS[st] || st}
186
- </Typography>
187
- </Box>
188
- ))}
189
- </Flex>
190
- </Box>
191
-
192
- <Box style={{ width: 120, flexShrink: 0 }}>
193
- <NumberInput
194
- placeholder={def.placeholder}
195
- value={params[key] ?? ''}
196
- onValueChange={(val: number | undefined) => setParamValue(key, val)}
197
- disabled={disabled}
198
- size="S"
199
- step={key === 'balanceThresholdMultiplier' ? 0.1 : 1}
200
- />
201
- </Box>
202
-
203
- <Tooltip label="Удалить параметр">
204
- <IconButton
205
- onClick={() => removeParam(key)}
206
- label="Delete"
207
- variant="ghost"
208
- size="S"
209
- disabled={disabled}
210
- style={{ color: '#d02b20', flexShrink: 0 }}
211
- >
212
- <Trash width={16} height={16} />
213
- </IconButton>
214
- </Tooltip>
215
- </Flex>
216
- </CardContent>
217
- </Card>
218
- );
219
- })}
415
+ <Flex direction="column" gap={0}>
416
+ {groups.map((group, idx) => (
417
+ <React.Fragment key={idx}>
418
+ {idx > 0 && <OrDivider colors={colors} />}
419
+ <ParamGroupCard
420
+ group={group}
421
+ groupIndex={idx}
422
+ totalGroups={groups.length}
423
+ onAddParam={(key) => addParamToGroup(idx, key)}
424
+ onRemoveParam={(key) => removeParamFromGroup(idx, key)}
425
+ onSetValue={(key, val) => setParamValue(idx, key, val)}
426
+ onRemoveGroup={() => removeGroup(idx)}
427
+ disabled={disabled}
428
+ colors={colors}
429
+ />
430
+ </React.Fragment>
431
+ ))}
220
432
  </Flex>
221
433
  )}
222
434
 
223
- {availableParams.length > 0 && (
435
+ {groups.length === 1 && allEmpty ? (
224
436
  <AddParamSelect
225
- available={availableParams}
226
- onAdd={addParam}
437
+ available={TRIGGER_PARAMS}
438
+ onAdd={(key) => addParamToGroup(0, key)}
227
439
  disabled={disabled}
228
440
  />
441
+ ) : (
442
+ <Button
443
+ startIcon={<Plus />}
444
+ onClick={addGroup}
445
+ disabled={disabled}
446
+ variant="tertiary"
447
+ size="S"
448
+ >
449
+ Добавить OR группу
450
+ </Button>
229
451
  )}
230
452
 
231
453
  {error && <Field.Error />}