@stormlmd/form-builder 0.1.0
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/README.md +73 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/package.json +60 -0
- package/public/vite.svg +1 -0
- package/src/App.css +42 -0
- package/src/App.tsx +83 -0
- package/src/assets/react.svg +1 -0
- package/src/components/FieldRegistry.ts +34 -0
- package/src/components/FormContainer.tsx +25 -0
- package/src/components/FormRenderer.tsx +121 -0
- package/src/components/builder/DraggableTool.tsx +66 -0
- package/src/components/builder/DroppableCanvas.tsx +51 -0
- package/src/components/builder/EditorWrapper.tsx +87 -0
- package/src/components/builder/FormBuilder.tsx +313 -0
- package/src/components/builder/FormChildrenRenderer.tsx +68 -0
- package/src/components/builder/IntegrationSettings.tsx +110 -0
- package/src/components/builder/PropertiesModal.tsx +75 -0
- package/src/components/builder/PropertiesPanel.tsx +858 -0
- package/src/components/builder/SortableNode.tsx +53 -0
- package/src/components/builder/Toolbox.tsx +123 -0
- package/src/components/fields/CheckboxField.tsx +41 -0
- package/src/components/fields/DateField.tsx +56 -0
- package/src/components/fields/FileUploadField.tsx +45 -0
- package/src/components/fields/LabelField.tsx +20 -0
- package/src/components/fields/NumberField.tsx +39 -0
- package/src/components/fields/RichTextField.tsx +39 -0
- package/src/components/fields/SelectField.tsx +64 -0
- package/src/components/fields/TextField.tsx +44 -0
- package/src/components/layout/FormCol.tsx +30 -0
- package/src/components/layout/FormDivider.tsx +19 -0
- package/src/components/layout/FormPaper.tsx +85 -0
- package/src/components/layout/FormRepeater.tsx +130 -0
- package/src/components/layout/FormRow.tsx +61 -0
- package/src/components/layout/FormTab.tsx +33 -0
- package/src/components/layout/FormTable.tsx +47 -0
- package/src/components/layout/FormTableCell.tsx +47 -0
- package/src/components/layout/FormTabs.tsx +77 -0
- package/src/components/layout/LayoutPlaceholder.tsx +85 -0
- package/src/components/registerComponents.ts +30 -0
- package/src/index.css +75 -0
- package/src/index.ts +5 -0
- package/src/main.tsx +10 -0
- package/src/store/FormStore.ts +811 -0
- package/src/utils/apiTransformer.ts +206 -0
- package/src/utils/idGenerator.ts +3 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +46 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useSortable } from '@dnd-kit/sortable';
|
|
3
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
4
|
+
import { Box, type SxProps, type Theme } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
import { observer } from 'mobx-react-lite';
|
|
7
|
+
import { formStore } from '../../store/FormStore';
|
|
8
|
+
|
|
9
|
+
interface SortableNodeProps {
|
|
10
|
+
id: string;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
sx?: SxProps<Theme>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SortableNode: React.FC<SortableNodeProps> = observer(({ id, children, sx }) => {
|
|
16
|
+
const {
|
|
17
|
+
attributes,
|
|
18
|
+
listeners,
|
|
19
|
+
setNodeRef,
|
|
20
|
+
transform,
|
|
21
|
+
transition,
|
|
22
|
+
isDragging,
|
|
23
|
+
isOver
|
|
24
|
+
} = useSortable({ id });
|
|
25
|
+
|
|
26
|
+
const style = {
|
|
27
|
+
transform: CSS.Transform.toString(transform),
|
|
28
|
+
transition,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Box
|
|
33
|
+
ref={setNodeRef}
|
|
34
|
+
style={style}
|
|
35
|
+
{...attributes}
|
|
36
|
+
{...listeners}
|
|
37
|
+
sx={{
|
|
38
|
+
display: 'flex',
|
|
39
|
+
flexDirection: 'column',
|
|
40
|
+
opacity: isDragging ? 0.5 : 1,
|
|
41
|
+
position: 'relative',
|
|
42
|
+
zIndex: isDragging ? 999 : 'auto',
|
|
43
|
+
boxSizing: 'border-box',
|
|
44
|
+
minHeight: '20px',
|
|
45
|
+
height: '100%', // Allow stretching
|
|
46
|
+
outline: isOver && !isDragging && !formStore.dropIndicator ? '1px dashed #4caf50' : undefined,
|
|
47
|
+
...sx
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, List, ListItem, Tabs, Tab } from '@mui/material';
|
|
3
|
+
import { formStore, type NodeType } from '../../store/FormStore';
|
|
4
|
+
import { generateId } from '../../utils/idGenerator';
|
|
5
|
+
import { DraggableTool } from './DraggableTool';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
|
8
|
+
import NumbersIcon from '@mui/icons-material/Numbers';
|
|
9
|
+
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
|
10
|
+
import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle';
|
|
11
|
+
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
|
12
|
+
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
|
13
|
+
import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
|
|
14
|
+
import LabelIcon from '@mui/icons-material/Label';
|
|
15
|
+
import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
|
|
16
|
+
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
|
|
17
|
+
import TabIcon from '@mui/icons-material/Tab';
|
|
18
|
+
import RepeatIcon from '@mui/icons-material/Repeat';
|
|
19
|
+
import TableChartIcon from '@mui/icons-material/TableChart';
|
|
20
|
+
import LayersIcon from '@mui/icons-material/Layers';
|
|
21
|
+
|
|
22
|
+
interface ToolItem {
|
|
23
|
+
type: NodeType;
|
|
24
|
+
label: string;
|
|
25
|
+
icon: React.ReactNode;
|
|
26
|
+
defaultProps?: Record<string, any>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TOOLS: ToolItem[] = [
|
|
30
|
+
{ type: 'text', label: 'Text Field', icon: <TextFieldsIcon />, defaultProps: { label: 'New Text Field', width: 6 } },
|
|
31
|
+
{ type: 'number', label: 'Number Field', icon: <NumbersIcon />, defaultProps: { label: 'New Number Field', width: 6 } },
|
|
32
|
+
{ type: 'checkbox', label: 'Checkbox', icon: <CheckBoxIcon />, defaultProps: { label: 'New Checkbox', width: 6 } },
|
|
33
|
+
{ type: 'select', label: 'Select', icon: <ArrowDropDownCircleIcon />, defaultProps: { label: 'New Select', width: 6, options: [{ label: 'Option 1', value: '1' }] } },
|
|
34
|
+
{ type: 'date', label: 'Date Picker', icon: <CalendarTodayIcon />, defaultProps: { label: 'New Date', width: 6 } },
|
|
35
|
+
{ type: 'file', label: 'File Upload', icon: <CloudUploadIcon />, defaultProps: { label: 'New File Upload', width: 6 } },
|
|
36
|
+
{ type: 'richtext', label: 'Rich Text', icon: <FormatQuoteIcon />, defaultProps: { label: 'New Rich Text', width: 6 } },
|
|
37
|
+
{ type: 'label', label: 'Label', icon: <LabelIcon />, defaultProps: { text: 'Static Tex', variant: 'body1', width: 6 } },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const LAYOUT_TOOLS: ToolItem[] = [
|
|
41
|
+
{ type: 'divider', label: 'Divider', icon: <HorizontalRuleIcon />, defaultProps: { text: '' } },
|
|
42
|
+
{ type: 'col', label: 'Column', icon: <ViewColumnIcon />, defaultProps: { cols: 6 } },
|
|
43
|
+
{ type: 'tabs', label: 'Tabs', icon: <TabIcon />, defaultProps: {} },
|
|
44
|
+
{ type: 'repeater', label: 'Repeater', icon: <RepeatIcon />, defaultProps: { label: 'Repeater', addLabel: 'addItem' } },
|
|
45
|
+
{ type: 'table', label: 'Table', icon: <TableChartIcon />, defaultProps: { rows: 2, cols: 2, width: 12 } },
|
|
46
|
+
{ type: 'paper', label: 'Paper', icon: <LayersIcon />, defaultProps: { label: 'Paper Group', padding: 2, variant: 'elevation', elevation: 1 } },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const Toolbox: React.FC = () => {
|
|
50
|
+
const { t } = useTranslation();
|
|
51
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
52
|
+
|
|
53
|
+
const handleAdd = (tool: ToolItem) => {
|
|
54
|
+
const parentId = formStore.selectedNodeId || 'root';
|
|
55
|
+
const newNode = {
|
|
56
|
+
id: generateId(tool.type),
|
|
57
|
+
type: tool.type,
|
|
58
|
+
props: { ...tool.defaultProps, name: generateId('field'), label: t(`toolbox.${tool.type}`) },
|
|
59
|
+
children: [] as any[]
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Special initialization for Table
|
|
63
|
+
if (tool.type === 'table') {
|
|
64
|
+
const rows = tool.defaultProps?.rows || 2;
|
|
65
|
+
const cols = tool.defaultProps?.cols || 2;
|
|
66
|
+
const cells = [];
|
|
67
|
+
for (let i = 0; i < rows * cols; i++) {
|
|
68
|
+
cells.push({
|
|
69
|
+
id: generateId('cell'),
|
|
70
|
+
type: 'cell',
|
|
71
|
+
children: []
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
newNode.children = cells;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log('Toolbox adding node:', newNode);
|
|
78
|
+
formStore.addNode(parentId, newNode as any);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Box display="flex" flexDirection="column" height="100%">
|
|
83
|
+
<Box borderBottom={1} borderColor="divider">
|
|
84
|
+
<Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)} variant="fullWidth">
|
|
85
|
+
<Tab label={t("toolbox.fields")} sx={{ textTransform: 'none', minHeight: 48 }} />
|
|
86
|
+
<Tab label={t("toolbox.layout")} sx={{ textTransform: 'none', minHeight: 48 }} />
|
|
87
|
+
</Tabs>
|
|
88
|
+
</Box>
|
|
89
|
+
<Box flexGrow={1} overflow="auto">
|
|
90
|
+
{activeTab === 0 && (
|
|
91
|
+
<List sx={{ pt: 1 }}>
|
|
92
|
+
{TOOLS.map((tool) => (
|
|
93
|
+
<ListItem key={tool.type} disablePadding sx={{ px: 2, py: 0.5 }}>
|
|
94
|
+
<DraggableTool
|
|
95
|
+
type={tool.type}
|
|
96
|
+
label={t(`toolbox.${tool.type}`)}
|
|
97
|
+
icon={tool.icon}
|
|
98
|
+
defaultProps={tool.defaultProps}
|
|
99
|
+
onClick={() => handleAdd(tool)}
|
|
100
|
+
/>
|
|
101
|
+
</ListItem>
|
|
102
|
+
))}
|
|
103
|
+
</List>
|
|
104
|
+
)}
|
|
105
|
+
{activeTab === 1 && (
|
|
106
|
+
<List sx={{ pt: 1 }}>
|
|
107
|
+
{LAYOUT_TOOLS.map((tool) => (
|
|
108
|
+
<ListItem key={tool.type} disablePadding sx={{ px: 2, py: 0.5 }}>
|
|
109
|
+
<DraggableTool
|
|
110
|
+
type={tool.type}
|
|
111
|
+
label={t(`toolbox.${tool.type}`)}
|
|
112
|
+
icon={tool.icon}
|
|
113
|
+
defaultProps={tool.defaultProps}
|
|
114
|
+
onClick={() => handleAdd(tool)}
|
|
115
|
+
/>
|
|
116
|
+
</ListItem>
|
|
117
|
+
))}
|
|
118
|
+
</List>
|
|
119
|
+
)}
|
|
120
|
+
</Box>
|
|
121
|
+
</Box>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Checkbox, FormControlLabel, FormGroup, Box, Typography } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { formStore } from '../../store/FormStore';
|
|
5
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
6
|
+
|
|
7
|
+
export const CheckboxField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const { name, label } = node.props || {};
|
|
9
|
+
const fullPath = path && name ? `${path}.${name}` : name;
|
|
10
|
+
const value = fullPath ? formStore.getValue(fullPath) : false;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Box mb={0.5}>
|
|
14
|
+
{isEditing && (
|
|
15
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
|
|
16
|
+
{label || 'Checkbox'} ({name})
|
|
17
|
+
</Typography>
|
|
18
|
+
)}
|
|
19
|
+
<FormGroup>
|
|
20
|
+
<FormControlLabel
|
|
21
|
+
control={
|
|
22
|
+
<Checkbox
|
|
23
|
+
size="small"
|
|
24
|
+
checked={!!value}
|
|
25
|
+
onChange={(e) => {
|
|
26
|
+
if (fullPath) formStore.updateValue(fullPath, e.target.checked);
|
|
27
|
+
}}
|
|
28
|
+
disabled={isEditing || !!node.calculation?.formula}
|
|
29
|
+
/>
|
|
30
|
+
}
|
|
31
|
+
label={
|
|
32
|
+
<Typography variant="body2" color="text.primary">
|
|
33
|
+
{label || name || 'Checkbox'}
|
|
34
|
+
{node.validation?.required && <span style={{ color: 'red' }}> *</span>}
|
|
35
|
+
</Typography>
|
|
36
|
+
}
|
|
37
|
+
/>
|
|
38
|
+
</FormGroup>
|
|
39
|
+
</Box>
|
|
40
|
+
);
|
|
41
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
3
|
+
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
4
|
+
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
|
5
|
+
import { observer } from 'mobx-react-lite';
|
|
6
|
+
import { formStore } from '../../store/FormStore';
|
|
7
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
8
|
+
import { Box, Typography, TextField } from '@mui/material';
|
|
9
|
+
|
|
10
|
+
export const DateField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
11
|
+
const { name, label, placeholder } = node.props || {};
|
|
12
|
+
const fullPath = path && name ? `${path}.${name}` : name;
|
|
13
|
+
const value = fullPath ? formStore.getValue(fullPath) : null;
|
|
14
|
+
const error = fullPath ? formStore.errors[fullPath] : undefined;
|
|
15
|
+
|
|
16
|
+
const handleChange = (newValue: Date | null) => {
|
|
17
|
+
if (fullPath) {
|
|
18
|
+
const isoVal = newValue ? newValue.toISOString() : null;
|
|
19
|
+
formStore.updateField(fullPath, isoVal, node.validation);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Create a Date object from value if string
|
|
24
|
+
const dateValue = value ? new Date(value) : null;
|
|
25
|
+
|
|
26
|
+
const content = (
|
|
27
|
+
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
|
28
|
+
<DatePicker
|
|
29
|
+
value={dateValue}
|
|
30
|
+
onChange={handleChange}
|
|
31
|
+
inputFormat="dd-MM-yyyy"
|
|
32
|
+
renderInput={(params) => (
|
|
33
|
+
<TextField
|
|
34
|
+
{...params}
|
|
35
|
+
fullWidth
|
|
36
|
+
size="small"
|
|
37
|
+
placeholder={placeholder}
|
|
38
|
+
disabled={isEditing || !!node.calculation?.formula}
|
|
39
|
+
error={!!error}
|
|
40
|
+
helperText={error}
|
|
41
|
+
/>
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
</LocalizationProvider>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Box mb={0.5}>
|
|
49
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
|
|
50
|
+
{label || (isEditing ? 'Date Field' : '')} {isEditing ? `(${name})` : ''}
|
|
51
|
+
{node.validation?.required && <span style={{ color: 'red' }}> *</span>}
|
|
52
|
+
</Typography>
|
|
53
|
+
{content}
|
|
54
|
+
</Box>
|
|
55
|
+
);
|
|
56
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Button, Typography, Input, FormHelperText } from '@mui/material';
|
|
3
|
+
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
|
4
|
+
import { observer } from 'mobx-react-lite';
|
|
5
|
+
import { formStore } from '../../store/FormStore';
|
|
6
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
7
|
+
|
|
8
|
+
export const FileUploadField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
9
|
+
const { name, label } = node.props || {};
|
|
10
|
+
const fullPath = path && name ? `${path}.${name}` : name;
|
|
11
|
+
const value = fullPath ? formStore.getValue(fullPath) : null;
|
|
12
|
+
const error = fullPath ? formStore.errors[fullPath] : undefined;
|
|
13
|
+
|
|
14
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
15
|
+
if (fullPath && event.target.files && event.target.files[0]) {
|
|
16
|
+
// Store file name for now (or base64 if needed, but keeping it simple)
|
|
17
|
+
formStore.updateField(fullPath, event.target.files[0].name, node.validation);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Box mb={1}>
|
|
23
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
|
|
24
|
+
{label || (isEditing ? 'File Upload' : '')} {isEditing ? `(${name})` : ''}
|
|
25
|
+
{node.validation?.required && <span style={{ color: 'red' }}> *</span>}
|
|
26
|
+
</Typography>
|
|
27
|
+
<Button
|
|
28
|
+
component="label"
|
|
29
|
+
variant="outlined"
|
|
30
|
+
startIcon={<CloudUploadIcon />}
|
|
31
|
+
fullWidth
|
|
32
|
+
disabled={isEditing || !!node.calculation?.formula}
|
|
33
|
+
color={error ? 'error' : 'primary'}
|
|
34
|
+
>
|
|
35
|
+
{value ? `Selected: ${value}` : (label || 'Upload File')}
|
|
36
|
+
<Input
|
|
37
|
+
type="file"
|
|
38
|
+
sx={{ display: 'none' }}
|
|
39
|
+
onChange={handleChange}
|
|
40
|
+
/>
|
|
41
|
+
</Button>
|
|
42
|
+
{error && <FormHelperText error>{error}</FormHelperText>}
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Typography, Box } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { formStore } from '../../store/FormStore';
|
|
5
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
6
|
+
|
|
7
|
+
export const LabelField: React.FC<FieldProps> = observer(({ node }) => {
|
|
8
|
+
const { name, label = '', align = 'left', variant = 'body1' } = node.props || {};
|
|
9
|
+
|
|
10
|
+
const value = name ? formStore.getValue(name) : undefined;
|
|
11
|
+
const displayText = value !== undefined ? value : label;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Box width="100%" sx={{ whiteSpace: 'pre-wrap' }}>
|
|
15
|
+
<Typography variant={variant} align={align} color="text.primary">
|
|
16
|
+
{displayText}
|
|
17
|
+
</Typography>
|
|
18
|
+
</Box>
|
|
19
|
+
);
|
|
20
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TextField as MuiTextField, Box, Typography } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { formStore } from '../../store/FormStore';
|
|
5
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
6
|
+
|
|
7
|
+
export const NumberField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const { name, label } = node.props || {};
|
|
9
|
+
|
|
10
|
+
const fullPath = path && name ? `${path}.${name}` : name;
|
|
11
|
+
const value = fullPath ? formStore.getValue(fullPath) : '';
|
|
12
|
+
const error = fullPath ? formStore.errors[fullPath] : undefined;
|
|
13
|
+
|
|
14
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
15
|
+
if (fullPath) {
|
|
16
|
+
const val = e.target.value === '' ? undefined : Number(e.target.value);
|
|
17
|
+
formStore.updateField(fullPath, val, node.validation);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Box mb={0.5}>
|
|
23
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
|
|
24
|
+
{label || (isEditing ? 'Number Field' : '')} {isEditing ? `(${name})` : ''}
|
|
25
|
+
{node.validation?.required && <span style={{ color: 'red' }}> *</span>}
|
|
26
|
+
</Typography>
|
|
27
|
+
<MuiTextField
|
|
28
|
+
type="number"
|
|
29
|
+
fullWidth
|
|
30
|
+
value={value ?? ''}
|
|
31
|
+
onChange={handleChange}
|
|
32
|
+
size="small"
|
|
33
|
+
disabled={isEditing || !!node.calculation?.formula}
|
|
34
|
+
error={!!error}
|
|
35
|
+
helperText={error}
|
|
36
|
+
/>
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TextField as MuiTextField, Typography, Box } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { formStore } from '../../store/FormStore';
|
|
5
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
6
|
+
|
|
7
|
+
export const RichTextField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const { name, label, placeholder } = node.props || {};
|
|
9
|
+
const fullPath = path && name ? `${path}.${name}` : name;
|
|
10
|
+
const value = fullPath ? formStore.getValue(fullPath) : '';
|
|
11
|
+
const error = fullPath ? formStore.errors[fullPath] : undefined;
|
|
12
|
+
|
|
13
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
14
|
+
if (fullPath) {
|
|
15
|
+
formStore.updateField(fullPath, e.target.value, node.validation);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box mb={0.5}>
|
|
21
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
|
|
22
|
+
{label || (isEditing ? 'Rich Text' : '')} {isEditing ? `(${name})` : ''}
|
|
23
|
+
{node.validation?.required && <span style={{ color: 'red' }}> *</span>}
|
|
24
|
+
</Typography>
|
|
25
|
+
<MuiTextField
|
|
26
|
+
placeholder={placeholder}
|
|
27
|
+
fullWidth
|
|
28
|
+
multiline
|
|
29
|
+
minRows={4}
|
|
30
|
+
value={value}
|
|
31
|
+
onChange={handleChange}
|
|
32
|
+
size="small"
|
|
33
|
+
disabled={isEditing || !!node.calculation?.formula}
|
|
34
|
+
error={!!error}
|
|
35
|
+
helperText={error}
|
|
36
|
+
/>
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FormControl, Select, MenuItem, Box, Typography, FormHelperText, Autocomplete, TextField as MuiTextField } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { formStore } from '../../store/FormStore';
|
|
5
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
6
|
+
|
|
7
|
+
export const SelectField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const { name, label, options = [], enableAutocomplete = false, placeholder } = node.props || {};
|
|
9
|
+
const fullPath = path && name ? `${path}.${name}` : name;
|
|
10
|
+
const value = fullPath ? formStore.getValue(fullPath) : '';
|
|
11
|
+
const error = fullPath ? formStore.errors[fullPath] : undefined;
|
|
12
|
+
|
|
13
|
+
const handleChange = (val: any) => {
|
|
14
|
+
if (fullPath) {
|
|
15
|
+
formStore.updateField(fullPath, val, node.validation);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const renderSelect = () => (
|
|
20
|
+
<FormControl fullWidth size="small" disabled={isEditing || !!node.calculation?.formula} error={!!error}>
|
|
21
|
+
<Select
|
|
22
|
+
value={value}
|
|
23
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
24
|
+
>
|
|
25
|
+
{options.map((opt: any, index: number) => (
|
|
26
|
+
<MenuItem key={index} value={opt.value}>
|
|
27
|
+
{opt.label}
|
|
28
|
+
</MenuItem>
|
|
29
|
+
))}
|
|
30
|
+
</Select>
|
|
31
|
+
{error && <FormHelperText>{error}</FormHelperText>}
|
|
32
|
+
</FormControl>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const renderAutocomplete = () => (
|
|
36
|
+
<Autocomplete
|
|
37
|
+
options={options}
|
|
38
|
+
getOptionLabel={(option: any) => option.label || ''}
|
|
39
|
+
value={options.find((opt: any) => opt.value === value) || null}
|
|
40
|
+
onChange={(_, newValue: any) => handleChange(newValue ? newValue.value : null)}
|
|
41
|
+
renderInput={(params) => (
|
|
42
|
+
<MuiTextField
|
|
43
|
+
{...params}
|
|
44
|
+
size="small"
|
|
45
|
+
placeholder={placeholder}
|
|
46
|
+
error={!!error}
|
|
47
|
+
helperText={error}
|
|
48
|
+
disabled={isEditing || !!node.calculation?.formula}
|
|
49
|
+
/>
|
|
50
|
+
)}
|
|
51
|
+
disabled={isEditing || !!node.calculation?.formula}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Box mb={0.5}>
|
|
57
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
|
|
58
|
+
{label || (isEditing ? 'Select Field' : '')} {isEditing ? `(${name})` : ''}
|
|
59
|
+
{node.validation?.required && <span style={{ color: 'red' }}> *</span>}
|
|
60
|
+
</Typography>
|
|
61
|
+
{enableAutocomplete ? renderAutocomplete() : renderSelect()}
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TextField as MuiTextField, Box, Typography } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { formStore } from '../../store/FormStore';
|
|
5
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
6
|
+
|
|
7
|
+
export const TextField: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
8
|
+
const { name, label, placeholder } = node.props || {};
|
|
9
|
+
|
|
10
|
+
// Resolve full path: path.name or just name
|
|
11
|
+
const fullPath = path && name ? `${path}.${name}` : name;
|
|
12
|
+
const value = fullPath ? (formStore.getValue(fullPath) ?? node.props?.value ?? '') : (node.props?.value ?? '');
|
|
13
|
+
const error = fullPath ? formStore.errors[fullPath] : undefined;
|
|
14
|
+
|
|
15
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
16
|
+
if (fullPath) {
|
|
17
|
+
formStore.updateField(fullPath, e.target.value, node.validation);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Box mb={0.5}>
|
|
23
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={0.2} ml={0.5}>
|
|
24
|
+
{label || (isEditing ? 'Text Field' : '')} {isEditing ? `(${name})` : ''}
|
|
25
|
+
{node.validation?.required && <span style={{ color: 'red' }}> *</span>}
|
|
26
|
+
</Typography>
|
|
27
|
+
<MuiTextField
|
|
28
|
+
placeholder={placeholder}
|
|
29
|
+
fullWidth
|
|
30
|
+
value={value}
|
|
31
|
+
onChange={handleChange}
|
|
32
|
+
size="small"
|
|
33
|
+
disabled={isEditing || !!node.calculation?.formula}
|
|
34
|
+
error={!!error}
|
|
35
|
+
helperText={error}
|
|
36
|
+
onBlur={() => {
|
|
37
|
+
if (fullPath && node.validation) {
|
|
38
|
+
formStore.validateField(fullPath, value, node.validation);
|
|
39
|
+
}
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
5
|
+
import { FormRenderer } from '../FormRenderer';
|
|
6
|
+
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
|
|
7
|
+
import { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
8
|
+
|
|
9
|
+
export const FormCol: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
10
|
+
const childrenIds = node.children?.map(c => c.id) || [];
|
|
11
|
+
|
|
12
|
+
const content = (
|
|
13
|
+
<Box display="flex" flexDirection="column" gap={0.5} width="100%" sx={{ flexGrow: 1, height: '100%' }}>
|
|
14
|
+
{node.children?.map(child => (
|
|
15
|
+
<FormRenderer key={child.id} node={child} path={path} isEditing={isEditing} />
|
|
16
|
+
))}
|
|
17
|
+
</Box>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (isEditing) {
|
|
21
|
+
return (
|
|
22
|
+
<LayoutPlaceholder isEditing={isEditing} label="Column" isEmpty={childrenIds.length === 0}>
|
|
23
|
+
<SortableContext items={childrenIds} strategy={rectSortingStrategy}>
|
|
24
|
+
{content}
|
|
25
|
+
</SortableContext>
|
|
26
|
+
</LayoutPlaceholder>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return content;
|
|
30
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Divider, Chip, Box } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
5
|
+
|
|
6
|
+
export const FormDivider: React.FC<FieldProps> = observer(({ node }) => {
|
|
7
|
+
const { text, align = 'center' } = node.props || {};
|
|
8
|
+
|
|
9
|
+
// Map align to Divider textAlign if text is present
|
|
10
|
+
const dividerAlign = (align === 'left' ? 'left' : align === 'right' ? 'right' : 'center') as 'left' | 'right' | 'center';
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Box width="100%" py={1}>
|
|
14
|
+
<Divider textAlign={text ? dividerAlign : undefined}>
|
|
15
|
+
{text && <Chip label={text} size="small" />}
|
|
16
|
+
</Divider>
|
|
17
|
+
</Box>
|
|
18
|
+
);
|
|
19
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Paper, Box, Typography } from '@mui/material';
|
|
3
|
+
import type { FieldProps } from '../FieldRegistry';
|
|
4
|
+
import { observer } from 'mobx-react-lite';
|
|
5
|
+
import { FormChildrenRenderer } from '../builder/FormChildrenRenderer';
|
|
6
|
+
import { LayoutPlaceholder } from './LayoutPlaceholder';
|
|
7
|
+
|
|
8
|
+
export const FormPaper: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
|
|
9
|
+
const { label, elevation: elevationProp, padding = 2, variant: variantProp } = node.props || {};
|
|
10
|
+
const childrenIds = node.children?.map(c => c.id) || [];
|
|
11
|
+
|
|
12
|
+
// In preview mode, default to cleaner look if no props specified
|
|
13
|
+
const elevation = isEditing ? (elevationProp ?? 1) : (elevationProp ?? 0);
|
|
14
|
+
const variant = isEditing ? (variantProp ?? 'elevation') : (variantProp ?? 'transparent');
|
|
15
|
+
const isTransparent = variant === 'transparent';
|
|
16
|
+
|
|
17
|
+
const childrenContent = (
|
|
18
|
+
<FormChildrenRenderer
|
|
19
|
+
parentId={node.id}
|
|
20
|
+
children={node.children}
|
|
21
|
+
isEditing={!!isEditing}
|
|
22
|
+
path={path}
|
|
23
|
+
layout="horizontal"
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const paperContent = (
|
|
28
|
+
<>
|
|
29
|
+
{label && <Typography variant="h6" mb={1}>{label}</Typography>}
|
|
30
|
+
{isEditing ? (
|
|
31
|
+
<LayoutPlaceholder
|
|
32
|
+
isEditing={isEditing}
|
|
33
|
+
label="Paper Content"
|
|
34
|
+
isEmpty={childrenIds.length === 0}
|
|
35
|
+
sx={{
|
|
36
|
+
flexGrow: 1,
|
|
37
|
+
display: 'flex',
|
|
38
|
+
flexDirection: 'column',
|
|
39
|
+
// Ensure the placeholder itself allows children to span height if possible
|
|
40
|
+
minHeight: childrenIds.length === 0 ? 120 : '100%'
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{childrenContent}
|
|
44
|
+
</LayoutPlaceholder>
|
|
45
|
+
) : (
|
|
46
|
+
childrenContent
|
|
47
|
+
)}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!isEditing && isTransparent) {
|
|
52
|
+
return (
|
|
53
|
+
<Box sx={{
|
|
54
|
+
p: padding,
|
|
55
|
+
height: '100%',
|
|
56
|
+
display: 'flex',
|
|
57
|
+
flexDirection: 'column',
|
|
58
|
+
mb: 1
|
|
59
|
+
}}>
|
|
60
|
+
{paperContent}
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Paper
|
|
67
|
+
variant={variant === 'outlined' ? 'outlined' : 'elevation'}
|
|
68
|
+
elevation={isTransparent ? 0 : (elevation ?? 1)}
|
|
69
|
+
sx={{
|
|
70
|
+
p: padding,
|
|
71
|
+
height: '100%',
|
|
72
|
+
display: 'flex',
|
|
73
|
+
flexDirection: 'column',
|
|
74
|
+
backgroundColor: isTransparent ? 'transparent' : undefined,
|
|
75
|
+
backgroundImage: isTransparent ? 'none' : undefined,
|
|
76
|
+
boxShadow: isTransparent ? 'none !important' : undefined,
|
|
77
|
+
border: isTransparent ? 'none !important' : undefined,
|
|
78
|
+
outline: isTransparent ? 'none !important' : undefined,
|
|
79
|
+
overflow: 'hidden' // Added to prevent inner margins from leaking out
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{paperContent}
|
|
83
|
+
</Paper>
|
|
84
|
+
);
|
|
85
|
+
});
|