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,269 @@
1
+ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import './FlexBuilder.scss';
4
+ import { useAuthContext } from "./contexts/AuthContext.jsx";
5
+ import { useTranslation } from "react-i18next";
6
+ import { applyFilter } from "data-primals-engine/data";
7
+ import { FieldSelectorModal } from "./FlexNode.jsx";
8
+ import { deepInsertNode, deepRemoveNode } from "./FlexBuilderUtils.js";
9
+ import useDragAndDrop from './hooks/useDragAndDrop.js';
10
+ import RichTextEditorModal from "./RichTextEditorModal.jsx";
11
+ import FlexBuilderControls from './FlexBuilderControls.jsx';
12
+ import FlexBuilderPreview from './FlexBuilderPreview.jsx';
13
+ import {
14
+ createNewNode,
15
+ cleanNodeForOutput,
16
+ findNodeRecursive,
17
+ updateNodeRecursive,
18
+ deleteNodeRecursive,
19
+ clearMappingsRecursive
20
+ } from './FlexTreeUtils.js';
21
+ import {DialogProvider} from "./Dialog.jsx";
22
+
23
+ const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], lang = 'fr' }) => {
24
+ const { me: user } = useAuthContext();
25
+ const { t } = useTranslation();
26
+ const dnd = useDragAndDrop();
27
+
28
+ const [editingHtmlInfo, setEditingHtmlInfo] = useState(null);
29
+
30
+ const defaultConfig = useMemo(() => ({
31
+ id: 'root', type: 'container',
32
+ containerStyle: { customCss: '', display: 'flex', flexDirection: 'column', flexWrap: 'nowrap', gap: '10px', padding: '10px', border: '1px dashed #999', width: '100%' },
33
+ children: [createNewNode('container')],
34
+ selectedModelName: null, dataFilter: null,
35
+ dataLimit: 3, refreshInterval: 60000,
36
+ }), [createNewNode]);
37
+
38
+ // --- CORRECTION ---
39
+ // Fonction utilitaire pour construire l'état à partir de la configuration chargée.
40
+ // Elle fusionne correctement les données par défaut, les métadonnées et la structure de la vue.
41
+ const buildStateFromConfig = useCallback((config) => {
42
+ if (config && config.flexStructure) {
43
+ const { flexStructure: structureData, ...metaData } = config;
44
+ return { ...defaultConfig, ...metaData, ...structureData };
45
+ }
46
+ return defaultConfig;
47
+ }, [defaultConfig]);
48
+
49
+ const [flexStructure, setFlexStructure] = useState(() => buildStateFromConfig(initialConfig));
50
+
51
+ const [selectedNodeId, setSelectedNodeId] = useState(null);
52
+ const [isFieldSelectorOpen, setIsFieldSelectorOpen] = useState(false);
53
+ const currentlyEditingViewIdRef = useRef(initialConfig?.id || null);
54
+
55
+ useEffect(() => {
56
+ const newViewId = initialConfig?.id || null;
57
+ if (newViewId !== currentlyEditingViewIdRef.current) {
58
+ // Utilise la même logique de construction d'état lors du changement de vue
59
+ setFlexStructure(buildStateFromConfig(initialConfig));
60
+ setSelectedNodeId(null);
61
+ currentlyEditingViewIdRef.current = newViewId;
62
+ }
63
+ }, [initialConfig, buildStateFromConfig]);
64
+ // --- FIN DE LA CORRECTION ---
65
+
66
+ const selectedNode = useMemo(() => {
67
+ if (!selectedNodeId) return null;
68
+ if (flexStructure.id === selectedNodeId) return flexStructure;
69
+ return findNodeRecursive(flexStructure.children, selectedNodeId);
70
+ }, [flexStructure, selectedNodeId]);
71
+
72
+ const currentModelFields = useMemo(() => {
73
+ if (!flexStructure.selectedModelName || !Array.isArray(models)) return [];
74
+ const model = models.find(m => m.name === flexStructure.selectedModelName);
75
+ return model?.fields || [];
76
+ }, [flexStructure.selectedModelName, models]);
77
+
78
+ const filteredData = useMemo(() => {
79
+ if (!flexStructure.selectedModelName) return [];
80
+ return applyFilter(data || [], flexStructure.dataFilter);
81
+ }, [data, flexStructure.dataFilter, flexStructure.selectedModelName]);
82
+
83
+ const dataIndexRef = useRef(0);
84
+ useEffect(() => { dataIndexRef.current = 0; }, [filteredData, flexStructure]);
85
+
86
+ useEffect(() => {
87
+ if (onChange) {
88
+ const { children, containerStyle, id, type, ...rootProps } = flexStructure;
89
+ const output = {
90
+ ...rootProps,
91
+ id: currentlyEditingViewIdRef.current,
92
+ flexStructure: cleanNodeForOutput({ children, containerStyle, id, type }),
93
+ };
94
+ onChange(output);
95
+ }
96
+ }, [flexStructure, onChange]);
97
+
98
+ const handleUpdateNode = (nodeId, propertyPath, value) => {
99
+ setFlexStructure(prev => {
100
+ const updateFn = (nodeToUpdate) => {
101
+ const newNode = JSON.parse(JSON.stringify(nodeToUpdate)); // Deep copy for safety
102
+ let current = newNode;
103
+ const pathArray = propertyPath.split('.');
104
+ for (let i = 0; i < pathArray.length - 1; i++) {
105
+ current[pathArray[i]] = current[pathArray[i]] || {};
106
+ current = current[pathArray[i]];
107
+ }
108
+ current[pathArray[pathArray.length - 1]] = value;
109
+ return newNode;
110
+ };
111
+
112
+ if (nodeId === prev.id) {
113
+ if (['selectedModelName', 'dataFilter', 'dataLimit', 'refreshInterval'].includes(propertyPath)) {
114
+ return { ...prev, [propertyPath]: value };
115
+ }
116
+ return updateFn(prev);
117
+ }
118
+ return { ...prev, children: updateNodeRecursive(prev.children, nodeId, updateFn) };
119
+ });
120
+ };
121
+
122
+ const handleAddChildNode = (parentId, type) => {
123
+ const newNode = createNewNode(type);
124
+ const updateFn = (node) => ({ ...node, children: [...(node.children || []), newNode] });
125
+ setFlexStructure(prev => {
126
+ if (prev.id === parentId) return updateFn(prev);
127
+ return { ...prev, children: updateNodeRecursive(prev.children, parentId, updateFn) };
128
+ });
129
+ setSelectedNodeId(newNode.id);
130
+ };
131
+
132
+ const handleMakeItemNestedContainer = (itemId) => {
133
+ const updateFn = (node) => {
134
+ if (node.type === 'item' && node.content.type !== 'nestedContainer') {
135
+ const newNestedContainer = createNewNode('container');
136
+ newNestedContainer.children = [createNewNode('item')];
137
+ return { ...node, content: { type: 'nestedContainer', nestedContainer: newNestedContainer } };
138
+ }
139
+ return node;
140
+ };
141
+ setFlexStructure(prev => ({ ...prev, children: updateNodeRecursive(prev.children, itemId, updateFn) }));
142
+ };
143
+
144
+ const handleDeleteNode = () => {
145
+ if (!selectedNodeId || selectedNodeId === flexStructure.id) {
146
+ alert(t('flexBuilder.cannotDeleteRoot'));
147
+ return;
148
+ }
149
+ if (window.confirm(t('flexBuilder.confirmDeleteNode'))) {
150
+ setFlexStructure(prev => ({ ...prev, children: deleteNodeRecursive(prev.children, selectedNodeId) }));
151
+ setSelectedNodeId(null);
152
+ }
153
+ };
154
+
155
+ const handleModelChange = (modelName) => {
156
+ setFlexStructure(prev => ({
157
+ ...prev,
158
+ selectedModelName: modelName,
159
+ dataFilter: null,
160
+ children: clearMappingsRecursive(prev.children)
161
+ }));
162
+ setSelectedNodeId(null);
163
+ };
164
+
165
+ const handleFieldSelectedForMapping = (field) => {
166
+ if (selectedNode && selectedNode.type === 'item') {
167
+ const fieldPath = field.path || field.name;
168
+ const displayName = field.displayName || field.name;
169
+ if (!fieldPath) return;
170
+ const newContent = { type: 'dataField', mapping: { fieldPath, displayName } };
171
+ handleUpdateNode(selectedNode.id, 'content', newContent);
172
+ }
173
+ setIsFieldSelectorOpen(false);
174
+ };
175
+
176
+ const handleMoveNode = useCallback((nodeId, newParentId, newIndex) => {
177
+ if (!nodeId || !newParentId) {
178
+ dnd.handleItemDragEnd();
179
+ return;
180
+ }
181
+ setFlexStructure(prev => {
182
+ const removal = deepRemoveNode(prev, nodeId);
183
+ if (!removal.removedNode) return prev;
184
+ const insertion = deepInsertNode(removal.newStructure, newParentId, removal.removedNode, newIndex);
185
+ if (!insertion.inserted) return prev;
186
+ if (selectedNodeId === nodeId) setSelectedNodeId(null);
187
+ return insertion.newStructure;
188
+ });
189
+ }, [dnd, selectedNodeId]);
190
+
191
+ const handleSaveHtmlContent = (newHtml) => {
192
+ if (editingHtmlInfo) {
193
+ const newContent = { type: 'richtext', html: newHtml };
194
+ handleUpdateNode(editingHtmlInfo.nodeId, 'content', newContent);
195
+ setEditingHtmlInfo(null);
196
+ }
197
+ };
198
+
199
+ const modelOptions = useMemo(() => models.map(m => ({ value: m.name, label: t(`model_${m.name}`, m.name) })), [models, t]);
200
+ const flexOptions = useMemo(() => ({
201
+ flexDirection: ['row', 'row-reverse', 'column', 'column-reverse'],
202
+ flexWrap: ['nowrap', 'wrap', 'wrap-reverse'],
203
+ justifyContent: ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly'],
204
+ alignItems: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline'],
205
+ }), []);
206
+
207
+ return (
208
+ <div className="flex-builder">
209
+ <FlexBuilderControls
210
+ flexStructure={flexStructure}
211
+ selectedNode={selectedNode}
212
+ models={models}
213
+ user={user}
214
+ lang={lang}
215
+ modelOptions={modelOptions}
216
+ flexOptions={flexOptions}
217
+ currentModelFields={currentModelFields}
218
+ onModelChange={handleModelChange}
219
+ onUpdateNode={handleUpdateNode}
220
+ onDeleteNode={handleDeleteNode}
221
+ onAddChildNode={handleAddChildNode}
222
+ onMakeItemNestedContainer={handleMakeItemNestedContainer}
223
+ onSetIsFieldSelectorOpen={setIsFieldSelectorOpen}
224
+ onSetEditingHtmlInfo={setEditingHtmlInfo}
225
+ />
226
+
227
+ <FlexBuilderPreview
228
+ flexStructure={flexStructure}
229
+ onSelectNode={setSelectedNodeId}
230
+ selectedNodeId={selectedNodeId}
231
+ filteredData={filteredData}
232
+ dataIndexRef={dataIndexRef}
233
+ modelFields={currentModelFields}
234
+ dnd={dnd}
235
+ onMoveNode={handleMoveNode}
236
+ />
237
+
238
+ <DialogProvider>
239
+ {selectedNode?.type === 'item' && (
240
+ <FieldSelectorModal
241
+ isOpen={isFieldSelectorOpen}
242
+ onClose={() => setIsFieldSelectorOpen(false)}
243
+ modelFields={currentModelFields}
244
+ allModels={models}
245
+ onSelectField={handleFieldSelectedForMapping}
246
+ />
247
+ )}
248
+
249
+ {editingHtmlInfo && (
250
+ <RichTextEditorModal
251
+ initialContent={editingHtmlInfo.initialContent}
252
+ onClose={() => setEditingHtmlInfo(null)}
253
+ onSave={handleSaveHtmlContent}
254
+ />
255
+ )}
256
+ </DialogProvider>
257
+ </div>
258
+ );
259
+ };
260
+
261
+ FlexBuilder.propTypes = {
262
+ initialConfig: PropTypes.object,
263
+ onChange: PropTypes.func.isRequired,
264
+ data: PropTypes.array,
265
+ models: PropTypes.array.isRequired,
266
+ lang: PropTypes.string,
267
+ };
268
+
269
+ export default FlexBuilder;
@@ -0,0 +1,226 @@
1
+ // src/components/FlexBuilder.scss
2
+
3
+ .flex-builder {
4
+ display: flex;
5
+ font-family: sans-serif;
6
+ border: 1px solid #ccc;
7
+ border-radius: 4px;
8
+ min-height: 600px; // Give it some space
9
+ background-color: #f9f9f9;
10
+
11
+ .flex-builder-controls {
12
+ width: 350px;
13
+ padding: 15px;
14
+ background-color: #fff;
15
+ border-right: 1px solid #ccc;
16
+ overflow-y: auto;
17
+ box-shadow: 2px 0 5px rgba(0,0,0,0.05);
18
+
19
+ h3, h4 {
20
+ margin-top: 0;
21
+ margin-bottom: 10px;
22
+ padding-bottom: 5px;
23
+ border-bottom: 1px solid #eee;
24
+ color: #333;
25
+ }
26
+ h4 {
27
+ font-size: 1em;
28
+ margin-top: 15px;
29
+ display: flex;
30
+ justify-content: space-between;
31
+ align-items: center;
32
+ }
33
+
34
+ .control-group {
35
+ margin-bottom: 12px;
36
+ label {
37
+ display: block;
38
+ margin-bottom: 4px;
39
+ font-size: 0.9em;
40
+ color: #555;
41
+ font-weight: 500;
42
+ }
43
+ input[type="text"],
44
+ input[type="number"],
45
+ select,
46
+ .select-field-container .select-field { // Target your SelectField specifically
47
+ width: 100%;
48
+ padding: 8px;
49
+ border: 1px solid #ccc;
50
+ border-radius: 4px;
51
+ box-sizing: border-box;
52
+ font-size: 0.9em;
53
+ }
54
+ button {
55
+ padding: 6px 10px;
56
+ margin-right: 5px;
57
+ margin-top: 5px;
58
+ border: 1px solid #ccc;
59
+ background-color: #f0f0f0;
60
+ border-radius: 4px;
61
+ cursor: pointer;
62
+ font-size: 0.85em;
63
+ color: #575757;
64
+ &:hover {
65
+ background-color: #e0e0e0;
66
+ }
67
+ &.btn-delete-node {
68
+ background-color: transparent;
69
+ border: none;
70
+ color: #dc3545;
71
+ padding: 2px 5px;
72
+ &:hover {
73
+ color: #c82333;
74
+ background-color: rgba(220, 53, 69, 0.1);
75
+ }
76
+ }
77
+ }
78
+ &.actions {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ gap: 5px;
82
+ }
83
+ }
84
+ .selected-node-controls {
85
+ border-top: 2px solid #007bff;
86
+ padding-top: 10px;
87
+ margin-top: 15px;
88
+ }
89
+ .no-node-selected-hint {
90
+ color: #777;
91
+ font-style: italic;
92
+ text-align: center;
93
+ margin-top: 20px;
94
+ }
95
+ }
96
+
97
+ .flex-builder-preview-area {
98
+ flex-grow: 1;
99
+ padding: 15px;
100
+ overflow: auto; // Important for deeply nested structures
101
+
102
+ h3 {
103
+ margin-top: 0;
104
+ margin-bottom: 15px;
105
+ }
106
+
107
+ .flex-node { // Common styles for both container and item in preview
108
+ transition: outline 0.2s ease-in-out;
109
+ background-color: #f5f5f5;
110
+ &.is-selected {
111
+ outline: 2px solid #007bff !important; // Force outline for selected
112
+ box-shadow: 0 0 8px rgba(0, 123, 255, 0.5);
113
+ }
114
+ }
115
+
116
+ .preview-container {
117
+ // Default styles are applied via JS, this is for base and selection
118
+ border: 1px dashed #aaa; // Default border for all containers
119
+ padding: 8px; // Default padding
120
+ min-height: 40px; // Minimum height to be clickable
121
+ position: relative; // For placeholder text
122
+
123
+ &.is-selected {
124
+ border-style: solid;
125
+ border-color: #007bff;
126
+ }
127
+ &:hover:not(.is-selected) {
128
+ border-color: #66afe9;
129
+ }
130
+ .empty-container-placeholder {
131
+ position: absolute;
132
+ top: 50%;
133
+ left: 50%;
134
+ transform: translate(-50%, -50%);
135
+ color: #aaa;
136
+ font-size: 0.9em;
137
+ pointer-events: none; // Allow clicks to pass through to the container
138
+ }
139
+ }
140
+
141
+ .preview-item {
142
+ border: 1px solid #ccc;
143
+ padding: 8px;
144
+ min-height: 30px; // Minimum height
145
+ display: flex; // To center content by default
146
+ align-items: center;
147
+ justify-content: center;
148
+ text-align: center;
149
+ overflow: hidden;
150
+ word-break: break-word;
151
+ cursor: pointer;
152
+ background-color: #fff;
153
+
154
+ &.is-selected {
155
+ border-color: #007bff;
156
+ background-color: #e7f3ff;
157
+ }
158
+ &:hover:not(.is-selected) {
159
+ border-color: #66afe9;
160
+ background-color: #f8f9fa;
161
+ }
162
+
163
+ .preview-item-content {
164
+ font-size: 0.9em;
165
+ color: #333;
166
+ max-width: 100%;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap; // Prevent content from wrapping and breaking layout
170
+ }
171
+
172
+ // When an item contains a nested container
173
+ & > .preview-container { // Direct child container
174
+ margin: -8px; // Offset parent item's padding
175
+ width: calc(100% + 16px); // Expand to fill parent item, accounting for offset padding
176
+ height: calc(100% + 16px);
177
+ min-height: calc(100% + 16px); // Ensure it fills
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ // Styles for FieldSelectorModal (can be reused or adapted from previous version)
184
+ .flex-builder-modal-overlay {
185
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
186
+ background-color: rgba(0, 0, 0, 0.6);
187
+ display: flex; align-items: center; justify-content: center;
188
+ z-index: 1050; padding: 15px;
189
+ }
190
+ .flex-builder-modal-content {
191
+ background-color: white; padding: 25px; border-radius: 8px;
192
+ min-width: 320px; max-width: 600px; width: 90%; max-height: 85vh;
193
+ display: flex; flex-direction: column; box-shadow: 0 5px 20px rgba(0,0,0,0.25);
194
+ h4 {
195
+ margin-top: 0; margin-bottom: 20px; border-bottom: 1px solid #e0e0e0;
196
+ padding-bottom: 15px; font-size: 1.2em; color: #333;
197
+ }
198
+ .field-selector-list {
199
+ list-style: none; padding: 0; margin: 0 0 20px 0;
200
+ overflow-y: auto; flex-grow: 1;
201
+ li {
202
+ padding: 10px 15px; cursor: pointer; border-bottom: 1px solid #f0f0f0;
203
+ transition: background-color 0.2s ease, color 0.2s ease;
204
+ display: flex; justify-content: space-between; align-items: center;
205
+ &:last-child { border-bottom: none; }
206
+ &:hover {
207
+ background-color: #007bff; color: white;
208
+ .field-path { color: #e0e0e0; }
209
+ }
210
+ em { font-style: italic; color: #6c757d; }
211
+ .field-path { font-size: 0.85em; color: #6c757d; margin-left: 10px; white-space: nowrap; }
212
+ }
213
+ }
214
+ .btn-cancel {
215
+ margin-top: auto; padding: 10px 18px; background-color: #6c757d;
216
+ color: white; border: none; border-radius: 4px; cursor: pointer;
217
+ align-self: flex-end;
218
+ &:hover { background-color: #5a6268; }
219
+ }
220
+ }
221
+
222
+ // Styles for ConditionBuilder integration
223
+ .flex-builder-controls .control-group .condition-builder {
224
+ border: 1px solid #e0e0e0; padding: 10px; border-radius: 4px;
225
+ background-color: #fdfdfd; margin-top: 5px;
226
+ }
@@ -0,0 +1,170 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { FaTrash, FaBox, FaExpandArrowsAlt, FaObjectGroup, FaEdit, FaFileAlt } from 'react-icons/fa';
5
+ import { CodeField, NumberField, SelectField } from './Field.jsx';
6
+ import ConditionBuilder from "./ConditionBuilder.jsx";
7
+
8
+ const FlexBuilderControls = ({
9
+ flexStructure,
10
+ selectedNode,
11
+ models,
12
+ user,
13
+ lang,
14
+ modelOptions,
15
+ flexOptions,
16
+ currentModelFields,
17
+ onModelChange,
18
+ onUpdateNode,
19
+ onDeleteNode,
20
+ onAddChildNode,
21
+ onMakeItemNestedContainer,
22
+ onSetIsFieldSelectorOpen,
23
+ onSetEditingHtmlInfo,
24
+ }) => {
25
+ const { t } = useTranslation();
26
+
27
+ const handleDataLimitChange = (e) => {
28
+ let value = parseInt(e.target.value, 10);
29
+ if (isNaN(value) || value < 1) value = 1;
30
+ if (value > 8) value = 8;
31
+ onUpdateNode(flexStructure.id, 'dataLimit', value);
32
+ };
33
+
34
+ const handleRefreshIntervalChange = (e) => {
35
+ onUpdateNode(flexStructure.id, 'refreshInterval', parseInt(e.target.value, 10) || 0);
36
+ };
37
+
38
+ return (
39
+ <div className="flex-builder-controls">
40
+ {/* Section des paramètres de données */}
41
+ <h3>{t('dataSettings', 'Paramètres des données')}</h3>
42
+ <div className="control-group">
43
+ <label htmlFor="selectedModel">{t('flexBuilder.selectDataModel', 'Modèle de données')}:</label>
44
+ <SelectField
45
+ id="selectedModel"
46
+ value={flexStructure.selectedModelName}
47
+ onChange={(selected) => onModelChange(selected.value)}
48
+ items={[{ value: null, label: t('flexBuilder.selectAModel', 'Sélectionner un modèle...') }, ...modelOptions]}
49
+ />
50
+ </div>
51
+ {flexStructure.selectedModelName && (
52
+ <>
53
+ <label>{t('dataFilter', 'Filtre des données')}:</label>
54
+ <div className="control-group">
55
+ <ConditionBuilder
56
+ modelFields={models.find(f => f.name === flexStructure.selectedModelName)?.fields || []}
57
+ model={flexStructure.selectedModelName}
58
+ models={models}
59
+ selectableModels={false}
60
+ initialValue={flexStructure.dataFilter}
61
+ onChange={(newCondition) => onUpdateNode(flexStructure.id, 'dataFilter', newCondition || {})}
62
+ lang={lang}
63
+ />
64
+ </div>
65
+ <div className="form">
66
+ <NumberField
67
+ label={t('flexBuilder.dataLimit', "Nombre d'éléments affichés (1-8)")}
68
+ id="flex-data-limit"
69
+ value={flexStructure.dataLimit || 1}
70
+ onChange={handleDataLimitChange}
71
+ min="1"
72
+ max="8"
73
+ />
74
+ <NumberField
75
+ label={t('flexBuilder.refreshInterval', "Intervalle de rafraîchissement (ms)")}
76
+ id="flex-refresh-interval"
77
+ value={flexStructure.refreshInterval || 60000}
78
+ onChange={handleRefreshIntervalChange}
79
+ min="0"
80
+ placeholder="0 pour désactiver"
81
+ />
82
+ </div>
83
+ </>
84
+ )}
85
+
86
+ {/* Section des paramètres du nœud sélectionné */}
87
+ {selectedNode && (
88
+ <div className="selected-node-controls">
89
+ <h4>
90
+ {t('flexBuilder.selectedNodeSettings', 'Paramètres')}: {t(selectedNode.type)} ({selectedNode.id.substring(0, 8)})
91
+ {selectedNode.id !== flexStructure.id &&
92
+ <button onClick={onDeleteNode} className="btn-delete-node" title={t('flexBuilder.deleteThisNode', 'Supprimer cet élément')}><FaTrash /></button>
93
+ }
94
+ </h4>
95
+
96
+ {/* Contrôles pour un conteneur */}
97
+ {selectedNode.type === 'container' && (
98
+ <>
99
+ <p>{t('flexBuilder.containerControlsTitle', 'Propriétés du conteneur Flex:')}</p>
100
+ <div className="control-group actions">
101
+ <button onClick={() => onAddChildNode(selectedNode.id, 'item')}><FaBox /> {t('flexBuilder.addItem', 'Ajouter une case')}</button>
102
+ <button onClick={() => onAddChildNode(selectedNode.id, 'container')}><FaExpandArrowsAlt /> {t('flexBuilder.addNestedContainer', 'Ajouter un conteneur')}</button>
103
+ </div>
104
+ <div className="control-group"><label>{t('flexDirection')}:</label><SelectField items={flexOptions.flexDirection.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.flexDirection} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.flexDirection', sel.value)} /></div>
105
+ <div className="control-group"><label>{t('flexWrap')}:</label><SelectField items={flexOptions.flexWrap.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.flexWrap} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.flexWrap', sel.value)} /></div>
106
+ <div className="control-group"><label>{t('justifyContent')}:</label><SelectField items={flexOptions.justifyContent.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.justifyContent} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.justifyContent', sel.value)} /></div>
107
+ <div className="control-group"><label>{t('alignItems')}:</label><SelectField items={flexOptions.alignItems.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.alignItems} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.alignItems', sel.value)} /></div>
108
+ <div className="control-group"><label>{t('gap')}:</label><input type="text" value={selectedNode.containerStyle.gap || ''} onChange={e => onUpdateNode(selectedNode.id, 'containerStyle.gap', e.target.value)} placeholder="ex: 10px" /></div>
109
+ <div className="form-group"><CodeField id="customCss" name="customCss" language="css" value={selectedNode.containerStyle.customCss || ''} onChange={(e) => onUpdateNode(selectedNode.id, 'containerStyle.customCss', e.value)} placeholder={t('flexBuilder.customCssPlaceholder', 'Ex: background-color: red;')} rows={4} /></div>
110
+ </>
111
+ )}
112
+
113
+ {/* Contrôles pour un item */}
114
+ {selectedNode.type === 'item' && (
115
+ <>
116
+ <p>{t('flexBuilder.itemControlsTitle', 'Propriétés de la case:')}</p>
117
+ <div className="control-group actions">
118
+ {flexStructure.selectedModelName && (
119
+ <button onClick={() => onSetIsFieldSelectorOpen(true)}>
120
+ <FaEdit /> {selectedNode.content.type === 'dataField' ? t('flexBuilder.editDataMapping', 'Modifier le champ lié') : t('flexBuilder.mapDataField', 'Lier un champ de données')}
121
+ </button>
122
+ )}
123
+ <button onClick={(e) => {
124
+ onSetEditingHtmlInfo({ nodeId: selectedNode.id, initialContent: selectedNode.content.html || '' });
125
+ e.stopPropagation()
126
+ }}>
127
+ <FaFileAlt /> {t('flexBuilder.editHTML', 'Éditer le contenu')}
128
+ </button>
129
+ {selectedNode.content.type !== 'nestedContainer' && (
130
+ <button onClick={() => onMakeItemNestedContainer(selectedNode.id)}>
131
+ <FaObjectGroup /> {t('flexBuilder.makeNestedContainer', 'Transformer en conteneur')}
132
+ </button>
133
+ )}
134
+ </div>
135
+ <div className="control-group"><label>{t('itemWidth')}:</label><input type="text" value={selectedNode.itemStyle.width || ''} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.width', e.target.value)} placeholder="ex: 100px, 20%" /></div>
136
+ <div className="control-group"><label>{t('itemHeight')}:</label><input type="text" value={selectedNode.itemStyle.height || ''} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.height', e.target.value)} placeholder="ex: 50px, auto" /></div>
137
+ <div className="control-group"><label>{t('flexBuilder.itemFlexGrow', 'Flex Grow')}:</label><input type="number" value={selectedNode.itemStyle.flexGrow || 0} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexGrow', parseFloat(e.target.value) || 0)} min="0" step="0.1" /></div>
138
+ <div className="control-group"><label>{t('flexBuilder.itemFlexShrink', 'Flex Shrink')}:</label><input type="number" value={selectedNode.itemStyle.flexShrink === undefined ? 1 : selectedNode.itemStyle.flexShrink} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexShrink', parseFloat(e.target.value))} min="0" step="0.1" /></div>
139
+ <div className="control-group"><label>{t('flexBuilder.itemFlexBasis', 'Flex Basis')}:</label><input type="text" value={selectedNode.itemStyle.flexBasis || 'auto'} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexBasis', e.target.value)} placeholder="auto, 100px, 20%" /></div>
140
+ <div className="form-group"><CodeField id="customCssItem" name="customCssItem" language="css" value={selectedNode.itemStyle.customCss || ''} onChange={(e) => onUpdateNode(selectedNode.id, 'itemStyle.customCss', e.value)} placeholder={t('flexBuilder.customCssPlaceholder', 'Ex: background-color: blue;')} rows={4} /></div>
141
+ </>
142
+ )}
143
+ </div>
144
+ )}
145
+ {!selectedNode &&
146
+ <p className="no-node-selected-hint">{t('flexBuilder.clickToSelectNode', 'Cliquez sur un élément dans l\'aperçu pour le configurer.')}</p>
147
+ }
148
+ </div>
149
+ );
150
+ };
151
+
152
+ FlexBuilderControls.propTypes = {
153
+ flexStructure: PropTypes.object.isRequired,
154
+ selectedNode: PropTypes.object,
155
+ models: PropTypes.array.isRequired,
156
+ user: PropTypes.object,
157
+ lang: PropTypes.string.isRequired,
158
+ modelOptions: PropTypes.array.isRequired,
159
+ flexOptions: PropTypes.object.isRequired,
160
+ currentModelFields: PropTypes.array.isRequired,
161
+ onModelChange: PropTypes.func.isRequired,
162
+ onUpdateNode: PropTypes.func.isRequired,
163
+ onDeleteNode: PropTypes.func.isRequired,
164
+ onAddChildNode: PropTypes.func.isRequired,
165
+ onMakeItemNestedContainer: PropTypes.func.isRequired,
166
+ onSetIsFieldSelectorOpen: PropTypes.func.isRequired,
167
+ onSetEditingHtmlInfo: PropTypes.func.isRequired,
168
+ };
169
+
170
+ export default FlexBuilderControls;