data-primals-engine 1.1.0 → 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.
package/README.md CHANGED
@@ -43,6 +43,21 @@ Possibly create a `.env` file:
43
43
  ```env
44
44
  MONGO_DB_URL=mongodb://127.0.0.1:27017
45
45
  ```
46
+ | Variable | Description | Example |
47
+ |:----------------------|:------------------------------------------------------------------------|:-----------------------------------------|
48
+ | MONGO_DB_URL | Connection URL for your MongoDB database. | mongodb://user:pass@host:27017/db |
49
+ | PORT | Port on which the Express server will listen. | 7633 |
50
+ | JWT_SECRET | Secret key for signing JWT authentication tokens. | a_long_random_secret_string |
51
+ | OPENAI_API_KEY | Your optional OpenAI API key for AI features. | sk-xxxxxxxxxxxxxxxxxxxx |
52
+ | GOOGLE_API_KEY | Your optional Google (Gemini) API key for AI features. | AIzaSyxxxxxxxxxxxxxxxxxxxx |
53
+ | AWS_ACCESS_KEY_ID | AWS access key for S3 storage (files, backups). Keep empty to disable | AKIAIOSFODNN7EXAMPLE |
54
+ | AWS_SECRET_ACCESS_KEY | AWS secret access key. | wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY | |
55
+ | AWS_REGION | Region for your S3 bucket. | eu-west-3 | |
56
+ | AWS_S3_BUCKET_NAME | Name of the S3 bucket to use. | my-backup-bucket | |
57
+ | SMTP_HOST | SMTP server host for sending emails. | smtp.example.com |
58
+ | SMTP_PORT | SMTP server port. | 587 |
59
+ | SMTP_USER | Username for SMTP authentication. | user@example.com |
60
+ | SMTP_PASS | Password for SMTP authentication. | password |
46
61
 
47
62
  Start the server:
48
63
  ```bash
@@ -73,6 +88,27 @@ Define schemas using JSON:
73
88
  ]
74
89
  }
75
90
  ```
91
+ | Type | Description | Properties/Notes |
92
+ |:------------|:------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
93
+ | string | Character string. | minLength, maxLength |
94
+ | string_t | International character string ID. | same as string, translated in { key, value } |
95
+ | number | Numeric value (integer or float). | min, max |
96
+ | boolean | Boolean value (true/false). | – |
97
+ | date | Stores a ISO date. | – |
98
+ | datetime | Stores an ISO date and time. | – |
99
+ | richtext | Rich text field (HTML) for WYSIWYG editors. | |
100
+ | richtext_t | International rich text field (HTML) for WYSIWYG editors. | i18n |
101
+ | email | String validated as an email address. | – |
102
+ | password | String that will be automatically hashed. | – |
103
+ | enum | Allows selecting a value from a predefined list. | items: ["value1", "value2"] |
104
+ | relation | Creates a link to a document in another model. | relation: "target_model_name", multiple: true/false |
105
+ | file | For uploading a file (stored on S3 if configured). | allowedTypes:['image/jpeg', 'image/png', 'image/bmp'], maxSize: 1024*1000 |
106
+ | image | Specialized file type for images, with preview. | – |
107
+ | array | Stores a list of values. | itemsType: 'enum' // any type except relations |
108
+ | object | Stores a nested JSON object. – | |
109
+ | code | Stores language="*" as string, stores language="json" as arbitrary JSON structure. | language="json" conditionBuilder=true |
110
+ | model | Stores a model by name | – |
111
+ | modelField | Stores a model field path | – |
76
112
 
77
113
  ### 2. Modules
78
114
  Activatable features:
@@ -371,14 +407,14 @@ await loadFromDump(currentUser, { modelsOnly: false });
371
407
 
372
408
  ## Pack Management
373
409
 
374
- ### installPack(logger, packId, user, lang)
410
+ ### installPack(packId, user, lang)
375
411
 
376
412
  > Installs a predefined data pack.
377
413
 
378
414
  Example:
379
415
 
380
416
  ```javascript
381
- const result = await installPack(logger, "61d1f1a9e3f1a9e3f1a9e3f1", user, "en");
417
+ const result = await installPack("61d1f1a9e3f1a9e3f1a9e3f1", user, "en");
382
418
  // Returns installation summary
383
419
  ```
384
420
 
