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
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
container:
|
|
17
|
-
image: node:
|
|
17
|
+
image: node:20 # Specify the Docker version you needx
|
|
18
18
|
env:
|
|
19
19
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
|
20
20
|
NODE_ENV: development
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
- 27017
|
|
24
24
|
strategy:
|
|
25
25
|
matrix:
|
|
26
|
-
node-version: [
|
|
26
|
+
node-version: [20.x]
|
|
27
27
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
28
28
|
services:
|
|
29
29
|
mongodb:
|
|
@@ -45,11 +45,6 @@ jobs:
|
|
|
45
45
|
apt-get update
|
|
46
46
|
apt-get install -y mongodb-org-tools
|
|
47
47
|
|
|
48
|
-
- name: Set up Node.js
|
|
49
|
-
uses: actions/setup-node@v2
|
|
50
|
-
with:
|
|
51
|
-
node-version: '18.18.2'
|
|
52
|
-
|
|
53
48
|
- name: Install dependencies
|
|
54
49
|
run: npm install
|
|
55
50
|
|
package/README.md
CHANGED
|
@@ -407,14 +407,14 @@ await loadFromDump(currentUser, { modelsOnly: false });
|
|
|
407
407
|
|
|
408
408
|
## Pack Management
|
|
409
409
|
|
|
410
|
-
### installPack(
|
|
410
|
+
### installPack(packId, user, lang)
|
|
411
411
|
|
|
412
412
|
> Installs a predefined data pack.
|
|
413
413
|
|
|
414
414
|
Example:
|
|
415
415
|
|
|
416
416
|
```javascript
|
|
417
|
-
const result = await installPack(
|
|
417
|
+
const result = await installPack("61d1f1a9e3f1a9e3f1a9e3f1", user, "en");
|
|
418
418
|
// Returns installation summary
|
|
419
419
|
```
|
|
420
420
|
|
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";
|
|
@@ -55,10 +55,13 @@ import {useCookies, CookiesProvider} from "react-cookie";
|
|
|
55
55
|
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
|
-
import {
|
|
58
|
+
import {seoTitle} from "./constants.js";
|
|
59
|
+
import {host} from "../../src/constants.js";
|
|
59
60
|
import i18next from "i18next";
|
|
60
61
|
import {websiteTranslations} from "./translations.js";
|
|
61
62
|
|
|
63
|
+
import { Tooltip } from 'react-tooltip';
|
|
64
|
+
|
|
62
65
|
let queryClient = new QueryClient();
|
|
63
66
|
|
|
64
67
|
export const setQueryClient = (q) => {
|
|
@@ -90,6 +93,14 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
90
93
|
// 2. NOUVEAU : État pour stocker la configuration de l'assistant
|
|
91
94
|
const [assistantConfig, setAssistantConfig] = useState(null);
|
|
92
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
|
+
];
|
|
93
104
|
useQuery(
|
|
94
105
|
'assistantConfig', // La clé de la requête reste la même
|
|
95
106
|
() => {
|
|
@@ -168,14 +179,6 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
168
179
|
}
|
|
169
180
|
];
|
|
170
181
|
|
|
171
|
-
const promotionalMessages = [
|
|
172
|
-
{ text: t('promo.rotation.6') },
|
|
173
|
-
{ text: t('promo.rotation.1') },
|
|
174
|
-
{ text: t('promo.rotation.2') },
|
|
175
|
-
{ text: t('promo.rotation.1') },
|
|
176
|
-
{ text: t('promo.rotation.4') },
|
|
177
|
-
{ text: t('promo.rotation.5') }
|
|
178
|
-
];
|
|
179
182
|
const { setCurrentTourSteps, setAllTourSteps, currentTour, setCurrentTour } = useUI();
|
|
180
183
|
const [translations, setTranslations] = useState([]);
|
|
181
184
|
|
|
@@ -253,7 +256,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
253
256
|
setCurrentProfile(null);
|
|
254
257
|
const username = 'demo'+getRandom(1, 99);
|
|
255
258
|
setMe({username});
|
|
256
|
-
setCookie('username', username, { path : "/", domain: isProd ?
|
|
259
|
+
setCookie('username', username, { path : "/", domain: isProd ? host : 'localhost'});
|
|
257
260
|
nav('/user/'+username, { state: { startTour: true } });
|
|
258
261
|
}}><Trans i18nKey={"links.demo"}>Demo</Trans></Button>)}</>
|
|
259
262
|
|
|
@@ -272,8 +275,11 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
272
275
|
};
|
|
273
276
|
|
|
274
277
|
useEffect(() => {
|
|
275
|
-
if( !cookies.username)
|
|
276
|
-
|
|
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
|
+
}
|
|
277
283
|
}, [cookies.username]);
|
|
278
284
|
|
|
279
285
|
|
|
@@ -358,7 +364,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
358
364
|
setMe({username});
|
|
359
365
|
|
|
360
366
|
setPromptResult(null);
|
|
361
|
-
setCookie('username', username, { path : "/", domain: isProd ?
|
|
367
|
+
setCookie('username', username, { path : "/", domain: isProd ? host : 'localhost'});
|
|
362
368
|
nav('/user/'+username, { state: { startTour: false } });
|
|
363
369
|
};
|
|
364
370
|
|
|
@@ -564,6 +570,17 @@ function UserPage({notifs,triggerSignin, onAuthenticated}) {
|
|
|
564
570
|
données</p>}</>;
|
|
565
571
|
}
|
|
566
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
|
+
}
|
|
567
584
|
function App() {
|
|
568
585
|
|
|
569
586
|
return (
|
|
@@ -574,7 +591,7 @@ function App() {
|
|
|
574
591
|
<BrowserRouter>
|
|
575
592
|
<UIProvider>
|
|
576
593
|
<NotificationProvider>
|
|
577
|
-
<
|
|
594
|
+
<BaseLayout></BaseLayout>x
|
|
578
595
|
</NotificationProvider>
|
|
579
596
|
</UIProvider>
|
|
580
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
|
+
|
|
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
|
}
|