data-primals-engine 1.4.3 → 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 (50) hide show
  1. package/README.md +878 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/App.jsx +1 -1
  5. package/client/src/App.scss +13 -3
  6. package/client/src/AssistantChat.scss +3 -2
  7. package/client/src/DashboardView.jsx +569 -569
  8. package/client/src/DataEditor.jsx +2 -2
  9. package/client/src/DataLayout.jsx +1 -8
  10. package/client/src/DataTable.jsx +26 -4
  11. package/client/src/Field.jsx +1825 -1788
  12. package/client/src/FlexDataRenderer.jsx +2 -0
  13. package/client/src/FlexTreeUtils.js +1 -1
  14. package/client/src/KPIDialog.jsx +11 -1
  15. package/client/src/ModelCreator.jsx +1 -2
  16. package/client/src/ModelCreatorField.jsx +23 -27
  17. package/client/src/ModelList.jsx +1 -1
  18. package/client/src/constants.js +1 -1
  19. package/client/src/hooks/useTutorials.jsx +62 -65
  20. package/client/src/translations.js +2 -0
  21. package/package.json +2 -1
  22. package/perf/README.md +147 -0
  23. package/perf/artillery-hooks.js +37 -0
  24. package/perf/perf-shot-hardwork.yml +84 -0
  25. package/perf/perf-shot-search.yml +45 -0
  26. package/perf/setup.yml +26 -0
  27. package/server.js +1 -1
  28. package/src/constants.js +2 -27
  29. package/src/core.js +15 -1
  30. package/src/data.js +1 -1
  31. package/src/defaultModels.js +1544 -1540
  32. package/src/email.js +5 -2
  33. package/src/engine.js +5 -3
  34. package/src/filter.js +5 -3
  35. package/src/modules/assistant/assistant.js +3 -1
  36. package/src/modules/bucket.js +12 -15
  37. package/src/modules/data/data.backup.js +11 -8
  38. package/src/modules/data/data.js +6 -3
  39. package/src/modules/data/data.operations.js +3231 -2999
  40. package/src/modules/data/data.routes.js +1821 -1785
  41. package/src/modules/data/data.scheduling.js +2 -1
  42. package/src/modules/data/data.validation.js +4 -1
  43. package/src/modules/file.js +4 -2
  44. package/src/modules/user.js +4 -1
  45. package/src/modules/workflow.js +9 -10
  46. package/src/openai.jobs.js +2 -0
  47. package/src/packs.js +22 -5
  48. package/src/providers.js +22 -7
  49. package/test/data.integration.test.js +1060 -981
  50. package/test/import_export.integration.test.js +1 -1