package/client/README.md CHANGED
@@ -1,8 +1,116 @@
1
- # React + Vite
1
+ This guide explains how to integrate and use the client-side part of the `data-primals-engine` library in an external React application (such as hackersonline).
2
+ The library provides a set of context Providers, Hooks, and UI components to interact with the `data-primals-engine` backend.
2
3
 
3
- This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+ # Table of Contents
5
+ ## Prerequisites
6
+ Your React application must have the following dependencies installed:
7
+ - react & react-dom (version >= 18.0.0)
8
+ - react-query (version >= 3.0.0)
9
+ - react-router-dom
10
+ - react-cookie
4
11
 
5
- Currently, two official plugins are available:
12
+ ## Installation
13
+ Install the library:
14
+ ```shell
15
+ npm install ../../data-primals-engine
16
+ ```
17
+ Install peer dependencies:
18
+ ```shell
19
+ npm install react-query react-router-dom react-cookie
20
+ ```
6
21
 
7
- - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8
- - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
22
+ ## Basic Setup
23
+
24
+ The setup mainly occurs in your application's root file (typically App.jsx or main.jsx).
25
+
26
+ It consists of initializing a single QueryClient instance and wrapping your app with the required Providers.
27
+
28
+ ### Set the QueryClient (Singleton)
29
+ For react-query caching to work correctly across your app (including inside the library), it is crucial to instantiate QueryClient only once.
30
+
31
+ The data-primals-engine library needs to know which QueryClient instance you're using. It exports a setQueryClient function for that purpose.
32
+
33
+ ### Set up the Providers
34
+ Your app must be wrapped by several Providers in the correct order so the contexts are available to all child components.
35
+
36
+ ## Full example (App.jsx)
37
+ Here's what your main application file should look like:
38
+ ```jsx
39
+ // In your main App.jsx file (e.g., hackersonline/client/src/App.jsx)
40
+
41
+ import React from 'react';
42
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
43
+ import { CookiesProvider } from 'react-cookie';
44
+
45
+ // 1. Import QueryClient and its Provider from react-query
46
+ import { QueryClient, QueryClientProvider } from 'react-query';
47
+
48
+ // 2. Import Providers and the config function from data-primals-engine
49
+ import {
50
+ ModelProvider,
51
+ AuthProvider,
52
+ UIProvider,
53
+ NotificationProvider,
54
+ setQueryClient // <-- Important config function
55
+ } from 'data-primals-engine/client';
56
+
57
+ // 3. Import the i18n config (see dedicated section)
58
+ import 'data-primals-engine/i18n';
59
+
60
+ // 4. (CRUCIAL) Create a unique instance of QueryClient
61
+ const queryClient = new QueryClient();
62
+
63
+ // 5. (CRUCIAL) Pass the instance to the shared library
64
+ setQueryClient(queryClient);
65
+
66
+ // Your page components
67
+ import HomePage from './pages/HomePage';
68
+ import UserDashboard from './pages/UserDashboard';
69
+
70
+ function App() {
71
+ return (
72
+ // 6. Wrap the app with all required providers
73
+ <QueryClientProvider client={queryClient}>
74
+ <AuthProvider>
75
+ <CookiesProvider>
76
+ <ModelProvider>
77
+ <BrowserRouter>
78
+ <UIProvider>
79
+ <NotificationProvider>
80
+
81
+ {/* Your app content */}
82
+ <Routes>
83
+ <Route path="/" element={<HomePage />} />
84
+ <Route path="/dashboard" element={<UserDashboard />} />
85
+ {/* ... other routes */}
86
+ </Routes>
87
+
88
+ </NotificationProvider>
89
+ </UIProvider>
90
+ </BrowserRouter>
91
+ </ModelProvider>
92
+ </CookiesProvider>
93
+ </AuthProvider>
94
+ </QueryClientProvider>
95
+ );
96
+ }
97
+
98
+ export default App;
99
+ ```
100
+
101
+ ## Internationalization (i18n)
102
+ The library comes with its own i18next configuration and translation files.
103
+ To enable it in your app, you just need to import the config module once in your main file (App.jsx or main.jsx):
104
+
105
+ ```jsx
106
+ // In App.jsx or main.jsx
107
+ import 'data-primals-engine/i18n';
108
+ ```
109
+
110
+ This simple import will:
111
+
112
+ - Initialize i18next.
113
+ - Load default translations (French, English, etc.).
114
+ - Enable automatic browser language detection.
115
+
116
+ You can then use react-i18next components and hooks (useTranslation, Trans) as usual in your application.
@@ -46,8 +46,8 @@ import {
46
46
  FaInfo,
47
47
  FaKey,
48
48
  FaLanguage,
49
- FaMobile,
50
- FaStar
49
+ FaMobile, FaQuestion, FaSignInAlt, FaSignOutAlt,
50
+ FaStar, FaUser
51
51
  } from "react-icons/fa";
