data-primals-engine 1.5.2 → 1.6.1

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