@stormlmd/form-builder 0.3.0 → 0.3.5

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stormlmd/form-builder",
3
- "version": "0.3.0",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -38,11 +38,11 @@
38
38
  "@mui/icons-material": "^5.16.0",
39
39
  "@mui/material": "^5.16.0",
40
40
  "@mui/x-date-pickers": "^5.0.0",
41
- "date-fns": "^2.30.0",
42
41
  "@types/node": "^24.10.1",
43
42
  "@types/react": "^18.3.1",
44
43
  "@types/react-dom": "^18.3.1",
45
44
  "@vitejs/plugin-react": "^5.1.1",
45
+ "date-fns": "^2.30.0",
46
46
  "eslint": "^9.39.1",
47
47
  "eslint-plugin-react-hooks": "^7.0.1",
48
48
  "eslint-plugin-react-refresh": "^0.4.24",
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "i18next": "^25.8.7",
58
- "react-i18next": "^16.5.4"
58
+ "react-i18next": "^16.5.4",
59
+ "react-quill-new": "^3.8.3"
59
60
  }
60
61
  }
@@ -12,6 +12,7 @@ export interface FieldProps {
12
12
  node: SchemaNode;
13
13
  path?: string; // current path for values, e.g. "addresses[0]"
14
14
  isEditing?: boolean;
15
+ isReadOnly?: boolean;
15
16
  }
16
17
 
17
18
  type ComponentType = React.FC<FieldProps>;
@@ -13,6 +13,7 @@ interface FormRendererProps {
13
13
  node: SchemaNode;
14
14
  path?: string;
15
15
  isEditing?: boolean;
16
+ isReadOnly?: boolean;
16
17
  }
17
18
 
18
19
  export const DropIndicator = () => (
@@ -27,7 +28,7 @@ export const DropIndicator = () => (
27
28
  }} />
28
29
  );
29
30
 
