data-primals-engine 1.4.0 → 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.
- package/README.md +856 -797
- package/client/src/AssistantChat.jsx +48 -5
- package/client/src/AssistantChat.scss +0 -1
- package/client/src/ChartConfigModal.jsx +34 -9
- package/client/src/Dashboard.jsx +396 -349
- package/client/src/DashboardChart.jsx +91 -12
- package/client/src/DataTable.jsx +817 -807
- package/client/src/Field.jsx +1784 -1782
- package/client/src/PackGallery.jsx +391 -290
- package/client/src/PackGallery.scss +231 -210
- package/client/src/contexts/UIContext.jsx +5 -2
- package/client/src/translations.js +188 -0
- package/package.json +19 -8
- package/src/config.js +2 -2
- package/src/core.js +21 -3
- package/src/engine.js +2 -1
- package/src/events.js +4 -3
- package/src/gameObject.js +1 -1
- package/src/middlewares/middleware-mongodb.js +3 -1
- package/src/modules/assistant/assistant.js +131 -42
- package/src/modules/auth-google/index.js +51 -0
- package/src/modules/auth-microsoft/index.js +82 -0
- package/src/modules/auth-saml/index.js +90 -0
- package/src/modules/bucket.js +3 -2
- package/src/modules/data/data.backup.js +374 -0
- package/src/modules/data/data.history.js +11 -8
- package/src/modules/data/data.js +190 -4543
- package/src/modules/data/data.operations.js +2790 -0
- package/src/modules/data/data.relations.js +687 -0
- package/src/modules/data/data.routes.js +1785 -1646
- package/src/modules/data/data.scheduling.js +274 -0
- package/src/modules/data/data.validation.js +245 -0
- package/src/modules/data/index.js +29 -1
- package/src/modules/user.js +2 -1
- package/src/modules/workflow.js +3 -2
- package/src/providers.js +110 -1
- package/src/services/stripe.js +3 -2
- package/src/sso.js +194 -0
- package/test/data.integration.test.js +3 -0
- package/test/user.test.js +1 -1
package/client/src/Field.jsx
CHANGED
|
@@ -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
|
-
{
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const {
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
onChange({name: field?.name, value: newModel});
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const [
|
|
1529
|
-
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
setIconSuggestions(
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
setIconSuggestions(
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
//
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
{
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
const
|
|
1639
|
-
remainder
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
(parseInt(
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
<NumberField name={`${name}-
|
|
1699
|
-
<NumberField name={`${name}-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
)
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
handleChange(
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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
|
};
|