@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.
- package/build_output.txt +0 -0
- package/package.json +4 -3
- package/src/components/FieldRegistry.ts +1 -0
- package/src/components/FormRenderer.tsx +4 -2
- package/src/components/builder/FormChildrenRenderer.tsx +4 -2
- package/src/components/builder/FormulaHelp.tsx +116 -0
- package/src/components/builder/OptionsEditor.tsx +1 -1
- package/src/components/builder/PropertiesPanel.tsx +149 -58
- package/src/components/fields/CheckboxField.tsx +15 -2
- package/src/components/fields/DateField.tsx +14 -1
- package/src/components/fields/FileUploadField.tsx +14 -1
- package/src/components/fields/LabelField.tsx +1 -1
- package/src/components/fields/NumberField.tsx +14 -1
- package/src/components/fields/RichTextField.tsx +95 -18
- package/src/components/fields/SelectField.tsx +15 -1
- package/src/components/fields/TextField.tsx +14 -1
- package/src/components/layout/FormCol.tsx +2 -2
- package/src/components/layout/FormPaper.tsx +2 -1
- package/src/components/layout/FormRepeater.tsx +5 -3
- package/src/components/layout/FormRow.tsx +6 -2
- package/src/components/layout/FormTab.tsx +2 -1
- package/src/components/layout/FormTable.tsx +2 -2
- package/src/components/layout/FormTableCell.tsx +3 -1
- package/src/components/layout/FormTabs.tsx +2 -2
- package/src/components/registerComponents.ts +1 -2
- package/src/store/FormStore.ts +40 -1
package/build_output.txt
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stormlmd/form-builder",
|
|
3
|
-
"version": "0.3.
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
-
);
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = (
|
|
16
|
+
const handleChange = (val: string) => {
|
|
15
17
|
if (fullPath) {
|
|
16
|
-
formStore.updateField(fullPath,
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 },
|
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) {
|