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,556 @@
1
+ import React, {forwardRef, useEffect, useState} from "react"; // Ajout de useEffect
2
+ import {conditionToApiSearchFilter, getDefaultForType} from "data-primals-engine/data";
3
+ import { Trans, useTranslation } from "react-i18next";
4
+ import {FaCode, FaLanguage, FaMinus, FaPlus, FaSitemap} from "react-icons/fa";
5
+ import Button from "./Button.jsx";
6
+ import { RTE } from "./RTE.jsx";
7
+ import RelationField from "./RelationField.jsx";
8
+ import {
9
+ CheckboxField,
10
+ CodeField,
11
+ ColorField,
12
+ EnumField,
13
+ FileField, ModelField, NumberField,
14
+ PhoneField,
15
+ SelectField,
16
+ TextField
17
+ } from "./Field.jsx";
18
+ import i18n from "data-primals-engine/i18n";
19
+ import ConditionBuilder from "./ConditionBuilder.jsx";
20
+ import {useModelContext} from "./contexts/ModelContext.jsx";
21
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
22
+ import {FaCircleInfo} from "react-icons/fa6";
23
+ import {Tooltip} from "react-tooltip";
24
+ import Draggable from "./Draggable.jsx";
25
+ import CronBuilder from "./CronBuilder.jsx";
26
+ import RTETrans from "./RTETrans.jsx";
27
+ import uniqid from "uniqid";
28
+
29
+ // ... (fonction getInputType) ...
30
+ // Fonction pour obtenir le type d'input HTML basé sur le type de champ du modèle
31
+ const getInputType = (fieldType) => {
32
+ switch (fieldType) {
33
+ case 'number':
34
+ return 'number';
35
+ case 'date':
36
+ return 'date';
37
+ case 'datetime':
38
+ return 'datetime-local';
39
+ case 'email':
40
+ return 'email';
41
+ case 'password':
42
+ return 'password';
43
+ case 'url':
44
+ return 'url';
45
+ case 'phone': // Note: 'tel' est le type HTML correct
46
+ return 'tel';
47
+ case 'color':
48
+ return 'color';
49
+ // Pour les autres types comme 'string', 'string_t', 'enum', etc.
50
+ default:
51
+ return 'text';
52
+ }
53
+ };
54
+
55
+ // C:/Dev/hackersonline-engine/client/src/DataEditor.jsx
56
+ // C:/Dev/hackersonline-engine/client/src/DataEditor.jsx
57
+
58
+
59
+ /**
60
+ * Evaluates a single condition against form data.
61
+ * @param {object} currentModelDef - The definition of the current model.
62
+ * @param {object} condition - The condition to evaluate.
63
+ * @param {object} formData - The form data.
64
+ * @param {object[]} allModels - An array of all model definitions.
65
+ * @param {object} user - The current user.
66
+ * @returns {boolean} - True if the condition is met, false otherwise.
67
+ */
68
+ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels, user) => {
69
+ // Condition est directement un filtre MongoDB, donc on l'applique
70
+ // en utilisant les opérateurs et les valeurs qu'il contient.
71
+
72
+ if (!condition || typeof condition !== 'object') {
73
+ console.warn("[Client Eval] Condition is not an object:", condition);
74
+ return true; // Permissive default
75
+ }
76
+
77
+ // Si la condition contient des opérateurs logiques, on les gère ici
78
+ if (condition.$and || condition.$or || condition.$not || condition.$nor) {
79
+ console.warn("[Client Eval] Condition logique détectée dans evaluateSingleCondition, ce n'est pas attendu. Devrait être géré par isConditionMet.");
80
+ return true; // Permissive default
81
+ }
82
+
83
+ if (condition.$find) {
84
+ const fieldName = Object.keys(condition)[0];
85
+ const fieldValue = formData[fieldName];
86
+ const findCondition = condition.$find;
87
+
88
+ if (!Array.isArray(fieldValue)) return false;
89
+
90
+ return fieldValue.some(item => {
91
+ // Gestion spéciale pour la syntaxe $eq: ["$$this.field", value]
92
+ if (findCondition.$eq && Array.isArray(findCondition.$eq)) {
93
+ const [fieldPath, value] = findCondition.$eq;
94
+ if (fieldPath.startsWith("$$this.")) {
95
+ const fieldToCheck = fieldPath.replace("$$this.", "");
96
+ return item[fieldToCheck] == value;
97
+ }
98
+ }
99
+
100
+ // Sinon, évaluation normale
101
+ const tempData = { ...item };
102
+ return evaluateSingleCondition(currentModelDef, findCondition, tempData, allModels, user);
103
+ });
104
+ }
105
+
106
+ // Si la condition contient un opérateur $exists, on le gère ici
107
+ if (condition.$exists !== undefined) {
108
+ const fieldName = Object.keys(condition)[0]; // Récupérer le nom du champ
109
+ const shouldExist = condition.$exists; // Récupérer la valeur de $exists (true ou false)
110
+ const exists = Object.prototype.hasOwnProperty.call(formData, fieldName) && formData[fieldName] !== undefined && formData[fieldName] !== null;
111
+ return exists === shouldExist;
112
+ }
113
+
114
+ // Si la condition contient un opérateur $find, on le gère ici
115
+ if (condition.$find) {
116
+ // Récupérer le nom du champ
117
+ const fieldName = Object.keys(condition)[0];
118
+ const fieldValue = formData[fieldName];
119
+ try {
120
+ // Assuming evaluateSingleCondition handles $find
121
+ return evaluateSingleCondition(currentModelDef, condition.$find, formData, allModels, user);
122
+ } catch (error) {
123
+ console.error("Error evaluating $find condition:", condition, error);
124
+ return false;
125
+ }
126
+ }
127
+
128
+ // Récupérer le nom du champ et la condition
129
+ const fieldName = Object.keys(condition)[0];
130
+ const fieldValue = condition[fieldName];
131
+
132
+ // Récupérer la définition du champ
133
+ const fieldDef = currentModelDef?.fields.find(f => f.name === fieldName);
134
+
135
+ // Si la définition du champ n'est pas trouvée, on retourne true
136
+ if (!fieldDef) {
137
+ console.warn(`[Client Eval] Field definition not found for field: ${fieldName}`);
138
+ return true; // Permissive default
139
+ }
140
+
141
+ let targetValue = formData[fieldName];
142
+ let processedConditionValue = fieldValue;
143
+
144
+ // 1. Handle $exists (on the first field)
145
+ // 2. Convert condition value based on operator's expected input type
146
+ const fieldType = fieldDef?.type; // Type of the first field
147
+
148
+ try {
149
+ processedConditionValue = convertValueType(fieldValue, fieldType);
150
+ } catch (e) {
151
+ logClientEvalWarning(`Error converting value type: ${e.message}`, condition);
152
+ return false;
153
+ }
154
+
155
+ return evaluateComparison(fieldValue, targetValue, processedConditionValue, condition);
156
+
157
+ function logClientEvalWarning(message, details) {
158
+ console.warn(`[Client Eval] ${message}:`, details);
159
+ }
160
+
161
+ function convertValueType(value, inputType) {
162
+ switch (inputType) {
163
+ case 'number':
164
+ const numValue = parseFloat(value);
165
+ if (isNaN(numValue)) {
166
+ throw new Error(`Invalid number value: ${value}`);
167
+ }
168
+ return numValue;
169
+ case 'boolean':
170
+ return String(value).toLowerCase() === 'true';
171
+ case 'csv':
172
+ return String(value).split(',').map(item => item.trim()).filter(Boolean);
173
+ case 'text':
174
+ default:
175
+ return String(value);
176
+ }
177
+ }
178
+
179
+ function evaluateComparison(operator, targetValue, processedConditionValue, condition) {
180
+ try {
181
+ switch (typeof operator === 'object' ? Object.keys(operator)[0] : null) {
182
+ case '$eq': return targetValue == processedConditionValue;
183
+ case '$ne': return targetValue != processedConditionValue;
184
+ case '$gt': return targetValue > processedConditionValue;
185
+ case '$lt': return targetValue < processedConditionValue;
186
+ case '$gte': return targetValue >= processedConditionValue;
187
+ case '$lte': return targetValue <= processedConditionValue;
188
+ case '$regex':
189
+ if (typeof targetValue !== 'string') return false;
190
+ if (typeof processedConditionValue !== 'string') return false;
191
+ try {
192
+ const regex = new RegExp(processedConditionValue, 'i');
193
+ return regex.test(targetValue);
194
+ } catch (e) {
195
+ logClientEvalWarning(`Invalid regex pattern: ${processedConditionValue}`, condition);
196
+ return false;
197
+ }
198
+ case '$in':
199
+ return Array.isArray(processedConditionValue) && processedConditionValue.includes(String(targetValue));
200
+ case '$nin':
201
+ return !Array.isArray(processedConditionValue) || !processedConditionValue.includes(String(targetValue));
202
+ default:
203
+ logClientEvalWarning(`Unhandled operator in client evaluation logic: ${operator}`, condition);
204
+ return true; // Permissive default
205
+ }
206
+ } catch (evalError) {
207
+ logClientEvalWarning(`Error during client condition evaluation: ${operator}, targetValue=${targetValue}, processedConditionValue=${processedConditionValue}`, condition);
208
+ return false;
209
+ }
210
+ }
211
+ };
212
+
213
+ export const isConditionMet = (model, cond, formData, allModels, user) => {
214
+ const isNode = (v) => typeof v === 'object' && v !== null;
215
+
216
+ const condition = cond;
217
+
218
+ if (!condition) return true;
219
+
220
+ if (condition.$and && Array.isArray(condition.$and)) {
221
+ if (condition.$and.length === 0) return true;
222
+ return condition.$and.every(sub => isConditionMet(model, sub, formData, allModels, user));
223
+ }
224
+
225
+ if (condition.$or && Array.isArray(condition.$or)) {
226
+ if (condition.$or.length === 0) return false;
227
+ return condition.$or.some(sub => isConditionMet(model, sub, formData, allModels, user));
228
+ }
229
+
230
+ if (condition.$not) {
231
+ return !isConditionMet(model, condition.$not, formData, allModels, user);
232
+ }
233
+
234
+ if (condition.$nor && Array.isArray(condition.$nor)) {
235
+ if (condition.$nor.length === 0) return true;
236
+ return !condition.$nor.some(sub => isConditionMet(model, sub, formData, allModels, user));
237
+ }
238
+
239
+ if (condition.$find) {
240
+ try {
241
+ // Assuming evaluateSingleCondition handles $find
242
+ return evaluateSingleCondition(model, condition, formData, allModels, user);
243
+ } catch (error) {
244
+ console.error("Error evaluating $find condition:", condition, error);
245
+ return false;
246
+ }
247
+ }
248
+
249
+ if (condition.path && condition.op) {
250
+ try {
251
+ return evaluateSingleCondition(model, condition, formData, allModels, user);
252
+ } catch (error) {
253
+ console.error("Error evaluating condition:", condition, error);
254
+ return false;
255
+ }
256
+ }
257
+
258
+ console.warn("Unknown condition format:", condition);
259
+ return true;
260
+ };
261
+
262
+ export const DataEditor = forwardRef(function MyDataEditor({
263
+ isLoading,
264
+ model,
265
+ onSubmit,
266
+ refreshTime,
267
+ formData,
268
+ setFormData, record, setRecord}, ref){
269
+
270
+ const [focusedField, setFocusedField] = useState({});
271
+ const {me} = useAuthContext()
272
+ const {models} = useModelContext()
273
+
274
+ const [viewMode, toggleViewMode]= useState(false);
275
+
276
+ function renderInputField(model, field, value, handleChange, refreshTime, formData, user, models) { // Ajout de formData
277
+
278
+ // Évaluer la condition AVANT de rendre le champ
279
+ if (!isConditionMet(model, field.condition, formData)) {
280
+ return null; // Ne rien rendre si la condition n'est pas remplie
281
+ }
282
+
283
+ const inputProps = {
284
+ id: field.name,
285
+ name: field.uniqueName || field.name,
286
+ value: typeof(value) === 'undefined' ? getDefaultForType(field): value,
287
+ onChange: (v) => handleChange(v),
288
+ required: field.required,
289
+ placeholder: field.placeholder,
290
+ key: field.name
291
+ };
292
+
293
+ // --- Le reste de la fonction renderInputField reste identique ---
294
+ switch (field.type) {
295
+ case 'model':
296
+ return <ModelField value={value} onChange={handleChange} model={model} field={field} />;
297
+ case 'modelField':
298
+ return <ModelField fields={true} value={value} onChange={handleChange} model={model} field={field} />;
299
+ case 'datetime':
300
+ case 'date':
301
+ if( field.min )
302
+ inputProps["min"] = field.min;
303
+ if( field.max)
304
+ inputProps["max"] = field.max;
305
+
306
+ // Utilisation d'un wrapper div pour appliquer la clé unique même si le champ est conditionnel
307
+ return <input key={field.name} type={getInputType(field.type)} {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} />;
308
+ case 'textarea':
309
+ return <textarea key={field.name} {...inputProps} />
310
+ case 'richtext':
311
+ return <RTE help={focusedField?.name === field.name ? t('field_'+model.name+'_'+field.name+'_hint', field.hint || '') : ''} onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} key={field.name} {...inputProps} field={field} name={formData._id} />;
312
+ case 'richtext_t':
313
+ return <RTETrans
314
+ key={field.name}
315
+ value={value}
316
+ onChange={(newValue) => handleChange({ name: field.name, value: newValue })}
317
+ field={field}
318
+ />;
319
+ case 'enum':
320
+ return <EnumField key={field.name} inputProps={inputProps} value={value} handleChange={handleChange} field={field} />
321
+ case "object":
322
+ return <CodeField
323
+ language={field.language}
324
+ name={inputProps.name}
325
+ // Passer la valeur brute (chaîne JSON) depuis formData
326
+ value={value ? JSON.stringify(value) : ''}
327
+ // CodeField appelle déjà handleChange avec { name, value }
328
+ onChange={handleChange}
329
+ />
330
+ case 'code':
331
+ // --- Détection pour afficher le ConditionBuilder ---
332
+ {
333
+ const isConditionBuilderField =
334
+ field.type === 'code' &&
335
+ field.language === 'json' &&
336
+ field.conditionBuilder === true;
337
+ if (isConditionBuilderField) {
338
+ // Utiliser la valeur de formData, parsée en objet
339
+ const conditionObject = formData[field.name];
340
+ const currentViewMode = viewMode || 'builder';
341
+ let builderModelName = model.name; // Par défaut, le modèle en cours d'édition
342
+ if (field.targetModel) {
343
+ if (typeof field.targetModel === 'string' && field.targetModel.startsWith('$')) {
344
+ // C'est une référence dynamique à un autre champ du formulaire
345
+ const dynamicFieldName = field.targetModel.substring(1);
346
+ // On utilise la valeur du champ pointé s'il existe dans formData, sinon on garde le modèle par défaut
347
+ builderModelName = formData[dynamicFieldName] || model.name;
348
+ } else {
349
+ // C'est un nom de modèle statique
350
+ builderModelName = field.targetModel;
351
+ }
352
+ console.log({builderModelName})
353
+ }
354
+ return <div className={"flex flex-1"} style={{width:'100%'}} key={field.name}>
355
+ {currentViewMode !== 'builder' && ( <div className="condition-builder-toggle">
356
+ <button
357
+ type="button"
358
+ onClick={e => toggleViewMode(!viewMode)}
359
+ title={currentViewMode === 'builder' ? "Passer à la vue JSON" : "Passer au constructeur visuel"}
360
+ >
361
+ {currentViewMode === 'builder' ? <FaCode /> : <FaSitemap />}
362
+ </button>
363
+ </div>)}
364
+
365
+
366
+ {currentViewMode === 'builder' ? (
367
+ <ConditionBuilder
368
+ key={builderModelName}
369
+ initialValue={conditionObject}
370
+ models={models}
371
+ user={user}
372
+ model={builderModelName}
373
+ modelFields={model.fields}
374
+ onChange={(newConditionObject) => {
375
+ handleChange({ name: field.name, value: newConditionObject });
376
+ }}
377
+ />
378
+ ) : (
379
+ <>
380
+ {/*JSON.stringify(conditionToApiSearchFilter(JSON.parse(formData[field.name])))*/}
381
+ {formData[field.name] && (<CodeField
382
+ language={field.language}
383
+ name={inputProps.name}
384
+ // Passer la valeur brute (chaîne JSON) depuis formData
385
+ value={formData[field.name] || ''}
386
+ // CodeField appelle déjà handleChange avec { name, value }
387
+ onChange={handleChange}
388
+ />)}</>
389
+ )}</div>
390
+ }
391
+ return <CodeField key={field.name} language={field.language} name={inputProps.name}
392
+ value={typeof (value) === 'string' ? value : JSON.stringify(value)}
393
+ onChange={handleChange}/>
394
+ }
395
+ case 'array':
396
+ if (field.itemsType === 'file') {
397
+ return <div key={field.name}><FileField name={field.name} maxSize={field.maxSize}
398
+ mimeTypes={field.mimeTypes} value={value} onChange={(files) => {
399
+ handleChange({name: field.name, value: files});
400
+ }} /></div>;
401
+ } else {
402
+ const tp = {...field, type: field.itemsType};
403
+ const lbl = tp.hint;
404
+ tp.hint = '';
405
+ tp.uniqueName = uniqid(field.name);
406
+ let vs = !Array.isArray(value) || !value.length ? [getDefaultForType(tp)] : value;
407
+ // Note: La condition s'applique au champ 'array' lui-même, pas aux éléments internes.
408
+ return <><p className="hint">{t('field_' + model.name + '_' + tp.name + '_hint', lbl || '')}</p><Draggable items={vs} onChange={(arr) => {
409
+ handleChange({name: field.name, value: arr});
410
+ }} renderItem={(item,i) => <div
411
+ className="array-element flex">{renderInputField(model, tp, item, (e) => { // Passer formData ici aussi si les éléments internes peuvent avoir des conditions
412
+ handleChange({name: field.name, value: vs.map(((v, vi) => vi === i ? e.value : v))});
413
+ }, refreshTime, formData, user, models)}
414
+ <FaMinus onClick={() => {
415
+ handleChange({name: field.name, value: value.filter((ve, vj) => vj !== i)});
416
+ }}/>
417
+ </div>}/><FaPlus className="cursor-pointer" onClick={() => {
418
+ handleChange({name: field.name, value: [...vs, getDefaultForType(tp)]});
419
+ }}/></>;
420
+ }
421
+ case "number":
422
+ inputProps["step"] = field.step || 0.1;
423
+ if( field.min )
424
+ inputProps["min"] = field.min;
425
+ if( field.max)
426
+ inputProps["max"] = field.max;
427
+ return <NumberField help={focusedField?.name === field.name ? t('field_'+model.name+'_'+field.name+'_hint', field.hint || '') : ''} onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} unit={field.unit} key={field.name} {...inputProps} onChange={(e) => handleChange({name: field.name, value: parseFloat(e.target.value.replace(',', '.'))})} />
428
+ case 'relation':
429
+ return (
430
+ <RelationField onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} help={focusedField?.name === field.name ? t('field_'+model.name+'_'+field.name+'_hint', field.hint || '') : ''} key={field.name} model={model} field={field} value={value} onChange={(e) => {
431
+ handleChange(e)
432
+ }} refreshTime={refreshTime} />
433
+ );
434
+ case 'cronSchedule':
435
+ return <CronBuilder
436
+ cronExpression={value}
437
+ cronMask={field.cronMask}
438
+ defaultCronExpression={field.default}
439
+ onCronChange={(newCron) => {
440
+ handleChange({name: field.name, value: newCron});
441
+ }}
442
+ />;
443
+ case 'phone':
444
+ return <PhoneField help={t('field_' + model.name + '_' + field.name + '_hint', field.hint || '')}
445
+ key={field.name} name={field.name} value={inputProps.value}
446
+ onChange={(e) => handleChange({name: field.name, value: e})}/>
447
+ case 'boolean':
448
+ return <CheckboxField
449
+ help={value && t('field_' + model.name + '_' + field.name + '_hint', field.hint || '')}
450
+ key={field.name} {...inputProps} checked={value}
451
+ onChange={(e) => handleChange({name: field.name, value: e.target.checked})}/>
452
+ case 'string':
453
+ case 'string_t':
454
+ {
455
+ if (field.maxlength)
456
+ inputProps["maxlength"] = field.maxlength;
457
+ if (typeof (field.multiline) !== 'undefined')
458
+ inputProps["multiline"] = !!field.multiline;
459
+
460
+ const displayValue = (typeof value === 'object' && value !== null) ? value.key : (value || '');
461
+
462
+ return <TextField
463
+ help={focusedField?.name === field.name ? t('field_' + model.name + '_' + field.name + '_hint', field.hint || '') : ''}
464
+ onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} key={field.name}
465
+ type={getInputType(field.type)} {...inputProps}
466
+ value={displayValue}
467
+ onChange={(e) => handleChange({name: field.name, value: e.target.value})} />
468
+ }
469
+ case 'file':
470
+ return <FileField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} name={field.name} maxSize={field.maxSize} mimeTypes={field.mimeTypes} value={value} onChange={(file) => handleChange({ name: name, value: file ? file.name : null })} />
471
+ case 'color':
472
+ return <ColorField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} name={field.name} value={value} onChange={handleChange} />
473
+ default:
474
+ return <TextField onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} help={focusedField?.name === field.name ? t('field_'+model.name+'_'+field.name+'_hint', field.hint || '') : ''} key={field.name} type={getInputType(field.type)} {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} />
475
+ }
476
+ }
477
+
478
+ // --- handleChange, handleSubmit, handleNew restent identiques ---
479
+ const handleChange = (event) => {
480
+ const { name, value } = event;
481
+ setFormData(currentFormData => ({ ...currentFormData, [name]: value }));
482
+ };
483
+
484
+ const handleSubmit = (event) => {
485
+ event.preventDefault();
486
+ onSubmit(formData, record); // Pass record to onSubmit
487
+ };
488
+
489
+ const handleNew = (e) => {
490
+ const t = [...model.fields].reduce((acc, field, index) => {
491
+ if( field.type === "relation"){
492
+ acc[field.name] = field.multiple ? [] : null;
493
+ }else {
494
+ acc[field.name] = getDefaultForType(field);
495
+ }
496
+ return acc;
497
+ }, {});
498
+ setFormData(t)
499
+ setRecord(null)
500
+ e.preventDefault()
501
+ }
502
+
503
+ const {t } = useTranslation();
504
+
505
+ // --- Le reste du composant DataEditor reste identique ---
506
+
507
+ if (!model) {
508
+ return <p>Sélectionnez un modèle pour afficher le formulaire.</p>;
509
+ }
510
+
511
+ const title = record ? t('btns.editModel', `Edit ${model.name}`) : `${t('add_in_model')} ${t(`model_${model.name}`, model.name)}`;
512
+
513
+ const hint = (model, field) => <div className="hint-icon"><FaCircleInfo data-field={t(`field_${model.name}_${field.name}`, field.name)} data-tooltip-id={`tooltipHint`} data-tooltip-content={i18n.t(`field_${model.name}_${field.name}_hint`, field.hint || '')} /></div>
514
+
515
+ return (
516
+ <div ref={ref} className="data-add">
517
+ <h2>{title}</h2>
518
+ <Tooltip id={"tooltipHint"}
519
+ place={"top-end"}
520
+ render={({ content, activeAnchor }) => {
521
+ gtag('render hint' + activeAnchor?.getAttribute('data-field'));
522
+ return (
523
+ <><strong>{activeAnchor?.getAttribute('data-field')}</strong><br/>
524
+ <p className="ws-pre-line">{content}</p></>
525
+ )
526
+ }} />
527
+ <form className="flex flex-start flex-row flex-1" onSubmit={handleSubmit}>
528
+ {model?.fields.map((field) => {
529
+ // Appel à renderInputField qui gère maintenant la condition
530
+ const inputElement = renderInputField(model, field, formData[field.name], handleChange, refreshTime, formData, me, models); // Passer formData
531
+
532
+ if( field.type === 'relation' && !models.find(f => f.name === field.relation && f._user === me?.username ))
533
+ return <></>
534
+
535
+ // Si l'élément n'estF pas null (condition remplie), on l'affiche avec son label
536
+ return inputElement ? (
537
+ <><div key={"editor-"+model.name+":"+field.name} className="flex flex-mini-gap">
538
+ {t('field_'+model.name+'_'+field.name, '') && hint(model, field)}
539
+ <label htmlFor={field.name}
540
+ data-tooltip-id={`tooltipHint`}
541
+ data-field={t(`field_${model.name}_${field.name}`, field.name)}
542
+ data-tooltip-content={i18n.t(`field_${model.name}_${field.name}_hint`, field.hint || '')}>
543
+ {t(`field_${model.name}_${field.name}`, field.name)} {field.required ? <span className={"required"}>*</span> : <></>} {field.type==="string_t" ? <><FaLanguage title={'Translated in "' + (formData?.[field.name]?.value || '')+'"'} /></> : <></>}
544
+ </label>
545
+ </div>
546
+ {inputElement}</>
547
+ ) : null; // Ne rien afficher si la condition n'est pas remplie
548
+ })}
549
+ <div className="flex flex-centered">
550
+ <Button type="submit" disabled={isLoading}><Trans i18nKey="btns.save">Enregistrer</Trans></Button>
551
+ <Button type="submit" onClick={handleNew}><Trans i18nKey="btns.new">Nouveau</Trans></Button>
552
+ </div>
553
+ </form>
554
+ </div>
555
+ );
556
+ });