@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.
@@ -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
+
@@ -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,