data-primals-engine 1.4.3 → 1.5.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.
- package/README.md +913 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +3 -7
- package/client/src/App.scss +25 -3
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +30 -12
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +104 -19
- package/client/src/DataEditor.jsx +12 -5
- package/client/src/DataLayout.jsx +805 -762
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +63 -77
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +591 -322
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +6 -6
- package/client/src/ModelCreatorField.jsx +74 -31
- package/client/src/ModelList.jsx +93 -54
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +26 -24
- package/package.json +3 -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 +3 -28
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +63 -7
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +151 -19
- package/src/modules/bucket.js +14 -16
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +9 -56
- package/src/modules/data/data.operations.js +3282 -2999
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +118 -24
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +85 -3
- package/src/modules/file.js +247 -236
- 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 +5482 -5461
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +136 -2
- package/test/import_export.integration.test.js +1 -1
package/client/src/Field.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
forwardRef, useCallback,
|
|
3
3
|
useEffect,
|
|
4
|
+
useMemo,
|
|
4
5
|
useImperativeHandle,
|
|
5
6
|
useRef,
|
|
6
7
|
useState,
|
|
@@ -14,13 +15,15 @@ import { CodeiumEditor } from "@codeium/react-code-editor";
|
|
|
14
15
|
import {debounce, escapeRegExp, isGUID, isLightColor} from "../../src/core.js";
|
|
15
16
|
import {mainFieldsTypes, maxFileSize} from "../../src/constants.js";
|
|
16
17
|
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
18
|
+
import { SketchPicker } from 'react-color'; // Importer le sélecteur
|
|
17
19
|
import {useQueryClient} from "react-query";
|
|
18
|
-
import
|
|
20
|
+
import tinycolor from 'tinycolor2';
|
|
21
|
+
import {
|
|
19
22
|
FaArrowDown,
|
|
20
|
-
FaArrowUp, FaAt, FaEye, FaEyeSlash,
|
|
23
|
+
FaArrowUp, FaAt, FaCheckCircle, FaEye, FaEyeSlash, FaExclamationCircle,
|
|
21
24
|
FaCalendar, FaCalendarWeek, FaCode, FaFile,
|
|
22
25
|
FaHashtag, FaIcons,
|
|
23
|
-
FaImage,
|
|
26
|
+
FaImage, FaSpinner,
|
|
24
27
|
FaLink, FaListOl, FaListUl,
|
|
25
28
|
FaLock, FaMinus,
|
|
26
29
|
FaPallet, FaPhone, FaSitemap,
|
|
@@ -28,13 +31,12 @@ import {
|
|
|
28
31
|
} from "react-icons/fa";
|
|
29
32
|
import * as Fa6Icons from 'react-icons/fa6'; // Importer Fa6
|
|
30
33
|
import {FaCalendarDays, FaCodeCompare, FaPencil, FaT, FaTableColumns} from "react-icons/fa6";
|
|
31
|
-
import { CodeBlock, tomorrowNightBright } from 'react-code-blocks';
|
|
32
34
|
import SyntaxHighlighter from 'react-syntax-highlighter';
|
|
33
|
-
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
|
34
35
|
|
|
35
36
|
import { PhoneInput } from 'react-international-phone';
|
|
36
37
|
import 'react-international-phone/style.css';
|
|
37
38
|
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
39
|
+
import {useRealtimeValidation} from "./hooks/useValidation.js";
|
|
38
40
|
import Switch from "react-switch";
|
|
39
41
|
export const Form = ({
|
|
40
42
|
name,
|
|
@@ -78,167 +80,292 @@ export const Form = ({
|
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
const TextField = forwardRef(function TextField(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
83
|
+
{
|
|
84
|
+
name,
|
|
85
|
+
label,
|
|
86
|
+
placeholder,
|
|
87
|
+
help,
|
|
88
|
+
editable,
|
|
89
|
+
value,
|
|
90
|
+
required,
|
|
91
|
+
readOnly,
|
|
92
|
+
onChange,
|
|
93
|
+
multiline,
|
|
94
|
+
minlength,
|
|
95
|
+
maxlength,
|
|
96
|
+
searchable,
|
|
97
|
+
labelProps,
|
|
98
|
+
showErrors = false,
|
|
99
|
+
before,
|
|
100
|
+
after,
|
|
101
|
+
validation,
|
|
102
|
+
showTooltipErrors = false,
|
|
103
|
+
mask,
|
|
104
|
+
replacement,
|
|
105
|
+
...rest
|
|
106
|
+
},
|
|
107
|
+
ref,
|
|
102
108
|
) {
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
109
|
+
const [id, setId] = useState("textfield-" + uniqid());
|
|
110
|
+
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
|
111
|
+
const [errors, setErrors] = useState([]);
|
|
112
|
+
const inputRef = useRef();
|
|
113
|
+
|
|
114
|
+
const parsedReplacement = useMemo(() => {
|
|
115
|
+
if (!replacement || typeof replacement !== 'object') return null;
|
|
116
|
+
const newRep = {};
|
|
117
|
+
try {
|
|
118
|
+
for (const key in replacement) {
|
|
119
|
+
// La valeur du modèle est une chaîne (ex: "\\d"), on crée un RegExp
|
|
120
|
+
newRep[key] = new RegExp(replacement[key]);
|
|
121
|
+
}
|
|
122
|
+
return newRep;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error("Invalid regex pattern in mask replacement:", e);
|
|
125
|
+
return null; // Configuration de remplacement invalide
|
|
126
|
+
}
|
|
127
|
+
}, [replacement]);
|
|
128
|
+
|
|
129
|
+
// --- Real-time validation logic ---
|
|
130
|
+
const {
|
|
131
|
+
validate: realtimeValidate,
|
|
132
|
+
validationState
|
|
133
|
+
} = useRealtimeValidation(
|
|
134
|
+
validation?.modelName,
|
|
135
|
+
validation?.docId
|
|
136
|
+
);
|
|
137
|
+
const isRealtimeValidationEnabled = !!validation?.modelName;
|
|
138
|
+
const fieldValidationState = validationState[name] || { status: 'idle' };
|
|
139
|
+
|
|
140
|
+
const { type, ...otherRest } = rest;
|
|
141
|
+
const isPasswordField = type === 'password';
|
|
142
|
+
|
|
143
|
+
const mult = typeof multiline !== 'undefined' ? multiline : maxlength > 255;
|
|
144
|
+
|
|
145
|
+
const validate = () => {
|
|
146
|
+
const errs = [];
|
|
147
|
+
if (required && (!value || String(value).trim() === "")) {
|
|
148
|
+
errs.push("Field required");
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
minlength > 0 &&
|
|
152
|
+
typeof value == "string" &&
|
|
153
|
+
value.trim().length < minlength
|
|
154
|
+
) {
|
|
155
|
+
errs.push("Value length must be >= to " + minlength);
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
maxlength !== undefined && maxlength > 0 &&
|
|
159
|
+
typeof value == "string" &&
|
|
160
|
+
value.trim().length > maxlength
|
|
161
|
+
) {
|
|
162
|
+
errs.push("Value length must be <= to " + maxlength);
|
|
163
|
+
}
|
|
164
|
+
if (isRealtimeValidationEnabled && fieldValidationState.status === 'invalid') {
|
|
165
|
+
errs.push(fieldValidationState.error);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (showErrors) {
|
|
169
|
+
setErrors(errs);
|
|
170
|
+
}
|
|
171
|
+
return !errs.length && fieldValidationState.status !== 'invalid';
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
useImperativeHandle(ref, () => ({
|
|
175
|
+
ref: inputRef.current,
|
|
176
|
+
validate,
|
|
177
|
+
getValue: () => value,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const handleMaskedChange = (e) => {
|
|
181
|
+
const inputValue = e.target.value;
|
|
182
|
+
let newValue = '';
|
|
183
|
+
let rawIndex = 0;
|
|
184
|
+
|
|
185
|
+
// Appliquer le masque
|
|
186
|
+
for (let i = 0; i < mask.length; i++) {
|
|
187
|
+
const maskChar = mask[i];
|
|
188
|
+
|
|
189
|
+
// Si nous avons dépassé la longueur de la valeur d'entrée, sortir de la boucle
|
|
190
|
+
if (rawIndex >= inputValue.length) break;
|
|
191
|
+
|
|
192
|
+
// Si le caractère du masque est un placeholder (défini dans replacement)
|
|
193
|
+
if (parsedReplacement && parsedReplacement[maskChar]) {
|
|
194
|
+
const pattern = parsedReplacement[maskChar];
|
|
195
|
+
const inputChar = inputValue[rawIndex];
|
|
196
|
+
|
|
197
|
+
// Vérifier si le caractère correspond au motif
|
|
198
|
+
if (pattern.test(inputChar)) {
|
|
199
|
+
newValue += inputChar;
|
|
200
|
+
rawIndex++;
|
|
201
|
+
} else {
|
|
202
|
+
// Ignorer les caractères qui ne correspondent pas au motif
|
|
203
|
+
rawIndex++;
|
|
204
|
+
i--; // Réessayer avec le même caractère de masque
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// Si c'est un caractère littéral du masque, l'ajouter
|
|
208
|
+
newValue += maskChar;
|
|
209
|
+
|
|
210
|
+
// Si le caractère d'entrée correspond au caractère littéral, avancer
|
|
211
|
+
if (inputValue[rawIndex] === maskChar) {
|
|
212
|
+
rawIndex++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Appeler le onChange parent avec la nouvelle valeur masquée
|
|
218
|
+
const syntheticEvent = {
|
|
219
|
+
...e,
|
|
220
|
+
target: {
|
|
221
|
+
...e.target,
|
|
222
|
+
value: newValue
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (onChange) {
|
|
227
|
+
onChange(syntheticEvent);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const handleChange = (e) => {
|
|
232
|
+
if (mask && parsedReplacement) {
|
|
233
|
+
handleMaskedChange(e);
|
|
234
|
+
} else if (onChange) {
|
|
235
|
+
onChange(e);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (isRealtimeValidationEnabled) {
|
|
239
|
+
realtimeValidate(name, e.target.value);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const togglePasswordVisibility = () => {
|
|
244
|
+
setIsPasswordVisible(prevState => !prevState);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (value !== null && !isRealtimeValidationEnabled) validate();
|
|
249
|
+
}, [value]);
|
|
250
|
+
|
|
251
|
+
const combinedErrors = [...errors];
|
|
252
|
+
if (isRealtimeValidationEnabled && fieldValidationState.status === 'invalid' && fieldValidationState.error && !combinedErrors.includes(fieldValidationState.error)) {
|
|
253
|
+
combinedErrors.push(fieldValidationState.error);
|
|
143
254
|
}
|
|
144
|
-
|
|
255
|
+
const hasErrors = combinedErrors.length > 0;
|
|
256
|
+
// Sanitize errors for HTML attribute to prevent XSS
|
|
257
|
+
const errorsHtml = hasErrors ? `<ul>${combinedErrors.map(e => `<li>${String(e).replace(/</g, '<').replace(/>/g, '>')}</li>`).join('')}</ul>` : '';
|
|
258
|
+
|
|
259
|
+
const renderValidationIcon = () => {
|
|
260
|
+
if (!isRealtimeValidationEnabled) return null;
|
|
261
|
+
const { status, error } = fieldValidationState;
|
|
262
|
+
switch (status) {
|
|
263
|
+
case 'validating': return <FaSpinner className="spin-icon validation-icon validating" title="Validating..." />;
|
|
264
|
+
case 'valid': return <FaCheckCircle className="validation-icon valid" title="Valid" />;
|
|
265
|
+
case 'invalid': return <FaExclamationCircle className="validation-icon invalid" title={error} />;
|
|
266
|
+
default: return null;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
145
269
|
|
|
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
|
-
|
|
270
|
+
return (
|
|
271
|
+
<>
|
|
272
|
+
<div
|
|
273
|
+
className={cn({
|
|
274
|
+
field: true,
|
|
275
|
+
flex: true,
|
|
276
|
+
"field-text": !mult,
|
|
277
|
+
"field-multiline": mult,
|
|
278
|
+
'is-validating': fieldValidationState.status === 'validating',
|
|
279
|
+
'is-valid': fieldValidationState.status === 'valid',
|
|
280
|
+
'is-invalid': hasErrors && fieldValidationState.status === 'invalid',
|
|
281
|
+
})}
|
|
282
|
+
{...(showTooltipErrors && hasErrors && {
|
|
283
|
+
'data-tooltip-id': "tooltipField",
|
|
284
|
+
'data-tooltip-html': errorsHtml
|
|
285
|
+
})}
|
|
286
|
+
>
|
|
287
|
+
{label && (
|
|
288
|
+
<label
|
|
289
|
+
contentEditable={editable}
|
|
290
|
+
className={cn({ help: !!help, 'flex-1': true })}
|
|
291
|
+
htmlFor={id}
|
|
292
|
+
{...labelProps}
|
|
293
|
+
>
|
|
294
|
+
{label}
|
|
295
|
+
{required ? (
|
|
296
|
+
<span className="mandatory" contentEditable={false}>
|
|
172
297
|
*
|
|
173
298
|
</span>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{help &&<div className="flex help">{help}</div>}
|
|
299
|
+
) : (
|
|
300
|
+
""
|
|
301
|
+
)}
|
|
302
|
+
</label>
|
|
303
|
+
)}
|
|
181
304
|
|
|
182
|
-
|
|
183
|
-
<textarea
|
|
184
|
-
ref={inputRef}
|
|
185
|
-
aria-required={required}
|
|
186
|
-
aria-readonly={readOnly}
|
|
187
|
-
readOnly={readOnly}
|
|
188
|
-
placeholder={placeholder}
|
|
189
|
-
id={id}
|
|
190
|
-
name={name}
|
|
191
|
-
value={value || ""}
|
|
192
|
-
rows={8}
|
|
193
|
-
onChange={handleChange}
|
|
194
|
-
minLength={minlength}
|
|
195
|
-
maxLength={maxlength}
|
|
196
|
-
{...rest}
|
|
197
|
-
></textarea>
|
|
198
|
-
)}
|
|
305
|
+
{help && <div className="flex help">{help}</div>}
|
|
199
306
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
{!mult && (
|
|
203
|
-
<input
|
|
307
|
+
{mult && (
|
|
308
|
+
<textarea
|
|
204
309
|
ref={inputRef}
|
|
205
310
|
aria-required={required}
|
|
206
311
|
aria-readonly={readOnly}
|
|
207
312
|
readOnly={readOnly}
|
|
208
|
-
type={isPasswordField ? (isPasswordVisible ? 'text' : 'password') : (searchable ? "search" : (type || "text"))}
|
|
209
313
|
placeholder={placeholder}
|
|
210
|
-
title={placeholder}
|
|
211
|
-
alt={placeholder}
|
|
212
314
|
id={id}
|
|
213
315
|
name={name}
|
|
214
316
|
value={value || ""}
|
|
317
|
+
rows={8}
|
|
215
318
|
onChange={handleChange}
|
|
216
319
|
minLength={minlength}
|
|
217
320
|
maxLength={maxlength}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{...otherRest}
|
|
221
|
-
/>
|
|
222
|
-
)}
|
|
223
|
-
{isPasswordField && !mult && (
|
|
224
|
-
<button type="button" onClick={togglePasswordVisibility} className="password-toggle-icon" style={{position: 'absolute', right: '10px', top: '50%', transform: 'translateY(-50%)', display: 'flex', alignItems: 'center'}}>
|
|
225
|
-
{isPasswordVisible ? <FaEyeSlash /> : <FaEye />}
|
|
226
|
-
</button>
|
|
321
|
+
{...rest}
|
|
322
|
+
></textarea>
|
|
227
323
|
)}
|
|
228
|
-
|
|
324
|
+
|
|
325
|
+
{before}
|
|
326
|
+
<div className={"flex flex-1 flex-no-gap flex-start"} style={{ position: 'relative' }}>
|
|
327
|
+
{!mult && (
|
|
328
|
+
<input
|
|
329
|
+
ref={inputRef}
|
|
330
|
+
aria-required={required}
|
|
331
|
+
aria-readonly={readOnly}
|
|
332
|
+
readOnly={readOnly}
|
|
333
|
+
type={isPasswordField ? (isPasswordVisible ? 'text' : 'password') : (searchable ? "search" : (type || "text"))}
|
|
334
|
+
placeholder={placeholder}
|
|
335
|
+
title={placeholder}
|
|
336
|
+
alt={placeholder}
|
|
337
|
+
id={id}
|
|
338
|
+
name={name}
|
|
339
|
+
value={value || ""}
|
|
340
|
+
onChange={handleChange}
|
|
341
|
+
minLength={minlength}
|
|
342
|
+
maxLength={maxlength}
|
|
343
|
+
required={required}
|
|
344
|
+
{...otherRest}
|
|
345
|
+
/>
|
|
346
|
+
)}
|
|
347
|
+
<div className="field-icons-wrapper" style={{ position: 'absolute', right: '10px', top: '50%', transform: 'translateY(-50%)', display: 'flex', alignItems: 'center', gap: '5px' }}>
|
|
348
|
+
{renderValidationIcon()}
|
|
349
|
+
{isPasswordField && !mult && (
|
|
350
|
+
<button type="button" onClick={togglePasswordVisibility} className="password-toggle-icon">
|
|
351
|
+
{isPasswordVisible ? <FaEyeSlash /> : <FaEye />}
|
|
352
|
+
</button>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
{after}
|
|
356
|
+
</div>
|
|
229
357
|
</div>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
);
|
|
358
|
+
{!showTooltipErrors && hasErrors && (
|
|
359
|
+
<ul className="error">
|
|
360
|
+
{combinedErrors.map((e, key) => (
|
|
361
|
+
<li key={key} aria-live="assertive" role="alert">
|
|
362
|
+
{e}
|
|
363
|
+
</li>
|
|
364
|
+
))}
|
|
365
|
+
</ul>
|
|
366
|
+
)}
|
|
367
|
+
</>
|
|
368
|
+
);
|
|
242
369
|
});
|
|
243
370
|
|
|
244
371
|
TextField.displayName = "TextField";
|
|
@@ -252,56 +379,103 @@ const EmailField = forwardRef(
|
|
|
252
379
|
placeholder,
|
|
253
380
|
help,
|
|
254
381
|
editable,
|
|
255
|
-
defaultValue
|
|
382
|
+
value, // Changed from defaultValue to be a controlled component
|
|
256
383
|
required,
|
|
257
384
|
readOnly,
|
|
258
385
|
onChange,
|
|
259
386
|
minlength,
|
|
260
387
|
maxlength,
|
|
261
|
-
|
|
388
|
+
validation,
|
|
389
|
+
showErrors = true,
|
|
390
|
+
showTooltipErrors = false,
|
|
262
391
|
},
|
|
263
392
|
ref,
|
|
264
393
|
) => {
|
|
265
394
|
const id = "emailfield-" + uniqid();
|
|
266
395
|
const [errors, setErrors] = useState([]);
|
|
267
|
-
const
|
|
396
|
+
const inputRef = useRef();
|
|
397
|
+
|
|
398
|
+
// --- Real-time validation logic ---
|
|
399
|
+
const {
|
|
400
|
+
validate: realtimeValidate,
|
|
401
|
+
validationState
|
|
402
|
+
} = useRealtimeValidation(
|
|
403
|
+
validation?.modelName,
|
|
404
|
+
validation?.docId
|
|
405
|
+
);
|
|
406
|
+
const isRealtimeValidationEnabled = !!validation?.modelName;
|
|
407
|
+
const fieldValidationState = validationState[name] || { status: 'idle' };
|
|
408
|
+
|
|
268
409
|
const validate = () => {
|
|
269
410
|
const errs = [];
|
|
270
|
-
if (required && (!value || value.trim() === "")) {
|
|
411
|
+
if (required && (!value || String(value).trim() === "")) {
|
|
271
412
|
errs.push("Field required");
|
|
272
413
|
}
|
|
273
|
-
if (minlength !== undefined && minlength > 0 && value && value.trim().length < minlength) {
|
|
414
|
+
if (minlength !== undefined && minlength > 0 && value && String(value).trim().length < minlength) {
|
|
274
415
|
errs.push("Value length must be >= to " + minlength);
|
|
275
416
|
}
|
|
276
|
-
if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
|
|
417
|
+
if (maxlength !== undefined && maxlength > 0 && value && String(value).trim().length > maxlength) {
|
|
277
418
|
errs.push("Value length must be <= to " + maxlength);
|
|
278
419
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
errs.push("Invalid email");
|
|
420
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
421
|
+
errs.push("Invalid email");
|
|
282
422
|
}
|
|
283
|
-
|
|
284
|
-
|
|
423
|
+
if (isRealtimeValidationEnabled && fieldValidationState.status === 'invalid') {
|
|
424
|
+
errs.push(fieldValidationState.error);
|
|
425
|
+
}
|
|
426
|
+
if (showErrors) setErrors(errs);
|
|
427
|
+
return !errs.length && fieldValidationState.status !== 'invalid';
|
|
285
428
|
};
|
|
429
|
+
|
|
286
430
|
useEffect(() => {
|
|
287
|
-
if (value !== null) validate();
|
|
431
|
+
if (value !== null && !isRealtimeValidationEnabled) validate();
|
|
288
432
|
}, [value]);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
433
|
+
|
|
434
|
+
const combinedErrors = [...errors];
|
|
435
|
+
if (isRealtimeValidationEnabled && fieldValidationState.status === 'invalid' && fieldValidationState.error && !combinedErrors.includes(fieldValidationState.error)) {
|
|
436
|
+
combinedErrors.push(fieldValidationState.error);
|
|
437
|
+
}
|
|
438
|
+
const hasErrors = combinedErrors.length > 0;
|
|
439
|
+
const errorsHtml = hasErrors ? `<ul>${combinedErrors.map(e => `<li>${String(e).replace(/</g, '<').replace(/>/g, '>')}</li>`).join('')}</ul>` : '';
|
|
440
|
+
|
|
292
441
|
useImperativeHandle(ref, () => ({
|
|
442
|
+
ref: inputRef.current,
|
|
293
443
|
validate,
|
|
294
444
|
getValue: () => value,
|
|
295
445
|
}));
|
|
446
|
+
|
|
296
447
|
const handleChange = (e) => {
|
|
297
|
-
setValue(e.target.value);
|
|
298
448
|
if (onChange) {
|
|
299
449
|
onChange(e);
|
|
300
450
|
}
|
|
451
|
+
if (isRealtimeValidationEnabled) {
|
|
452
|
+
realtimeValidate(name, e.target.value);
|
|
453
|
+
}
|
|
301
454
|
};
|
|
455
|
+
|
|
456
|
+
const renderValidationIcon = () => {
|
|
457
|
+
if (!isRealtimeValidationEnabled) return null;
|
|
458
|
+
const { status, error } = fieldValidationState;
|
|
459
|
+
switch (status) {
|
|
460
|
+
case 'validating': return <FaSpinner className="spin-icon validation-icon validating" title="Validating..." />;
|
|
461
|
+
case 'valid': return <FaCheckCircle className="validation-icon valid" title="Valid" />;
|
|
462
|
+
case 'invalid': return <FaExclamationCircle className="validation-icon invalid" title={error} />;
|
|
463
|
+
default: return null;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
302
467
|
return (
|
|
303
468
|
<>
|
|
304
|
-
<div className={cn({ field: true, "field-email": true
|
|
469
|
+
<div className={cn({ field: true, "flex": true, "field-email": true,
|
|
470
|
+
'is-validating': fieldValidationState.status === 'validating',
|
|
471
|
+
'is-valid': fieldValidationState.status === 'valid',
|
|
472
|
+
'is-invalid': hasErrors && fieldValidationState.status === 'invalid',
|
|
473
|
+
})}
|
|
474
|
+
{...(showTooltipErrors && hasErrors && {
|
|
475
|
+
'data-tooltip-id': "tooltipField",
|
|
476
|
+
'data-tooltip-html': errorsHtml
|
|
477
|
+
})}
|
|
478
|
+
>
|
|
305
479
|
<label
|
|
306
480
|
contentEditable={editable}
|
|
307
481
|
className={cn({ help: !!help })}
|
|
@@ -317,27 +491,33 @@ const EmailField = forwardRef(
|
|
|
317
491
|
""
|
|
318
492
|
)}
|
|
319
493
|
</label>
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
494
|
+
<div className={"flex flex-1 flex-no-gap flex-start"} style={{ position: 'relative' }}>
|
|
495
|
+
<input
|
|
496
|
+
ref={inputRef}
|
|
497
|
+
aria-required={required}
|
|
498
|
+
aria-readonly={readOnly}
|
|
499
|
+
readOnly={readOnly}
|
|
500
|
+
type="email"
|
|
501
|
+
placeholder={placeholder}
|
|
502
|
+
id={id}
|
|
503
|
+
name={name}
|
|
504
|
+
value={value || ""}
|
|
505
|
+
onChange={handleChange}
|
|
506
|
+
minLength={minlength}
|
|
507
|
+
maxLength={maxlength}
|
|
508
|
+
/>
|
|
509
|
+
<div className="field-icons-wrapper" style={{position: 'absolute', right: '10px', top: '50%', transform: 'translateY(-50%)', display: 'flex', alignItems: 'center', gap: '5px'}}>
|
|
510
|
+
{renderValidationIcon()}
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
333
513
|
</div>
|
|
334
|
-
{
|
|
514
|
+
{!showTooltipErrors && hasErrors && (
|
|
335
515
|
<ul className="error">
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
516
|
+
{combinedErrors.map((e, key) => (
|
|
517
|
+
<li key={key} aria-live="assertive" role="alert">
|
|
518
|
+
{e}
|
|
519
|
+
</li>
|
|
520
|
+
))}
|
|
341
521
|
</ul>
|
|
342
522
|
)}
|
|
343
523
|
</>
|
|
@@ -365,6 +545,9 @@ const NumberField = forwardRef(
|
|
|
365
545
|
max,
|
|
366
546
|
step,
|
|
367
547
|
unit,
|
|
548
|
+
validation,
|
|
549
|
+
showErrors = true,
|
|
550
|
+
showTooltipErrors = false,
|
|
368
551
|
...rest
|
|
369
552
|
},
|
|
370
553
|
ref,
|
|
@@ -372,6 +555,14 @@ const NumberField = forwardRef(
|
|
|
372
555
|
const id = "numberfield-" + uniqid();
|
|
373
556
|
const [errors, setErrors] = useState([]);
|
|
374
557
|
const inputRef = useRef();
|
|
558
|
+
|
|
559
|
+
const {
|
|
560
|
+
validate: realtimeValidate,
|
|
561
|
+
validationState
|
|
562
|
+
} = useRealtimeValidation(validation?.modelName, validation?.docId);
|
|
563
|
+
const isRealtimeValidationEnabled = !!validation?.modelName;
|
|
564
|
+
const fieldValidationState = validationState[name] || { status: 'idle' };
|
|
565
|
+
|
|
375
566
|
const validate = () => {
|
|
376
567
|
const errs = [];
|
|
377
568
|
if (required && value === undefined) {
|
|
@@ -389,25 +580,62 @@ const NumberField = forwardRef(
|
|
|
389
580
|
if ((max || max === 0) && (value || value === 0) && max < value) {
|
|
390
581
|
errs.push("Value > to " + max);
|
|
391
582
|
}
|
|
392
|
-
|
|
393
|
-
|
|
583
|
+
if (isRealtimeValidationEnabled && fieldValidationState.status === 'invalid') {
|
|
584
|
+
errs.push(fieldValidationState.error);
|
|
585
|
+
}
|
|
586
|
+
if(showErrors) setErrors(errs);
|
|
587
|
+
return !errs.length && fieldValidationState.status !== 'invalid';
|
|
394
588
|
};
|
|
589
|
+
|
|
395
590
|
useEffect(() => {
|
|
396
|
-
if (value !== null) validate();
|
|
591
|
+
if (value !== null && !isRealtimeValidationEnabled) validate();
|
|
397
592
|
}, [value]);
|
|
593
|
+
|
|
594
|
+
const combinedErrors = [...errors];
|
|
595
|
+
if (isRealtimeValidationEnabled && fieldValidationState.status === 'invalid' && fieldValidationState.error && !combinedErrors.includes(fieldValidationState.error)) {
|
|
596
|
+
combinedErrors.push(fieldValidationState.error);
|
|
597
|
+
}
|
|
598
|
+
const hasErrors = combinedErrors.length > 0;
|
|
599
|
+
const errorsHtml = hasErrors ? `<ul>${combinedErrors.map(e => `<li>${String(e).replace(/</g, '<').replace(/>/g, '>')}</li>`).join('')}</ul>` : '';
|
|
600
|
+
|
|
398
601
|
useImperativeHandle(ref, () => ({
|
|
399
602
|
ref: inputRef.current,
|
|
400
603
|
validate,
|
|
401
604
|
getValue: () => value,
|
|
402
605
|
}));
|
|
606
|
+
|
|
403
607
|
const handleChange = (e) => {
|
|
404
608
|
if (onChange) {
|
|
405
609
|
onChange(e);
|
|
406
610
|
}
|
|
611
|
+
if (isRealtimeValidationEnabled) {
|
|
612
|
+
realtimeValidate(name, e.target.value);
|
|
613
|
+
}
|
|
407
614
|
};
|
|
615
|
+
|
|
616
|
+
const renderValidationIcon = () => {
|
|
617
|
+
if (!isRealtimeValidationEnabled) return null;
|
|
618
|
+
const { status, error } = fieldValidationState;
|
|
619
|
+
switch (status) {
|
|
620
|
+
case 'validating': return <FaSpinner className="spin-icon validation-icon validating" title="Validating..." />;
|
|
621
|
+
case 'valid': return <FaCheckCircle className="validation-icon valid" title="Valid" />;
|
|
622
|
+
case 'invalid': return <FaExclamationCircle className="validation-icon invalid" title={error} />;
|
|
623
|
+
default: return null;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
408
627
|
return (
|
|
409
628
|
<>
|
|
410
|
-
<div className={cn({ field: true, "field-number": true
|
|
629
|
+
<div className={cn({ field: true, "field-number": true,
|
|
630
|
+
'is-validating': fieldValidationState.status === 'validating',
|
|
631
|
+
'is-valid': fieldValidationState.status === 'valid',
|
|
632
|
+
'is-invalid': hasErrors && fieldValidationState.status === 'invalid',
|
|
633
|
+
})}
|
|
634
|
+
{...(showTooltipErrors && hasErrors && {
|
|
635
|
+
'data-tooltip-id': "tooltipField",
|
|
636
|
+
'data-tooltip-html': errorsHtml
|
|
637
|
+
})}
|
|
638
|
+
>
|
|
411
639
|
|
|
412
640
|
<div className="flex flex-1">
|
|
413
641
|
{label && (
|
|
@@ -428,36 +656,39 @@ const NumberField = forwardRef(
|
|
|
428
656
|
</label>
|
|
429
657
|
)}
|
|
430
658
|
{help && <div className="flex help">{help}</div>}
|
|
431
|
-
<div className={"flex flex-1 flex-no-wrap flex-mini-gap"}>
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
659
|
+
<div className={"flex flex-1 flex-no-wrap flex-mini-gap flex-end"} style={{ position: 'relative' }}>
|
|
660
|
+
<input
|
|
661
|
+
ref={inputRef}
|
|
662
|
+
aria-required={required}
|
|
663
|
+
aria-readonly={readOnly}
|
|
664
|
+
readOnly={readOnly}
|
|
665
|
+
type="number"
|
|
666
|
+
placeholder={placeholder}
|
|
667
|
+
id={id}
|
|
668
|
+
name={name}
|
|
669
|
+
value={value ?? ""}
|
|
670
|
+
onChange={handleChange}
|
|
671
|
+
minLength={minlength}
|
|
672
|
+
maxLength={maxlength}
|
|
673
|
+
min={min}
|
|
674
|
+
max={max}
|
|
675
|
+
step={step}
|
|
676
|
+
{...rest}
|
|
677
|
+
/>
|
|
678
|
+
<div className="field-icons-wrapper" style={{position: 'absolute', right: unit ? '40px' : '10px', top: '50%', transform: 'translateY(-50%)', display: 'flex', alignItems: 'center', gap: '5px'}}>
|
|
679
|
+
{renderValidationIcon()}
|
|
680
|
+
</div>
|
|
681
|
+
{unit && <span className="unit">{unit}</span>}
|
|
451
682
|
</div>
|
|
452
683
|
</div>
|
|
453
684
|
</div>
|
|
454
|
-
{
|
|
685
|
+
{!showTooltipErrors && hasErrors && (
|
|
455
686
|
<ul className="error">
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
687
|
+
{combinedErrors.map((e, key) => (
|
|
688
|
+
<li key={key} aria-live="assertive" role="alert">
|
|
689
|
+
{e}
|
|
690
|
+
</li>
|
|
691
|
+
))}
|
|
461
692
|
</ul>
|
|
462
693
|
)}
|
|
463
694
|
</>
|
|
@@ -1178,7 +1409,7 @@ export const FilterStringField = ({ field, onChangeFilterValue, filterValues, se
|
|
|
1178
1409
|
|
|
1179
1410
|
if (currentValue === '') {
|
|
1180
1411
|
// No need to call setFilterValues here as it's done immediately in handleChange
|
|
1181
|
-
onChangeFilterValue(field, field.multiple ? [] :
|
|
1412
|
+
onChangeFilterValue(field, field.multiple ? [] : {}, true);
|
|
1182
1413
|
return;
|
|
1183
1414
|
}
|
|
1184
1415
|
|
|
@@ -1363,7 +1594,7 @@ export const FilterField = ({advanced,model, reversed, field, active, onChangeFi
|
|
|
1363
1594
|
}
|
|
1364
1595
|
return <FaPencil />
|
|
1365
1596
|
}
|
|
1366
|
-
return <th key={field.name} className={`form filter-field`} style={{backgroundColor: field.color, color: !field.color ||isLightColor(field.color) ? 'black': "white"}}>
|
|
1597
|
+
return <th key={`filter_${field.name}`} className={`form filter-field`} style={{backgroundColor: field.color, color: !field.color ||isLightColor(field.color) ? 'black': "white"}}>
|
|
1367
1598
|
<div className="flex flex-centered flex-mini-gap flex-row">
|
|
1368
1599
|
<div className="flex flex-1 flex-mini-gap flex-no-wrap">
|
|
1369
1600
|
{renderIconFromType(field)}
|
|
@@ -1424,101 +1655,136 @@ export const PhoneField = ({name, value, onChange}) => {
|
|
|
1424
1655
|
);
|
|
1425
1656
|
}
|
|
1426
1657
|
|
|
1427
|
-
export const ModelField = ({field, disableable=false, showModel=true, value,
|
|
1658
|
+
export const ModelField = ({field, formData, disableable=false, showModel=true, value, fields=false, onChange, ...rest}) => {
|
|
1428
1659
|
const {models} = useModelContext();
|
|
1429
1660
|
const {me} = useAuthContext();
|
|
1430
|
-
const {t} = useTranslation()
|
|
1661
|
+
const {t} = useTranslation();
|
|
1431
1662
|
const [checked, setChecked] = useState(true);
|
|
1432
1663
|
|
|
1433
|
-
//
|
|
1434
|
-
const
|
|
1664
|
+
// --- LOGIQUE AMÉLIORÉE POUR DÉTERMINER LE MODÈLE CIBLE ET LES VALEURS ---
|
|
1665
|
+
const hasTargetModel = !!field?.targetModel;
|
|
1666
|
+
let modelValue, fieldValue, targetModelName;
|
|
1667
|
+
|
|
1668
|
+
if (hasTargetModel) {
|
|
1669
|
+
// Le modèle est déterminé par un autre champ. La valeur de ce champ est juste le nom du champ (string).
|
|
1670
|
+
if (typeof field.targetModel === 'string' && field.targetModel.startsWith('$')) {
|
|
1671
|
+
const dynamicFieldName = field.targetModel.substring(1);
|
|
1672
|
+
targetModelName = formData?.[dynamicFieldName] || null;
|
|
1673
|
+
} else {
|
|
1674
|
+
targetModelName = field.targetModel;
|
|
1675
|
+
}
|
|
1676
|
+
modelValue = targetModelName;
|
|
1677
|
+
fieldValue = value;
|
|
1678
|
+
} else {
|
|
1679
|
+
// La valeur de ce champ contient le modèle et/ou le champ.
|
|
1680
|
+
if (fields) { // fields=true: on sélectionne un modèle ET un champ. La valeur est un objet.
|
|
1681
|
+
targetModelName = value?.model;
|
|
1682
|
+
modelValue = value?.model;
|
|
1683
|
+
fieldValue = value?.field;
|
|
1684
|
+
} else { // fields=false: on sélectionne seulement un modèle. La valeur est une chaîne.
|
|
1685
|
+
targetModelName = value;
|
|
1686
|
+
modelValue = value;
|
|
1687
|
+
fieldValue = undefined;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const selectedModel = models.find(m => m.name === targetModelName); // Note: Removed user check for broader compatibility with system models
|
|
1435
1692
|
|
|
1436
1693
|
// Préparer les options pour les champs du modèle
|
|
1437
1694
|
const fieldOptions = selectedModel?.fields.map(f => ({
|
|
1438
|
-
label: t(`field_${f.name}`, f.name),
|
|
1695
|
+
label: t(`field_${selectedModel.name}_${f.name}`, f.name),
|
|
1439
1696
|
value: f.name
|
|
1440
1697
|
})) || [];
|
|
1441
1698
|
|
|
1442
|
-
//
|
|
1699
|
+
// Effet pour réinitialiser le champ si le modèle cible change et que le champ actuel n'est plus valide
|
|
1700
|
+
useEffect(() => {
|
|
1701
|
+
if (hasTargetModel) {
|
|
1702
|
+
const isValid = fieldOptions.some(opt => opt.value === fieldValue);
|
|
1703
|
+
if (!isValid) {
|
|
1704
|
+
const newValue = fieldOptions.length > 0 ? fieldOptions[0].value : null;
|
|
1705
|
+
if (fieldValue !== newValue) {
|
|
1706
|
+
onChange({ name: field.name, value: newValue });
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}, [targetModelName]); // Se déclenche quand le nom du modèle cible change
|
|
1711
|
+
|
|
1712
|
+
// Gestion du changement de modèle (uniquement si pas de targetModel)
|
|
1443
1713
|
const handleModelChange = (e) => {
|
|
1444
|
-
const
|
|
1445
|
-
const
|
|
1714
|
+
const newModelName = e.value;
|
|
1715
|
+
const newModel = models.find(m => m.name === newModelName);
|
|
1716
|
+
const firstField = newModel?.fields[0]?.name || null;
|
|
1446
1717
|
|
|
1447
1718
|
if (fields) {
|
|
1448
|
-
onChange({name: field?.name, value: { model:
|
|
1719
|
+
onChange({name: field?.name, value: { model: newModelName, field: firstField }});
|
|
1449
1720
|
} else {
|
|
1450
|
-
onChange({name: field?.name, value:
|
|
1721
|
+
onChange({name: field?.name, value: newModelName});
|
|
1451
1722
|
}
|
|
1452
1723
|
};
|
|
1453
1724
|
|
|
1454
1725
|
// Gestion du changement de champ
|
|
1455
1726
|
const handleFieldChange = (e) => {
|
|
1456
|
-
|
|
1727
|
+
const newFieldName = e.value;
|
|
1728
|
+
if (hasTargetModel) {
|
|
1729
|
+
onChange({ name: field.name, value: newFieldName });
|
|
1730
|
+
} else {
|
|
1731
|
+
onChange({name: field?.name, value: { model: modelValue, field: newFieldName }});
|
|
1732
|
+
}
|
|
1457
1733
|
};
|
|
1458
1734
|
|
|
1459
|
-
const dis = disableable ? (
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
/>
|
|
1469
|
-
) : null;
|
|
1735
|
+
const dis = disableable ? (<CheckboxField
|
|
1736
|
+
checked={checked}
|
|
1737
|
+
onChange={e => {
|
|
1738
|
+
setChecked(e);
|
|
1739
|
+
if (!e) {
|
|
1740
|
+
onChange({name: field?.name, value: null});
|
|
1741
|
+
}
|
|
1742
|
+
}}
|
|
1743
|
+
/>) : null;
|
|
1470
1744
|
|
|
1471
1745
|
if (!fields) {
|
|
1472
|
-
return (
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
/>
|
|
1488
|
-
)}
|
|
1489
|
-
</div>
|
|
1490
|
-
);
|
|
1746
|
+
return (<div className="flex flex-1">
|
|
1747
|
+
{dis}
|
|
1748
|
+
{checked && (<SelectField
|
|
1749
|
+
className="flex-1"
|
|
1750
|
+
value={modelValue}
|
|
1751
|
+
onChange={handleModelChange}
|
|
1752
|
+
items={models
|
|
1753
|
+
.filter(m => m._user === me?.username) // Keep user filter for selection
|
|
1754
|
+
.map(m => ({
|
|
1755
|
+
label: t(`model_${m.name}`, m.name),
|
|
1756
|
+
value: m.name
|
|
1757
|
+
}))}
|
|
1758
|
+
{...rest}
|
|
1759
|
+
/>)}
|
|
1760
|
+
</div>);
|
|
1491
1761
|
}
|
|
1492
1762
|
|
|
1493
|
-
return (
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
{
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
</div>
|
|
1519
|
-
)}
|
|
1520
|
-
</div>
|
|
1521
|
-
);
|
|
1763
|
+
return (<div className="flex flex-1">
|
|
1764
|
+
{dis}
|
|
1765
|
+
{checked && (<div className="flex flex-stretch" key={field?.name ?? 'def'}>
|
|
1766
|
+
{showModel && (<SelectField
|
|
1767
|
+
className="flex-1"
|
|
1768
|
+
value={modelValue}
|
|
1769
|
+
onChange={handleModelChange}
|
|
1770
|
+
items={models
|
|
1771
|
+
.filter(m => m._user === me?.username) // Keep user filter for selection
|
|
1772
|
+
.map(m => ({
|
|
1773
|
+
label: t(`model_${m.name}`, m.name),
|
|
1774
|
+
value: m.name
|
|
1775
|
+
}))}
|
|
1776
|
+
disabled={hasTargetModel} // Le sélecteur de modèle est désactivé si le modèle est piloté par un autre champ
|
|
1777
|
+
/>)}
|
|
1778
|
+
<SelectField
|
|
1779
|
+
className="flex-1"
|
|
1780
|
+
value={fieldValue}
|
|
1781
|
+
onChange={handleFieldChange}
|
|
1782
|
+
items={fieldOptions}
|
|
1783
|
+
disabled={!targetModelName}
|
|
1784
|
+
{...rest}
|
|
1785
|
+
/>
|
|
1786
|
+
</div>)}
|
|
1787
|
+
</div>);
|
|
1522
1788
|
};
|
|
1523
1789
|
|
|
1524
1790
|
// Fonction pour obtenir le composant icône par son nom
|
|
@@ -1589,47 +1855,49 @@ export const IconField = ({name, label, value, disabled, onChange, className, ..
|
|
|
1589
1855
|
)}
|
|
1590
1856
|
</div>
|
|
1591
1857
|
};
|
|
1592
|
-
export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
|
|
1593
|
-
// 1. État interne pour une réactivité immédiate de l'interface.
|
|
1594
|
-
const [internalValue, setInternalValue] = useState(value);
|
|
1595
|
-
|
|
1596
|
-
// 2. On mémoïze le gestionnaire d'événements avec debounce pour éviter de le recréer à chaque rendu.
|
|
1597
|
-
const debouncedOnChange = useCallback(
|
|
1598
|
-
debounce((newValue) => {
|
|
1599
|
-
// On notifie le parent du changement après un court délai.
|
|
1600
|
-
onChange?.({ name, value: newValue });
|
|
1601
|
-
}, 200), // Un délai de 200ms est confortable pour un sélecteur de couleur.
|
|
1602
|
-
[onChange, name] // Dépendances de useCallback
|
|
1603
|
-
);
|
|
1604
1858
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1859
|
+
export const ColorField = ({ name, label, value, disabled, onChange, className, ...rest }) => {
|
|
1860
|
+
const [displayColorPicker, setDisplayColorPicker] = useState(false);
|
|
1861
|
+
|
|
1862
|
+
const handleClick = () => {
|
|
1863
|
+
if (!disabled) {
|
|
1864
|
+
setDisplayColorPicker(!displayColorPicker);
|
|
1609
1865
|
}
|
|
1610
|
-
}
|
|
1866
|
+
};
|
|
1611
1867
|
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1868
|
+
const handleClose = () => {
|
|
1869
|
+
setDisplayColorPicker(false);
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
const handleChange = (color) => {
|
|
1873
|
+
// react-color nous donne un objet avec tous les formats.
|
|
1874
|
+
// On utilise tinycolor pour le convertir au format canonique attendu par le backend (#RRGGBBAA).
|
|
1875
|
+
const newColor = tinycolor(color.rgb); // color.rgb contient {r, g, b, a}
|
|
1876
|
+
onChange?.({ name, value: newColor.toHex8String().toUpperCase() });
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
const color = tinycolor(value || '#FFFFFFFF');
|
|
1880
|
+
const swatchStyle = {
|
|
1881
|
+
background: color.toRgbString(),
|
|
1882
|
+
width: '100%',
|
|
1883
|
+
minWidth: '52px',
|
|
1884
|
+
height: '36px',
|
|
1885
|
+
borderRadius: '2px',
|
|
1886
|
+
border: '1px solid #ccc',
|
|
1887
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
1618
1888
|
};
|
|
1619
1889
|
|
|
1620
1890
|
return (
|
|
1621
|
-
<div className={`flex flex-1
|
|
1622
|
-
{label && (<label className="flex-1">{label}</label>)}
|
|
1623
|
-
<div
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
<span className="color-value">{internalValue || '#FFFFFF'}</span>
|
|
1632
|
-
</div>
|
|
1891
|
+
<div className={`flex flex-1 flex-no-wrap ${className || ''}`}>
|
|
1892
|
+
{label && (<label className="flex-1 mb-1">{label}</label>)}
|
|
1893
|
+
<div style={swatchStyle} onClick={handleClick}> </div>
|
|
1894
|
+
<div className={"flex-1"}>{value}</div>
|
|
1895
|
+
{displayColorPicker ? (
|
|
1896
|
+
<div style={{ position: 'absolute', zIndex: '2' }}>
|
|
1897
|
+
<div style={{ position: 'fixed', top: '0px', right: '0px', bottom: '0px', left: '0px' }} onClick={handleClose} />
|
|
1898
|
+
<SketchPicker color={value || '#FFFFFFFF'} onChange={handleChange} />
|
|
1899
|
+
</div>
|
|
1900
|
+
) : null}
|
|
1633
1901
|
</div>
|
|
1634
1902
|
);
|
|
1635
1903
|
};
|
|
@@ -1717,7 +1985,7 @@ export const DurationField = forwardRef(({ value, onChange, name, label, help, r
|
|
|
1717
1985
|
});
|
|
1718
1986
|
DurationField.displayName = "DurationField";
|
|
1719
1987
|
|
|
1720
|
-
export const CodeField = ({name, label, language, value, disabled, onChange}) => {
|
|
1988
|
+
export const CodeField = ({name, label, language, defaultValue, value, disabled, onChange}) => {
|
|
1721
1989
|
const u = name || uniqid();
|
|
1722
1990
|
const [currentEditor, setEditor] = useState(null);
|
|
1723
1991
|
|
|
@@ -1735,12 +2003,13 @@ export const CodeField = ({name, label, language, value, disabled, onChange}) =>
|
|
|
1735
2003
|
onChange({name, value: code});
|
|
1736
2004
|
} catch (e) {
|
|
1737
2005
|
}
|
|
1738
|
-
} else
|
|
2006
|
+
} else {
|
|
1739
2007
|
onChange({name, value: e});
|
|
2008
|
+
}
|
|
1740
2009
|
}}
|
|
1741
2010
|
height="300px"
|
|
1742
2011
|
/></div> : <div className="code"><SyntaxHighlighter
|
|
1743
|
-
language={language || "javascript"}
|
|
2012
|
+
language={language || "javascript"}>{value}</SyntaxHighlighter></div>
|
|
1744
2013
|
}</>
|
|
1745
2014
|
}
|
|
1746
2015
|
|