data-primals-engine 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +5390 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. package/vitest.config.js +2 -2
@@ -0,0 +1,1558 @@
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 "data-primals-engine/core";
15
+ import {mainFieldsTypes, maxFileSize} from "data-primals-engine/constants";
16
+ import {useModelContext} from "./contexts/ModelContext.jsx";
17
+ import {useQueryClient} from "react-query";
18
+ import {
19
+ FaArrowDown,
20
+ FaArrowUp, FaAt,
21
+ FaCalendar, FaCalendarWeek, FaCode, FaFile,
22
+ FaHashtag,
23
+ FaImage,
24
+ FaLink, FaListOl, FaListUl,
25
+ FaLock, FaMinus,
26
+ FaPallet, FaPhone, FaSitemap,
27
+ FaToggleOn
28
+ } from "react-icons/fa";
29
+ import {FaCalendarDays, FaCodeCompare, FaPencil, FaT, FaTableColumns} from "react-icons/fa6";
30
+ import { CodeBlock, tomorrowNightBright } from 'react-code-blocks';
31
+ import SyntaxHighlighter from 'react-syntax-highlighter';
32
+ import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
33
+
34
+ import { PhoneInput } from 'react-international-phone';
35
+ import 'react-international-phone/style.css';
36
+ import CodeMirror, {basicSetup} from "@uiw/react-codemirror";
37
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
38
+
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
+ ...rest
98
+ },
99
+ ref,
100
+ ) {
101
+ const [id, setId] = useState("textfield-" + uniqid());
102
+ const [errors, setErrors] = useState([]);
103
+ const inputRef = useRef();
104
+
105
+ const mult = typeof multiline !== 'undefined' ? multiline : maxlength > 255;
106
+ const validate = () => {
107
+ const errs = [];
108
+ if (required && (!value || value.trim() === "")) {
109
+ errs.push("Field required");
110
+ }
111
+ if (
112
+ minlength > 0 &&
113
+ typeof value == "string" &&
114
+ value.trim().length < minlength
115
+ ) {
116
+ errs.push("Value length must be >= to " + minlength);
117
+ }
118
+ if (
119
+ maxlength !== undefined && maxlength > 0 &&
120
+ typeof value == "string" &&
121
+ value.trim().length > maxlength
122
+ ) {
123
+ errs.push("Value length must be <= to " + maxlength);
124
+ }
125
+ if( showErrors )
126
+ setErrors(errs);
127
+ return !errs.length;
128
+ };
129
+ useImperativeHandle(ref, () => ({
130
+ ref: inputRef.current,
131
+ validate,
132
+ getValue: () => value,
133
+ }));
134
+ const handleChange = (e) => {
135
+ if (onChange) {
136
+ onChange(e);
137
+ }
138
+ };
139
+ useEffect(() => {
140
+ if (value !== null) validate();
141
+ }, [value]);
142
+ return (
143
+ <>
144
+ <div
145
+ className={cn({
146
+ field: true,
147
+ flex: true,
148
+ "field-text": !mult,
149
+ "field-multiline": mult,
150
+ })}
151
+ >
152
+ {label && (
153
+ <label
154
+ contentEditable={editable}
155
+ className={cn({help: !!help, 'flex-1': true})}
156
+ htmlFor={id}
157
+ {...labelProps}
158
+ >
159
+ {label}
160
+ {required ? (
161
+ <span className="mandatory" contentEditable={false}>
162
+ *
163
+ </span>
164
+ ) : (
165
+ ""
166
+ )}
167
+ </label>
168
+ )}
169
+
170
+ {help &&<div className="flex help">{help}</div>}
171
+
172
+ {mult && (
173
+ <textarea
174
+ ref={inputRef}
175
+ aria-required={required}
176
+ aria-readonly={readOnly}
177
+ readOnly={readOnly}
178
+ placeholder={placeholder}
179
+ id={id}
180
+ name={name}
181
+ value={value || ""}
182
+ rows={8}
183
+ onChange={handleChange}
184
+ minLength={minlength}
185
+ maxLength={maxlength}
186
+ {...rest}
187
+ ></textarea>
188
+ )}
189
+
190
+ <div className={"flex flex-row flex-1 flex-start"}>
191
+ {!mult && (
192
+ <input
193
+ ref={inputRef}
194
+ aria-required={required}
195
+ aria-readonly={readOnly}
196
+ readOnly={readOnly}
197
+ type={searchable ? "search" : "text"}
198
+ placeholder={placeholder}
199
+ title={placeholder}
200
+ alt={placeholder}
201
+ id={id}
202
+ name={name}
203
+ value={value || ""}
204
+ onChange={handleChange}
205
+ minLength={minlength}
206
+ maxLength={maxlength}
207
+ required={required}
208
+ {...rest}
209
+ />
210
+ )}
211
+
212
+ </div>
213
+ </div>
214
+ {errors.length > 0 && (
215
+ <ul className="error">
216
+ {errors.map((e, key) => (
217
+ <li key={key} aria-live="assertive" role="alert">
218
+ {e}
219
+ </li>
220
+ ))}
221
+ </ul>
222
+ )}
223
+ </>
224
+ );
225
+ });
226
+
227
+ TextField.displayName = "TextField";
228
+ export { TextField };
229
+
230
+ const EmailField = forwardRef(
231
+ (
232
+ {
233
+ name,
234
+ label,
235
+ placeholder,
236
+ help,
237
+ editable,
238
+ defaultValue,
239
+ required,
240
+ readOnly,
241
+ onChange,
242
+ minlength,
243
+ maxlength,
244
+ fieldValidated,
245
+ },
246
+ ref,
247
+ ) => {
248
+ const id = "emailfield-" + uniqid();
249
+ const [errors, setErrors] = useState([]);
250
+ const [value, setValue] = useState(defaultValue || null);
251
+ const validate = () => {
252
+ const errs = [];
253
+ if (required && (!value || value.trim() === "")) {
254
+ errs.push("Field required");
255
+ }
256
+ if (minlength !== undefined && minlength > 0 && value && value.trim().length < minlength) {
257
+ errs.push("Value length must be >= to " + minlength);
258
+ }
259
+ if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
260
+ errs.push("Value length must be <= to " + maxlength);
261
+ }
262
+ if (
263
+ value &&
264
+ !value.match(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)
265
+ ) {
266
+ errs.push("Invalid email");
267
+ }
268
+ setErrors(errs);
269
+ return !errs.length;
270
+ };
271
+ useEffect(() => {
272
+ if (value !== null) validate();
273
+ }, [value]);
274
+ useEffect(() => {
275
+ if (fieldValidated) validate();
276
+ }, [fieldValidated]);
277
+ useImperativeHandle(ref, () => ({
278
+ validate,
279
+ getValue: () => value,
280
+ }));
281
+ const handleChange = (e) => {
282
+ setValue(e.target.value);
283
+ if (onChange) {
284
+ onChange(e);
285
+ }
286
+ };
287
+ return (
288
+ <>
289
+ <div className={cn({ field: true, "field-email": true })}>
290
+ <label
291
+ contentEditable={editable}
292
+ className={cn({ help: !!help })}
293
+ title={help}
294
+ htmlFor={id}
295
+ >
296
+ {label}
297
+ {required ? (
298
+ <span className="mandatory" contentEditable={false}>
299
+ *
300
+ </span>
301
+ ) : (
302
+ ""
303
+ )}
304
+ </label>
305
+ <input
306
+ aria-required={required}
307
+ aria-readonly={readOnly}
308
+ readOnly={readOnly}
309
+ type="email"
310
+ placeholder={placeholder}
311
+ id={id}
312
+ name={name}
313
+ value={value || ""}
314
+ onChange={handleChange}
315
+ minLength={minlength}
316
+ maxLength={maxlength}
317
+ />
318
+ </div>
319
+ {errors.length > 0 && (
320
+ <ul className="error">
321
+ {errors.map((e, key) => (
322
+ <li key={key} aria-live="assertive" role="alert">
323
+ {e}
324
+ </li>
325
+ ))}
326
+ </ul>
327
+ )}
328
+ </>
329
+ );
330
+ },
331
+ );
332
+ EmailField.displayName = "EmailField";
333
+ export { EmailField };
334
+
335
+ const NumberField = forwardRef(
336
+ (
337
+ {
338
+ name,
339
+ label,
340
+ placeholder,
341
+ help,
342
+ editable,
343
+ value,
344
+ required,
345
+ readOnly,
346
+ onChange,
347
+ minlength,
348
+ maxlength,
349
+ min,
350
+ max,
351
+ step,
352
+ unit,
353
+ ...rest
354
+ },
355
+ ref,
356
+ ) => {
357
+ const id = "numberfield-" + uniqid();
358
+ const [errors, setErrors] = useState([]);
359
+ const inputRef = useRef();
360
+ const validate = () => {
361
+ const errs = [];
362
+ if (required && value === undefined) {
363
+ errs.push("Field required");
364
+ }
365
+ if (minlength !== undefined && minlength > 0 && value && value.trim().length < minlength) {
366
+ errs.push("Value length must be >= to " + minlength);
367
+ }
368
+ if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
369
+ errs.push("Value length must be <= to " + maxlength);
370
+ }
371
+ if ((min || min === 0) && (value || value === 0) && min > value) {
372
+ errs.push("Value < to " + min);
373
+ }
374
+ if ((max || max === 0) && (value || value === 0) && max < value) {
375
+ errs.push("Value > to " + max);
376
+ }
377
+ setErrors(errs);
378
+ return !errs.length;
379
+ };
380
+ useEffect(() => {
381
+ if (value !== null) validate();
382
+ }, [value]);
383
+ useImperativeHandle(ref, () => ({
384
+ ref: inputRef.current,
385
+ validate,
386
+ getValue: () => value,
387
+ }));
388
+ const handleChange = (e) => {
389
+ if (onChange) {
390
+ onChange(e);
391
+ }
392
+ };
393
+ return (
394
+ <>
395
+ <div className={cn({ field: true, "field-number": true })}>
396
+
397
+ <div className="flex flex-1">
398
+ {label && (
399
+ <label
400
+ contentEditable={editable}
401
+ className={cn({ help: !!help })}
402
+ title={help}
403
+ htmlFor={id}
404
+ >
405
+ {label}
406
+ {required ? (
407
+ <span className="mandatory" contentEditable={false}>
408
+ *
409
+ </span>
410
+ ) : (
411
+ ""
412
+ )}
413
+ </label>
414
+ )}
415
+ {help && <div className="flex help">{help}</div>}
416
+ <input
417
+ ref={inputRef}
418
+ aria-required={required}
419
+ aria-readonly={readOnly}
420
+ readOnly={readOnly}
421
+ type="number"
422
+ placeholder={placeholder}
423
+ id={id}
424
+ name={name}
425
+ value={value || ""}
426
+ onChange={handleChange}
427
+ minLength={minlength}
428
+ maxLength={maxlength}
429
+ min={min}
430
+ max={max}
431
+ step={step}
432
+ {...rest}
433
+ /><span className="unit">{unit}</span></div>
434
+ </div>
435
+ {errors.length > 0 && (
436
+ <ul className="error">
437
+ {errors.map((e, key) => (
438
+ <li key={key} aria-live="assertive" role="alert">
439
+ {e}
440
+ </li>
441
+ ))}
442
+ </ul>
443
+ )}
444
+ </>
445
+ );
446
+ },
447
+ );
448
+ NumberField.displayName = "NumberField";
449
+ export { NumberField };
450
+
451
+ const CheckboxField = forwardRef(
452
+ (
453
+ {
454
+ name,
455
+ label,
456
+ placeholder,
457
+ help,
458
+ editable,
459
+ defaultValue,
460
+ required,
461
+ readOnly,
462
+ onChange,
463
+ minlength,
464
+ maxlength,
465
+ checked,
466
+ ...rest
467
+ },
468
+ ref,
469
+ ) => {
470
+ const id = "checkfield-" + uniqid();
471
+ const [errors, setErrors] = useState([]);
472
+ const [value, setValue] = useState(checked || false);
473
+ useEffect(() => {
474
+ setValue(checked);
475
+ }, [checked]);
476
+ const validate = () => {
477
+ const errs = [];
478
+ if (required && !value) {
479
+ errs.push("Field must be checked.");
480
+ }
481
+ setErrors(errs);
482
+ return !errs.length;
483
+ };
484
+ useEffect(() => {
485
+ if (value !== null) validate();
486
+ }, [value]);
487
+ useImperativeHandle(ref, () => ({
488
+ validate,
489
+ getValue: () => value,
490
+ }));
491
+ const handleChange = (e) => {
492
+ setValue(!value);
493
+ onChange?.(e);
494
+ };
495
+ return (
496
+ <>
497
+ <div className={cn({field: true, "field-checkbox": true})}>
498
+ <div className="inline"><input
499
+ aria-required={required}
500
+ aria-readonly={readOnly}
501
+ readOnly={readOnly}
502
+ type="checkbox"
503
+ checked={value}
504
+ placeholder={placeholder}
505
+ id={id}
506
+ name={name}
507
+ onChange={handleChange}
508
+ minLength={minlength}
509
+ maxLength={maxlength}
510
+ {...rest}
511
+ />
512
+ {label && (
513
+ <label
514
+ contentEditable={editable}
515
+ title={help}
516
+ htmlFor={id}
517
+ >
518
+ {label}
519
+ {required ? (
520
+ <span className="mandatory" contentEditable={false}>
521
+ *
522
+ </span>
523
+ ) : (
524
+ ""
525
+ )}
526
+ </label>
527
+ )}
528
+ </div>
529
+ {help && <div className="flex help">{help}</div>}
530
+ </div>
531
+ {errors.length > 0 && (
532
+ <ul className="error">
533
+ {errors.map((e, key) => (
534
+ <li key={key} aria-live="assertive" role="alert">
535
+ {e}
536
+ </li>
537
+ ))}
538
+ </ul>
539
+ )}
540
+ </>
541
+ );
542
+ },
543
+ );
544
+ CheckboxField.displayName = "CheckboxField";
545
+ export { CheckboxField };
546
+
547
+ const RadioField = forwardRef(
548
+ (
549
+ {
550
+ name,
551
+ label,
552
+ placeholder,
553
+ help,
554
+ editable,
555
+ checked,
556
+ required,
557
+ readOnly,
558
+ onChange,
559
+ minlength,
560
+ maxlength,
561
+ },
562
+ ref,
563
+ ) => {
564
+ const id = "radiofield-" + uniqid();
565
+ const [errors, setErrors] = useState([]);
566
+ const [value, setValue] = useState(checked || null);
567
+ const validate = () => {
568
+ const errs = [];
569
+ if (required && !value) {
570
+ errs.push("Field must be checked.");
571
+ }
572
+ setErrors(errs);
573
+ return !errs.length;
574
+ };
575
+ useEffect(() => {
576
+ if (value !== null) validate();
577
+ }, [value]);
578
+ useImperativeHandle(ref, () => ({
579
+ validate,
580
+ getValue: () => value,
581
+ }));
582
+ const handleChange = (e) => {
583
+ setValue(!value);
584
+ if (onChange) {
585
+ onChange(e);
586
+ }
587
+ };
588
+ return (
589
+ <>
590
+ <div className={cn({ field: true, "field-radio": true })}>
591
+ <input
592
+ aria-required={required}
593
+ aria-readonly={readOnly}
594
+ readOnly={readOnly}
595
+ type="radio"
596
+ checked={value}
597
+ value={value || label}
598
+ placeholder={placeholder}
599
+ id={id}
600
+ name={name}
601
+ onChange={handleChange}
602
+ minLength={minlength}
603
+ maxLength={maxlength}
604
+ />
605
+ <label
606
+ contentEditable={editable}
607
+ className={cn({ help: !!help })}
608
+ title={help}
609
+ htmlFor={id}
610
+ >
611
+ {label}
612
+ {required ? (
613
+ <span className="mandatory" contentEditable={false}>
614
+ *
615
+ </span>
616
+ ) : (
617
+ ""
618
+ )}
619
+ </label>
620
+ </div>
621
+ {errors.length > 0 && (
622
+ <ul className="error">
623
+ {errors.map((e, key) => (
624
+ <li key={key} aria-live="assertive" role="alert">
625
+ {e}
626
+ </li>
627
+ ))}
628
+ </ul>
629
+ )}
630
+ </>
631
+ );
632
+ },
633
+ );
634
+ RadioField.displayName = "RadioField";
635
+ export { RadioField };
636
+
637
+ const SelectField = forwardRef(
638
+ (
639
+ {
640
+ name,
641
+ value,
642
+ items,
643
+ label,
644
+ placeholder,
645
+ disabled,
646
+ help,
647
+ editable,
648
+ checked,
649
+ required,
650
+ readOnly,
651
+ onChange,
652
+ minlength,
653
+ maxlength,
654
+ multiple,
655
+ ...rest
656
+ },
657
+ ref,
658
+ ) => {
659
+ const [values, setValues] = useState([]);
660
+ const id = "selectfield-" + uniqid();
661
+ const [errors, setErrors] = useState([]);
662
+ const [_value, setValue] = useState(value);
663
+ useEffect(() => {
664
+ if( value === undefined && required && items[0]){
665
+ setValue(items[0].value);
666
+ }else {
667
+ setValue(value);
668
+ setValues(value)
669
+ }
670
+ if (!multiple && value) {
671
+ //const index = items.findIndex((i) => i.value === value);
672
+ //onChange({name, value:items[index]}, index);
673
+ }
674
+ }, [value]);
675
+ const validate = () => {
676
+ const errs = [];
677
+ if (required && _value === undefined) {
678
+ errs.push("Field is required.");
679
+ }
680
+ setErrors(errs);
681
+ return !errs.length;
682
+ };
683
+ useEffect(() => {
684
+ if (_value !== null) validate();
685
+ }, [_value]);
686
+ useImperativeHandle(ref, () => ({
687
+ validate,
688
+ getValue: () => _value,
689
+ setValue,
690
+ }));
691
+ const handleChange = (e) => {
692
+ setValue(e.target.value);
693
+ if (onChange) {
694
+ let options = e.target.options;
695
+ let value = [];
696
+ for (var i = 0, l = options.length; i < l; i++) {
697
+ if (options[i].selected) {
698
+ value.push(options[i].value);
699
+ }
700
+ }
701
+ if( multiple ) {
702
+ setValues(value);
703
+ onChange(value);
704
+ }else {
705
+ const index = items.findIndex((i) => i.value === e.target.value);
706
+ onChange(items[index], index);
707
+ }
708
+ }
709
+ };
710
+ return (
711
+ <>
712
+ <div className={cn({ field: true, 'flex-1': true, flex: true, "field-select": true })}>
713
+ {label && (
714
+ <label
715
+ contentEditable={editable}
716
+ className={cn({ help: !!help, 'flex-1': true })}
717
+ title={help}
718
+ htmlFor={id}
719
+ >
720
+ {label}
721
+ {required ? (
722
+ <span className="mandatory" contentEditable={false}>
723
+ *
724
+ </span>
725
+ ) : (
726
+ ""
727
+ )}
728
+ </label>
729
+ )}
730
+ <select
731
+ aria-required={required}
732
+ aria-readonly={readOnly}
733
+ value={(_value)}
734
+ id={id}
735
+ name={name}
736
+ onChange={handleChange}
737
+ multiple={multiple}
738
+ disabled={disabled}
739
+ className={"flex-1"}
740
+ {...rest}
741
+ >
742
+ {items.map((i) => (
743
+ <option value={i.value}>{i.label}</option>
744
+ ))}
745
+ </select>
746
+ </div>
747
+ {help && <div className="flex help">{help}</div>}
748
+ {errors.length > 0 && (
749
+ <ul className="error">
750
+ {errors.map((e, key) => (
751
+ <li key={key} aria-live="assertive" role="alert">
752
+ {e}
753
+ </li>
754
+ ))}
755
+ </ul>
756
+ )}
757
+ </>
758
+ );
759
+ },
760
+ );
761
+ SelectField.displayName = "SelectField";
762
+ export { SelectField };
763
+
764
+ const RadioGroup = forwardRef(
765
+ ({ id, label, help, editable, name, required, children }, ref) => {
766
+ const [childrenRef, registerRef] = useRefs();
767
+ const [errors, setErrors] = useState([]);
768
+ const validate = () => {
769
+ const errs = [];
770
+ let res = false;
771
+ Object.keys(childrenRef.current).forEach((item) => {
772
+ res = !!childrenRef.current[item].getValue() || res;
773
+ });
774
+ if (!res && required) {
775
+ errs.push("Field is required");
776
+ }
777
+ setErrors(errs);
778
+ return !errs.length;
779
+ };
780
+ useImperativeHandle(ref, () => ({
781
+ validate,
782
+ }));
783
+ const handleChange = () => {
784
+ setTimeout(() => validate(), 0);
785
+ };
786
+ return (
787
+ <>
788
+ <label
789
+ contentEditable={editable}
790
+ className={cn({ help: !!help })}
791
+ title={help}
792
+ htmlFor={children[0].props.id}
793
+ >
794
+ {label}
795
+ {required ? (
796
+ <span className="mandatory" contentEditable={false}>
797
+ *
798
+ </span>
799
+ ) : (
800
+ ""
801
+ )}
802
+ </label>
803
+ {[
804
+ recursiveMap(children, (child, index) => {
805
+ if (child.type.displayName === "RadioField") {
806
+ const props = {
807
+ ...child.props,
808
+ name: name ? name : child.props.name,
809
+ onChange: () => handleChange(child.props.onChange),
810
+ };
811
+ return (
812
+ <child.type
813
+ {...props}
814
+ ref={registerRef("Radio" + index)}
815
+ name={child.props.name || "btn" + id}
816
+ />
817
+ );
818
+ }
819
+ return child;
820
+ }),
821
+ errors.length > 0 ? (
822
+ <ul className="error">
823
+ {errors.map((e, key) => (
824
+ <li key={key} aria-live="assertive" role="alert">
825
+ {e}
826
+ </li>
827
+ ))}
828
+ </ul>
829
+ ) : (
830
+ <></>
831
+ ),
832
+ ]}
833
+ </>
834
+ );
835
+ },
836
+ );
837
+
838
+ RadioGroup.displayName = "RadioGroup";
839
+ export { RadioGroup };
840
+
841
+ // New FileField component
842
+ const FileField = ({ inputProps, value, onChange, name, mimeTypes, maxSize, multiple}) => {
843
+ const [fileInfos, setFileInfos] = useState(value);
844
+ const { t } = useTranslation();
845
+
846
+ const handleFileChange = (e) => {
847
+ const selectedFiles = Array.from(e.target.files);
848
+ const newFileInfos = [];
849
+
850
+ const promises = selectedFiles.map(selectedFile => {
851
+ if (selectedFile && selectedFile.size > (maxSize || maxFileSize)) {
852
+ alert(`Le fichier est trop volumineux. La taille maximale autorisée est de ${(maxSize || maxFileSize) / (1024 * 1024)} Mo.`);
853
+ e.target.value = '';
854
+ return Promise.resolve();
855
+ }
856
+ return new Promise((resolve) => {
857
+ const reader = new FileReader();
858
+ reader.onloadend = () => {
859
+ newFileInfos.push({
860
+ preview: reader.result,
861
+ newFile: true,
862
+ file: selectedFile,
863
+ name: selectedFile.name
864
+ });
865
+ resolve();
866
+ };
867
+ reader.readAsDataURL(selectedFile);
868
+ });
869
+ });
870
+
871
+ Promise.all(promises).then(() => {
872
+ if(!multiple){
873
+ setFileInfos(newFileInfos);
874
+ }else{
875
+ setFileInfos(fileInfos => [...fileInfos, ...newFileInfos]);
876
+ }
877
+ onChange([...fileInfos.map(m => ({...m, newFile: false})), ...newFileInfos]);
878
+ });
879
+ };
880
+
881
+ const handleRemove = (e, index) => {
882
+ e.preventDefault();
883
+ const newFileInfos = fileInfos.filter((_, i) => i !== index);
884
+ setFileInfos(newFileInfos);
885
+ onChange(newFileInfos.map(m => ({...m, newFile: false})));
886
+ };
887
+
888
+ useEffect(() => {
889
+ if( value == null || (Array.isArray(value) && value.length === 0))
890
+ setFileInfos([])
891
+ else{
892
+ const v = Array.isArray(value) ? value : [value];
893
+ setFileInfos(v)
894
+ }
895
+ }, [value]);
896
+
897
+ console.log(fileInfos)
898
+ return (
899
+ <div className="field field-file">
900
+ <input
901
+ id={"field-file-" + name}
902
+ type="file"
903
+ data-field={name}
904
+ accept={mimeTypes ? mimeTypes.join(',') : '*'}
905
+ onChange={handleFileChange}
906
+ multiple={multiple} // Add multiple attribute
907
+ />
908
+ {fileInfos?.length > 0 && (
909
+ <div>
910
+ {fileInfos.filter(f => isGUID(f.guid) || f.preview).map((fileInfo, index) => (
911
+ <div key={index}>
912
+ {fileInfo.preview ? (
913
+ <a href={fileInfo.preview} target="_blank" rel="noopener noreferrer">
914
+ <img src={fileInfo.preview} alt={"Preview"} width='200' height='200' />
915
+ </a>
916
+ ) :(isGUID(fileInfo.guid) ? (
917
+ <a href={"/resources/"+fileInfo.guid} target="_blank" rel="noopener noreferrer">
918
+ <img src={"/resources/"+fileInfo.guid} alt={"Preview"} width='200' height='200' />
919
+ </a>
920
+ ) :(
921
+ <img src={fileInfo.preview} alt="Preview" style={{ maxWidth: '200px', maxHeight: '200px' }} />
922
+ ))}
923
+ <button onClick={(e) => handleRemove(e, index)}><FaMinus /></button>
924
+ </div>
925
+ ))}
926
+ </div>
927
+ )}
928
+ </div>
929
+ );
930
+ };
931
+
932
+ export { FileField };
933
+
934
+ export const FilterNumberField = ({ model, field, onChangeFilterValue, filterValues, setFilterValues }) => {
935
+ const { t } = useTranslation();
936
+ const { models, setPage, dataByModel } = useModelContext(); // dataByModel is not used, consider removing
937
+ const [min, setMin] = useState(null);
938
+ const [max, setMax] = useState(null);
939
+
940
+ // Debounced version of the function that actually calls onChangeFilterValue
941
+ const debouncedApplyFilter = useCallback(
942
+ debounce((currentMin, currentMax) => {
943
+ const conditions = [];
944
+ setPage(1);
945
+ if (currentMin !== null && !isNaN(currentMin)) {
946
+ conditions.push({ $gte: ['$' + field.name, parseFloat(currentMin)] });
947
+ }
948
+ if (currentMax !== null && !isNaN(currentMax)) {
949
+ conditions.push({ $lte: ['$' + field.name, parseFloat(currentMax)] });
950
+ }
951
+
952
+ if (conditions.length > 0) {
953
+ onChangeFilterValue(field, { $and: conditions });
954
+ } else {
955
+ onChangeFilterValue(field, {}); // Clear filter if both are invalid/null
956
+ }
957
+ }, 300), // Adjust delay as needed
958
+ [field, onChangeFilterValue] // Dependencies for useCallback
959
+ );
960
+
961
+ useEffect(() => {
962
+ // This effect is to reset local min/max if the global filterValues are cleared externally
963
+ // It should not call debouncedApplyFilter directly if filterValues is the source of truth
964
+ // for the parent component.
965
+ if (filterValues && typeof filterValues[field.name] === 'object') {
966
+ const andConditions = filterValues[field.name]?.$and;
967
+ if (andConditions && Array.isArray(andConditions)) {
968
+ const gteCondition = andConditions.find(cond => cond.$gte);
969
+ const lteCondition = andConditions.find(cond => cond.$lte);
970
+ setMin(gteCondition ? gteCondition.$gte[1] : null);
971
+ setMax(lteCondition ? lteCondition.$lte[1] : null);
972
+ } else {
973
+ // If the structure is not $and or it's cleared
974
+ setMin(null);
975
+ setMax(null);
976
+ }
977
+ } else if (!filterValues || filterValues[field.name] === undefined || Object.keys(filterValues[field.name] || {}).length === 0) {
978
+ // If filterValues for this field is cleared or doesn't exist
979
+ setMin(null);
980
+ setMax(null);
981
+ }
982
+ }, [filterValues, field.name]);
983
+
984
+
985
+ const handleMinChange = (e) => {
986
+ const inputValue = e.target.value;
987
+ if (inputValue === "") {
988
+ setMin(null);
989
+ debouncedApplyFilter(null, max);
990
+ } else {
991
+ const pi = parseFloat(inputValue); // Use parseFloat for potentially decimal numbers
992
+ if (!isNaN(pi)) {
993
+ setMin(pi);
994
+ debouncedApplyFilter(pi, max);
995
+ } else {
996
+ setMin(inputValue); // Keep invalid input in state to show user, but don't filter
997
+ // Or setMin(null) if you want to clear on invalid
998
+ // Potentially call debouncedApplyFilter(null, max) if invalid min means no min filter
999
+ }
1000
+ }
1001
+ gtag('event', 'search (number,min)');
1002
+ };
1003
+
1004
+ const handleMaxChange = (e) => {
1005
+ const inputValue = e.target.value;
1006
+ if (inputValue === "") {
1007
+ setMax(null);
1008
+ debouncedApplyFilter(min, null);
1009
+ } else {
1010
+ const pi = parseFloat(inputValue);
1011
+ if (!isNaN(pi)) {
1012
+ setMax(pi);
1013
+ debouncedApplyFilter(min, pi);
1014
+ } else {
1015
+ setMax(inputValue);
1016
+ // Potentially call debouncedApplyFilter(min, null) if invalid max means no max filter
1017
+ }
1018
+ }
1019
+ gtag('event', 'search (number,max)');
1020
+ };
1021
+
1022
+ return (
1023
+ <>
1024
+ <NumberField
1025
+ value={min === null ? '' : min} // Handle null for empty display
1026
+ label="Min:"
1027
+ onChange={handleMinChange}
1028
+ type="number" // Ensure type is number for appropriate input behavior
1029
+ />
1030
+ <NumberField
1031
+ value={max === null ? '' : max} // Handle null for empty display
1032
+ label="Max:"
1033
+ onChange={handleMaxChange}
1034
+ type="number" // Ensure type is number
1035
+ />
1036
+ </>
1037
+ );
1038
+ };
1039
+
1040
+ export const FilterEnumField = ({model, field, onChangeFilterValue, filterValues, setFilterValues}) => {
1041
+ const {t} = useTranslation();
1042
+ const debounced = debounce((field,filter) => onChangeFilterValue(field, { $find: filter }));
1043
+ const { models, setPage, elementsPerPage,pagedFilters, pagedSort, page } = useModelContext();
1044
+
1045
+ const [val, setVal] = useState(null);
1046
+ const queryClient= useQueryClient()
1047
+
1048
+ useEffect(() => {
1049
+ if (Object.keys(filterValues).length === 0){
1050
+ onChangeFilterValue(field, { });
1051
+ setVal('');
1052
+ }
1053
+ }, [filterValues]);
1054
+
1055
+ 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) => {
1056
+
1057
+ setPage(1);
1058
+
1059
+ if( !e || e.value === '') {
1060
+ setVal('');
1061
+ onChangeFilterValue(field, undefined);
1062
+ }
1063
+ else {
1064
+ onChangeFilterValue(field, {$eq: ['$' + field.name, e.value]});
1065
+ setVal(e.value);
1066
+ }
1067
+
1068
+ gtag('event', 'search (enum)');
1069
+ queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1070
+ }} /><button onClick={() => {
1071
+ onChangeFilterValue(field, { });
1072
+ setVal('');
1073
+ }}>x</button></div>
1074
+ }
1075
+ export const FilterBooleanField = ({model, field, filterValues, onChangeFilterValue }) => {
1076
+ const {t} = useTranslation();
1077
+ const { setPage, pagedFilters, pagedSort, page,elementsPerPage } = useModelContext();
1078
+
1079
+ useEffect(() => {
1080
+ if( Object.keys(filterValues).length === 0 ){
1081
+ setVal('null');
1082
+ onChangeFilterValue(field, { });
1083
+ }
1084
+ }, [filterValues]);
1085
+ const [val, setVal] = useState(null);
1086
+ const queryClient= useQueryClient()
1087
+ return <div className={"flex flex-no-gap flex-no-wrap"}><SelectField value={val} className={"flex-1"} items={[
1088
+ {label: t(''), value: 'null'},
1089
+ {label: t('yes'), value: '1'},
1090
+ { label: t('no'), value: '0'}]}
1091
+ onChange={(e) => {
1092
+ setPage(1);
1093
+ if( !e || e.value === 'null') {
1094
+ setVal(null);
1095
+ onChangeFilterValue(field, { });
1096
+ }
1097
+ else {
1098
+ onChangeFilterValue(field, {$or: [{$eq: ['$' + field.name, e.value === '1']}, {$eq: [{ $type: '$'+field.name }, "missing"]}]});
1099
+ setVal(e.value);
1100
+ }
1101
+
1102
+ gtag('event', 'search (boolean)');
1103
+ queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1104
+ }} /></div>
1105
+ }
1106
+ export const FilterDateField = ({model, field, filterValues, onChangeFilterValue }) => {
1107
+ const {t} = useTranslation();
1108
+
1109
+ const [minDate ,setMinDate] = useState(null);
1110
+ const [maxDate ,setMaxDate] = useState(null);
1111
+ useEffect(() => {
1112
+ if( Object.keys(filterValues).length === 0 ){
1113
+ onChangeFilterValue(field, { });
1114
+ setMinDate('');
1115
+ setMaxDate('');
1116
+ }
1117
+ }, [filterValues]);
1118
+ const onChange = (minDate, maxDate) =>{
1119
+ const min = minDate ? { $gte: ['$'+field.name, minDate]} : null;
1120
+
1121
+ const fm = new Date(maxDate);
1122
+ fm.setDate(fm.getDate() + 1);
1123
+
1124
+ const max = maxDate ? {$lte: ['$' + field.name, fm.toISOString()]} : null;
1125
+ const and= [];
1126
+ if( min) and.push(min);
1127
+ if( max) and.push(max);
1128
+ if( !min && !max)
1129
+ onChangeFilterValue(field, { });
1130
+ else
1131
+ onChangeFilterValue(field, { $and: and});
1132
+ gtag('event', 'search (date)');
1133
+ }
1134
+ return <div className={"flex flex-no-gap flex-no-wrap"}>
1135
+ <label htmlFor={"minDate"+model.name+field.name}>
1136
+ Min:
1137
+ <input id={"minDate"+model.name+field.name} type={"datetime-local"} value={minDate} onChange={e => {
1138
+ setMinDate(e.target.value);
1139
+ onChange?.(e.target.value, maxDate);
1140
+ }} />
1141
+ </label>
1142
+ <label htmlFor={"maxDate"+model.name+field.name}>
1143
+ Max:
1144
+ <input id={"maxDate"+model.name+field.name} type={"datetime-local"} value={maxDate} onChange={e => {
1145
+ setMaxDate(e.target.value);
1146
+ onChange?.(minDate, e.target.value);
1147
+ }} />
1148
+ </label>
1149
+ </div>
1150
+ }
1151
+ export const FilterStringField = ({ field, onChangeFilterValue, filterValues, setFilterValues }) => {
1152
+ const { models, setPage } = useModelContext();
1153
+ const [isRegex, setIsRegex] = useState(false);
1154
+ const { t } = useTranslation();
1155
+
1156
+
1157
+ useEffect(() => {
1158
+ if( Object.keys(filterValues).length === 0 ){
1159
+ onChangeFilterValue(field, { });
1160
+ }
1161
+ }, [filterValues]);
1162
+
1163
+ // Debounced function to apply the filter
1164
+ const debouncedApplyFilter = useCallback(
1165
+ debounce((currentValue, currentIsRegex) => {
1166
+ setPage(1); // Reset page to 1 when filter changes
1167
+
1168
+ if (currentValue === '') {
1169
+ // No need to call setFilterValues here as it's done immediately in handleChange
1170
+ onChangeFilterValue(field, field.multiple ? [] : undefined, true);
1171
+ return;
1172
+ }
1173
+
1174
+ let filterQuery;
1175
+ if (field.type === 'relation') {
1176
+ const relationModel = models.find(f => f.name === field.relation);
1177
+ if (relationModel) {
1178
+ const relationFilters = relationModel.fields
1179
+ .filter(f => mainFieldsTypes.includes(f.type))
1180
+ .map(mf => ({
1181
+ $regexMatch: { input: `$$this.${mf.name}`, regex: currentIsRegex ? currentValue : escapeRegExp(currentValue) }
1182
+ }));
1183
+ if (relationFilters.length > 0) {
1184
+ filterQuery = { [field.name]: {$find: { $and: [{ $or: relationFilters }] }}};
1185
+ } else {
1186
+ filterQuery = {}; // Or handle as no match if no searchable fields
1187
+ }
1188
+ } else {
1189
+ filterQuery = {}; // Relation model not found
1190
+ }
1191
+ } else { // Not a relation type
1192
+ const regexToUse = currentIsRegex ? currentValue : escapeRegExp(currentValue);
1193
+ if (field.type === 'array') {
1194
+ filterQuery = {
1195
+ $gt: [
1196
+ {
1197
+ $size: {
1198
+ $filter: {
1199
+ input: '$' + field.name,
1200
+ as: 'item',
1201
+ cond: {
1202
+ $regexMatch: {
1203
+ input: '$$item',
1204
+ regex: regexToUse
1205
+ }
1206
+ }
1207
+ }
1208
+ }
1209
+ },
1210
+ 0
1211
+ ]
1212
+ };
1213
+ } else { // Simple string field
1214
+ filterQuery = {
1215
+ $and: [{
1216
+ $regexMatch: {
1217
+ input: '$' + field.name,
1218
+ regex: regexToUse
1219
+ }
1220
+ }]
1221
+ };
1222
+ }
1223
+ }
1224
+ onChangeFilterValue(field, filterQuery, true);
1225
+ gtag('event', 'search (string)');
1226
+ }, 1200), // Debounce delay
1227
+ [] // Dependencies for useCallback
1228
+ );
1229
+
1230
+ const handleInputChange = (e) => {
1231
+ const newValue = e.target.value;
1232
+ // Update the displayed value immediately
1233
+ setFilterValues(filter => ({ ...filter, [field.name]: newValue }));
1234
+ // Call the debounced function to apply the filter
1235
+ debouncedApplyFilter(newValue, isRegex);
1236
+ };
1237
+
1238
+ const handleToggleRegex = () => {
1239
+ const newIsRegex = !isRegex;
1240
+ setIsRegex(newIsRegex);
1241
+ // Re-apply filter with the new regex state and current value
1242
+ // The value from filterValues should be up-to-date
1243
+ const currentValue = filterValues[field.name] || '';
1244
+ debouncedApplyFilter(currentValue, newIsRegex);
1245
+ };
1246
+
1247
+ return (
1248
+ <>
1249
+ <TextField
1250
+ type="text"
1251
+ name={`filter_${field.name}`}
1252
+ value={filterValues[field.name] || ''} // Ensure controlled component with a default empty string
1253
+ placeholder={isRegex ? t("filterstringfield.placeholder.regex", "regular expression") : t("filterstringfield.placeholder", "...")}
1254
+ onChange={handleInputChange}
1255
+ maxLength={1000}
1256
+ />
1257
+ <button title={"regex"} className={isRegex ? 'active' : ''} onClick={handleToggleRegex}>.*</button>
1258
+ </>
1259
+ );
1260
+ };
1261
+ export const FilterField = ({model, reversed, field, active, onChangeFilterValue, filterValues, setFilterValues}) => {
1262
+ const { elementsPerPage, pagedSort, setPagedSort, setPage, page, pagedFilters, lockedColumns, setLockedColumns } = useModelContext();
1263
+ const {t} = useTranslation();
1264
+ const [locked, setLocked] = useState(lockedColumns.includes(field.name));
1265
+ const queryClient = useQueryClient()
1266
+
1267
+ useEffect(() => {
1268
+ if(!reversed) {
1269
+ setFilterValues(filter => ({...filter, [field.name]: ''}));
1270
+ onChangeFilterValue(field, '', true);
1271
+ }
1272
+ }, [field]);
1273
+
1274
+ const handleToggleLock = () => {
1275
+ if( locked ) {
1276
+ if (lockedColumns.includes(field.name))
1277
+ setLockedColumns(cols => [...cols].filter(f => f !== field.name));
1278
+ }else{
1279
+ if (!lockedColumns.includes(field.name))
1280
+ setLockedColumns(cols => [...cols, field.name]);
1281
+ }
1282
+ setLocked(!locked);
1283
+ }
1284
+
1285
+ const [reset, setReset] = useState(false);
1286
+ const handleChangeSort = (up) => {
1287
+ setPagedSort(sort => {
1288
+ const s = lockedColumns.length > 0 ? {...sort[model.name] || {}} : {};
1289
+ if( up ){
1290
+ if( reset ){
1291
+ delete s[field.name];
1292
+ setReset(false);
1293
+ }else {
1294
+ s[field.name] = 1;
1295
+ }
1296
+ }else{
1297
+ s[field.name] = -1;
1298
+ setReset(true);
1299
+ }
1300
+ return {...sort, [model.name]: s};
1301
+ });
1302
+ queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
1303
+ }
1304
+
1305
+ const resetClass = pagedSort[model.name]?.[field.name] ? (((pagedSort[model.name]?.[field.name] === 1) || (pagedSort[model.name]?.[field.name] === -1)) ? 'active' : 'reset') : '';
1306
+
1307
+ const renderIconFromType =(field)=>{
1308
+ const type = field.type;
1309
+ if( type === 'color'){
1310
+ return <FaPallet/>;
1311
+ }
1312
+ if( type === 'code'){
1313
+ return <FaCode />;
1314
+ }
1315
+ else if( type === 'date'){
1316
+ return <FaCalendarWeek />;
1317
+ }else if( type === 'datetime'){
1318
+ return <FaCalendarDays />;
1319
+ }
1320
+ else if( type === 'richtext' || type === 'string' || type === 'string_t'){
1321
+ return <></>;
1322
+ }
1323
+ else if( type === 'url'){
1324
+ return <FaLink />;
1325
+ }
1326
+ else if( type === 'number'){
1327
+ return <FaHashtag />;
1328
+ }
1329
+ else if( type === 'file'){
1330
+ return <FaFile />;
1331
+ }
1332
+ else if( type === 'enum'){
1333
+ return <FaListUl />;
1334
+ }
1335
+ else if( type === 'boolean'){
1336
+ return <FaToggleOn />;
1337
+ }
1338
+ else if( type === 'image'){
1339
+ return <FaImage/>;
1340
+ }
1341
+ else if( type === 'relation'){
1342
+ return field.multiple ? <FaSitemap /> : <FaLink />;
1343
+ }
1344
+ else if( type === 'email'){
1345
+ return <FaAt />;
1346
+ }
1347
+ else if( type === 'phone'){
1348
+ return <FaPhone />;
1349
+ }
1350
+ else if( type === 'array'){
1351
+ return <FaTableColumns />;
1352
+ }
1353
+ return <FaPencil />
1354
+ }
1355
+ return <th key={field.name} className={`form filter-field`} style={{backgroundColor: field.color, color: !field.color ||isLightColor(field.color) ? 'black': "white"}}>
1356
+ <div className="flex flex-centered flex-mini-gap flex-row">
1357
+ <div className="flex flex-1 flex-mini-gap flex-no-wrap">
1358
+ {renderIconFromType(field)}
1359
+ <span title={field.name} className={"flex-1 title"}>{t(`field_${model.name}_${field.name}`, field.name)}</span>
1360
+ </div>
1361
+ { 'password'!==field.type && (<div className={"flex flex-no-gap"}>
1362
+ {(<>
1363
+ {(pagedSort[model.name]?.[field.name] !== 1) &&
1364
+ <button onClick={() => handleChangeSort(true)}
1365
+ className={resetClass}>
1366
+ {pagedSort[model.name]?.[field.name] === undefined ? <FaArrowDown/> : <FaArrowUp/>}</button>}
1367
+ {(pagedSort[model.name]?.[field.name] === 1) &&
1368
+ <button onClick={() => handleChangeSort(false)}
1369
+ className={resetClass}>
1370
+ <FaArrowDown/></button>}
1371
+ </>
1372
+ )}
1373
+ {!field.unique && (
1374
+ <button onClick={() => handleToggleLock()} className={locked ? 'active' : ''}><FaLock/></button>)}
1375
+ </div>)}
1376
+ {active && !['date','datetime','enum', 'boolean', 'number', 'password'].includes(field.type) && <div className="filter flex flex-no-wrap flex-mini-gap">
1377
+ <FilterStringField setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1378
+ </div>}
1379
+ {active && field.type === 'enum' && <div className="filter flex flex-no-wrap flex-mini-gap">
1380
+ <FilterEnumField model={model} setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1381
+ </div>}
1382
+ {active && field.type === 'boolean' && <div className="filter flex flex-no-wrap flex-mini-gap">
1383
+ <FilterBooleanField filterValues={filterValues} model={model} field={field} onChangeFilterValue={onChangeFilterValue} />
1384
+ </div>}
1385
+ {active && ['date', 'datetime'].includes(field.type) && <div className="filter flex flex-no-wrap flex-mini-gap">
1386
+ <FilterDateField filterValues={filterValues} model={model} field={field} onChangeFilterValue={onChangeFilterValue} />
1387
+ </div>}
1388
+ {active && field.type === 'number' && <div className="filter flex flex-no-wrap flex-mini-gap">
1389
+ <FilterNumberField model={model} setFilterValues={setFilterValues} filterValues={filterValues} field={field} onChangeFilterValue={onChangeFilterValue} />
1390
+ </div>}
1391
+ </div>
1392
+ </th>
1393
+ }
1394
+
1395
+ export const PhoneField = ({name, value, onChange}) => {
1396
+ const [phone, setPhone] = useState(value);
1397
+ useEffect(() => {
1398
+ setPhone(value);
1399
+ }, [value]);
1400
+ return (
1401
+ <div>
1402
+ <PhoneInput
1403
+ defaultCountry="ua"
1404
+ value={phone || ''}
1405
+ onChange={(phone) => {
1406
+ setPhone(phone);
1407
+ onChange?.(phone);
1408
+ }}
1409
+ />
1410
+ </div>
1411
+ );
1412
+ }
1413
+
1414
+ export const ModelField = ({field, disableable=false, showModel=true, value, fieldValue, fields=false, onChange}) => {
1415
+ const {models} = useModelContext();
1416
+ const {me} = useAuthContext();
1417
+ const {t} = useTranslation()
1418
+ const [modelValue, setModelValue] = useState(value);
1419
+ const [modelFieldValue, setModelFieldValue] = useState(null);
1420
+ const [checked, setChecked] = useState(true);
1421
+
1422
+ const itemsFields = [...models.find(f=>f.name === modelValue && me?.username === f._user)?.fields.map(m => ({label: m.name, value: m.name})) || []];
1423
+
1424
+ useEffect(() => {
1425
+ console.log({value})
1426
+ setModelValue(value)
1427
+ }, [value]);
1428
+
1429
+ useEffect(() => {
1430
+ console.log(modelValue)
1431
+ onChange({name: field.name, value: modelValue});
1432
+ }, [modelValue]);
1433
+
1434
+ useEffect(() => {
1435
+ if( !fieldValue){
1436
+ setModelFieldValue(itemsFields.length > 0 ? itemsFields[0].value : null)
1437
+ }else
1438
+ setModelFieldValue(fieldValue)
1439
+ }, [fieldValue]);
1440
+
1441
+ const dis = disableable ? <><CheckboxField checked={checked} onChange={e => {
1442
+ setChecked(e.target.checked)
1443
+ if (!e.target.checked) {
1444
+ setModelValue(null);
1445
+ }
1446
+ }} /></> : null;
1447
+
1448
+ if( !fields )
1449
+ return <div className={"flex flex-1"}>
1450
+ {dis}{checked && (<SelectField className="flex-1" value={modelValue} onChange={(e) => {
1451
+ setModelValue(e.value)
1452
+ onChange({name: field.name, value: e.value});
1453
+ }} items={[...models.filter(f=>f._user === me?.username).map(m => ({label: t(`model_${m.name}`, m.name), value: m.name}))]}/>
1454
+ )}
1455
+ </div>;
1456
+ return <div className={"flex flex-1"}>{dis}{checked && (<div className="flex flex-stretch" key={field?.name??'def'}>
1457
+ {showModel && (<SelectField disabled={!!checked} className="flex-1" value={modelValue} onChange={(e) => {
1458
+ setModelValue(e.value)
1459
+ onChange({name: field.name, value: { model: e.value, field: itemsFields[0] }});
1460
+ }} items={[...models.filter(f=>f._user === me?.username).map(m => ({label: t(`model_${m.name}`, m.name), value: m.name}))]}/>
1461
+ )}
1462
+ <SelectField className="flex-1" value={modelFieldValue} onChange={(e) =>{
1463
+ setModelFieldValue(e.value)
1464
+ onChange({name: field.name, value: { model: modelValue, field:e.value}});
1465
+ }} items={itemsFields}/>
1466
+ </div>)}
1467
+ </div>
1468
+ }
1469
+ export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1470
+ // 1. État interne pour une réactivité immédiate de l'interface.
1471
+ const [internalValue, setInternalValue] = useState(value);
1472
+
1473
+ // 2. On mémoïze le gestionnaire d'événements avec debounce pour éviter de le recréer à chaque rendu.
1474
+ const debouncedOnChange = useCallback(
1475
+ debounce((newValue) => {
1476
+ // On notifie le parent du changement après un court délai.
1477
+ onChange?.({ name, value: newValue });
1478
+ }, 200), // Un délai de 200ms est confortable pour un sélecteur de couleur.
1479
+ [onChange, name] // Dépendances de useCallback
1480
+ );
1481
+
1482
+ // 3. Effet pour synchroniser l'état interne si la prop `value` du parent change.
1483
+ useEffect(() => {
1484
+ if (value !== internalValue) {
1485
+ setInternalValue(value);
1486
+ }
1487
+ }, [value]);
1488
+
1489
+ const handleChange = (e) => {
1490
+ const newValue = e.target.value;
1491
+ // Met à jour l'état interne instantanément pour que l'input soit réactif.
1492
+ setInternalValue(newValue);
1493
+ // Appelle la fonction "debounced" pour notifier le parent.
1494
+ debouncedOnChange(newValue);
1495
+ };
1496
+
1497
+ return (
1498
+ <div className={`flex flex-1 flex-no-wrap ${className || ''}`}>
1499
+ {label && (<label className="flex-1">{label}</label>)}
1500
+ <div className="flex flex-1 flex-no-wrap"><input
1501
+ disabled={disabled}
1502
+ type="color"
1503
+ // L'input est maintenant contrôlé par notre état interne.
1504
+ value={internalValue || '#FFFFFF'}
1505
+ onChange={handleChange}
1506
+ {...rest}
1507
+ />
1508
+ <span className="color-value">{internalValue || '#FFFFFF'}</span>
1509
+ </div>
1510
+ </div>
1511
+ );
1512
+ };
1513
+
1514
+ export const CodeField = ({name, label, language, value, disabled, onChange}) => {
1515
+ const u = name || uniqid();
1516
+ const [currentEditor, setEditor] = useState(null);
1517
+
1518
+ return <>
1519
+ {label && (<label className="flex flex-1">{label}</label>)}
1520
+ {!disabled ? <div className={"codefield"}><span><b>{language}</b> : </span><CodeiumEditor
1521
+ language={language || 'json'}
1522
+ theme={"vs-dark"}
1523
+ value={value}
1524
+ onChange={e => {
1525
+ if (language === 'json') {
1526
+ let code;
1527
+ try {
1528
+ code = JSON.parse(e);
1529
+ onChange({name, value: code});
1530
+ } catch (e) {
1531
+ }
1532
+ } else
1533
+ onChange({name, value: e});
1534
+ }}
1535
+ height="300px"
1536
+ /></div> : <div className="code"><SyntaxHighlighter
1537
+ language={language || "javascript"} theme={docco}>{value}</SyntaxHighlighter></div>
1538
+ }</>
1539
+ }
1540
+
1541
+ export const EnumField = ({inputProps, value, handleChange, field}) => {
1542
+ const { t} = useTranslation()
1543
+ useEffect(() => {
1544
+ if( field.items.includes(value))
1545
+ handleChange(value);
1546
+ else{
1547
+ handleChange({name: field.name, value: field.items[0]})
1548
+ }
1549
+ }, []);
1550
+ return (
1551
+ <select {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} >{(field.items || []).map(item => {
1552
+ if( typeof(item) === 'string'){
1553
+ return <option value={item}>{t(item, item)}</option>;
1554
+ }
1555
+ return <></>
1556
+ })}</select>
1557
+ );
1558
+ }