52
52
  import {Helmet} from "react-helmet";
53
53
  import {useCookies, CookiesProvider} from "react-cookie";
@@ -56,6 +56,11 @@ import { translations as allTranslations} from "data-primals-engine/i18n";
56
56
  import {getRandom} from "data-primals-engine/core";
57
57
  import {getUserHash} from "data-primals-engine/data";
58
58
  import {seoTitle} from "./constants.js";
59
+ import {host} from "../../src/constants.js";
60
+ import i18next from "i18next";
61
+ import {websiteTranslations} from "./translations.js";
62
+
63
+ import { Tooltip } from 'react-tooltip';
59
64
 
60
65
  let queryClient = new QueryClient();
61
66
 
@@ -69,6 +74,7 @@ function TopBar({header}) {
69
74
  const { i18n, t } = useTranslation();
70
75
  const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
71
76
 
77
+
72
78
  useEffect(() => {
73
79
  if( location.pathname ){
74
80
  gtag('config', 'G-NDHNSVB4YB', { page_path: location.pathname });
@@ -87,6 +93,14 @@ function Layout ({header, translationMutation, routes, body, footer}) {
87
93
  // 2. NOUVEAU : État pour stocker la configuration de l'assistant
88
94
  const [assistantConfig, setAssistantConfig] = useState(null);
89
95
 
96
+ const promotionalMessages = [
97
+ { text: t('promo.rotation.6') },
98
+ { text: t('promo.rotation.1') },
99
+ { text: t('promo.rotation.2') },
100
+ { text: t('promo.rotation.1') },
101
+ { text: t('promo.rotation.4') },
102
+ { text: t('promo.rotation.5') }
103
+ ];
90
104
  useQuery(
91
105
  'assistantConfig', // La clé de la requête reste la même
92
106
  () => {
@@ -175,13 +189,13 @@ function Layout ({header, translationMutation, routes, body, footer}) {
175
189
  const changeLanguage = (newLang) => {
176
190
  if (typeof(newLang) === 'string' && newLang) {
177
191
 
178
- i18n.removeResourceBundle(lang, "translations");
179
- i18n.addResourceBundle(newLang, "translations", translations);
180
192
  i18n.changeLanguage(newLang, (err)=>{
181
193
 
194
+ i18next.removeResourceBundle(lang, "translation");
195
+ i18next.addResourceBundle(newLang, 'translation', {...websiteTranslations[newLang]['translation'], ...translations[newLang]?.['translation']});
196
+
182
197
  gtag("event", "change_language "+newLang);
183
198
  });
184
-
185
199
  }
186
200
  };
187
201
 
@@ -242,7 +256,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
242
256
  setCurrentProfile(null);
243
257
  const username = 'demo'+getRandom(1, 99);
244
258
  setMe({username});
245
- setCookie('username', username, { path : "/", domain: isProd ? 'primals.net' : 'localhost'});
259
+ setCookie('username', username, { path : "/", domain: isProd ? host : 'localhost'});
246
260
  nav('/user/'+username, { state: { startTour: true } });
247
261
  }}><Trans i18nKey={"links.demo"}>Demo</Trans></Button>)}</>
248
262
 
@@ -261,8 +275,11 @@ function Layout ({header, translationMutation, routes, body, footer}) {
261
275
  };
262
276
 
263
277
  useEffect(() => {
264
- if( !cookies.username)
265
- onAuthenticated({username: 'demo'+getRandom(1, 99)}, true);
278
+ if( !cookies.username) {
279
+ const username ='demo' + getRandom(1, 99);
280
+ setCookie("username", username, { path : "/", domain: isProd ? host : 'localhost'});
281
+ onAuthenticated({username}, true);
282
+ }
266
283
  }, [cookies.username]);
267
284
 
268
285
 
@@ -277,15 +294,6 @@ function Layout ({header, translationMutation, routes, body, footer}) {
277
294
  const [lightboxOpened, setLightboxOpened] = React.useState(false);
278
295
  const [lightboxIndex, setLightboxIndex] = React.useState(0);
279
296
 
280
- const myMessages = [
281
- { text: t('promo.rotation.6') },
282
- { text: t('promo.rotation.1') },
283
- { text: t('promo.rotation.2') },
284
- { text: t('promo.rotation.1') },
285
- { text: t('promo.rotation.4') },
286
- { text: t('promo.rotation.5') },
287
- ];
288
-
289
297
  const slides = [{src: "/prez1.jpg", title: t('prez1')},{src: "/prez6.jpg", title: i18n.t('prez3')},{src: "/prez2.jpg", title: t('prez2')},{src: "/prez4.jpg", title: i18n.t('prez4')},{src: "/prez5.jpg", title: i18n.t('prez5')}];
290
298
 
291
299
  const { addNotification } = useNotificationContext();
@@ -356,7 +364,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
356
364
  setMe({username});
357
365
 
358
366
  setPromptResult(null);
359
- setCookie('username', username, { path : "/", domain: isProd ? 'primals.net' : 'localhost'});
367
+ setCookie('username', username, { path : "/", domain: isProd ? host : 'localhost'});
360
368
  nav('/user/'+username, { state: { startTour: false } });
361
369
  };
362
370
 
@@ -389,8 +397,8 @@ function Layout ({header, translationMutation, routes, body, footer}) {
389
397
  width={250}
390
398
  />
391
399
  <div className="bubble flex-1">
392
- <h1><Trans i18nKey={"prez.title"}>F</Trans></h1>
393
- <div className="inner"><MessageRotator messages={myMessages} fadeDuration={200} defaultDelay={7500}/></div>
400
+ <h1><Trans i18nKey={"prez.title"}>{seoTitle}</Trans></h1>
401
+ <div className="inner"><MessageRotator messages={promotionalMessages} fadeDuration={200} defaultDelay={7500}/></div>
394
402
  </div>
395
403
  </div>
396
404
  <div className="flex flex-row">
@@ -562,6 +570,17 @@ function UserPage({notifs,triggerSignin, onAuthenticated}) {
562
570
  données</p>}</>;
563
571
  }
564
572
 
573
+ const BaseLayout=()=>{
574
+ return <Layout header={<header className={"flex"}>
575
+ <Tooltip id={"header"}/>
576
+ <h1 className="flex-1">{seoTitle}</h1>
577
+ <div className="flex">
578
+ <FaQuestion data-tooltip-id="header" data-tooltip-content="Documentation" onClick={()=> {
579
+ window.open("https://data.primals.net/en/documentation/", "_blank");
580
+ }} />
581
+ </div>
582
+ </header>} />
583
+ }
565
584
  function App() {
566
585
 
567
586
  return (
@@ -572,7 +591,7 @@ function App() {
572
591
  <BrowserRouter>
573
592
  <UIProvider>
574
593
  <NotificationProvider>
575
- <Layout />
594
+ <BaseLayout></BaseLayout>x
576
595
  </NotificationProvider>
577
596
  </UIProvider>
578
597
  </BrowserRouter>
@@ -14,6 +14,27 @@ body {
14
14
  padding: 0;
15
15
  }
16
16
 
17
+ header {
18
+ background-color: #3498db;
19
+ color: white;
20
+ align-items: center;
21
+ h1 {
22
+ font-size: 100%;
23
+ line-height: 100%;
24
+ color: white;
25
+ padding: 0;
26
+ }
27
+ > div.flex {
28
+ gap: 16px;
29
+ padding: 8px;
30
+ font-size: 20px;
31
+ }
32
+ svg {
33
+ cursor: pointer;
34
+ }
35
+ height: 50px;
36
+ overflow: hidden;
37
+ }
17
38
  .flex {
18
39
  display: flex;
19
40
  flex-wrap: wrap;
@@ -369,7 +369,11 @@
369
369
 
370
370
  .flex-node{
371
371
  margin: 0;
372
- max-width: 480px;
372
+ width: 100%;
373
+ @media only screen and (min-width: 480px) {
374
+ max-width: fit-content;
375
+ }
376
+ display: flex;
373
377
  .relation-value{
374
378
  padding: 6px 10px;
375
379
  border-radius: 20px;
@@ -83,8 +83,7 @@ const DashboardFlexViewItem = ({ flexViewConfig, allModels }) => {
83
83
  return <div className="flex-view-placeholder error" title={error.message}>{t('dashboards.error.displayFlexData', 'Erreur d\'affichage')}</div>;
84
84
  }
85
85
 
86
- console.log(flexStructure)
87
- if (!flexStructure || (!selectedModelName && !flexStructure.htmlContent)) {
86
+ if (!flexStructure) {
88
87
  return <div className="flex-view-placeholder">{t('dashboards.flexViewNotConfigured', 'Vue Flex non configurée ou modèle manquant.')}</div>;
89
88
  }
90
89
 
@@ -2,15 +2,125 @@
2
2
  import {useTranslation} from "react-i18next";
3
3
  import {cssProps} from "data-primals-engine/core";
4
4
  import FlexDataRenderer from "./FlexDataRenderer.jsx";
5
- import React from "react";
5
+ import React, {useState} from "react";
6
6
  import {getFieldPathValue} from "data-primals-engine/data";
7
7
  import {getFieldDefinitionFromPath} from "./core/data.js";
8
+ import {Dialog, DialogProvider} from "./Dialog.jsx";
9
+ import {FaPlay} from "react-icons/fa";
10
+ import {substituteVariables} from "data-primals-engine/modules/workflow";
11
+
12
+ /**
13
+ * Récupère une valeur imbriquée dans un objet (ex: 'user.address.city').
14
+ */
15
+ const getNestedValue = (obj, path) => {
16
+ if (!path || !obj) return undefined;
17
+ return path.split('.').reduce((acc, part) => acc && acc[part] !== undefined ? acc[part] : undefined, obj);
18
+ };
19
+
20
+
21
+ /**
22
+ * Remplace les placeholders {{...}} dans une chane de caractères JSON
23
+ * par les valeurs de l'objet de données.
24
+ * C'est une version simplifiée, spécifique au client.
25
+ */
26
+ const substituteClientVariables = (templateString, dataObject) => {
27
+ if (typeof templateString !== 'string' || !dataObject) {
28
+ return templateString;
29
+ }
30
+ return templateString.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
31
+ const value = getNestedValue(dataObject, key.trim());
32
+ if (value === undefined) {
33
+ return match; // Garde le placeholder si la valeur n'est pas trouvée
34
+ }
35
+ if (value === null) {
36
+ return 'null';
37
+ }
38
+ // Pour les nombres et booléens, on les retourne tels quels.
39
+ // Pour les chaînes, l'utilisateur doit mettre les guillemets dans le template : "name": "{{name}}"
40
+ // Pour les objets, on les stringify pour éviter [object Object]
41
+ if (typeof value === 'object') {
42
+ return JSON.stringify(value);
43
+ }
44
+ return value;
45
+ });
46
+ };
47
+
48
+ const CtaNode = ({ node, nodeStyle, dataItem }) => {
49
+ const [isLoading, setIsLoading] = useState(false);
50
+ const [result, setResult] = useState(null);
51
+ const [isModalOpen, setIsModalOpen] = useState(false);
52
+
53
+ const handleCtaClick = async () => {
54
+ if (!node.endpointPath) return;
55
+ setIsLoading(true);
56
+ setResult(null);
57
+
58
+ try {
59
+ // 1. Construire l'URL finale avec les paramètres de requête
60
+ let finalUrl = `/api/actions/${node.endpointPath}`;
61
+ if (node.requestQueryTemplate) {
62
+ const substitutedQuery = substituteClientVariables(node.requestQueryTemplate, dataItem);
63
+ try {
64
+ const queryParams = JSON.parse(substitutedQuery || '{}');
65
+ const searchParams = new URLSearchParams(queryParams);
66
+ if (searchParams.toString()) {
67
+ finalUrl += `?${searchParams.toString()}`;
68
+ }
69
+ } catch (e) {
70
+ console.error("Erreur lors du parsing des paramètres de requête JSON:", e, "Template:", substitutedQuery);
71
+ }
72
+ }
73
+
74
+ // 2. Préparer les options du fetch (méthode, corps, etc.)
75
+ const method = node.httpMethod || 'GET';
76
+ const fetchOptions = {
77
+ method: method,
78
+ headers: { 'Content-Type': 'application/json' },
79
+ };
80
+
81
+ if (method !== 'GET' && method !== 'HEAD' && node.requestBodyTemplate) {
82
+ fetchOptions.body = substituteClientVariables(node.requestBodyTemplate, dataItem);
83
+ }
84
+
85
+ // 3. Exécuter la requête
86
+ const response = await fetch(finalUrl, fetchOptions);
87
+ const responseData = await response.json();
88
+ setResult({ status: response.status, ok: response.ok, data: responseData });
89
+
90
+ } catch (error) {
91
+ setResult({ ok: false, error: error.message });
92
+ } finally {
93
+ setIsLoading(false);
94
+ setIsModalOpen(true);
95
+ }
96
+ };
97
+
98
+ return (
99
+ <>
100
+ <div className="flex-node preview-item" style={nodeStyle}>
101
+ <button className="btn btn-primary" onClick={handleCtaClick} disabled={isLoading}>
102
+ {isLoading ? <span className="loading loading-spinner"></span> : <><FaPlay className="mr-2" />{node.label || 'Execute'}</>}
103
+ </button>
104
+ </div>
105
+ <DialogProvider>
106
+ {isModalOpen && (
107
+ <Dialog isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} title={`Résultat de ${node.httpMethod} /api/actions/${node.endpointPath}`}>
108
+ <div className="p-4 bg-gray-900 text-white rounded-md mt-4">
109
+ <pre><code>{JSON.stringify(result, null, 2)}</code></pre>
110
+ </div>
111
+ </Dialog>
112
+ )}
113
+ </DialogProvider>
114
+ </>
115
+ );
116
+ };
117
+
8
118
 
9
119
  const DisplayFlexNodeRenderer = ({ node, data, dataIndexRef, allModels, baseModelFields, model }) => {
10
120
  const { t } = useTranslation();
121
+ const dataItem = data && data.length > 0 ? data[dataIndexRef.current % data.length] : null;
11
122
 
12
123
  const baseStyle = node.type === 'container' ? node.containerStyle : node.itemStyle;
13
- // MODIFICATION: Correction pour appliquer le CSS custom sur les items aussi
14
124
  const customCssStyle = (node.type === 'container' ? node.containerStyle?.customCss : node.itemStyle?.customCss) || '';
15
125
  const customStyle = customCssStyle ? cssProps(customCssStyle) : {};
16
126
  const nodeStyle = { ...baseStyle, border: 'none', ...customStyle };
@@ -35,9 +145,15 @@ const DisplayFlexNodeRenderer = ({ node, data, dataIndexRef, allModels, baseMode
35
145
  ))}
36
146
  </div>
37
147
  );
38
- } else if (node.type === 'item') {
39
- let contentToRender;
148
+ }
40
149
 
150
+ if (node.type === 'cta') {
151
+ // On utilise notre nouveau composant intelligent
152
+ return <CtaNode node={node} nodeStyle={nodeStyle} dataItem={dataItem} />;
153
+ }
154
+
155
+ if (node.type === 'item') {
156
+ let contentToRender;
41
157
  if (node.content.type === 'dataField' && node.content.mapping) {
42
158
  const dataItem = data && data.length > 0 ? data[dataIndexRef.current % data.length] : null;
43
159
  let fieldValue;
@@ -18,7 +18,7 @@ import {
18
18
  deleteNodeRecursive,
19
19
  clearMappingsRecursive
20
20
  } from './FlexTreeUtils.js';
21
- import {DialogProvider} from "./Dialog.jsx";
21
+ import {Dialog, DialogProvider} from "./Dialog.jsx";
22
22
 
23
23
  const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], lang = 'fr' }) => {
24
24
  const { me: user } = useAuthContext();
@@ -35,7 +35,6 @@ const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], l
35
35
  dataLimit: 3, refreshInterval: 60000,
36
36
  }), [createNewNode]);
37
37
 
38
- // --- CORRECTION ---
39
38
  // Fonction utilitaire pour construire l'état à partir de la configuration chargée.
40
39
  // Elle fusionne correctement les données par défaut, les métadonnées et la structure de la vue.
41
40
  const buildStateFromConfig = useCallback((config) => {
@@ -253,6 +252,7 @@ const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], l
253
252
  onSave={handleSaveHtmlContent}
254
253
  />
255
254
  )}
255
+
256
256
  </DialogProvider>
257
257
  </div>
258
258
  );
@@ -106,6 +106,7 @@
106
106
 
107
107
  .flex-node { // Common styles for both container and item in preview
108
108
  transition: outline 0.2s ease-in-out;
109
+ display: flex;
109
110
  background-color: #f5f5f5;
110
111
  &.is-selected {
111
112
  outline: 2px solid #007bff !important; // Force outline for selected
@@ -223,4 +224,8 @@
223
224
  .flex-builder-controls .control-group .condition-builder {
224
225
  border: 1px solid #e0e0e0; padding: 10px; border-radius: 4px;
225
226
  background-color: #fdfdfd; margin-top: 5px;
227
+ }
228
+
229
+ .flex-node-cta {
230
+ pointer-events: none;
226
231
  }