@inspirer-dev/crm-dashboard 1.0.81 → 1.0.83
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/TriggerParamsField/constants.ts +109 -0
- package/admin/src/components/TriggerParamsField/index.tsx +290 -0
- package/admin/src/index.ts +26 -1
- package/admin/src/translations/en.json +4 -1
- package/admin/src/translations/ru.json +4 -1
- package/dist/_chunks/{en-DEUgX5uV.mjs → en-B5BgIROW.mjs} +3 -1
- package/dist/_chunks/{en-D2kTkBns.js → en-GKZo4lia.js} +3 -1
- package/dist/_chunks/index-2rErXqfH.js +308 -0
- package/dist/_chunks/index-BMvCnlEy.mjs +308 -0
- package/dist/_chunks/{ru-BKzplvmu.js → ru-bj5iiIPr.js} +3 -1
- package/dist/_chunks/{ru-DOt1yfNm.mjs → ru-h22ZdPCS.mjs} +3 -1
- package/dist/admin/index.js +25 -1
- package/dist/admin/index.mjs +26 -2
- package/dist/server/index.js +5 -0
- package/dist/server/index.mjs +5 -0
- package/package.json +1 -1
- package/server/src/register.ts +6 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export interface TriggerParamDef {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description: string;
|
|
5
|
+
unit: string;
|
|
6
|
+
min: number;
|
|
7
|
+
placeholder: string;
|
|
8
|
+
stageTypes: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TRIGGER_PARAMS: TriggerParamDef[] = [
|
|
12
|
+
{
|
|
13
|
+
key: 'delaySeconds',
|
|
14
|
+
label: 'Задержка',
|
|
15
|
+
description: 'Задержка перед показом после срабатывания триггера',
|
|
16
|
+
unit: 'сек',
|
|
17
|
+
min: 0,
|
|
18
|
+
placeholder: '7',
|
|
19
|
+
stageTypes: ['side_hint', 'modal', 'retry'],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'timeOnSiteSeconds',
|
|
23
|
+
label: 'Время на сайте',
|
|
24
|
+
description: 'Мин. время на сайте для срабатывания',
|
|
25
|
+
unit: 'сек',
|
|
26
|
+
min: 0,
|
|
27
|
+
placeholder: '120',
|
|
28
|
+
stageTypes: ['exit_intent', 'side_hint', 'modal'],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'scrollThresholdPx',
|
|
32
|
+
label: 'Порог скролла',
|
|
33
|
+
description: 'Скролл в пикселях для срабатывания',
|
|
34
|
+
unit: 'px',
|
|
35
|
+
min: 0,
|
|
36
|
+
placeholder: '200',
|
|
37
|
+
stageTypes: ['side_hint'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'idleSeconds',
|
|
41
|
+
label: 'Бездействие',
|
|
42
|
+
description: 'Время без кликов/скролла',
|
|
43
|
+
unit: 'сек',
|
|
44
|
+
min: 0,
|
|
45
|
+
placeholder: '15',
|
|
46
|
+
stageTypes: ['side_hint'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'caseViewSeconds',
|
|
50
|
+
label: 'Просмотр кейса',
|
|
51
|
+
description: 'Время на странице кейса',
|
|
52
|
+
unit: 'сек',
|
|
53
|
+
min: 0,
|
|
54
|
+
placeholder: '12',
|
|
55
|
+
stageTypes: ['modal', 'side_hint'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: 'activePlayMinutes',
|
|
59
|
+
label: 'Активная игра',
|
|
60
|
+
description: 'Время активной игры',
|
|
61
|
+
unit: 'мин',
|
|
62
|
+
min: 0,
|
|
63
|
+
placeholder: '10',
|
|
64
|
+
stageTypes: ['modal'],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: 'actionCountThreshold',
|
|
68
|
+
label: 'Порог действий',
|
|
69
|
+
description: 'Кол-во значимых действий + низкий баланс',
|
|
70
|
+
unit: '',
|
|
71
|
+
min: 0,
|
|
72
|
+
placeholder: '2',
|
|
73
|
+
stageTypes: ['modal'],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: 'balanceThresholdMultiplier',
|
|
77
|
+
label: 'Множитель баланса',
|
|
78
|
+
description: 'Множитель мин. цены кейса для "низкого баланса"',
|
|
79
|
+
unit: '×',
|
|
80
|
+
min: 0,
|
|
81
|
+
placeholder: '1.5',
|
|
82
|
+
stageTypes: ['modal', 'side_hint'],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'abandonedTimeoutMinutes',
|
|
86
|
+
label: 'Таймаут заброшенного депозита',
|
|
87
|
+
description: 'Через сколько минут депозит считается заброшенным',
|
|
88
|
+
unit: 'мин',
|
|
89
|
+
min: 1,
|
|
90
|
+
placeholder: '10',
|
|
91
|
+
stageTypes: ['retry'],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: 'minDepositAmount',
|
|
95
|
+
label: 'Мин. сумма депозита',
|
|
96
|
+
description: 'Подстановка в {{min_deposit}} в текстах',
|
|
97
|
+
unit: '₽',
|
|
98
|
+
min: 0,
|
|
99
|
+
placeholder: '250',
|
|
100
|
+
stageTypes: ['modal', 'side_hint', 'retry'],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
export const STAGE_TYPE_LABELS: Record<string, string> = {
|
|
105
|
+
side_hint: 'Side-hint',
|
|
106
|
+
modal: 'Modal',
|
|
107
|
+
exit_intent: 'Exit intent',
|
|
108
|
+
retry: 'Retry',
|
|
109
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Field,
|
|
6
|
+
Flex,
|
|
7
|
+
IconButton,
|
|
8
|
+
NumberInput,
|
|
9
|
+
Typography,
|
|
10
|
+
SingleSelect,
|
|
11
|
+
SingleSelectOption,
|
|
12
|
+
Card,
|
|
13
|
+
CardContent,
|
|
14
|
+
Tooltip,
|
|
15
|
+
} from '@strapi/design-system';
|
|
16
|
+
import { Plus, Trash } from '@strapi/icons';
|
|
17
|
+
import { useTheme } from 'styled-components';
|
|
18
|
+
import { TRIGGER_PARAMS, STAGE_TYPE_LABELS, type TriggerParamDef } from './constants';
|
|
19
|
+
|
|
20
|
+
type TriggerParamsValue = Record<string, number | undefined>;
|
|
21
|
+
|
|
22
|
+
interface TriggerParamsFieldProps {
|
|
23
|
+
name: string;
|
|
24
|
+
value?: string | TriggerParamsValue | null;
|
|
25
|
+
onChange: (event: { target: { name: string; value: string } }) => void;
|
|
26
|
+
intlLabel: {
|
|
27
|
+
id: string;
|
|
28
|
+
defaultMessage: string;
|
|
29
|
+
};
|
|
30
|
+
attribute?: unknown;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
required?: boolean;
|
|
34
|
+
hint?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
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;
|
|
40
|
+
if (typeof value === 'string') {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(value);
|
|
43
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
return {};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const serialize = (params: TriggerParamsValue): string => {
|
|
50
|
+
const clean: Record<string, number> = {};
|
|
51
|
+
for (const [key, val] of Object.entries(params)) {
|
|
52
|
+
if (val !== undefined && val !== null) {
|
|
53
|
+
clean[key] = val;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return JSON.stringify(clean);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
interface StrapiTheme {
|
|
60
|
+
colors?: Record<string, string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const useThemeColors = () => {
|
|
64
|
+
const theme = useTheme() as unknown as StrapiTheme;
|
|
65
|
+
const isDark = theme?.colors?.neutral0 === '#212134';
|
|
66
|
+
|
|
67
|
+
return useMemo(
|
|
68
|
+
() => ({
|
|
69
|
+
isDark,
|
|
70
|
+
emptyBorder: isDark ? '#32324d' : '#dcdce4',
|
|
71
|
+
cardBorder: isDark ? '#32324d' : '#eaeaef',
|
|
72
|
+
tagBg: isDark ? '#2d2d4a' : '#f0f0ff',
|
|
73
|
+
}),
|
|
74
|
+
[isDark]
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const TriggerParamsField = forwardRef<HTMLDivElement, TriggerParamsFieldProps>(
|
|
79
|
+
({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
|
|
80
|
+
const [params, setParams] = useState<TriggerParamsValue>(() => parseValue(value));
|
|
81
|
+
const colors = useThemeColors();
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setParams(parseValue(value));
|
|
85
|
+
}, [value]);
|
|
86
|
+
|
|
87
|
+
const update = (next: TriggerParamsValue) => {
|
|
88
|
+
setParams(next);
|
|
89
|
+
onChange({ target: { name, value: serialize(next) } });
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const activeKeys = Object.keys(params);
|
|
93
|
+
|
|
94
|
+
const availableParams = TRIGGER_PARAMS.filter((p) => !activeKeys.includes(p.key));
|
|
95
|
+
|
|
96
|
+
const addParam = (key: string) => {
|
|
97
|
+
const def = TRIGGER_PARAMS.find((p) => p.key === key);
|
|
98
|
+
if (!def) return;
|
|
99
|
+
update({ ...params, [key]: undefined });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const removeParam = (key: string) => {
|
|
103
|
+
const next = { ...params };
|
|
104
|
+
delete next[key];
|
|
105
|
+
update(next);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const setParamValue = (key: string, val: number | undefined) => {
|
|
109
|
+
update({ ...params, [key]: val });
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const getParamDef = (key: string): TriggerParamDef | undefined =>
|
|
113
|
+
TRIGGER_PARAMS.find((p) => p.key === key);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Field.Root name={name} error={error} required={required} hint={hint} ref={ref}>
|
|
117
|
+
<Flex direction="column" gap={3}>
|
|
118
|
+
<Flex justifyContent="space-between" alignItems="center">
|
|
119
|
+
<Box>
|
|
120
|
+
<Field.Label>{intlLabel?.defaultMessage || 'Trigger Params'}</Field.Label>
|
|
121
|
+
<Typography variant="pi" textColor="neutral500">
|
|
122
|
+
Настройте параметры срабатывания для этого этапа
|
|
123
|
+
</Typography>
|
|
124
|
+
</Box>
|
|
125
|
+
</Flex>
|
|
126
|
+
|
|
127
|
+
{activeKeys.length === 0 ? (
|
|
128
|
+
<Box
|
|
129
|
+
padding={5}
|
|
130
|
+
background="neutral100"
|
|
131
|
+
hasRadius
|
|
132
|
+
style={{
|
|
133
|
+
border: `2px dashed ${colors.emptyBorder}`,
|
|
134
|
+
textAlign: 'center',
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<Typography variant="omega" textColor="neutral500">
|
|
138
|
+
Параметры не настроены
|
|
139
|
+
</Typography>
|
|
140
|
+
<Typography
|
|
141
|
+
variant="pi"
|
|
142
|
+
textColor="neutral400"
|
|
143
|
+
style={{ display: 'block', marginTop: 4 }}
|
|
144
|
+
>
|
|
145
|
+
Добавьте параметры триггера, чтобы управлять условиями показа этапа
|
|
146
|
+
</Typography>
|
|
147
|
+
</Box>
|
|
148
|
+
) : (
|
|
149
|
+
<Flex direction="column" gap={2}>
|
|
150
|
+
{activeKeys.map((key) => {
|
|
151
|
+
const def = getParamDef(key);
|
|
152
|
+
if (!def) return null;
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Card
|
|
156
|
+
key={key}
|
|
157
|
+
background="neutral0"
|
|
158
|
+
style={{
|
|
159
|
+
border: `1px solid ${colors.cardBorder}`,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<CardContent>
|
|
163
|
+
<Flex gap={3} alignItems="center" padding={3}>
|
|
164
|
+
<Box style={{ flex: 1, minWidth: 0 }}>
|
|
165
|
+
<Flex gap={2} alignItems="center" marginBottom={1}>
|
|
166
|
+
<Typography variant="omega" fontWeight="semiBold">
|
|
167
|
+
{def.label}
|
|
168
|
+
</Typography>
|
|
169
|
+
{def.unit && (
|
|
170
|
+
<Typography variant="pi" textColor="neutral500">
|
|
171
|
+
({def.unit})
|
|
172
|
+
</Typography>
|
|
173
|
+
)}
|
|
174
|
+
</Flex>
|
|
175
|
+
<Typography variant="pi" textColor="neutral500">
|
|
176
|
+
{def.description}
|
|
177
|
+
</Typography>
|
|
178
|
+
<Flex gap={1} marginTop={1} wrap="wrap">
|
|
179
|
+
{def.stageTypes.map((st) => (
|
|
180
|
+
<Box
|
|
181
|
+
key={st}
|
|
182
|
+
style={{
|
|
183
|
+
padding: '1px 6px',
|
|
184
|
+
borderRadius: '4px',
|
|
185
|
+
backgroundColor: colors.tagBg,
|
|
186
|
+
fontSize: '11px',
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<Typography variant="pi" textColor="primary600">
|
|
190
|
+
{STAGE_TYPE_LABELS[st] || st}
|
|
191
|
+
</Typography>
|
|
192
|
+
</Box>
|
|
193
|
+
))}
|
|
194
|
+
</Flex>
|
|
195
|
+
</Box>
|
|
196
|
+
|
|
197
|
+
<Box style={{ width: 120, flexShrink: 0 }}>
|
|
198
|
+
<NumberInput
|
|
199
|
+
placeholder={def.placeholder}
|
|
200
|
+
value={params[key] ?? ''}
|
|
201
|
+
onValueChange={(val: number | undefined) => setParamValue(key, val)}
|
|
202
|
+
disabled={disabled}
|
|
203
|
+
size="S"
|
|
204
|
+
step={key === 'balanceThresholdMultiplier' ? 0.1 : 1}
|
|
205
|
+
/>
|
|
206
|
+
</Box>
|
|
207
|
+
|
|
208
|
+
<Tooltip label="Удалить параметр">
|
|
209
|
+
<IconButton
|
|
210
|
+
onClick={() => removeParam(key)}
|
|
211
|
+
label="Delete"
|
|
212
|
+
variant="ghost"
|
|
213
|
+
size="S"
|
|
214
|
+
disabled={disabled}
|
|
215
|
+
style={{ color: '#d02b20', flexShrink: 0 }}
|
|
216
|
+
>
|
|
217
|
+
<Trash width={16} height={16} />
|
|
218
|
+
</IconButton>
|
|
219
|
+
</Tooltip>
|
|
220
|
+
</Flex>
|
|
221
|
+
</CardContent>
|
|
222
|
+
</Card>
|
|
223
|
+
);
|
|
224
|
+
})}
|
|
225
|
+
</Flex>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{availableParams.length > 0 && (
|
|
229
|
+
<AddParamSelect
|
|
230
|
+
available={availableParams}
|
|
231
|
+
onAdd={addParam}
|
|
232
|
+
disabled={disabled}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{error && <Field.Error />}
|
|
237
|
+
{hint && <Field.Hint />}
|
|
238
|
+
</Flex>
|
|
239
|
+
</Field.Root>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
TriggerParamsField.displayName = 'TriggerParamsField';
|
|
245
|
+
|
|
246
|
+
interface AddParamSelectProps {
|
|
247
|
+
available: TriggerParamDef[];
|
|
248
|
+
onAdd: (key: string) => void;
|
|
249
|
+
disabled?: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const AddParamSelect: React.FC<AddParamSelectProps> = ({ available, onAdd, disabled }) => {
|
|
253
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<Flex gap={2} alignItems="flex-end">
|
|
257
|
+
<Box style={{ flex: 1 }}>
|
|
258
|
+
<SingleSelect
|
|
259
|
+
placeholder="Выберите параметр..."
|
|
260
|
+
value={selected}
|
|
261
|
+
onChange={(val: string) => setSelected(val)}
|
|
262
|
+
disabled={disabled}
|
|
263
|
+
size="S"
|
|
264
|
+
>
|
|
265
|
+
{available.map((p) => (
|
|
266
|
+
<SingleSelectOption key={p.key} value={p.key}>
|
|
267
|
+
{p.label} — {p.description}
|
|
268
|
+
</SingleSelectOption>
|
|
269
|
+
))}
|
|
270
|
+
</SingleSelect>
|
|
271
|
+
</Box>
|
|
272
|
+
<Button
|
|
273
|
+
startIcon={<Plus />}
|
|
274
|
+
onClick={() => {
|
|
275
|
+
if (selected) {
|
|
276
|
+
onAdd(selected);
|
|
277
|
+
setSelected(null);
|
|
278
|
+
}
|
|
279
|
+
}}
|
|
280
|
+
disabled={disabled || !selected}
|
|
281
|
+
variant="secondary"
|
|
282
|
+
size="S"
|
|
283
|
+
>
|
|
284
|
+
Добавить
|
|
285
|
+
</Button>
|
|
286
|
+
</Flex>
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export default TriggerParamsField;
|
package/admin/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, Clock, Cross, Link, ArrowRight } from '@strapi/icons';
|
|
1
|
+
import { Message, Clock, Cross, Link, ArrowRight, Filter } from '@strapi/icons';
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = 'crm-dashboard';
|
|
4
4
|
|
|
@@ -129,6 +129,31 @@ export default {
|
|
|
129
129
|
},
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
app.customFields.register({
|
|
133
|
+
name: 'trigger-params',
|
|
134
|
+
pluginId: PLUGIN_ID,
|
|
135
|
+
type: 'json',
|
|
136
|
+
intlLabel: {
|
|
137
|
+
id: `${PLUGIN_ID}.trigger-params.label`,
|
|
138
|
+
defaultMessage: 'Trigger Params',
|
|
139
|
+
},
|
|
140
|
+
intlDescription: {
|
|
141
|
+
id: `${PLUGIN_ID}.trigger-params.description`,
|
|
142
|
+
defaultMessage: 'Configure trigger parameters for popup flow stages',
|
|
143
|
+
},
|
|
144
|
+
icon: Filter,
|
|
145
|
+
components: {
|
|
146
|
+
Input: async () =>
|
|
147
|
+
import(
|
|
148
|
+
/* webpackChunkName: "crm-trigger-params" */ './components/TriggerParamsField/index'
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
options: {
|
|
152
|
+
base: [],
|
|
153
|
+
advanced: [],
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
132
157
|
app.addMenuLink({
|
|
133
158
|
to: `/plugins/${PLUGIN_ID}`,
|
|
134
159
|
icon: Message,
|
|
@@ -119,5 +119,8 @@
|
|
|
119
119
|
"crm-dashboard.template.title.description": "For email: subject line. For Telegram: optional internal title (not shown to user). For push: notification title.",
|
|
120
120
|
|
|
121
121
|
"crm-dashboard.template.body.label": "Message Body",
|
|
122
|
-
"crm-dashboard.template.body.description": "The message text. Variables: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *bold*, _italic_"
|
|
122
|
+
"crm-dashboard.template.body.description": "The message text. Variables: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *bold*, _italic_",
|
|
123
|
+
|
|
124
|
+
"crm-dashboard.trigger-params.label": "Trigger Params",
|
|
125
|
+
"crm-dashboard.trigger-params.description": "Configure trigger parameters for popup flow stages. Add only the parameters relevant to this stage type."
|
|
123
126
|
}
|
|
@@ -119,5 +119,8 @@
|
|
|
119
119
|
"crm-dashboard.template.title.description": "Для email: тема письма. Для Telegram: внутренний заголовок (не виден пользователю). Для push: заголовок уведомления.",
|
|
120
120
|
|
|
121
121
|
"crm-dashboard.template.body.label": "Текст сообщения",
|
|
122
|
-
"crm-dashboard.template.body.description": "Текст сообщения. Переменные: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *жирный*, _курсив_"
|
|
122
|
+
"crm-dashboard.template.body.description": "Текст сообщения. Переменные: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *жирный*, _курсив_",
|
|
123
|
+
|
|
124
|
+
"crm-dashboard.trigger-params.label": "Параметры триггера",
|
|
125
|
+
"crm-dashboard.trigger-params.description": "Настройте параметры срабатывания для этапов popup-флоу. Добавляйте только параметры, релевантные для данного типа этапа."
|
|
123
126
|
}
|
|
@@ -79,7 +79,9 @@ const en = {
|
|
|
79
79
|
"crm-dashboard.template.title.label": "Title",
|
|
80
80
|
"crm-dashboard.template.title.description": "For email: subject line. For Telegram: optional internal title (not shown to user). For push: notification title.",
|
|
81
81
|
"crm-dashboard.template.body.label": "Message Body",
|
|
82
|
-
"crm-dashboard.template.body.description": "The message text. Variables: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *bold*, _italic_"
|
|
82
|
+
"crm-dashboard.template.body.description": "The message text. Variables: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *bold*, _italic_",
|
|
83
|
+
"crm-dashboard.trigger-params.label": "Trigger Params",
|
|
84
|
+
"crm-dashboard.trigger-params.description": "Configure trigger parameters for popup flow stages. Add only the parameters relevant to this stage type."
|
|
83
85
|
};
|
|
84
86
|
export {
|
|
85
87
|
en as default
|
|
@@ -81,6 +81,8 @@ const en = {
|
|
|
81
81
|
"crm-dashboard.template.title.label": "Title",
|
|
82
82
|
"crm-dashboard.template.title.description": "For email: subject line. For Telegram: optional internal title (not shown to user). For push: notification title.",
|
|
83
83
|
"crm-dashboard.template.body.label": "Message Body",
|
|
84
|
-
"crm-dashboard.template.body.description": "The message text. Variables: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *bold*, _italic_"
|
|
84
|
+
"crm-dashboard.template.body.description": "The message text. Variables: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *bold*, _italic_",
|
|
85
|
+
"crm-dashboard.trigger-params.label": "Trigger Params",
|
|
86
|
+
"crm-dashboard.trigger-params.description": "Configure trigger parameters for popup flow stages. Add only the parameters relevant to this stage type."
|
|
85
87
|
};
|
|
86
88
|
exports.default = en;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const designSystem = require("@strapi/design-system");
|
|
6
|
+
const icons = require("@strapi/icons");
|
|
7
|
+
const styledComponents = require("styled-components");
|
|
8
|
+
const TRIGGER_PARAMS = [
|
|
9
|
+
{
|
|
10
|
+
key: "delaySeconds",
|
|
11
|
+
label: "Задержка",
|
|
12
|
+
description: "Задержка перед показом после срабатывания триггера",
|
|
13
|
+
unit: "сек",
|
|
14
|
+
min: 0,
|
|
15
|
+
placeholder: "7",
|
|
16
|
+
stageTypes: ["side_hint", "modal", "retry"]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: "timeOnSiteSeconds",
|
|
20
|
+
label: "Время на сайте",
|
|
21
|
+
description: "Мин. время на сайте для срабатывания",
|
|
22
|
+
unit: "сек",
|
|
23
|
+
min: 0,
|
|
24
|
+
placeholder: "120",
|
|
25
|
+
stageTypes: ["exit_intent", "side_hint", "modal"]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "scrollThresholdPx",
|
|
29
|
+
label: "Порог скролла",
|
|
30
|
+
description: "Скролл в пикселях для срабатывания",
|
|
31
|
+
unit: "px",
|
|
32
|
+
min: 0,
|
|
33
|
+
placeholder: "200",
|
|
34
|
+
stageTypes: ["side_hint"]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "idleSeconds",
|
|
38
|
+
label: "Бездействие",
|
|
39
|
+
description: "Время без кликов/скролла",
|
|
40
|
+
unit: "сек",
|
|
41
|
+
min: 0,
|
|
42
|
+
placeholder: "15",
|
|
43
|
+
stageTypes: ["side_hint"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: "caseViewSeconds",
|
|
47
|
+
label: "Просмотр кейса",
|
|
48
|
+
description: "Время на странице кейса",
|
|
49
|
+
unit: "сек",
|
|
50
|
+
min: 0,
|
|
51
|
+
placeholder: "12",
|
|
52
|
+
stageTypes: ["modal", "side_hint"]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "activePlayMinutes",
|
|
56
|
+
label: "Активная игра",
|
|
57
|
+
description: "Время активной игры",
|
|
58
|
+
unit: "мин",
|
|
59
|
+
min: 0,
|
|
60
|
+
placeholder: "10",
|
|
61
|
+
stageTypes: ["modal"]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: "actionCountThreshold",
|
|
65
|
+
label: "Порог действий",
|
|
66
|
+
description: "Кол-во значимых действий + низкий баланс",
|
|
67
|
+
unit: "",
|
|
68
|
+
min: 0,
|
|
69
|
+
placeholder: "2",
|
|
70
|
+
stageTypes: ["modal"]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: "balanceThresholdMultiplier",
|
|
74
|
+
label: "Множитель баланса",
|
|
75
|
+
description: 'Множитель мин. цены кейса для "низкого баланса"',
|
|
76
|
+
unit: "×",
|
|
77
|
+
min: 0,
|
|
78
|
+
placeholder: "1.5",
|
|
79
|
+
stageTypes: ["modal", "side_hint"]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "abandonedTimeoutMinutes",
|
|
83
|
+
label: "Таймаут заброшенного депозита",
|
|
84
|
+
description: "Через сколько минут депозит считается заброшенным",
|
|
85
|
+
unit: "мин",
|
|
86
|
+
min: 1,
|
|
87
|
+
placeholder: "10",
|
|
88
|
+
stageTypes: ["retry"]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "minDepositAmount",
|
|
92
|
+
label: "Мин. сумма депозита",
|
|
93
|
+
description: "Подстановка в {{min_deposit}} в текстах",
|
|
94
|
+
unit: "₽",
|
|
95
|
+
min: 0,
|
|
96
|
+
placeholder: "250",
|
|
97
|
+
stageTypes: ["modal", "side_hint", "retry"]
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
const STAGE_TYPE_LABELS = {
|
|
101
|
+
side_hint: "Side-hint",
|
|
102
|
+
modal: "Modal",
|
|
103
|
+
exit_intent: "Exit intent",
|
|
104
|
+
retry: "Retry"
|
|
105
|
+
};
|
|
106
|
+
const parseValue = (value) => {
|
|
107
|
+
if (!value) return {};
|
|
108
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(value);
|
|
112
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {};
|
|
117
|
+
};
|
|
118
|
+
const serialize = (params) => {
|
|
119
|
+
const clean = {};
|
|
120
|
+
for (const [key, val] of Object.entries(params)) {
|
|
121
|
+
if (val !== void 0 && val !== null) {
|
|
122
|
+
clean[key] = val;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return JSON.stringify(clean);
|
|
126
|
+
};
|
|
127
|
+
const useThemeColors = () => {
|
|
128
|
+
const theme = styledComponents.useTheme();
|
|
129
|
+
const isDark = theme?.colors?.neutral0 === "#212134";
|
|
130
|
+
return React.useMemo(
|
|
131
|
+
() => ({
|
|
132
|
+
isDark,
|
|
133
|
+
emptyBorder: isDark ? "#32324d" : "#dcdce4",
|
|
134
|
+
cardBorder: isDark ? "#32324d" : "#eaeaef",
|
|
135
|
+
tagBg: isDark ? "#2d2d4a" : "#f0f0ff"
|
|
136
|
+
}),
|
|
137
|
+
[isDark]
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
const TriggerParamsField = React.forwardRef(
|
|
141
|
+
({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
|
|
142
|
+
const [params, setParams] = React.useState(() => parseValue(value));
|
|
143
|
+
const colors = useThemeColors();
|
|
144
|
+
React.useEffect(() => {
|
|
145
|
+
setParams(parseValue(value));
|
|
146
|
+
}, [value]);
|
|
147
|
+
const update = (next) => {
|
|
148
|
+
setParams(next);
|
|
149
|
+
onChange({ target: { name, value: serialize(next) } });
|
|
150
|
+
};
|
|
151
|
+
const activeKeys = Object.keys(params);
|
|
152
|
+
const availableParams = TRIGGER_PARAMS.filter((p) => !activeKeys.includes(p.key));
|
|
153
|
+
const addParam = (key) => {
|
|
154
|
+
const def = TRIGGER_PARAMS.find((p) => p.key === key);
|
|
155
|
+
if (!def) return;
|
|
156
|
+
update({ ...params, [key]: void 0 });
|
|
157
|
+
};
|
|
158
|
+
const removeParam = (key) => {
|
|
159
|
+
const next = { ...params };
|
|
160
|
+
delete next[key];
|
|
161
|
+
update(next);
|
|
162
|
+
};
|
|
163
|
+
const setParamValue = (key, val) => {
|
|
164
|
+
update({ ...params, [key]: val });
|
|
165
|
+
};
|
|
166
|
+
const getParamDef = (key) => TRIGGER_PARAMS.find((p) => p.key === key);
|
|
167
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Root, { name, error, required, hint, ref, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 3, children: [
|
|
168
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
|
|
169
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: intlLabel?.defaultMessage || "Trigger Params" }),
|
|
170
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Настройте параметры срабатывания для этого этапа" })
|
|
171
|
+
] }) }),
|
|
172
|
+
activeKeys.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
173
|
+
designSystem.Box,
|
|
174
|
+
{
|
|
175
|
+
padding: 5,
|
|
176
|
+
background: "neutral100",
|
|
177
|
+
hasRadius: true,
|
|
178
|
+
style: {
|
|
179
|
+
border: `2px dashed ${colors.emptyBorder}`,
|
|
180
|
+
textAlign: "center"
|
|
181
|
+
},
|
|
182
|
+
children: [
|
|
183
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral500", children: "Параметры не настроены" }),
|
|
184
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
185
|
+
designSystem.Typography,
|
|
186
|
+
{
|
|
187
|
+
variant: "pi",
|
|
188
|
+
textColor: "neutral400",
|
|
189
|
+
style: { display: "block", marginTop: 4 },
|
|
190
|
+
children: "Добавьте параметры триггера, чтобы управлять условиями показа этапа"
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, children: activeKeys.map((key) => {
|
|
196
|
+
const def = getParamDef(key);
|
|
197
|
+
if (!def) return null;
|
|
198
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
199
|
+
designSystem.Card,
|
|
200
|
+
{
|
|
201
|
+
background: "neutral0",
|
|
202
|
+
style: {
|
|
203
|
+
border: `1px solid ${colors.cardBorder}`
|
|
204
|
+
},
|
|
205
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "center", padding: 3, children: [
|
|
206
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { style: { flex: 1, minWidth: 0 }, children: [
|
|
207
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", marginBottom: 1, children: [
|
|
208
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: def.label }),
|
|
209
|
+
def.unit && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
210
|
+
"(",
|
|
211
|
+
def.unit,
|
|
212
|
+
")"
|
|
213
|
+
] })
|
|
214
|
+
] }),
|
|
215
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: def.description }),
|
|
216
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 1, marginTop: 1, wrap: "wrap", children: def.stageTypes.map((st) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
217
|
+
designSystem.Box,
|
|
218
|
+
{
|
|
219
|
+
style: {
|
|
220
|
+
padding: "1px 6px",
|
|
221
|
+
borderRadius: "4px",
|
|
222
|
+
backgroundColor: colors.tagBg,
|
|
223
|
+
fontSize: "11px"
|
|
224
|
+
},
|
|
225
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "primary600", children: STAGE_TYPE_LABELS[st] || st })
|
|
226
|
+
},
|
|
227
|
+
st
|
|
228
|
+
)) })
|
|
229
|
+
] }),
|
|
230
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: 120, flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
231
|
+
designSystem.NumberInput,
|
|
232
|
+
{
|
|
233
|
+
placeholder: def.placeholder,
|
|
234
|
+
value: params[key] ?? "",
|
|
235
|
+
onValueChange: (val) => setParamValue(key, val),
|
|
236
|
+
disabled,
|
|
237
|
+
size: "S",
|
|
238
|
+
step: key === "balanceThresholdMultiplier" ? 0.1 : 1
|
|
239
|
+
}
|
|
240
|
+
) }),
|
|
241
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Tooltip, { label: "Удалить параметр", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
242
|
+
designSystem.IconButton,
|
|
243
|
+
{
|
|
244
|
+
onClick: () => removeParam(key),
|
|
245
|
+
label: "Delete",
|
|
246
|
+
variant: "ghost",
|
|
247
|
+
size: "S",
|
|
248
|
+
disabled,
|
|
249
|
+
style: { color: "#d02b20", flexShrink: 0 },
|
|
250
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, { width: 16, height: 16 })
|
|
251
|
+
}
|
|
252
|
+
) })
|
|
253
|
+
] }) })
|
|
254
|
+
},
|
|
255
|
+
key
|
|
256
|
+
);
|
|
257
|
+
}) }),
|
|
258
|
+
availableParams.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
259
|
+
AddParamSelect,
|
|
260
|
+
{
|
|
261
|
+
available: availableParams,
|
|
262
|
+
onAdd: addParam,
|
|
263
|
+
disabled
|
|
264
|
+
}
|
|
265
|
+
),
|
|
266
|
+
error && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {}),
|
|
267
|
+
hint && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
|
|
268
|
+
] }) });
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
TriggerParamsField.displayName = "TriggerParamsField";
|
|
272
|
+
const AddParamSelect = ({ available, onAdd, disabled }) => {
|
|
273
|
+
const [selected, setSelected] = React.useState(null);
|
|
274
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "flex-end", children: [
|
|
275
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
276
|
+
designSystem.SingleSelect,
|
|
277
|
+
{
|
|
278
|
+
placeholder: "Выберите параметр...",
|
|
279
|
+
value: selected,
|
|
280
|
+
onChange: (val) => setSelected(val),
|
|
281
|
+
disabled,
|
|
282
|
+
size: "S",
|
|
283
|
+
children: available.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.SingleSelectOption, { value: p.key, children: [
|
|
284
|
+
p.label,
|
|
285
|
+
" — ",
|
|
286
|
+
p.description
|
|
287
|
+
] }, p.key))
|
|
288
|
+
}
|
|
289
|
+
) }),
|
|
290
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
291
|
+
designSystem.Button,
|
|
292
|
+
{
|
|
293
|
+
startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
|
|
294
|
+
onClick: () => {
|
|
295
|
+
if (selected) {
|
|
296
|
+
onAdd(selected);
|
|
297
|
+
setSelected(null);
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
disabled: disabled || !selected,
|
|
301
|
+
variant: "secondary",
|
|
302
|
+
size: "S",
|
|
303
|
+
children: "Добавить"
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
] });
|
|
307
|
+
};
|
|
308
|
+
exports.default = TriggerParamsField;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useState, useEffect, useMemo } from "react";
|
|
3
|
+
import { Field, Flex, Box, Typography, Card, CardContent, NumberInput, Tooltip, IconButton, SingleSelect, SingleSelectOption, Button } from "@strapi/design-system";
|
|
4
|
+
import { Trash, Plus } from "@strapi/icons";
|
|
5
|
+
import { useTheme } from "styled-components";
|
|
6
|
+
const TRIGGER_PARAMS = [
|
|
7
|
+
{
|
|
8
|
+
key: "delaySeconds",
|
|
9
|
+
label: "Задержка",
|
|
10
|
+
description: "Задержка перед показом после срабатывания триггера",
|
|
11
|
+
unit: "сек",
|
|
12
|
+
min: 0,
|
|
13
|
+
placeholder: "7",
|
|
14
|
+
stageTypes: ["side_hint", "modal", "retry"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "timeOnSiteSeconds",
|
|
18
|
+
label: "Время на сайте",
|
|
19
|
+
description: "Мин. время на сайте для срабатывания",
|
|
20
|
+
unit: "сек",
|
|
21
|
+
min: 0,
|
|
22
|
+
placeholder: "120",
|
|
23
|
+
stageTypes: ["exit_intent", "side_hint", "modal"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: "scrollThresholdPx",
|
|
27
|
+
label: "Порог скролла",
|
|
28
|
+
description: "Скролл в пикселях для срабатывания",
|
|
29
|
+
unit: "px",
|
|
30
|
+
min: 0,
|
|
31
|
+
placeholder: "200",
|
|
32
|
+
stageTypes: ["side_hint"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "idleSeconds",
|
|
36
|
+
label: "Бездействие",
|
|
37
|
+
description: "Время без кликов/скролла",
|
|
38
|
+
unit: "сек",
|
|
39
|
+
min: 0,
|
|
40
|
+
placeholder: "15",
|
|
41
|
+
stageTypes: ["side_hint"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "caseViewSeconds",
|
|
45
|
+
label: "Просмотр кейса",
|
|
46
|
+
description: "Время на странице кейса",
|
|
47
|
+
unit: "сек",
|
|
48
|
+
min: 0,
|
|
49
|
+
placeholder: "12",
|
|
50
|
+
stageTypes: ["modal", "side_hint"]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "activePlayMinutes",
|
|
54
|
+
label: "Активная игра",
|
|
55
|
+
description: "Время активной игры",
|
|
56
|
+
unit: "мин",
|
|
57
|
+
min: 0,
|
|
58
|
+
placeholder: "10",
|
|
59
|
+
stageTypes: ["modal"]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "actionCountThreshold",
|
|
63
|
+
label: "Порог действий",
|
|
64
|
+
description: "Кол-во значимых действий + низкий баланс",
|
|
65
|
+
unit: "",
|
|
66
|
+
min: 0,
|
|
67
|
+
placeholder: "2",
|
|
68
|
+
stageTypes: ["modal"]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "balanceThresholdMultiplier",
|
|
72
|
+
label: "Множитель баланса",
|
|
73
|
+
description: 'Множитель мин. цены кейса для "низкого баланса"',
|
|
74
|
+
unit: "×",
|
|
75
|
+
min: 0,
|
|
76
|
+
placeholder: "1.5",
|
|
77
|
+
stageTypes: ["modal", "side_hint"]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: "abandonedTimeoutMinutes",
|
|
81
|
+
label: "Таймаут заброшенного депозита",
|
|
82
|
+
description: "Через сколько минут депозит считается заброшенным",
|
|
83
|
+
unit: "мин",
|
|
84
|
+
min: 1,
|
|
85
|
+
placeholder: "10",
|
|
86
|
+
stageTypes: ["retry"]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: "minDepositAmount",
|
|
90
|
+
label: "Мин. сумма депозита",
|
|
91
|
+
description: "Подстановка в {{min_deposit}} в текстах",
|
|
92
|
+
unit: "₽",
|
|
93
|
+
min: 0,
|
|
94
|
+
placeholder: "250",
|
|
95
|
+
stageTypes: ["modal", "side_hint", "retry"]
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
const STAGE_TYPE_LABELS = {
|
|
99
|
+
side_hint: "Side-hint",
|
|
100
|
+
modal: "Modal",
|
|
101
|
+
exit_intent: "Exit intent",
|
|
102
|
+
retry: "Retry"
|
|
103
|
+
};
|
|
104
|
+
const parseValue = (value) => {
|
|
105
|
+
if (!value) return {};
|
|
106
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
107
|
+
if (typeof value === "string") {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(value);
|
|
110
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {};
|
|
115
|
+
};
|
|
116
|
+
const serialize = (params) => {
|
|
117
|
+
const clean = {};
|
|
118
|
+
for (const [key, val] of Object.entries(params)) {
|
|
119
|
+
if (val !== void 0 && val !== null) {
|
|
120
|
+
clean[key] = val;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return JSON.stringify(clean);
|
|
124
|
+
};
|
|
125
|
+
const useThemeColors = () => {
|
|
126
|
+
const theme = useTheme();
|
|
127
|
+
const isDark = theme?.colors?.neutral0 === "#212134";
|
|
128
|
+
return useMemo(
|
|
129
|
+
() => ({
|
|
130
|
+
isDark,
|
|
131
|
+
emptyBorder: isDark ? "#32324d" : "#dcdce4",
|
|
132
|
+
cardBorder: isDark ? "#32324d" : "#eaeaef",
|
|
133
|
+
tagBg: isDark ? "#2d2d4a" : "#f0f0ff"
|
|
134
|
+
}),
|
|
135
|
+
[isDark]
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
const TriggerParamsField = forwardRef(
|
|
139
|
+
({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
|
|
140
|
+
const [params, setParams] = useState(() => parseValue(value));
|
|
141
|
+
const colors = useThemeColors();
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
setParams(parseValue(value));
|
|
144
|
+
}, [value]);
|
|
145
|
+
const update = (next) => {
|
|
146
|
+
setParams(next);
|
|
147
|
+
onChange({ target: { name, value: serialize(next) } });
|
|
148
|
+
};
|
|
149
|
+
const activeKeys = Object.keys(params);
|
|
150
|
+
const availableParams = TRIGGER_PARAMS.filter((p) => !activeKeys.includes(p.key));
|
|
151
|
+
const addParam = (key) => {
|
|
152
|
+
const def = TRIGGER_PARAMS.find((p) => p.key === key);
|
|
153
|
+
if (!def) return;
|
|
154
|
+
update({ ...params, [key]: void 0 });
|
|
155
|
+
};
|
|
156
|
+
const removeParam = (key) => {
|
|
157
|
+
const next = { ...params };
|
|
158
|
+
delete next[key];
|
|
159
|
+
update(next);
|
|
160
|
+
};
|
|
161
|
+
const setParamValue = (key, val) => {
|
|
162
|
+
update({ ...params, [key]: val });
|
|
163
|
+
};
|
|
164
|
+
const getParamDef = (key) => TRIGGER_PARAMS.find((p) => p.key === key);
|
|
165
|
+
return /* @__PURE__ */ jsx(Field.Root, { name, error, required, hint, ref, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
166
|
+
/* @__PURE__ */ jsx(Flex, { justifyContent: "space-between", alignItems: "center", children: /* @__PURE__ */ jsxs(Box, { children: [
|
|
167
|
+
/* @__PURE__ */ jsx(Field.Label, { children: intlLabel?.defaultMessage || "Trigger Params" }),
|
|
168
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Настройте параметры срабатывания для этого этапа" })
|
|
169
|
+
] }) }),
|
|
170
|
+
activeKeys.length === 0 ? /* @__PURE__ */ jsxs(
|
|
171
|
+
Box,
|
|
172
|
+
{
|
|
173
|
+
padding: 5,
|
|
174
|
+
background: "neutral100",
|
|
175
|
+
hasRadius: true,
|
|
176
|
+
style: {
|
|
177
|
+
border: `2px dashed ${colors.emptyBorder}`,
|
|
178
|
+
textAlign: "center"
|
|
179
|
+
},
|
|
180
|
+
children: [
|
|
181
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral500", children: "Параметры не настроены" }),
|
|
182
|
+
/* @__PURE__ */ jsx(
|
|
183
|
+
Typography,
|
|
184
|
+
{
|
|
185
|
+
variant: "pi",
|
|
186
|
+
textColor: "neutral400",
|
|
187
|
+
style: { display: "block", marginTop: 4 },
|
|
188
|
+
children: "Добавьте параметры триггера, чтобы управлять условиями показа этапа"
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: activeKeys.map((key) => {
|
|
194
|
+
const def = getParamDef(key);
|
|
195
|
+
if (!def) return null;
|
|
196
|
+
return /* @__PURE__ */ jsx(
|
|
197
|
+
Card,
|
|
198
|
+
{
|
|
199
|
+
background: "neutral0",
|
|
200
|
+
style: {
|
|
201
|
+
border: `1px solid ${colors.cardBorder}`
|
|
202
|
+
},
|
|
203
|
+
children: /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", padding: 3, children: [
|
|
204
|
+
/* @__PURE__ */ jsxs(Box, { style: { flex: 1, minWidth: 0 }, children: [
|
|
205
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", marginBottom: 1, children: [
|
|
206
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: def.label }),
|
|
207
|
+
def.unit && /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
208
|
+
"(",
|
|
209
|
+
def.unit,
|
|
210
|
+
")"
|
|
211
|
+
] })
|
|
212
|
+
] }),
|
|
213
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: def.description }),
|
|
214
|
+
/* @__PURE__ */ jsx(Flex, { gap: 1, marginTop: 1, wrap: "wrap", children: def.stageTypes.map((st) => /* @__PURE__ */ jsx(
|
|
215
|
+
Box,
|
|
216
|
+
{
|
|
217
|
+
style: {
|
|
218
|
+
padding: "1px 6px",
|
|
219
|
+
borderRadius: "4px",
|
|
220
|
+
backgroundColor: colors.tagBg,
|
|
221
|
+
fontSize: "11px"
|
|
222
|
+
},
|
|
223
|
+
children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "primary600", children: STAGE_TYPE_LABELS[st] || st })
|
|
224
|
+
},
|
|
225
|
+
st
|
|
226
|
+
)) })
|
|
227
|
+
] }),
|
|
228
|
+
/* @__PURE__ */ jsx(Box, { style: { width: 120, flexShrink: 0 }, children: /* @__PURE__ */ jsx(
|
|
229
|
+
NumberInput,
|
|
230
|
+
{
|
|
231
|
+
placeholder: def.placeholder,
|
|
232
|
+
value: params[key] ?? "",
|
|
233
|
+
onValueChange: (val) => setParamValue(key, val),
|
|
234
|
+
disabled,
|
|
235
|
+
size: "S",
|
|
236
|
+
step: key === "balanceThresholdMultiplier" ? 0.1 : 1
|
|
237
|
+
}
|
|
238
|
+
) }),
|
|
239
|
+
/* @__PURE__ */ jsx(Tooltip, { label: "Удалить параметр", children: /* @__PURE__ */ jsx(
|
|
240
|
+
IconButton,
|
|
241
|
+
{
|
|
242
|
+
onClick: () => removeParam(key),
|
|
243
|
+
label: "Delete",
|
|
244
|
+
variant: "ghost",
|
|
245
|
+
size: "S",
|
|
246
|
+
disabled,
|
|
247
|
+
style: { color: "#d02b20", flexShrink: 0 },
|
|
248
|
+
children: /* @__PURE__ */ jsx(Trash, { width: 16, height: 16 })
|
|
249
|
+
}
|
|
250
|
+
) })
|
|
251
|
+
] }) })
|
|
252
|
+
},
|
|
253
|
+
key
|
|
254
|
+
);
|
|
255
|
+
}) }),
|
|
256
|
+
availableParams.length > 0 && /* @__PURE__ */ jsx(
|
|
257
|
+
AddParamSelect,
|
|
258
|
+
{
|
|
259
|
+
available: availableParams,
|
|
260
|
+
onAdd: addParam,
|
|
261
|
+
disabled
|
|
262
|
+
}
|
|
263
|
+
),
|
|
264
|
+
error && /* @__PURE__ */ jsx(Field.Error, {}),
|
|
265
|
+
hint && /* @__PURE__ */ jsx(Field.Hint, {})
|
|
266
|
+
] }) });
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
TriggerParamsField.displayName = "TriggerParamsField";
|
|
270
|
+
const AddParamSelect = ({ available, onAdd, disabled }) => {
|
|
271
|
+
const [selected, setSelected] = useState(null);
|
|
272
|
+
return /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "flex-end", children: [
|
|
273
|
+
/* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
|
|
274
|
+
SingleSelect,
|
|
275
|
+
{
|
|
276
|
+
placeholder: "Выберите параметр...",
|
|
277
|
+
value: selected,
|
|
278
|
+
onChange: (val) => setSelected(val),
|
|
279
|
+
disabled,
|
|
280
|
+
size: "S",
|
|
281
|
+
children: available.map((p) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: p.key, children: [
|
|
282
|
+
p.label,
|
|
283
|
+
" — ",
|
|
284
|
+
p.description
|
|
285
|
+
] }, p.key))
|
|
286
|
+
}
|
|
287
|
+
) }),
|
|
288
|
+
/* @__PURE__ */ jsx(
|
|
289
|
+
Button,
|
|
290
|
+
{
|
|
291
|
+
startIcon: /* @__PURE__ */ jsx(Plus, {}),
|
|
292
|
+
onClick: () => {
|
|
293
|
+
if (selected) {
|
|
294
|
+
onAdd(selected);
|
|
295
|
+
setSelected(null);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
disabled: disabled || !selected,
|
|
299
|
+
variant: "secondary",
|
|
300
|
+
size: "S",
|
|
301
|
+
children: "Добавить"
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
] });
|
|
305
|
+
};
|
|
306
|
+
export {
|
|
307
|
+
TriggerParamsField as default
|
|
308
|
+
};
|
|
@@ -81,6 +81,8 @@ const ru = {
|
|
|
81
81
|
"crm-dashboard.template.title.label": "Заголовок",
|
|
82
82
|
"crm-dashboard.template.title.description": "Для email: тема письма. Для Telegram: внутренний заголовок (не виден пользователю). Для push: заголовок уведомления.",
|
|
83
83
|
"crm-dashboard.template.body.label": "Текст сообщения",
|
|
84
|
-
"crm-dashboard.template.body.description": "Текст сообщения. Переменные: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *жирный*, _курсив_"
|
|
84
|
+
"crm-dashboard.template.body.description": "Текст сообщения. Переменные: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *жирный*, _курсив_",
|
|
85
|
+
"crm-dashboard.trigger-params.label": "Параметры триггера",
|
|
86
|
+
"crm-dashboard.trigger-params.description": "Настройте параметры срабатывания для этапов popup-флоу. Добавляйте только параметры, релевантные для данного типа этапа."
|
|
85
87
|
};
|
|
86
88
|
exports.default = ru;
|
|
@@ -79,7 +79,9 @@ const ru = {
|
|
|
79
79
|
"crm-dashboard.template.title.label": "Заголовок",
|
|
80
80
|
"crm-dashboard.template.title.description": "Для email: тема письма. Для Telegram: внутренний заголовок (не виден пользователю). Для push: заголовок уведомления.",
|
|
81
81
|
"crm-dashboard.template.body.label": "Текст сообщения",
|
|
82
|
-
"crm-dashboard.template.body.description": "Текст сообщения. Переменные: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *жирный*, _курсив_"
|
|
82
|
+
"crm-dashboard.template.body.description": "Текст сообщения. Переменные: {{userName}}, {{balance}}, {{depositCount}}, {{depositTotal}}, {{amount}}. Markdown: *жирный*, _курсив_",
|
|
83
|
+
"crm-dashboard.trigger-params.label": "Параметры триггера",
|
|
84
|
+
"crm-dashboard.trigger-params.description": "Настройте параметры срабатывания для этапов popup-флоу. Добавляйте только параметры, релевантные для данного типа этапа."
|
|
83
85
|
};
|
|
84
86
|
export {
|
|
85
87
|
ru as default
|
package/dist/admin/index.js
CHANGED
|
@@ -139,6 +139,30 @@ const index = {
|
|
|
139
139
|
advanced: []
|
|
140
140
|
}
|
|
141
141
|
});
|
|
142
|
+
app.customFields.register({
|
|
143
|
+
name: "trigger-params",
|
|
144
|
+
pluginId: PLUGIN_ID,
|
|
145
|
+
type: "json",
|
|
146
|
+
intlLabel: {
|
|
147
|
+
id: `${PLUGIN_ID}.trigger-params.label`,
|
|
148
|
+
defaultMessage: "Trigger Params"
|
|
149
|
+
},
|
|
150
|
+
intlDescription: {
|
|
151
|
+
id: `${PLUGIN_ID}.trigger-params.description`,
|
|
152
|
+
defaultMessage: "Configure trigger parameters for popup flow stages"
|
|
153
|
+
},
|
|
154
|
+
icon: icons.Filter,
|
|
155
|
+
components: {
|
|
156
|
+
Input: async () => Promise.resolve().then(() => require(
|
|
157
|
+
/* webpackChunkName: "crm-trigger-params" */
|
|
158
|
+
"../_chunks/index-2rErXqfH.js"
|
|
159
|
+
))
|
|
160
|
+
},
|
|
161
|
+
options: {
|
|
162
|
+
base: [],
|
|
163
|
+
advanced: []
|
|
164
|
+
}
|
|
165
|
+
});
|
|
142
166
|
app.addMenuLink({
|
|
143
167
|
to: `/plugins/${PLUGIN_ID}`,
|
|
144
168
|
icon: icons.Message,
|
|
@@ -168,7 +192,7 @@ const index = {
|
|
|
168
192
|
return Promise.all(
|
|
169
193
|
locales.map(async (locale) => {
|
|
170
194
|
try {
|
|
171
|
-
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("../_chunks/en-
|
|
195
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("../_chunks/en-GKZo4lia.js")), "./translations/ru.json": () => Promise.resolve().then(() => require("../_chunks/ru-bj5iiIPr.js")) }), `./translations/${locale}.json`, 3);
|
|
172
196
|
return { data, locale };
|
|
173
197
|
} catch {
|
|
174
198
|
return { data: {}, locale };
|
package/dist/admin/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, Clock, Cross, Link, ArrowRight } from "@strapi/icons";
|
|
1
|
+
import { Message, Clock, Cross, Link, ArrowRight, Filter } from "@strapi/icons";
|
|
2
2
|
const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
|
|
3
3
|
const v = glob[path];
|
|
4
4
|
if (v) {
|
|
@@ -138,6 +138,30 @@ const index = {
|
|
|
138
138
|
advanced: []
|
|
139
139
|
}
|
|
140
140
|
});
|
|
141
|
+
app.customFields.register({
|
|
142
|
+
name: "trigger-params",
|
|
143
|
+
pluginId: PLUGIN_ID,
|
|
144
|
+
type: "json",
|
|
145
|
+
intlLabel: {
|
|
146
|
+
id: `${PLUGIN_ID}.trigger-params.label`,
|
|
147
|
+
defaultMessage: "Trigger Params"
|
|
148
|
+
},
|
|
149
|
+
intlDescription: {
|
|
150
|
+
id: `${PLUGIN_ID}.trigger-params.description`,
|
|
151
|
+
defaultMessage: "Configure trigger parameters for popup flow stages"
|
|
152
|
+
},
|
|
153
|
+
icon: Filter,
|
|
154
|
+
components: {
|
|
155
|
+
Input: async () => import(
|
|
156
|
+
/* webpackChunkName: "crm-trigger-params" */
|
|
157
|
+
"../_chunks/index-BMvCnlEy.mjs"
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
options: {
|
|
161
|
+
base: [],
|
|
162
|
+
advanced: []
|
|
163
|
+
}
|
|
164
|
+
});
|
|
141
165
|
app.addMenuLink({
|
|
142
166
|
to: `/plugins/${PLUGIN_ID}`,
|
|
143
167
|
icon: Message,
|
|
@@ -167,7 +191,7 @@ const index = {
|
|
|
167
191
|
return Promise.all(
|
|
168
192
|
locales.map(async (locale) => {
|
|
169
193
|
try {
|
|
170
|
-
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-
|
|
194
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-B5BgIROW.mjs"), "./translations/ru.json": () => import("../_chunks/ru-h22ZdPCS.mjs") }), `./translations/${locale}.json`, 3);
|
|
171
195
|
return { data, locale };
|
|
172
196
|
} catch {
|
|
173
197
|
return { data: {}, locale };
|
package/dist/server/index.js
CHANGED
package/dist/server/index.mjs
CHANGED
package/package.json
CHANGED
package/server/src/register.ts
CHANGED
|
@@ -38,6 +38,12 @@ const register = ({ strapi }: { strapi: Core.Strapi }) => {
|
|
|
38
38
|
plugin: 'crm-dashboard',
|
|
39
39
|
type: 'json',
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
strapi.customFields.register({
|
|
43
|
+
name: 'trigger-params',
|
|
44
|
+
plugin: 'crm-dashboard',
|
|
45
|
+
type: 'json',
|
|
46
|
+
});
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
export default register;
|