data-primals-engine 1.1.1 → 1.1.2

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.
@@ -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 { FaTrash, FaBox, FaExpandArrowsAlt, FaObjectGroup, FaEdit, FaFileAlt } from 'react-icons/fa';
5
- import { CodeField, NumberField, SelectField } from './Field.jsx';
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={[{ value: null, label: t('flexBuilder.selectAModel', 'Sélectionner un modèle...') }, ...modelOptions]}
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" title={t('flexBuilder.deleteThisNode', 'Supprimer cet élément')}><FaTrash /></button>
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')}><FaBox /> {t('flexBuilder.addItem', 'Ajouter une case')}</button>
102
- <button onClick={() => onAddChildNode(selectedNode.id, 'container')}><FaExpandArrowsAlt /> {t('flexBuilder.addNestedContainer', 'Ajouter un conteneur')}</button>
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('flexDirection')}:</label><SelectField items={flexOptions.flexDirection.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.flexDirection} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.flexDirection', sel.value)} /></div>
105
- <div className="control-group"><label>{t('flexWrap')}:</label><SelectField items={flexOptions.flexWrap.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.flexWrap} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.flexWrap', sel.value)} /></div>
106
- <div className="control-group"><label>{t('justifyContent')}:</label><SelectField items={flexOptions.justifyContent.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.justifyContent} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.justifyContent', sel.value)} /></div>
107
- <div className="control-group"><label>{t('alignItems')}:</label><SelectField items={flexOptions.alignItems.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.alignItems} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.alignItems', sel.value)} /></div>
108
- <div className="control-group"><label>{t('gap')}:</label><input type="text" value={selectedNode.containerStyle.gap || ''} onChange={e => onUpdateNode(selectedNode.id, 'containerStyle.gap', e.target.value)} placeholder="ex: 10px" /></div>
109
- <div className="form-group"><CodeField id="customCss" name="customCss" language="css" value={selectedNode.containerStyle.customCss || ''} onChange={(e) => onUpdateNode(selectedNode.id, 'containerStyle.customCss', e.value)} placeholder={t('flexBuilder.customCssPlaceholder', 'Ex: background-color: red;')} rows={4} /></div>
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 /> {selectedNode.content.type === 'dataField' ? t('flexBuilder.editDataMapping', 'Modifier le champ lié') : t('flexBuilder.mapDataField', 'Lier un champ de données')}
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({ nodeId: selectedNode.id, initialContent: selectedNode.content.html || '' });
271
+ onSetEditingHtmlInfo({
272
+ nodeId: selectedNode.id,
273
+ initialContent: selectedNode.content.html || ''
274
+ });
125
275
  e.stopPropagation()
126
276
  }}>
127
- <FaFileAlt /> {t('flexBuilder.editHTML', 'Éditer le contenu')}
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 /> {t('flexBuilder.makeNestedContainer', 'Transformer en conteneur')}
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" value={selectedNode.itemStyle.width || ''} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.width', e.target.value)} placeholder="ex: 100px, 20%" /></div>
136
- <div className="control-group"><label>{t('itemHeight')}:</label><input type="text" value={selectedNode.itemStyle.height || ''} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.height', e.target.value)} placeholder="ex: 50px, auto" /></div>
137
- <div className="control-group"><label>{t('flexBuilder.itemFlexGrow', 'Flex Grow')}:</label><input type="number" value={selectedNode.itemStyle.flexGrow || 0} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexGrow', parseFloat(e.target.value) || 0)} min="0" step="0.1" /></div>
138
- <div className="control-group"><label>{t('flexBuilder.itemFlexShrink', 'Flex Shrink')}:</label><input type="number" value={selectedNode.itemStyle.flexShrink === undefined ? 1 : selectedNode.itemStyle.flexShrink} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexShrink', parseFloat(e.target.value))} min="0" step="0.1" /></div>
139
- <div className="control-group"><label>{t('flexBuilder.itemFlexBasis', 'Flex Basis')}:</label><input type="text" value={selectedNode.itemStyle.flexBasis || 'auto'} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexBasis', e.target.value)} placeholder="auto, 100px, 20%" /></div>
140
- <div className="form-group"><CodeField id="customCssItem" name="customCssItem" language="css" value={selectedNode.itemStyle.customCss || ''} onChange={(e) => onUpdateNode(selectedNode.id, 'itemStyle.customCss', e.value)} placeholder={t('flexBuilder.customCssPlaceholder', 'Ex: background-color: blue;')} rows={4} /></div>
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
  );
@@ -1,7 +1,17 @@
1
1
  // C:/Dev/hackersonline-engine/client/src/FlexNode.jsx
2
- import React, { useEffect, useRef } from 'react';
2
+ import React, {useEffect, useRef, useState} from 'react';
3
3
  import PropTypes from 'prop-types';
4
- import { FaPlus, FaTrash, FaEdit, FaRegSquare, FaRegCheckSquare, FaBox, FaExpandArrowsAlt, FaObjectGroup } from 'react-icons/fa';
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, path, onSelectNode, selectedNodeId, data, dataIndexRef, modelFields, t, onMoveNode,
60
- dnd, // Recevoir l'objet dnd en tant que prop
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 baseStyle = { border: '1px solid #ddd', padding: '5px' };
8
- if (type === 'container') {
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
- id: uuidv4(), type: 'container',
11
- containerStyle: { ...baseStyle, display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '5px', minHeight: '50px'},
12
- children: [],
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
  };
@@ -47,28 +47,41 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
47
47
  }
48
48
  };
49
49
 
50
- const importModelsMutation = useMutation((profile) => {
51
- return fetch('/api/models/import', { method: 'POST', headers: {
52
- 'Content-Type': 'application/json'
53
- },
54
- body: JSON.stringify({ install: true, models: ['request', ...profiles[profile]] })
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
- importModelsMutation.mutateAsync(profile).then(e => {
59
- setCurrentProfile(profile);
60
- gtag('event', 'profile ' + profile);
61
- queryClient.invalidateQueries('api/models');
62
-
63
- const profileSteps = allTourSteps[profile];
64
- if (profileSteps) {
65
- setCurrentTourSteps(profileSteps);
66
- setTourStepIndex(0);
67
- setIsTourOpen(true); // Start the tour
68
- } else {
69
- console.warn(`No tour steps defined for profile: ${profile}`);
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([]);