data-primals-engine 1.1.1 → 1.1.3
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/.github/workflows/node.js.yml +2 -7
- package/README.md +2 -2
- package/client/src/App.jsx +33 -16
- package/client/src/App.scss +21 -0
- package/client/src/Dashboard.scss +5 -1
- package/client/src/DashboardFlexViewItem.jsx +1 -2
- package/client/src/DisplayFlexNodeRenderer.jsx +120 -4
- package/client/src/FlexBuilder.jsx +2 -2
- package/client/src/FlexBuilder.scss +5 -0
- package/client/src/FlexBuilderControls.jsx +201 -22
- package/client/src/FlexBuilderPreview.jsx +43 -1
- package/client/src/FlexNode.jsx +93 -5
- package/client/src/FlexTreeUtils.js +42 -11
- package/client/src/ModelList.jsx +33 -20
- package/client/src/constants.js +4 -4
- package/package.json +3 -3
- package/server.js +1 -7
- package/src/constants.js +165 -16
- package/src/defaultModels.js +49 -0
- package/src/email.js +1 -1
- package/src/engine.js +17 -0
- package/src/modules/data.js +146 -13
- package/src/modules/workflow.js +86 -56
- package/test/data.integration.test.js +2 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import {FaTrash, FaBox, FaExpandArrowsAlt, FaObjectGroup, FaEdit, FaFileAlt, FaPlay} from 'react-icons/fa';
|
|
5
|
+
import {CodeField, NumberField, SelectField, TextField} from './Field.jsx';
|
|
6
6
|
import ConditionBuilder from "./ConditionBuilder.jsx";
|
|
7
|
+
import {useQuery} from "react-query";
|
|
7
8
|
|
|
8
9
|
const FlexBuilderControls = ({
|
|
9
10
|
flexStructure,
|
|
@@ -24,6 +25,60 @@ const FlexBuilderControls = ({
|
|
|
24
25
|
}) => {
|
|
25
26
|
const { t } = useTranslation();
|
|
26
27
|
|
|
28
|
+
// 1. Récupérer la liste des endpoints GET disponibles pour les lier au bouton
|
|
29
|
+
const { data: availableEndpoints } = useQuery(
|
|
30
|
+
['endpoints', 'GET'], // Clé de requête
|
|
31
|
+
() => fetch('/api/data/search', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
model: 'endpoint',
|
|
36
|
+
filter: { method: 'GET', isActive: true }
|
|
37
|
+
})
|
|
38
|
+
}).then(res => res.json()),
|
|
39
|
+
{
|
|
40
|
+
select: (response) => response.data || [], // On ne garde que le tableau de données
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// 2. Fonction pour ajouter un nouveau nœud de type CTA
|
|
45
|
+
const handleAddCtaNode = () => {
|
|
46
|
+
const newNode = {
|
|
47
|
+
id: `cta-${Date.now()}`, // ID unique
|
|
48
|
+
type: 'cta',
|
|
49
|
+
label: 'Exécuter', // Label par défaut
|
|
50
|
+
endpointPath: '', // Chemin à configurer
|
|
51
|
+
// Ajoutez d'autres propriétés de style si nécessaire
|
|
52
|
+
};
|
|
53
|
+
// Logique pour ajouter ce nœud à la structure flex...
|
|
54
|
+
// onChange(updatedStructure);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// 3. Dans le panneau de configuration (quand un nœud CTA est sélectionné)
|
|
58
|
+
const renderCtaNodeEditor = (selectedNode) => {
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
<h4>Configurer le Bouton d'Action</h4>
|
|
62
|
+
<TextField
|
|
63
|
+
label="Texte du bouton"
|
|
64
|
+
value={selectedNode.label}
|
|
65
|
+
onChange={(e) => updateNode(selectedNode.id, { label: e.target.value })}
|
|
66
|
+
/>
|
|
67
|
+
<SelectField
|
|
68
|
+
label="Endpoint à appeler"
|
|
69
|
+
value={selectedNode.endpointPath}
|
|
70
|
+
onChange={(e) => updateNode(selectedNode.id, { endpointPath: e.target.value })}
|
|
71
|
+
>
|
|
72
|
+
<option value="">-- Sélectionner un endpoint --</option>
|
|
73
|
+
{(availableEndpoints || []).map(ep => (
|
|
74
|
+
<option key={ep._id} value={ep.path}>
|
|
75
|
+
{ep.name} (/api/actions/{ep.path})
|
|
76
|
+
</option>
|
|
77
|
+
))}
|
|
78
|
+
</SelectField>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
27
82
|
const handleDataLimitChange = (e) => {
|
|
28
83
|
let value = parseInt(e.target.value, 10);
|
|
29
84
|
if (isNaN(value) || value < 1) value = 1;
|
|
@@ -45,7 +100,10 @@ const FlexBuilderControls = ({
|
|
|
45
100
|
id="selectedModel"
|
|
46
101
|
value={flexStructure.selectedModelName}
|
|
47
102
|
onChange={(selected) => onModelChange(selected.value)}
|
|
48
|
-
items={[{
|
|
103
|
+
items={[{
|
|
104
|
+
value: null,
|
|
105
|
+
label: t('flexBuilder.selectAModel', 'Sélectionner un modèle...')
|
|
106
|
+
}, ...modelOptions]}
|
|
49
107
|
/>
|
|
50
108
|
</div>
|
|
51
109
|
{flexStructure.selectedModelName && (
|
|
@@ -83,30 +141,119 @@ const FlexBuilderControls = ({
|
|
|
83
141
|
</>
|
|
84
142
|
)}
|
|
85
143
|
|
|
144
|
+
|
|
145
|
+
|
|
86
146
|
{/* Section des paramètres du nœud sélectionné */}
|
|
87
147
|
{selectedNode && (
|
|
88
148
|
<div className="selected-node-controls">
|
|
89
149
|
<h4>
|
|
90
150
|
{t('flexBuilder.selectedNodeSettings', 'Paramètres')}: {t(selectedNode.type)} ({selectedNode.id.substring(0, 8)})
|
|
91
151
|
{selectedNode.id !== flexStructure.id &&
|
|
92
|
-
<button onClick={onDeleteNode} className="btn-delete-node"
|
|
152
|
+
<button onClick={onDeleteNode} className="btn-delete-node"
|
|
153
|
+
title={t('flexBuilder.deleteThisNode', 'Supprimer cet élément')}><FaTrash/></button>
|
|
93
154
|
}
|
|
94
155
|
</h4>
|
|
156
|
+
{selectedNode.type === 'cta' && (
|
|
157
|
+
<>
|
|
158
|
+
<p>{t('flexBuilder.ctaControlsTitle', 'Propriétés du Bouton d\'Action:')}</p>
|
|
159
|
+
<TextField
|
|
160
|
+
label={t('flexBuilder.ctaLabel', 'Texte du bouton')}
|
|
161
|
+
value={selectedNode.label || ''}
|
|
162
|
+
onChange={(e) => onUpdateNode(selectedNode.id, 'label', e.target.value)}
|
|
163
|
+
/>
|
|
164
|
+
<SelectField
|
|
165
|
+
label={t('flexBuilder.ctaEndpoint', 'Endpoint à appeler')}
|
|
166
|
+
value={selectedNode.endpointPath || ''}
|
|
167
|
+
onChange={(sel) => onUpdateNode(selectedNode.id, 'endpointPath', sel.value)}
|
|
168
|
+
items={[
|
|
169
|
+
{value: '', label: t('flexBuilder.selectAnEndpoint', 'Select an endpoint')},
|
|
170
|
+
...availableEndpoints.map(ep => ({
|
|
171
|
+
value: ep.path,
|
|
172
|
+
label: `${ep.name} (/api/actions/${ep.path})`
|
|
173
|
+
}))
|
|
174
|
+
]}
|
|
175
|
+
/>
|
|
176
|
+
<hr className="my-4 border-gray-600"/>
|
|
177
|
+
<p>{t('flexBuilder.ctaRequestConfig', 'Configuration de la requête')}</p>
|
|
178
|
+
<SelectField
|
|
179
|
+
label={t('flexBuilder.httpMethod', 'Méthode HTTP')}
|
|
180
|
+
value={selectedNode.httpMethod || 'GET'}
|
|
181
|
+
onChange={(sel) => onUpdateNode(selectedNode.id, 'httpMethod', sel.value)}
|
|
182
|
+
items={['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map(m => ({value: m, label: m}))}
|
|
183
|
+
/>
|
|
95
184
|
|
|
185
|
+
{/* Affiche l'éditeur de corps de requête pour les méthodes appropriées */}
|
|
186
|
+
{(selectedNode.httpMethod === 'POST' || selectedNode.httpMethod === 'PUT' || selectedNode.httpMethod === 'PATCH') && (
|
|
187
|
+
<div className="form-group">
|
|
188
|
+
<label>{t('flexBuilder.requestBody', 'Corps de la requête (JSON)')}</label>
|
|
189
|
+
<CodeField
|
|
190
|
+
language="json"
|
|
191
|
+
value={selectedNode.requestBodyTemplate || ''}
|
|
192
|
+
onChange={(e) => onUpdateNode(selectedNode.id, 'requestBodyTemplate', e.value)}
|
|
193
|
+
placeholder={`{\n "productId": "{{_id}}",\n "name": "{{name}}"\n}`}
|
|
194
|
+
rows={5}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
<div className="form-group">
|
|
199
|
+
<label>{t('flexBuilder.requestQuery', 'Paramètres de requête (JSON)')}</label>
|
|
200
|
+
<CodeField
|
|
201
|
+
language="json"
|
|
202
|
+
value={selectedNode.requestQueryTemplate || ''}
|
|
203
|
+
onChange={(e) => onUpdateNode(selectedNode.id, 'requestQueryTemplate', e.value)}
|
|
204
|
+
placeholder={`{\n "source": "dashboard",\n "userId": "{{user._id}}"\n}`}
|
|
205
|
+
rows={4}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
<small
|
|
209
|
+
className="block my-2 text-xs text-gray-500">{t('flexBuilder.usePlaceholders', 'Utilisez "{{fieldName}}" pour insérer des données de l\'élément courant. Le champ doit être une chaîne de caractères.')}</small>
|
|
210
|
+
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
96
213
|
{/* Contrôles pour un conteneur */}
|
|
97
214
|
{selectedNode.type === 'container' && (
|
|
98
215
|
<>
|
|
99
216
|
<p>{t('flexBuilder.containerControlsTitle', 'Propriétés du conteneur Flex:')}</p>
|
|
100
217
|
<div className="control-group actions">
|
|
101
|
-
<button onClick={() => onAddChildNode(selectedNode.id, 'item')}
|
|
102
|
-
|
|
218
|
+
<button onClick={() => onAddChildNode(selectedNode.id, 'item')}>
|
|
219
|
+
<FaBox/> {t('flexBuilder.addItem', 'Ajouter une case')}</button>
|
|
220
|
+
<button onClick={() => onAddChildNode(selectedNode.id, 'container')}>
|
|
221
|
+
<FaExpandArrowsAlt/> {t('flexBuilder.addNestedContainer', 'Ajouter un conteneur')}
|
|
222
|
+
</button>
|
|
223
|
+
<button onClick={() => onAddChildNode(selectedNode.id, 'cta')}>
|
|
224
|
+
<FaPlay/> {t('flexBuilder.addCtaButton', 'Ajouter un Bouton CTA')}
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="control-group"><label>{t('flexDirection')}:</label><SelectField
|
|
228
|
+
items={flexOptions.flexDirection.map(o => ({label: t(o, o), value: o}))}
|
|
229
|
+
value={selectedNode.containerStyle.flexDirection}
|
|
230
|
+
onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.flexDirection', sel.value)}/>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="control-group"><label>{t('flexWrap')}:</label><SelectField
|
|
233
|
+
items={flexOptions.flexWrap.map(o => ({label: t(o, o), value: o}))}
|
|
234
|
+
value={selectedNode.containerStyle.flexWrap}
|
|
235
|
+
onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.flexWrap', sel.value)}/>
|
|
236
|
+
</div>
|
|
237
|
+
<div className="control-group"><label>{t('justifyContent')}:</label><SelectField
|
|
238
|
+
items={flexOptions.justifyContent.map(o => ({label: t(o, o), value: o}))}
|
|
239
|
+
value={selectedNode.containerStyle.justifyContent}
|
|
240
|
+
onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.justifyContent', sel.value)}/>
|
|
241
|
+
</div>
|
|
242
|
+
<div className="control-group"><label>{t('alignItems')}:</label><SelectField
|
|
243
|
+
items={flexOptions.alignItems.map(o => ({label: t(o, o), value: o}))}
|
|
244
|
+
value={selectedNode.containerStyle.alignItems}
|
|
245
|
+
onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.alignItems', sel.value)}/>
|
|
103
246
|
</div>
|
|
104
|
-
<div className="control-group"><label>{t('
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<div className="form-group"><CodeField id="customCss" name="customCss" language="css"
|
|
247
|
+
<div className="control-group"><label>{t('gap')}:</label><input type="text"
|
|
248
|
+
value={selectedNode.containerStyle.gap || ''}
|
|
249
|
+
onChange={e => onUpdateNode(selectedNode.id, 'containerStyle.gap', e.target.value)}
|
|
250
|
+
placeholder="ex: 10px"/>
|
|
251
|
+
</div>
|
|
252
|
+
<div className="form-group"><CodeField id="customCss" name="customCss" language="css"
|
|
253
|
+
value={selectedNode.containerStyle.customCss || ''}
|
|
254
|
+
onChange={(e) => onUpdateNode(selectedNode.id, 'containerStyle.customCss', e.value)}
|
|
255
|
+
placeholder={t('flexBuilder.customCssPlaceholder', 'Ex: background-color: red;')}
|
|
256
|
+
rows={4}/></div>
|
|
110
257
|
</>
|
|
111
258
|
)}
|
|
112
259
|
|
|
@@ -117,27 +264,59 @@ const FlexBuilderControls = ({
|
|
|
117
264
|
<div className="control-group actions">
|
|
118
265
|
{flexStructure.selectedModelName && (
|
|
119
266
|
<button onClick={() => onSetIsFieldSelectorOpen(true)}>
|
|
120
|
-
<FaEdit
|
|
267
|
+
<FaEdit/> {selectedNode.content.type === 'dataField' ? t('flexBuilder.editDataMapping', 'Modifier le champ lié') : t('flexBuilder.mapDataField', 'Lier un champ de données')}
|
|
121
268
|
</button>
|
|
122
269
|
)}
|
|
123
270
|
<button onClick={(e) => {
|
|
124
|
-
onSetEditingHtmlInfo({
|
|
271
|
+
onSetEditingHtmlInfo({
|
|
272
|
+
nodeId: selectedNode.id,
|
|
273
|
+
initialContent: selectedNode.content.html || ''
|
|
274
|
+
});
|
|
125
275
|
e.stopPropagation()
|
|
126
276
|
}}>
|
|
127
|
-
<FaFileAlt
|
|
277
|
+
<FaFileAlt/> {t('flexBuilder.editHTML', 'Éditer le contenu')}
|
|
128
278
|
</button>
|
|
129
279
|
{selectedNode.content.type !== 'nestedContainer' && (
|
|
130
280
|
<button onClick={() => onMakeItemNestedContainer(selectedNode.id)}>
|
|
131
|
-
<FaObjectGroup
|
|
281
|
+
<FaObjectGroup/> {t('flexBuilder.makeNestedContainer', 'Transformer en conteneur')}
|
|
132
282
|
</button>
|
|
133
283
|
)}
|
|
134
284
|
</div>
|
|
135
|
-
<div className="control-group"><label>{t('itemWidth')}:</label><input type="text"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<div className="
|
|
285
|
+
<div className="control-group"><label>{t('itemWidth')}:</label><input type="text"
|
|
286
|
+
value={selectedNode.itemStyle.width || ''}
|
|
287
|
+
onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.width', e.target.value)}
|
|
288
|
+
placeholder="ex: 100px, 20%"/>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="control-group"><label>{t('itemHeight')}:</label><input type="text"
|
|
291
|
+
value={selectedNode.itemStyle.height || ''}
|
|
292
|
+
onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.height', e.target.value)}
|
|
293
|
+
placeholder="ex: 50px, auto"/>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="control-group">
|
|
296
|
+
<label>{t('flexBuilder.itemFlexGrow', 'Flex Grow')}:</label><input type="number"
|
|
297
|
+
value={selectedNode.itemStyle.flexGrow || 0}
|
|
298
|
+
onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexGrow', parseFloat(e.target.value) || 0)}
|
|
299
|
+
min="0" step="0.1"/>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="control-group">
|
|
302
|
+
<label>{t('flexBuilder.itemFlexShrink', 'Flex Shrink')}:</label><input type="number"
|
|
303
|
+
value={selectedNode.itemStyle.flexShrink === undefined ? 1 : selectedNode.itemStyle.flexShrink}
|
|
304
|
+
onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexShrink', parseFloat(e.target.value))}
|
|
305
|
+
min="0"
|
|
306
|
+
step="0.1"/>
|
|
307
|
+
</div>
|
|
308
|
+
<div className="control-group">
|
|
309
|
+
<label>{t('flexBuilder.itemFlexBasis', 'Flex Basis')}:</label><input type="text"
|
|
310
|
+
value={selectedNode.itemStyle.flexBasis || 'auto'}
|
|
311
|
+
onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexBasis', e.target.value)}
|
|
312
|
+
placeholder="auto, 100px, 20%"/>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="form-group"><CodeField id="customCssItem" name="customCssItem"
|
|
315
|
+
language="css"
|
|
316
|
+
value={selectedNode.itemStyle.customCss || ''}
|
|
317
|
+
onChange={(e) => onUpdateNode(selectedNode.id, 'itemStyle.customCss', e.value)}
|
|
318
|
+
placeholder={t('flexBuilder.customCssPlaceholder', 'Ex: background-color: blue;')}
|
|
319
|
+
rows={4}/></div>
|
|
141
320
|
</>
|
|
142
321
|
)}
|
|
143
322
|
</div>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, {useState} from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { FlexNodeRenderer } from "./FlexNode.jsx";
|
|
5
|
+
import {Dialog, DialogProvider} from "./Dialog.jsx";
|
|
5
6
|
|
|
6
7
|
const FlexBuilderPreview = ({
|
|
7
8
|
flexStructure,
|
|
@@ -14,10 +15,50 @@ const FlexBuilderPreview = ({
|
|
|
14
15
|
onMoveNode
|
|
15
16
|
}) => {
|
|
16
17
|
const { t } = useTranslation();
|
|
18
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
19
|
+
const [modalContent, setModalContent] = useState(null);
|
|
20
|
+
const handleCtaClick = async (node) => {
|
|
21
|
+
if (!node.endpointPath) {
|
|
22
|
+
alert("Cet endpoint n'est pas configuré.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// On prépare le contenu du modal avant même l'appel
|
|
27
|
+
setModalContent({ isLoading: true, path: node.endpointPath });
|
|
28
|
+
setIsModalOpen(true);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(`/api/actions/${node.endpointPath}`, { method: 'GET' });
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
setModalContent({ path: node.endpointPath, status: response.status, data });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
setModalContent({ path: node.endpointPath, error: error.message });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
17
38
|
|
|
18
39
|
return (
|
|
19
40
|
<div className="flex-builder-preview-area">
|
|
20
41
|
<h3>{t('preview', 'Aperçu')}</h3>
|
|
42
|
+
<DialogProvider>
|
|
43
|
+
{isModalOpen && (
|
|
44
|
+
<Dialog
|
|
45
|
+
isOpen={isModalOpen}
|
|
46
|
+
onClose={() => setIsModalOpen(false)}
|
|
47
|
+
title={`Résultat de /api/actions/${modalContent?.path}`}
|
|
48
|
+
>
|
|
49
|
+
<div className="p-4 bg-gray-900 text-white rounded-md">
|
|
50
|
+
<pre>
|
|
51
|
+
<code>
|
|
52
|
+
{modalContent?.isLoading
|
|
53
|
+
? 'Chargement...'
|
|
54
|
+
: JSON.stringify(modalContent, null, 2)
|
|
55
|
+
}
|
|
56
|
+
</code>
|
|
57
|
+
</pre>
|
|
58
|
+
</div>
|
|
59
|
+
</Dialog>
|
|
60
|
+
)}
|
|
61
|
+
</DialogProvider>
|
|
21
62
|
<FlexNodeRenderer
|
|
22
63
|
node={flexStructure}
|
|
23
64
|
path={[]}
|
|
@@ -29,6 +70,7 @@ const FlexBuilderPreview = ({
|
|
|
29
70
|
modelFields={modelFields}
|
|
30
71
|
dnd={dnd}
|
|
31
72
|
onMoveNode={onMoveNode}
|
|
73
|
+
onCtaClick={handleCtaClick}
|
|
32
74
|
/>
|
|
33
75
|
</div>
|
|
34
76
|
);
|
package/client/src/FlexNode.jsx
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// C:/Dev/hackersonline-engine/client/src/FlexNode.jsx
|
|
2
|
-
import React, {
|
|
2
|
+
import React, {useEffect, useRef, useState} from 'react';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FaPlus,
|
|
6
|
+
FaTrash,
|
|
7
|
+
FaEdit,
|
|
8
|
+
FaRegSquare,
|
|
9
|
+
FaRegCheckSquare,
|
|
10
|
+
FaBox,
|
|
11
|
+
FaExpandArrowsAlt,
|
|
12
|
+
FaObjectGroup,
|
|
13
|
+
FaPlay
|
|
14
|
+
} from 'react-icons/fa';
|
|
5
15
|
// Remove Droppable, Draggable from '@hello-pangea/dnd' if no longer used
|
|
6
16
|
import { useTranslation } from 'react-i18next';
|
|
7
17
|
// Supprimer l'import de useDragAndDrop s'il n'est plus utilisé localement
|
|
@@ -55,11 +65,45 @@ FieldSelectorModal.propTypes = {
|
|
|
55
65
|
};
|
|
56
66
|
|
|
57
67
|
export const FlexNodeRenderer = React.forwardRef((props, ref) => {
|
|
68
|
+
|
|
58
69
|
const {
|
|
59
|
-
node,
|
|
60
|
-
|
|
70
|
+
node,
|
|
71
|
+
path,
|
|
72
|
+
onSelectNode,
|
|
73
|
+
selectedNodeId,
|
|
74
|
+
data,
|
|
75
|
+
dataIndexRef,
|
|
76
|
+
modelFields,
|
|
77
|
+
t,
|
|
78
|
+
onMoveNode,
|
|
79
|
+
dnd,
|
|
80
|
+
onCtaClick, // <-- On récupère la fonction du parent
|
|
61
81
|
} = props;
|
|
62
82
|
|
|
83
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
84
|
+
const [result, setResult] = useState(null);
|
|
85
|
+
|
|
86
|
+
// --- NOUVELLE FONCTION POUR EXÉCUTER LE CTA ---
|
|
87
|
+
const handleCtaClick = async (endpointPath) => {
|
|
88
|
+
if (!endpointPath) {
|
|
89
|
+
alert("Cet endpoint n'est pas configuré.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
setIsLoading(true);
|
|
93
|
+
setResult(null);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(`/api/actions/${endpointPath}`, { method: 'GET' });
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
setResult({ status: response.status, data });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
setResult({ error: error.message });
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
|
|
63
107
|
// Déstructurer les valeurs et gestionnaires du prop dnd
|
|
64
108
|
const {
|
|
65
109
|
isDragging, draggedItemRef, dragOverContainerId,
|
|
@@ -141,7 +185,29 @@ export const FlexNodeRenderer = React.forwardRef((props, ref) => {
|
|
|
141
185
|
// console.log(`FlexNode (${node.id} - ${node.type}): onMoveNode type:`, typeof onMoveNode);
|
|
142
186
|
// }, [onMoveNode, node.id, node.type]);
|
|
143
187
|
|
|
144
|
-
|
|
188
|
+
if (node.type === 'cta') {
|
|
189
|
+
return (
|
|
190
|
+
<div
|
|
191
|
+
ref={ref} // Important pour le D&D
|
|
192
|
+
className={`flex-node-wrapper ${dnd.isDragging && dnd.draggedItemRef.current === node.id ? 'is-dragging-dnd' : ''}`}
|
|
193
|
+
draggable
|
|
194
|
+
onDragStart={(e) => dnd.handleNodeDragStart(node.id)}
|
|
195
|
+
onDragEnd={dnd.handleItemDragEnd}
|
|
196
|
+
onClick={(e) => { e.stopPropagation(); props.onSelectNode(node.id); }}
|
|
197
|
+
>
|
|
198
|
+
<div className={`flex-node flex-node-cta ${props.selectedNodeId === node.id ? 'is-selected' : ''}`}>
|
|
199
|
+
<button
|
|
200
|
+
className="btn btn-primary btn-sm"
|
|
201
|
+
// On appelle la fonction du parent
|
|
202
|
+
onClick={(e) => { e.stopPropagation(); onCtaClick(node); }}
|
|
203
|
+
>
|
|
204
|
+
<FaPlay className="mr-2" />
|
|
205
|
+
{node.label || 'Execute'}
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
145
211
|
if (node.type === 'container' || (node.type === 'item' && node.content?.type === 'nestedContainer')) {
|
|
146
212
|
const containerNode = node.type === 'container' ? node : node.content.nestedContainer;
|
|
147
213
|
const children = containerNode.children || [];
|
|
@@ -181,6 +247,7 @@ export const FlexNodeRenderer = React.forwardRef((props, ref) => {
|
|
|
181
247
|
key={childNode.id}
|
|
182
248
|
node={childNode}
|
|
183
249
|
path={[...path, index]}
|
|
250
|
+
onCtaClick={onCtaClick}
|
|
184
251
|
onSelectNode={onSelectNode}
|
|
185
252
|
selectedNodeId={selectedNodeId}
|
|
186
253
|
data={data}
|
|
@@ -194,6 +261,25 @@ export const FlexNodeRenderer = React.forwardRef((props, ref) => {
|
|
|
194
261
|
</div>
|
|
195
262
|
</div>
|
|
196
263
|
);
|
|
264
|
+
} else if (node.type === 'cta') {
|
|
265
|
+
return (
|
|
266
|
+
<div className="flex-node flex-node-cta">
|
|
267
|
+
<button
|
|
268
|
+
className="btn btn-primary btn-sm" // Utilisez vos classes de bouton
|
|
269
|
+
onClick={() => handleCtaClick(node.endpointPath)}
|
|
270
|
+
disabled={isLoading}
|
|
271
|
+
>
|
|
272
|
+
{isLoading ? (
|
|
273
|
+
<span className="loading loading-spinner loading-xs"></span>
|
|
274
|
+
) : (
|
|
275
|
+
<>
|
|
276
|
+
<FaPlay className="mr-2" />
|
|
277
|
+
{node.label || 'Execute'}
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
197
283
|
} else if (node.type === 'item') {
|
|
198
284
|
const itemStyleWithDefaults = {
|
|
199
285
|
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'grab', ...node.itemStyle,
|
|
@@ -283,8 +369,10 @@ FlexNodeRenderer.propTypes = {
|
|
|
283
369
|
modelFields: PropTypes.array,
|
|
284
370
|
t: PropTypes.func.isRequired,
|
|
285
371
|
onMoveNode: PropTypes.func,
|
|
372
|
+
|
|
286
373
|
dnd: PropTypes.object.isRequired, // dnd est maintenant requis
|
|
287
374
|
};
|
|
288
375
|
FlexNodeRenderer.defaultProps = {
|
|
376
|
+
onCtaClick: () => {},
|
|
289
377
|
data: [], modelFields: [], onMoveNode: null,
|
|
290
378
|
};
|
|
@@ -3,27 +3,50 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
3
3
|
/**
|
|
4
4
|
* Crée un nouveau nœud de type 'container' ou 'item'.
|
|
5
5
|
*/
|
|
6
|
+
|
|
6
7
|
export const createNewNode = (type) => {
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const baseNode = { id: `${type}-${Date.now()}-${Math.random().toString(36).substring(2, 7)}` };
|
|
9
|
+
|
|
10
|
+
switch (type) {
|
|
11
|
+
case 'container':
|
|
12
|
+
return {
|
|
13
|
+
...baseNode,
|
|
14
|
+
type: 'container',
|
|
15
|
+
containerStyle: { /* ... styles ... */ },
|
|
16
|
+
children: []
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// --- AJOUTER CE BLOC ---
|
|
20
|
+
case 'cta':
|
|
21
|
+
return {
|
|
22
|
+
...baseNode,
|
|
23
|
+
type: 'cta',
|
|
24
|
+
label: 'Mon Bouton',
|
|
25
|
+
endpointPath: '',
|
|
26
|
+
itemStyle: { width: 'auto', height: 'auto' },
|
|
27
|
+
// --- AJOUTS POUR LA CONFIGURATION DE LA REQUÊTE ---
|
|
28
|
+
httpMethod: 'GET',
|
|
29
|
+
requestBodyTemplate: '',
|
|
30
|
+
requestQueryTemplate: ''
|
|
31
|
+
};
|
|
32
|
+
// --- FIN DE L'AJOUT ---
|
|
33
|
+
|
|
34
|
+
case 'item':
|
|
35
|
+
default: // Le 'cta' tombait ici, créant un 'item' par défaut
|
|
9
36
|
return {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
37
|
+
...baseNode,
|
|
38
|
+
type: 'item',
|
|
39
|
+
itemStyle: { width: '100px', height: '50px' },
|
|
40
|
+
content: { type: 'placeholder' }
|
|
13
41
|
};
|
|
14
42
|
}
|
|
15
|
-
return {
|
|
16
|
-
id: uuidv4(), type: 'item',
|
|
17
|
-
itemStyle: { ...baseStyle, width: '', height: '', display: 'flex', alignItems: 'center', justifyContent: 'center' },
|
|
18
|
-
content: { type: 'placeholder' },
|
|
19
|
-
};
|
|
20
43
|
};
|
|
21
44
|
|
|
22
45
|
/**
|
|
23
46
|
* Nettoie un nœud pour la sauvegarde.
|
|
24
47
|
*/
|
|
25
48
|
export const cleanNodeForOutput = (node) => {
|
|
26
|
-
const { id, type, children, content, containerStyle, itemStyle } = node;
|
|
49
|
+
const { id, type, children, content, containerStyle, itemStyle, label, endpointPath, httpMethod, requestBodyTemplate, requestQueryTemplate } = node;
|
|
27
50
|
const outputNode = { id, type };
|
|
28
51
|
|
|
29
52
|
if (type === 'container') {
|
|
@@ -43,6 +66,14 @@ export const cleanNodeForOutput = (node) => {
|
|
|
43
66
|
} else {
|
|
44
67
|
outputNode.content = { type: 'placeholder' };
|
|
45
68
|
}
|
|
69
|
+
} else if (type === 'cta') {
|
|
70
|
+
// On sauvegarde les propriétés spécifiques au CTA
|
|
71
|
+
outputNode.label = label || 'Execute';
|
|
72
|
+
outputNode.itemStyle = node.itemStyle || { width: 'auto', height: 'auto' };
|
|
73
|
+
outputNode.httpMethod = httpMethod || 'GET';
|
|
74
|
+
outputNode.requestBodyTemplate = requestBodyTemplate || '';
|
|
75
|
+
outputNode.requestQueryTemplate = requestQueryTemplate || '';
|
|
76
|
+
outputNode.endpointPath = endpointPath || '';
|
|
46
77
|
}
|
|
47
78
|
return outputNode;
|
|
48
79
|
};
|
package/client/src/ModelList.jsx
CHANGED
|
@@ -47,28 +47,41 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
return fetch('/api/
|
|
52
|
-
|
|
53
|
-
},
|
|
54
|
-
body: JSON.stringify({
|
|
55
|
-
})
|
|
50
|
+
const demoInitMutation = useMutation((profile) => {
|
|
51
|
+
return fetch('/api/demo/initialize', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { "Content-Type": "application/json"},
|
|
54
|
+
body: JSON.stringify({profile: profile}),
|
|
55
|
+
});
|
|
56
56
|
});
|
|
57
|
+
|
|
57
58
|
const handleProfile = (profile) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
// --- CHANGEMENT ICI : On appelle la nouvelle mutation sans argument ---
|
|
60
|
+
demoInitMutation.mutateAsync(profile).then(response => {
|
|
61
|
+
// On vérifie que la réponse est OK avant de continuer
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
// Gérer l'erreur si l'initialisation échoue
|
|
64
|
+
console.error("L'initialisation de la démo a échoué.");
|
|
65
|
+
// Vous pourriez afficher une notification à l'utilisateur ici.
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Le reste de la logique est parfait et ne change pas
|
|
70
|
+
setCurrentProfile(profile);
|
|
71
|
+
gtag('event', 'profile ' + profile);
|
|
72
|
+
queryClient.invalidateQueries('api/models');
|
|
73
|
+
|
|
74
|
+
const profileSteps = allTourSteps[profile];
|
|
75
|
+
if (profileSteps) {
|
|
76
|
+
setCurrentTourSteps(profileSteps);
|
|
77
|
+
setTourStepIndex(0);
|
|
78
|
+
setIsTourOpen(true); // Start the tour
|
|
79
|
+
} else {
|
|
80
|
+
console.warn(`No tour steps defined for profile: ${profile}`);
|
|
81
|
+
}
|
|
82
|
+
}).catch(error => {
|
|
83
|
+
console.error("Erreur critique lors de l'appel d'initialisation de la démo:", error);
|
|
84
|
+
});
|
|
72
85
|
};
|
|
73
86
|
|
|
74
87
|
const [mods, setMods] = useState([]);
|