data-primals-engine 1.0.11 → 1.1.0

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.
Files changed (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +212 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. package/vitest.config.js +2 -2
@@ -0,0 +1,290 @@
1
+ // C:/Dev/hackersonline-engine/client/src/FlexNode.jsx
2
+ import React, { useEffect, useRef } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import { FaPlus, FaTrash, FaEdit, FaRegSquare, FaRegCheckSquare, FaBox, FaExpandArrowsAlt, FaObjectGroup } from 'react-icons/fa';
5
+ // Remove Droppable, Draggable from '@hello-pangea/dnd' if no longer used
6
+ import { useTranslation } from 'react-i18next';
7
+ // Supprimer l'import de useDragAndDrop s'il n'est plus utilisé localement
8
+ // import useDragAndDrop from './hooks/useDragAndDrop.js';
9
+
10
+ export const FieldSelectorModal = ({ isOpen, onClose, modelFields, allModels, onSelectField }) => {
11
+ const { t } = useTranslation();
12
+ if (!isOpen) return null;
13
+ const getFieldDisplayName = (field) => {
14
+ let displayName = field.displayName || field.name;
15
+ if (field.modelName && field.modelName !== field.originalModelName) {
16
+ displayName = `${t(`model_${field.modelName}`, field.modelName)}.${displayName}`;
17
+ }
18
+ return displayName;
19
+ };
20
+ const renderFieldItem = (field, index) => (
21
+ <li key={`${field.path}-${index}`} onClick={() => onSelectField(field)} title={field.path}>
22
+ {getFieldDisplayName(field)}
23
+ <span className="field-path">({field.type}{field.relation ? ` -> ${field.relation}` : ''})</span>
24
+ </li>
25
+ );
26
+ const groupedFields = modelFields.reduce((acc, field) => {
27
+ const modelName = field.modelName || t('flexBuilder.currentModel', 'Modle actuel');
28
+ if (!acc[modelName]) { acc[modelName] = []; }
29
+ acc[modelName].push(field);
30
+ return acc;
31
+ }, {});
32
+ return (
33
+ <div className="flex-builder-modal-overlay" onClick={onClose}>
34
+ <div className="flex-builder-modal-content" onClick={e => e.stopPropagation()}>
35
+ <h4>{t('flexBuilder.selectDataFieldTitle', 'Sélectionner un champ de données')}</h4>
36
+ {Object.keys(groupedFields).length > 0 ? (
37
+ Object.entries(groupedFields).map(([modelName, fields]) => (
38
+ <div key={modelName}>
39
+ <h5>{modelName === t(`model_${modelName}`, modelName)}</h5>
40
+ <ul className="field-selector-list">{fields.map(renderFieldItem)}</ul>
41
+ </div>
42
+ ))
43
+ ) : (<p>{t('flexBuilder.noFieldsAvailable', 'Aucun champ disponible pour ce modèle.')}</p>)}
44
+ <button onClick={onClose} className="btn-cancel">{t('cancel', 'Annuler')}</button>
45
+ </div>
46
+ </div>
47
+ );
48
+ };
49
+ FieldSelectorModal.propTypes = {
50
+ isOpen: PropTypes.bool.isRequired,
51
+ onClose: PropTypes.func.isRequired,
52
+ modelFields: PropTypes.array.isRequired,
53
+ allModels: PropTypes.array.isRequired,
54
+ onSelectField: PropTypes.func.isRequired,
55
+ };
56
+
57
+ export const FlexNodeRenderer = React.forwardRef((props, ref) => {
58
+ const {
59
+ node, path, onSelectNode, selectedNodeId, data, dataIndexRef, modelFields, t, onMoveNode,
60
+ dnd, // Recevoir l'objet dnd en tant que prop
61
+ } = props;
62
+
63
+ // Déstructurer les valeurs et gestionnaires du prop dnd
64
+ const {
65
+ isDragging, draggedItemRef, dragOverContainerId,
66
+ handleNodeDragStart, handleContainerDragEnter, handleContainerDragLeave,
67
+ handleItemDragEnd,
68
+ finalizeDropAndGetData
69
+ } = dnd;
70
+
71
+ const isSelected = selectedNodeId === node.id;
72
+
73
+ const handleNodeClick = (e) => {
74
+ e.stopPropagation();
75
+ onSelectNode(node.id);
76
+ };
77
+
78
+ const getNestedValue = (obj, fieldPath) => {
79
+ if (!obj || !fieldPath) return 'N/A';
80
+ return fieldPath.split('.').reduce((o, k) => (o && o[k] !== undefined) ? o[k] : 'N/A', obj);
81
+ };
82
+
83
+ const isCurrentlyDraggingThisNode = isDragging && draggedItemRef.current === node.id;
84
+ // Correction: dragOverContainerId vient directement du hook/prop, pas besoin de le recalculer ici
85
+ // const currentDragOverContainerId = node.type === 'container' ? node.id : (node.content?.nestedContainer?.id);
86
+ // const isContainerBeingDraggedOver = dragOverContainerId === currentDragOverContainerId;
87
+ // Utiliser directement dragOverContainerId pour vérifier si CE conteneur est survolé
88
+ const isThisContainerBeingDraggedOver = (node.type === 'container' && dragOverContainerId === node.id) ||
89
+ (node.type === 'item' && node.content?.type === 'nestedContainer' && dragOverContainerId === node.content.nestedContainer.id);
90
+
91
+
92
+ const onItemDragStartInternal = (e, nodeId) => {
93
+ console.log("FlexNode: Item drag start", nodeId);
94
+ e.dataTransfer.setData("text/plain", nodeId);
95
+ e.dataTransfer.effectAllowed = "move";
96
+ handleNodeDragStart(nodeId); // Utiliser la fonction du prop dnd
97
+ };
98
+
99
+ const onItemDragEndInternal = () => {
100
+ console.log("FlexNode: Item drag end. Current draggedItemRef before hook.handleItemDragEnd:", draggedItemRef.current);
101
+ handleItemDragEnd(); // Utiliser la fonction du prop dnd
102
+ };
103
+
104
+ const onContainerDragEnterInternal = (e, containerId) => {
105
+ e.preventDefault();
106
+ console.log("FlexNode: Container drag enter", containerId);
107
+ handleContainerDragEnter(containerId); // Utiliser la fonction du prop dnd
108
+ };
109
+
110
+ const onContainerDragOverInternal = (e) => {
111
+ e.preventDefault();
112
+ e.dataTransfer.dropEffect = "move";
113
+ };
114
+
115
+ const onContainerDragLeaveInternal = () => {
116
+ console.log("FlexNode: Container drag leave");
117
+ handleContainerDragLeave(); // Utiliser la fonction du prop dnd
118
+ };
119
+
120
+ const onContainerDropInternal = (e) => {
121
+ e.preventDefault();
122
+ e.stopPropagation();
123
+
124
+ const droppedItemId = finalizeDropAndGetData(); // Utiliser la fonction du prop dnd
125
+ const targetContainerNode = node.type === 'container' ? node : node.content?.nestedContainer;
126
+ const targetContainerId = targetContainerNode?.id;
127
+
128
+
129
+ console.log("FlexNode: Drop on container. Dropped Item ID:", droppedItemId, "Target Container ID:", targetContainerId);
130
+
131
+ if (typeof onMoveNode === 'function' && droppedItemId && targetContainerId) {
132
+ // TODO: Déterminer le bon newIndex
133
+ const newIndex = 0;
134
+ onMoveNode(droppedItemId, targetContainerId, newIndex);
135
+ } else {
136
+ console.warn("FlexNode: onMoveNode not called. Check props or IDs.", { onMoveNodeExists: typeof onMoveNode === 'function', droppedItemId, targetContainerId });
137
+ }
138
+ };
139
+
140
+ // useEffect(() => {
141
+ // console.log(`FlexNode (${node.id} - ${node.type}): onMoveNode type:`, typeof onMoveNode);
142
+ // }, [onMoveNode, node.id, node.type]);
143
+
144
+
145
+ if (node.type === 'container' || (node.type === 'item' && node.content?.type === 'nestedContainer')) {
146
+ const containerNode = node.type === 'container' ? node : node.content.nestedContainer;
147
+ const children = containerNode.children || [];
148
+ const containerStyle = node.type === 'item' ?
149
+ { ...containerNode.containerStyle, padding: '0', border: 'none', minHeight: '40px' }
150
+ : containerNode.containerStyle;
151
+
152
+ const isThisNodeSelected = isSelected && (node.id === selectedNodeId || (node.type === 'item' && containerNode.id === selectedNodeId));
153
+ // Correction pour isThisContainerBeingDraggedOver
154
+ const isThisSpecificContainerDraggedOver = dragOverContainerId === containerNode.id;
155
+
156
+
157
+ return (
158
+ <div
159
+ ref={ref}
160
+ style={node.type === 'item' ? node.itemStyle : {}}
161
+ className={`flex-node-wrapper ${node.type === 'item' ? 'is-nested-container-wrapper' : ''} ${isCurrentlyDraggingThisNode && node.type === 'item' ? 'is-dragging-dnd' : ''}`}
162
+ draggable={node.type === 'item'}
163
+ onDragStart={node.type === 'item' ? (e) => onItemDragStartInternal(e, node.id) : undefined}
164
+ onDragEnd={node.type === 'item' ? onItemDragEndInternal : undefined}
165
+ onClick={node.type === 'item' ? handleNodeClick : undefined} // Permet de s'item qui est un conteneur imbriqué
166
+ >
167
+ <div
168
+ style={containerStyle}
169
+ className={`flex-node preview-container ${isThisNodeSelected ? 'is-selected' : ''} ${isThisSpecificContainerDraggedOver ? 'drag-over-active' : ''}`}
170
+ onClick={node.type === 'container' ? handleNodeClick : (e) => e.stopPropagation()} // Le clic sur le conteneur racine le sélectionne
171
+ onDragEnter={(e) => onContainerDragEnterInternal(e, containerNode.id)}
172
+ onDragLeave={onContainerDragLeaveInternal}
173
+ onDragOver={onContainerDragOverInternal}
174
+ onDrop={onContainerDropInternal}
175
+ >
176
+ {children.length === 0 && !isThisSpecificContainerDraggedOver && (
177
+ <span className="empty-container-placeholder">{t('flexBuilder.emptyContainerHint', 'Glissez des i ou ajoutez-en via les contrôles.')}</span>
178
+ )}
179
+ {children.map((childNode, index) => (
180
+ <FlexNodeRenderer
181
+ key={childNode.id}
182
+ node={childNode}
183
+ path={[...path, index]}
184
+ onSelectNode={onSelectNode}
185
+ selectedNodeId={selectedNodeId}
186
+ data={data}
187
+ dataIndexRef={dataIndexRef}
188
+ modelFields={modelFields}
189
+ t={t}
190
+ dnd={dnd} // Passer le prop dnd récursivement
191
+ onMoveNode={onMoveNode}
192
+ />
193
+ ))}
194
+ </div>
195
+ </div>
196
+ );
197
+ } else if (node.type === 'item') {
198
+ const itemStyleWithDefaults = {
199
+ display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'grab', ...node.itemStyle,
200
+ };
201
+ if (isCurrentlyDraggingThisNode) {
202
+ itemStyleWithDefaults.opacity = 0.7;
203
+ itemStyleWithDefaults.boxShadow = '0 5px 15px rgba(0,0,0,0.3)';
204
+ }
205
+
206
+ let contentElement;
207
+ if (node.content.type === 'placeholder') {
208
+ contentElement = <span className="preview-item-content">{t('flexBuilder.emptyItemHint', 'Case vide')}</span>;
209
+ } else if (node.content.type === 'dataField' && node.content.mapping) {
210
+ // Logs de débogage pour dataField (vous pouvez les garder ou les commenter)
211
+ console.log(`[FlexNodeRenderer - ${node.id}] Rendu DataField:`, {
212
+ nodeContent: JSON.parse(JSON.stringify(node.content)),
213
+ nodeMapping: JSON.parse(JSON.stringify(node.content.mapping)),
214
+ fieldPathToUse: node.content.mapping?.fieldPath,
215
+ dataPropLength: data?.length,
216
+ dataIndexAtStartOfItem: dataIndexRef.current,
217
+ });
218
+
219
+ let displayValue;
220
+ const fieldPathForDisplay = node.content.mapping?.displayName || node.content.mapping?.fieldPath || t('flexBuilder.noFieldSelected', 'Champ non lié');
221
+
222
+ if (data && data.length > 0) {
223
+ let currentDataRecord = {};
224
+ let recordIndex = -1;
225
+ recordIndex = dataIndexRef.current % data.length;
226
+ currentDataRecord = data[recordIndex];
227
+
228
+ console.log(`[FlexNodeRenderer - ${node.id}] currentDataRecord (index ${recordIndex}):`, JSON.parse(JSON.stringify(currentDataRecord)));
229
+
230
+ const value = node.content.mapping?.fieldPath
231
+ ? getNestedValue(currentDataRecord, node.content.mapping.fieldPath)
232
+ : t('flexBuilder.noDataMapped', 'N/A');
233
+
234
+ console.log(`[FlexNodeRenderer - ${node.id}] Valeur récupérée pour ${node.content.mapping?.fieldPath}:`, value);
235
+
236
+ displayValue = `${fieldPathForDisplay}`;
237
+
238
+ if (data && data.length > 0) {
239
+ dataIndexRef.current = (dataIndexRef.current + 1);
240
+ }
241
+ } else {
242
+ // Si data est vide, afficher le nom du champ
243
+ console.warn(`[FlexNodeRenderer - ${node.id}] data prop est vide ou null. Affichage du nom du champ.`);
244
+ displayValue = `${fieldPathForDisplay}`; // Affiche le nom du champ entre crochets
245
+ }
246
+
247
+ contentElement = <span className="preview-item-content" title={node.content.mapping?.fieldPath || 'Champ non lié'}>{displayValue}</span>;
248
+
249
+ } else {
250
+ contentElement = <span className="preview-item-content">{node.content.type}</span>;
251
+ }
252
+
253
+ return (
254
+ <div
255
+ ref={ref}
256
+ style={itemStyleWithDefaults}
257
+ className={`flex-node preview-item ${isSelected ? 'is-selected' : ''} ${isCurrentlyDraggingThisNode ? 'is-dragging-dnd' : ''}`}
258
+ onClick={handleNodeClick}
259
+ draggable // Les items sont toujours déplaçables
260
+ onDragStart={(e) => onItemDragStartInternal(e, node.id)}
261
+ onDragEnd={onItemDragEndInternal}
262
+ >
263
+ {contentElement}
264
+ </div>
265
+ );
266
+ }
267
+
268
+ return (
269
+ <div ref={ref} onClick={handleNodeClick} style={{ padding: '10px', border: '1px dashed red' }}>
270
+ {t('flexBuilder.unknownNodeType', 'Type de nœud inconnu')}: {node.type}
271
+ </div>
272
+ );
273
+ });
274
+
275
+ FlexNodeRenderer.displayName = 'FlexNodeRenderer';
276
+ FlexNodeRenderer.propTypes = {
277
+ node: PropTypes.object.isRequired,
278
+ path: PropTypes.array.isRequired,
279
+ onSelectNode: PropTypes.func.isRequired,
280
+ selectedNodeId: PropTypes.string,
281
+ data: PropTypes.array,
282
+ dataIndexRef: PropTypes.object,
283
+ modelFields: PropTypes.array,
284
+ t: PropTypes.func.isRequired,
285
+ onMoveNode: PropTypes.func,
286
+ dnd: PropTypes.object.isRequired, // dnd est maintenant requis
287
+ };
288
+ FlexNodeRenderer.defaultProps = {
289
+ data: [], modelFields: [], onMoveNode: null,
290
+ };
@@ -0,0 +1,129 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ /**
4
+ * Crée un nouveau nœud de type 'container' ou 'item'.
5
+ */
6
+ export const createNewNode = (type) => {
7
+ const baseStyle = { border: '1px solid #ddd', padding: '5px' };
8
+ if (type === 'container') {
9
+ return {
10
+ id: uuidv4(), type: 'container',
11
+ containerStyle: { ...baseStyle, display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '5px', minHeight: '50px'},
12
+ children: [],
13
+ };
14
+ }
15
+ return {
16
+ id: uuidv4(), type: 'item',
17
+ itemStyle: { ...baseStyle, width: '', height: '', display: 'flex', alignItems: 'center', justifyContent: 'center' },
18
+ content: { type: 'placeholder' },
19
+ };
20
+ };
21
+
22
+ /**
23
+ * Nettoie un nœud pour la sauvegarde.
24
+ */
25
+ export const cleanNodeForOutput = (node) => {
26
+ const { id, type, children, content, containerStyle, itemStyle } = node;
27
+ const outputNode = { id, type };
28
+
29
+ if (type === 'container') {
30
+ outputNode.containerStyle = containerStyle || {};
31
+ outputNode.children = children ? children.map(cleanNodeForOutput) : [];
32
+ } else if (type === 'item') {
33
+ outputNode.itemStyle = itemStyle || {};
34
+ if (content) {
35
+ outputNode.content = { type: content.type };
36
+ if (content.type === 'dataField' && content.mapping) {
37
+ outputNode.content.mapping = content.mapping;
38
+ } else if (content.type === 'nestedContainer' && content.nestedContainer) {
39
+ outputNode.content.nestedContainer = cleanNodeForOutput(content.nestedContainer);
40
+ } else if (content.type === 'richtext' && content.html) {
41
+ outputNode.content.html = content.html;
42
+ }
43
+ } else {
44
+ outputNode.content = { type: 'placeholder' };
45
+ }
46
+ }
47
+ return outputNode;
48
+ };
49
+
50
+ /**
51
+ * Trouve un nœud par son ID de manière récursive.
52
+ */
53
+ export const findNodeRecursive = (nodes, id) => {
54
+ for (const node of nodes) {
55
+ if (node.id === id) return node;
56
+ if (node.type === 'container' && node.children) {
57
+ const found = findNodeRecursive(node.children, id);
58
+ if (found) return found;
59
+ } else if (node.type === 'item' && node.content.type === 'nestedContainer' && node.content.nestedContainer) {
60
+ if (node.content.nestedContainer.id === id) return node.content.nestedContainer;
61
+ const found = findNodeRecursive(node.content.nestedContainer.children, id);
62
+ if (found) return found;
63
+ }
64
+ }
65
+ return null;
66
+ };
67
+
68
+ /**
69
+ * Met de mise à jour.
70
+ */
71
+ export const updateNodeRecursive = (nodes, nodeId, updateFn) => {
72
+ return nodes.map(node => {
73
+ if (node.id === nodeId) return updateFn(node);
74
+ if (node.type === 'container' && node.children) {
75
+ return { ...node, children: updateNodeRecursive(node.children, nodeId, updateFn) };
76
+ }
77
+ if (node.type === 'item' && node.content.type === 'nestedContainer' && node.content.nestedContainer) {
78
+ const nested = node.content.nestedContainer;
79
+ if (nested.id === nodeId) {
80
+ return { ...node, content: { ...node.content, nestedContainer: updateFn(nested) }};
81
+ }
82
+ const updatedChildren = updateNodeRecursive(nested.children, nodeId, updateFn);
83
+ if (updatedChildren !== nested.children) {
84
+ return { ...node, content: { ...node.content, nestedContainer: { ...nested, children: updatedChildren }}};
85
+ }
86
+ }
87
+ return node;
88
+ });
89
+ };
90
+
91
+ /**
92
+ * Supprime un nœud de manière récursive.
93
+ */
94
+ export const deleteNodeRecursive = (nodes, nodeId) => {
95
+ const filteredNodes = nodes.filter(node => node.id !== nodeId);
96
+ if (filteredNodes.length < nodes.length) return filteredNodes;
97
+
98
+ return nodes.map(node => {
99
+ if (node.type === 'container' && node.children) {
100
+ const updatedChildren = deleteNodeRecursive(node.children, nodeId);
101
+ if (updatedChildren !== node.children) return { ...node, children: updatedChildren };
102
+ } else if (node.type === 'item' && node.content.type === 'nestedContainer' && node.content.nestedContainer) {
103
+ const nested = node.content.nestedContainer;
104
+ if (nested.id === nodeId) return { ...node, content: { type: 'placeholder' } }; // Reset item
105
+ const updatedNestedChildren = deleteNodeRecursive(nested.children, nodeId);
106
+ if (updatedNestedChildren !== nested.children) {
107
+ return { ...node, content: { ...node.content, nestedContainer: { ...nested, children: updatedNestedChildren }}};
108
+ }
109
+ }
110
+ return node;
111
+ });
112
+ };
113
+
114
+ /**
115
+ * Efface les mappings de données de manière récursive.
116
+ */
117
+ export const clearMappingsRecursive = (nodes) => nodes.map(node => {
118
+ if (node.type === 'item') {
119
+ if (node.content.type === 'dataField') {
120
+ return { ...node, content: { type: 'placeholder' } };
121
+ }
122
+ if (node.content.type === 'nestedContainer' && node.content.nestedContainer) {
123
+ return { ...node, content: { ...node.content, nestedContainer: { ...node.content.nestedContainer, children: clearMappingsRecursive(node.content.nestedContainer.children) } } };
124
+ }
125
+ } else if (node.type === 'container') {
126
+ return { ...node, children: clearMappingsRecursive(node.children) };
127
+ }
128
+ return node;
129
+ });
@@ -0,0 +1,46 @@
1
+ // client/src/HiddenableCell.jsx
2
+ import React, { useState } from 'react';
3
+ import { FaEye, FaEyeSlash } from 'react-icons/fa';
4
+ import { useTranslation } from 'react-i18next';
5
+ import './HiddenableCell.scss';
6
+
7
+ export const HiddenableCell = ({ value }) => {
8
+ const { t } = useTranslation();
9
+ const [isRevealed, setIsRevealed] = useState(false);
10
+
11
+ const toggleVisibility = () => {
12
+ setIsRevealed(!isRevealed);
13
+ };
14
+
15
+ return (
16
+ <div className="hiddenable-cell">
17
+ {isRevealed ? (
18
+ // --- État visible ---
19
+ <>
20
+ <span className="revealed-content">{value}</span>
21
+ <button
22
+ type="button"
23
+ onClick={toggleVisibility}
24
+ className="hide-toggle-btn"
25
+ title={t('tooltip.hideContent', 'Cacher le contenu')}
26
+ >
27
+ <FaEyeSlash />
28
+ </button>
29
+ </>
30
+ ) : (
31
+ // --- État masqué (par défaut) ---
32
+ <>
33
+ <span className="hidden-placeholder">••••••••••</span>
34
+ <button
35
+ type="button"
36
+ onClick={toggleVisibility}
37
+ className="hide-toggle-btn"
38
+ title={t('tooltip.showHiddenContent', 'Afficher le contenu')}
39
+ >
40
+ <FaEye />
41
+ </button>
42
+ </>
43
+ )}
44
+ </div>
45
+ );
46
+ };
@@ -0,0 +1,36 @@
1
+ // client/src/HiddenableCell.scss
2
+
3
+ .hiddenable-cell {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: space-between; // Aligne le bouton n gap: 8px; // Espace entre le contenu et le bouton
7
+ width: 100%;
8
+
9
+ .hidden-placeholder, .revealed-content {
10
+ flex-grow: 1; // Le contenu prend l'espace disponible
11
+ white-space: nowrap;
12
+ overflow: hidden;
13
+ text-overflow: ellipsis; // Ajoute "..." si le contenu est trop long
14
+ }
15
+
16
+ .hidden-placeholder {
17
+ color: #999;
18
+ }
19
+
20
+ .hide-toggle-btn {
21
+ background: none;
22
+ border: none;
23
+ cursor: pointer;
24
+ color: #6c757d;
25
+ padding: 2px 4px;
26
+ display: flex;
27
+ align-items: center;
28
+ border-radius: 4px;
29
+ transition: background-color 0.2s, color 0.2s;
30
+
31
+ &:hover {
32
+ background-color: #f0f0f0;
33
+ color: #333;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,39 @@
1
+ import React, { useState } from 'react';
2
+ import { Dialog } from './Dialog.jsx'; // Assure-toi que Dialog est importé
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ function KPIDialog({ availableKpis = [], onAddKpi, onClose }) {
6
+ const { t } = useTranslation();
7
+ const [searchTerm, setSearchTerm] = useState('');
8
+
9
+ const filteredKpis = (availableKpis || []).filter(kpi =>
10
+ kpi.name.value.toLowerCase().includes(searchTerm.toLowerCase())
11
+ );
12
+
13
+ return (
14
+ <Dialog isModal={true} isClosable={true} onClose={onClose} className="add-kpi-dialog">
15
+ <h2>{t('dashboards.add_kpi_title', 'Ajouter un indicateur (kpi)')}</h2>
16
+ <input
17
+ type="text"
18
+ placeholder={t('dashboards.search_kpi', 'Rechercher un KPI...')}
19
+ value={searchTerm}
20
+ onChange={(e) => setSearchTerm(e.target.value)}
21
+ style={{ width: '100%', marginBottom: '15px' }}
22
+ />
23
+ <ul className="kpi-selection-list">
24
+ {filteredKpis.length > 0 ? (
25
+ filteredKpis.map(kpi => (
26
+ <li key={kpi._id} onClick={() => onAddKpi(kpi)}>
27
+ {kpi.name.value || (typeof(kpi.name) === 'string' ? kpi.name : '')}
28
+ {/* Tu peux ajouter l'icône ou l'unité ici pour aider oisir */}
29
+ </li>
30
+ ))
31
+ ) : (
32
+ <li>{t('dashboards.no_kpi_found', 'Aucun KPI disponible ou correspondant.')}</li>
33
+ )}
34
+ </ul>
35
+ </Dialog>
36
+ );
37
+ }
38
+
39
+ export default KPIDialog;