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