data-primals-engine 1.4.1 → 1.4.2

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,1783 +1,1785 @@
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
- );
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 = ({advanced,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
+ {advanced && (<>
1369
+ { 'password'!==field.type && (<div className={"flex flex-no-gap"}>
1370
+ {(<>
1371
+ {(pagedSort[model.name]?.[field.name] !== 1) &&
1372
+ <button onClick={() => handleChangeSort(true)}
1373
+ className={resetClass}>
1374
+ {pagedSort[model.name]?.[field.name] === undefined ? <FaArrowDown/> : <FaArrowUp/>}</button>}
1375
+ {(pagedSort[model.name]?.[field.name] === 1) &&
1376
+ <button onClick={() => handleChangeSort(false)}
1377
+ className={resetClass}>
1378
+ <FaArrowDown/></button>}
1379
+ </>
1380
+ )}
1381
+ {!field.unique && (
1382
+ <button onClick={() => handleToggleLock()} className={locked ? 'active' : ''}><FaLock/></button>)}
1383
+ </div>)}
1384
+ {active && !['date','datetime','enum', 'boolean', 'number', 'password'].includes(field.type) && <div className="filter flex flex-no-wrap flex-mini-gap">
1385
+ <FilterStringField setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1386
+ </div>}
1387
+ {active && field.type === 'enum' && <div className="filter flex flex-no-wrap flex-mini-gap">
1388
+ <FilterEnumField model={model} setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1389
+ </div>}
1390
+ {active && field.type === 'boolean' && <div className="filter flex flex-no-wrap flex-mini-gap">
1391
+ <FilterBooleanField filterValues={filterValues} model={model} field={field} onChangeFilterValue={onChangeFilterValue} />
1392
+ </div>}
1393
+ {active && ['date', 'datetime'].includes(field.type) && <div className="filter flex flex-no-wrap flex-mini-gap">
1394
+ <FilterDateField filterValues={filterValues} model={model} field={field} onChangeFilterValue={onChangeFilterValue} />
1395
+ </div>}
1396
+ {active && field.type === 'number' && <div className="filter flex flex-no-wrap flex-mini-gap">
1397
+ <FilterNumberField model={model} setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1398
+ </div>}
1399
+ </>)}
1400
+ </div>
1401
+ </th>
1402
+ }
1403
+
1404
+ export const PhoneField = ({name, value, onChange}) => {
1405
+ const [phone, setPhone] = useState(value);
1406
+ useEffect(() => {
1407
+ setPhone(value);
1408
+ }, [value]);
1409
+ return (
1410
+ <div>
1411
+ <PhoneInput
1412
+ defaultCountry="ua"
1413
+ value={phone || ''}
1414
+ onChange={(phone) => {
1415
+ setPhone(phone);
1416
+ onChange?.(phone);
1417
+ }}
1418
+ />
1419
+ </div>
1420
+ );
1421
+ }
1422
+
1423
+ export const ModelField = ({field, disableable=false, showModel=true, value, fieldValue, fields=false, onChange}) => {
1424
+ const {models} = useModelContext();
1425
+ const {me} = useAuthContext();
1426
+ const {t} = useTranslation()
1427
+ const [checked, setChecked] = useState(true);
1428
+
1429
+ // Trouver le modèle correspondant à la valeur
1430
+ const selectedModel = models.find(m => m.name === value && m._user === me?.username);
1431
+
1432
+ // Préparer les options pour les champs du modèle
1433
+ const fieldOptions = selectedModel?.fields.map(f => ({
1434
+ label: t(`field_${f.name}`, f.name),
1435
+ value: f.name
1436
+ })) || [];
1437
+
1438
+ // Gestion du changement de modèle
1439
+ const handleModelChange = (e) => {
1440
+ const newModel = e.value;
1441
+ const firstField = fieldOptions[0]?.value || null;
1442
+
1443
+ if (fields) {
1444
+ onChange({name: field?.name, value: { model: newModel, field: firstField }});
1445
+ } else {
1446
+ onChange({name: field?.name, value: newModel});
1447
+ }
1448
+ };
1449
+
1450
+ // Gestion du changement de champ
1451
+ const handleFieldChange = (e) => {
1452
+ onChange({name: field?.name, value: { model: value, field: e.value }});
1453
+ };
1454
+
1455
+ const dis = disableable ? (
1456
+ <CheckboxField
1457
+ checked={checked}
1458
+ onChange={e => {
1459
+ setChecked(e);
1460
+ if (!e) {
1461
+ onChange({name: field?.name, value: null});
1462
+ }
1463
+ }}
1464
+ />
1465
+ ) : null;
1466
+
1467
+ if (!fields) {
1468
+ return (
1469
+ <div className="flex flex-1">
1470
+ {dis}
1471
+ {checked && (
1472
+ <SelectField
1473
+ className="flex-1"
1474
+ value={value}
1475
+ onChange={handleModelChange}
1476
+ items={models
1477
+ .filter(m => m._user === me?.username)
1478
+ .map(m => ({
1479
+ label: t(`model_${m.name}`, m.name),
1480
+ value: m.name
1481
+ }))
1482
+ }
1483
+ />
1484
+ )}
1485
+ </div>
1486
+ );
1487
+ }
1488
+
1489
+ return (
1490
+ <div className="flex flex-1">
1491
+ {dis}
1492
+ {checked && (
1493
+ <div className="flex flex-stretch" key={field?.name ?? 'def'}>
1494
+ {showModel && (
1495
+ <SelectField
1496
+ className="flex-1"
1497
+ value={value}
1498
+ onChange={handleModelChange}
1499
+ items={models
1500
+ .filter(m => m._user === me?.username)
1501
+ .map(m => ({
1502
+ label: t(`model_${m.name}`, m.name),
1503
+ value: m.name
1504
+ }))
1505
+ }
1506
+ />
1507
+ )}
1508
+ <SelectField
1509
+ className="flex-1"
1510
+ value={fieldValue || (fieldOptions[0]?.value || null)}
1511
+ onChange={handleFieldChange}
1512
+ items={fieldOptions}
1513
+ />
1514
+ </div>
1515
+ )}
1516
+ </div>
1517
+ );
1518
+ };
1519
+
1520
+ // Fonction pour obtenir le composant icône par son nom
1521
+ const getIconComponent = (iconName) => {
1522
+ if (!iconName) return null;
1523
+ const IconComponent = FaIcons[iconName] || Fa6Icons[iconName];
1524
+ return IconComponent ? <IconComponent /> : null; // Retourne l'élément React ou null
1525
+ };
1526
+ export const IconField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1527
+ const { t } = useTranslation();
1528
+ const [iconSuggestions, setIconSuggestions] = useState([]);
1529
+ // Tri alphabétique pour une recherche plus prévisible
1530
+ const [allFaIcons] = useState(() => [...Object.keys(FaIcons), ...Object.keys(Fa6Icons)].sort());
1531
+
1532
+ const handleIconChange = (e) => {
1533
+ const value = e.target.value;
1534
+ onChange(value);
1535
+ if (value) {
1536
+ const filtered = allFaIcons.filter(
1537
+ icon => icon.toLowerCase().includes(value.toLowerCase())
1538
+ );
1539
+ setIconSuggestions(filtered.slice(0, 20));
1540
+ } else {
1541
+ setIconSuggestions([]);
1542
+ }
1543
+ };
1544
+
1545
+ const handleIconFocus = () => {
1546
+ if (value) {
1547
+ const filtered = allFaIcons.filter(
1548
+ icon => icon.toLowerCase().includes(value.toLowerCase())
1549
+ );
1550
+ setIconSuggestions(filtered.slice(0, 20));
1551
+ } else {
1552
+ setIconSuggestions(allFaIcons.slice(0, 10));
1553
+ }
1554
+ };
1555
+
1556
+ const onSuggestionClick = (suggestion) => {
1557
+ onChange(suggestion);
1558
+ setIconSuggestions([]);
1559
+ };
1560
+
1561
+ return <div className="textfield-wrapper with-suggestions">
1562
+ <div className={"flex flex-1 flex-no-wrap"}>
1563
+ <TextField
1564
+ help={t('modelcreator.field.icon')}
1565
+ id="modelIcon"
1566
+ disabled={disabled}
1567
+ value={value}
1568
+ label={label}
1569
+ before={<div>{getIconComponent(value)}</div>}
1570
+ onChange={handleIconChange}
1571
+ onFocus={handleIconFocus}
1572
+ onBlur={() => setTimeout(() => setIconSuggestions([]), 200)}
1573
+ autoComplete="off"
1574
+ />
1575
+ </div>
1576
+ {iconSuggestions.length > 0 && (
1577
+ <ul className="suggestions-list">
1578
+ {iconSuggestions.map(icon => (
1579
+ <li key={icon} onMouseDown={() => onSuggestionClick(icon)}>
1580
+ <span className="suggestion-icon">{getIconComponent(icon)}</span>
1581
+ <span>{icon}</span>
1582
+ </li>
1583
+ ))}
1584
+ </ul>
1585
+ )}
1586
+ </div>
1587
+ };
1588
+ export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1589
+ // 1. État interne pour une réactivité immédiate de l'interface.
1590
+ const [internalValue, setInternalValue] = useState(value);
1591
+
1592
+ // 2. On mémoïze le gestionnaire d'événements avec debounce pour éviter de le recréer à chaque rendu.
1593
+ const debouncedOnChange = useCallback(
1594
+ debounce((newValue) => {
1595
+ // On notifie le parent du changement après un court délai.
1596
+ onChange?.({ name, value: newValue });
1597
+ }, 200), // Un délai de 200ms est confortable pour un sélecteur de couleur.
1598
+ [onChange, name] // Dépendances de useCallback
1599
+ );
1600
+
1601
+ // 3. Effet pour synchroniser l'état interne si la prop `value` du parent change.
1602
+ useEffect(() => {
1603
+ if (value !== internalValue) {
1604
+ setInternalValue(value);
1605
+ }
1606
+ }, [value]);
1607
+
1608
+ const handleChange = (e) => {
1609
+ const newValue = e.target.value;
1610
+ // Met à jour l'état interne instantanément pour que l'input soit réactif.
1611
+ setInternalValue(newValue);
1612
+ // Appelle la fonction "debounced" pour notifier le parent.
1613
+ debouncedOnChange(newValue);
1614
+ };
1615
+
1616
+ return (
1617
+ <div className={`flex flex-1 flex-no-wrap ${className || ''}`}>
1618
+ {label && (<label className="flex-1">{label}</label>)}
1619
+ <div className="flex flex-1 flex-no-wrap"><input
1620
+ disabled={disabled}
1621
+ type="color"
1622
+ // L'input est maintenant contrôlé par notre état interne.
1623
+ value={internalValue || '#FFFFFF'}
1624
+ onChange={handleChange}
1625
+ {...rest}
1626
+ />
1627
+ <span className="color-value">{internalValue || '#FFFFFF'}</span>
1628
+ </div>
1629
+ </div>
1630
+ );
1631
+ };
1632
+
1633
+ const secondsToDuration = (totalSeconds) => {
1634
+ if (totalSeconds === null || totalSeconds === undefined || isNaN(totalSeconds) || totalSeconds === '') {
1635
+ return { days: '', hours: '', minutes: '', seconds: '' };
1636
+ }
1637
+ const total = parseInt(totalSeconds, 10);
1638
+ const d = Math.floor(total / 86400);
1639
+ let remainder = total % 86400;
1640
+ const h = Math.floor(remainder / 3600);
1641
+ remainder %= 3600;
1642
+ const m = Math.floor(remainder / 60);
1643
+ const s = remainder % 60;
1644
+ return { days: d, hours: h, minutes: m, seconds: s };
1645
+ };
1646
+
1647
+ const durationToSeconds = ({ days, hours, minutes, seconds }) => {
1648
+ return (parseInt(days, 10) || 0) * 86400 +
1649
+ (parseInt(hours, 10) || 0) * 3600 +
1650
+ (parseInt(minutes, 10) || 0) * 60 +
1651
+ (parseInt(seconds, 10) || 0);
1652
+ };
1653
+
1654
+ export const DurationField = forwardRef(({ value, onChange, name, label, help, required, editable, readOnly }, ref) => {
1655
+ const { t } = useTranslation();
1656
+ const [duration, setDuration] = useState(secondsToDuration(value));
1657
+ const [errors, setErrors] = useState([]);
1658
+
1659
+ useEffect(() => {
1660
+ setDuration(secondsToDuration(value));
1661
+ }, [value]);
1662
+
1663
+ const validate = () => {
1664
+ const errs = [];
1665
+ const totalSeconds = durationToSeconds(duration);
1666
+ if (required && totalSeconds <= 0) {
1667
+ errs.push(t('form.validation.required', "Field required"));
1668
+ }
1669
+ setErrors(errs);
1670
+ return !errs.length;
1671
+ };
1672
+
1673
+ useImperativeHandle(ref, () => ({
1674
+ validate,
1675
+ getValue: () => durationToSeconds(duration),
1676
+ }));
1677
+
1678
+ const handlePartChange = (part) => (e) => {
1679
+ const newDuration = { ...duration, [part]: e.target.value };
1680
+ setDuration(newDuration);
1681
+ if (onChange) {
1682
+ const totalSeconds = durationToSeconds(newDuration);
1683
+ onChange({ name, value: totalSeconds });
1684
+ }
1685
+ };
1686
+
1687
+ return (
1688
+ <>
1689
+ <div className={cn({ field: true, "field-duration": true, 'flex-1': true, flex: true, "field-bg": true })}>
1690
+ {label && (
1691
+ <label contentEditable={editable} className={cn({ help: !!help, 'flex-1': true })}>
1692
+ {label}
1693
+ {required && <span className="mandatory" contentEditable={false}>*</span>}
1694
+ </label>
1695
+ )}
1696
+ {help && <div className="flex help">{help}</div>}
1697
+ <div className="duration-inputs flex flex-no-wrap flex-mini-gap">
1698
+ <NumberField name={`${name}-days`} unit={t('duration.unit.days', 'days')} value={duration.days} onChange={handlePartChange('days')} readOnly={readOnly} min={0} />
1699
+ <NumberField name={`${name}-hours`} unit={t('duration.unit.hours', 'hours')} value={duration.hours} onChange={handlePartChange('hours')} readOnly={readOnly} min={0} max={23} />
1700
+ <NumberField name={`${name}-minutes`} unit={t('duration.unit.minutes', 'minutes')} value={duration.minutes} onChange={handlePartChange('minutes')} readOnly={readOnly} min={0} max={59} />
1701
+ <NumberField name={`${name}-seconds`} unit={t('duration.unit.seconds', 'seconds')} value={duration.seconds} onChange={handlePartChange('seconds')} readOnly={readOnly} min={0} max={59} />
1702
+ </div>
1703
+ </div>
1704
+ {errors.length > 0 && (
1705
+ <ul className="error">
1706
+ {errors.map((e, key) => (
1707
+ <li key={key} aria-live="assertive" role="alert">{e}</li>
1708
+ ))}
1709
+ </ul>
1710
+ )}
1711
+ </>
1712
+ );
1713
+ });
1714
+ DurationField.displayName = "DurationField";
1715
+
1716
+ export const CodeField = ({name, label, language, value, disabled, onChange}) => {
1717
+ const u = name || uniqid();
1718
+ const [currentEditor, setEditor] = useState(null);
1719
+
1720
+ return <>
1721
+ {label && (<label className="flex flex-1">{label}</label>)}
1722
+ {!disabled ? <div className={"codefield"}><span><b>{language}</b> : </span><CodeiumEditor
1723
+ language={language || 'json'}
1724
+ theme={"vs-dark"}
1725
+ value={value}
1726
+ onChange={e => {
1727
+ if (language === 'json') {
1728
+ let code;
1729
+ try {
1730
+ code = JSON.parse(e);
1731
+ onChange({name, value: code});
1732
+ } catch (e) {
1733
+ }
1734
+ } else
1735
+ onChange({name, value: e});
1736
+ }}
1737
+ height="300px"
1738
+ /></div> : <div className="code"><SyntaxHighlighter
1739
+ language={language || "javascript"} theme={docco}>{value}</SyntaxHighlighter></div>
1740
+ }</>
1741
+ }
1742
+
1743
+ export const EnumField = ({inputProps, value, handleChange, field}) => {
1744
+ const { t} = useTranslation()
1745
+ useEffect(() => {
1746
+ if( field.items.includes(value))
1747
+ handleChange(value);
1748
+ else{
1749
+ handleChange({name: field.name, value: field.items[0]})
1750
+ }
1751
+ }, []);
1752
+ return (
1753
+ <select {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} >{(field.items || []).map(item => {
1754
+ if( typeof(item) === 'string'){
1755
+ return <option value={item}>{t(item, item)}</option>;
1756
+ }
1757
+ return <></>
1758
+ })}</select>
1759
+ );
1760
+ }
1761
+
1762
+ export const RangeField = ({ name, value, onChange, min = 0, max = 100, step = 1, percent = false }) => {
1763
+ const handleChange = (e) => {
1764
+ // The onChange from the form probably expects the field name and value
1765
+ onChange(parseFloat(e.target.value));
1766
+ };
1767
+
1768
+ const percentage = max > min ? Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100)) : 0;
1769
+ const displayValue = percent ? `${Math.round(percentage)}%` : value;
1770
+
1771
+ return (
1772
+ <div className="range-field">
1773
+ <input
1774
+ type="range"
1775
+ name={name}
1776
+ value={value || 0}
1777
+ onChange={handleChange}
1778
+ min={min}
1779
+ max={max}
1780
+ step={step}
1781
+ />
1782
+ <span className="range-value">{displayValue}</span>
1783
+ </div>
1784
+ );
1783
1785
  };