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.
@@ -14,7 +14,7 @@ jobs:
14
14
 
15
15
  runs-on: ubuntu-latest
16
16
  container:
17
- image: node:18 # Specify the Docker version you needx
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: [18.x]
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(logger, packId, user, lang)
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(logger, "61d1f1a9e3f1a9e3f1a9e3f1", user, "en");
417
+ const result = await installPack("61d1f1a9e3f1a9e3f1a9e3f1", user, "en");
418
418
  // Returns installation summary
419
419
  ```
420
420
 
@@ -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 {promotionalMessages, seoTitle} from "./constants.js";
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 ? 'primals.net' : 'localhost'});
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
- 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
+ }
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 ? 'primals.net' : 'localhost'});
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
- <Layout />
594
+ <BaseLayout></BaseLayout>x
578
595
  </NotificationProvider>
579
596
  </UIProvider>
580
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
+
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
  }