data-primals-engine 1.4.2 → 1.5.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.
- package/README.md +878 -856
- package/client/package-lock.json +82 -0
- package/client/package.json +2 -0
- package/client/src/App.jsx +1 -1
- package/client/src/App.scss +25 -7
- package/client/src/AssistantChat.scss +3 -2
- package/client/src/ConditionBuilder.jsx +1 -1
- package/client/src/ConditionBuilder2.jsx +2 -1
- package/client/src/DashboardView.jsx +569 -569
- package/client/src/DataEditor.jsx +376 -368
- package/client/src/DataLayout.jsx +4 -10
- package/client/src/DataTable.jsx +858 -817
- package/client/src/Field.jsx +1825 -1784
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/GeolocationField.jsx +94 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/ModelCreator.jsx +1 -2
- package/client/src/ModelCreatorField.jsx +24 -27
- package/client/src/ModelList.jsx +1 -1
- package/client/src/RelationField.jsx +2 -2
- package/client/src/constants.js +3 -3
- package/client/src/filter.js +0 -155
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/translations.js +14 -2
- package/package.json +2 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +264 -31
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +1544 -1540
- package/src/email.js +5 -2
- package/src/engine.js +10 -3
- package/src/filter.js +274 -260
- package/src/i18n.js +187 -177
- package/src/modules/assistant/assistant.js +3 -1
- package/src/modules/bucket.js +12 -15
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +2 -1
- package/src/modules/data/data.js +6 -3
- package/src/modules/data/data.operations.js +610 -168
- package/src/modules/data/data.routes.js +1821 -1785
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +7 -1
- package/src/modules/file.js +4 -2
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +22 -5
- package/src/providers.js +22 -7
- package/swagger-en.yml +133 -0
- package/test/data.integration.test.js +1060 -981
- package/test/import_export.integration.test.js +1 -1
- package/test/model.integration.test.js +377 -221
|
@@ -1,369 +1,377 @@
|
|
|
1
|
-
import React, {forwardRef, useEffect, useState} from "react"; // Ajout de useEffect
|
|
2
|
-
import {conditionToApiSearchFilter, getDefaultForType} from "../../src/data.js";
|
|
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, DurationField, EmailField,
|
|
12
|
-
EnumField,
|
|
13
|
-
FileField, ModelField, NumberField,
|
|
14
|
-
PhoneField, RangeField,
|
|
15
|
-
SelectField,
|
|
16
|
-
TextField
|
|
17
|
-
} from "./Field.jsx";
|
|
18
|
-
import i18n from "../../src/i18n.js";
|
|
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
|
-
import {isConditionMet} from "../../src/filter";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
case '
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
field.
|
|
130
|
-
field.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{formData[field.name]
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
tp.hint
|
|
198
|
-
tp.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
onChange
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
case '
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
1
|
+
import React, {forwardRef, useEffect, useState} from "react"; // Ajout de useEffect
|
|
2
|
+
import {conditionToApiSearchFilter, getDefaultForType} from "../../src/data.js";
|
|
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, DurationField, EmailField,
|
|
12
|
+
EnumField,
|
|
13
|
+
FileField, ModelField, NumberField,
|
|
14
|
+
PhoneField, RangeField,
|
|
15
|
+
SelectField,
|
|
16
|
+
TextField
|
|
17
|
+
} from "./Field.jsx";
|
|
18
|
+
import i18n from "../../src/i18n.js";
|
|
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
|
+
import {isConditionMet} from "../../src/filter";
|
|
29
|
+
import GeolocationField from "./GeolocationField.jsx";
|
|
30
|
+
|
|
31
|
+
// ... (fonction getInputType) ...
|
|
32
|
+
// Fonction pour obtenir le type d'input HTML basé sur le type de champ du modèle
|
|
33
|
+
const getInputType = (fieldType) => {
|
|
34
|
+
switch (fieldType) {
|
|
35
|
+
case 'number':
|
|
36
|
+
return 'number';
|
|
37
|
+
case 'date':
|
|
38
|
+
return 'date';
|
|
39
|
+
case 'datetime':
|
|
40
|
+
return 'datetime-local';
|
|
41
|
+
case 'email':
|
|
42
|
+
return 'email';
|
|
43
|
+
case 'password':
|
|
44
|
+
return 'password';
|
|
45
|
+
case 'url':
|
|
46
|
+
return 'url';
|
|
47
|
+
case 'phone': // Note: 'tel' est le type HTML correct
|
|
48
|
+
return 'tel';
|
|
49
|
+
case 'color':
|
|
50
|
+
return 'color';
|
|
51
|
+
// Pour les autres types comme 'string', 'string_t', 'enum', etc.
|
|
52
|
+
default:
|
|
53
|
+
return 'text';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
export const DataEditor = forwardRef(function MyDataEditor({
|
|
59
|
+
isLoading,
|
|
60
|
+
model,
|
|
61
|
+
onSubmit,
|
|
62
|
+
refreshTime,
|
|
63
|
+
formData,
|
|
64
|
+
setFormData, record, setRecord}, ref){
|
|
65
|
+
|
|
66
|
+
const {me} = useAuthContext()
|
|
67
|
+
const {models} = useModelContext()
|
|
68
|
+
|
|
69
|
+
const [viewMode, toggleViewMode]= useState(false);
|
|
70
|
+
|
|
71
|
+
function renderInputField(model, field, value, handleChange, refreshTime, formData, user, models) { // Ajout de formData
|
|
72
|
+
|
|
73
|
+
// Évaluer la condition AVANT de rendre le champ
|
|
74
|
+
if (!isConditionMet(model, field.condition, formData, models, user, false)) {
|
|
75
|
+
return null; // Ne rien rendre si la condition n'est pas remplie
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const inputProps = {
|
|
79
|
+
id: field.name,
|
|
80
|
+
name: field.uniqueName || field.name,
|
|
81
|
+
value: typeof(value) === 'undefined' ? getDefaultForType(field): value,
|
|
82
|
+
onChange: (v) => handleChange(v),
|
|
83
|
+
required: field.required,
|
|
84
|
+
placeholder: field.placeholder,
|
|
85
|
+
key: field.name
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// --- Le reste de la fonction renderInputField reste identique ---
|
|
89
|
+
switch (field.type) {
|
|
90
|
+
case 'model':
|
|
91
|
+
return <ModelField value={value} onChange={handleChange} model={model} field={field} formData={formData} />;
|
|
92
|
+
case 'modelField':
|
|
93
|
+
return <ModelField fields={true} value={value} onChange={handleChange} model={model} field={field} formData={formData} />;
|
|
94
|
+
case 'datetime':
|
|
95
|
+
case 'date':
|
|
96
|
+
if( field.min )
|
|
97
|
+
inputProps["min"] = field.min;
|
|
98
|
+
if( field.max)
|
|
99
|
+
inputProps["max"] = field.max;
|
|
100
|
+
|
|
101
|
+
// Utilisation d'un wrapper div pour appliquer la clé unique même si le champ est conditionnel
|
|
102
|
+
return <input key={field.name} type={getInputType(field.type)} {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} />;
|
|
103
|
+
case 'textarea':
|
|
104
|
+
return <textarea key={field.name} {...inputProps} />
|
|
105
|
+
case 'richtext':
|
|
106
|
+
return <RTE help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} {...inputProps} field={field} name={formData._id} />;
|
|
107
|
+
case 'richtext_t':
|
|
108
|
+
return <RTETrans
|
|
109
|
+
key={field.name}
|
|
110
|
+
value={value}
|
|
111
|
+
onChange={(newValue) => handleChange({ name: field.name, value: newValue })}
|
|
112
|
+
field={field}
|
|
113
|
+
/>;
|
|
114
|
+
case 'enum':
|
|
115
|
+
return <EnumField key={field.name} inputProps={inputProps} value={value} handleChange={handleChange} field={field} />
|
|
116
|
+
case "object":
|
|
117
|
+
return <CodeField
|
|
118
|
+
language={field.language}
|
|
119
|
+
name={inputProps.name}
|
|
120
|
+
// Passer la valeur brute (chaîne JSON) depuis formData
|
|
121
|
+
value={value ? JSON.stringify(value) : ''}
|
|
122
|
+
// CodeField appelle déjà handleChange avec { name, value }
|
|
123
|
+
onChange={handleChange}
|
|
124
|
+
/>
|
|
125
|
+
case 'code':
|
|
126
|
+
// --- Détection pour afficher le ConditionBuilder ---
|
|
127
|
+
{
|
|
128
|
+
const isConditionBuilderField =
|
|
129
|
+
field.type === 'code' &&
|
|
130
|
+
field.language === 'json' &&
|
|
131
|
+
field.conditionBuilder === true;
|
|
132
|
+
if (isConditionBuilderField) {
|
|
133
|
+
// Utiliser la valeur de formData, parsée en objet
|
|
134
|
+
const conditionObject = formData[field.name];
|
|
135
|
+
const currentViewMode = viewMode || 'builder';
|
|
136
|
+
let builderModelName = model.name; // Par défaut, le modèle en cours d'édition
|
|
137
|
+
if (field.targetModel) {
|
|
138
|
+
if (typeof field.targetModel === 'string' && field.targetModel.startsWith('$')) {
|
|
139
|
+
// C'est une référence dynamique à un autre champ du formulaire
|
|
140
|
+
const dynamicFieldName = field.targetModel.substring(1);
|
|
141
|
+
// On utilise la valeur du champ pointé s'il existe dans formData, sinon on garde le modèle par défaut
|
|
142
|
+
builderModelName = formData[dynamicFieldName] || model.name;
|
|
143
|
+
} else {
|
|
144
|
+
// C'est un nom de modèle statique
|
|
145
|
+
builderModelName = field.targetModel;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return <div className={"flex flex-1"} style={{width:'100%'}} key={field.name}>
|
|
149
|
+
{currentViewMode !== 'builder' && ( <div className="condition-builder-toggle">
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={e => toggleViewMode(!viewMode)}
|
|
153
|
+
title={currentViewMode === 'builder' ? "Passer à la vue JSON" : "Passer au constructeur visuel"}
|
|
154
|
+
>
|
|
155
|
+
{currentViewMode === 'builder' ? <FaCode /> : <FaSitemap />}
|
|
156
|
+
</button>
|
|
157
|
+
</div>)}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
{currentViewMode === 'builder' ? (
|
|
161
|
+
<ConditionBuilder
|
|
162
|
+
key={builderModelName}
|
|
163
|
+
initialValue={conditionObject}
|
|
164
|
+
models={models}
|
|
165
|
+
user={user}
|
|
166
|
+
model={builderModelName}
|
|
167
|
+
modelFields={model.fields}
|
|
168
|
+
onChange={(newConditionObject) => {
|
|
169
|
+
handleChange({ name: field.name, value: newConditionObject });
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
) : (
|
|
173
|
+
<>
|
|
174
|
+
{/*JSON.stringify(conditionToApiSearchFilter(JSON.parse(formData[field.name])))*/}
|
|
175
|
+
{formData[field.name] && (<CodeField
|
|
176
|
+
language={field.language}
|
|
177
|
+
name={inputProps.name}
|
|
178
|
+
// Passer la valeur brute (chaîne JSON) depuis formData
|
|
179
|
+
value={formData[field.name] || ''}
|
|
180
|
+
// CodeField appelle déjà handleChange avec { name, value }
|
|
181
|
+
onChange={handleChange}
|
|
182
|
+
/>)}</>
|
|
183
|
+
)}</div>
|
|
184
|
+
}
|
|
185
|
+
return <CodeField key={field.name} language={field.language} name={inputProps.name}
|
|
186
|
+
value={typeof (value) === 'string' ? value : JSON.stringify(value)}
|
|
187
|
+
onChange={handleChange}/>
|
|
188
|
+
}
|
|
189
|
+
case 'array':
|
|
190
|
+
if (field.itemsType === 'file') {
|
|
191
|
+
return <div key={field.name}><FileField name={field.name} maxSize={field.maxSize}
|
|
192
|
+
mimeTypes={field.mimeTypes} value={value} onChange={(files) => {
|
|
193
|
+
handleChange({name: field.name, value: files});
|
|
194
|
+
}} /></div>;
|
|
195
|
+
} else {
|
|
196
|
+
const tp = {...field, type: field.itemsType};
|
|
197
|
+
const lbl = tp.hint;
|
|
198
|
+
tp.hint = '';
|
|
199
|
+
tp.uniqueName = uniqid(field.name);
|
|
200
|
+
let vs = !Array.isArray(value) || !value.length ? [getDefaultForType(tp)] : value;
|
|
201
|
+
// Note: La condition s'applique au champ 'array' lui-même, pas aux éléments internes.
|
|
202
|
+
return <><p className="hint">{t('field_' + model.name + '_' + tp.name + '_hint', lbl || '')}</p><Draggable items={vs} onChange={(arr) => {
|
|
203
|
+
handleChange({name: field.name, value: arr});
|
|
204
|
+
}} renderItem={(item,i) => <div
|
|
205
|
+
className="array-element flex">{renderInputField(model, tp, item, (e) => { // Passer formData ici aussi si les éléments internes peuvent avoir des conditions
|
|
206
|
+
handleChange({name: field.name, value: vs.map(((v, vi) => vi === i ? e.value : v))});
|
|
207
|
+
}, refreshTime, formData, user, models)}
|
|
208
|
+
<FaMinus onClick={() => {
|
|
209
|
+
handleChange({name: field.name, value: value.filter((ve, vj) => vj !== i)});
|
|
210
|
+
}}/>
|
|
211
|
+
</div>}/><FaPlus className="cursor-pointer" onClick={() => {
|
|
212
|
+
handleChange({name: field.name, value: [...vs, getDefaultForType(tp)]});
|
|
213
|
+
}}/></>;
|
|
214
|
+
}
|
|
215
|
+
case "number":
|
|
216
|
+
if (field.type === 'number' && field.gauge) {
|
|
217
|
+
return <RangeField
|
|
218
|
+
name={field.name}
|
|
219
|
+
value={value}
|
|
220
|
+
onChange={(value) => handleChange({name: field.name, value})}
|
|
221
|
+
min={field.min}
|
|
222
|
+
max={field.max}
|
|
223
|
+
percent={field.percent}
|
|
224
|
+
step={field.step || 1}
|
|
225
|
+
/>;
|
|
226
|
+
}
|
|
227
|
+
if (field.delay) {
|
|
228
|
+
return <DurationField
|
|
229
|
+
{...inputProps}
|
|
230
|
+
// DurationField's onChange provides an object: { name, value }
|
|
231
|
+
onChange={({ value }) => handleChange({name: field.name, value})}
|
|
232
|
+
/>;
|
|
233
|
+
}
|
|
234
|
+
inputProps["step"] = field.step || 0.1;
|
|
235
|
+
if( field.min )
|
|
236
|
+
inputProps["min"] = field.min;
|
|
237
|
+
if( field.max)
|
|
238
|
+
inputProps["max"] = field.max;
|
|
239
|
+
return <NumberField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} unit={field.unit} key={field.name} {...inputProps} onChange={(e) => handleChange({name: field.name, value: parseFloat(e.target.value.replace(',', '.'))})} />
|
|
240
|
+
case 'relation':
|
|
241
|
+
return (
|
|
242
|
+
<RelationField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} model={model} field={field} value={value} onChange={(e) => {
|
|
243
|
+
handleChange(e)
|
|
244
|
+
}} refreshTime={refreshTime} />
|
|
245
|
+
);
|
|
246
|
+
case 'cronSchedule':
|
|
247
|
+
return <CronBuilder
|
|
248
|
+
cronExpression={value}
|
|
249
|
+
cronMask={field.cronMask}
|
|
250
|
+
defaultCronExpression={field.default}
|
|
251
|
+
onCronChange={(newCron) => {
|
|
252
|
+
handleChange({name: field.name, value: newCron});
|
|
253
|
+
}}
|
|
254
|
+
/>;
|
|
255
|
+
case 'phone':
|
|
256
|
+
return <PhoneField help={t('field_' + model.name + '_' + field.name + '_hint', field.hint || '')}
|
|
257
|
+
key={field.name} name={field.name} value={inputProps.value}
|
|
258
|
+
onChange={(e) => handleChange({name: field.name, value: e})}/>
|
|
259
|
+
case 'boolean':
|
|
260
|
+
return <CheckboxField
|
|
261
|
+
help={t('field_' + model.name + '_' + field.name + '_hint', field.hint || '')}
|
|
262
|
+
key={field.name} {...inputProps} checked={value}
|
|
263
|
+
onChange={(e) => handleChange({name: field.name, value: e})}/>
|
|
264
|
+
case 'string':
|
|
265
|
+
case 'string_t':
|
|
266
|
+
{
|
|
267
|
+
if (field.maxlength)
|
|
268
|
+
inputProps["maxlength"] = field.maxlength;
|
|
269
|
+
if (typeof (field.multiline) !== 'undefined')
|
|
270
|
+
inputProps["multiline"] = !!field.multiline;
|
|
271
|
+
|
|
272
|
+
const displayValue = (typeof value === 'object' && value !== null) ? value.key : (value || '');
|
|
273
|
+
|
|
274
|
+
return <TextField
|
|
275
|
+
help={t('field_' + model.name + '_' + field.name + '_hint', field.hint || '')}
|
|
276
|
+
key={field.name}
|
|
277
|
+
type={getInputType(field.type)} {...inputProps}
|
|
278
|
+
value={displayValue}
|
|
279
|
+
onChange={(e) => handleChange({name: field.name, value: e.target.value})} />
|
|
280
|
+
}
|
|
281
|
+
case 'file':
|
|
282
|
+
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 })} />
|
|
283
|
+
case 'color':
|
|
284
|
+
return <ColorField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} name={field.name} value={value} onChange={handleChange} />
|
|
285
|
+
case 'email':
|
|
286
|
+
return <EmailField help={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})} />
|
|
287
|
+
case 'geolocation':
|
|
288
|
+
return <GeolocationField
|
|
289
|
+
key={field.name}
|
|
290
|
+
name={field.name}
|
|
291
|
+
value={value}
|
|
292
|
+
onChange={(newValue) => handleChange(newValue)}
|
|
293
|
+
/>;
|
|
294
|
+
default:
|
|
295
|
+
return <TextField help={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})} />
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- handleChange, handleSubmit, handleNew restent identiques ---
|
|
300
|
+
const handleChange = (event) => {
|
|
301
|
+
const { name, value } = event;
|
|
302
|
+
setFormData(currentFormData => ({ ...currentFormData, [name]: value }));
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const handleSubmit = (event) => {
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
onSubmit(formData, record); // Pass record to onSubmit
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const handleNew = (e) => {
|
|
311
|
+
const t = [...model.fields].reduce((acc, field, index) => {
|
|
312
|
+
if( field.type === "relation"){
|
|
313
|
+
acc[field.name] = field.multiple ? [] : null;
|
|
314
|
+
}else {
|
|
315
|
+
acc[field.name] = getDefaultForType(field);
|
|
316
|
+
}
|
|
317
|
+
return acc;
|
|
318
|
+
}, {});
|
|
319
|
+
setFormData(t)
|
|
320
|
+
setRecord(null)
|
|
321
|
+
e.preventDefault()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const {t } = useTranslation();
|
|
325
|
+
|
|
326
|
+
// --- Le reste du composant DataEditor reste identique ---
|
|
327
|
+
|
|
328
|
+
if (!model) {
|
|
329
|
+
return <p>Sélectionnez un modèle pour afficher le formulaire.</p>;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const title = record ? t('editData', 'Edit {{0}}', [model.name]) : `${t('add_in_model')} ${t(`model_${model.name}`, model.name)}`;
|
|
333
|
+
|
|
334
|
+
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>
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div ref={ref} className="data-add">
|
|
338
|
+
<h2>{title}</h2>
|
|
339
|
+
<Tooltip id={"tooltipHint"}
|
|
340
|
+
place={"top-end"}
|
|
341
|
+
render={({ content, activeAnchor }) => {
|
|
342
|
+
gtag('render hint' + activeAnchor?.getAttribute('data-field'));
|
|
343
|
+
return (
|
|
344
|
+
<><strong>{activeAnchor?.getAttribute('data-field')}</strong><br/>
|
|
345
|
+
<p className="ws-pre-line">{content}</p></>
|
|
346
|
+
)
|
|
347
|
+
}} />
|
|
348
|
+
<form className="flex flex-start flex-row flex-1" onSubmit={handleSubmit}>
|
|
349
|
+
{model?.fields.map((field) => {
|
|
350
|
+
// Appel à renderInputField qui gère maintenant la condition
|
|
351
|
+
const inputElement = renderInputField(model, field, formData[field.name], handleChange, refreshTime, formData, me, models); // Passer formData
|
|
352
|
+
|
|
353
|
+
if( field.type === 'relation' && !models.find(f => f.name === field.relation && f._user === me?.username ))
|
|
354
|
+
return <></>
|
|
355
|
+
|
|
356
|
+
// Si l'élément n'estF pas null (condition remplie), on l'affiche avec son label
|
|
357
|
+
return inputElement ? (
|
|
358
|
+
<><div key={"editor-"+model.name+":"+field.name} className="flex flex-mini-gap">
|
|
359
|
+
{t('field_'+model.name+'_'+field.name, '') && hint(model, field)}
|
|
360
|
+
<label htmlFor={field.name}
|
|
361
|
+
data-tooltip-id={`tooltipHint`}
|
|
362
|
+
data-field={t(`field_${model.name}_${field.name}`, field.name)}
|
|
363
|
+
data-tooltip-content={i18n.t(`field_${model.name}_${field.name}_hint`, field.hint || '')}>
|
|
364
|
+
{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 || '')+'"'} /></> : <></>}
|
|
365
|
+
</label>
|
|
366
|
+
</div>
|
|
367
|
+
{inputElement}</>
|
|
368
|
+
) : null; // Ne rien afficher si la condition n'est pas remplie
|
|
369
|
+
})}
|
|
370
|
+
<div className="flex flex-centered">
|
|
371
|
+
<Button type="submit" disabled={isLoading}><Trans i18nKey="btns.save">Enregistrer</Trans></Button>
|
|
372
|
+
<Button type="submit" onClick={handleNew}><Trans i18nKey="btns.new">Nouveau</Trans></Button>
|
|
373
|
+
</div>
|
|
374
|
+
</form>
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
369
377
|
});
|