30
- export const FormRenderer: React.FC<FormRendererProps> = observer(({ node, path, isEditing }) => {
31
+ export const FormRenderer: React.FC<FormRendererProps> = observer(({ node, path, isEditing, isReadOnly }) => {
31
32
  const formStore = useFormStore();
32
33
  // Evaluate condition if present (skip in editing mode)
33
34
  if (!isEditing && node.condition) {
@@ -55,6 +56,7 @@ export const FormRenderer: React.FC<FormRendererProps> = observer(({ node, path,
55
56
  parentId={node.id}
56
57
  children={node.children}
57
58
  isEditing={!!isEditing}
59
+ isReadOnly={!!isReadOnly}
58
60
  path={path}
59
61
  />
60
62
  </Box>
@@ -92,7 +94,7 @@ export const FormRenderer: React.FC<FormRendererProps> = observer(({ node, path,
92
94
  flexDirection: 'column'
93
95
  };
94
96
 
95
- let renderedComponent = <Component node={node} path={path} isEditing={isEditing} />;
97
+ let renderedComponent = <Component node={node} path={path} isEditing={isEditing} isReadOnly={isReadOnly} />;
96
98
 
97
99
  // Wrap in Tooltip if present (only in preview mode, or also in editor? User said "in form" - обычно в превью, но в редакторе тоже неплохо)
98
100
  if (!isEditing && tooltip) {
@@ -12,6 +12,7 @@ interface FormChildrenRendererProps {
12
12
  isEditing: boolean;
13
13
  layout?: 'horizontal' | 'vertical';
14
14
  path?: string;
15
+ isReadOnly?: boolean;
15
16
  }
16
17
 
17
18
  export const FormChildrenRenderer: React.FC<FormChildrenRendererProps> = observer(({
@@ -19,7 +20,8 @@ export const FormChildrenRenderer: React.FC<FormChildrenRendererProps> = observe
19
20
  children,
20
21
  isEditing,
21
22
  layout = 'horizontal',
22
- path
23
+ path,
24
+ isReadOnly
23
25
  }) => {
24
26
  const formStore = useFormStore();
25
27
  const childIds = children?.map(c => c.id) || [];
@@ -40,7 +42,7 @@ export const FormChildrenRenderer: React.FC<FormChildrenRendererProps> = observe
40
42
  return (
41
43
  <Box sx={containerSx}>
42
44
  {children?.map(child => (
43
- <FormRenderer key={child.id} node={child} path={path} isEditing={false} />
45
+ <FormRenderer key={child.id} node={child} path={path} isEditing={false} isReadOnly={isReadOnly} />
44
46
  ))}
45
47
  </Box>
46
48
  );
@@ -0,0 +1,116 @@
1
+ import React from 'react';
2
+ import {
3
+ Dialog,
4
+ DialogTitle,
5
+ DialogContent,
6
+ DialogActions,
7
+ Button,
8
+ Typography,
9
+ Box,
10
+ IconButton,
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableContainer,
15
+ TableHead,
16
+ TableRow,
17
+ Paper
18
+ } from '@mui/material';
19
+ import CloseIcon from '@mui/icons-material/Close';
20
+
21
+ interface FormulaHelpProps {
22
+ open: boolean;
23
+ onClose: () => void;
24
+ }
25
+
26
+ export const FormulaHelp: React.FC<FormulaHelpProps> = ({ open, onClose }) => {
27
+
28
+ const examples = [
29
+ {
30
+ func: 'IF(cond, t, f)',
31
+ desc: 'Условие: если истина -> t, иначе -> f',
32
+ example: 'IF({{price}} > 100, "Высокая", "Низкая")'
33
+ },
34
+ {
35
+ func: 'SWITCH(v, v1, r1, ...)',
36
+ desc: 'Выбор по значению одного поля',
37
+ example: 'SWITCH({{type}}, "A", 10, "B", 20, 0)'
38
+ },
39
+ {
40
+ func: 'IFS(c1, r1, c2, r2, ...)',
41
+ desc: 'Цепочка условий (первое истинное)',
42
+ example: 'IFS({{score}} > 90, "A", {{score}} > 50, "B", "F")'
43
+ },
44
+ {
45
+ func: 'DATE_ADD(d, days)',
46
+ desc: 'Прибавить дни к дате',
47
+ example: 'DATE_ADD({{today}}, 7)'
48
+ },
49
+ {
50
+ func: 'DATE_DIFF(d1, d2)',
51
+ desc: 'Разница между датами в днях',
52
+ example: 'DATE_DIFF({{today}}, {{start_date}})'
53
+ },
54
+ {
55
+ func: 'ROUND(v, p)',
56
+ desc: 'Округление до p знаков',
57
+ example: 'ROUND({{price}} * 1.2, 2)'
58
+ },
59
+ {
60
+ func: 'AND/OR/NOT',
61
+ desc: 'Логические операторы',
62
+ example: 'AND({{active}}, NOT({{hidden}}))'
63
+ }
64
+ ];
65
+
66
+ return (
67
+ <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
68
+ <DialogTitle sx={{ m: 0, p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
69
+ <Typography variant="h6">Подсказка по формулам</Typography>
70
+ <IconButton onClick={onClose} size="small">
71
+ <CloseIcon />
72
+ </IconButton>
73
+ </DialogTitle>
74
+ <DialogContent dividers>
75
+ <Typography variant="body2" gutterBottom color="text.secondary">
76
+ Используйте <code>{`{{имя_поля}}`}</code> для доступа к значениям. Ниже приведены популярные функции:
77
+ </Typography>
78
+
79
+ <TableContainer component={Paper} variant="outlined" sx={{ mt: 2 }}>
80
+ <Table size="small">
81
+ <TableHead>
82
+ <TableRow sx={{ bgcolor: 'action.hover' }}>
83
+ <TableCell><strong>Функция</strong></TableCell>
84
+ <TableCell><strong>Описание</strong></TableCell>
85
+ <TableCell><strong>Пример</strong></TableCell>
86
+ </TableRow>
87
+ </TableHead>
88
+ <TableBody>
89
+ {examples.map((item, index) => (
90
+ <TableRow key={index}>
91
+ <TableCell sx={{ fontFamily: 'monospace', fontWeight: 'bold' }}>{item.func}</TableCell>
92
+ <TableCell>{item.desc}</TableCell>
93
+ <TableCell sx={{ fontFamily: 'monospace', fontSize: '0.8rem', color: 'primary.main' }}>
94
+ {item.example}
95
+ </TableCell>
96
+ </TableRow>
97
+ ))}
98
+ </TableBody>
99
+ </Table>
100
+ </TableContainer>
101
+
102
+ <Box mt={3}>
103
+ <Typography variant="subtitle2" gutterBottom>Полезные советы:</Typography>
104
+ <Typography variant="body2" component="ul" sx={{ pl: 2 }}>
105
+ <li>Пустые поля автоматически считаются <code>0</code> в математике.</li>
106
+ <li>Для строк используйте кавычки: <code>"Текст"</code>.</li>
107
+ <li><code>{`{{today}}`}</code> - текущая дата в формате DD-MM-YYYY.</li>
108
+ </Typography>
109
+ </Box>
110
+ </DialogContent>
111
+ <DialogActions>
112
+ <Button onClick={onClose} color="primary">Закрыть</Button>
113
+ </DialogActions>
114
+ </Dialog>
115
+ );
116
+ };
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Box, TextField, IconButton, Button, Typography, Paper } from '@mui/material';
2
+ import { Box, TextField, IconButton, Button, Typography } from '@mui/material';
3
3
  import DeleteIcon from '@mui/icons-material/Delete';
4
4
  import AddIcon from '@mui/icons-material/Add';
5
5
  import { useTranslation } from 'react-i18next';
@@ -1,11 +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
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';
8
9
  import { getPlugin } from '../../plugins/PluginRegistry';
10
+ // import { FormulaHelp } from './FormulaHelp';
9
11
 
10
12
  interface TabPanelProps {
11
13
  children?: React.ReactNode;
@@ -39,6 +41,48 @@ function CustomTabPanel(props: TabPanelProps) {
39
41
  );
40
42
  }
41
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
+
42
86
  function a11yProps(index: number) {
43
87
  return {
44
88
  id: `simple-tab-${index}`,
@@ -96,6 +140,7 @@ export const PropertiesPanel: React.FC = observer(() => {
96
140
  };
97
141
 
98
142
  const handleDelete = () => {
143
+ if (node.id === 'root') return;
99
144
  formStore.removeNode(node.id);
100
145
  formStore.closePropertiesModal(); // Also close modal on delete
101
146
  };
@@ -158,58 +203,22 @@ export const PropertiesPanel: React.FC = observer(() => {
158
203
  setAnchorEl(null);
159
204
  };
160
205
 
161
- const FormulaHints = ({ fieldType }: { fieldType: 'calculation' | 'defaultValue' | 'min' | 'max' }) => {
162
- const insertText = (text: string) => {
163
- let current = '';
164
- if (fieldType === 'calculation') current = node.calculation?.formula || '';
165
- else if (fieldType === 'defaultValue') current = node.defaultValue || '';
166
- else if (fieldType === 'min') current = String(node.validation?.min || '');
167
- else if (fieldType === 'max') current = String(node.validation?.max || '');
168
-
169
- const newVal = current + text;
170
- if (fieldType === 'calculation') formStore.updateNodeCalculation(node.id, { formula: newVal });
171
- else if (fieldType === 'defaultValue') {
172
- formStore.updateNode(node.id, { defaultValue: newVal });
173
- formStore.applyDefaultValues();
174
- }
175
- else if (fieldType === 'min') formStore.updateNodeValidation(node.id, { min: newVal });
176
- else if (fieldType === 'max') formStore.updateNodeValidation(node.id, { max: newVal });
177
- };
178
-
179
- return (
180
- <Box mt={1} display="flex" flexWrap="wrap" gap={0.5}>
181
- <Typography variant="caption" width="100%" color="text.secondary">{t("properties.hints")}</Typography>
182
- {['today', 'now', ...formStore.getAllFieldNames()]
183
- .filter(name => name !== node.props?.name)
184
- .map(name => (
185
- <Button
186
- key={name}
187
- size="small"
188
- variant="outlined"
189
- sx={{ textTransform: 'none', px: 1, py: 0, fontSize: '0.7rem' }}
190
- onClick={() => insertText(`{{${name}}}`)}
191
- >
192
- {name}
193
- </Button>
194
- ))}
195
-
196
- <Box width="100%" mt={1}>
197
- <Typography variant="caption" color="text.secondary">{t("properties.functions") || "Functions"}</Typography>
198
- </Box>
199
- {['IF()', 'AND()', 'OR()', 'NOT()', 'ABS()', 'ROUND()'].map(func => (
200
- <Button
201
- key={func}
202
- size="small"
203
- variant="outlined"
204
- color="secondary"
205
- sx={{ textTransform: 'none', px: 1, py: 0, fontSize: '0.7rem' }}
206
- onClick={() => insertText(func)}
207
- >
208
- {func}
209
- </Button>
210
- ))}
211
- </Box>
212
- );
206
+ const insertText = (fieldType: string, text: string) => {
207
+ if (!node) return;
208
+ let current = '';
209
+ if (fieldType === 'calculation') current = node.calculation?.formula || '';
210
+ else if (fieldType === 'defaultValue') current = node.defaultValue || '';
211
+ else if (fieldType === 'min') current = String(node.validation?.min || '');
212
+ else if (fieldType === 'max') current = String(node.validation?.max || '');
213
+
214
+ const newVal = current + text;
215
+ if (fieldType === 'calculation') formStore.updateNodeCalculation(node.id, { formula: newVal });
216
+ else if (fieldType === 'defaultValue') {
217
+ formStore.updateNode(node.id, { defaultValue: newVal });
218
+ formStore.applyDefaultValues();
219
+ }
220
+ else if (fieldType === 'min') formStore.updateNodeValidation(node.id, { min: newVal });
221
+ else if (fieldType === 'max') formStore.updateNodeValidation(node.id, { max: newVal });
213
222
  };
214
223
 
215
224
  return (
@@ -240,7 +249,7 @@ export const PropertiesPanel: React.FC = observer(() => {
240
249
  <Box>
241
250
  <Typography variant="subtitle1" fontWeight="bold">{t('properties.title')}</Typography>
242
251
  <Typography variant="caption" color="text.secondary">
243
- {node.type.toUpperCase()} | ID: {node.id}
252
+ {(node.type || 'unknown').toUpperCase()} | ID: {node.id || 'none'}
244
253
  </Typography>
245
254
  </Box>
246
255
  <Button
@@ -377,6 +386,17 @@ export const PropertiesPanel: React.FC = observer(() => {
377
386
  size="small"
378
387
  fullWidth
379
388
  value={node.defaultValue ?? ''}
389
+ InputProps={{
390
+ endAdornment: (
391
+ <InputAdornment position="end">
392
+ <Tooltip title="Справка по формулам">
393
+ <IconButton size="small">
394
+ <HelpOutlineIcon fontSize="small" />
395
+ </IconButton>
396
+ </Tooltip>
397
+ </InputAdornment>
398
+ ),
399
+ }}
380
400
  onChange={(e) => {
381
401
  const val = e.target.value;
382
402
  formStore.updateNode(node.id, { defaultValue: val });
@@ -388,7 +408,7 @@ export const PropertiesPanel: React.FC = observer(() => {
388
408
  setCursorPos(target.selectionStart || 0);
389
409
  }}
390
410
  />
391
- <FormulaHints fieldType="defaultValue" />
411
+ <FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('defaultValue', txt)} t={t} />
392
412
  </>
393
413
  )}
394
414
  </Box>
@@ -620,6 +640,41 @@ export const PropertiesPanel: React.FC = observer(() => {
620
640
  </Box>
621
641
  )}
622
642
 
643
+ {node.type === 'richtext' && (
644
+ <Box display="flex" alignItems="center" mt={1}>
645
+ <input
646
+ type="checkbox"
647
+ id="enable-visual-editor"
648
+ checked={node.props?.enableVisualEditor || false}
649
+ onChange={(e) => handleChange('enableVisualEditor', e.target.checked)}
650
+ style={{ marginRight: 8 }}
651
+ />
652
+ <Typography variant="body2" component="label" htmlFor="enable-visual-editor" sx={{ cursor: 'pointer' }}>
653
+ {t('properties.enableVisualEditor') || 'Enable Visual Editor'}
654
+ </Typography>
655
+ </Box>
656
+ )}
657
+
658
+ {node.type === 'checkbox' && (
659
+ <Box mt={2} display="flex" flexDirection="column" gap={2}>
660
+ <Typography variant="subtitle2" color="primary">{t('properties.checkboxResultsLabels') || 'Results View Labels'}</Typography>
661
+ <TextField
662
+ label={t('properties.trueText') || 'Positive (e.g. "Да")'}
663
+ size="small"
664
+ fullWidth
665
+ value={node.props?.trueText || ''}
666
+ onChange={(e) => handleChange('trueText', e.target.value)}
667
+ />
668
+ <TextField
669
+ label={t('properties.falseText') || 'Negative (e.g. "Нет")'}
670
+ size="small"
671
+ fullWidth
672
+ value={node.props?.falseText || ''}
673
+ onChange={(e) => handleChange('falseText', e.target.value)}
674
+ />
675
+ </Box>
676
+ )}
677
+
623
678
  {/* Plugin custom properties editor */}
624
679
  {plugin?.propertiesEditor && (
625
680
  <Box mt={2}>
@@ -686,6 +741,17 @@ export const PropertiesPanel: React.FC = observer(() => {
686
741
  size="small"
687
742
  fullWidth
688
743
  value={node.validation?.min ?? ''}
744
+ InputProps={{
745
+ endAdornment: (
746
+ <InputAdornment position="end">
747
+ <Tooltip title="Справка по формулам">
748
+ <IconButton size="small">
749
+ <HelpOutlineIcon fontSize="small" />
750
+ </IconButton>
751
+ </Tooltip>
752
+ </InputAdornment>
753
+ ),
754
+ }}
689
755
  onChange={(e) => {
690
756
  const val = e.target.value;
691
757
  formStore.updateNodeValidation(node.id, { min: val });
@@ -697,7 +763,7 @@ export const PropertiesPanel: React.FC = observer(() => {
697
763
  }}
698
764
  placeholder="e.g. 10 or {{otherField}} + 5"
699
765
  />
700
- <FormulaHints fieldType="min" />
766
+ <FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('min', txt)} t={t} />
701
767
  </Box>
702
768
  <Box mb={1}>
703
769
  <TextField
@@ -705,6 +771,17 @@ export const PropertiesPanel: React.FC = observer(() => {
705
771
  size="small"
706
772
  fullWidth
707
773
  value={node.validation?.max ?? ''}
774
+ InputProps={{
775
+ endAdornment: (
776
+ <InputAdornment position="end">
777
+ <Tooltip title="Справка по формулам">
778
+ <IconButton size="small">
779
+ <HelpOutlineIcon fontSize="small" />
780
+ </IconButton>
781
+ </Tooltip>
782
+ </InputAdornment>
783
+ ),
784
+ }}
708
785
  onChange={(e) => {
709
786
  const val = e.target.value;
710
787
  formStore.updateNodeValidation(node.id, { max: val });
@@ -716,7 +793,7 @@ export const PropertiesPanel: React.FC = observer(() => {
716
793
  }}
717
794
  placeholder="e.g. 100 or {{otherField}} * 2"
718
795
  />
719
- <FormulaHints fieldType="max" />
796
+ <FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('max', txt)} t={t} />
720
797
  </Box>
721
798
  </Box>
722
799
  )}
@@ -837,6 +914,17 @@ export const PropertiesPanel: React.FC = observer(() => {
837
914
  fullWidth
838
915
  placeholder="e.g. {{price}} * {{qty}}"
839
916
  value={node.calculation?.formula || ''}
917
+ InputProps={{
918
+ endAdornment: (
919
+ <InputAdornment position="end">
920
+ <Tooltip title="Справка по формулам">
921
+ <IconButton size="small">
922
+ <HelpOutlineIcon fontSize="small" />
923
+ </IconButton>
924
+ </Tooltip>
925
+ </InputAdornment>
926
+ ),
927
+ }}
840
928
  onChange={(e) => {
841
929
  const val = e.target.value;
842
930
  formStore.updateNodeCalculation(node.id, val ? { formula: val } : undefined);
@@ -848,7 +936,7 @@ export const PropertiesPanel: React.FC = observer(() => {
848
936
  }}
849
937
  helperText={t("properties.formulaHelper")}
850
938
  />
851
- <FormulaHints fieldType="calculation" />
939
+ <FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('calculation', txt)} t={t} />
852
940
  </CustomTabPanel>
853
941
 
854
942
  {/* API Mapping Tab */}
@@ -890,6 +978,9 @@ export const PropertiesPanel: React.FC = observer(() => {
890
978
  </FormControl>
891
979
  )}
892
980
  </CustomTabPanel>
981
+
982
+
983
+ {/* <FormulaHelp open={helpOpen} onClose={() => setHelpOpen(false)} /> */}
893
984
  </Box>
894
985
  );
895
986
  });
@@ -4,12 +4,25 @@ import { observer } from 'mobx-react-lite';
4
4
  import { useFormStore } from '../../store/FormStoreContext';
5
5
  import type { FieldProps } from '../FieldRegistry';
6
6
 
7
- export const CheckboxField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
7
+ export const CheckboxField: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
8
8
  const formStore = useFormStore();
9
- const { name, label } = node.props || {};
9
+ const { name, label, trueText = 'Да', falseText = 'Нет' } = node.props || {};
10
10
  const fullPath = path && name ? `${path}.${name}` : name;
11
11
  const value = fullPath ? formStore.getValue(fullPath) : false;
12
12
 
13
+ if (isReadOnly) {
14
+ return (
15
+ <Box mb={1}>
16
+ <Typography variant="caption" color="text.secondary" display="block">
17
+ {label || ''}
18
+ </Typography>
19
+ <Typography variant="body1" sx={{ minHeight: '1.5em', borderBottom: '1px solid #eee', pb: 0.5 }}>
20
+ {value ? trueText : falseText}
21
+ </Typography>
22
+ </Box>
23
+ );
24
+ }
25
+
13
26
  return (
14
27
  <Box mb={0.5}>
15
28
  {isEditing && (
@@ -7,7 +7,7 @@ import { useFormStore } from '../../store/FormStoreContext';
7
7
  import type { FieldProps } from '../FieldRegistry';
8
8
  import { Box, Typography, TextField } from '@mui/material';
9
9
 
10
- export const DateField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
10
+ export const DateField: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
11
11
  const formStore = useFormStore();
12
12
  const { name, label, placeholder } = node.props || {};
13
13
  const fullPath = path && name ? `${path}.${name}` : name;
@@ -24,6 +24,19 @@ export const DateField: React.FC<FieldProps> = observer(({ node, path, isEditing
24
24
  // Create a Date object from value if string
25
25
  const dateValue = value ? new Date(value) : null;
26
26
 
27
+ if (isReadOnly) {
28
+ return (
29
+ <Box mb={1}>
30
+ <Typography variant="caption" color="text.secondary" display="block">
31
+ {label || ''}
32
+ </Typography>
33
+ <Typography variant="body1" sx={{ minHeight: '1.5em', borderBottom: '1px solid #eee', pb: 0.5 }}>
34
+ {dateValue ? dateValue.toLocaleDateString() : '-'}
35
+ </Typography>
36
+ </Box>
37
+ );
38
+ }
39
+
27
40
  const content = (
28
41
  <LocalizationProvider dateAdapter={AdapterDateFns}>
29
42
  <DatePicker
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
5
5
  import { useFormStore } from '../../store/FormStoreContext';
6
6
  import type { FieldProps } from '../FieldRegistry';
7
7
 
8
- export const FileUploadField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
8
+ export const FileUploadField: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
9
9
  const formStore = useFormStore();
10
10
  const { name, label } = node.props || {};
11
11
  const fullPath = path && name ? `${path}.${name}` : name;
@@ -19,6 +19,19 @@ export const FileUploadField: React.FC<FieldProps> = observer(({ node, path, isE
19
19
  }
20
20
  };
21
21
 
22
+ if (isReadOnly) {
23
+ return (
24
+ <Box mb={1}>
25
+ <Typography variant="caption" color="text.secondary" display="block">
26
+ {label || ''}
27
+ </Typography>
28
+ <Typography variant="body1" sx={{ minHeight: '1.5em', borderBottom: '1px solid #eee', pb: 0.5 }}>
29
+ {value || '-'}
30
+ </Typography>
31
+ </Box>
32
+ );
33
+ }
34
+
22
35
  return (
23
36
  <Box mb={1}>
24
37
  <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite';
4
4
  import { useFormStore } from '../../store/FormStoreContext';
5
5
  import type { FieldProps } from '../FieldRegistry';
6
6
 
7
- export const LabelField: React.FC<FieldProps> = observer(({ node }) => {
7
+ export const LabelField: React.FC<FieldProps> = observer(({ node, isReadOnly: _isReadOnly }) => {
8
8
  const formStore = useFormStore();
9
9
  const { name, label = '', align = 'left', variant = 'body1' } = node.props || {};
10
10
 
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite';
4
4
  import { useFormStore } from '../../store/FormStoreContext';
5
5
  import type { FieldProps } from '../FieldRegistry';
6
6
 
7
- export const NumberField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
7
+ export const NumberField: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
8
8
  const formStore = useFormStore();
9
9
  const { name, label } = node.props || {};
10
10
 
@@ -19,6 +19,19 @@ export const NumberField: React.FC<FieldProps> = observer(({ node, path, isEditi
19
19
  }
20
20
  };
21
21
 
22
+ if (isReadOnly) {
23
+ return (
24
+ <Box mb={1}>
25
+ <Typography variant="caption" color="text.secondary" display="block">
26
+ {label || ''}
27
+ </Typography>
28
+ <Typography variant="body1" sx={{ minHeight: '1.5em', borderBottom: '1px solid #eee', pb: 0.5 }}>
29
+ {value !== undefined && value !== null && value !== '' ? value.toString() : '-'}
30
+ </Typography>
31
+ </Box>
32
+ );
33
+ }
34
+
22
35
  return (
23
36
  <Box mb={0.5}>
24
37
  <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
@@ -1,40 +1,117 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { TextField as MuiTextField, Typography, Box } from '@mui/material';
3
3
  import { observer } from 'mobx-react-lite';
4
4
  import { useFormStore } from '../../store/FormStoreContext';
5
5
  import type { FieldProps } from '../FieldRegistry';
6
+ import ReactQuill from 'react-quill-new';
7
+ import 'react-quill-new/dist/quill.snow.css';
6
8
 
7
- export const RichTextField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
9
+ export const RichTextField: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
8
10
  const formStore = useFormStore();
9
- const { name, label, placeholder } = node.props || {};
11
+ const { name, label, placeholder, enableVisualEditor } = node.props || {};
10
12
  const fullPath = path && name ? `${path}.${name}` : name;
11
- const value = fullPath ? formStore.getValue(fullPath) : '';
13
+ const value = (fullPath ? formStore.getValue(fullPath) : '') || '';
12
14
  const error = fullPath ? formStore.errors[fullPath] : undefined;
13
15
 
14
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
16
+ const handleChange = (val: string) => {
15
17
  if (fullPath) {
16
- formStore.updateField(fullPath, e.target.value, node.validation);
18
+ formStore.updateField(fullPath, val, node.validation);
17
19
  }
18
20
  };
19
21
 
22
+ const modules = useMemo(() => ({
23
+ toolbar: [
24
+ ['bold'],
25
+ [{ 'header': '3' }, { 'header': '4' }],
26
+ [{ 'list': 'ordered' }, { 'list': 'bullet' }],
27
+ ['link'],
28
+ ['clean']
29
+ ],
30
+ }), []);
31
+
32
+ const formats = [
33
+ 'header',
34
+ 'bold',
35
+ 'list', 'bullet',
36
+ 'link'
37
+ ];
38
+
39
+ if (isReadOnly) {
40
+ return (
41
+ <Box mb={2}>
42
+ <Typography variant="caption" color="text.secondary" display="block">
43
+ {label || ''}
44
+ </Typography>
45
+ <Box
46
+ sx={{
47
+ mt: 0.5,
48
+ p: 0,
49
+ '& h3': { m: '0.5em 0' },
50
+ '& h4': { m: '0.4em 0' },
51
+ '& ul, & ol': { pl: 2 }
52
+ }}
53
+ dangerouslySetInnerHTML={{ __html: value || '-' }}
54
+ />
55
+ </Box>
56
+ );
57
+ }
58
+
20
59
  return (
21
60
  <Box mb={0.5}>
22
61
  <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
23
62
  {label || (isEditing ? 'Rich Text' : '')} {isEditing ? `(${name})` : ''}
24
63
  {node.validation?.required && <span style={{ color: 'red' }}> *</span>}
25
64
  </Typography>
26
- <MuiTextField
27
- placeholder={placeholder}
28
- fullWidth
29
- multiline
30
- minRows={4}
31
- value={value}
32
- onChange={handleChange}
33
- size="small"
34
- disabled={isEditing || !!node.calculation?.formula}
35
- error={!!error}
36
- helperText={error}
37
- />
65
+ {enableVisualEditor ? (
66
+ <Box sx={{
67
+ '& .quill': {
68
+ bgcolor: 'background.paper',
69
+ borderRadius: 1,
70
+ border: theme => `1px solid ${error ? theme.palette.error.main : theme.palette.divider}`,
71
+ '& .ql-toolbar': {
72
+ borderTop: 'none',
73
+ borderLeft: 'none',
74
+ borderRight: 'none',
75
+ borderBottom: '1px solid',
76
+ borderColor: 'divider',
77
+ borderRadius: '4px 4px 0 0'
78
+ },
79
+ '& .ql-container': {
80
+ border: 'none',
81
+ minHeight: '120px',
82
+ fontSize: '0.875rem'
83
+ }
84
+ }
85
+ }}>
86
+ <ReactQuill
87
+ theme="snow"
88
+ value={value}
89
+ onChange={handleChange}
90
+ modules={modules}
91
+ formats={formats}
92
+ placeholder={placeholder}
93
+ readOnly={isEditing || !!node.calculation?.formula}
94
+ />
95
+ {error && (
96
+ <Typography variant="caption" color="error" sx={{ ml: 1.5, mt: 0.5, display: 'block' }}>
97
+ {error}
98
+ </Typography>
99
+ )}
100
+ </Box>
101
+ ) : (
102
+ <MuiTextField
103
+ placeholder={placeholder}
104
+ fullWidth
105
+ multiline
106
+ minRows={4}
107
+ value={value}
108
+ onChange={(e) => handleChange(e.target.value)}
109
+ size="small"
110
+ disabled={isEditing || !!node.calculation?.formula}
111
+ error={!!error}
112
+ helperText={error}
113
+ />
114
+ )}
38
115
  </Box>
39
116
  );
40
117
  });
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite';
4
4
  import { useFormStore } from '../../store/FormStoreContext';
5
5
  import type { FieldProps } from '../FieldRegistry';
6
6
 
7
- export const SelectField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
7
+ export const SelectField: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
8
8
  const formStore = useFormStore();
9
9
  const { name, label, options: staticOptions = [], optionsSource, enableAutocomplete = false, placeholder } = node.props || {};
10
10
 
@@ -22,6 +22,20 @@ export const SelectField: React.FC<FieldProps> = observer(({ node, path, isEditi
22
22
  }
23
23
  };
24
24
 
25
+ if (isReadOnly) {
26
+ const selectedOption = options.find((opt: any) => opt.value === value);
27
+ return (
28
+ <Box mb={1}>
29
+ <Typography variant="caption" color="text.secondary" display="block">
30
+ {label || ''}
31
+ </Typography>
32
+ <Typography variant="body1" sx={{ minHeight: '1.5em', borderBottom: '1px solid #eee', pb: 0.5 }}>
33
+ {selectedOption ? selectedOption.label : (value || '-')}
34
+ </Typography>
35
+ </Box>
36
+ );
37
+ }
38
+
25
39
  const renderSelect = () => (
26
40
  <FormControl fullWidth size="small" disabled={isEditing || !!node.calculation?.formula} error={!!error}>
27
41
  <Select
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite';
4
4
  import { useFormStore } from '../../store/FormStoreContext';
5
5
  import type { FieldProps } from '../FieldRegistry';
6
6
 
7
- export const TextField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
7
+ export const TextField: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
8
8
  const formStore = useFormStore();
9
9
  const { name, label, placeholder } = node.props || {};
10
10
 
@@ -19,6 +19,19 @@ export const TextField: React.FC<FieldProps> = observer(({ node, path, isEditing
19
19
  }
20
20
  };
21
21
 
22
+ if (isReadOnly) {
23
+ return (
24
+ <Box mb={1}>
25
+ <Typography variant="caption" color="text.secondary" display="block">
26
+ {label || ''}
27
+ </Typography>
28
+ <Typography variant="body1" sx={{ minHeight: '1.5em', borderBottom: '1px solid #eee', pb: 0.5 }}>
29
+ {value || '-'}
30
+ </Typography>
31
+ </Box>
32
+ );
33
+ }
34
+
22
35
  return (
23
36
  <Box mb={0.5}>
24
37
  <Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
@@ -6,13 +6,13 @@ import { FormRenderer } from '../FormRenderer';
6
6
  import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
7
7
  import { LayoutPlaceholder } from './LayoutPlaceholder';
8
8
 
9
- export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
9
+ export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
10
10
  const childrenIds = node.children?.map(c => c.id) || [];
11
11
 
12
12
  const content = (
13
13
  <Box display="flex" flexDirection="column" gap={0.5} width="100%" sx={{ flexGrow: 1, height: '100%' }}>
14
14
  {node.children?.map(child => (
15
- <FormRenderer key={child.id} node={child} path={path} isEditing={isEditing} />
15
+ <FormRenderer key={child.id} node={child} path={path} isEditing={isEditing} isReadOnly={isReadOnly} />
16
16
  ))}
17
17
  </Box>
18
18
  );
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
5
5
  import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
6
6
  import { LayoutPlaceholder } from './LayoutPlaceholder';
7
7
 
8
- export const FormPaper: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
8
+ export const FormPaper: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
9
9
  const { label, elevation: elevationProp, padding = 2, variant: variantProp } = node.props || {};
10
10
  const childrenIds = node.children?.map(c => c.id) || [];
11
11
 
@@ -21,6 +21,7 @@ export const FormPaper: React.FC<FieldProps> = observer(({ node, path, isEditing
21
21
  isEditing={!!isEditing}
22
22
  path={path}
23
23
  layout="horizontal"
24
+ isReadOnly={isReadOnly}
24
25
  />
25
26
  );
26
27
 
@@ -8,7 +8,7 @@ import type { FieldProps } from '../FieldRegistry';
8
8
  import { LayoutPlaceholder } from './LayoutPlaceholder';
9
9
  import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
10
10
 
11
- export const FormRepeater: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
11
+ export const FormRepeater: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
12
12
  const formStore = useFormStore();
13
13
  const { name, label, addLabel = 'Add Item' } = node.props || {};
14
14
  const fullPath = path && name ? `${path}.${name}` : name;
@@ -81,7 +81,7 @@ export const FormRepeater: React.FC<FieldProps> = observer(({ node, path, isEdit
81
81
  {displayItems.map((_item, index) => (
82
82
  <Paper key={index} variant="outlined" sx={{ p: 1, mb: 0.5, position: 'relative' }}>
83
83
  <Box position="absolute" right={4} top={4} zIndex={5}>
84
- <IconButton size="small" onClick={() => handleRemove(index)} color="error" disabled={isEditing}>
84
+ <IconButton size="small" onClick={() => handleRemove(index)} color="error" disabled={isEditing || isReadOnly}>
85
85
  <DeleteIcon fontSize="inherit" />
86
86
  </IconButton>
87
87
  </Box>
@@ -100,6 +100,7 @@ export const FormRepeater: React.FC<FieldProps> = observer(({ node, path, isEdit
100
100
  isEditing={true}
101
101
  layout="horizontal"
102
102
  path={`${fullPath}[${index}]`}
103
+ isReadOnly={isReadOnly}
103
104
  />
104
105
  </LayoutPlaceholder>
105
106
  ) : (
@@ -109,13 +110,14 @@ export const FormRepeater: React.FC<FieldProps> = observer(({ node, path, isEdit
109
110
  isEditing={false}
110
111
  layout="horizontal"
111
112
  path={`${fullPath}[${index}]`}
113
+ isReadOnly={isReadOnly}
112
114
  />
113
115
  )}
114
116
  </Box>
115
117
  </Paper>
116
118
  ))}
117
119
 
118
- {!isEditing && (
120
+ {!isEditing && !isReadOnly && (
119
121
  <Button
120
122
  startIcon={<AddIcon />}
121
123
  variant="outlined"
@@ -4,7 +4,7 @@ import type { FieldProps } from '../FieldRegistry';
4
4
  import { LayoutPlaceholder } from './LayoutPlaceholder';
5
5
  import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
6
6
 
7
- export const FormRow: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
7
+ export const FormRow: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
8
8
  const childrenIds = node.children?.map(c => c.id) || [];
9
9
 
10
10
  if (isEditing) {
@@ -16,6 +16,7 @@ export const FormRow: React.FC<FieldProps> = observer(({ node, path, isEditing }
16
16
  isEditing={true}
17
17
  layout="horizontal"
18
18
  path={path}
19
+ isReadOnly={isReadOnly}
19
20
  />
20
21
  </LayoutPlaceholder>
21
22
  );
@@ -28,11 +29,12 @@ export const FormRow: React.FC<FieldProps> = observer(({ node, path, isEditing }
28
29
  isEditing={false}
29
30
  layout="horizontal"
30
31
  path={path}
32
+ isReadOnly={isReadOnly}
31
33
  />
32
34
  );
33
35
  });
34
36
 
35
- export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
37
+ export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
36
38
  const childrenIds = node.children?.map(c => c.id) || [];
37
39
 
38
40
  if (isEditing) {
@@ -44,6 +46,7 @@ export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing }
44
46
  isEditing={true}
45
47
  layout="vertical"
46
48
  path={path}
49
+ isReadOnly={isReadOnly}
47
50
  />
48
51
  </LayoutPlaceholder>
49
52
  );
@@ -56,6 +59,7 @@ export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing }
56
59
  isEditing={false}
57
60
  layout="vertical"
58
61
  path={path}
62
+ isReadOnly={isReadOnly}
59
63
  />
60
64
  );
61
65
  });
@@ -4,7 +4,7 @@ import type { FieldProps } from '../FieldRegistry';
4
4
  import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
5
5
  import { LayoutPlaceholder } from './LayoutPlaceholder';
6
6
 
7
- export const FormTab: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
7
+ export const FormTab: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
8
8
  const childrenIds = node.children?.map(c => c.id) || [];
9
9
 
10
10
  const content = (
@@ -14,6 +14,7 @@ export const FormTab: React.FC<FieldProps> = observer(({ node, path, isEditing }
14
14
  isEditing={!!isEditing}
15
15
  path={path}
16
16
  layout="vertical"
17
+ isReadOnly={isReadOnly}
17
18
  />
18
19
  );
19
20
 
@@ -5,7 +5,7 @@ import type { FieldProps } from '../FieldRegistry';
5
5
  import { FormRenderer } from '../FormRenderer';
6
6
  import { LayoutPlaceholder } from './LayoutPlaceholder';
7
7
 
8
- export const FormTable: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
8
+ export const FormTable: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
9
9
  const { rows = 2, cols = 2 } = node.props || {};
10
10
 
11
11
  console.log(`FormTable Render: ${node.id}`, {
@@ -28,7 +28,7 @@ export const FormTable: React.FC<FieldProps> = observer(({ node, path, isEditing
28
28
  <Box sx={gridStyle}>
29
29
  {node.children?.map((child, index) => (
30
30
  <Box key={child.id} sx={{ width: '100%', height: '100%' }}>
31
- <FormRenderer node={child} path={`${path}[${index}]`} isEditing={isEditing} />
31
+ <FormRenderer node={child} path={`${path}[${index}]`} isEditing={isEditing} isReadOnly={isReadOnly} />
32
32
  </Box>
33
33
  ))}
34
34
  </Box>
@@ -4,7 +4,7 @@ import type { FieldProps } from '../FieldRegistry';
4
4
  import { LayoutPlaceholder } from './LayoutPlaceholder';
5
5
  import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
6
6
 
7
- export const FormTableCell: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
7
+ export const FormTableCell: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
8
8
  const childrenIds = node.children?.map(c => c.id) || [];
9
9
  const isEmpty = childrenIds.length === 0;
10
10
 
@@ -30,6 +30,7 @@ export const FormTableCell: React.FC<FieldProps> = observer(({ node, path, isEdi
30
30
  isEditing={true}
31
31
  layout="vertical"
32
32
  path={path}
33
+ isReadOnly={isReadOnly}
33
34
  />
34
35
  </LayoutPlaceholder>
35
36
  );
@@ -42,6 +43,7 @@ export const FormTableCell: React.FC<FieldProps> = observer(({ node, path, isEdi
42
43
  isEditing={false}
43
44
  layout="vertical"
44
45
  path={path}
46
+ isReadOnly={isReadOnly}
45
47
  />
46
48
  );
47
49
  });
@@ -6,7 +6,7 @@ import { FormRenderer } from '../FormRenderer';
6
6
  import { useFormStore } from '../../store/FormStoreContext';
7
7
  import { LayoutPlaceholder } from './LayoutPlaceholder';
8
8
 
9
- export const FormTabs: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
9
+ export const FormTabs: React.FC<FieldProps> = observer(({ node, path, isEditing, isReadOnly }) => {
10
10
  const formStore = useFormStore();
11
11
  const [activeTab, setActiveTab] = React.useState(0);
12
12
  const allTabs = node.children || [];
@@ -70,7 +70,7 @@ export const FormTabs: React.FC<FieldProps> = observer(({ node, path, isEditing
70
70
  <Box />
71
71
  </LayoutPlaceholder>
72
72
  ) : (
73
- currentTabNode && <FormRenderer key={currentTabNode.id} node={currentTabNode} path={path} isEditing={isEditing} />
73
+ currentTabNode && <FormRenderer key={currentTabNode.id} node={currentTabNode} path={path} isEditing={isEditing} isReadOnly={isReadOnly} />
74
74
  )}
75
75
  </Box>
76
76
  </Paper>
@@ -1,5 +1,4 @@
1
1
  import { registerPlugin } from '../plugins/PluginRegistry';
2
- import { registerComponent } from './FieldRegistry';
3
2
  import type { FieldPlugin } from '../plugins/FieldPlugin';
4
3
 
5
4
  // Field components
@@ -46,7 +45,7 @@ import React from 'react';
46
45
  const BUILT_IN_FIELD_PLUGINS: FieldPlugin[] = [
47
46
  { type: 'text', label: 'Text Field', icon: React.createElement(TextFieldsIcon), category: 'field', defaultProps: { label: 'New Text Field', width: 6 }, component: TextField },
48
47
  { type: 'number', label: 'Number Field', icon: React.createElement(NumbersIcon), category: 'field', defaultProps: { label: 'New Number Field', width: 6 }, component: NumberField },
49
- { type: 'checkbox', label: 'Checkbox', icon: React.createElement(CheckBoxIcon), category: 'field', defaultProps: { label: 'New Checkbox', width: 6 }, component: CheckboxField },
48
+ { type: 'checkbox', label: 'Checkbox', icon: React.createElement(CheckBoxIcon), category: 'field', defaultProps: { label: 'New Checkbox', width: 6, trueText: 'Да', falseText: 'Нет' }, component: CheckboxField },
50
49
  { type: 'select', label: 'Select', icon: React.createElement(ArrowDropDownCircleIcon), category: 'field', defaultProps: { label: 'New Select', width: 6, options: [{ label: 'Option 1', value: '1' }] }, component: SelectField },
51
50
  { type: 'date', label: 'Date Picker', icon: React.createElement(CalendarTodayIcon), category: 'field', defaultProps: { label: 'New Date', width: 6 }, component: DateField },
52
51
  { type: 'file', label: 'File Upload', icon: React.createElement(CloudUploadIcon), category: 'field', defaultProps: { label: 'New File Upload', width: 6 }, component: FileUploadField },
@@ -1,5 +1,6 @@
1
1
  import { makeAutoObservable, toJS } from "mobx";
2
2
  import i18next from 'i18next';
3
+ import { addDays, differenceInDays, parse, format, parseISO, isValid } from 'date-fns';
3
4
  import { transformAPIDataToSchema, transformSchemaToSubmission, transformDictionaryToOptions } from '../utils/apiTransformer';
4
5
  import { getPlugin, isContainerType } from '../plugins/PluginRegistry';
5
6
 
@@ -140,6 +141,7 @@ export class FormStore {
140
141
  const response = await fetch(`${this.apiBase}/schema/${formId}`);
141
142
  if (!response.ok) throw new Error("Failed to fetch schema");
142
143
  const schema = await response.json();
144
+ if (schema && !schema.id) schema.id = 'root'; // Enforce root ID
143
145
  this.rootNode = schema;
144
146
  this.applyDefaultValues();
145
147
  } catch (error) {
@@ -448,6 +450,40 @@ export class FormStore {
448
450
  ROUND: (v: any, p: number = 0) => {
449
451
  const factor = Math.pow(10, p);
450
452
  return Math.round(v * factor) / factor;
453
+ },
454
+ DATE_ADD: (date: any, days: any) => {
455
+ if (!date) return '';
456
+ const daysNum = Number(days) || 0;
457
+ let d = parseISO(date);
458
+ let isISO = true;
459
+ if (!isValid(d)) {
460
+ d = parse(date, 'dd-MM-yyyy', new Date());
461
+ isISO = false;
462
+ }
463
+ if (!isValid(d)) return date;
464
+ const result = addDays(d, daysNum);
465
+ return isISO ? result.toISOString() : format(result, 'dd-MM-yyyy');
466
+ },
467
+ DATE_DIFF: (date1: any, date2: any) => {
468
+ if (!date1 || !date2) return 0;
469
+ let d1 = parseISO(date1);
470
+ if (!isValid(d1)) d1 = parse(date1, 'dd-MM-yyyy', new Date());
471
+ let d2 = parseISO(date2);
472
+ if (!isValid(d2)) d2 = parse(date2, 'dd-MM-yyyy', new Date());
473
+ if (!isValid(d1) || !isValid(d2)) return 0;
474
+ return differenceInDays(d1, d2);
475
+ },
476
+ SWITCH: (val: any, ...args: any[]) => {
477
+ for (let i = 0; i < args.length - 1; i += 2) {
478
+ if (val === args[i]) return args[i + 1];
479
+ }
480
+ return args.length % 2 === 1 ? args[args.length - 1] : undefined;
481
+ },
482
+ IFS: (...args: any[]) => {
483
+ for (let i = 0; i < args.length - 1; i += 2) {
484
+ if (args[i]) return args[i + 1];
485
+ }
486
+ return args.length % 2 === 1 ? args[args.length - 1] : undefined;
451
487
  }
452
488
  };
453
489
 
@@ -739,7 +775,9 @@ export class FormStore {
739
775
  try {
740
776
  const stored = localStorage.getItem('form_schema');
741
777
  if (stored) {
742
- this.rootNode = JSON.parse(stored);
778
+ const schema = JSON.parse(stored);
779
+ if (schema && !schema.id) schema.id = 'root'; // Enforce root ID
780
+ this.rootNode = schema;
743
781
  this.applyDefaultValues();
744
782
  console.log('Schema loaded from localStorage');
745
783
  }
@@ -780,6 +818,7 @@ export class FormStore {
780
818
  }
781
819
 
782
820
  findNode(root: SchemaNode, id: string): SchemaNode | null {
821
+ if (id === 'root' && (root === this.rootNode || root.id === 'root')) return root;
783
822
  if (root.id === id) return root;
784
823
  if (root.children) {
785
824
  for (const child of root.children) {