@@ -1,1789 +1,1826 @@
1
- import React, {
2
- forwardRef, useCallback,
3
- useEffect,
4
- useImperativeHandle,
5
- useRef,
6
- useState,
7
- } from "react";
8
- import uniqid from "uniqid";
9
- import cn from "classnames";
10
- import { recursiveMap, useRefs } from "./Utils.jsx";
11
- import {useTranslation} from "react-i18next";
12
- import { CodeiumEditor } from "@codeium/react-code-editor";
13
-
14
- import {debounce, escapeRegExp, isGUID, isLightColor} from "../../src/core.js";
15
- import {mainFieldsTypes, maxFileSize} from "../../src/constants.js";
16
- import {useModelContext} from "./contexts/ModelContext.jsx";
17
- import {useQueryClient} from "react-query";
18
- import {
19
- FaArrowDown,
20
- FaArrowUp, FaAt, FaEye, FaEyeSlash,
21
- FaCalendar, FaCalendarWeek, FaCode, FaFile,
22
- FaHashtag, FaIcons,
23
- FaImage,
24
- FaLink, FaListOl, FaListUl,
25
- FaLock, FaMinus,
26
- FaPallet, FaPhone, FaSitemap,
27
- FaToggleOn
28
- } from "react-icons/fa";
29
- import * as Fa6Icons from 'react-icons/fa6'; // Importer Fa6
30
- import {FaCalendarDays, FaCodeCompare, FaPencil, FaT, FaTableColumns} from "react-icons/fa6";
31
- import { CodeBlock, tomorrowNightBright } from 'react-code-blocks';
32
- import SyntaxHighlighter from 'react-syntax-highlighter';
33
- import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
34
-
35
- import { PhoneInput } from 'react-international-phone';
36
- import 'react-international-phone/style.css';
37
- import {useAuthContext} from "./contexts/AuthContext.jsx";
38
- import Switch from "react-switch";
39
- export const Form = ({
40
- name,
41
- onValidate,
42
- onError,
43
- children,
44
- editable,
45
- className = "",
46
- }) => {
47
- const [childrenRef, registerRef] = useRefs();
48
- const onSubmit = (e) => {
49
- e.preventDefault();
50
- let res = true;
51
- Object.keys(childrenRef.current).forEach((item) => {
52
- res = childrenRef.current[item].validate() && res;
53
- });
54
- if (res) {
55
- if (onValidate) onValidate(e);
56
- } else {
57
- if (onError) onError();
58
- }
59
- };
60
-
61
- /**/
62
- return (
63
- <form
64
- name={name}
65
- noValidate={true}
66
- contentEditable={editable}
67
- className={cn({ ["form-" + name]: true }) + " " + (className || "")}
68
- onSubmit={onSubmit}
69
- >
70
- {recursiveMap(children, (child, index) => {
71
- /*if( child?.type?.displayName?.match(/(Field|RadioGroup)/)){
72
- return <child.type {...child.props} ref={registerRef(child?.type?.displayName.concat('-').concat(child.props.name || uniqid()))} />
73
- }*/
74
- return child;
75
- })}
76
- </form>
77
- );
78
- };
79
-
80
- const TextField = forwardRef(function TextField(
81
- {
82
- name,
83
- label,
84
- placeholder,
85
- help,
86
- editable,
87
- value,
88
- required,
89
- readOnly,
90
- onChange,
91
- multiline,
92
- minlength,
93
- maxlength,
94
- searchable,
95
- labelProps,
96
- showErrors=false,
97
- before,
98
- after,
99
- ...rest
100
- },
101
- ref,
102
- ) {
103
- const [id, setId] = useState("textfield-" + uniqid());
104
- const [isPasswordVisible, setIsPasswordVisible] = useState(false);
105
- const [errors, setErrors] = useState([]);
106
- const inputRef = useRef();
107
-
108
- const { type, ...otherRest } = rest;
109
- const isPasswordField = type === 'password';
110
-
111
- const mult = typeof multiline !== 'undefined' ? multiline : maxlength > 255;
112
- const validate = () => {
113
- const errs = [];
114
- if (required && (!value || value.trim() === "")) {
115
- errs.push("Field required");
116
- }
117
- if (
118
- minlength > 0 &&
119
- typeof value == "string" &&
120
- value.trim().length < minlength
121
- ) {
122
- errs.push("Value length must be >= to " + minlength);
123
- }
124
- if (
125
- maxlength !== undefined && maxlength > 0 &&
126
- typeof value == "string" &&
127
- value.trim().length > maxlength
128
- ) {
129
- errs.push("Value length must be <= to " + maxlength);
130
- }
131
- if( showErrors )
132
- setErrors(errs);
133
- return !errs.length;
134
- };
135
- useImperativeHandle(ref, () => ({
136
- ref: inputRef.current,
137
- validate,
138
- getValue: () => value,
139
- }));
140
- const handleChange = (e) => {
141
- if (onChange) {
142
- onChange(e);
143
- }
144
- };
145
-
146
- const togglePasswordVisibility = () => {
147
- setIsPasswordVisible(prevState => !prevState);
148
- };
149
- useEffect(() => {
150
- if (value !== null) validate();
151
- }, [value]);
152
- return (
153
- <>
154
- <div
155
- className={cn({
156
- field: true,
157
- flex: true,
158
- "field-text": !mult,
159
- "field-multiline": mult,
160
- })}
161
- >
162
- {label && (
163
- <label
164
- contentEditable={editable}
165
- className={cn({help: !!help, 'flex-1': true})}
166
- htmlFor={id}
167
- {...labelProps}
168
- >
169
- {label}
170
- {required ? (
171
- <span className="mandatory" contentEditable={false}>
172
- *
173
- </span>
174
- ) : (
175
- ""
176
- )}
177
- </label>
178
- )}
179
-
180
- {help &&<div className="flex help">{help}</div>}
181
-
182
- {mult && (
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
- )}
199
-
200
- {before}
201
- <div className={"flex flex-1 flex-no-gap flex-start"} style={{ position: 'relative' }}>
202
- {!mult && (
203
- <input
204
- ref={inputRef}
205
- aria-required={required}
206
- aria-readonly={readOnly}
207
- readOnly={readOnly}
208
- type={isPasswordField ? (isPasswordVisible ? 'text' : 'password') : (searchable ? "search" : (type || "text"))}
209
- placeholder={placeholder}
210
- title={placeholder}
211
- alt={placeholder}
212
- id={id}
213
- name={name}
214
- value={value || ""}
215
- onChange={handleChange}
216
- minLength={minlength}
217
- maxLength={maxlength}
218
- required={required}
219
- style={isPasswordField ? { paddingRight: '40px' } : {}}
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>
227
- )}
228
- {after}
229
- </div>
230
- </div>
231
- {errors.length > 0 && (
232
- <ul className="error">
233
- {errors.map((e, key) => (
234
- <li key={key} aria-live="assertive" role="alert">
235
- {e}
236
- </li>
237
- ))}
238
- </ul>
239
- )}
240
- </>
241
- );
242
- });
243
-
244
- TextField.displayName = "TextField";
245
- export { TextField };
246
-
247
- const EmailField = forwardRef(
248
- (
249
- {
250
- name,
251
- label,
252
- placeholder,
253
- help,
254
- editable,
255
- defaultValue,
256
- required,
257
- readOnly,
258
- onChange,
259
- minlength,
260
- maxlength,
261
- fieldValidated,
262
- },
263
- ref,
264
- ) => {
265
- const id = "emailfield-" + uniqid();
266
- const [errors, setErrors] = useState([]);
267
- const [value, setValue] = useState(defaultValue || null);
268
- const validate = () => {
269
- const errs = [];
270
- if (required && (!value || value.trim() === "")) {
271
- errs.push("Field required");
272
- }
273
- if (minlength !== undefined && minlength > 0 && value && value.trim().length < minlength) {
274
- errs.push("Value length must be >= to " + minlength);
275
- }
276
- if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
277
- errs.push("Value length must be <= to " + maxlength);
278
- }
279
-
280
- if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
281
- errs.push("Invalid email");
282
- }
283
- setErrors(errs);
284
- return !errs.length;
285
- };
286
- useEffect(() => {
287
- if (value !== null) validate();
288
- }, [value]);
289
- useEffect(() => {
290
- if (fieldValidated) validate();
291
- }, [fieldValidated]);
292
- useImperativeHandle(ref, () => ({
293
- validate,
294
- getValue: () => value,
295
- }));
296
- const handleChange = (e) => {
297
- setValue(e.target.value);
298
- if (onChange) {
299
- onChange(e);
300
- }
301
- };
302
- return (
303
- <>
304
- <div className={cn({ field: true, "field-email": true })}>
305
- <label
306
- contentEditable={editable}
307
- className={cn({ help: !!help })}
308
- title={help}
309
- htmlFor={id}
310
- >
311
- {label}
312
- {required ? (
313
- <span className="mandatory" contentEditable={false}>
314
- *
315
- </span>
316
- ) : (
317
- ""
318
- )}
319
- </label>
320
- <input
321
- aria-required={required}
322
- aria-readonly={readOnly}
323
- readOnly={readOnly}
324
- type="email"
325
- placeholder={placeholder}
326
- id={id}
327
- name={name}
328
- value={value || ""}
329
- onChange={handleChange}
330
- minLength={minlength}
331
- maxLength={maxlength}
332
- />
333
- </div>
334
- {errors.length > 0 && (
335
- <ul className="error">
336
- {errors.map((e, key) => (
337
- <li key={key} aria-live="assertive" role="alert">
338
- {e}
339
- </li>
340
- ))}
341
- </ul>
342
- )}
343
- </>
344
- );
345
- },
346
- );
347
- EmailField.displayName = "EmailField";
348
- export { EmailField };
349
-
350
- const NumberField = forwardRef(
351
- (
352
- {
353
- name,
354
- label,
355
- placeholder,
356
- help,
357
- editable,
358
- value,
359
- required,
360
- readOnly,
361
- onChange,
362
- minlength,
363
- maxlength,
364
- min,
365
- max,
366
- step,
367
- unit,
368
- ...rest
369
- },
370
- ref,
371
- ) => {
372
- const id = "numberfield-" + uniqid();
373
- const [errors, setErrors] = useState([]);
374
- const inputRef = useRef();
375
- const validate = () => {
376
- const errs = [];
377
- if (required && value === undefined) {
378
- errs.push("Field required");
379
- }
380
- if (minlength !== undefined && minlength > 0 && value && value.trim().length < minlength) {
381
- errs.push("Value length must be >= to " + minlength);
382
- }
383
- if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
384
- errs.push("Value length must be <= to " + maxlength);
385
- }
386
- if ((min || min === 0) && (value || value === 0) && min > value) {
387
- errs.push("Value < to " + min);
388
- }
389
- if ((max || max === 0) && (value || value === 0) && max < value) {
390
- errs.push("Value > to " + max);
391
- }
392
- setErrors(errs);
393
- return !errs.length;
394
- };
395
- useEffect(() => {
396
- if (value !== null) validate();
397
- }, [value]);
398
- useImperativeHandle(ref, () => ({
399
- ref: inputRef.current,
400
- validate,
401
- getValue: () => value,
402
- }));
403
- const handleChange = (e) => {
404
- if (onChange) {
405
- onChange(e);
406
- }
407
- };
408
- return (
409
- <>
410
- <div className={cn({ field: true, "field-number": true })}>
411
-
412
- <div className="flex flex-1">
413
- {label && (
414
- <label
415
- contentEditable={editable}
416
- className={cn({ help: !!help, flex: true, 'flex-1': true })}
417
- title={help}
418
- htmlFor={id}
419
- >
420
- {label}
421
- {required ? (
422
- <span className="mandatory" contentEditable={false}>
423
- *
424
- </span>
425
- ) : (
426
- ""
427
- )}
428
- </label>
429
- )}
430
- {help && <div className="flex help">{help}</div>}
431
- <div className={"flex flex-1 flex-no-wrap flex-mini-gap"}>
432
- <input
433
- ref={inputRef}
434
- aria-required={required}
435
- aria-readonly={readOnly}
436
- readOnly={readOnly}
437
- type="number"
438
- placeholder={placeholder}
439
- id={id}
440
- name={name}
441
- value={value || ""}
442
- onChange={handleChange}
443
- minLength={minlength}
444
- maxLength={maxlength}
445
- min={min}
446
- max={max}
447
- step={step}
448
- {...rest}
449
- />
450
- {unit && <span className="unit">{unit}</span>}
451
- </div>
452
- </div>
453
- </div>
454
- {errors.length > 0 && (
455
- <ul className="error">
456
- {errors.map((e, key) => (
457
- <li key={key} aria-live="assertive" role="alert">
458
- {e}
459
- </li>
460
- ))}
461
- </ul>
462
- )}
463
- </>
464
- );
465
- },
466
- );
467
- NumberField.displayName = "NumberField";
468
- export { NumberField };
469
-
470
- const CheckboxField = forwardRef(
471
- (
472
- {
473
- name,
474
- label,
475
- placeholder,
476
- help,
477
- editable,
478
- defaultValue,
479
- required,
480
- readOnly,
481
- onChange,
482
- minlength,
483
- maxlength,
484
- checked,
485
- checkbox=false,
486
- ...rest
487
- },
488
- ref,
489
- ) => {
490
- const id = "checkfield-" + uniqid();
491
- const [errors, setErrors] = useState([]);
492
- const [value, setValue] = useState(checked || false);
493
- useEffect(() => {
494
- setValue(checked);
495
- }, [checked]);
496
- const validate = () => {
497
- const errs = [];
498
- if (required && !value) {
499
- errs.push("Field must be checked.");
500
- }
501
- setErrors(errs);
502
- return !errs.length;
503
- };
504
- useEffect(() => {
505
- if (value !== null) validate();
506
- }, [value]);
507
- useImperativeHandle(ref, () => ({
508
- validate,
509
- getValue: () => value,
510
- }));
511
- const handleChange = (e) => {
512
- setValue(!value);
513
- onChange?.(e);
514
- };
515
- return (
516
- <>
517
- <div className={cn({field: true, "field-checkbox": true,"field-bg": true})}>
518
- {label && (
519
- <label
520
- contentEditable={editable}
521
- title={help}
522
- htmlFor={id}
523
- >
524
- {label}
525
- {required ? (
526
- <span className="mandatory" contentEditable={false}>
527
- *
528
- </span>
529
- ) : (
530
- ""
531
- )}
532
- </label>
533
- )}
534
- {help && <div className="flex help">{help}</div>}
535
- {!checkbox && (<Switch
536
- id={id}
537
- onChange={handleChange}
538
- checked={value} />)}
539
- {checkbox && (
540
- <input type={"checkbox"} id={id} onChange={handleChange} checked={value} />
541
- )}
542
- </div>
543
- {errors.length > 0 && (
544
- <ul className="error">
545
- {errors.map((e, key) => (
546
- <li key={key} aria-live="assertive" role="alert">
547
- {JSON.stringify(e, null, 2)}
548
- </li>
549
- ))}
550
- </ul>
551
- )}
552
- </>
553
- );
554
- },
555
- );
556
- CheckboxField.displayName = "CheckboxField";
557
- export { CheckboxField };
558
-
559
- const RadioField = forwardRef(
560
- (
561
- {
562
- name,
563
- label,
564
- placeholder,
565
- help,
566
- editable,
567
- checked,
568
- required,
569
- readOnly,
570
- onChange,
571
- minlength,
572
- maxlength,
573
- },
574
- ref,
575
- ) => {
576
- const id = "radiofield-" + uniqid();
577
- const [errors, setErrors] = useState([]);
578
- const [value, setValue] = useState(checked || null);
579
- const validate = () => {
580
- const errs = [];
581
- if (required && !value) {
582
- errs.push("Field must be checked.");
583
- }
584
- setErrors(errs);
585
- return !errs.length;
586
- };
587
- useEffect(() => {
588
- if (value !== null) validate();
589
- }, [value]);
590
- useImperativeHandle(ref, () => ({
591
- validate,
592
- getValue: () => value,
593
- }));
594
- const handleChange = (e) => {
595
- setValue(!value);
596
- if (onChange) {
597
- onChange(e);
598
- }
599
- };
600
- return (
601
- <>
602
- <div className={cn({ field: true, "field-radio": true })}>
603
- <input
604
- aria-required={required}
605
- aria-readonly={readOnly}
606
- readOnly={readOnly}
607
- type="radio"
608
- checked={value}
609
- value={value || label}
610
- placeholder={placeholder}
611
- id={id}
612
- name={name}
613
- onChange={handleChange}
614
- minLength={minlength}
615
- maxLength={maxlength}
616
- />
617
- <label
618
- contentEditable={editable}
619
- className={cn({ help: !!help })}
620
- title={help}
621
- htmlFor={id}
622
- >
623
- {label}
624
- {required ? (
625
- <span className="mandatory" contentEditable={false}>
626
- *
627
- </span>
628
- ) : (
629
- ""
630
- )}
631
- </label>
632
- </div>
633
- {errors.length > 0 && (
634
- <ul className="error">
635
- {errors.map((e, key) => (
636
- <li key={key} aria-live="assertive" role="alert">
637
- {e}
638
- </li>
639
- ))}
640
- </ul>
641
- )}
642
- </>
643
- );
644
- },
645
- );
646
- RadioField.displayName = "RadioField";
647
- export { RadioField };
648
-
649
- const SelectField = forwardRef(
650
- (
651
- {
652
- name,
653
- value,
654
- items,
655
- label,
656
- placeholder,
657
- disabled,
658
- help,
659
- editable,
660
- checked,
661
- required,
662
- readOnly,
663
- onChange,
664
- minlength,
665
- maxlength,
666
- multiple,
667
- ...rest
668
- },
669
- ref,
670
- ) => {
671
- const [values, setValues] = useState([]);
672
- const id = "selectfield-" + uniqid();
673
- const [errors, setErrors] = useState([]);
674
- const [_value, setValue] = useState(value);
675
- useEffect(() => {
676
- if( value === undefined && required && items[0]){
677
- setValue(items[0].value);
678
- }else {
679
- setValue(value);
680
- setValues(value)
681
- }
682
- if (!multiple && value) {
683
- //const index = items.findIndex((i) => i.value === value);
684
- //onChange({name, value:items[index]}, index);
685
- }
686
- }, [value]);
687
- const validate = () => {
688
- const errs = [];
689
- if (required && _value === undefined) {
690
- errs.push("Field is required.");
691
- }
692
- setErrors(errs);
693
- return !errs.length;
694
- };
695
- useEffect(() => {
696
- if (_value !== null) validate();
697
- }, [_value]);
698
- useImperativeHandle(ref, () => ({
699
- validate,
700
- getValue: () => _value,
701
- setValue,
702
- }));
703
- const handleChange = (e) => {
704
- setValue(e.target.value);
705
- if (onChange) {
706
- let options = e.target.options;
707
- let value = [];
708
- for (var i = 0, l = options.length; i < l; i++) {
709
- if (options[i].selected) {
710
- value.push(options[i].value);
711
- }
712
- }
713
- if( multiple ) {
714
- setValues(value);
715
- onChange(value);
716
- }else {
717
- const index = items.findIndex((i) => i.value+'' === e.target.value);
718
- onChange(items[index], index);
719
- }
720
- }
721
- };
722
- return (
723
- <>
724
- <div className={cn({ field: true, 'flex-1': true, flex: true, "field-select": true })}>
725
- {label && (
726
- <label
727
- contentEditable={editable}
728
- className={cn({ help: !!help, 'flex-1': true })}
729
- title={help}
730
- htmlFor={id}
731
- >
732
- {label}
733
- {required ? (
734
- <span className="mandatory" contentEditable={false}>
735
- *
736
- </span>
737
- ) : (
738
- ""
739
- )}
740
- </label>
741
- )}
742
- <select
743
- aria-required={required}
744
- aria-readonly={readOnly}
745
- value={(_value)}
746
- id={id}
747
- name={name}
748
- onChange={handleChange}
749
- multiple={multiple}
750
- disabled={disabled}
751
- className={"flex-1"}
752
- {...rest}
753
- >
754
- {items.map((i) => (
755
- <option value={i.value}>{i.label}</option>
756
- ))}
757
- </select>
758
- </div>
759
- {help && <div className="flex help">{help}</div>}
760
- {errors.length > 0 && (
761
- <ul className="error">
762
- {errors.map((e, key) => (
763
- <li key={key} aria-live="assertive" role="alert">
764
- {e}
765
- </li>
766
- ))}
767
- </ul>
768
- )}
769
- </>
770
- );
771
- },
772
- );
773
- SelectField.displayName = "SelectField";
774
- export { SelectField };
775
-
776
- const RadioGroup = forwardRef(
777
- ({ id, label, help, editable, name, required, children }, ref) => {
778
- const [childrenRef, registerRef] = useRefs();
779
- const [errors, setErrors] = useState([]);
780
- const validate = () => {
781
- const errs = [];
782
- let res = false;
783
- Object.keys(childrenRef.current).forEach((item) => {
784
- res = !!childrenRef.current[item].getValue() || res;
785
- });
786
- if (!res && required) {
787
- errs.push("Field is required");
788
- }
789
- setErrors(errs);
790
- return !errs.length;
791
- };
792
- useImperativeHandle(ref, () => ({
793
- validate,
794
- }));
795
- const handleChange = () => {
796
- setTimeout(() => validate(), 0);
797
- };
798
- return (
799
- <>
800
- <label
801
- contentEditable={editable}
802
- className={cn({ help: !!help })}
803
- title={help}
804
- htmlFor={children[0].props.id}
805
- >
806
- {label}
807
- {required ? (
808
- <span className="mandatory" contentEditable={false}>
809
- *
810
- </span>
811
- ) : (
812
- ""
813
- )}
814
- </label>
815
- {[
816
- recursiveMap(children, (child, index) => {
817
- if (child.type.displayName === "RadioField") {
818
- const props = {
819
- ...child.props,
820
- name: name ? name : child.props.name,
821
- onChange: () => handleChange(child.props.onChange),
822
- };
823
- return (
824
- <child.type
825
- {...props}
826
- ref={registerRef("Radio" + index)}
827
- name={child.props.name || "btn" + id}
828
- />
829
- );
830
- }
831
- return child;
832
- }),
833
- errors.length > 0 ? (
834
- <ul className="error">
835
- {errors.map((e, key) => (
836
- <li key={key} aria-live="assertive" role="alert">
837
- {e}
838
- </li>
839
- ))}
840
- </ul>
841
- ) : (
842
- <></>
843
- ),
844
- ]}
845
- </>
846
- );
847
- },
848
- );
849
-
850
- RadioGroup.displayName = "RadioGroup";
851
- export { RadioGroup };
852
-
853
- // New FileField component
854
- const FileField = ({ inputProps, value, onChange, name, mimeTypes, maxSize, multiple}) => {
855
- const [fileInfos, setFileInfos] = useState(value);
856
- const { t } = useTranslation();
857
-
858
- const handleFileChange = (e) => {
859
- const selectedFiles = Array.from(e.target.files);
860
- const newFileInfos = [];
861
-
862
- const promises = selectedFiles.map(selectedFile => {
863
- if (selectedFile && selectedFile.size > (maxSize || maxFileSize)) {
864
- alert(`Le fichier est trop volumineux. La taille maximale autorisée est de ${(maxSize || maxFileSize) / (1024 * 1024)} Mo.`);
865
- e.target.value = '';
866
- return Promise.resolve();
867
- }
868
- return new Promise((resolve) => {
869
- const reader = new FileReader();
870
- reader.onloadend = () => {
871
- newFileInfos.push({
872
- preview: reader.result,
873
- newFile: true,
874
- file: selectedFile,
875
- name: selectedFile.name
876
- });
877
- resolve();
878
- };
879
- reader.readAsDataURL(selectedFile);
880
- });
881
- });
882
-
883
- Promise.all(promises).then(() => {
884
- if(!multiple){
885
- setFileInfos(newFileInfos);
886
- }else{
887
- setFileInfos(fileInfos => [...fileInfos, ...newFileInfos]);
888
- }
889
- onChange([...fileInfos.map(m => ({...m, newFile: false})), ...newFileInfos]);
890
- });
891
- };
892
-
893
- const handleRemove = (e, index) => {
894
- e.preventDefault();
895
- const newFileInfos = fileInfos.filter((_, i) => i !== index);
896
- setFileInfos(newFileInfos);
897
- onChange(newFileInfos.map(m => ({...m, newFile: false})));
898
- };
899
-
900
- useEffect(() => {
901
- if( value == null || (Array.isArray(value) && value.length === 0))
902
- setFileInfos([])
903
- else{
904
- const v = Array.isArray(value) ? value : [value];
905
- setFileInfos(v)
906
- }
907
- }, [value]);
908
-
909
- return (
910
- <div className="field field-file">
911
- <input
912
- id={"field-file-" + name}
913
- type="file"
914
- data-field={name}
915
- accept={mimeTypes ? mimeTypes.join(',') : '*'}
916
- onChange={handleFileChange}
917
- multiple={multiple} // Add multiple attribute
918
- />
919
- {fileInfos?.length > 0 && (
920
- <div>
921
- {fileInfos.filter(f => isGUID(f.guid) || f.preview).map((fileInfo, index) => (
922
- <div key={index}>
923
- {fileInfo.preview ? (
924
- <a href={fileInfo.preview} target="_blank" rel="noopener noreferrer">
925
- <img src={fileInfo.preview} alt={"Preview"} width='200' height='200' />
926
- </a>
927
- ) :(isGUID(fileInfo.guid) ? (
928
- <a href={"/resources/"+fileInfo.guid} target="_blank" rel="noopener noreferrer">
929
- <img src={"/resources/"+fileInfo.guid} alt={"Preview"} width='200' height='200' />
930
- </a>
931
- ) :(
932
- <img src={fileInfo.preview} alt="Preview" style={{ maxWidth: '200px', maxHeight: '200px' }} />
933
- ))}
934
- <button onClick={(e) => handleRemove(e, index)}><FaMinus /></button>
935
- </div>
936
- ))}
937
- </div>
938
- )}
939
- </div>
940
- );
941
- };
942
-
943
- export { FileField };
944
-
945
- export const FilterNumberField = ({ model, field, onChangeFilterValue, filterValues, setFilterValues }) => {
946
- const { t } = useTranslation();
947
- const { models, setPage, dataByModel } = useModelContext(); // dataByModel is not used, consider removing
948
- const [min, setMin] = useState(null);
949
- const [max, setMax] = useState(null);
950
-
951
- // Debounced version of the function that actually calls onChangeFilterValue
952
- const debouncedApplyFilter = useCallback(
953
- debounce((currentMin, currentMax) => {
954
- const conditions = [];
955
- setPage(1);
956
- if (currentMin !== null && !isNaN(currentMin)) {
957
- conditions.push({ $gte: ['$' + field.name, parseFloat(currentMin)] });
958
- }
959
- if (currentMax !== null && !isNaN(currentMax)) {
960
- conditions.push({ $lte: ['$' + field.name, parseFloat(currentMax)] });
961
- }
962
-
963
- if (conditions.length > 0) {
964
- onChangeFilterValue(field, { $and: conditions });
965
- } else {
966
- onChangeFilterValue(field, {}); // Clear filter if both are invalid/null
967
- }
968
- }, 300), // Adjust delay as needed
969
- [field, onChangeFilterValue] // Dependencies for useCallback
970
- );
971
-
972
- useEffect(() => {
973
- // This effect is to reset local min/max if the global filterValues are cleared externally
974
- // It should not call debouncedApplyFilter directly if filterValues is the source of truth
975
- // for the parent component.
976
- if (filterValues && typeof filterValues[field.name] === 'object') {
977
- const andConditions = filterValues[field.name]?.$and;
978
- if (andConditions && Array.isArray(andConditions)) {
979
- const gteCondition = andConditions.find(cond => cond.$gte);
980
- const lteCondition = andConditions.find(cond => cond.$lte);
981
- setMin(gteCondition ? gteCondition.$gte[1] : null);
982
- setMax(lteCondition ? lteCondition.$lte[1] : null);
983
- } else {
984
- // If the structure is not $and or it's cleared
985
- setMin(null);
986
- setMax(null);
987
- }
988
- } else if (!filterValues || filterValues[field.name] === undefined || Object.keys(filterValues[field.name] || {}).length === 0) {
989
- // If filterValues for this field is cleared or doesn't exist
990
- setMin(null);
991
- setMax(null);
992
- }
993
- }, [filterValues, field.name]);
994
-
995
-
996
- const handleMinChange = (e) => {
997
- const inputValue = e.target.value;
998
- if (inputValue === "") {
999
- setMin(null);
1000
- debouncedApplyFilter(null, max);
1001
- } else {
1002
- const pi = parseFloat(inputValue); // Use parseFloat for potentially decimal numbers
1003
- if (!isNaN(pi)) {
1004
- setMin(pi);
1005
- debouncedApplyFilter(pi, max);
1006
- } else {
1007
- setMin(inputValue); // Keep invalid input in state to show user, but don't filter
1008
- // Or setMin(null) if you want to clear on invalid
1009
- // Potentially call debouncedApplyFilter(null, max) if invalid min means no min filter
1010
- }
1011
- }
1012
- gtag('event', 'search (number,min)');
1013
- };
1014
-
1015
- const handleMaxChange = (e) => {
1016
- const inputValue = e.target.value;
1017
- if (inputValue === "") {
1018
- setMax(null);
1019
- debouncedApplyFilter(min, null);
1020
- } else {
1021
- const pi = parseFloat(inputValue);
1022
- if (!isNaN(pi)) {
1023
- setMax(pi);
1024
- debouncedApplyFilter(min, pi);
1025
- } else {
1026
- setMax(inputValue);
1027
- // Potentially call debouncedApplyFilter(min, null) if invalid max means no max filter
1028
- }
1029
- }
1030
- gtag('event', 'search (number,max)');
1031
- };
1032
-
1033
- return (
1034
- <>
1035
- <NumberField
1036
- value={min === null ? '' : min} // Handle null for empty display
1037
- label="Min:"
1038
- onChange={handleMinChange}
1039
- type="number" // Ensure type is number for appropriate input behavior
1040
- />
1041
- <NumberField
1042
- value={max === null ? '' : max} // Handle null for empty display
1043
- label="Max:"
1044
- onChange={handleMaxChange}
1045
- type="number" // Ensure type is number
1046
- />
1047
- </>
1048
- );
1049
- };
1050
-
1051
- export const FilterEnumField = ({model, field, onChangeFilterValue, filterValues, setFilterValues}) => {
1052
- const {t} = useTranslation();
1053
- const debounced = debounce((field,filter) => onChangeFilterValue(field, { $find: filter }));
1054
- const { models, setPage, elementsPerPage,pagedFilters, pagedSort, page } = useModelContext();
1055
-
1056
- const [val, setVal] = useState(null);
1057
- const queryClient= useQueryClient()
1058
-
1059
- useEffect(() => {
1060
- if (Object.keys(filterValues).length === 0){
1061
- onChangeFilterValue(field, { });
1062
- setVal('');
1063
- }
1064
- }, [filterValues]);
1065
-
1066
- return <div className={"flex flex-no-gap flex-no-wrap"}><SelectField value={val} className={"flex-1"} items={['', ...(field.items || [])].map(m => ({label: t(m), value: m}))} onChange={(e) => {
1067
-
1068
- setPage(1);
1069
-
1070
- if( !e || e.value === '') {
1071
- setVal('');
1072
- onChangeFilterValue(field, undefined);
1073
- }
1074
- else {
1075
- onChangeFilterValue(field, {$eq: ['$' + field.name, e.value]});
1076
- setVal(e.value);
1077
- }
1078
-
1079
- gtag('event', 'search (enum)');
1080
- queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1081
- }} /><button onClick={() => {
1082
- onChangeFilterValue(field, { });
1083
- setVal('');
1084
- }}>x</button></div>
1085
- }
1086
- export const FilterBooleanField = ({model, field, filterValues, onChangeFilterValue }) => {
1087
- const {t} = useTranslation();
1088
- const { setPage, pagedFilters, pagedSort, page,elementsPerPage } = useModelContext();
1089
-
1090
- useEffect(() => {
1091
- if( Object.keys(filterValues).length === 0 ){
1092
- setVal('null');
1093
- onChangeFilterValue(field, { });
1094
- }
1095
- }, [filterValues]);
1096
- const [val, setVal] = useState(null);
1097
- const queryClient= useQueryClient()
1098
- return <div className={"flex flex-no-gap flex-no-wrap"}><SelectField value={val} className={"flex-1"} items={[
1099
- {label: t(''), value: 'null'},
1100
- {label: t('yes'), value: '1'},
1101
- { label: t('no'), value: '0'}]}
1102
- onChange={(e) => {
1103
- setPage(1);
1104
- if( !e || e.value === 'null') {
1105
- setVal(null);
1106
- onChangeFilterValue(field, { });
1107
- }
1108
- else {
1109
- onChangeFilterValue(field, {$or: [{$eq: ['$' + field.name, e.value === '1']}, {$eq: [{ $type: '$'+field.name }, "missing"]}]});
1110
- setVal(e.value);
1111
- }
1112
-
1113
- gtag('event', 'search (boolean)');
1114
- queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1115
- }} /></div>
1116
- }
1117
- export const FilterDateField = ({model, field, filterValues, onChangeFilterValue }) => {
1118
- const {t} = useTranslation();
1119
-
1120
- const [minDate ,setMinDate] = useState(null);
1121
- const [maxDate ,setMaxDate] = useState(null);
1122
- useEffect(() => {
1123
- if( Object.keys(filterValues).length === 0 ){
1124
- onChangeFilterValue(field, { });
1125
- setMinDate('');
1126
- setMaxDate('');
1127
- }
1128
- }, [filterValues]);
1129
- const onChange = (minDate, maxDate) =>{
1130
- const min = minDate ? { $gte: ['$'+field.name, minDate]} : null;
1131
-
1132
- const fm = new Date(maxDate);
1133
- fm.setDate(fm.getDate() + 1);
1134
-
1135
- const max = maxDate ? {$lte: ['$' + field.name, fm.toISOString()]} : null;
1136
- const and= [];
1137
- if( min) and.push(min);
1138
- if( max) and.push(max);
1139
- if( !min && !max)
1140
- onChangeFilterValue(field, { });
1141
- else
1142
- onChangeFilterValue(field, { $and: and});
1143
- gtag('event', 'search (date)');
1144
- }
1145
- return <div className={"flex flex-no-gap flex-no-wrap"}>
1146
- <label htmlFor={"minDate"+model.name+field.name}>
1147
- Min:
1148
- <input id={"minDate"+model.name+field.name} type={"datetime-local"} value={minDate} onChange={e => {
1149
- setMinDate(e.target.value);
1150
- onChange?.(e.target.value, maxDate);
1151
- }} />
1152
- </label>
1153
- <label htmlFor={"maxDate"+model.name+field.name}>
1154
- Max:
1155
- <input id={"maxDate"+model.name+field.name} type={"datetime-local"} value={maxDate} onChange={e => {
1156
- setMaxDate(e.target.value);
1157
- onChange?.(minDate, e.target.value);
1158
- }} />
1159
- </label>
1160
- </div>
1161
- }
1162
- export const FilterStringField = ({ field, onChangeFilterValue, filterValues, setFilterValues }) => {
1163
- const { models, setPage } = useModelContext();
1164
- const [isRegex, setIsRegex] = useState(false);
1165
- const { t } = useTranslation();
1166
-
1167
-
1168
- useEffect(() => {
1169
- if( Object.keys(filterValues).length === 0 ){
1170
- onChangeFilterValue(field, { });
1171
- }
1172
- }, [filterValues]);
1173
-
1174
- // Debounced function to apply the filter
1175
- const debouncedApplyFilter = useCallback(
1176
- debounce((currentValue, currentIsRegex) => {
1177
- setPage(1); // Reset page to 1 when filter changes
1178
-
1179
- if (currentValue === '') {
1180
- // No need to call setFilterValues here as it's done immediately in handleChange
1181
- onChangeFilterValue(field, field.multiple ? [] : undefined, true);
1182
- return;
1183
- }
1184
-
1185
- let filterQuery;
1186
- if (field.type === 'relation') {
1187
- const relationModel = models.find(f => f.name === field.relation);
1188
- if (relationModel) {
1189
- const relationFilters = relationModel.fields
1190
- .filter(f => mainFieldsTypes.includes(f.type))
1191
- .map(mf => ({
1192
- $regexMatch: { input: `$$this.${mf.name}`, regex: currentIsRegex ? currentValue : escapeRegExp(currentValue) }
1193
- }));
1194
- if (relationFilters.length > 0) {
1195
- filterQuery = { [field.name]: {$find: { $and: [{ $or: relationFilters }] }}};
1196
- } else {
1197
- filterQuery = {}; // Or handle as no match if no searchable fields
1198
- }
1199
- } else {
1200
- filterQuery = {}; // Relation model not found
1201
- }
1202
- } else { // Not a relation type
1203
- const regexToUse = currentIsRegex ? currentValue : escapeRegExp(currentValue);
1204
- if (field.type === 'array') {
1205
- filterQuery = {
1206
- $gt: [
1207
- {
1208
- $size: {
1209
- $filter: {
1210
- input: '$' + field.name,
1211
- as: 'item',
1212
- cond: {
1213
- $regexMatch: {
1214
- input: '$$item',
1215
- regex: regexToUse
1216
- }
1217
- }
1218
- }
1219
- }
1220
- },
1221
- 0
1222
- ]
1223
- };
1224
- } else { // Simple string field
1225
- filterQuery = {
1226
- $and: [{
1227
- $regexMatch: {
1228
- input: '$' + field.name,
1229
- regex: regexToUse
1230
- }
1231
- }]
1232
- };
1233
- }
1234
- }
1235
- onChangeFilterValue(field, filterQuery, true);
1236
- gtag('event', 'search (string)');
1237
- }, 1200), // Debounce delay
1238
- [] // Dependencies for useCallback
1239
- );
1240
-
1241
- const handleInputChange = (e) => {
1242
- const newValue = e.target.value;
1243
- // Update the displayed value immediately
1244
- setFilterValues(filter => ({ ...filter, [field.name]: newValue }));
1245
- // Call the debounced function to apply the filter
1246
- debouncedApplyFilter(newValue, isRegex);
1247
- };
1248
-
1249
- const handleToggleRegex = () => {
1250
- const newIsRegex = !isRegex;
1251
- setIsRegex(newIsRegex);
1252
- // Re-apply filter with the new regex state and current value
1253
- // The value from filterValues should be up-to-date
1254
- const currentValue = filterValues[field.name] || '';
1255
- debouncedApplyFilter(currentValue, newIsRegex);
1256
- };
1257
-
1258
- return (
1259
- <>
1260
- <TextField
1261
- type="text"
1262
- name={`filter_${field.name}`}
1263
- value={filterValues[field.name] || ''} // Ensure controlled component with a default empty string
1264
- placeholder={isRegex ? t("filterstringfield.placeholder.regex", "regular expression") : t("filterstringfield.placeholder", "...")}
1265
- onChange={handleInputChange}
1266
- maxLength={1000}
1267
- />
1268
- <button title={"regex"} className={isRegex ? 'active' : ''} onClick={handleToggleRegex}>.*</button>
1269
- </>
1270
- );
1271
- };
1272
- export const FilterField = ({advanced,model, reversed, field, active, onChangeFilterValue, filterValues, setFilterValues}) => {
1273
- const { elementsPerPage, pagedSort, setPagedSort, setPage, page, pagedFilters, lockedColumns, setLockedColumns } = useModelContext();
1274
- const {t} = useTranslation();
1275
- const [locked, setLocked] = useState(lockedColumns.includes(field.name));
1276
- const queryClient = useQueryClient()
1277
-
1278
- useEffect(() => {
1279
- if(!reversed) {
1280
- setFilterValues(filter => ({...filter, [field.name]: ''}));
1281
- onChangeFilterValue(field, '', true);
1282
- }
1283
- }, [field]);
1284
-
1285
- const handleToggleLock = () => {
1286
- if( locked ) {
1287
- if (lockedColumns.includes(field.name))
1288
- setLockedColumns(cols => [...cols].filter(f => f !== field.name));
1289
- }else{
1290
- if (!lockedColumns.includes(field.name))
1291
- setLockedColumns(cols => [...cols, field.name]);
1292
- }
1293
- setLocked(!locked);
1294
- }
1295
-
1296
- const [reset, setReset] = useState(false);
1297
- const handleChangeSort = (up) => {
1298
- setPagedSort(sort => {
1299
- const s = lockedColumns.length > 0 ? {...sort[model.name] || {}} : {};
1300
- if( up ){
1301
- if( reset ){
1302
- delete s[field.name];
1303
- setReset(false);
1304
- }else {
1305
- s[field.name] = 1;
1306
- }
1307
- }else{
1308
- s[field.name] = -1;
1309
- setReset(true);
1310
- }
1311
- return {...sort, [model.name]: s};
1312
- });
1313
- queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1314
- }
1315
-
1316
- const resetClass = pagedSort[model.name]?.[field.name] ? (((pagedSort[model.name]?.[field.name] === 1) || (pagedSort[model.name]?.[field.name] === -1)) ? 'active' : 'reset') : '';
1317
-
1318
- const renderIconFromType =(field)=>{
1319
- const type = field.type;
1320
- if( type === 'color'){
1321
- return <FaPallet/>;
1322
- }
1323
- if( type === 'code'){
1324
- return <FaCode />;
1325
- }
1326
- else if( type === 'date'){
1327
- return <FaCalendarWeek />;
1328
- }else if( type === 'datetime'){
1329
- return <FaCalendarDays />;
1330
- }
1331
- else if( type === 'richtext' || type === 'string' || type === 'string_t'){
1332
- return <></>;
1333
- }
1334
- else if( type === 'url'){
1335
- return <FaLink />;
1336
- }
1337
- else if( type === 'number'){
1338
- return <FaHashtag />;
1339
- }
1340
- else if( type === 'file'){
1341
- return <FaFile />;
1342
- }
1343
- else if( type === 'enum'){
1344
- return <FaListUl />;
1345
- }
1346
- else if( type === 'boolean'){
1347
- return <FaToggleOn />;
1348
- }
1349
- else if( type === 'image'){
1350
- return <FaImage/>;
1351
- }
1352
- else if( type === 'relation'){
1353
- return field.multiple ? <FaSitemap /> : <FaLink />;
1354
- }
1355
- else if( type === 'email'){
1356
- return <FaAt />;
1357
- }
1358
- else if( type === 'phone'){
1359
- return <FaPhone />;
1360
- }
1361
- else if( type === 'array'){
1362
- return <FaTableColumns />;
1363
- }
1364
- return <FaPencil />
1365
- }
1366
- return <th key={field.name} className={`form filter-field`} style={{backgroundColor: field.color, color: !field.color ||isLightColor(field.color) ? 'black': "white"}}>
1367
- <div className="flex flex-centered flex-mini-gap flex-row">
1368
- <div className="flex flex-1 flex-mini-gap flex-no-wrap">
1369
- {renderIconFromType(field)}
1370
- <span title={field.name} className={"flex-1 title"}>{t(`field_${model.name}_${field.name}`, field.name)}</span>
1371
- </div>
1372
- {advanced && (<>
1373
- { 'password'!==field.type && (<div className={"flex flex-no-gap"}>
1374
- {(<>
1375
- {(pagedSort[model.name]?.[field.name] !== 1) &&
1376
- <button onClick={() => handleChangeSort(true)}
1377
- className={resetClass}>
1378
- {pagedSort[model.name]?.[field.name] === undefined ? <FaArrowDown/> : <FaArrowUp/>}</button>}
1379
- {(pagedSort[model.name]?.[field.name] === 1) &&
1380
- <button onClick={() => handleChangeSort(false)}
1381
- className={resetClass}>
1382
- <FaArrowDown/></button>}
1383
- </>
1384
- )}
1385
- {!field.unique && (
1386
- <button onClick={() => handleToggleLock()} className={locked ? 'active' : ''}><FaLock/></button>)}
1387
- </div>)}
1388
- {active && !['date','datetime','enum', 'boolean', 'number', 'password'].includes(field.type) && <div className="filter flex flex-no-wrap flex-mini-gap">
1389
- <FilterStringField setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1390
- </div>}
1391
- {active && field.type === 'enum' && <div className="filter flex flex-no-wrap flex-mini-gap">
1392
- <FilterEnumField model={model} setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1393
- </div>}
1394
- {active && field.type === 'boolean' && <div className="filter flex flex-no-wrap flex-mini-gap">
1395
- <FilterBooleanField filterValues={filterValues} model={model} field={field} onChangeFilterValue={onChangeFilterValue} />
1396
- </div>}
1397
- {active && ['date', 'datetime'].includes(field.type) && <div className="filter flex flex-no-wrap flex-mini-gap">
1398
- <FilterDateField filterValues={filterValues} model={model} field={field} onChangeFilterValue={onChangeFilterValue} />
1399
- </div>}
1400
- {active && field.type === 'number' && <div className="filter flex flex-no-wrap flex-mini-gap">
1401
- <FilterNumberField model={model} setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1402
- </div>}
1403
- </>)}
1404
- </div>
1405
- </th>
1406
- }
1407
-
1408
- export const PhoneField = ({name, value, onChange}) => {
1409
- const [phone, setPhone] = useState(value);
1410
- useEffect(() => {
1411
- setPhone(value);
1412
- }, [value]);
1413
- return (
1414
- <div>
1415
- <PhoneInput
1416
- defaultCountry="ua"
1417
- value={phone || ''}
1418
- onChange={(phone) => {
1419
- setPhone(phone);
1420
- onChange?.(phone);
1421
- }}
1422
- />
1423
- </div>
1424
- );
1425
- }
1426
-
1427
- export const ModelField = ({field, disableable=false, showModel=true, value, fieldValue, fields=false, onChange}) => {
1428
- const {models} = useModelContext();
1429
- const {me} = useAuthContext();
1430
- const {t} = useTranslation()
1431
- const [checked, setChecked] = useState(true);
1432
-
1433
- // Trouver le modèle correspondant à la valeur
1434
- const selectedModel = models.find(m => m.name === value && m._user === me?.username);
1435
-
1436
- // Préparer les options pour les champs du modèle
1437
- const fieldOptions = selectedModel?.fields.map(f => ({
1438
- label: t(`field_${f.name}`, f.name),
1439
- value: f.name
1440
- })) || [];
1441
-
1442
- // Gestion du changement de modèle
1443
- const handleModelChange = (e) => {
1444
- const newModel = e.value;
1445
- const firstField = fieldOptions[0]?.value || null;
1446
-
1447
- if (fields) {
1448
- onChange({name: field?.name, value: { model: newModel, field: firstField }});
1449
- } else {
1450
- onChange({name: field?.name, value: newModel});
1451
- }
1452
- };
1453
-
1454
- // Gestion du changement de champ
1455
- const handleFieldChange = (e) => {
1456
- onChange({name: field?.name, value: { model: value, field: e.value }});
1457
- };
1458
-
1459
- const dis = disableable ? (
1460
- <CheckboxField
1461
- checked={checked}
1462
- onChange={e => {
1463
- setChecked(e);
1464
- if (!e) {
1465
- onChange({name: field?.name, value: null});
1466
- }
1467
- }}
1468
- />
1469
- ) : null;
1470
-
1471
- if (!fields) {
1472
- return (
1473
- <div className="flex flex-1">
1474
- {dis}
1475
- {checked && (
1476
- <SelectField
1477
- className="flex-1"
1478
- value={value}
1479
- onChange={handleModelChange}
1480
- items={models
1481
- .filter(m => m._user === me?.username)
1482
- .map(m => ({
1483
- label: t(`model_${m.name}`, m.name),
1484
- value: m.name
1485
- }))
1486
- }
1487
- />
1488
- )}
1489
- </div>
1490
- );
1491
- }
1492
-
1493
- return (
1494
- <div className="flex flex-1">
1495
- {dis}
1496
- {checked && (
1497
- <div className="flex flex-stretch" key={field?.name ?? 'def'}>
1498
- {showModel && (
1499
- <SelectField
1500
- className="flex-1"
1501
- value={value}
1502
- onChange={handleModelChange}
1503
- items={models
1504
- .filter(m => m._user === me?.username)
1505
- .map(m => ({
1506
- label: t(`model_${m.name}`, m.name),
1507
- value: m.name
1508
- }))
1509
- }
1510
- />
1511
- )}
1512
- <SelectField
1513
- className="flex-1"
1514
- value={fieldValue || (fieldOptions[0]?.value || null)}
1515
- onChange={handleFieldChange}
1516
- items={fieldOptions}
1517
- />
1518
- </div>
1519
- )}
1520
- </div>
1521
- );
1522
- };
1523
-
1524
- // Fonction pour obtenir le composant icône par son nom
1525
- const getIconComponent = (iconName) => {
1526
- if (!iconName) return null;
1527
- const IconComponent = FaIcons[iconName] || Fa6Icons[iconName];
1528
- return IconComponent ? <IconComponent /> : null; // Retourne l'élément React ou null
1529
- };
1530
- export const IconField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1531
- const { t } = useTranslation();
1532
- const [iconSuggestions, setIconSuggestions] = useState([]);
1533
- // Tri alphabétique pour une recherche plus prévisible
1534
- const [allFaIcons] = useState(() => [...Object.keys(FaIcons), ...Object.keys(Fa6Icons)].sort());
1535
-
1536
- const handleIconChange = (e) => {
1537
- const value = e.target.value;
1538
- onChange(value);
1539
- if (value) {
1540
- const filtered = allFaIcons.filter(
1541
- icon => icon.toLowerCase().includes(value.toLowerCase())
1542
- );
1543
- setIconSuggestions(filtered.slice(0, 20));
1544
- } else {
1545
- setIconSuggestions([]);
1546
- }
1547
- };
1548
-
1549
- const handleIconFocus = () => {
1550
- if (value) {
1551
- const filtered = allFaIcons.filter(
1552
- icon => icon.toLowerCase().includes(value.toLowerCase())
1553
- );
1554
- setIconSuggestions(filtered.slice(0, 20));
1555
- } else {
1556
- setIconSuggestions(allFaIcons.slice(0, 10));
1557
- }
1558
- };
1559
-
1560
- const onSuggestionClick = (suggestion) => {
1561
- onChange(suggestion);
1562
- setIconSuggestions([]);
1563
- };
1564
-
1565
- return <div className="textfield-wrapper with-suggestions">
1566
- <div className={"flex flex-1 flex-no-wrap"}>
1567
- <TextField
1568
- help={t('modelcreator.field.icon')}
1569
- id="modelIcon"
1570
- disabled={disabled}
1571
- value={value}
1572
- label={label}
1573
- before={<div>{getIconComponent(value)}</div>}
1574
- onChange={handleIconChange}
1575
- onFocus={handleIconFocus}
1576
- onBlur={() => setTimeout(() => setIconSuggestions([]), 200)}
1577
- autoComplete="off"
1578
- />
1579
- </div>
1580
- {iconSuggestions.length > 0 && (
1581
- <ul className="suggestions-list">
1582
- {iconSuggestions.map(icon => (
1583
- <li key={icon} onMouseDown={() => onSuggestionClick(icon)}>
1584
- <span className="suggestion-icon">{getIconComponent(icon)}</span>
1585
- <span>{icon}</span>
1586
- </li>
1587
- ))}
1588
- </ul>
1589
- )}
1590
- </div>
1591
- };
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
-
1605
- // 3. Effet pour synchroniser l'état interne si la prop `value` du parent change.
1606
- useEffect(() => {
1607
- if (value !== internalValue) {
1608
- setInternalValue(value);
1609
- }
1610
- }, [value]);
1611
-
1612
- const handleChange = (e) => {
1613
- const newValue = e.target.value;
1614
- // Met à jour l'état interne instantanément pour que l'input soit réactif.
1615
- setInternalValue(newValue);
1616
- // Appelle la fonction "debounced" pour notifier le parent.
1617
- debouncedOnChange(newValue);
1618
- };
1619
-
1620
- return (
1621
- <div className={`flex flex-1 flex-no-wrap ${className || ''}`}>
1622
- {label && (<label className="flex-1">{label}</label>)}
1623
- <div className="flex flex-1 flex-no-wrap"><input
1624
- disabled={disabled}
1625
- type="color"
1626
- // L'input est maintenant contrôlé par notre état interne.
1627
- value={internalValue || '#FFFFFF'}
1628
- onChange={handleChange}
1629
- {...rest}
1630
- />
1631
- <span className="color-value">{internalValue || '#FFFFFF'}</span>
1632
- </div>
1633
- </div>
1634
- );
1635
- };
1636
-
1637
- const secondsToDuration = (totalSeconds) => {
1638
- if (totalSeconds === null || totalSeconds === undefined || isNaN(totalSeconds) || totalSeconds === '') {
1639
- return { days: '', hours: '', minutes: '', seconds: '' };
1640
- }
1641
- const total = parseInt(totalSeconds, 10);
1642
- const d = Math.floor(total / 86400);
1643
- let remainder = total % 86400;
1644
- const h = Math.floor(remainder / 3600);
1645
- remainder %= 3600;
1646
- const m = Math.floor(remainder / 60);
1647
- const s = remainder % 60;
1648
- return { days: d, hours: h, minutes: m, seconds: s };
1649
- };
1650
-
1651
- const durationToSeconds = ({ days, hours, minutes, seconds }) => {
1652
- return (parseInt(days, 10) || 0) * 86400 +
1653
- (parseInt(hours, 10) || 0) * 3600 +
1654
- (parseInt(minutes, 10) || 0) * 60 +
1655
- (parseInt(seconds, 10) || 0);
1656
- };
1657
-
1658
- export const DurationField = forwardRef(({ value, onChange, name, label, help, required, editable, readOnly }, ref) => {
1659
- const { t } = useTranslation();
1660
- const [duration, setDuration] = useState(secondsToDuration(value));
1661
- const [errors, setErrors] = useState([]);
1662
-
1663
- useEffect(() => {
1664
- setDuration(secondsToDuration(value));
1665
- }, [value]);
1666
-
1667
- const validate = () => {
1668
- const errs = [];
1669
- const totalSeconds = durationToSeconds(duration);
1670
- if (required && totalSeconds <= 0) {
1671
- errs.push(t('form.validation.required', "Field required"));
1672
- }
1673
- setErrors(errs);
1674
- return !errs.length;
1675
- };
1676
-
1677
- useImperativeHandle(ref, () => ({
1678
- validate,
1679
- getValue: () => durationToSeconds(duration),
1680
- }));
1681
-
1682
- const handlePartChange = (part) => (e) => {
1683
- const newDuration = { ...duration, [part]: e.target.value };
1684
- setDuration(newDuration);
1685
- if (onChange) {
1686
- const totalSeconds = durationToSeconds(newDuration);
1687
- onChange({ name, value: totalSeconds });
1688
- }
1689
- };
1690
-
1691
- return (
1692
- <>
1693
- <div className={cn({ field: true, "field-duration": true, 'flex-1': true, flex: true, "field-bg": true })}>
1694
- {label && (
1695
- <label contentEditable={editable} className={cn({ help: !!help, 'flex-1': true })}>
1696
- {label}
1697
- {required && <span className="mandatory" contentEditable={false}>*</span>}
1698
- </label>
1699
- )}
1700
- {help && <div className="flex help">{help}</div>}
1701
- <div className="duration-inputs flex flex-no-wrap flex-mini-gap">
1702
- <NumberField name={`${name}-days`} unit={t('duration.unit.days', 'days')} value={duration.days} onChange={handlePartChange('days')} readOnly={readOnly} min={0} />
1703
- <NumberField name={`${name}-hours`} unit={t('duration.unit.hours', 'hours')} value={duration.hours} onChange={handlePartChange('hours')} readOnly={readOnly} min={0} max={23} />
1704
- <NumberField name={`${name}-minutes`} unit={t('duration.unit.minutes', 'minutes')} value={duration.minutes} onChange={handlePartChange('minutes')} readOnly={readOnly} min={0} max={59} />
1705
- <NumberField name={`${name}-seconds`} unit={t('duration.unit.seconds', 'seconds')} value={duration.seconds} onChange={handlePartChange('seconds')} readOnly={readOnly} min={0} max={59} />
1706
- </div>
1707
- </div>
1708
- {errors.length > 0 && (
1709
- <ul className="error">
1710
- {errors.map((e, key) => (
1711
- <li key={key} aria-live="assertive" role="alert">{e}</li>
1712
- ))}
1713
- </ul>
1714
- )}
1715
- </>
1716
- );
1717
- });
1718
- DurationField.displayName = "DurationField";
1719
-
1720
- export const CodeField = ({name, label, language, value, disabled, onChange}) => {
1721
- const u = name || uniqid();
1722
- const [currentEditor, setEditor] = useState(null);
1723
-
1724
- return <>
1725
- {label && (<label className="flex flex-1">{label}</label>)}
1726
- {!disabled ? <div className={"codefield"}><span><b>{language}</b> : </span><CodeiumEditor
1727
- language={language || 'json'}
1728
- theme={"vs-dark"}
1729
- value={value}
1730
- onChange={e => {
1731
- if (language === 'json') {
1732
- let code;
1733
- try {
1734
- code = JSON.parse(e);
1735
- onChange({name, value: code});
1736
- } catch (e) {
1737
- }
1738
- } else
1739
- onChange({name, value: e});
1740
- }}
1741
- height="300px"
1742
- /></div> : <div className="code"><SyntaxHighlighter
1743
- language={language || "javascript"} theme={docco}>{value}</SyntaxHighlighter></div>
1744
- }</>
1745
- }
1746
-
1747
- export const EnumField = ({inputProps, value, handleChange, field}) => {
1748
- const { t} = useTranslation()
1749
- useEffect(() => {
1750
- if( field.items.includes(value))
1751
- handleChange(value);
1752
- else{
1753
- handleChange({name: field.name, value: field.items[0]})
1754
- }
1755
- }, []);
1756
- return (
1757
- <select {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} >{(field.items || []).map(item => {
1758
- if( typeof(item) === 'string'){
1759
- return <option value={item}>{t(item, item)}</option>;
1760
- }
1761
- return <></>
1762
- })}</select>
1763
- );
1764
- }
1765
-
1766
- export const RangeField = ({ name, value, onChange, min = 0, max = 100, step = 1, percent = false }) => {
1767
- const handleChange = (e) => {
1768
- // The onChange from the form probably expects the field name and value
1769
- onChange(parseFloat(e.target.value));
1770
- };
1771
-
1772
- const percentage = max > min ? Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100)) : 0;
1773
- const displayValue = percent ? `${Math.round(percentage)}%` : value;
1774
-
1775
- return (
1776
- <div className="range-field">
1777
- <input
1778
- type="range"
1779
- name={name}
1780
- value={value || 0}
1781
- onChange={handleChange}
1782
- min={min}
1783
- max={max}
1784
- step={step}
1785
- />
1786
- <span className="range-value">{displayValue}</span>
1787
- </div>
1788
- );
1
+ import React, {
2
+ forwardRef, useCallback,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import uniqid from "uniqid";
9
+ import cn from "classnames";
10
+ import { recursiveMap, useRefs } from "./Utils.jsx";
11
+ import {useTranslation} from "react-i18next";
12
+ import { CodeiumEditor } from "@codeium/react-code-editor";
13
+
14
+ import {debounce, escapeRegExp, isGUID, isLightColor} from "../../src/core.js";
15
+ import {mainFieldsTypes, maxFileSize} from "../../src/constants.js";
16
+ import {useModelContext} from "./contexts/ModelContext.jsx";
17
+ import { SketchPicker } from 'react-color'; // Importer le sélecteur
18
+ import {useQueryClient} from "react-query";
19
+ import tinycolor from 'tinycolor2';
20
+ import {
21
+ FaArrowDown,
22
+ FaArrowUp, FaAt, FaEye, FaEyeSlash,
23
+ FaCalendar, FaCalendarWeek, FaCode, FaFile,
24
+ FaHashtag, FaIcons,
25
+ FaImage,
26
+ FaLink, FaListOl, FaListUl,
27
+ FaLock, FaMinus,
28
+ FaPallet, FaPhone, FaSitemap,
29
+ FaToggleOn
30
+ } from "react-icons/fa";
31
+ import * as Fa6Icons from 'react-icons/fa6'; // Importer Fa6
32
+ import {FaCalendarDays, FaCodeCompare, FaPencil, FaT, FaTableColumns} from "react-icons/fa6";
33
+ import { CodeBlock, tomorrowNightBright } from 'react-code-blocks';
34
+ import SyntaxHighlighter from 'react-syntax-highlighter';
35
+ import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
36
+
37
+ import { PhoneInput } from 'react-international-phone';
38
+ import 'react-international-phone/style.css';
39
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
40
+ import Switch from "react-switch";
41
+ export const Form = ({
42
+ name,
43
+ onValidate,
44
+ onError,
45
+ children,
46
+ editable,
47
+ className = "",
48
+ }) => {
49
+ const [childrenRef, registerRef] = useRefs();
50
+ const onSubmit = (e) => {
51
+ e.preventDefault();
52
+ let res = true;
53
+ Object.keys(childrenRef.current).forEach((item) => {
54
+ res = childrenRef.current[item].validate() && res;
55
+ });
56
+ if (res) {
57
+ if (onValidate) onValidate(e);
58
+ } else {
59
+ if (onError) onError();
60
+ }
61
+ };
62
+
63
+ /**/
64
+ return (
65
+ <form
66
+ name={name}
67
+ noValidate={true}
68
+ contentEditable={editable}
69
+ className={cn({ ["form-" + name]: true }) + " " + (className || "")}
70
+ onSubmit={onSubmit}
71
+ >
72
+ {recursiveMap(children, (child, index) => {
73
+ /*if( child?.type?.displayName?.match(/(Field|RadioGroup)/)){
74
+ return <child.type {...child.props} ref={registerRef(child?.type?.displayName.concat('-').concat(child.props.name || uniqid()))} />
75
+ }*/
76
+ return child;
77
+ })}
78
+ </form>
79
+ );
80
+ };
81
+
82
+ const TextField = forwardRef(function TextField(
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
+ ...rest
102
+ },
103
+ ref,
104
+ ) {
105
+ const [id, setId] = useState("textfield-" + uniqid());
106
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false);
107
+ const [errors, setErrors] = useState([]);
108
+ const inputRef = useRef();
109
+
110
+ const { type, ...otherRest } = rest;
111
+ const isPasswordField = type === 'password';
112
+
113
+ const mult = typeof multiline !== 'undefined' ? multiline : maxlength > 255;
114
+ const validate = () => {
115
+ const errs = [];
116
+ if (required && (!value || value.trim() === "")) {
117
+ errs.push("Field required");
118
+ }
119
+ if (
120
+ minlength > 0 &&
121
+ typeof value == "string" &&
122
+ value.trim().length < minlength
123
+ ) {
124
+ errs.push("Value length must be >= to " + minlength);
125
+ }
126
+ if (
127
+ maxlength !== undefined && maxlength > 0 &&
128
+ typeof value == "string" &&
129
+ value.trim().length > maxlength
130
+ ) {
131
+ errs.push("Value length must be <= to " + maxlength);
132
+ }
133
+ if( showErrors )
134
+ setErrors(errs);
135
+ return !errs.length;
136
+ };
137
+ useImperativeHandle(ref, () => ({
138
+ ref: inputRef.current,
139
+ validate,
140
+ getValue: () => value,
141
+ }));
142
+ const handleChange = (e) => {
143
+ if (onChange) {
144
+ onChange(e);
145
+ }
146
+ };
147
+
148
+ const togglePasswordVisibility = () => {
149
+ setIsPasswordVisible(prevState => !prevState);
150
+ };
151
+ useEffect(() => {
152
+ if (value !== null) validate();
153
+ }, [value]);
154
+ return (
155
+ <>
156
+ <div
157
+ className={cn({
158
+ field: true,
159
+ flex: true,
160
+ "field-text": !mult,
161
+ "field-multiline": mult,
162
+ })}
163
+ >
164
+ {label && (
165
+ <label
166
+ contentEditable={editable}
167
+ className={cn({help: !!help, 'flex-1': true})}
168
+ htmlFor={id}
169
+ {...labelProps}
170
+ >
171
+ {label}
172
+ {required ? (
173
+ <span className="mandatory" contentEditable={false}>
174
+ *
175
+ </span>
176
+ ) : (
177
+ ""
178
+ )}
179
+ </label>
180
+ )}
181
+
182
+ {help &&<div className="flex help">{help}</div>}
183
+
184
+ {mult && (
185
+ <textarea
186
+ ref={inputRef}
187
+ aria-required={required}
188
+ aria-readonly={readOnly}
189
+ readOnly={readOnly}
190
+ placeholder={placeholder}
191
+ id={id}
192
+ name={name}
193
+ value={value || ""}
194
+ rows={8}
195
+ onChange={handleChange}
196
+ minLength={minlength}
197
+ maxLength={maxlength}
198
+ {...rest}
199
+ ></textarea>
200
+ )}
201
+
202
+ {before}
203
+ <div className={"flex flex-1 flex-no-gap flex-start"} style={{ position: 'relative' }}>
204
+ {!mult && (
205
+ <input
206
+ ref={inputRef}
207
+ aria-required={required}
208
+ aria-readonly={readOnly}
209
+ readOnly={readOnly}
210
+ type={isPasswordField ? (isPasswordVisible ? 'text' : 'password') : (searchable ? "search" : (type || "text"))}
211
+ placeholder={placeholder}
212
+ title={placeholder}
213
+ alt={placeholder}
214
+ id={id}
215
+ name={name}
216
+ value={value || ""}
217
+ onChange={handleChange}
218
+ minLength={minlength}
219
+ maxLength={maxlength}
220
+ required={required}
221
+ style={isPasswordField ? { paddingRight: '40px' } : {}}
222
+ {...otherRest}
223
+ />
224
+ )}
225
+ {isPasswordField && !mult && (
226
+ <button type="button" onClick={togglePasswordVisibility} className="password-toggle-icon" style={{position: 'absolute', right: '10px', top: '50%', transform: 'translateY(-50%)', display: 'flex', alignItems: 'center'}}>
227
+ {isPasswordVisible ? <FaEyeSlash /> : <FaEye />}
228
+ </button>
229
+ )}
230
+ {after}
231
+ </div>
232
+ </div>
233
+ {errors.length > 0 && (
234
+ <ul className="error">
235
+ {errors.map((e, key) => (
236
+ <li key={key} aria-live="assertive" role="alert">
237
+ {e}
238
+ </li>
239
+ ))}
240
+ </ul>
241
+ )}
242
+ </>
243
+ );
244
+ });
245
+
246
+ TextField.displayName = "TextField";
247
+ export { TextField };
248
+
249
+ const EmailField = forwardRef(
250
+ (
251
+ {
252
+ name,
253
+ label,
254
+ placeholder,
255
+ help,
256
+ editable,
257
+ defaultValue,
258
+ required,
259
+ readOnly,
260
+ onChange,
261
+ minlength,
262
+ maxlength,
263
+ fieldValidated,
264
+ },
265
+ ref,
266
+ ) => {
267
+ const id = "emailfield-" + uniqid();
268
+ const [errors, setErrors] = useState([]);
269
+ const [value, setValue] = useState(defaultValue || null);
270
+ const validate = () => {
271
+ const errs = [];
272
+ if (required && (!value || value.trim() === "")) {
273
+ errs.push("Field required");
274
+ }
275
+ if (minlength !== undefined && minlength > 0 && value && value.trim().length < minlength) {
276
+ errs.push("Value length must be >= to " + minlength);
277
+ }
278
+ if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
279
+ errs.push("Value length must be <= to " + maxlength);
280
+ }
281
+
282
+ if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
283
+ errs.push("Invalid email");
284
+ }
285
+ setErrors(errs);
286
+ return !errs.length;
287
+ };
288
+ useEffect(() => {
289
+ if (value !== null) validate();
290
+ }, [value]);
291
+ useEffect(() => {
292
+ if (fieldValidated) validate();
293
+ }, [fieldValidated]);
294
+ useImperativeHandle(ref, () => ({
295
+ validate,
296
+ getValue: () => value,
297
+ }));
298
+ const handleChange = (e) => {
299
+ setValue(e.target.value);
300
+ if (onChange) {
301
+ onChange(e);
302
+ }
303
+ };
304
+ return (
305
+ <>
306
+ <div className={cn({ field: true, "field-email": true })}>
307
+ <label
308
+ contentEditable={editable}
309
+ className={cn({ help: !!help })}
310
+ title={help}
311
+ htmlFor={id}
312
+ >
313
+ {label}
314
+ {required ? (
315
+ <span className="mandatory" contentEditable={false}>
316
+ *
317
+ </span>
318
+ ) : (
319
+ ""
320
+ )}
321
+ </label>
322
+ <input
323
+ aria-required={required}
324
+ aria-readonly={readOnly}
325
+ readOnly={readOnly}
326
+ type="email"
327
+ placeholder={placeholder}
328
+ id={id}
329
+ name={name}
330
+ value={value || ""}
331
+ onChange={handleChange}
332
+ minLength={minlength}
333
+ maxLength={maxlength}
334
+ />
335
+ </div>
336
+ {errors.length > 0 && (
337
+ <ul className="error">
338
+ {errors.map((e, key) => (
339
+ <li key={key} aria-live="assertive" role="alert">
340
+ {e}
341
+ </li>
342
+ ))}
343
+ </ul>
344
+ )}
345
+ </>
346
+ );
347
+ },
348
+ );
349
+ EmailField.displayName = "EmailField";
350
+ export { EmailField };
351
+
352
+ const NumberField = forwardRef(
353
+ (
354
+ {
355
+ name,
356
+ label,
357
+ placeholder,
358
+ help,
359
+ editable,
360
+ value,
361
+ required,
362
+ readOnly,
363
+ onChange,
364
+ minlength,
365
+ maxlength,
366
+ min,
367
+ max,
368
+ step,
369
+ unit,
370
+ ...rest
371
+ },
372
+ ref,
373
+ ) => {
374
+ const id = "numberfield-" + uniqid();
375
+ const [errors, setErrors] = useState([]);
376
+ const inputRef = useRef();
377
+ const validate = () => {
378
+ const errs = [];
379
+ if (required && value === undefined) {
380
+ errs.push("Field required");
381
+ }
382
+ if (minlength !== undefined && minlength > 0 && value && value.trim().length < minlength) {
383
+ errs.push("Value length must be >= to " + minlength);
384
+ }
385
+ if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
386
+ errs.push("Value length must be <= to " + maxlength);
387
+ }
388
+ if ((min || min === 0) && (value || value === 0) && min > value) {
389
+ errs.push("Value < to " + min);
390
+ }
391
+ if ((max || max === 0) && (value || value === 0) && max < value) {
392
+ errs.push("Value > to " + max);
393
+ }
394
+ setErrors(errs);
395
+ return !errs.length;
396
+ };
397
+ useEffect(() => {
398
+ if (value !== null) validate();
399
+ }, [value]);
400
+ useImperativeHandle(ref, () => ({
401
+ ref: inputRef.current,
402
+ validate,
403
+ getValue: () => value,
404
+ }));
405
+ const handleChange = (e) => {
406
+ if (onChange) {
407
+ onChange(e);
408
+ }
409
+ };
410
+ return (
411
+ <>
412
+ <div className={cn({ field: true, "field-number": true })}>
413
+
414
+ <div className="flex flex-1">
415
+ {label && (
416
+ <label
417
+ contentEditable={editable}
418
+ className={cn({ help: !!help, flex: true, 'flex-1': true })}
419
+ title={help}
420
+ htmlFor={id}
421
+ >
422
+ {label}
423
+ {required ? (
424
+ <span className="mandatory" contentEditable={false}>
425
+ *
426
+ </span>
427
+ ) : (
428
+ ""
429
+ )}
430
+ </label>
431
+ )}
432
+ {help && <div className="flex help">{help}</div>}
433
+ <div className={"flex flex-1 flex-no-wrap flex-mini-gap flex-end"}>
434
+ <input
435
+ ref={inputRef}
436
+ aria-required={required}
437
+ aria-readonly={readOnly}
438
+ readOnly={readOnly}
439
+ type="number"
440
+ placeholder={placeholder}
441
+ id={id}
442
+ name={name}
443
+ value={value || ""}
444
+ onChange={handleChange}
445
+ minLength={minlength}
446
+ maxLength={maxlength}
447
+ min={min}
448
+ max={max}
449
+ step={step}
450
+ {...rest}
451
+ />
452
+ {unit && <span className="unit">{unit}</span>}
453
+ </div>
454
+ </div>
455
+ </div>
456
+ {errors.length > 0 && (
457
+ <ul className="error">
458
+ {errors.map((e, key) => (
459
+ <li key={key} aria-live="assertive" role="alert">
460
+ {e}
461
+ </li>
462
+ ))}
463
+ </ul>
464
+ )}
465
+ </>
466
+ );
467
+ },
468
+ );
469
+ NumberField.displayName = "NumberField";
470
+ export { NumberField };
471
+
472
+ const CheckboxField = forwardRef(
473
+ (
474
+ {
475
+ name,
476
+ label,
477
+ placeholder,
478
+ help,
479
+ editable,
480
+ defaultValue,
481
+ required,
482
+ readOnly,
483
+ onChange,
484
+ minlength,
485
+ maxlength,
486
+ checked,
487
+ checkbox=false,
488
+ ...rest
489
+ },
490
+ ref,
491
+ ) => {
492
+ const id = "checkfield-" + uniqid();
493
+ const [errors, setErrors] = useState([]);
494
+ const [value, setValue] = useState(checked || false);
495
+ useEffect(() => {
496
+ setValue(checked);
497
+ }, [checked]);
498
+ const validate = () => {
499
+ const errs = [];
500
+ if (required && !value) {
501
+ errs.push("Field must be checked.");
502
+ }
503
+ setErrors(errs);
504
+ return !errs.length;
505
+ };
506
+ useEffect(() => {
507
+ if (value !== null) validate();
508
+ }, [value]);
509
+ useImperativeHandle(ref, () => ({
510
+ validate,
511
+ getValue: () => value,
512
+ }));
513
+ const handleChange = (e) => {
514
+ setValue(!value);
515
+ onChange?.(e);
516
+ };
517
+ return (
518
+ <>
519
+ <div className={cn({field: true, "field-checkbox": true,"field-bg": true})}>
520
+ {label && (
521
+ <label
522
+ contentEditable={editable}
523
+ title={help}
524
+ htmlFor={id}
525
+ >
526
+ {label}
527
+ {required ? (
528
+ <span className="mandatory" contentEditable={false}>
529
+ *
530
+ </span>
531
+ ) : (
532
+ ""
533
+ )}
534
+ </label>
535
+ )}
536
+ {help && <div className="flex help">{help}</div>}
537
+ {!checkbox && (<Switch
538
+ id={id}
539
+ onChange={handleChange}
540
+ checked={value} />)}
541
+ {checkbox && (
542
+ <input type={"checkbox"} id={id} onChange={handleChange} checked={value} />
543
+ )}
544
+ </div>
545
+ {errors.length > 0 && (
546
+ <ul className="error">
547
+ {errors.map((e, key) => (
548
+ <li key={key} aria-live="assertive" role="alert">
549
+ {JSON.stringify(e, null, 2)}
550
+ </li>
551
+ ))}
552
+ </ul>
553
+ )}
554
+ </>
555
+ );
556
+ },
557
+ );
558
+ CheckboxField.displayName = "CheckboxField";
559
+ export { CheckboxField };
560
+
561
+ const RadioField = forwardRef(
562
+ (
563
+ {
564
+ name,
565
+ label,
566
+ placeholder,
567
+ help,
568
+ editable,
569
+ checked,
570
+ required,
571
+ readOnly,
572
+ onChange,
573
+ minlength,
574
+ maxlength,
575
+ },
576
+ ref,
577
+ ) => {
578
+ const id = "radiofield-" + uniqid();
579
+ const [errors, setErrors] = useState([]);
580
+ const [value, setValue] = useState(checked || null);
581
+ const validate = () => {
582
+ const errs = [];
583
+ if (required && !value) {
584
+ errs.push("Field must be checked.");
585
+ }
586
+ setErrors(errs);
587
+ return !errs.length;
588
+ };
589
+ useEffect(() => {
590
+ if (value !== null) validate();
591
+ }, [value]);
592
+ useImperativeHandle(ref, () => ({
593
+ validate,
594
+ getValue: () => value,
595
+ }));
596
+ const handleChange = (e) => {
597
+ setValue(!value);
598
+ if (onChange) {
599
+ onChange(e);
600
+ }
601
+ };
602
+ return (
603
+ <>
604
+ <div className={cn({ field: true, "field-radio": true })}>
605
+ <input
606
+ aria-required={required}
607
+ aria-readonly={readOnly}
608
+ readOnly={readOnly}
609
+ type="radio"
610
+ checked={value}
611
+ value={value || label}
612
+ placeholder={placeholder}
613
+ id={id}
614
+ name={name}
615
+ onChange={handleChange}
616
+ minLength={minlength}
617
+ maxLength={maxlength}
618
+ />
619
+ <label
620
+ contentEditable={editable}
621
+ className={cn({ help: !!help })}
622
+ title={help}
623
+ htmlFor={id}
624
+ >
625
+ {label}
626
+ {required ? (
627
+ <span className="mandatory" contentEditable={false}>
628
+ *
629
+ </span>
630
+ ) : (
631
+ ""
632
+ )}
633
+ </label>
634
+ </div>
635
+ {errors.length > 0 && (
636
+ <ul className="error">
637
+ {errors.map((e, key) => (
638
+ <li key={key} aria-live="assertive" role="alert">
639
+ {e}
640
+ </li>
641
+ ))}
642
+ </ul>
643
+ )}
644
+ </>
645
+ );
646
+ },
647
+ );
648
+ RadioField.displayName = "RadioField";
649
+ export { RadioField };
650
+
651
+ const SelectField = forwardRef(
652
+ (
653
+ {
654
+ name,
655
+ value,
656
+ items,
657
+ label,
658
+ placeholder,
659
+ disabled,
660
+ help,
661
+ editable,
662
+ checked,
663
+ required,
664
+ readOnly,
665
+ onChange,
666
+ minlength,
667
+ maxlength,
668
+ multiple,
669
+ ...rest
670
+ },
671
+ ref,
672
+ ) => {
673
+ const [values, setValues] = useState([]);
674
+ const id = "selectfield-" + uniqid();
675
+ const [errors, setErrors] = useState([]);
676
+ const [_value, setValue] = useState(value);
677
+ useEffect(() => {
678
+ if( value === undefined && required && items[0]){
679
+ setValue(items[0].value);
680
+ }else {
681
+ setValue(value);
682
+ setValues(value)
683
+ }
684
+ if (!multiple && value) {
685
+ //const index = items.findIndex((i) => i.value === value);
686
+ //onChange({name, value:items[index]}, index);
687
+ }
688
+ }, [value]);
689
+ const validate = () => {
690
+ const errs = [];
691
+ if (required && _value === undefined) {
692
+ errs.push("Field is required.");
693
+ }
694
+ setErrors(errs);
695
+ return !errs.length;
696
+ };
697
+ useEffect(() => {
698
+ if (_value !== null) validate();
699
+ }, [_value]);
700
+ useImperativeHandle(ref, () => ({
701
+ validate,
702
+ getValue: () => _value,
703
+ setValue,
704
+ }));
705
+ const handleChange = (e) => {
706
+ setValue(e.target.value);
707
+ if (onChange) {
708
+ let options = e.target.options;
709
+ let value = [];
710
+ for (var i = 0, l = options.length; i < l; i++) {
711
+ if (options[i].selected) {
712
+ value.push(options[i].value);
713
+ }
714
+ }
715
+ if( multiple ) {
716
+ setValues(value);
717
+ onChange(value);
718
+ }else {
719
+ const index = items.findIndex((i) => i.value+'' === e.target.value);
720
+ onChange(items[index], index);
721
+ }
722
+ }
723
+ };
724
+ return (
725
+ <>
726
+ <div className={cn({ field: true, 'flex-1': true, flex: true, "field-select": true })}>
727
+ {label && (
728
+ <label
729
+ contentEditable={editable}
730
+ className={cn({ help: !!help, 'flex-1': true })}
731
+ title={help}
732
+ htmlFor={id}
733
+ >
734
+ {label}
735
+ {required ? (
736
+ <span className="mandatory" contentEditable={false}>
737
+ *
738
+ </span>
739
+ ) : (
740
+ ""
741
+ )}
742
+ </label>
743
+ )}
744
+ <select
745
+ aria-required={required}
746
+ aria-readonly={readOnly}
747
+ value={(_value)}
748
+ id={id}
749
+ name={name}
750
+ onChange={handleChange}
751
+ multiple={multiple}
752
+ disabled={disabled}
753
+ className={"flex-1"}
754
+ {...rest}
755
+ >
756
+ {items.map((i) => (
757
+ <option value={i.value}>{i.label}</option>
758
+ ))}
759
+ </select>
760
+ </div>
761
+ {help && <div className="flex help">{help}</div>}
762
+ {errors.length > 0 && (
763
+ <ul className="error">
764
+ {errors.map((e, key) => (
765
+ <li key={key} aria-live="assertive" role="alert">
766
+ {e}
767
+ </li>
768
+ ))}
769
+ </ul>
770
+ )}
771
+ </>
772
+ );
773
+ },
774
+ );
775
+ SelectField.displayName = "SelectField";
776
+ export { SelectField };
777
+
778
+ const RadioGroup = forwardRef(
779
+ ({ id, label, help, editable, name, required, children }, ref) => {
780
+ const [childrenRef, registerRef] = useRefs();
781
+ const [errors, setErrors] = useState([]);
782
+ const validate = () => {
783
+ const errs = [];
784
+ let res = false;
785
+ Object.keys(childrenRef.current).forEach((item) => {
786
+ res = !!childrenRef.current[item].getValue() || res;
787
+ });
788
+ if (!res && required) {
789
+ errs.push("Field is required");
790
+ }
791
+ setErrors(errs);
792
+ return !errs.length;
793
+ };
794
+ useImperativeHandle(ref, () => ({
795
+ validate,
796
+ }));
797
+ const handleChange = () => {
798
+ setTimeout(() => validate(), 0);
799
+ };
800
+ return (
801
+ <>
802
+ <label
803
+ contentEditable={editable}
804
+ className={cn({ help: !!help })}
805
+ title={help}
806
+ htmlFor={children[0].props.id}
807
+ >
808
+ {label}
809
+ {required ? (
810
+ <span className="mandatory" contentEditable={false}>
811
+ *
812
+ </span>
813
+ ) : (
814
+ ""
815
+ )}
816
+ </label>
817
+ {[
818
+ recursiveMap(children, (child, index) => {
819
+ if (child.type.displayName === "RadioField") {
820
+ const props = {
821
+ ...child.props,
822
+ name: name ? name : child.props.name,
823
+ onChange: () => handleChange(child.props.onChange),
824
+ };
825
+ return (
826
+ <child.type
827
+ {...props}
828
+ ref={registerRef("Radio" + index)}
829
+ name={child.props.name || "btn" + id}
830
+ />
831
+ );
832
+ }
833
+ return child;
834
+ }),
835
+ errors.length > 0 ? (
836
+ <ul className="error">
837
+ {errors.map((e, key) => (
838
+ <li key={key} aria-live="assertive" role="alert">
839
+ {e}
840
+ </li>
841
+ ))}
842
+ </ul>
843
+ ) : (
844
+ <></>
845
+ ),
846
+ ]}
847
+ </>
848
+ );
849
+ },
850
+ );
851
+
852
+ RadioGroup.displayName = "RadioGroup";
853
+ export { RadioGroup };
854
+
855
+ // New FileField component
856
+ const FileField = ({ inputProps, value, onChange, name, mimeTypes, maxSize, multiple}) => {
857
+ const [fileInfos, setFileInfos] = useState(value);
858
+ const { t } = useTranslation();
859
+
860
+ const handleFileChange = (e) => {
861
+ const selectedFiles = Array.from(e.target.files);
862
+ const newFileInfos = [];
863
+
864
+ const promises = selectedFiles.map(selectedFile => {
865
+ if (selectedFile && selectedFile.size > (maxSize || maxFileSize)) {
866
+ alert(`Le fichier est trop volumineux. La taille maximale autorisée est de ${(maxSize || maxFileSize) / (1024 * 1024)} Mo.`);
867
+ e.target.value = '';
868
+ return Promise.resolve();
869
+ }
870
+ return new Promise((resolve) => {
871
+ const reader = new FileReader();
872
+ reader.onloadend = () => {
873
+ newFileInfos.push({
874
+ preview: reader.result,
875
+ newFile: true,
876
+ file: selectedFile,
877
+ name: selectedFile.name
878
+ });
879
+ resolve();
880
+ };
881
+ reader.readAsDataURL(selectedFile);
882
+ });
883
+ });
884
+
885
+ Promise.all(promises).then(() => {
886
+ if(!multiple){
887
+ setFileInfos(newFileInfos);
888
+ }else{
889
+ setFileInfos(fileInfos => [...fileInfos, ...newFileInfos]);
890
+ }
891
+ onChange([...fileInfos.map(m => ({...m, newFile: false})), ...newFileInfos]);
892
+ });
893
+ };
894
+
895
+ const handleRemove = (e, index) => {
896
+ e.preventDefault();
897
+ const newFileInfos = fileInfos.filter((_, i) => i !== index);
898
+ setFileInfos(newFileInfos);
899
+ onChange(newFileInfos.map(m => ({...m, newFile: false})));
900
+ };
901
+
902
+ useEffect(() => {
903
+ if( value == null || (Array.isArray(value) && value.length === 0))
904
+ setFileInfos([])
905
+ else{
906
+ const v = Array.isArray(value) ? value : [value];
907
+ setFileInfos(v)
908
+ }
909
+ }, [value]);
910
+
911
+ return (
912
+ <div className="field field-file">
913
+ <input
914
+ id={"field-file-" + name}
915
+ type="file"
916
+ data-field={name}
917
+ accept={mimeTypes ? mimeTypes.join(',') : '*'}
918
+ onChange={handleFileChange}
919
+ multiple={multiple} // Add multiple attribute
920
+ />
921
+ {fileInfos?.length > 0 && (
922
+ <div>
923
+ {fileInfos.filter(f => isGUID(f.guid) || f.preview).map((fileInfo, index) => (
924
+ <div key={index}>
925
+ {fileInfo.preview ? (
926
+ <a href={fileInfo.preview} target="_blank" rel="noopener noreferrer">
927
+ <img src={fileInfo.preview} alt={"Preview"} width='200' height='200' />
928
+ </a>
929
+ ) :(isGUID(fileInfo.guid) ? (
930
+ <a href={"/resources/"+fileInfo.guid} target="_blank" rel="noopener noreferrer">
931
+ <img src={"/resources/"+fileInfo.guid} alt={"Preview"} width='200' height='200' />
932
+ </a>
933
+ ) :(
934
+ <img src={fileInfo.preview} alt="Preview" style={{ maxWidth: '200px', maxHeight: '200px' }} />
935
+ ))}
936
+ <button onClick={(e) => handleRemove(e, index)}><FaMinus /></button>
937
+ </div>
938
+ ))}
939
+ </div>
940
+ )}
941
+ </div>
942
+ );
943
+ };
944
+
945
+ export { FileField };
946
+
947
+ export const FilterNumberField = ({ model, field, onChangeFilterValue, filterValues, setFilterValues }) => {
948
+ const { t } = useTranslation();
949
+ const { models, setPage, dataByModel } = useModelContext(); // dataByModel is not used, consider removing
950
+ const [min, setMin] = useState(null);
951
+ const [max, setMax] = useState(null);
952
+
953
+ // Debounced version of the function that actually calls onChangeFilterValue
954
+ const debouncedApplyFilter = useCallback(
955
+ debounce((currentMin, currentMax) => {
956
+ const conditions = [];
957
+ setPage(1);
958
+ if (currentMin !== null && !isNaN(currentMin)) {
959
+ conditions.push({ $gte: ['$' + field.name, parseFloat(currentMin)] });
960
+ }
961
+ if (currentMax !== null && !isNaN(currentMax)) {
962
+ conditions.push({ $lte: ['$' + field.name, parseFloat(currentMax)] });
963
+ }
964
+
965
+ if (conditions.length > 0) {
966
+ onChangeFilterValue(field, { $and: conditions });
967
+ } else {
968
+ onChangeFilterValue(field, {}); // Clear filter if both are invalid/null
969
+ }
970
+ }, 300), // Adjust delay as needed
971
+ [field, onChangeFilterValue] // Dependencies for useCallback
972
+ );
973
+
974
+ useEffect(() => {
975
+ // This effect is to reset local min/max if the global filterValues are cleared externally
976
+ // It should not call debouncedApplyFilter directly if filterValues is the source of truth
977
+ // for the parent component.
978
+ if (filterValues && typeof filterValues[field.name] === 'object') {
979
+ const andConditions = filterValues[field.name]?.$and;
980
+ if (andConditions && Array.isArray(andConditions)) {
981
+ const gteCondition = andConditions.find(cond => cond.$gte);
982
+ const lteCondition = andConditions.find(cond => cond.$lte);
983
+ setMin(gteCondition ? gteCondition.$gte[1] : null);
984
+ setMax(lteCondition ? lteCondition.$lte[1] : null);
985
+ } else {
986
+ // If the structure is not $and or it's cleared
987
+ setMin(null);
988
+ setMax(null);
989
+ }
990
+ } else if (!filterValues || filterValues[field.name] === undefined || Object.keys(filterValues[field.name] || {}).length === 0) {
991
+ // If filterValues for this field is cleared or doesn't exist
992
+ setMin(null);
993
+ setMax(null);
994
+ }
995
+ }, [filterValues, field.name]);
996
+
997
+
998
+ const handleMinChange = (e) => {
999
+ const inputValue = e.target.value;
1000
+ if (inputValue === "") {
1001
+ setMin(null);
1002
+ debouncedApplyFilter(null, max);
1003
+ } else {
1004
+ const pi = parseFloat(inputValue); // Use parseFloat for potentially decimal numbers
1005
+ if (!isNaN(pi)) {
1006
+ setMin(pi);
1007
+ debouncedApplyFilter(pi, max);
1008
+ } else {
1009
+ setMin(inputValue); // Keep invalid input in state to show user, but don't filter
1010
+ // Or setMin(null) if you want to clear on invalid
1011
+ // Potentially call debouncedApplyFilter(null, max) if invalid min means no min filter
1012
+ }
1013
+ }
1014
+ gtag('event', 'search (number,min)');
1015
+ };
1016
+
1017
+ const handleMaxChange = (e) => {
1018
+ const inputValue = e.target.value;
1019
+ if (inputValue === "") {
1020
+ setMax(null);
1021
+ debouncedApplyFilter(min, null);
1022
+ } else {
1023
+ const pi = parseFloat(inputValue);
1024
+ if (!isNaN(pi)) {
1025
+ setMax(pi);
1026
+ debouncedApplyFilter(min, pi);
1027
+ } else {
1028
+ setMax(inputValue);
1029
+ // Potentially call debouncedApplyFilter(min, null) if invalid max means no max filter
1030
+ }
1031
+ }
1032
+ gtag('event', 'search (number,max)');
1033
+ };
1034
+
1035
+ return (
1036
+ <>
1037
+ <NumberField
1038
+ value={min === null ? '' : min} // Handle null for empty display
1039
+ label="Min:"
1040
+ onChange={handleMinChange}
1041
+ type="number" // Ensure type is number for appropriate input behavior
1042
+ />
1043
+ <NumberField
1044
+ value={max === null ? '' : max} // Handle null for empty display
1045
+ label="Max:"
1046
+ onChange={handleMaxChange}
1047
+ type="number" // Ensure type is number
1048
+ />
1049
+ </>
1050
+ );
1051
+ };
1052
+
1053
+ export const FilterEnumField = ({model, field, onChangeFilterValue, filterValues, setFilterValues}) => {
1054
+ const {t} = useTranslation();
1055
+ const debounced = debounce((field,filter) => onChangeFilterValue(field, { $find: filter }));
1056
+ const { models, setPage, elementsPerPage,pagedFilters, pagedSort, page } = useModelContext();
1057
+
1058
+ const [val, setVal] = useState(null);
1059
+ const queryClient= useQueryClient()
1060
+
1061
+ useEffect(() => {
1062
+ if (Object.keys(filterValues).length === 0){
1063
+ onChangeFilterValue(field, { });
1064
+ setVal('');
1065
+ }
1066
+ }, [filterValues]);
1067
+
1068
+ return <div className={"flex flex-no-gap flex-no-wrap"}><SelectField value={val} className={"flex-1"} items={['', ...(field.items || [])].map(m => ({label: t(m), value: m}))} onChange={(e) => {
1069
+
1070
+ setPage(1);
1071
+
1072
+ if( !e || e.value === '') {
1073
+ setVal('');
1074
+ onChangeFilterValue(field, undefined);
1075
+ }
1076
+ else {
1077
+ onChangeFilterValue(field, {$eq: ['$' + field.name, e.value]});
1078
+ setVal(e.value);
1079
+ }
1080
+
1081
+ gtag('event', 'search (enum)');
1082
+ queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1083
+ }} /><button onClick={() => {
1084
+ onChangeFilterValue(field, { });
1085
+ setVal('');
1086
+ }}>x</button></div>
1087
+ }
1088
+ export const FilterBooleanField = ({model, field, filterValues, onChangeFilterValue }) => {
1089
+ const {t} = useTranslation();
1090
+ const { setPage, pagedFilters, pagedSort, page,elementsPerPage } = useModelContext();
1091
+
1092
+ useEffect(() => {
1093
+ if( Object.keys(filterValues).length === 0 ){
1094
+ setVal('null');
1095
+ onChangeFilterValue(field, { });
1096
+ }
1097
+ }, [filterValues]);
1098
+ const [val, setVal] = useState(null);
1099
+ const queryClient= useQueryClient()
1100
+ return <div className={"flex flex-no-gap flex-no-wrap"}><SelectField value={val} className={"flex-1"} items={[
1101
+ {label: t(''), value: 'null'},
1102
+ {label: t('yes'), value: '1'},
1103
+ { label: t('no'), value: '0'}]}
1104
+ onChange={(e) => {
1105
+ setPage(1);
1106
+ if( !e || e.value === 'null') {
1107
+ setVal(null);
1108
+ onChangeFilterValue(field, { });
1109
+ }
1110
+ else {
1111
+ onChangeFilterValue(field, {$or: [{$eq: ['$' + field.name, e.value === '1']}, {$eq: [{ $type: '$'+field.name }, "missing"]}]});
1112
+ setVal(e.value);
1113
+ }
1114
+
1115
+ gtag('event', 'search (boolean)');
1116
+ queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1117
+ }} /></div>
1118
+ }
1119
+ export const FilterDateField = ({model, field, filterValues, onChangeFilterValue }) => {
1120
+ const {t} = useTranslation();
1121
+
1122
+ const [minDate ,setMinDate] = useState(null);
1123
+ const [maxDate ,setMaxDate] = useState(null);
1124
+ useEffect(() => {
1125
+ if( Object.keys(filterValues).length === 0 ){
1126
+ onChangeFilterValue(field, { });
1127
+ setMinDate('');
1128
+ setMaxDate('');
1129
+ }
1130
+ }, [filterValues]);
1131
+ const onChange = (minDate, maxDate) =>{
1132
+ const min = minDate ? { $gte: ['$'+field.name, minDate]} : null;
1133
+
1134
+ const fm = new Date(maxDate);
1135
+ fm.setDate(fm.getDate() + 1);
1136
+
1137
+ const max = maxDate ? {$lte: ['$' + field.name, fm.toISOString()]} : null;
1138
+ const and= [];
1139
+ if( min) and.push(min);
1140
+ if( max) and.push(max);
1141
+ if( !min && !max)
1142
+ onChangeFilterValue(field, { });
1143
+ else
1144
+ onChangeFilterValue(field, { $and: and});
1145
+ gtag('event', 'search (date)');
1146
+ }
1147
+ return <div className={"flex flex-no-gap flex-no-wrap"}>
1148
+ <label htmlFor={"minDate"+model.name+field.name}>
1149
+ Min:
1150
+ <input id={"minDate"+model.name+field.name} type={"datetime-local"} value={minDate} onChange={e => {
1151
+ setMinDate(e.target.value);
1152
+ onChange?.(e.target.value, maxDate);
1153
+ }} />
1154
+ </label>
1155
+ <label htmlFor={"maxDate"+model.name+field.name}>
1156
+ Max:
1157
+ <input id={"maxDate"+model.name+field.name} type={"datetime-local"} value={maxDate} onChange={e => {
1158
+ setMaxDate(e.target.value);
1159
+ onChange?.(minDate, e.target.value);
1160
+ }} />
1161
+ </label>
1162
+ </div>
1163
+ }
1164
+ export const FilterStringField = ({ field, onChangeFilterValue, filterValues, setFilterValues }) => {
1165
+ const { models, setPage } = useModelContext();
1166
+ const [isRegex, setIsRegex] = useState(false);
1167
+ const { t } = useTranslation();
1168
+
1169
+
1170
+ useEffect(() => {
1171
+ if( Object.keys(filterValues).length === 0 ){
1172
+ onChangeFilterValue(field, { });
1173
+ }
1174
+ }, [filterValues]);
1175
+
1176
+ // Debounced function to apply the filter
1177
+ const debouncedApplyFilter = useCallback(
1178
+ debounce((currentValue, currentIsRegex) => {
1179
+ setPage(1); // Reset page to 1 when filter changes
1180
+
1181
+ if (currentValue === '') {
1182
+ // No need to call setFilterValues here as it's done immediately in handleChange
1183
+ onChangeFilterValue(field, field.multiple ? [] : {}, true);
1184
+ return;
1185
+ }
1186
+
1187
+ let filterQuery;
1188
+ if (field.type === 'relation') {
1189
+ const relationModel = models.find(f => f.name === field.relation);
1190
+ if (relationModel) {
1191
+ const relationFilters = relationModel.fields
1192
+ .filter(f => mainFieldsTypes.includes(f.type))
1193
+ .map(mf => ({
1194
+ $regexMatch: { input: `$$this.${mf.name}`, regex: currentIsRegex ? currentValue : escapeRegExp(currentValue) }
1195
+ }));
1196
+ if (relationFilters.length > 0) {
1197
+ filterQuery = { [field.name]: {$find: { $and: [{ $or: relationFilters }] }}};
1198
+ } else {
1199
+ filterQuery = {}; // Or handle as no match if no searchable fields
1200
+ }
1201
+ } else {
1202
+ filterQuery = {}; // Relation model not found
1203
+ }
1204
+ } else { // Not a relation type
1205
+ const regexToUse = currentIsRegex ? currentValue : escapeRegExp(currentValue);
1206
+ if (field.type === 'array') {
1207
+ filterQuery = {
1208
+ $gt: [
1209
+ {
1210
+ $size: {
1211
+ $filter: {
1212
+ input: '$' + field.name,
1213
+ as: 'item',
1214
+ cond: {
1215
+ $regexMatch: {
1216
+ input: '$$item',
1217
+ regex: regexToUse
1218
+ }
1219
+ }
1220
+ }
1221
+ }
1222
+ },
1223
+ 0
1224
+ ]
1225
+ };
1226
+ } else { // Simple string field
1227
+ filterQuery = {
1228
+ $and: [{
1229
+ $regexMatch: {
1230
+ input: '$' + field.name,
1231
+ regex: regexToUse
1232
+ }
1233
+ }]
1234
+ };
1235
+ }
1236
+ }
1237
+ onChangeFilterValue(field, filterQuery, true);
1238
+ gtag('event', 'search (string)');
1239
+ }, 1200), // Debounce delay
1240
+ [] // Dependencies for useCallback
1241
+ );
1242
+
1243
+ const handleInputChange = (e) => {
1244
+ const newValue = e.target.value;
1245
+ // Update the displayed value immediately
1246
+ setFilterValues(filter => ({ ...filter, [field.name]: newValue }));
1247
+ // Call the debounced function to apply the filter
1248
+ debouncedApplyFilter(newValue, isRegex);
1249
+ };
1250
+
1251
+ const handleToggleRegex = () => {
1252
+ const newIsRegex = !isRegex;
1253
+ setIsRegex(newIsRegex);
1254
+ // Re-apply filter with the new regex state and current value
1255
+ // The value from filterValues should be up-to-date
1256
+ const currentValue = filterValues[field.name] || '';
1257
+ debouncedApplyFilter(currentValue, newIsRegex);
1258
+ };
1259
+
1260
+ return (
1261
+ <>
1262
+ <TextField
1263
+ type="text"
1264
+ name={`filter_${field.name}`}
1265
+ value={filterValues[field.name] || ''} // Ensure controlled component with a default empty string
1266
+ placeholder={isRegex ? t("filterstringfield.placeholder.regex", "regular expression") : t("filterstringfield.placeholder", "...")}
1267
+ onChange={handleInputChange}
1268
+ maxLength={1000}
1269
+ />
1270
+ <button title={"regex"} className={isRegex ? 'active' : ''} onClick={handleToggleRegex}>.*</button>
1271
+ </>
1272
+ );
1273
+ };
1274
+ export const FilterField = ({advanced,model, reversed, field, active, onChangeFilterValue, filterValues, setFilterValues}) => {
1275
+ const { elementsPerPage, pagedSort, setPagedSort, setPage, page, pagedFilters, lockedColumns, setLockedColumns } = useModelContext();
1276
+ const {t} = useTranslation();
1277
+ const [locked, setLocked] = useState(lockedColumns.includes(field.name));
1278
+ const queryClient = useQueryClient()
1279
+
1280
+ useEffect(() => {
1281
+ if(!reversed) {
1282
+ setFilterValues(filter => ({...filter, [field.name]: ''}));
1283
+ onChangeFilterValue(field, '', true);
1284
+ }
1285
+ }, [field]);
1286
+
1287
+ const handleToggleLock = () => {
1288
+ if( locked ) {
1289
+ if (lockedColumns.includes(field.name))
1290
+ setLockedColumns(cols => [...cols].filter(f => f !== field.name));
1291
+ }else{
1292
+ if (!lockedColumns.includes(field.name))
1293
+ setLockedColumns(cols => [...cols, field.name]);
1294
+ }
1295
+ setLocked(!locked);
1296
+ }
1297
+
1298
+ const [reset, setReset] = useState(false);
1299
+ const handleChangeSort = (up) => {
1300
+ setPagedSort(sort => {
1301
+ const s = lockedColumns.length > 0 ? {...sort[model.name] || {}} : {};
1302
+ if( up ){
1303
+ if( reset ){
1304
+ delete s[field.name];
1305
+ setReset(false);
1306
+ }else {
1307
+ s[field.name] = 1;
1308
+ }
1309
+ }else{
1310
+ s[field.name] = -1;
1311
+ setReset(true);
1312
+ }
1313
+ return {...sort, [model.name]: s};
1314
+ });
1315
+ queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1316
+ }
1317
+
1318
+ const resetClass = pagedSort[model.name]?.[field.name] ? (((pagedSort[model.name]?.[field.name] === 1) || (pagedSort[model.name]?.[field.name] === -1)) ? 'active' : 'reset') : '';
1319
+
1320
+ const renderIconFromType =(field)=>{
1321
+ const type = field.type;
1322
+ if( type === 'color'){
1323
+ return <FaPallet/>;
1324
+ }
1325
+ if( type === 'code'){
1326
+ return <FaCode />;
1327
+ }
1328
+ else if( type === 'date'){
1329
+ return <FaCalendarWeek />;
1330
+ }else if( type === 'datetime'){
1331
+ return <FaCalendarDays />;
1332
+ }
1333
+ else if( type === 'richtext' || type === 'string' || type === 'string_t'){
1334
+ return <></>;
1335
+ }
1336
+ else if( type === 'url'){
1337
+ return <FaLink />;
1338
+ }
1339
+ else if( type === 'number'){
1340
+ return <FaHashtag />;
1341
+ }
1342
+ else if( type === 'file'){
1343
+ return <FaFile />;
1344
+ }
1345
+ else if( type === 'enum'){
1346
+ return <FaListUl />;
1347
+ }
1348
+ else if( type === 'boolean'){
1349
+ return <FaToggleOn />;
1350
+ }
1351
+ else if( type === 'image'){
1352
+ return <FaImage/>;
1353
+ }
1354
+ else if( type === 'relation'){
1355
+ return field.multiple ? <FaSitemap /> : <FaLink />;
1356
+ }
1357
+ else if( type === 'email'){
1358
+ return <FaAt />;
1359
+ }
1360
+ else if( type === 'phone'){
1361
+ return <FaPhone />;
1362
+ }
1363
+ else if( type === 'array'){
1364
+ return <FaTableColumns />;
1365
+ }
1366
+ return <FaPencil />
1367
+ }
1368
+ return <th key={field.name} className={`form filter-field`} style={{backgroundColor: field.color, color: !field.color ||isLightColor(field.color) ? 'black': "white"}}>
1369
+ <div className="flex flex-centered flex-mini-gap flex-row">
1370
+ <div className="flex flex-1 flex-mini-gap flex-no-wrap">
1371
+ {renderIconFromType(field)}
1372
+ <span title={field.name} className={"flex-1 title"}>{t(`field_${model.name}_${field.name}`, field.name)}</span>
1373
+ </div>
1374
+ {advanced && (<>
1375
+ { 'password'!==field.type && (<div className={"flex flex-no-gap"}>
1376
+ {(<>
1377
+ {(pagedSort[model.name]?.[field.name] !== 1) &&
1378
+ <button onClick={() => handleChangeSort(true)}
1379
+ className={resetClass}>
1380
+ {pagedSort[model.name]?.[field.name] === undefined ? <FaArrowDown/> : <FaArrowUp/>}</button>}
1381
+ {(pagedSort[model.name]?.[field.name] === 1) &&
1382
+ <button onClick={() => handleChangeSort(false)}
1383
+ className={resetClass}>
1384
+ <FaArrowDown/></button>}
1385
+ </>
1386
+ )}
1387
+ {!field.unique && (
1388
+ <button onClick={() => handleToggleLock()} className={locked ? 'active' : ''}><FaLock/></button>)}
1389
+ </div>)}
1390
+ {active && !['date','datetime','enum', 'boolean', 'number', 'password'].includes(field.type) && <div className="filter flex flex-no-wrap flex-mini-gap">
1391
+ <FilterStringField setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1392
+ </div>}
1393
+ {active && field.type === 'enum' && <div className="filter flex flex-no-wrap flex-mini-gap">
1394
+ <FilterEnumField model={model} setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1395
+ </div>}
1396
+ {active && field.type === 'boolean' && <div className="filter flex flex-no-wrap flex-mini-gap">
1397
+ <FilterBooleanField filterValues={filterValues} model={model} field={field} onChangeFilterValue={onChangeFilterValue} />
1398
+ </div>}
1399
+ {active && ['date', 'datetime'].includes(field.type) && <div className="filter flex flex-no-wrap flex-mini-gap">
1400
+ <FilterDateField filterValues={filterValues} model={model} field={field} onChangeFilterValue={onChangeFilterValue} />
1401
+ </div>}
1402
+ {active && field.type === 'number' && <div className="filter flex flex-no-wrap flex-mini-gap">
1403
+ <FilterNumberField model={model} setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1404
+ </div>}
1405
+ </>)}
1406
+ </div>
1407
+ </th>
1408
+ }
1409
+
1410
+ export const PhoneField = ({name, value, onChange}) => {
1411
+ const [phone, setPhone] = useState(value);
1412
+ useEffect(() => {
1413
+ setPhone(value);
1414
+ }, [value]);
1415
+ return (
1416
+ <div>
1417
+ <PhoneInput
1418
+ defaultCountry="ua"
1419
+ value={phone || ''}
1420
+ onChange={(phone) => {
1421
+ setPhone(phone);
1422
+ onChange?.(phone);
1423
+ }}
1424
+ />
1425
+ </div>
1426
+ );
1427
+ }
1428
+
1429
+ export const ModelField = ({field, formData, disableable=false, showModel=true, value, fields=false, onChange}) => {
1430
+ const {models} = useModelContext();
1431
+ const {me} = useAuthContext();
1432
+ const {t} = useTranslation();
1433
+ const [checked, setChecked] = useState(true);
1434
+
1435
+ // --- LOGIQUE AMÉLIORÉE POUR DÉTERMINER LE MODÈLE CIBLE ET LES VALEURS ---
1436
+ const hasTargetModel = !!field?.targetModel;
1437
+ let modelValue, fieldValue, targetModelName;
1438
+
1439
+ if (hasTargetModel) {
1440
+ // Le modèle est déterminé par un autre champ. La valeur de ce champ est juste le nom du champ (string).
1441
+ if (typeof field.targetModel === 'string' && field.targetModel.startsWith('$')) {
1442
+ const dynamicFieldName = field.targetModel.substring(1);
1443
+ targetModelName = formData?.[dynamicFieldName] || null;
1444
+ } else {
1445
+ targetModelName = field.targetModel;
1446
+ }
1447
+ modelValue = targetModelName;
1448
+ fieldValue = value;
1449
+ } else {
1450
+ // La valeur de ce champ contient le modèle et/ou le champ.
1451
+ if (fields) { // fields=true: on sélectionne un modèle ET un champ. La valeur est un objet.
1452
+ targetModelName = value?.model;
1453
+ modelValue = value?.model;
1454
+ fieldValue = value?.field;
1455
+ } else { // fields=false: on sélectionne seulement un modèle. La valeur est une chaîne.
1456
+ targetModelName = value;
1457
+ modelValue = value;
1458
+ fieldValue = undefined;
1459
+ }
1460
+ }
1461
+
1462
+ const selectedModel = models.find(m => m.name === targetModelName); // Note: Removed user check for broader compatibility with system models
1463
+
1464
+ // Préparer les options pour les champs du modèle
1465
+ const fieldOptions = selectedModel?.fields.map(f => ({
1466
+ label: t(`field_${selectedModel.name}_${f.name}`, f.name),
1467
+ value: f.name
1468
+ })) || [];
1469
+
1470
+ // Effet pour réinitialiser le champ si le modèle cible change et que le champ actuel n'est plus valide
1471
+ useEffect(() => {
1472
+ if (hasTargetModel) {
1473
+ const isValid = fieldOptions.some(opt => opt.value === fieldValue);
1474
+ if (!isValid) {
1475
+ const newValue = fieldOptions.length > 0 ? fieldOptions[0].value : null;
1476
+ if (fieldValue !== newValue) {
1477
+ onChange({ name: field.name, value: newValue });
1478
+ }
1479
+ }
1480
+ }
1481
+ }, [targetModelName]); // Se déclenche quand le nom du modèle cible change
1482
+
1483
+ // Gestion du changement de modèle (uniquement si pas de targetModel)
1484
+ const handleModelChange = (e) => {
1485
+ const newModelName = e.value;
1486
+ const newModel = models.find(m => m.name === newModelName);
1487
+ const firstField = newModel?.fields[0]?.name || null;
1488
+
1489
+ if (fields) {
1490
+ onChange({name: field?.name, value: { model: newModelName, field: firstField }});
1491
+ } else {
1492
+ onChange({name: field?.name, value: newModelName});
1493
+ }
1494
+ };
1495
+
1496
+ // Gestion du changement de champ
1497
+ const handleFieldChange = (e) => {
1498
+ const newFieldName = e.value;
1499
+ if (hasTargetModel) {
1500
+ onChange({ name: field.name, value: newFieldName });
1501
+ } else {
1502
+ onChange({name: field?.name, value: { model: modelValue, field: newFieldName }});
1503
+ }
1504
+ };
1505
+
1506
+ const dis = disableable ? (<CheckboxField
1507
+ checked={checked}
1508
+ onChange={e => {
1509
+ setChecked(e);
1510
+ if (!e) {
1511
+ onChange({name: field?.name, value: null});
1512
+ }
1513
+ }}
1514
+ />) : null;
1515
+
1516
+ if (!fields) {
1517
+ return (<div className="flex flex-1">
1518
+ {dis}
1519
+ {checked && (<SelectField
1520
+ className="flex-1"
1521
+ value={modelValue}
1522
+ onChange={handleModelChange}
1523
+ items={models
1524
+ .filter(m => m._user === me?.username) // Keep user filter for selection
1525
+ .map(m => ({
1526
+ label: t(`model_${m.name}`, m.name),
1527
+ value: m.name
1528
+ }))}
1529
+ />)}
1530
+ </div>);
1531
+ }
1532
+
1533
+ return (<div className="flex flex-1">
1534
+ {dis}
1535
+ {checked && (<div className="flex flex-stretch" key={field?.name ?? 'def'}>
1536
+ {showModel && (<SelectField
1537
+ className="flex-1"
1538
+ value={modelValue}
1539
+ onChange={handleModelChange}
1540
+ items={models
1541
+ .filter(m => m._user === me?.username) // Keep user filter for selection
1542
+ .map(m => ({
1543
+ label: t(`model_${m.name}`, m.name),
1544
+ value: m.name
1545
+ }))}
1546
+ disabled={hasTargetModel} // Le sélecteur de modèle est désactivé si le modèle est piloté par un autre champ
1547
+ />)}
1548
+ <SelectField
1549
+ className="flex-1"
1550
+ value={fieldValue}
1551
+ onChange={handleFieldChange}
1552
+ items={fieldOptions}
1553
+ disabled={!targetModelName}
1554
+ />
1555
+ </div>)}
1556
+ </div>);
1557
+ };
1558
+
1559
+ // Fonction pour obtenir le composant icône par son nom
1560
+ const getIconComponent = (iconName) => {
1561
+ if (!iconName) return null;
1562
+ const IconComponent = FaIcons[iconName] || Fa6Icons[iconName];
1563
+ return IconComponent ? <IconComponent /> : null; // Retourne l'élément React ou null
1564
+ };
1565
+ export const IconField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1566
+ const { t } = useTranslation();
1567
+ const [iconSuggestions, setIconSuggestions] = useState([]);
1568
+ // Tri alphabétique pour une recherche plus prévisible
1569
+ const [allFaIcons] = useState(() => [...Object.keys(FaIcons), ...Object.keys(Fa6Icons)].sort());
1570
+
1571
+ const handleIconChange = (e) => {
1572
+ const value = e.target.value;
1573
+ onChange(value);
1574
+ if (value) {
1575
+ const filtered = allFaIcons.filter(
1576
+ icon => icon.toLowerCase().includes(value.toLowerCase())
1577
+ );
1578
+ setIconSuggestions(filtered.slice(0, 20));
1579
+ } else {
1580
+ setIconSuggestions([]);
1581
+ }
1582
+ };
1583
+
1584
+ const handleIconFocus = () => {
1585
+ if (value) {
1586
+ const filtered = allFaIcons.filter(
1587
+ icon => icon.toLowerCase().includes(value.toLowerCase())
1588
+ );
1589
+ setIconSuggestions(filtered.slice(0, 20));
1590
+ } else {
1591
+ setIconSuggestions(allFaIcons.slice(0, 10));
1592
+ }
1593
+ };
1594
+
1595
+ const onSuggestionClick = (suggestion) => {
1596
+ onChange(suggestion);
1597
+ setIconSuggestions([]);
1598
+ };
1599
+
1600
+ return <div className="textfield-wrapper with-suggestions">
1601
+ <div className={"flex flex-1 flex-no-wrap"}>
1602
+ <TextField
1603
+ help={t('modelcreator.field.icon')}
1604
+ id="modelIcon"
1605
+ disabled={disabled}
1606
+ value={value}
1607
+ label={label}
1608
+ before={<div>{getIconComponent(value)}</div>}
1609
+ onChange={handleIconChange}
1610
+ onFocus={handleIconFocus}
1611
+ onBlur={() => setTimeout(() => setIconSuggestions([]), 200)}
1612
+ autoComplete="off"
1613
+ />
1614
+ </div>
1615
+ {iconSuggestions.length > 0 && (
1616
+ <ul className="suggestions-list">
1617
+ {iconSuggestions.map(icon => (
1618
+ <li key={icon} onMouseDown={() => onSuggestionClick(icon)}>
1619
+ <span className="suggestion-icon">{getIconComponent(icon)}</span>
1620
+ <span>{icon}</span>
1621
+ </li>
1622
+ ))}
1623
+ </ul>
1624
+ )}
1625
+ </div>
1626
+ };
1627
+
1628
+ export const ColorField = ({ name, label, value, disabled, onChange, className, ...rest }) => {
1629
+ const [displayColorPicker, setDisplayColorPicker] = useState(false);
1630
+
1631
+ const handleClick = () => {
1632
+ if (!disabled) {
1633
+ setDisplayColorPicker(!displayColorPicker);
1634
+ }
1635
+ };
1636
+
1637
+ const handleClose = () => {
1638
+ setDisplayColorPicker(false);
1639
+ };
1640
+
1641
+ const handleChange = (color) => {
1642
+ // react-color nous donne un objet avec tous les formats.
1643
+ // On utilise tinycolor pour le convertir au format canonique attendu par le backend (#RRGGBBAA).
1644
+ const newColor = tinycolor(color.rgb); // color.rgb contient {r, g, b, a}
1645
+ onChange?.({ name, value: newColor.toHex8String().toUpperCase() });
1646
+ };
1647
+
1648
+ const color = tinycolor(value || '#FFFFFFFF');
1649
+ const swatchStyle = {
1650
+ background: color.toRgbString(),
1651
+ width: '100%',
1652
+ minWidth: '52px',
1653
+ height: '36px',
1654
+ borderRadius: '2px',
1655
+ border: '1px solid #ccc',
1656
+ cursor: disabled ? 'not-allowed' : 'pointer',
1657
+ };
1658
+
1659
+ return (
1660
+ <div className={`flex flex-1 flex-no-wrap ${className || ''}`}>
1661
+ {label && (<label className="flex-1 mb-1">{label}</label>)}
1662
+ <div style={swatchStyle} onClick={handleClick}>&nbsp;</div>
1663
+ <div className={"flex-1"}>{value}</div>
1664
+ {displayColorPicker ? (
1665
+ <div style={{ position: 'absolute', zIndex: '2' }}>
1666
+ <div style={{ position: 'fixed', top: '0px', right: '0px', bottom: '0px', left: '0px' }} onClick={handleClose} />
1667
+ <SketchPicker color={value || '#FFFFFFFF'} onChange={handleChange} />
1668
+ </div>
1669
+ ) : null}
1670
+ </div>
1671
+ );
1672
+ };
1673
+
1674
+ const secondsToDuration = (totalSeconds) => {
1675
+ if (totalSeconds === null || totalSeconds === undefined || isNaN(totalSeconds) || totalSeconds === '') {
1676
+ return { days: '', hours: '', minutes: '', seconds: '' };
1677
+ }
1678
+ const total = parseInt(totalSeconds, 10);
1679
+ const d = Math.floor(total / 86400);
1680
+ let remainder = total % 86400;
1681
+ const h = Math.floor(remainder / 3600);
1682
+ remainder %= 3600;
1683
+ const m = Math.floor(remainder / 60);
1684
+ const s = remainder % 60;
1685
+ return { days: d, hours: h, minutes: m, seconds: s };
1686
+ };
1687
+
1688
+ const durationToSeconds = ({ days, hours, minutes, seconds }) => {
1689
+ return (parseInt(days, 10) || 0) * 86400 +
1690
+ (parseInt(hours, 10) || 0) * 3600 +
1691
+ (parseInt(minutes, 10) || 0) * 60 +
1692
+ (parseInt(seconds, 10) || 0);
1693
+ };
1694
+
1695
+ export const DurationField = forwardRef(({ value, onChange, name, label, help, required, editable, readOnly }, ref) => {
1696
+ const { t } = useTranslation();
1697
+ const [duration, setDuration] = useState(secondsToDuration(value));
1698
+ const [errors, setErrors] = useState([]);
1699
+
1700
+ useEffect(() => {
1701
+ setDuration(secondsToDuration(value));
1702
+ }, [value]);
1703
+
1704
+ const validate = () => {
1705
+ const errs = [];
1706
+ const totalSeconds = durationToSeconds(duration);
1707
+ if (required && totalSeconds <= 0) {
1708
+ errs.push(t('form.validation.required', "Field required"));
1709
+ }
1710
+ setErrors(errs);
1711
+ return !errs.length;
1712
+ };
1713
+
1714
+ useImperativeHandle(ref, () => ({
1715
+ validate,
1716
+ getValue: () => durationToSeconds(duration),
1717
+ }));
1718
+
1719
+ const handlePartChange = (part) => (e) => {
1720
+ const newDuration = { ...duration, [part]: e.target.value };
1721
+ setDuration(newDuration);
1722
+ if (onChange) {
1723
+ const totalSeconds = durationToSeconds(newDuration);
1724
+ onChange({ name, value: totalSeconds });
1725
+ }
1726
+ };
1727
+
1728
+ return (
1729
+ <>
1730
+ <div className={cn({ field: true, "field-duration": true, 'flex-1': true, flex: true, "field-bg": true })}>
1731
+ {label && (
1732
+ <label contentEditable={editable} className={cn({ help: !!help, 'flex-1': true })}>
1733
+ {label}
1734
+ {required && <span className="mandatory" contentEditable={false}>*</span>}
1735
+ </label>
1736
+ )}
1737
+ {help && <div className="flex help">{help}</div>}
1738
+ <div className="duration-inputs flex flex-no-wrap flex-mini-gap">
1739
+ <NumberField name={`${name}-days`} unit={t('duration.unit.days', 'days')} value={duration.days} onChange={handlePartChange('days')} readOnly={readOnly} min={0} />
1740
+ <NumberField name={`${name}-hours`} unit={t('duration.unit.hours', 'hours')} value={duration.hours} onChange={handlePartChange('hours')} readOnly={readOnly} min={0} max={23} />
1741
+ <NumberField name={`${name}-minutes`} unit={t('duration.unit.minutes', 'minutes')} value={duration.minutes} onChange={handlePartChange('minutes')} readOnly={readOnly} min={0} max={59} />
1742
+ <NumberField name={`${name}-seconds`} unit={t('duration.unit.seconds', 'seconds')} value={duration.seconds} onChange={handlePartChange('seconds')} readOnly={readOnly} min={0} max={59} />
1743
+ </div>
1744
+ </div>
1745
+ {errors.length > 0 && (
1746
+ <ul className="error">
1747
+ {errors.map((e, key) => (
1748
+ <li key={key} aria-live="assertive" role="alert">{e}</li>
1749
+ ))}
1750
+ </ul>
1751
+ )}
1752
+ </>
1753
+ );
1754
+ });
1755
+ DurationField.displayName = "DurationField";
1756
+
1757
+ export const CodeField = ({name, label, language, value, disabled, onChange}) => {
1758
+ const u = name || uniqid();
1759
+ const [currentEditor, setEditor] = useState(null);
1760
+
1761
+ return <>
1762
+ {label && (<label className="flex flex-1">{label}</label>)}
1763
+ {!disabled ? <div className={"codefield"}><span><b>{language}</b> : </span><CodeiumEditor
1764
+ language={language || 'json'}
1765
+ theme={"vs-dark"}
1766
+ value={value}
1767
+ onChange={e => {
1768
+ if (language === 'json') {
1769
+ let code;
1770
+ try {
1771
+ code = JSON.parse(e);
1772
+ onChange({name, value: code});
1773
+ } catch (e) {
1774
+ }
1775
+ } else
1776
+ onChange({name, value: e});
1777
+ }}
1778
+ height="300px"
1779
+ /></div> : <div className="code"><SyntaxHighlighter
1780
+ language={language || "javascript"}>{value}</SyntaxHighlighter></div>
1781
+ }</>
1782
+ }
1783
+
1784
+ export const EnumField = ({inputProps, value, handleChange, field}) => {
1785
+ const { t} = useTranslation()
1786
+ useEffect(() => {
1787
+ if( field.items.includes(value))
1788
+ handleChange(value);
1789
+ else{
1790
+ handleChange({name: field.name, value: field.items[0]})
1791
+ }
1792
+ }, []);
1793
+ return (
1794
+ <select {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} >{(field.items || []).map(item => {
1795
+ if( typeof(item) === 'string'){
1796
+ return <option value={item}>{t(item, item)}</option>;
1797
+ }
1798
+ return <></>
1799
+ })}</select>
1800
+ );
1801
+ }
1802
+
1803
+ export const RangeField = ({ name, value, onChange, min = 0, max = 100, step = 1, percent = false }) => {
1804
+ const handleChange = (e) => {
1805
+ // The onChange from the form probably expects the field name and value
1806
+ onChange(parseFloat(e.target.value));
1807
+ };
1808
+
1809
+ const percentage = max > min ? Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100)) : 0;
1810
+ const displayValue = percent ? `${Math.round(percentage)}%` : value;
1811
+
1812
+ return (
1813
+ <div className="range-field">
1814
+ <input
1815
+ type="range"
1816
+ name={name}
1817
+ value={value || 0}
1818
+ onChange={handleChange}
1819
+ min={min}
1820
+ max={max}
1821
+ step={step}
1822
+ />
1823
+ <span className="range-value">{displayValue}</span>
1824
+ </div>
1825
+ );
1789
1826
  };