@stormlmd/form-builder 0.1.0 → 0.3.1
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/build_output.txt +0 -0
- package/package.json +1 -1
- package/src/components/FieldRegistry.ts +16 -18
- package/src/components/FormRenderer.tsx +4 -2
- package/src/components/builder/EditorWrapper.tsx +3 -1
- package/src/components/builder/FormBuilder.tsx +20 -2
- package/src/components/builder/FormChildrenRenderer.tsx +3 -1
- package/src/components/builder/FormulaHelp.tsx +116 -0
- package/src/components/builder/IntegrationSettings.tsx +3 -1
- package/src/components/builder/OptionsEditor.tsx +90 -0
- package/src/components/builder/PropertiesModal.tsx +2 -1
- package/src/components/builder/PropertiesPanel.tsx +155 -62
- package/src/components/builder/SortableNode.tsx +2 -1
- package/src/components/builder/Toolbox.tsx +30 -63
- package/src/components/fields/CheckboxField.tsx +2 -1
- package/src/components/fields/DateField.tsx +2 -1
- package/src/components/fields/FileUploadField.tsx +2 -1
- package/src/components/fields/LabelField.tsx +2 -1
- package/src/components/fields/NumberField.tsx +2 -1
- package/src/components/fields/RichTextField.tsx +2 -1
- package/src/components/fields/SelectField.tsx +8 -2
- package/src/components/fields/TextField.tsx +2 -1
- package/src/components/layout/FormRepeater.tsx +2 -1
- package/src/components/layout/FormTabs.tsx +2 -1
- package/src/components/registerComponents.ts +69 -14
- package/src/index.ts +7 -0
- package/src/plugins/FieldPlugin.ts +63 -0
- package/src/plugins/PluginRegistry.ts +94 -0
- package/src/store/FormStore.ts +72 -24
- package/src/store/FormStoreContext.tsx +66 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Box, Typography, TextField, FormControl, FormLabel, Select, MenuItem, Button, Popper, Paper, List, ListItemButton, ListItemText, ClickAwayListener, Tabs, Tab, InputLabel } from '@mui/material';
|
|
2
|
+
import { Box, Typography, TextField, FormControl, FormLabel, Select, MenuItem, Button, Popper, Paper, List, ListItemButton, ListItemText, ClickAwayListener, Tabs, Tab, InputLabel, IconButton, InputAdornment, Tooltip } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react-lite';
|
|
4
|
-
import {
|
|
4
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
5
5
|
import type { SchemaNode } from '../../store/FormStore';
|
|
6
6
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
7
|
+
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
|
7
8
|
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { getPlugin } from '../../plugins/PluginRegistry';
|
|
10
|
+
import { FormulaHelp } from './FormulaHelp';
|
|
8
11
|
|
|
9
12
|
interface TabPanelProps {
|
|
10
13
|
children?: React.ReactNode;
|
|
@@ -38,6 +41,48 @@ function CustomTabPanel(props: TabPanelProps) {
|
|
|
38
41
|
);
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
const FormulaHints: React.FC<{
|
|
45
|
+
node: SchemaNode,
|
|
46
|
+
formStore: any,
|
|
47
|
+
insertText: (text: string) => void,
|
|
48
|
+
t: any
|
|
49
|
+
}> = ({ node, formStore, insertText, t }) => {
|
|
50
|
+
return (
|
|
51
|
+
<Box mt={1} display="flex" flexWrap="wrap" gap={0.5}>
|
|
52
|
+
<Typography variant="caption" width="100%" color="text.secondary">{t("properties.hints")}</Typography>
|
|
53
|
+
{['today', 'now', ...formStore.getAllFieldNames()]
|
|
54
|
+
.filter(name => name !== node.props?.name)
|
|
55
|
+
.map(name => (
|
|
56
|
+
<Button
|
|
57
|
+
key={name}
|
|
58
|
+
size="small"
|
|
59
|
+
variant="outlined"
|
|
60
|
+
sx={{ textTransform: 'none', px: 1, py: 0, fontSize: '0.7rem' }}
|
|
61
|
+
onClick={() => insertText(`{{${name}}}`)}
|
|
62
|
+
>
|
|
63
|
+
{name}
|
|
64
|
+
</Button>
|
|
65
|
+
))}
|
|
66
|
+
|
|
67
|
+
<Box width="100%" mt={1}>
|
|
68
|
+
<Typography variant="caption" color="text.secondary">{t("properties.functions") || "Functions"}</Typography>
|
|
69
|
+
</Box>
|
|
70
|
+
{['IF()', 'AND()', 'OR()', 'NOT()', 'ABS()', 'ROUND()', 'SWITCH()', 'IFS()', 'DATE_ADD()', 'DATE_DIFF()'].map(func => (
|
|
71
|
+
<Button
|
|
72
|
+
key={func}
|
|
73
|
+
size="small"
|
|
74
|
+
variant="outlined"
|
|
75
|
+
color="secondary"
|
|
76
|
+
sx={{ textTransform: 'none', px: 1, py: 0, fontSize: '0.7rem' }}
|
|
77
|
+
onClick={() => insertText(func)}
|
|
78
|
+
>
|
|
79
|
+
{func}
|
|
80
|
+
</Button>
|
|
81
|
+
))}
|
|
82
|
+
</Box>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
41
86
|
function a11yProps(index: number) {
|
|
42
87
|
return {
|
|
43
88
|
id: `simple-tab-${index}`,
|
|
@@ -46,11 +91,15 @@ function a11yProps(index: number) {
|
|
|
46
91
|
}
|
|
47
92
|
|
|
48
93
|
export const PropertiesPanel: React.FC = observer(() => {
|
|
94
|
+
const formStore = useFormStore();
|
|
49
95
|
const { t } = useTranslation();
|
|
50
96
|
const node = formStore.selectedNode;
|
|
51
97
|
const [tabValue, setTabValue] = React.useState(0);
|
|
98
|
+
const [helpOpen, setHelpOpen] = React.useState(false);
|
|
52
99
|
|
|
53
|
-
const
|
|
100
|
+
const BUILT_IN_LAYOUT_TYPES = ['root', 'row', 'col', 'tabs', 'tab', 'paper', 'repeater', 'table', 'cell', 'divider'];
|
|
101
|
+
const isLayout = node ? BUILT_IN_LAYOUT_TYPES.includes(node.type) : false;
|
|
102
|
+
const plugin = node ? getPlugin(node.type) : undefined;
|
|
54
103
|
|
|
55
104
|
const allTabs = [
|
|
56
105
|
{ label: t("properties.tabView"), index: 0, visible: true },
|
|
@@ -92,6 +141,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
92
141
|
};
|
|
93
142
|
|
|
94
143
|
const handleDelete = () => {
|
|
144
|
+
if (node.id === 'root') return;
|
|
95
145
|
formStore.removeNode(node.id);
|
|
96
146
|
formStore.closePropertiesModal(); // Also close modal on delete
|
|
97
147
|
};
|
|
@@ -154,58 +204,22 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
154
204
|
setAnchorEl(null);
|
|
155
205
|
};
|
|
156
206
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<Box mt={1} display="flex" flexWrap="wrap" gap={0.5}>
|
|
177
|
-
<Typography variant="caption" width="100%" color="text.secondary">{t("properties.hints")}</Typography>
|
|
178
|
-
{['today', 'now', ...formStore.getAllFieldNames()]
|
|
179
|
-
.filter(name => name !== node.props?.name)
|
|
180
|
-
.map(name => (
|
|
181
|
-
<Button
|
|
182
|
-
key={name}
|
|
183
|
-
size="small"
|
|
184
|
-
variant="outlined"
|
|
185
|
-
sx={{ textTransform: 'none', px: 1, py: 0, fontSize: '0.7rem' }}
|
|
186
|
-
onClick={() => insertText(`{{${name}}}`)}
|
|
187
|
-
>
|
|
188
|
-
{name}
|
|
189
|
-
</Button>
|
|
190
|
-
))}
|
|
191
|
-
|
|
192
|
-
<Box width="100%" mt={1}>
|
|
193
|
-
<Typography variant="caption" color="text.secondary">{t("properties.functions") || "Functions"}</Typography>
|
|
194
|
-
</Box>
|
|
195
|
-
{['IF()', 'AND()', 'OR()', 'NOT()', 'ABS()', 'ROUND()'].map(func => (
|
|
196
|
-
<Button
|
|
197
|
-
key={func}
|
|
198
|
-
size="small"
|
|
199
|
-
variant="outlined"
|
|
200
|
-
color="secondary"
|
|
201
|
-
sx={{ textTransform: 'none', px: 1, py: 0, fontSize: '0.7rem' }}
|
|
202
|
-
onClick={() => insertText(func)}
|
|
203
|
-
>
|
|
204
|
-
{func}
|
|
205
|
-
</Button>
|
|
206
|
-
))}
|
|
207
|
-
</Box>
|
|
208
|
-
);
|
|
207
|
+
const insertText = (fieldType: string, text: string) => {
|
|
208
|
+
if (!node) return;
|
|
209
|
+
let current = '';
|
|
210
|
+
if (fieldType === 'calculation') current = node.calculation?.formula || '';
|
|
211
|
+
else if (fieldType === 'defaultValue') current = node.defaultValue || '';
|
|
212
|
+
else if (fieldType === 'min') current = String(node.validation?.min || '');
|
|
213
|
+
else if (fieldType === 'max') current = String(node.validation?.max || '');
|
|
214
|
+
|
|
215
|
+
const newVal = current + text;
|
|
216
|
+
if (fieldType === 'calculation') formStore.updateNodeCalculation(node.id, { formula: newVal });
|
|
217
|
+
else if (fieldType === 'defaultValue') {
|
|
218
|
+
formStore.updateNode(node.id, { defaultValue: newVal });
|
|
219
|
+
formStore.applyDefaultValues();
|
|
220
|
+
}
|
|
221
|
+
else if (fieldType === 'min') formStore.updateNodeValidation(node.id, { min: newVal });
|
|
222
|
+
else if (fieldType === 'max') formStore.updateNodeValidation(node.id, { max: newVal });
|
|
209
223
|
};
|
|
210
224
|
|
|
211
225
|
return (
|
|
@@ -236,7 +250,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
236
250
|
<Box>
|
|
237
251
|
<Typography variant="subtitle1" fontWeight="bold">{t('properties.title')}</Typography>
|
|
238
252
|
<Typography variant="caption" color="text.secondary">
|
|
239
|
-
{node.type.toUpperCase()} | ID: {node.id}
|
|
253
|
+
{(node.type || 'unknown').toUpperCase()} | ID: {node.id || 'none'}
|
|
240
254
|
</Typography>
|
|
241
255
|
</Box>
|
|
242
256
|
<Button
|
|
@@ -373,6 +387,17 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
373
387
|
size="small"
|
|
374
388
|
fullWidth
|
|
375
389
|
value={node.defaultValue ?? ''}
|
|
390
|
+
InputProps={{
|
|
391
|
+
endAdornment: (
|
|
392
|
+
<InputAdornment position="end">
|
|
393
|
+
<Tooltip title="Справка по формулам">
|
|
394
|
+
<IconButton size="small" onClick={() => setHelpOpen(true)}>
|
|
395
|
+
<HelpOutlineIcon fontSize="small" />
|
|
396
|
+
</IconButton>
|
|
397
|
+
</Tooltip>
|
|
398
|
+
</InputAdornment>
|
|
399
|
+
),
|
|
400
|
+
}}
|
|
376
401
|
onChange={(e) => {
|
|
377
402
|
const val = e.target.value;
|
|
378
403
|
formStore.updateNode(node.id, { defaultValue: val });
|
|
@@ -384,7 +409,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
384
409
|
setCursorPos(target.selectionStart || 0);
|
|
385
410
|
}}
|
|
386
411
|
/>
|
|
387
|
-
<FormulaHints
|
|
412
|
+
<FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('defaultValue', txt)} t={t} />
|
|
388
413
|
</>
|
|
389
414
|
)}
|
|
390
415
|
</Box>
|
|
@@ -416,13 +441,31 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
416
441
|
|
|
417
442
|
{node.type === 'select' && (
|
|
418
443
|
<Box display="flex" flexDirection="column" gap={2} mt={2}>
|
|
444
|
+
<TextField
|
|
445
|
+
label={t('properties.optionsSource') || 'Options Source (Constant Name)'}
|
|
446
|
+
size="small"
|
|
447
|
+
fullWidth
|
|
448
|
+
value={node.props?.optionsSource || ''}
|
|
449
|
+
onChange={(e) => handleChange('optionsSource', e.target.value || undefined)}
|
|
450
|
+
helperText={
|
|
451
|
+
node.props?.optionsSource
|
|
452
|
+
? `${t('properties.optionsSourceActive') || 'Dynamic list active'}: formStore.setConstant('${node.props.optionsSource}', [...])`
|
|
453
|
+
: (t('properties.optionsSourceHelper') || 'Name of a constant set via setConstant(). Leave empty for static options.')
|
|
454
|
+
}
|
|
455
|
+
placeholder="e.g. userList"
|
|
456
|
+
/>
|
|
457
|
+
{!!node.props?.optionsSource && (
|
|
458
|
+
<Typography variant="caption" color="info.main">
|
|
459
|
+
{t('properties.optionsSourceHint') || 'Static options below are ignored when Options Source is set.'}
|
|
460
|
+
</Typography>
|
|
461
|
+
)}
|
|
419
462
|
<TextField
|
|
420
463
|
label={t('properties.selectOptions')}
|
|
421
464
|
multiline
|
|
422
465
|
rows={6}
|
|
423
466
|
size="small"
|
|
424
467
|
fullWidth
|
|
425
|
-
disabled={!!node.props?.dictionaryInfo}
|
|
468
|
+
disabled={!!node.props?.dictionaryInfo || !!node.props?.optionsSource}
|
|
426
469
|
value={(node.props?.options || []).map((opt: any) => opt.label).join('\n')}
|
|
427
470
|
onChange={(e) => {
|
|
428
471
|
const lines = e.target.value.split('\n');
|
|
@@ -433,7 +476,9 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
433
476
|
}}
|
|
434
477
|
helperText={node.props?.dictionaryInfo
|
|
435
478
|
? "Options are managed by API dictionary"
|
|
436
|
-
:
|
|
479
|
+
: node.props?.optionsSource
|
|
480
|
+
? (t('properties.optionsSourceOverride') || 'Overridden by Options Source above')
|
|
481
|
+
: t('properties.selectOptionsHelper')}
|
|
437
482
|
/>
|
|
438
483
|
<Box display="flex" alignItems="center" mt={1}>
|
|
439
484
|
<input
|
|
@@ -595,6 +640,19 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
595
640
|
/>
|
|
596
641
|
</Box>
|
|
597
642
|
)}
|
|
643
|
+
|
|
644
|
+
{/* Plugin custom properties editor */}
|
|
645
|
+
{plugin?.propertiesEditor && (
|
|
646
|
+
<Box mt={2}>
|
|
647
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={1}>
|
|
648
|
+
{plugin.label} Settings
|
|
649
|
+
</Typography>
|
|
650
|
+
{React.createElement(plugin.propertiesEditor, {
|
|
651
|
+
node,
|
|
652
|
+
onChange: handleChange,
|
|
653
|
+
})}
|
|
654
|
+
</Box>
|
|
655
|
+
)}
|
|
598
656
|
</CustomTabPanel>
|
|
599
657
|
|
|
600
658
|
{/* Validation Tab */}
|
|
@@ -649,6 +707,17 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
649
707
|
size="small"
|
|
650
708
|
fullWidth
|
|
651
709
|
value={node.validation?.min ?? ''}
|
|
710
|
+
InputProps={{
|
|
711
|
+
endAdornment: (
|
|
712
|
+
<InputAdornment position="end">
|
|
713
|
+
<Tooltip title="Справка по формулам">
|
|
714
|
+
<IconButton size="small" onClick={() => setHelpOpen(true)}>
|
|
715
|
+
<HelpOutlineIcon fontSize="small" />
|
|
716
|
+
</IconButton>
|
|
717
|
+
</Tooltip>
|
|
718
|
+
</InputAdornment>
|
|
719
|
+
),
|
|
720
|
+
}}
|
|
652
721
|
onChange={(e) => {
|
|
653
722
|
const val = e.target.value;
|
|
654
723
|
formStore.updateNodeValidation(node.id, { min: val });
|
|
@@ -660,7 +729,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
660
729
|
}}
|
|
661
730
|
placeholder="e.g. 10 or {{otherField}} + 5"
|
|
662
731
|
/>
|
|
663
|
-
<FormulaHints
|
|
732
|
+
<FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('min', txt)} t={t} />
|
|
664
733
|
</Box>
|
|
665
734
|
<Box mb={1}>
|
|
666
735
|
<TextField
|
|
@@ -668,6 +737,17 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
668
737
|
size="small"
|
|
669
738
|
fullWidth
|
|
670
739
|
value={node.validation?.max ?? ''}
|
|
740
|
+
InputProps={{
|
|
741
|
+
endAdornment: (
|
|
742
|
+
<InputAdornment position="end">
|
|
743
|
+
<Tooltip title="Справка по формулам">
|
|
744
|
+
<IconButton size="small" onClick={() => setHelpOpen(true)}>
|
|
745
|
+
<HelpOutlineIcon fontSize="small" />
|
|
746
|
+
</IconButton>
|
|
747
|
+
</Tooltip>
|
|
748
|
+
</InputAdornment>
|
|
749
|
+
),
|
|
750
|
+
}}
|
|
671
751
|
onChange={(e) => {
|
|
672
752
|
const val = e.target.value;
|
|
673
753
|
formStore.updateNodeValidation(node.id, { max: val });
|
|
@@ -679,7 +759,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
679
759
|
}}
|
|
680
760
|
placeholder="e.g. 100 or {{otherField}} * 2"
|
|
681
761
|
/>
|
|
682
|
-
<FormulaHints
|
|
762
|
+
<FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('max', txt)} t={t} />
|
|
683
763
|
</Box>
|
|
684
764
|
</Box>
|
|
685
765
|
)}
|
|
@@ -800,6 +880,17 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
800
880
|
fullWidth
|
|
801
881
|
placeholder="e.g. {{price}} * {{qty}}"
|
|
802
882
|
value={node.calculation?.formula || ''}
|
|
883
|
+
InputProps={{
|
|
884
|
+
endAdornment: (
|
|
885
|
+
<InputAdornment position="end">
|
|
886
|
+
<Tooltip title="Справка по формулам">
|
|
887
|
+
<IconButton size="small" onClick={() => setHelpOpen(true)}>
|
|
888
|
+
<HelpOutlineIcon fontSize="small" />
|
|
889
|
+
</IconButton>
|
|
890
|
+
</Tooltip>
|
|
891
|
+
</InputAdornment>
|
|
892
|
+
),
|
|
893
|
+
}}
|
|
803
894
|
onChange={(e) => {
|
|
804
895
|
const val = e.target.value;
|
|
805
896
|
formStore.updateNodeCalculation(node.id, val ? { formula: val } : undefined);
|
|
@@ -811,7 +902,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
811
902
|
}}
|
|
812
903
|
helperText={t("properties.formulaHelper")}
|
|
813
904
|
/>
|
|
814
|
-
<FormulaHints
|
|
905
|
+
<FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('calculation', txt)} t={t} />
|
|
815
906
|
</CustomTabPanel>
|
|
816
907
|
|
|
817
908
|
{/* API Mapping Tab */}
|
|
@@ -853,6 +944,8 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
853
944
|
</FormControl>
|
|
854
945
|
)}
|
|
855
946
|
</CustomTabPanel>
|
|
947
|
+
|
|
948
|
+
<FormulaHelp open={helpOpen} onClose={() => setHelpOpen(false)} />
|
|
856
949
|
</Box>
|
|
857
950
|
);
|
|
858
951
|
});
|
|
@@ -4,7 +4,7 @@ import { CSS } from '@dnd-kit/utilities';
|
|
|
4
4
|
import { Box, type SxProps, type Theme } from '@mui/material';
|
|
5
5
|
|
|
6
6
|
import { observer } from 'mobx-react-lite';
|
|
7
|
-
import {
|
|
7
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
8
8
|
|
|
9
9
|
interface SortableNodeProps {
|
|
10
10
|
id: string;
|
|
@@ -13,6 +13,7 @@ interface SortableNodeProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const SortableNode: React.FC<SortableNodeProps> = observer(({ id, children, sx }) => {
|
|
16
|
+
const formStore = useFormStore();
|
|
16
17
|
const {
|
|
17
18
|
attributes,
|
|
18
19
|
listeners,
|
|
@@ -1,68 +1,35 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Box, List, ListItem, Tabs, Tab } from '@mui/material';
|
|
3
|
-
import {
|
|
3
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
4
4
|
import { generateId } from '../../utils/idGenerator';
|
|
5
5
|
import { DraggableTool } from './DraggableTool';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle';
|
|
11
|
-
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
|
12
|
-
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
|
13
|
-
import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
|
|
14
|
-
import LabelIcon from '@mui/icons-material/Label';
|
|
15
|
-
import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
|
|
16
|
-
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
|
|
17
|
-
import TabIcon from '@mui/icons-material/Tab';
|
|
18
|
-
import RepeatIcon from '@mui/icons-material/Repeat';
|
|
19
|
-
import TableChartIcon from '@mui/icons-material/TableChart';
|
|
20
|
-
import LayersIcon from '@mui/icons-material/Layers';
|
|
21
|
-
|
|
22
|
-
interface ToolItem {
|
|
23
|
-
type: NodeType;
|
|
24
|
-
label: string;
|
|
25
|
-
icon: React.ReactNode;
|
|
26
|
-
defaultProps?: Record<string, any>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const TOOLS: ToolItem[] = [
|
|
30
|
-
{ type: 'text', label: 'Text Field', icon: <TextFieldsIcon />, defaultProps: { label: 'New Text Field', width: 6 } },
|
|
31
|
-
{ type: 'number', label: 'Number Field', icon: <NumbersIcon />, defaultProps: { label: 'New Number Field', width: 6 } },
|
|
32
|
-
{ type: 'checkbox', label: 'Checkbox', icon: <CheckBoxIcon />, defaultProps: { label: 'New Checkbox', width: 6 } },
|
|
33
|
-
{ type: 'select', label: 'Select', icon: <ArrowDropDownCircleIcon />, defaultProps: { label: 'New Select', width: 6, options: [{ label: 'Option 1', value: '1' }] } },
|
|
34
|
-
{ type: 'date', label: 'Date Picker', icon: <CalendarTodayIcon />, defaultProps: { label: 'New Date', width: 6 } },
|
|
35
|
-
{ type: 'file', label: 'File Upload', icon: <CloudUploadIcon />, defaultProps: { label: 'New File Upload', width: 6 } },
|
|
36
|
-
{ type: 'richtext', label: 'Rich Text', icon: <FormatQuoteIcon />, defaultProps: { label: 'New Rich Text', width: 6 } },
|
|
37
|
-
{ type: 'label', label: 'Label', icon: <LabelIcon />, defaultProps: { text: 'Static Tex', variant: 'body1', width: 6 } },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const LAYOUT_TOOLS: ToolItem[] = [
|
|
41
|
-
{ type: 'divider', label: 'Divider', icon: <HorizontalRuleIcon />, defaultProps: { text: '' } },
|
|
42
|
-
{ type: 'col', label: 'Column', icon: <ViewColumnIcon />, defaultProps: { cols: 6 } },
|
|
43
|
-
{ type: 'tabs', label: 'Tabs', icon: <TabIcon />, defaultProps: {} },
|
|
44
|
-
{ type: 'repeater', label: 'Repeater', icon: <RepeatIcon />, defaultProps: { label: 'Repeater', addLabel: 'addItem' } },
|
|
45
|
-
{ type: 'table', label: 'Table', icon: <TableChartIcon />, defaultProps: { rows: 2, cols: 2, width: 12 } },
|
|
46
|
-
{ type: 'paper', label: 'Paper', icon: <LayersIcon />, defaultProps: { label: 'Paper Group', padding: 2, variant: 'elevation', elevation: 1 } },
|
|
47
|
-
];
|
|
7
|
+
import { getToolboxItems } from '../../plugins/PluginRegistry';
|
|
8
|
+
import type { FieldPlugin } from '../../plugins/FieldPlugin';
|
|
9
|
+
import ExtensionIcon from '@mui/icons-material/Extension';
|
|
48
10
|
|
|
49
11
|
export const Toolbox: React.FC = () => {
|
|
12
|
+
const formStore = useFormStore();
|
|
50
13
|
const { t } = useTranslation();
|
|
51
14
|
const [activeTab, setActiveTab] = useState(0);
|
|
52
15
|
|
|
53
|
-
|
|
16
|
+
// Get all plugins that should appear in the Toolbox
|
|
17
|
+
const fieldPlugins = getToolboxItems('field');
|
|
18
|
+
const layoutPlugins = getToolboxItems('layout');
|
|
19
|
+
|
|
20
|
+
const handleAdd = (plugin: FieldPlugin) => {
|
|
54
21
|
const parentId = formStore.selectedNodeId || 'root';
|
|
55
22
|
const newNode = {
|
|
56
|
-
id: generateId(
|
|
57
|
-
type:
|
|
58
|
-
props: { ...
|
|
23
|
+
id: generateId(plugin.type),
|
|
24
|
+
type: plugin.type,
|
|
25
|
+
props: { ...plugin.defaultProps, name: generateId('field'), label: t(`toolbox.${plugin.type}`, plugin.label) },
|
|
59
26
|
children: [] as any[]
|
|
60
27
|
};
|
|
61
28
|
|
|
62
29
|
// Special initialization for Table
|
|
63
|
-
if (
|
|
64
|
-
const rows =
|
|
65
|
-
const cols =
|
|
30
|
+
if (plugin.type === 'table') {
|
|
31
|
+
const rows = plugin.defaultProps?.rows || 2;
|
|
32
|
+
const cols = plugin.defaultProps?.cols || 2;
|
|
66
33
|
const cells = [];
|
|
67
34
|
for (let i = 0; i < rows * cols; i++) {
|
|
68
35
|
cells.push({
|
|
@@ -89,14 +56,14 @@ export const Toolbox: React.FC = () => {
|
|
|
89
56
|
<Box flexGrow={1} overflow="auto">
|
|
90
57
|
{activeTab === 0 && (
|
|
91
58
|
<List sx={{ pt: 1 }}>
|
|
92
|
-
{
|
|
93
|
-
<ListItem key={
|
|
59
|
+
{fieldPlugins.map((plugin) => (
|
|
60
|
+
<ListItem key={plugin.type} disablePadding sx={{ px: 2, py: 0.5 }}>
|
|
94
61
|
<DraggableTool
|
|
95
|
-
type={
|
|
96
|
-
label={t(`toolbox.${
|
|
97
|
-
icon={
|
|
98
|
-
defaultProps={
|
|
99
|
-
onClick={() => handleAdd(
|
|
62
|
+
type={plugin.type}
|
|
63
|
+
label={t(`toolbox.${plugin.type}`, plugin.label)}
|
|
64
|
+
icon={plugin.icon || <ExtensionIcon />}
|
|
65
|
+
defaultProps={plugin.defaultProps}
|
|
66
|
+
onClick={() => handleAdd(plugin)}
|
|
100
67
|
/>
|
|
101
68
|
</ListItem>
|
|
102
69
|
))}
|
|
@@ -104,14 +71,14 @@ export const Toolbox: React.FC = () => {
|
|
|
104
71
|
)}
|
|
105
72
|
{activeTab === 1 && (
|
|
106
73
|
<List sx={{ pt: 1 }}>
|
|
107
|
-
{
|
|
108
|
-
<ListItem key={
|
|
74
|
+
{layoutPlugins.map((plugin) => (
|
|
75
|
+
<ListItem key={plugin.type} disablePadding sx={{ px: 2, py: 0.5 }}>
|
|
109
76
|
<DraggableTool
|
|
110
|
-
type={
|
|
111
|
-
label={t(`toolbox.${
|
|
112
|
-
icon={
|
|
113
|
-
defaultProps={
|
|
114
|
-
onClick={() => handleAdd(
|
|
77
|
+
type={plugin.type}
|
|
78
|
+
label={t(`toolbox.${plugin.type}`, plugin.label)}
|
|
79
|
+
icon={plugin.icon || <ExtensionIcon />}
|
|
80
|
+
defaultProps={plugin.defaultProps}
|
|
81
|
+
onClick={() => handleAdd(plugin)}
|
|
115
82
|
/>
|
|
116
83
|
</ListItem>
|
|
117
84
|
))}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Checkbox, FormControlLabel, FormGroup, Box, Typography } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react-lite';
|
|
4
|
-
import {
|
|
4
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
5
5
|
import type { FieldProps } from '../FieldRegistry';
|
|
6
6
|
|
|
7
7
|
export const CheckboxField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const formStore = useFormStore();
|
|
8
9
|
const { name, label } = node.props || {};
|
|
9
10
|
const fullPath = path && name ? `${path}.${name}` : name;
|
|
10
11
|
const value = fullPath ? formStore.getValue(fullPath) : false;
|
|
@@ -3,11 +3,12 @@ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
|
3
3
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
4
4
|
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
|
5
5
|
import { observer } from 'mobx-react-lite';
|
|
6
|
-
import {
|
|
6
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
7
7
|
import type { FieldProps } from '../FieldRegistry';
|
|
8
8
|
import { Box, Typography, TextField } from '@mui/material';
|
|
9
9
|
|
|
10
10
|
export const DateField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
11
|
+
const formStore = useFormStore();
|
|
11
12
|
const { name, label, placeholder } = node.props || {};
|
|
12
13
|
const fullPath = path && name ? `${path}.${name}` : name;
|
|
13
14
|
const value = fullPath ? formStore.getValue(fullPath) : null;
|
|
@@ -2,10 +2,11 @@ import React from 'react';
|
|
|
2
2
|
import { Box, Button, Typography, Input, FormHelperText } from '@mui/material';
|
|
3
3
|
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
|
4
4
|
import { observer } from 'mobx-react-lite';
|
|
5
|
-
import {
|
|
5
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
6
6
|
import type { FieldProps } from '../FieldRegistry';
|
|
7
7
|
|
|
8
8
|
export const FileUploadField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
9
|
+
const formStore = useFormStore();
|
|
9
10
|
const { name, label } = node.props || {};
|
|
10
11
|
const fullPath = path && name ? `${path}.${name}` : name;
|
|
11
12
|
const value = fullPath ? formStore.getValue(fullPath) : null;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Typography, Box } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react-lite';
|
|
4
|
-
import {
|
|
4
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
5
5
|
import type { FieldProps } from '../FieldRegistry';
|
|
6
6
|
|
|
7
7
|
export const LabelField: React.FC<FieldProps> = observer(({ node }) => {
|
|
8
|
+
const formStore = useFormStore();
|
|
8
9
|
const { name, label = '', align = 'left', variant = 'body1' } = node.props || {};
|
|
9
10
|
|
|
10
11
|
const value = name ? formStore.getValue(name) : undefined;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { TextField as MuiTextField, Box, Typography } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react-lite';
|
|
4
|
-
import {
|
|
4
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
5
5
|
import type { FieldProps } from '../FieldRegistry';
|
|
6
6
|
|
|
7
7
|
export const NumberField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const formStore = useFormStore();
|
|
8
9
|
const { name, label } = node.props || {};
|
|
9
10
|
|
|
10
11
|
const fullPath = path && name ? `${path}.${name}` : name;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { TextField as MuiTextField, Typography, Box } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react-lite';
|
|
4
|
-
import {
|
|
4
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
5
5
|
import type { FieldProps } from '../FieldRegistry';
|
|
6
6
|
|
|
7
7
|
export const RichTextField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const formStore = useFormStore();
|
|
8
9
|
const { name, label, placeholder } = node.props || {};
|
|
9
10
|
const fullPath = path && name ? `${path}.${name}` : name;
|
|
10
11
|
const value = fullPath ? formStore.getValue(fullPath) : '';
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { FormControl, Select, MenuItem, Box, Typography, FormHelperText, Autocomplete, TextField as MuiTextField } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react-lite';
|
|
4
|
-
import {
|
|
4
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
5
5
|
import type { FieldProps } from '../FieldRegistry';
|
|
6
6
|
|
|
7
7
|
export const SelectField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
-
const
|
|
8
|
+
const formStore = useFormStore();
|
|
9
|
+
const { name, label, options: staticOptions = [], optionsSource, enableAutocomplete = false, placeholder } = node.props || {};
|
|
10
|
+
|
|
11
|
+
// Resolve dynamic options if optionsSource is provided
|
|
12
|
+
const dynamicOptions = optionsSource ? (formStore.externalConstants[optionsSource] || []) : [];
|
|
13
|
+
const options = optionsSource ? dynamicOptions : staticOptions;
|
|
14
|
+
|
|
9
15
|
const fullPath = path && name ? `${path}.${name}` : name;
|
|
10
16
|
const value = fullPath ? formStore.getValue(fullPath) : '';
|
|
11
17
|
const error = fullPath ? formStore.errors[fullPath] : undefined;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { TextField as MuiTextField, Box, Typography } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react-lite';
|
|
4
|
-
import {
|
|
4
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
5
5
|
import type { FieldProps } from '../FieldRegistry';
|
|
6
6
|
|
|
7
7
|
export const TextField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const formStore = useFormStore();
|
|
8
9
|
const { name, label, placeholder } = node.props || {};
|
|
9
10
|
|
|
10
11
|
// Resolve full path: path.name or just name
|
|
@@ -3,12 +3,13 @@ import { Box, Button, Typography, Paper, IconButton } from '@mui/material';
|
|
|
3
3
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
4
4
|
import AddIcon from '@mui/icons-material/Add';
|
|
5
5
|
import { observer } from 'mobx-react-lite';
|
|
6
|
-
import {
|
|
6
|
+
import { useFormStore } from '../../store/FormStoreContext';
|
|
7
7
|
import type { FieldProps } from '../FieldRegistry';
|
|
8
8
|
import { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
9
9
|
import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
|
|
10
10
|
|
|
11
11
|
export const FormRepeater: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
12
|
+
const formStore = useFormStore();
|
|
12
13
|
const { name, label, addLabel = 'Add Item' } = node.props || {};
|
|
13
14
|
const fullPath = path && name ? `${path}.${name}` : name;
|
|
14
15
|
const items: any[] = (fullPath ? formStore.getValue(fullPath) : []) || [];
|