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 +38 -2
- package/client/README.md +113 -5
- package/client/src/App.jsx +40 -21
- 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 +6 -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 +155 -17
- package/test/data.integration.test.js +2 -2
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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.
|
package/client/src/App.jsx
CHANGED
|
@@ -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 ?
|
|
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
|
-
|
|
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 ?
|
|
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"}>
|
|
393
|
-
<div className="inner"><MessageRotator messages={
|
|
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
|
-
<
|
|
594
|
+
<BaseLayout></BaseLayout>x
|
|
576
595
|
</NotificationProvider>
|
|
577
596
|
</UIProvider>
|
|
578
597
|
</BrowserRouter>
|
package/client/src/App.scss
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
}
|