@aatulwork/customform-renderer 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fields.js ADDED
@@ -0,0 +1,1230 @@
1
+ 'use strict';
2
+
3
+ var reactHookForm = require('react-hook-form');
4
+ var material = require('@mui/material');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var react = require('react');
7
+ var RefreshIcon = require('@mui/icons-material/Refresh');
8
+ var DatePicker = require('@mui/x-date-pickers/DatePicker');
9
+ var DateTimePicker = require('@mui/x-date-pickers/DateTimePicker');
10
+ var TimePicker = require('@mui/x-date-pickers/TimePicker');
11
+ var LocalizationProvider = require('@mui/x-date-pickers/LocalizationProvider');
12
+ var AdapterDayjs = require('@mui/x-date-pickers/AdapterDayjs');
13
+ var dayjs = require('dayjs');
14
+ var ckeditor5React = require('@ckeditor/ckeditor5-react');
15
+ var CloseIcon = require('@mui/icons-material/Close');
16
+ var CloudUploadIcon = require('@mui/icons-material/CloudUpload');
17
+ var InsertDriveFileIcon = require('@mui/icons-material/InsertDriveFile');
18
+
19
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
20
+
21
+ var RefreshIcon__default = /*#__PURE__*/_interopDefault(RefreshIcon);
22
+ var dayjs__default = /*#__PURE__*/_interopDefault(dayjs);
23
+ var CloseIcon__default = /*#__PURE__*/_interopDefault(CloseIcon);
24
+ var CloudUploadIcon__default = /*#__PURE__*/_interopDefault(CloudUploadIcon);
25
+ var InsertDriveFileIcon__default = /*#__PURE__*/_interopDefault(InsertDriveFileIcon);
26
+
27
+ // src/components/fields/TextField.tsx
28
+ var TextField = ({ field, control, defaultValue, rules, errors }) => {
29
+ return /* @__PURE__ */ jsxRuntime.jsx(
30
+ reactHookForm.Controller,
31
+ {
32
+ name: field.name,
33
+ control,
34
+ defaultValue,
35
+ rules,
36
+ render: ({ field: formField }) => /* @__PURE__ */ jsxRuntime.jsxs(
37
+ material.FormControl,
38
+ {
39
+ fullWidth: true,
40
+ required: field.required,
41
+ error: !!errors[field.name],
42
+ children: [
43
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormLabel, { children: [
44
+ " ",
45
+ field.label
46
+ ] }),
47
+ /* @__PURE__ */ jsxRuntime.jsx(
48
+ material.TextField,
49
+ {
50
+ ...formField,
51
+ type: field.type === "number" ? "number" : field.type,
52
+ placeholder: field.placeholder || "Enter value",
53
+ fullWidth: true,
54
+ size: "small",
55
+ required: field.required,
56
+ error: !!errors[field.name],
57
+ helperText: errors[field.name]?.message,
58
+ inputProps: field.type === "number" ? {
59
+ min: field.validation?.min,
60
+ max: field.validation?.max
61
+ } : void 0
62
+ }
63
+ )
64
+ ]
65
+ }
66
+ )
67
+ },
68
+ field.name
69
+ );
70
+ };
71
+
72
+ // src/utils/fieldHelpers.ts
73
+ var formatFileSize = (bytes) => {
74
+ if (bytes === 0) return "0 Bytes";
75
+ const k = 1024;
76
+ const sizes = ["Bytes", "KB", "MB", "GB"];
77
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
78
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
79
+ };
80
+ var validateFile = (file, field) => {
81
+ if (field.validation?.allowedFileTypes && field.validation.allowedFileTypes.length > 0) {
82
+ const fileExtension = file.name.split(".").pop()?.toLowerCase();
83
+ const allowedTypes = field.validation.allowedFileTypes.map((t) => t.toLowerCase().replace(".", ""));
84
+ if (!fileExtension || !allowedTypes.includes(fileExtension)) {
85
+ return `File type not allowed. Allowed types: ${field.validation.allowedFileTypes.join(", ")}`;
86
+ }
87
+ }
88
+ if (field.validation?.maxFileSize) {
89
+ if (file.size > field.validation.maxFileSize) {
90
+ const maxSizeFormatted = formatFileSize(field.validation.maxFileSize);
91
+ return `File size exceeds maximum allowed size of ${maxSizeFormatted}`;
92
+ }
93
+ }
94
+ return true;
95
+ };
96
+ var buildFieldRules = (field) => {
97
+ const rules = {};
98
+ if (field.required) {
99
+ rules.required = `${field.label} is required`;
100
+ }
101
+ if (field.type === "file") {
102
+ rules.validate = (value) => {
103
+ if (field.required) {
104
+ if (field.allowMultiple) {
105
+ const files = Array.isArray(value) ? value : [];
106
+ if (files.length === 0) {
107
+ return `${field.label} is required`;
108
+ }
109
+ const hasValidFiles = files.some(
110
+ (file) => file && typeof file === "object" && ("fileName" in file || "fileUrl" in file)
111
+ );
112
+ if (!hasValidFiles) {
113
+ return `${field.label} is required`;
114
+ }
115
+ } else {
116
+ if (!value || typeof value === "object" && !("fileName" in value || "fileUrl" in value)) {
117
+ return `${field.label} is required`;
118
+ }
119
+ }
120
+ }
121
+ return true;
122
+ };
123
+ }
124
+ if ((field.type === "select" || field.type === "formReference" || field.type === "apiReference") && field.allowMultiple) {
125
+ rules.validate = (value) => {
126
+ const values = value;
127
+ if (!values || values.length === 0) {
128
+ if (field.required) {
129
+ return `${field.label} is required`;
130
+ }
131
+ return true;
132
+ }
133
+ return true;
134
+ };
135
+ }
136
+ return rules;
137
+ };
138
+ var normalizeOptions = (options) => {
139
+ if (!options) return [];
140
+ return options.map((opt) => {
141
+ if (typeof opt === "string") {
142
+ return { label: opt, value: opt };
143
+ }
144
+ return opt;
145
+ });
146
+ };
147
+ var SimpleSelect = ({
148
+ label,
149
+ value,
150
+ onChange,
151
+ options,
152
+ placeholder,
153
+ helperText,
154
+ fullWidth = true,
155
+ size = "small",
156
+ required = false,
157
+ error = false,
158
+ disabled = false,
159
+ multiple = false,
160
+ isLoading = false,
161
+ filterable = true,
162
+ filterPlaceholder = "Filter...",
163
+ refreshable = false,
164
+ onRefresh
165
+ }) => {
166
+ const [filterText, setFilterText] = react.useState("");
167
+ const handleChange = (event) => {
168
+ const val = event.target.value;
169
+ if (val === "__refresh__") {
170
+ onRefresh?.();
171
+ return;
172
+ }
173
+ onChange(val);
174
+ };
175
+ const filteredOptions = filterable && filterText ? options.filter(
176
+ (opt) => String(opt.label).toLowerCase().includes(filterText.toLowerCase())
177
+ ) : options;
178
+ const handleFilterChange = (e) => {
179
+ setFilterText(e.target.value);
180
+ };
181
+ const handleClose = () => {
182
+ setFilterText("");
183
+ };
184
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { fullWidth, size, required, error, disabled, children: [
185
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { children: label }),
186
+ /* @__PURE__ */ jsxRuntime.jsxs(
187
+ material.Select,
188
+ {
189
+ value: value ?? (multiple ? [] : ""),
190
+ onChange: handleChange,
191
+ onClose: handleClose,
192
+ input: /* @__PURE__ */ jsxRuntime.jsx(material.OutlinedInput, { label }),
193
+ MenuProps: {
194
+ autoFocus: false
195
+ },
196
+ multiple,
197
+ disabled: disabled || isLoading,
198
+ endAdornment: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(material.CircularProgress, { size: 20 }) : refreshable && onRefresh ? /* @__PURE__ */ jsxRuntime.jsx(material.InputAdornment, { position: "end", sx: { mr: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(
199
+ material.IconButton,
200
+ {
201
+ size: "small",
202
+ onClick: (e) => {
203
+ e.stopPropagation();
204
+ onRefresh();
205
+ },
206
+ disabled,
207
+ "aria-label": "Refresh options",
208
+ children: /* @__PURE__ */ jsxRuntime.jsx(RefreshIcon__default.default, { fontSize: "small" })
209
+ }
210
+ ) }) : void 0,
211
+ renderValue: (selected) => {
212
+ if (multiple) {
213
+ const selectedValues = selected;
214
+ if (selectedValues.length === 0) {
215
+ return /* @__PURE__ */ jsxRuntime.jsx("em", { children: placeholder || "Select..." });
216
+ }
217
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Box, { sx: { display: "flex", flexWrap: "wrap", gap: 0.5 }, children: selectedValues.map((val) => {
218
+ const option2 = options.find((opt) => opt.value === val);
219
+ return /* @__PURE__ */ jsxRuntime.jsx(
220
+ material.Chip,
221
+ {
222
+ label: option2?.label || val,
223
+ size: "small"
224
+ },
225
+ val
226
+ );
227
+ }) });
228
+ }
229
+ const option = options.find((opt) => opt.value === selected);
230
+ return option?.label || selected || /* @__PURE__ */ jsxRuntime.jsx("em", { children: placeholder || "Select..." });
231
+ },
232
+ children: [
233
+ filterable && /* @__PURE__ */ jsxRuntime.jsx(
234
+ material.ListSubheader,
235
+ {
236
+ sx: { py: 1 },
237
+ onMouseDown: (e) => e.stopPropagation(),
238
+ onClick: (e) => e.stopPropagation(),
239
+ children: /* @__PURE__ */ jsxRuntime.jsx(
240
+ material.TextField,
241
+ {
242
+ size: "small",
243
+ fullWidth: true,
244
+ placeholder: filterPlaceholder,
245
+ value: filterText,
246
+ onChange: handleFilterChange,
247
+ variant: "outlined",
248
+ sx: { mt: -0.5 },
249
+ autoFocus: true
250
+ }
251
+ )
252
+ }
253
+ ),
254
+ filteredOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: option.value, children: option.label }, option.value))
255
+ ]
256
+ }
257
+ ),
258
+ helperText && /* @__PURE__ */ jsxRuntime.jsx(material.FormHelperText, { children: helperText })
259
+ ] });
260
+ };
261
+ var SelectField = ({ field, control, defaultValue, rules, errors }) => {
262
+ const isMultiple = field.allowMultiple || false;
263
+ const normalizedOptions = normalizeOptions(field.options);
264
+ const selectOptions = react.useMemo(() => {
265
+ return normalizedOptions.map((opt) => ({
266
+ value: String(opt.value),
267
+ label: opt.label
268
+ }));
269
+ }, [normalizedOptions]);
270
+ return /* @__PURE__ */ jsxRuntime.jsx(
271
+ reactHookForm.Controller,
272
+ {
273
+ name: field.name,
274
+ control,
275
+ defaultValue,
276
+ rules,
277
+ render: ({ field: formField }) => {
278
+ return /* @__PURE__ */ jsxRuntime.jsx(
279
+ SimpleSelect,
280
+ {
281
+ label: field.label,
282
+ value: formField.value,
283
+ onChange: (value) => {
284
+ formField.onChange(value);
285
+ },
286
+ options: selectOptions,
287
+ placeholder: field.placeholder || "Select...",
288
+ helperText: errors[field.name]?.message,
289
+ fullWidth: true,
290
+ size: "small",
291
+ required: field.required,
292
+ error: !!errors[field.name],
293
+ disabled: false,
294
+ multiple: isMultiple
295
+ }
296
+ );
297
+ }
298
+ },
299
+ field.name
300
+ );
301
+ };
302
+ var CheckboxField = ({ field, control, defaultValue, rules }) => {
303
+ return /* @__PURE__ */ jsxRuntime.jsx(
304
+ reactHookForm.Controller,
305
+ {
306
+ name: field.name,
307
+ control,
308
+ defaultValue,
309
+ rules,
310
+ render: ({ field: formField }) => /* @__PURE__ */ jsxRuntime.jsx(
311
+ material.FormControlLabel,
312
+ {
313
+ control: /* @__PURE__ */ jsxRuntime.jsx(
314
+ material.Checkbox,
315
+ {
316
+ ...formField,
317
+ checked: formField.value || false,
318
+ size: "small"
319
+ }
320
+ ),
321
+ label: field.label
322
+ }
323
+ )
324
+ },
325
+ field.name
326
+ );
327
+ };
328
+ var RadioField = ({ field, control, defaultValue, rules, errors }) => {
329
+ const normalizedOptions = normalizeOptions(field.options);
330
+ return /* @__PURE__ */ jsxRuntime.jsx(
331
+ reactHookForm.Controller,
332
+ {
333
+ name: field.name,
334
+ control,
335
+ defaultValue,
336
+ rules,
337
+ render: ({ field: formField }) => /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { component: "fieldset", required: field.required, error: !!errors[field.name], children: [
338
+ /* @__PURE__ */ jsxRuntime.jsx(material.FormLabel, { required: field.required, error: !!errors[field.name], children: field.label }),
339
+ /* @__PURE__ */ jsxRuntime.jsx(material.RadioGroup, { ...formField, row: true, children: normalizedOptions.map((option, index) => /* @__PURE__ */ jsxRuntime.jsx(
340
+ material.FormControlLabel,
341
+ {
342
+ value: option.value,
343
+ control: /* @__PURE__ */ jsxRuntime.jsx(material.Radio, { size: "small" }),
344
+ label: option.label
345
+ },
346
+ index
347
+ )) })
348
+ ] })
349
+ },
350
+ field.name
351
+ );
352
+ };
353
+ var ToggleField = ({ field, control, defaultValue, rules, errors }) => {
354
+ return /* @__PURE__ */ jsxRuntime.jsx(
355
+ reactHookForm.Controller,
356
+ {
357
+ name: field.name,
358
+ control,
359
+ defaultValue,
360
+ rules,
361
+ render: ({ field: formField }) => /* @__PURE__ */ jsxRuntime.jsx(
362
+ material.FormControlLabel,
363
+ {
364
+ control: /* @__PURE__ */ jsxRuntime.jsx(
365
+ material.Switch,
366
+ {
367
+ ...formField,
368
+ checked: formField.value || false,
369
+ size: "medium"
370
+ }
371
+ ),
372
+ label: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
373
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", children: [
374
+ field.label,
375
+ " ",
376
+ field.required && "*"
377
+ ] }),
378
+ errors[field.name] && /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "error", sx: { display: "block", mt: 0.5 }, children: errors[field.name]?.message })
379
+ ] })
380
+ }
381
+ )
382
+ },
383
+ field.name
384
+ );
385
+ };
386
+ var ColorField = ({ field, control, defaultValue, rules, errors }) => {
387
+ return /* @__PURE__ */ jsxRuntime.jsx(
388
+ reactHookForm.Controller,
389
+ {
390
+ name: field.name,
391
+ control,
392
+ defaultValue,
393
+ rules,
394
+ render: ({ field: formField }) => /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", gap: 2, width: "100%" }, children: [
395
+ /* @__PURE__ */ jsxRuntime.jsx(
396
+ material.FormLabel,
397
+ {
398
+ required: field.required,
399
+ error: !!errors[field.name],
400
+ children: field.label
401
+ }
402
+ ),
403
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", flexDirection: "column", flex: 1, maxWidth: 200 }, children: [
404
+ /* @__PURE__ */ jsxRuntime.jsx(
405
+ material.Input,
406
+ {
407
+ ...formField,
408
+ type: "color",
409
+ sx: {
410
+ width: "20%",
411
+ height: "40px",
412
+ cursor: "pointer",
413
+ border: errors[field.name] ? "1px solid red" : "1px solid rgba(0, 0, 0, 0.23)",
414
+ borderRadius: "4px",
415
+ padding: "1px"
416
+ },
417
+ inputProps: {
418
+ style: {
419
+ height: "100%",
420
+ cursor: "pointer"
421
+ }
422
+ }
423
+ }
424
+ ),
425
+ errors[field.name] && /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "error", sx: { mt: 0.5 }, children: errors[field.name]?.message })
426
+ ] })
427
+ ] })
428
+ },
429
+ field.name
430
+ );
431
+ };
432
+ var textFieldSlotProps = (field, errors) => ({
433
+ fullWidth: true,
434
+ size: "small",
435
+ required: field.required,
436
+ error: !!errors[field.name],
437
+ helperText: errors[field.name]?.message
438
+ });
439
+ function resolveDatePickerMode(field) {
440
+ if (field.datePickerMode) return field.datePickerMode;
441
+ return field?.displayTime ? "datetime" : "date";
442
+ }
443
+ var DateTimePickerField = ({ field, control, defaultValue, rules, errors }) => {
444
+ const mode = resolveDatePickerMode(field);
445
+ return /* @__PURE__ */ jsxRuntime.jsx(
446
+ reactHookForm.Controller,
447
+ {
448
+ name: field.name,
449
+ control,
450
+ defaultValue,
451
+ rules,
452
+ render: ({ field: formField }) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
453
+ /* @__PURE__ */ jsxRuntime.jsx(
454
+ material.FormLabel,
455
+ {
456
+ required: field.required,
457
+ error: !!errors[field.name],
458
+ children: field.label
459
+ }
460
+ ),
461
+ /* @__PURE__ */ jsxRuntime.jsxs(LocalizationProvider.LocalizationProvider, { dateAdapter: AdapterDayjs.AdapterDayjs, children: [
462
+ mode === "date" && /* @__PURE__ */ jsxRuntime.jsx(
463
+ DatePicker.DatePicker,
464
+ {
465
+ format: "DD-MM-YYYY",
466
+ value: formField.value ? dayjs__default.default(formField.value) : null,
467
+ onChange: (date) => formField.onChange(date?.toISOString() ?? null),
468
+ slotProps: { textField: textFieldSlotProps(field, errors) }
469
+ }
470
+ ),
471
+ mode === "datetime" && /* @__PURE__ */ jsxRuntime.jsx(
472
+ DateTimePicker.DateTimePicker,
473
+ {
474
+ format: "DD-MM-YYYY hh:mm A",
475
+ value: formField.value ? dayjs__default.default(formField.value) : null,
476
+ onChange: (date) => formField.onChange(date?.toISOString() ?? null),
477
+ slotProps: { textField: textFieldSlotProps(field, errors) }
478
+ }
479
+ ),
480
+ mode === "time" && /* @__PURE__ */ jsxRuntime.jsx(
481
+ TimePicker.TimePicker,
482
+ {
483
+ format: "hh:mm A",
484
+ value: formField.value ? dayjs__default.default(formField.value) : null,
485
+ onChange: (date) => formField.onChange(date?.toISOString() ?? null),
486
+ slotProps: { textField: textFieldSlotProps(field, errors) }
487
+ }
488
+ )
489
+ ] })
490
+ ] })
491
+ },
492
+ field.name
493
+ );
494
+ };
495
+ var useFormColors = (customColors) => {
496
+ const theme = material.useTheme();
497
+ return {
498
+ primary: customColors?.primary || theme.palette.primary.main,
499
+ secondary: customColors?.secondary || theme.palette.secondary.main,
500
+ error: customColors?.error || theme.palette.error.main,
501
+ success: customColors?.success || theme.palette.success.main,
502
+ warning: customColors?.warning || theme.palette.warning.main,
503
+ info: customColors?.info || theme.palette.info.main,
504
+ textPrimary: customColors?.textPrimary || theme.palette.text.primary,
505
+ textSecondary: customColors?.textSecondary || theme.palette.text.secondary,
506
+ divider: customColors?.divider || theme.palette.divider,
507
+ background: customColors?.background || theme.palette.background.default,
508
+ backgroundPaper: customColors?.backgroundPaper || theme.palette.background.paper
509
+ };
510
+ };
511
+ var defaultFileUploadService = {
512
+ uploadFiles: async () => {
513
+ throw new Error("File upload service not provided. Please provide a fileUpload service in FormServices.");
514
+ }
515
+ };
516
+ var defaultFormReferenceService = {
517
+ fetchOptions: async () => {
518
+ throw new Error("Form reference service not provided. Please provide a formReference service in FormServices.");
519
+ }
520
+ };
521
+ var defaultApiReferenceService = {
522
+ fetchOptions: async () => {
523
+ throw new Error("API reference service not provided. Please provide an apiReference service in FormServices.");
524
+ }
525
+ };
526
+
527
+ // src/utils/ckeditorLoader.ts
528
+ var loadCKEditor = (scriptPath = "/lib/ckeditor/ckeditor.js") => {
529
+ return new Promise((resolve, reject) => {
530
+ if (window.ClassicEditor) {
531
+ resolve();
532
+ return;
533
+ }
534
+ const existingScript = document.querySelector(`script[src="${scriptPath}"]`);
535
+ if (existingScript) {
536
+ existingScript.addEventListener("load", () => {
537
+ if (window.ClassicEditor) {
538
+ resolve();
539
+ } else {
540
+ reject(new Error("CKEditor script loaded but ClassicEditor not found on window"));
541
+ }
542
+ });
543
+ existingScript.addEventListener("error", () => {
544
+ reject(new Error("Failed to load CKEditor script"));
545
+ });
546
+ return;
547
+ }
548
+ const script = document.createElement("script");
549
+ script.src = scriptPath;
550
+ script.async = true;
551
+ script.onload = () => {
552
+ const checkInterval = setInterval(() => {
553
+ if (window.ClassicEditor) {
554
+ clearInterval(checkInterval);
555
+ resolve();
556
+ }
557
+ }, 100);
558
+ setTimeout(() => {
559
+ clearInterval(checkInterval);
560
+ if (!window.ClassicEditor) {
561
+ reject(
562
+ new Error(
563
+ "CKEditor script loaded but ClassicEditor was not found on window. Ensure you are using the package-provided build from lib/ckeditor/ckeditor.js (see CKEDITOR_SETUP.md)."
564
+ )
565
+ );
566
+ }
567
+ }, 1e4);
568
+ };
569
+ script.onerror = () => {
570
+ reject(
571
+ new Error(
572
+ `Failed to load CKEditor from ${scriptPath}. Ensure the file exists at that URL (e.g. copy from node_modules/@aatulwork/customform-renderer/lib/ckeditor/ckeditor.js to public/lib/ckeditor/ckeditor.js) or set services.ckEditorScriptPath to a working URL.`
573
+ )
574
+ );
575
+ };
576
+ document.head.appendChild(script);
577
+ });
578
+ };
579
+ var isCKEditorAvailable = () => {
580
+ return typeof window !== "undefined" && typeof window.ClassicEditor !== "undefined";
581
+ };
582
+ var waitForCKEditor = (timeout = 1e4) => {
583
+ return new Promise((resolve, reject) => {
584
+ if (isCKEditorAvailable()) {
585
+ resolve();
586
+ return;
587
+ }
588
+ const startTime = Date.now();
589
+ const checkInterval = setInterval(() => {
590
+ if (isCKEditorAvailable()) {
591
+ clearInterval(checkInterval);
592
+ resolve();
593
+ } else if (Date.now() - startTime > timeout) {
594
+ clearInterval(checkInterval);
595
+ reject(new Error("CKEditor not available after timeout"));
596
+ }
597
+ }, 100);
598
+ });
599
+ };
600
+
601
+ // src/hooks/useCKEditor.ts
602
+ var useCKEditor = (options = {}) => {
603
+ const {
604
+ scriptPath = "/lib/ckeditor/ckeditor.js",
605
+ autoLoad = true,
606
+ timeout = 1e4
607
+ } = options;
608
+ const [isReady, setIsReady] = react.useState(false);
609
+ const [isLoading, setIsLoading] = react.useState(false);
610
+ const [error, setError] = react.useState(null);
611
+ const load = async () => {
612
+ if (isCKEditorAvailable()) {
613
+ setIsReady(true);
614
+ setIsLoading(false);
615
+ return;
616
+ }
617
+ setIsLoading(true);
618
+ setError(null);
619
+ try {
620
+ await loadCKEditor(scriptPath);
621
+ setIsReady(true);
622
+ } catch (err) {
623
+ const error2 = err instanceof Error ? err : new Error("Failed to load CKEditor");
624
+ setError(error2);
625
+ setIsReady(false);
626
+ } finally {
627
+ setIsLoading(false);
628
+ }
629
+ };
630
+ react.useEffect(() => {
631
+ if (isCKEditorAvailable()) {
632
+ setIsReady(true);
633
+ return;
634
+ }
635
+ if (autoLoad) {
636
+ load();
637
+ } else {
638
+ waitForCKEditor(timeout).then(() => setIsReady(true)).catch((err) => setError(err instanceof Error ? err : new Error("CKEditor not available")));
639
+ }
640
+ }, [scriptPath, autoLoad, timeout]);
641
+ return {
642
+ isReady,
643
+ isLoading,
644
+ error,
645
+ load
646
+ };
647
+ };
648
+ var CKEditorField = ({ field, control, defaultValue, rules, errors, setValue, formSchema, services, colors }) => {
649
+ const theme = material.useTheme();
650
+ const formColors = useFormColors(colors);
651
+ const editorContainerRef = react.useRef(null);
652
+ const editorInstanceRef = react.useRef(null);
653
+ const isUserTypingRef = react.useRef(false);
654
+ const fileUploadService = services?.fileUpload || defaultFileUploadService;
655
+ const fileBaseUrl = services?.fileBaseUrl || "";
656
+ const licenseKey = services?.ckEditorLicenseKey || "GPL";
657
+ const ckEditorScriptPath = services?.ckEditorScriptPath || "/lib/ckeditor/ckeditor.js";
658
+ const { isReady: isCKEditorReady, isLoading: isCKEditorLoading, error: ckEditorError } = useCKEditor({
659
+ scriptPath: ckEditorScriptPath,
660
+ autoLoad: true
661
+ });
662
+ const watchedValue = reactHookForm.useWatch({
663
+ control,
664
+ name: field.name,
665
+ defaultValue: defaultValue || ""
666
+ });
667
+ react.useEffect(() => {
668
+ if (editorInstanceRef.current && isCKEditorReady && !isUserTypingRef.current) {
669
+ const currentEditorData = editorInstanceRef.current.getData();
670
+ const formValue = watchedValue || "";
671
+ if (currentEditorData !== formValue) {
672
+ editorInstanceRef.current.setData(formValue);
673
+ }
674
+ }
675
+ isUserTypingRef.current = false;
676
+ }, [watchedValue, isCKEditorReady]);
677
+ const createCustomUploadAdapter = react.useCallback((loader) => {
678
+ return {
679
+ upload: async () => {
680
+ if (!formSchema?.name) {
681
+ throw new Error("Form schema name is required for image uploads");
682
+ }
683
+ try {
684
+ const file = await loader.file;
685
+ if (!file) {
686
+ throw new Error("No file provided");
687
+ }
688
+ if (!file.type.startsWith("image/")) {
689
+ throw new Error("Only image files are allowed");
690
+ }
691
+ const uploadedFiles = await fileUploadService.uploadFiles(
692
+ formSchema.name,
693
+ field.name,
694
+ [file]
695
+ );
696
+ if (uploadedFiles && uploadedFiles.length > 0 && uploadedFiles[0].fileUrl) {
697
+ const fileUrl = uploadedFiles[0].fileUrl;
698
+ const isFullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://");
699
+ const url = isFullUrl ? fileUrl : fileBaseUrl + fileUrl;
700
+ return {
701
+ default: url
702
+ };
703
+ }
704
+ throw new Error("Upload failed: No file URL returned");
705
+ } catch (error) {
706
+ console.error("Upload error:", error);
707
+ const errorMessage = error.response?.data?.message || error.message || "Upload failed";
708
+ throw new Error(errorMessage);
709
+ }
710
+ },
711
+ abort: () => {
712
+ console.log("Upload aborted");
713
+ }
714
+ };
715
+ }, [formSchema?.name, field.name, fileUploadService, fileBaseUrl]);
716
+ if (isCKEditorLoading) {
717
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
718
+ /* @__PURE__ */ jsxRuntime.jsx(material.FormLabel, { required: field.required, error: !!errors[field.name], children: field.label }),
719
+ /* @__PURE__ */ jsxRuntime.jsx(material.Box, { sx: { p: 2, textAlign: "center", color: formColors.textSecondary }, children: "Loading editor..." })
720
+ ] });
721
+ }
722
+ if (ckEditorError || !isCKEditorReady) {
723
+ const message = ckEditorError?.message || `CKEditor failed to load. Script path: ${ckEditorScriptPath}. Copy ckeditor.js to public/lib/ckeditor/ or set services.ckEditorScriptPath. See CKEDITOR_SETUP.md.`;
724
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
725
+ /* @__PURE__ */ jsxRuntime.jsx(material.FormLabel, { required: field.required, error: !!errors[field.name], children: field.label }),
726
+ /* @__PURE__ */ jsxRuntime.jsx(material.Box, { sx: { p: 2, border: "1px solid", borderColor: formColors.error, borderRadius: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.FormHelperText, { error: true, children: message }) })
727
+ ] });
728
+ }
729
+ return /* @__PURE__ */ jsxRuntime.jsx(
730
+ reactHookForm.Controller,
731
+ {
732
+ name: field.name,
733
+ control,
734
+ defaultValue: defaultValue || "",
735
+ rules,
736
+ render: ({ field: formField }) => /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
737
+ /* @__PURE__ */ jsxRuntime.jsx(
738
+ material.FormLabel,
739
+ {
740
+ required: field.required,
741
+ error: !!errors[field.name],
742
+ children: field.label
743
+ }
744
+ ),
745
+ /* @__PURE__ */ jsxRuntime.jsx(
746
+ material.Box,
747
+ {
748
+ ref: editorContainerRef,
749
+ sx: {
750
+ "& .ck-editor": {
751
+ borderRadius: "4px",
752
+ "& .ck-toolbar": {
753
+ width: "100%",
754
+ maxWidth: "100%",
755
+ overflow: "visible",
756
+ display: "flex",
757
+ flexWrap: "wrap",
758
+ "& .ck-toolbar__items": {
759
+ display: "flex",
760
+ flexWrap: "wrap !important",
761
+ width: "100%",
762
+ maxWidth: "100%",
763
+ "& > *": {
764
+ flexShrink: 0
765
+ }
766
+ }
767
+ },
768
+ "& .ck-editor__editable": {
769
+ minHeight: "100px"
770
+ },
771
+ "&:hover": {
772
+ borderColor: errors[field.name] ? formColors.error : theme.palette.mode === "dark" ? "rgba(255, 255, 255, 0.87)" : formColors.primary
773
+ },
774
+ "& .ck-focused": {
775
+ border: errors[field.name] ? `1px solid ${formColors.error} !important` : `1px solid ${theme.palette.mode === "dark" ? "rgba(255, 255, 255, 0.23)" : formColors.primary} !important`,
776
+ boxShadow: errors[field.name] ? `0 0 0 1px ${formColors.error}` : `0 0 0 1px ${material.alpha(formColors.primary, 0.5)} !important`
777
+ }
778
+ }
779
+ },
780
+ children: window.ClassicEditor && isCKEditorReady && /* @__PURE__ */ jsxRuntime.jsx(
781
+ ckeditor5React.CKEditor,
782
+ {
783
+ editor: window.ClassicEditor,
784
+ config: {
785
+ licenseKey,
786
+ initialData: formField.value || ""
787
+ },
788
+ data: formField.value || "",
789
+ onReady: (editor) => {
790
+ editorInstanceRef.current = editor;
791
+ if (formSchema?.name) {
792
+ try {
793
+ const fileRepository = editor.plugins.get("FileRepository");
794
+ if (fileRepository) {
795
+ fileRepository.createUploadAdapter = (loader) => {
796
+ return createCustomUploadAdapter(loader);
797
+ };
798
+ } else {
799
+ console.warn("FileRepository plugin not found");
800
+ }
801
+ } catch (error) {
802
+ console.error("Error setting up upload adapter:", error);
803
+ }
804
+ } else {
805
+ console.warn("Form schema name not available, upload adapter not set");
806
+ }
807
+ if (defaultValue && !formField.value) {
808
+ editor.setData(defaultValue);
809
+ setValue?.(field.name, defaultValue);
810
+ }
811
+ },
812
+ onChange: (_event, editor) => {
813
+ const data = editor.getData();
814
+ isUserTypingRef.current = true;
815
+ formField.onChange(data);
816
+ },
817
+ onBlur: () => {
818
+ formField.onBlur();
819
+ }
820
+ }
821
+ )
822
+ }
823
+ ),
824
+ errors[field.name] && /* @__PURE__ */ jsxRuntime.jsx(material.FormHelperText, { error: true, sx: { mt: 0.5, mx: 0 }, children: errors[field.name]?.message })
825
+ ] })
826
+ },
827
+ field.name
828
+ );
829
+ };
830
+ var FileField = ({
831
+ field,
832
+ control,
833
+ defaultValue,
834
+ rules,
835
+ errors,
836
+ formSchema,
837
+ uploadingFiles = {},
838
+ setUploadingFiles,
839
+ setError,
840
+ clearErrors,
841
+ services,
842
+ colors
843
+ }) => {
844
+ const theme = material.useTheme();
845
+ const isMobile = material.useMediaQuery(theme.breakpoints.down("md"));
846
+ const formColors = useFormColors(colors);
847
+ const acceptTypes = field.validation?.allowedFileTypes ? field.validation.allowedFileTypes.map((type) => `.${type.replace(".", "")}`).join(",") : void 0;
848
+ const isMultiple = field.allowMultiple || false;
849
+ const fileUploadService = services?.fileUpload || defaultFileUploadService;
850
+ return /* @__PURE__ */ jsxRuntime.jsx(
851
+ reactHookForm.Controller,
852
+ {
853
+ name: field.name,
854
+ control,
855
+ defaultValue,
856
+ rules,
857
+ render: ({ field: formField }) => {
858
+ const isUploading = uploadingFiles[field.name] || false;
859
+ let files = [];
860
+ if (isMultiple) {
861
+ files = Array.isArray(formField.value) ? formField.value : [];
862
+ } else {
863
+ files = formField.value ? [formField.value] : [];
864
+ }
865
+ const hasFiles = files.length > 0;
866
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
867
+ /* @__PURE__ */ jsxRuntime.jsx(
868
+ material.FormLabel,
869
+ {
870
+ required: field.required,
871
+ error: !!errors[field.name],
872
+ children: field.label
873
+ }
874
+ ),
875
+ /* @__PURE__ */ jsxRuntime.jsxs(
876
+ material.Box,
877
+ {
878
+ component: "label",
879
+ htmlFor: `file-input-${field.name}`,
880
+ sx: {
881
+ display: "flex",
882
+ flexDirection: "column",
883
+ alignItems: "center",
884
+ justifyContent: "center",
885
+ border: "1px dashed",
886
+ borderColor: errors[field.name] ? formColors.error : isUploading ? formColors.primary : hasFiles ? formColors.primary : formColors.divider,
887
+ p: 1,
888
+ cursor: isUploading ? "wait" : "pointer",
889
+ transition: "all 0.2s ease-in-out",
890
+ backgroundColor: isUploading || hasFiles ? "action.hover" : formColors.backgroundPaper,
891
+ opacity: isUploading ? 0.7 : 1,
892
+ pointerEvents: isUploading ? "none" : "auto",
893
+ "&:hover": {
894
+ borderColor: isUploading ? formColors.primary : formColors.primary,
895
+ backgroundColor: "action.hover"
896
+ }
897
+ },
898
+ children: [
899
+ /* @__PURE__ */ jsxRuntime.jsx(
900
+ "input",
901
+ {
902
+ id: `file-input-${field.name}`,
903
+ type: "file",
904
+ hidden: true,
905
+ multiple: isMultiple,
906
+ accept: acceptTypes,
907
+ onChange: async (e) => {
908
+ const fileList = e.target.files;
909
+ if (!fileList || fileList.length === 0) return;
910
+ const newFiles = Array.from(fileList);
911
+ const isUploading2 = uploadingFiles[field.name] || false;
912
+ if (isUploading2) {
913
+ e.target.value = "";
914
+ return;
915
+ }
916
+ for (const file of newFiles) {
917
+ const validationResult = validateFile(file, field);
918
+ if (validationResult !== true) {
919
+ setError?.(field.name, {
920
+ type: "manual",
921
+ message: validationResult
922
+ });
923
+ e.target.value = "";
924
+ return;
925
+ }
926
+ }
927
+ try {
928
+ setUploadingFiles?.((prev) => ({ ...prev, [field.name]: true }));
929
+ clearErrors?.(field.name);
930
+ if (!formSchema?.name) {
931
+ throw new Error("Form schema name is required for file uploads");
932
+ }
933
+ const uploadedFiles = await fileUploadService.uploadFiles(
934
+ formSchema.name,
935
+ field.name,
936
+ newFiles
937
+ );
938
+ if (isMultiple) {
939
+ const currentValue = formField.value;
940
+ const existingFiles = Array.isArray(currentValue) ? currentValue.filter((item) => item && typeof item === "object" && "fileName" in item) : [];
941
+ const allUploadedFiles = [...existingFiles, ...uploadedFiles];
942
+ formField.onChange(allUploadedFiles);
943
+ } else {
944
+ formField.onChange(uploadedFiles[0] || null);
945
+ }
946
+ } catch (error) {
947
+ console.error(`Failed to upload files for field ${field.name}:`, error);
948
+ const errorMessage = error.response?.data?.message || error.message || "Failed to upload files";
949
+ setError?.(field.name, {
950
+ type: "manual",
951
+ message: `Failed to upload files: ${errorMessage}`
952
+ });
953
+ } finally {
954
+ setUploadingFiles?.((prev) => ({ ...prev, [field.name]: false }));
955
+ e.target.value = "";
956
+ }
957
+ },
958
+ disabled: uploadingFiles[field.name] || false
959
+ }
960
+ ),
961
+ isUploading ? /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", flexDirection: "column", alignItems: "center", gap: 1, py: 3, width: "100%" }, children: [
962
+ /* @__PURE__ */ jsxRuntime.jsx(material.CircularProgress, { size: 40 }),
963
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", sx: { fontWeight: 500, color: formColors.primary }, children: "Uploading files..." }),
964
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", sx: { color: formColors.textSecondary }, children: "Please wait while files are being uploaded" })
965
+ ] }) : !hasFiles ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
966
+ /* @__PURE__ */ jsxRuntime.jsx(CloudUploadIcon__default.default, { sx: { fontSize: 40, color: formColors.textSecondary, mb: 1 } }),
967
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", sx: { color: formColors.textSecondary }, children: [
968
+ "Click to upload or drag and drop",
969
+ isMultiple && " (multiple files allowed)"
970
+ ] }),
971
+ field.validation?.allowedFileTypes && field.validation.allowedFileTypes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", sx: { mt: 0.5, color: formColors.textSecondary }, children: [
972
+ "Allowed: ",
973
+ field.validation.allowedFileTypes.join(", ")
974
+ ] }),
975
+ field.validation?.maxFileSize && /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", sx: { mt: 0.5, color: formColors.textSecondary }, children: [
976
+ "Max size: ",
977
+ formatFileSize(field.validation.maxFileSize),
978
+ " per file"
979
+ ] })
980
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { width: "100%" }, children: [
981
+ isMultiple && /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", sx: { fontWeight: 500, mb: 1.5 }, children: [
982
+ files.length,
983
+ " file",
984
+ files.length !== 1 ? "s" : "",
985
+ " uploaded"
986
+ ] }),
987
+ files.length === 1 && !isMultiple ? /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", gap: 2, width: "100%" }, children: [
988
+ /* @__PURE__ */ jsxRuntime.jsx(InsertDriveFileIcon__default.default, { sx: { fontSize: 40, color: formColors.primary } }),
989
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { flexGrow: 1, minWidth: 0 }, children: [
990
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", sx: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", maxWidth: isMobile ? "200px" : "300px" }, children: files[0] instanceof File ? files[0].name : files[0].originalName || files[0].fileName }),
991
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", sx: { color: formColors.textSecondary }, children: formatFileSize(files[0].size || 0) })
992
+ ] }),
993
+ /* @__PURE__ */ jsxRuntime.jsx(material.Tooltip, { title: "Remove file", placement: "bottom", arrow: true, children: /* @__PURE__ */ jsxRuntime.jsx(
994
+ material.IconButton,
995
+ {
996
+ size: "small",
997
+ onClick: (e) => {
998
+ e.preventDefault();
999
+ e.stopPropagation();
1000
+ formField.onChange(null);
1001
+ const fileInput = document.getElementById(`file-input-${field.name}`);
1002
+ if (fileInput) {
1003
+ fileInput.value = "";
1004
+ }
1005
+ },
1006
+ sx: { color: formColors.error },
1007
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon__default.default, { fontSize: "small" })
1008
+ }
1009
+ ) })
1010
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(material.Box, { sx: { display: "flex", flexDirection: "column", gap: 1, width: "100%" }, children: files.map((file, index) => {
1011
+ const fileName = file instanceof File ? file.name : file.originalName || file.fileName;
1012
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1013
+ material.Box,
1014
+ {
1015
+ sx: {
1016
+ display: "flex",
1017
+ alignItems: "center",
1018
+ gap: 1.5,
1019
+ p: 1,
1020
+ borderRadius: 1,
1021
+ backgroundColor: formColors.background,
1022
+ border: "1px solid",
1023
+ borderColor: formColors.divider
1024
+ },
1025
+ children: [
1026
+ /* @__PURE__ */ jsxRuntime.jsx(InsertDriveFileIcon__default.default, { sx: { fontSize: 32, color: formColors.primary } }),
1027
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { flexGrow: 1, minWidth: 0 }, children: [
1028
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", sx: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: fileName }),
1029
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", sx: { color: formColors.textSecondary }, children: formatFileSize(file.size || 0) })
1030
+ ] }),
1031
+ /* @__PURE__ */ jsxRuntime.jsx(material.Tooltip, { title: "Remove file", placement: "bottom", arrow: true, children: /* @__PURE__ */ jsxRuntime.jsx(
1032
+ material.IconButton,
1033
+ {
1034
+ size: "small",
1035
+ onClick: (e) => {
1036
+ e.preventDefault();
1037
+ e.stopPropagation();
1038
+ if (isMultiple) {
1039
+ const updatedFiles = files.filter((_, i) => i !== index);
1040
+ formField.onChange(updatedFiles.length > 0 ? updatedFiles : []);
1041
+ } else {
1042
+ formField.onChange(null);
1043
+ }
1044
+ },
1045
+ sx: { color: formColors.error },
1046
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon__default.default, { fontSize: "small" })
1047
+ }
1048
+ ) })
1049
+ ]
1050
+ },
1051
+ `${fileName}-${index}`
1052
+ );
1053
+ }) })
1054
+ ] })
1055
+ ]
1056
+ }
1057
+ ),
1058
+ errors[field.name] && /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", sx: { mt: 1, display: "block", color: formColors.error }, children: errors[field.name]?.message })
1059
+ ] });
1060
+ }
1061
+ },
1062
+ field.name
1063
+ );
1064
+ };
1065
+
1066
+ // src/utils/formHelpers.ts
1067
+ var getDefaultValue = (field) => {
1068
+ switch (field.type) {
1069
+ case "checkbox":
1070
+ case "toggle":
1071
+ return false;
1072
+ case "datepicker":
1073
+ return null;
1074
+ case "file":
1075
+ return field.allowMultiple ? [] : null;
1076
+ case "select":
1077
+ case "formReference":
1078
+ case "apiReference":
1079
+ return field.allowMultiple ? [] : "";
1080
+ case "text":
1081
+ case "email":
1082
+ case "number":
1083
+ case "radio":
1084
+ case "ckeditor":
1085
+ default:
1086
+ return "";
1087
+ }
1088
+ };
1089
+ var FormReferenceField = ({ field, control, defaultValue, rules, errors, services }) => {
1090
+ const isMultiple = field.allowMultiple || false;
1091
+ const [options, setOptions] = react.useState([]);
1092
+ const [isLoading, setIsLoading] = react.useState(false);
1093
+ const formReferenceService = services?.formReference || defaultFormReferenceService;
1094
+ const fetchOptions = react.useCallback(() => {
1095
+ if (!field.referenceFormName || !field.referenceFieldName) return;
1096
+ setIsLoading(true);
1097
+ formReferenceService.fetchOptions(field.referenceFormName, field.referenceFieldName).then((opts) => {
1098
+ setOptions(opts);
1099
+ }).catch((error) => {
1100
+ console.error("Failed to fetch form reference options:", error);
1101
+ setOptions([]);
1102
+ }).finally(() => {
1103
+ setIsLoading(false);
1104
+ });
1105
+ }, [field.referenceFormName, field.referenceFieldName, formReferenceService]);
1106
+ react.useEffect(() => {
1107
+ fetchOptions();
1108
+ }, [fetchOptions]);
1109
+ const selectOptions = react.useMemo(() => {
1110
+ return options.map((opt) => ({
1111
+ value: String(opt.value),
1112
+ label: opt.label
1113
+ }));
1114
+ }, [options]);
1115
+ const fieldDefaultValue = defaultValue ?? getDefaultValue(field);
1116
+ const fieldRules = rules ?? buildFieldRules(field);
1117
+ const isDisabled = !field.referenceFormName || !field.referenceFieldName;
1118
+ return /* @__PURE__ */ jsxRuntime.jsx(
1119
+ reactHookForm.Controller,
1120
+ {
1121
+ name: field.name,
1122
+ control,
1123
+ defaultValue: fieldDefaultValue,
1124
+ rules: fieldRules,
1125
+ render: ({ field: formField }) => {
1126
+ return /* @__PURE__ */ jsxRuntime.jsx(
1127
+ SimpleSelect,
1128
+ {
1129
+ label: field.label,
1130
+ value: formField.value,
1131
+ onChange: (value) => {
1132
+ formField.onChange(value);
1133
+ },
1134
+ options: selectOptions,
1135
+ placeholder: field.placeholder || "Search and select...",
1136
+ helperText: errors[field.name]?.message,
1137
+ fullWidth: true,
1138
+ size: "small",
1139
+ required: field.required,
1140
+ error: !!errors[field.name],
1141
+ disabled: isDisabled || isLoading,
1142
+ multiple: isMultiple,
1143
+ isLoading,
1144
+ refreshable: true,
1145
+ onRefresh: fetchOptions
1146
+ }
1147
+ );
1148
+ }
1149
+ },
1150
+ field.name
1151
+ );
1152
+ };
1153
+ var ApiReferenceField = ({ field, control, defaultValue, rules, errors, services }) => {
1154
+ const isMultiple = field.allowMultiple || false;
1155
+ const [options, setOptions] = react.useState([]);
1156
+ const [isLoading, setIsLoading] = react.useState(false);
1157
+ const apiReferenceService = services?.apiReference || defaultApiReferenceService;
1158
+ const fetchOptions = react.useCallback(() => {
1159
+ if (!field.apiEndpoint || !field.apiLabelField) return;
1160
+ setIsLoading(true);
1161
+ apiReferenceService.fetchOptions(field.apiEndpoint, field.apiLabelField, field.apiValueField || "_id").then((opts) => {
1162
+ setOptions(opts);
1163
+ }).catch((error) => {
1164
+ console.error("Failed to fetch API reference options:", error);
1165
+ setOptions([]);
1166
+ }).finally(() => {
1167
+ setIsLoading(false);
1168
+ });
1169
+ }, [field.apiEndpoint, field.apiLabelField, field.apiValueField, apiReferenceService]);
1170
+ react.useEffect(() => {
1171
+ fetchOptions();
1172
+ }, [fetchOptions]);
1173
+ const selectOptions = react.useMemo(() => {
1174
+ return options.map((opt) => ({
1175
+ value: String(opt.value),
1176
+ label: opt.label
1177
+ }));
1178
+ }, [options]);
1179
+ const fieldDefaultValue = defaultValue ?? getDefaultValue(field);
1180
+ const fieldRules = rules ?? buildFieldRules(field);
1181
+ const isDisabled = !field.apiEndpoint || !field.apiLabelField;
1182
+ return /* @__PURE__ */ jsxRuntime.jsx(
1183
+ reactHookForm.Controller,
1184
+ {
1185
+ name: field.name,
1186
+ control,
1187
+ defaultValue: fieldDefaultValue,
1188
+ rules: fieldRules,
1189
+ render: ({ field: formField }) => {
1190
+ return /* @__PURE__ */ jsxRuntime.jsx(
1191
+ SimpleSelect,
1192
+ {
1193
+ label: field.label,
1194
+ value: formField.value,
1195
+ onChange: (value) => {
1196
+ formField.onChange(value);
1197
+ },
1198
+ options: selectOptions,
1199
+ placeholder: field.placeholder || "Search and select...",
1200
+ helperText: errors[field.name]?.message,
1201
+ fullWidth: true,
1202
+ size: "small",
1203
+ required: field.required,
1204
+ error: !!errors[field.name],
1205
+ disabled: isDisabled || isLoading,
1206
+ multiple: isMultiple,
1207
+ isLoading,
1208
+ refreshable: true,
1209
+ onRefresh: fetchOptions
1210
+ }
1211
+ );
1212
+ }
1213
+ },
1214
+ field.name
1215
+ );
1216
+ };
1217
+
1218
+ exports.ApiReferenceField = ApiReferenceField;
1219
+ exports.CKEditorField = CKEditorField;
1220
+ exports.CheckboxField = CheckboxField;
1221
+ exports.ColorField = ColorField;
1222
+ exports.DateTimePickerField = DateTimePickerField;
1223
+ exports.FileField = FileField;
1224
+ exports.FormReferenceField = FormReferenceField;
1225
+ exports.RadioField = RadioField;
1226
+ exports.SelectField = SelectField;
1227
+ exports.TextField = TextField;
1228
+ exports.ToggleField = ToggleField;
1229
+ //# sourceMappingURL=fields.js.map
1230
+ //# sourceMappingURL=fields.js.map