@inspirer-dev/crm-dashboard 1.0.11 → 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/index.ts +26 -1
- package/dist/_chunks/{index-CNvqpnNt.mjs → index-BSDO36Pf.mjs} +1 -468
- package/dist/_chunks/index-CiwOzO0B.js +377 -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 +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
- package/dist/_chunks/index-DIwnSzER.js +0 -844
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
Field,
|
|
7
|
+
Flex,
|
|
8
|
+
IconButton,
|
|
9
|
+
TextInput,
|
|
10
|
+
Typography,
|
|
11
|
+
Tooltip,
|
|
12
|
+
} from '@strapi/design-system';
|
|
13
|
+
import { Plus, Trash, ArrowUp, ArrowDown } from '@strapi/icons';
|
|
14
|
+
import { generateId } from '../RulesBuilder/utils';
|
|
15
|
+
|
|
16
|
+
type TelegramButton = {
|
|
17
|
+
id: string;
|
|
18
|
+
text: string;
|
|
19
|
+
url: string;
|
|
20
|
+
row?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface ButtonsBuilderProps {
|
|
24
|
+
name: string;
|
|
25
|
+
value?: string | null;
|
|
26
|
+
onChange: (event: { target: { name: string; value: string } }) => void;
|
|
27
|
+
intlLabel: {
|
|
28
|
+
id: string;
|
|
29
|
+
defaultMessage: string;
|
|
30
|
+
};
|
|
31
|
+
attribute?: unknown;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
error?: string;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
hint?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parseButtons = (value: string | null | undefined): TelegramButton[] => {
|
|
39
|
+
if (!value) return [];
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(value);
|
|
42
|
+
if (!Array.isArray(parsed)) return [];
|
|
43
|
+
return parsed.map((b) => ({
|
|
44
|
+
id: typeof b?.id === 'string' ? b.id : generateId(),
|
|
45
|
+
text: typeof b?.text === 'string' ? b.text : '',
|
|
46
|
+
url: typeof b?.url === 'string' ? b.url : '',
|
|
47
|
+
row: typeof b?.row === 'number' ? b.row : 0,
|
|
48
|
+
}));
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const serializeButtons = (buttons: TelegramButton[]): string => JSON.stringify(buttons);
|
|
55
|
+
|
|
56
|
+
const isValidUrl = (url: string): boolean => {
|
|
57
|
+
try {
|
|
58
|
+
const u = new URL(url);
|
|
59
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const ButtonsBuilder = forwardRef<HTMLDivElement, ButtonsBuilderProps>(
|
|
66
|
+
({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
|
|
67
|
+
const [buttons, setButtons] = useState<TelegramButton[]>(() => parseButtons(value));
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
setButtons(parseButtons(value));
|
|
71
|
+
}, [value]);
|
|
72
|
+
|
|
73
|
+
const update = (next: TelegramButton[]) => {
|
|
74
|
+
setButtons(next);
|
|
75
|
+
onChange({ target: { name, value: serializeButtons(next) } });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const addButton = () => {
|
|
79
|
+
update([
|
|
80
|
+
...buttons,
|
|
81
|
+
{ id: generateId(), text: 'Button', url: 'https://cases.gg', row: 0 },
|
|
82
|
+
]);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const updateButton = (id: string, patch: Partial<TelegramButton>) => {
|
|
86
|
+
update(buttons.map((b) => (b.id === id ? { ...b, ...patch } : b)));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const deleteButton = (id: string) => {
|
|
90
|
+
update(buttons.filter((b) => b.id !== id));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const move = (from: number, to: number) => {
|
|
94
|
+
if (to < 0 || to >= buttons.length) return;
|
|
95
|
+
const copy = [...buttons];
|
|
96
|
+
const [item] = copy.splice(from, 1);
|
|
97
|
+
copy.splice(to, 0, item);
|
|
98
|
+
update(copy);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const previewRows = useMemo(() => {
|
|
102
|
+
const rows = new Map<number, TelegramButton[]>();
|
|
103
|
+
for (const b of buttons) {
|
|
104
|
+
const row = b.row ?? 0;
|
|
105
|
+
rows.set(row, [...(rows.get(row) || []), b]);
|
|
106
|
+
}
|
|
107
|
+
return Array.from(rows.entries())
|
|
108
|
+
.sort((a, b) => a[0] - b[0])
|
|
109
|
+
.map(([row, items]) => ({ row, items }));
|
|
110
|
+
}, [buttons]);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Field.Root name={name} error={error} required={required} hint={hint} ref={ref}>
|
|
114
|
+
<Flex direction="column" gap={3}>
|
|
115
|
+
<Field.Label>{intlLabel?.defaultMessage || 'Buttons'}</Field.Label>
|
|
116
|
+
<Typography variant="pi" textColor="neutral600">
|
|
117
|
+
Build Telegram inline keyboard buttons (text + URL). Stored as JSON.
|
|
118
|
+
</Typography>
|
|
119
|
+
|
|
120
|
+
<Flex direction="column" gap={2}>
|
|
121
|
+
{buttons.map((btn, idx) => {
|
|
122
|
+
const urlOk = btn.url.length === 0 ? true : isValidUrl(btn.url);
|
|
123
|
+
return (
|
|
124
|
+
<Flex
|
|
125
|
+
key={btn.id}
|
|
126
|
+
gap={2}
|
|
127
|
+
alignItems="flex-start"
|
|
128
|
+
padding={3}
|
|
129
|
+
background="neutral0"
|
|
130
|
+
hasRadius
|
|
131
|
+
style={{ border: '1px solid #dcdce4' }}
|
|
132
|
+
>
|
|
133
|
+
<Box style={{ flex: 2, minWidth: 180 }}>
|
|
134
|
+
<Field.Root name={`${name}.${btn.id}.text`}>
|
|
135
|
+
<Field.Label>Text</Field.Label>
|
|
136
|
+
<TextInput
|
|
137
|
+
value={btn.text}
|
|
138
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
139
|
+
updateButton(btn.id, { text: e.target.value })
|
|
140
|
+
}
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
/>
|
|
143
|
+
</Field.Root>
|
|
144
|
+
</Box>
|
|
145
|
+
|
|
146
|
+
<Box style={{ flex: 3, minWidth: 220 }}>
|
|
147
|
+
<Field.Root name={`${name}.${btn.id}.url`}>
|
|
148
|
+
<Field.Label>URL</Field.Label>
|
|
149
|
+
<TextInput
|
|
150
|
+
value={btn.url}
|
|
151
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
152
|
+
updateButton(btn.id, { url: e.target.value })
|
|
153
|
+
}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
/>
|
|
156
|
+
{!urlOk && (
|
|
157
|
+
<Typography variant="pi" textColor="danger600" style={{ marginTop: 4 }}>
|
|
158
|
+
Please enter a valid http/https URL
|
|
159
|
+
</Typography>
|
|
160
|
+
)}
|
|
161
|
+
</Field.Root>
|
|
162
|
+
</Box>
|
|
163
|
+
|
|
164
|
+
<Box style={{ width: 90 }}>
|
|
165
|
+
<Field.Root name={`${name}.${btn.id}.row`}>
|
|
166
|
+
<Field.Label>Row</Field.Label>
|
|
167
|
+
<TextInput
|
|
168
|
+
type="number"
|
|
169
|
+
value={String(btn.row ?? 0)}
|
|
170
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
171
|
+
updateButton(btn.id, { row: parseInt(e.target.value, 10) || 0 })
|
|
172
|
+
}
|
|
173
|
+
disabled={disabled}
|
|
174
|
+
/>
|
|
175
|
+
</Field.Root>
|
|
176
|
+
</Box>
|
|
177
|
+
|
|
178
|
+
<Flex direction="column" gap={1} paddingTop={6}>
|
|
179
|
+
<Tooltip label="Move up">
|
|
180
|
+
<IconButton
|
|
181
|
+
onClick={() => move(idx, idx - 1)}
|
|
182
|
+
label="Move up"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
disabled={disabled || idx === 0}
|
|
185
|
+
>
|
|
186
|
+
<ArrowUp />
|
|
187
|
+
</IconButton>
|
|
188
|
+
</Tooltip>
|
|
189
|
+
<Tooltip label="Move down">
|
|
190
|
+
<IconButton
|
|
191
|
+
onClick={() => move(idx, idx + 1)}
|
|
192
|
+
label="Move down"
|
|
193
|
+
variant="ghost"
|
|
194
|
+
disabled={disabled || idx === buttons.length - 1}
|
|
195
|
+
>
|
|
196
|
+
<ArrowDown />
|
|
197
|
+
</IconButton>
|
|
198
|
+
</Tooltip>
|
|
199
|
+
<Tooltip label="Delete button">
|
|
200
|
+
<IconButton
|
|
201
|
+
onClick={() => deleteButton(btn.id)}
|
|
202
|
+
label="Delete button"
|
|
203
|
+
variant="ghost"
|
|
204
|
+
disabled={disabled}
|
|
205
|
+
>
|
|
206
|
+
<Trash />
|
|
207
|
+
</IconButton>
|
|
208
|
+
</Tooltip>
|
|
209
|
+
</Flex>
|
|
210
|
+
</Flex>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
|
|
214
|
+
{buttons.length === 0 && (
|
|
215
|
+
<Box padding={4} background="neutral0" hasRadius style={{ border: '1px dashed #dcdce4' }}>
|
|
216
|
+
<Typography variant="omega" textColor="neutral600">
|
|
217
|
+
No buttons. Add one to create an inline keyboard.
|
|
218
|
+
</Typography>
|
|
219
|
+
</Box>
|
|
220
|
+
)}
|
|
221
|
+
</Flex>
|
|
222
|
+
|
|
223
|
+
<Flex gap={2}>
|
|
224
|
+
<Button startIcon={<Plus />} onClick={addButton} disabled={disabled} variant="secondary">
|
|
225
|
+
Add button
|
|
226
|
+
</Button>
|
|
227
|
+
</Flex>
|
|
228
|
+
|
|
229
|
+
{buttons.length > 0 && (
|
|
230
|
+
<Box paddingTop={2}>
|
|
231
|
+
<Typography variant="pi" textColor="neutral600" style={{ marginBottom: 8, display: 'block' }}>
|
|
232
|
+
Preview
|
|
233
|
+
</Typography>
|
|
234
|
+
<Flex direction="column" gap={2}>
|
|
235
|
+
{previewRows.map((row) => (
|
|
236
|
+
<Flex key={row.row} gap={1} wrap="wrap">
|
|
237
|
+
{row.items.map((b) => (
|
|
238
|
+
<Badge key={b.id} backgroundColor="primary100">
|
|
239
|
+
{b.text || '(empty)'}
|
|
240
|
+
</Badge>
|
|
241
|
+
))}
|
|
242
|
+
</Flex>
|
|
243
|
+
))}
|
|
244
|
+
</Flex>
|
|
245
|
+
</Box>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{error && <Field.Error>{error}</Field.Error>}
|
|
249
|
+
{hint && <Field.Hint>{hint}</Field.Hint>}
|
|
250
|
+
</Flex>
|
|
251
|
+
</Field.Root>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
ButtonsBuilder.displayName = 'ButtonsBuilder';
|
|
257
|
+
|
|
258
|
+
export default ButtonsBuilder;
|
|
259
|
+
|
package/admin/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, Clock, Cross } from '@strapi/icons';
|
|
1
|
+
import { Message, Clock, Cross, Link } from '@strapi/icons';
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = 'crm-dashboard';
|
|
4
4
|
|
|
@@ -79,6 +79,31 @@ export default {
|
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
app.customFields.register({
|
|
83
|
+
name: 'telegram-buttons',
|
|
84
|
+
pluginId: PLUGIN_ID,
|
|
85
|
+
type: 'json',
|
|
86
|
+
intlLabel: {
|
|
87
|
+
id: `${PLUGIN_ID}.telegram-buttons.label`,
|
|
88
|
+
defaultMessage: 'Buttons',
|
|
89
|
+
},
|
|
90
|
+
intlDescription: {
|
|
91
|
+
id: `${PLUGIN_ID}.telegram-buttons.description`,
|
|
92
|
+
defaultMessage: 'Build Telegram inline buttons (text + URL)',
|
|
93
|
+
},
|
|
94
|
+
icon: Link,
|
|
95
|
+
components: {
|
|
96
|
+
Input: async () =>
|
|
97
|
+
import(
|
|
98
|
+
/* webpackChunkName: "crm-telegram-buttons" */ './components/ButtonsBuilder/index'
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
options: {
|
|
102
|
+
base: [],
|
|
103
|
+
advanced: [],
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
82
107
|
app.addMenuLink({
|
|
83
108
|
to: `/plugins/${PLUGIN_ID}`,
|
|
84
109
|
icon: Message,
|