@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.
Files changed (30) hide show
  1. package/build_output.txt +0 -0
  2. package/package.json +1 -1
  3. package/src/components/FieldRegistry.ts +16 -18
  4. package/src/components/FormRenderer.tsx +4 -2
  5. package/src/components/builder/EditorWrapper.tsx +3 -1
  6. package/src/components/builder/FormBuilder.tsx +20 -2
  7. package/src/components/builder/FormChildrenRenderer.tsx +3 -1
  8. package/src/components/builder/FormulaHelp.tsx +116 -0
  9. package/src/components/builder/IntegrationSettings.tsx +3 -1
  10. package/src/components/builder/OptionsEditor.tsx +90 -0
  11. package/src/components/builder/PropertiesModal.tsx +2 -1
  12. package/src/components/builder/PropertiesPanel.tsx +155 -62
  13. package/src/components/builder/SortableNode.tsx +2 -1
  14. package/src/components/builder/Toolbox.tsx +30 -63
  15. package/src/components/fields/CheckboxField.tsx +2 -1
  16. package/src/components/fields/DateField.tsx +2 -1
  17. package/src/components/fields/FileUploadField.tsx +2 -1
  18. package/src/components/fields/LabelField.tsx +2 -1
  19. package/src/components/fields/NumberField.tsx +2 -1
  20. package/src/components/fields/RichTextField.tsx +2 -1
  21. package/src/components/fields/SelectField.tsx +8 -2
  22. package/src/components/fields/TextField.tsx +2 -1
  23. package/src/components/layout/FormRepeater.tsx +2 -1
  24. package/src/components/layout/FormTabs.tsx +2 -1
  25. package/src/components/registerComponents.ts +69 -14
  26. package/src/index.ts +7 -0
  27. package/src/plugins/FieldPlugin.ts +63 -0
  28. package/src/plugins/PluginRegistry.ts +94 -0
  29. package/src/store/FormStore.ts +72 -24
  30. 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 { formStore } from '../../store/FormStore';
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 isLayout = node ? ['root', 'row', 'col', 'tabs', 'tab', 'paper', 'repeater', 'table', 'cell', 'divider'].includes(node.type) : false;
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 FormulaHints = ({ fieldType }: { fieldType: 'calculation' | 'defaultValue' | 'min' | 'max' }) => {
158
- const insertText = (text: string) => {
159
- let current = '';
160
- if (fieldType === 'calculation') current = node.calculation?.formula || '';
161
- else if (fieldType === 'defaultValue') current = node.defaultValue || '';
162
- else if (fieldType === 'min') current = String(node.validation?.min || '');
163
- else if (fieldType === 'max') current = String(node.validation?.max || '');
164
-
165
- const newVal = current + text;
166
- if (fieldType === 'calculation') formStore.updateNodeCalculation(node.id, { formula: newVal });
167
- else if (fieldType === 'defaultValue') {
168
- formStore.updateNode(node.id, { defaultValue: newVal });
169
- formStore.applyDefaultValues();
170
- }
171
- else if (fieldType === 'min') formStore.updateNodeValidation(node.id, { min: newVal });
172
- else if (fieldType === 'max') formStore.updateNodeValidation(node.id, { max: newVal });
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 fieldType="defaultValue" />
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
- : t('properties.selectOptionsHelper')}
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 fieldType="min" />
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 fieldType="max" />
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 fieldType="calculation" />
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 { formStore } from '../../store/FormStore';
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 { formStore, type NodeType } from '../../store/FormStore';
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 TextFieldsIcon from '@mui/icons-material/TextFields';
8
- import NumbersIcon from '@mui/icons-material/Numbers';
9
- import CheckBoxIcon from '@mui/icons-material/CheckBox';
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
- const handleAdd = (tool: ToolItem) => {
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(tool.type),
57
- type: tool.type,
58
- props: { ...tool.defaultProps, name: generateId('field'), label: t(`toolbox.${tool.type}`) },
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 (tool.type === 'table') {
64
- const rows = tool.defaultProps?.rows || 2;
65
- const cols = tool.defaultProps?.cols || 2;
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
- {TOOLS.map((tool) => (
93
- <ListItem key={tool.type} disablePadding sx={{ px: 2, py: 0.5 }}>
59
+ {fieldPlugins.map((plugin) => (
60
+ <ListItem key={plugin.type} disablePadding sx={{ px: 2, py: 0.5 }}>
94
61
  <DraggableTool
95
- type={tool.type}
96
- label={t(`toolbox.${tool.type}`)}
97
- icon={tool.icon}
98
- defaultProps={tool.defaultProps}
99
- onClick={() => handleAdd(tool)}
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
- {LAYOUT_TOOLS.map((tool) => (
108
- <ListItem key={tool.type} disablePadding sx={{ px: 2, py: 0.5 }}>
74
+ {layoutPlugins.map((plugin) => (
75
+ <ListItem key={plugin.type} disablePadding sx={{ px: 2, py: 0.5 }}>
109
76
  <DraggableTool
110
- type={tool.type}
111
- label={t(`toolbox.${tool.type}`)}
112
- icon={tool.icon}
113
- defaultProps={tool.defaultProps}
114
- onClick={() => handleAdd(tool)}
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 { formStore } from '../../store/FormStore';
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 { formStore } from '../../store/FormStore';
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 { formStore } from '../../store/FormStore';
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 { formStore } from '../../store/FormStore';
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 { formStore } from '../../store/FormStore';
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 { formStore } from '../../store/FormStore';
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 { formStore } from '../../store/FormStore';
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 { name, label, options = [], enableAutocomplete = false, placeholder } = node.props || {};
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 { formStore } from '../../store/FormStore';
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 { formStore } from '../../store/FormStore';
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) : []) || [];