data-primals-engine 1.0.10 → 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 +5390 -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,757 @@
1
+ import React, {useEffect, useState} from 'react';
2
+ import { Tooltip } from 'react-tooltip';
3
+ import {FaPlus, FaProjectDiagram, FaTrash, FaInfoCircle, FaEdit, FaTags} from 'react-icons/fa';
4
+ import { Trans } from 'react-i18next';
5
+ import {convertInputValue, MONGO_CALC_OPERATORS} from "./filter.js";
6
+ import i18n from "i18next";
7
+ import {FaRepeat} from "react-icons/fa6";
8
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
9
+
10
+
11
+ const OperatorSelector = ({ onSelect }) => {
12
+ const [search, setSearch] = useState('');
13
+
14
+ const filteredOps = Object.entries(MONGO_CALC_OPERATORS)
15
+ .filter(([key, config]) =>
16
+ key.toLowerCase().includes(search.toLowerCase()) ||
17
+ config.label.toLowerCase().includes(search.toLowerCase())
18
+ );
19
+
20
+ return (
21
+ <div className="operator-selector">
22
+ <input
23
+ type="text"
24
+ placeholder={i18n.t("cb.searchOperator")}
25
+ value={search}
26
+ onChange={(e) => setSearch(e.target.value)}
27
+ autoFocus
28
+ />
29
+ <div className="operator-list">
30
+ {filteredOps.map(([key, config]) => (
31
+ <div
32
+ key={'ol'+key}
33
+ className="operator-item"
34
+ onClick={() => onSelect(key)}
35
+ data-tooltip-id="op-tooltip"
36
+ data-tooltip-content={config.description}
37
+ >
38
+ <span className="op-key">{key}</span>
39
+ <span className="op-label">{config.label}</span>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ <Tooltip id="op-tooltip" />
44
+ </div>
45
+ );
46
+ };
47
+
48
+ const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = false, models = [], currentModelFields = [], isInFindContext = false }) => {
49
+ const [editing, setEditing] = useState(false);
50
+ const [currentValue, setCurrentValue] = useState(value);
51
+ useEffect(() => {
52
+ setCurrentValue(value);
53
+ }, [value]);
54
+
55
+
56
+ const openDoc =(op) =>{
57
+ window.open(`https://www.mongodb.com/docs/manual/reference/operator/aggregation/${op}`, '_blank');
58
+ }
59
+
60
+ const handleOperatorSelect = (operator) => {
61
+ const opConfig = MONGO_CALC_OPERATORS[operator];
62
+ let newValue;
63
+ if (operator === '$find') {
64
+ // Cas spécial pour créer un $find au niveau racine dans un champ
65
+ const newValue = { "": { "$find": { "$eq": ["", ""] } } };
66
+ onChange(newValue, path);
67
+ setCurrentValue(newValue);
68
+ setEditing(false);
69
+ return;
70
+ }
71
+ if (operator === '$and' || operator === '$or' || operator === '$not' || operator === '$nor') {
72
+ // Cas spécial pour les opérateurs logiques - conserve la structure existante si possible
73
+ if (currentValue && typeof currentValue === 'object' && !Array.isArray(currentValue)) {
74
+ const existingKey = Object.keys(currentValue)[0];
75
+ if (existingKey === '$and' || existingKey === '$or' || existingKey === '$not' || existingKey === '$nor') {
76
+ // Si on transforme un opérateur logique en un autre, on garde le contenu
77
+ newValue = { [operator]: currentValue[existingKey] };
78
+ } else {
79
+ // Sinon, on crée une nouvelle structure avec l'opérateur logique
80
+ newValue = { [operator]: [currentValue] };
81
+ }
82
+ } else {
83
+ // Par défaut, création d'une nouvelle structure logique
84
+ newValue = { [operator]: [{ $eq: ["", ""] }] };
85
+ }
86
+ }else if (operator === '$find') {
87
+ // Cas spécial pour $find qui prend une expression de recherche
88
+ newValue = { [operator]: { $eq: ["", ""] } };
89
+ }else if (opConfig.converter || (!opConfig.multi && !opConfig.args)) {
90
+ newValue = { [operator]: "" };
91
+ } else {
92
+ // Nouvelle logique qui prend en compte args et multi
93
+ if (opConfig.args) {
94
+ // Cas où on a un nombre fixe d'arguments
95
+ const argsArray = Array(opConfig.args).fill("");
96
+ newValue = { [operator]: argsArray };
97
+ } else if (opConfig.multi) {
98
+ // Cas multi-arguments (tableau avec un élément par défaut)
99
+ newValue = { [operator]: [""] };
100
+ } else {
101
+ // Cas mono-argument (valeur directe)
102
+ newValue = { [operator]: "" };
103
+ }
104
+ }
105
+
106
+ gtag('event', '[cb] operator ' + operator);
107
+ onChange(newValue, path);
108
+ setCurrentValue(newValue);
109
+ setEditing(false);
110
+ };
111
+
112
+
113
+ const renderArgument = (arg, index, parentOperator, args) => {
114
+ const isSimpleValue = typeof arg !== 'object' || arg === null || Array.isArray(arg);
115
+
116
+ // Fonction pour mettre à jour cet argument spécifique
117
+ const handleArgChange = (newArgValue) => {
118
+ const updatedArgs = [...args];
119
+
120
+ gtag('event', '[cb] advanced');
121
+ updatedArgs[index] = newArgValue;
122
+ onChange({ [parentOperator]: updatedArgs }, path);
123
+ };
124
+
125
+ // Fonction pour supprimer cet argument
126
+ const removeArgument = () => {
127
+ // Vérifier si args est un tableau avant de tenter de le modifier
128
+ if (!Array.isArray(args)) {
129
+ console.error("Impossible de supprimer - args n'est pas un tableau", args);
130
+ return;
131
+ }
132
+
133
+ const updatedArgs = [...args]; // Crée une copie du tableau
134
+ updatedArgs.splice(index, 1); // Supprime l'élément
135
+
136
+ // Si c'est le dernier argument, on supprime complètement
137
+ if (updatedArgs.length === 0) {
138
+ onChange(null, path); // Demande de suppression
139
+ } else {
140
+ onChange({ [parentOperator]: updatedArgs }, path);
141
+ }
142
+ };
143
+ const parentOpConfig = MONGO_CALC_OPERATORS[parentOperator];
144
+
145
+ return (
146
+ <div key={`arg-${index}`} className="expression-arg">
147
+ <div className="arg-content">
148
+ {isSimpleValue ? (
149
+ // Cas 1: C'est une valeur simple (texte, nombre, $champ)
150
+ <div className="simple-value-container">
151
+ <FieldInput
152
+ value={arg}
153
+ onChange={(newValue) => {
154
+ const updatedArgs = [...args];
155
+ updatedArgs[index] = newValue;
156
+ onChange({ [parentOperator]: updatedArgs }, path);
157
+ }}
158
+ fieldNames={fieldNames}
159
+ isInFindContext={isInFindContext}
160
+ />
161
+ {!parentOpConfig?.disableAdvancedValue && (<button
162
+ type="button"
163
+ className="switch-to-expr"
164
+ onClick={() => handleArgChange({ $eq: [arg, ""] })}
165
+ title={i18n.t("cb.convertToExpression")}
166
+ >
167
+ <FaProjectDiagram />
168
+ </button>)}
169
+ <button
170
+ type="button"
171
+ onClick={removeArgument}
172
+ className="remove-arg"
173
+ title={i18n.t("cb.deleteArgument")}
174
+ >
175
+ <FaTrash/>
176
+ </button>
177
+ </div>
178
+ ) : (
179
+ // Cas 2: C'est une expression imbriquée
180
+ <ExpressionField
181
+ value={arg}
182
+ onChange={handleArgChange}
183
+ fieldNames={fieldNames}
184
+ path={[...path, parentOperator, index]}
185
+ currentModelFields={currentModelFields}
186
+ models={models}
187
+ />
188
+ )}
189
+ </div>
190
+ </div>
191
+ );
192
+ };
193
+
194
+ if (isRoot && (!currentValue || Object.keys(currentValue).length === 0)) {
195
+ const addRootGroup = (operator) => {
196
+ onChange({ [operator]: [{ $eq: ["", ""] }] });
197
+ };
198
+ const addRootCondition = () => {
199
+ onChange({ $eq: ["", ""] });
200
+ }
201
+
202
+ return (
203
+ <div className="empty-state">
204
+ <div className="empty-actions">
205
+ <button type="button" onClick={addRootCondition}>
206
+ <FaPlus/> <Trans i18nKey={"cb.condition"}>Condition</Trans>
207
+ </button>
208
+ <button type="button" onClick={() => addRootGroup('$and')}>
209
+ <FaProjectDiagram/> <Trans i18nKey={"conditionBuilder.andGroup"} />
210
+ </button>
211
+ <button type="button" onClick={() => addRootGroup('$or')}>
212
+ <FaProjectDiagram/> <Trans i18nKey={"conditionBuilder.orGroup"} />
213
+ </button>
214
+ </div>
215
+ </div>
216
+ );
217
+ }
218
+
219
+ if (typeof currentValue === 'object' && currentValue !== null && !Array.isArray(currentValue)) {
220
+
221
+ const operator = Object.keys(currentValue).find(k => k.startsWith('$'));
222
+ const args = currentValue[operator];
223
+ const isFieldWithFind = !operator && Object.values(currentValue)[0]?.$find;
224
+
225
+ // Cas unique pour un champ contenant $find
226
+ if (isFieldWithFind) {
227
+ const fieldName = Object.keys(currentValue)[0];
228
+ const findExpr = currentValue[fieldName].$find;
229
+ const findCondition = findExpr || { $eq: ["", ""] };
230
+
231
+ // --- Logique corrigée pour trouver les champs du modèle lié ---
232
+ const fieldDef = currentModelFields.find(f => f.name === fieldName);
233
+ let relatedFieldNames = [];
234
+ let relatedModelFields = [];
235
+ if (fieldDef && fieldDef.type === 'relation' && fieldDef.relation) {
236
+ const relatedModel = models.find(m => m.name === fieldDef.relation);
237
+ if (relatedModel) {
238
+ relatedFieldNames = relatedModel.fields.map(f => f.name);
239
+ relatedModelFields = relatedModel.fields;
240
+ }
241
+ console.log({models,relatedModel})
242
+ }
243
+
244
+ // --- Fin de la correction ---
245
+
246
+ const handleFieldChange = (newField) => {
247
+ const newValue = {
248
+ [newField]: {
249
+ $find: findCondition
250
+ }
251
+ };
252
+ onChange(newValue, path);
253
+ setCurrentValue(newValue);
254
+ };
255
+
256
+ const handleConditionChange = (newCondition) => {
257
+ const newValue = {
258
+ [fieldName]: {
259
+ $find: newCondition
260
+ }
261
+ };
262
+ onChange(newValue, path);
263
+ setCurrentValue(newValue);
264
+ };
265
+
266
+ return (
267
+ <div className="expression-block field-with-find">
268
+ <div className="field-header">
269
+ <div className="expression-header">
270
+ {/* L'opérateur est implicite ($find), on affiche directement le sélecteur de champ */}
271
+ <FieldInput
272
+ value={fieldName}
273
+ onChange={handleFieldChange}
274
+ fieldNames={fieldNames}
275
+ isFieldName={true}
276
+ currentModelFields={currentModelFields} // Passer les champs complets
277
+ onlyRelations={true}
278
+ />
279
+ <div className="expression-actions">
280
+ <button
281
+ type="button"
282
+ onClick={() => setEditing(!editing)}
283
+ title={i18n.t("cb.changeOperator")}
284
+ >
285
+ <FaRepeat/>
286
+ </button>
287
+
288
+ <button
289
+ type="button"
290
+ onClick={() => onChange(null, path)}
291
+ title={i18n.t("cb.deleteBlock")}
292
+ >
293
+ <FaTrash/>
294
+ </button>
295
+ </div>
296
+ </div>
297
+ </div>
298
+
299
+ {editing && (
300
+ <div className="operator-selector-overlay">
301
+ <OperatorSelector onSelect={handleOperatorSelect}/>
302
+ <button type="button" onClick={() => setEditing(false)}>Annuler</button>
303
+ </div>
304
+ )}
305
+
306
+ <div className="find-content">
307
+ <div className="find-condition">
308
+ <ExpressionField
309
+ value={findCondition}
310
+ onChange={handleConditionChange}
311
+ // On passe les champs du modèle lié s'ils existent
312
+ fieldNames={relatedFieldNames.length > 0 ? relatedFieldNames : fieldNames}
313
+ path={[...path, fieldName, '$find']}
314
+ models={models}
315
+ currentModelFields={relatedModelFields.length > 0 ? relatedModelFields : currentModelFields}
316
+ isInFindContext={true} // On active le contexte $$this pour les enfants
317
+ />
318
+ </div>
319
+ </div>
320
+ </div>
321
+ );
322
+ }
323
+
324
+ // Gestion spéciale pour $and et $or
325
+ let opConfig;
326
+ if (operator === '$and' || operator === '$or' || operator === '$not' || operator === '$nor') {
327
+ opConfig = {
328
+ label: operator === '$and' ? 'ET logique'
329
+ : operator === '$or' ? 'OU logique'
330
+ : operator === '$not' ? 'NON logique'
331
+ : 'OU NON logique',
332
+ description: operator === '$and'
333
+ ? 'Combine plusieurs expressions avec ET logique'
334
+ : operator === '$or'
335
+ ? 'Combine plusieurs expressions avec OU logique'
336
+ : operator === '$not'
337
+ ? 'Inverse la condition (ex: non égal à)'
338
+ : 'Renvoie vrai si aucune des conditions n\'est vérifiée',
339
+ multi: true
340
+ };
341
+ } else if (operator) {
342
+ opConfig = MONGO_CALC_OPERATORS[operator];
343
+ } else {
344
+ return <></>;
345
+ }
346
+
347
+ if( !opConfig )
348
+ return <></>
349
+ const isMulti = opConfig.multi && !opConfig.args; // vrai seulement si multi sans args fixe
350
+ const isFixedArgs = opConfig.args; // vrai si nombre d'args fixé
351
+
352
+ // Déterminer si le parent est un opérateur logique ou $find
353
+ const parentIsLogicalOrFind = path.length === 0 ||
354
+ (path[path.length - 1] === '$and' ||
355
+ path[path.length - 1] === '$or' ||
356
+ path[path.length - 1] === '$and' ||
357
+ path[path.length - 1] === '$or' ||
358
+ path[path.length - 1] === '$find');
359
+
360
+ if (!opConfig) {
361
+ console.error(`Opérateur non reconnu: ${operator}`);
362
+ return <div>Erreur: Opérateur non reconnu</div>;
363
+ }
364
+ // Dans ExpressionField, remplacer la partie qui gère opConfig.converter par:
365
+ if (opConfig.converter || (!isMulti && !isFixedArgs)) {
366
+ // Rendu pour les convertisseurs et opérateurs mono-argument
367
+ return (
368
+ <div className="expression-block single-arg-block">
369
+ <div className="expression-header">
370
+ <span className="operator">{operator}</span>
371
+ {editing && (
372
+ <div className="operator-selector-overlay">
373
+ <OperatorSelector onSelect={handleOperatorSelect} />
374
+ <button type="button" onClick={() => setEditing(false)}>Annuler</button>
375
+ </div>
376
+ )}
377
+ <div className="expression-actions">
378
+ <button type="button" onClick={() => setEditing(!editing)}><FaRepeat /></button>
379
+ <button type="button" onClick={() => openDoc(operator.substring(1))}><FaInfoCircle /></button>
380
+ {!parentIsLogicalOrFind && (
381
+ <button
382
+ type="button"
383
+ className="switch-to-expr"
384
+ onClick={() => {
385
+ // Convertir en valeur simple
386
+ onChange(args, path);
387
+ }}
388
+ title={i18n.t("cb.enterValue")}
389
+ >
390
+ <FaEdit/>
391
+ </button>
392
+ )}
393
+ <button
394
+ type="button"
395
+ onClick={() => onChange(null, path)}
396
+ className="remove-expr"
397
+ title={i18n.t("cb.deleteBlock")}
398
+ >
399
+ <FaTrash/>
400
+ </button>
401
+ </div>
402
+ </div>
403
+ <div className="single-arg">
404
+ <ExpressionField
405
+ value={args} // args est directement la valeur pour mono-argument
406
+ onChange={(newVal) => {
407
+ const newValue = { [operator]: newVal };
408
+ onChange(newValue, path);
409
+ setCurrentValue(newValue);
410
+ }}
411
+ fieldNames={fieldNames}
412
+ path={[...path, operator]}
413
+ models={models}
414
+ currentModelFields={currentModelFields}
415
+ isInFindContext={isInFindContext}
416
+ />
417
+ </div>
418
+ </div>
419
+ );
420
+ }
421
+ // Rendu pour les opérateurs avec nombre d'arguments fixe
422
+ if (isFixedArgs) {
423
+ return (
424
+ <div className="expression-block fixed-args-block">
425
+ <div className="expression-header">
426
+ <span className="operator">{operator}</span>
427
+ {editing && (
428
+ <div className="operator-selector-overlay">
429
+ <OperatorSelector onSelect={handleOperatorSelect}/>
430
+ <button type="button" onClick={() => setEditing(false)}>Annuler</button>
431
+ </div>
432
+ )}
433
+ <div className="expression-actions">
434
+ <button type="button" onClick={() => setEditing(!editing)}><FaRepeat/></button>
435
+ <button type="button" onClick={() => openDoc(operator.substring(1))}><FaInfoCircle /></button>
436
+ {!parentIsLogicalOrFind && (<button
437
+ type="button"
438
+ className="switch-to-expr"
439
+ onClick={() => {
440
+ // Convertir l'expression en valeur simple
441
+ onChange("", path);
442
+ }}
443
+ title={i18n.t("cb.enterValue")}
444
+ >
445
+ <FaEdit/>
446
+ </button>)}
447
+ <button
448
+ type="button"
449
+ onClick={() => onChange(null, path)} // Envoie null pour indiquer la suppression
450
+ className="remove-expr"
451
+ title={i18n.t("cb.deleteBlock")}
452
+ >
453
+ <FaTrash/>
454
+ </button>
455
+ </div>
456
+ </div>
457
+ <div className="expression-args">
458
+ {Array.isArray(args) && args.map((arg, i) => (
459
+ <div key={`arg-${i}`} className="expression-arg">
460
+ <ExpressionField
461
+ value={arg}
462
+ onChange={(newVal) => {
463
+ const updatedArgs = [...args];
464
+ updatedArgs[i] = newVal;
465
+ onChange({[operator]: updatedArgs}, path);
466
+ }}
467
+ fieldNames={fieldNames}
468
+ path={[...path, operator, i]}
469
+ models={models}
470
+ currentModelFields={currentModelFields}
471
+ isInFindContext={isInFindContext}
472
+ />
473
+ </div>
474
+ ))}
475
+ </div>
476
+ </div>
477
+ );
478
+ }
479
+ return (
480
+ <div className="expression-block">
481
+ <div className="expression-header">
482
+ <span className="operator">{operator}</span>
483
+ {editing && (
484
+ <div className="operator-selector-overlay">
485
+ <OperatorSelector onSelect={handleOperatorSelect}/>
486
+ <button type="button" onClick={() => setEditing(false)}><Trans i18nKey={"cb.cancel"}>Annuler</Trans></button>
487
+ </div>
488
+ )}
489
+ <div className="expression-actions">
490
+ {!parentIsLogicalOrFind && (
491
+ <button
492
+ type="button"
493
+ className="switch-to-expr"
494
+ onClick={() => onChange("", path)}
495
+ title={i18n.t("cb.enterValue")}
496
+ >
497
+ <FaEdit/>
498
+ </button>
499
+ )}
500
+ <button type="button" onClick={() => setEditing(!editing)}><FaRepeat/></button>
501
+ <button type="button" onClick={() => openDoc(operator.substring(1))}><FaInfoCircle/></button>
502
+ </div>
503
+ </div>
504
+ <div className="expression-args">
505
+ {Array.isArray(args) ? (
506
+ <>
507
+ {args.map((arg, i) => renderArgument(arg, i, operator, args))}
508
+ {opConfig.multi && (
509
+ <button
510
+ type="button"
511
+ onClick={() => {
512
+ const updatedArgs = [...args, operator === '$and' || operator === '$or' || operator === '$not' || operator === '$nor' ? {$eq: ["", ""]} : ""];
513
+ const newValue = {[operator]: updatedArgs};
514
+ onChange(newValue, path);
515
+ setCurrentValue(newValue);
516
+ }}
517
+ className="add-arg"
518
+ >
519
+ <FaPlus/> <Trans i18nKey={"cb.addArgument"}>Ajouter un argument</Trans>
520
+ </button>
521
+ )}
522
+ </>
523
+ ) : (
524
+ <ExpressionField
525
+ value={args}
526
+ onChange={(newVal) => {
527
+ onChange({[operator]: newVal}, path);
528
+ }}
529
+ fieldNames={fieldNames}
530
+ path={[...path, operator]}
531
+ models={models}
532
+ currentModelFields={currentModelFields}
533
+ />
534
+ )}
535
+ </div>
536
+ </div>
537
+ );
538
+ }
539
+ const parentOperator = path.length > 1 ? path[path.length - 1] : null;
540
+ const parentOpConfig = parentOperator ? MONGO_CALC_OPERATORS[parentOperator] : null;
541
+
542
+
543
+ return (
544
+ <div className="simple-value">
545
+ <div className="simple-value-container">
546
+ <FieldInput
547
+ value={currentValue}
548
+ onChange={(newValue) => {
549
+ setCurrentValue(newValue);
550
+ onChange(convertInputValue(newValue), path);
551
+ }}
552
+ fieldNames={fieldNames}
553
+ isInFindContext={isInFindContext}
554
+ currentModelFields={currentModelFields} // On propage le contexte
555
+ />
556
+
557
+ {/* --- MODIFICATION --- */}
558
+ {/* On affiche le bouton de conversion uniquement si l'opérateur parent le permet */}
559
+ {parentOpConfig?.disableAdvancedValue !== true && (
560
+ <button
561
+ type="button"
562
+ className="switch-to-expr"
563
+ onClick={() => {
564
+ gtag('event', '[cb] advanced');
565
+ // On transforme la valeur simple en une expression $eq par défaut
566
+ onChange({ $eq: [currentValue, ""] }, path);
567
+ }}
568
+ title={i18n.t("cb.convertToExpression")}
569
+ >
570
+ <FaProjectDiagram />
571
+ </button>
572
+ )}
573
+ </div>
574
+ </div>
575
+ );
576
+ };
577
+ const FieldInput = ({
578
+ value,
579
+ onChange,
580
+ fieldNames,
581
+ isFieldName = false,
582
+ isInFindContext = false,
583
+ currentModelFields = [],
584
+ onlyRelations = false
585
+ }) => {
586
+ const [inputValue, setInputValue] = useState(value);
587
+ const [suggestions, setSuggestions] = useState([]);
588
+ const [showSuggestions, setShowSuggestions] = useState(false);
589
+
590
+ useEffect(() => {
591
+ setInputValue(value);
592
+ }, [value]);
593
+
594
+ useEffect(() => {
595
+ // Filtrer les suggestions selon le contexte
596
+ let filtered = fieldNames;
597
+ if (onlyRelations && currentModelFields.length > 0) {
598
+ filtered = currentModelFields
599
+ .filter(field => field.type === 'relation')
600
+ .map(field => field.name);
601
+ }
602
+ setSuggestions(filtered);
603
+ }, [fieldNames, currentModelFields, onlyRelations]);
604
+
605
+ const handleInputChange = (e) => {
606
+ const val = e.target.value;
607
+ setInputValue(val);
608
+
609
+ const prefix = isFieldName ? '' : (isInFindContext ? '$$this.' : '$');
610
+
611
+ if (val.startsWith(prefix)) {
612
+ const searchTerm = val.substring(prefix.length).toLowerCase();
613
+ const filtered = suggestions.filter(name =>
614
+ name.toLowerCase().includes(searchTerm));
615
+ setSuggestions(filtered);
616
+ setShowSuggestions(true);
617
+ } else {
618
+ setShowSuggestions(false);
619
+ }
620
+
621
+ onChange(val);
622
+ };
623
+
624
+ const handleSuggestionClick = (suggestion) => {
625
+ const prefix = isFieldName ? '' : (isInFindContext ? '$$this.' : '$');
626
+ const newValue = `${prefix}${suggestion}`;
627
+ setInputValue(newValue);
628
+ onChange(newValue);
629
+ setShowSuggestions(false);
630
+ };
631
+
632
+ return (
633
+ <div className="field-input-container">
634
+ <input
635
+ type="text"
636
+ value={inputValue}
637
+ onChange={handleInputChange}
638
+ placeholder={
639
+ onlyRelations ? i18n.t("cb.selectRelationField") :
640
+ isInFindContext ?i18n.t("cb.enterThisField") :
641
+ i18n.t("cb.enterValueOrField")
642
+ }
643
+ onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
644
+ onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
645
+ />
646
+ {showSuggestions && suggestions.length > 0 && (
647
+ <div className="suggestions-dropdown">
648
+ {suggestions.map((suggestion, index) => {
649
+ // Trouver le champ complet pour afficher des infos supplémentaires
650
+ const fieldInfo = currentModelFields.find(f => f.name === suggestion);
651
+ return (
652
+ <div
653
+ key={index}
654
+ className="suggestion-item"
655
+ onClick={() => handleSuggestionClick(suggestion)}
656
+ data-tooltip-id="field-tooltip"
657
+ data-tooltip-content={fieldInfo?.relation ? i18n.t('cb.relationTooltip', { relation: fieldInfo.relation}) : ''}
658
+ >
659
+ {suggestion}
660
+ {fieldInfo?.relation && (
661
+ <span className="relation-info">
662
+ <FaTags /> {fieldInfo.relation}
663
+ </span>
664
+ )}
665
+ </div>
666
+ );
667
+ })}
668
+ </div>
669
+ )}
670
+ </div>
671
+ );
672
+ };
673
+ export const ConditionBuilder2 = ({initialValue = {}, onChange, models, model}) => {
674
+ const { me: user } = useAuthContext();
675
+ const mod = (models||[]).find(f=>f.name===model && f._user === user.username ) || models.find(f => f._user === user.username) || null;
676
+ const fieldNames = mod?.fields?.map(f => f.name) || [];
677
+ const modelFields = mod?.fields || [];
678
+
679
+ // Parse initial value if it's a string
680
+ const parsedInitialValue = typeof initialValue === 'string'
681
+ ? JSON.parse(initialValue)
682
+ : initialValue;
683
+
684
+ // L'état reste le même
685
+ const [expression, setExpression] = useState(() => ({ ...parsedInitialValue }));
686
+
687
+ const handleExpressionChange = (newExpr, path = []) => {
688
+ // Cas de suppression (quand newExpr est null)
689
+ if (newExpr === null) {
690
+ // Fonction récursive pour supprimer un élément dans une structure imbriquée
691
+ const removeAtPath = (obj, path) => {
692
+ if (path.length === 0) return null;
693
+
694
+ const [currentKey, ...remainingPath] = path;
695
+
696
+ if (remainingPath.length === 0) {
697
+ // Cas 1: Suppression dans un tableau
698
+ if (Array.isArray(obj)) {
699
+ const newArray = [...obj];
700
+ newArray.splice(currentKey, 1);
701
+ return newArray.length === 0 ? null : newArray;
702
+ }
703
+ // Cas 2: Suppression dans un objet
704
+ else if (obj && typeof obj === 'object') {
705
+ const newObj = {...obj};
706
+ delete newObj[currentKey];
707
+ return Object.keys(newObj).length === 0 ? null : newObj;
708
+ }
709
+ }
710
+
711
+ // Traverser la structure
712
+ if (Array.isArray(obj)) {
713
+ const newArray = [...obj];
714
+ newArray[currentKey] = removeAtPath(obj[currentKey], remainingPath);
715
+ return newArray.filter(x => x !== null); // Retire les nulls
716
+ }
717
+ else if (obj && typeof obj === 'object') {
718
+ const newObj = {...obj};
719
+ newObj[currentKey] = removeAtPath(obj[currentKey], remainingPath);
720
+ return Object.keys(newObj).every(k => newObj[k] === null) ? null : newObj;
721
+ }
722
+
723
+ return obj;
724
+ };
725
+
726
+ const newExpression = path.length === 0
727
+ ? {}
728
+ : removeAtPath(JSON.parse(JSON.stringify(expression)), path);
729
+
730
+ setExpression(newExpression || {});
731
+ if (onChange) onChange(newExpression || {});
732
+ }
733
+ // Cas normal de mise à jour
734
+ else {
735
+ setExpression(newExpr);
736
+ if (onChange) onChange(newExpr);
737
+ }
738
+ };
739
+
740
+ // Le rendu est maintenant beaucoup plus propre !
741
+ return (
742
+ <div className="expr-builder">
743
+ <div className="expression-container">
744
+ <ExpressionField
745
+ value={expression}
746
+ onChange={handleExpressionChange}
747
+ fieldNames={fieldNames}
748
+ // On passe des props supplémentaires pour gérer les actions au niveau racine
749
+ isRoot={true}
750
+ models={models}
751
+ currentModelFields={modelFields}
752
+ />
753
+ </div>
754
+ <Tooltip id="field-tooltip"/>
755
+ </div>
756
+ );
757
+ };