@stormlmd/form-builder 0.3.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/builder/FormulaHelp.tsx +116 -0
- package/src/components/builder/OptionsEditor.tsx +1 -1
- package/src/components/builder/PropertiesPanel.tsx +114 -58
- package/src/components/registerComponents.ts +0 -1
- package/src/store/FormStore.ts +40 -1
package/build_output.txt
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -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
|
|
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}`,
|
|
@@ -51,6 +95,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
51
95
|
const { t } = useTranslation();
|
|
52
96
|
const node = formStore.selectedNode;
|
|
53
97
|
const [tabValue, setTabValue] = React.useState(0);
|
|
98
|
+
const [helpOpen, setHelpOpen] = React.useState(false);
|
|
54
99
|
|
|
55
100
|
const BUILT_IN_LAYOUT_TYPES = ['root', 'row', 'col', 'tabs', 'tab', 'paper', 'repeater', 'table', 'cell', 'divider'];
|
|
56
101
|
const isLayout = node ? BUILT_IN_LAYOUT_TYPES.includes(node.type) : false;
|
|
@@ -96,6 +141,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
96
141
|
};
|
|
97
142
|
|
|
98
143
|
const handleDelete = () => {
|
|
144
|
+
if (node.id === 'root') return;
|
|
99
145
|
formStore.removeNode(node.id);
|
|
100
146
|
formStore.closePropertiesModal(); // Also close modal on delete
|
|
101
147
|
};
|
|
@@ -158,58 +204,22 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
158
204
|
setAnchorEl(null);
|
|
159
205
|
};
|
|
160
206
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
);
|
|
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 });
|
|
213
223
|
};
|
|
214
224
|
|
|
215
225
|
return (
|
|
@@ -240,7 +250,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
240
250
|
<Box>
|
|
241
251
|
<Typography variant="subtitle1" fontWeight="bold">{t('properties.title')}</Typography>
|
|
242
252
|
<Typography variant="caption" color="text.secondary">
|
|
243
|
-
{node.type.toUpperCase()} | ID: {node.id}
|
|
253
|
+
{(node.type || 'unknown').toUpperCase()} | ID: {node.id || 'none'}
|
|
244
254
|
</Typography>
|
|
245
255
|
</Box>
|
|
246
256
|
<Button
|
|
@@ -377,6 +387,17 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
377
387
|
size="small"
|
|
378
388
|
fullWidth
|
|
379
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
|
+
}}
|
|
380
401
|
onChange={(e) => {
|
|
381
402
|
const val = e.target.value;
|
|
382
403
|
formStore.updateNode(node.id, { defaultValue: val });
|
|
@@ -388,7 +409,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
388
409
|
setCursorPos(target.selectionStart || 0);
|
|
389
410
|
}}
|
|
390
411
|
/>
|
|
391
|
-
<FormulaHints
|
|
412
|
+
<FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('defaultValue', txt)} t={t} />
|
|
392
413
|
</>
|
|
393
414
|
)}
|
|
394
415
|
</Box>
|
|
@@ -686,6 +707,17 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
686
707
|
size="small"
|
|
687
708
|
fullWidth
|
|
688
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
|
+
}}
|
|
689
721
|
onChange={(e) => {
|
|
690
722
|
const val = e.target.value;
|
|
691
723
|
formStore.updateNodeValidation(node.id, { min: val });
|
|
@@ -697,7 +729,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
697
729
|
}}
|
|
698
730
|
placeholder="e.g. 10 or {{otherField}} + 5"
|
|
699
731
|
/>
|
|
700
|
-
<FormulaHints
|
|
732
|
+
<FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('min', txt)} t={t} />
|
|
701
733
|
</Box>
|
|
702
734
|
<Box mb={1}>
|
|
703
735
|
<TextField
|
|
@@ -705,6 +737,17 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
705
737
|
size="small"
|
|
706
738
|
fullWidth
|
|
707
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
|
+
}}
|
|
708
751
|
onChange={(e) => {
|
|
709
752
|
const val = e.target.value;
|
|
710
753
|
formStore.updateNodeValidation(node.id, { max: val });
|
|
@@ -716,7 +759,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
716
759
|
}}
|
|
717
760
|
placeholder="e.g. 100 or {{otherField}} * 2"
|
|
718
761
|
/>
|
|
719
|
-
<FormulaHints
|
|
762
|
+
<FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('max', txt)} t={t} />
|
|
720
763
|
</Box>
|
|
721
764
|
</Box>
|
|
722
765
|
)}
|
|
@@ -837,6 +880,17 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
837
880
|
fullWidth
|
|
838
881
|
placeholder="e.g. {{price}} * {{qty}}"
|
|
839
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
|
+
}}
|
|
840
894
|
onChange={(e) => {
|
|
841
895
|
const val = e.target.value;
|
|
842
896
|
formStore.updateNodeCalculation(node.id, val ? { formula: val } : undefined);
|
|
@@ -848,7 +902,7 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
848
902
|
}}
|
|
849
903
|
helperText={t("properties.formulaHelper")}
|
|
850
904
|
/>
|
|
851
|
-
<FormulaHints
|
|
905
|
+
<FormulaHints node={node} formStore={formStore} insertText={(txt) => insertText('calculation', txt)} t={t} />
|
|
852
906
|
</CustomTabPanel>
|
|
853
907
|
|
|
854
908
|
{/* API Mapping Tab */}
|
|
@@ -890,6 +944,8 @@ export const PropertiesPanel: React.FC = observer(() => {
|
|
|
890
944
|
</FormControl>
|
|
891
945
|
)}
|
|
892
946
|
</CustomTabPanel>
|
|
947
|
+
|
|
948
|
+
<FormulaHelp open={helpOpen} onClose={() => setHelpOpen(false)} />
|
|
893
949
|
</Box>
|
|
894
950
|
);
|
|
895
951
|
});
|
package/src/store/FormStore.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|