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