@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,858 @@
|
|
|
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';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { formStore } from '../../store/FormStore';
|
|
5
|
+
import type { SchemaNode } from '../../store/FormStore';
|
|
6
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
|
|
9
|
+
interface TabPanelProps {
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
index: number;
|
|
12
|
+
value: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function CustomTabPanel(props: TabPanelProps) {
|
|
16
|
+
const { children, value, index, ...other } = props;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Box
|
|
20
|
+
role="tabpanel"
|
|
21
|
+
hidden={value !== index}
|
|
22
|
+
id={`simple-tabpanel-${index}`}
|
|
23
|
+
aria-labelledby={`simple-tab-${index}`}
|
|
24
|
+
{...other}
|
|
25
|
+
sx={{
|
|
26
|
+
flexGrow: 1,
|
|
27
|
+
overflow: 'hidden',
|
|
28
|
+
display: value === index ? 'flex' : 'none',
|
|
29
|
+
flexDirection: 'column'
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{value === index && (
|
|
33
|
+
<Box sx={{ p: 2, pt: 2, flexGrow: 1, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
34
|
+
{children}
|
|
35
|
+
</Box>
|
|
36
|
+
)}
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function a11yProps(index: number) {
|
|
42
|
+
return {
|
|
43
|
+
id: `simple-tab-${index}`,
|
|
44
|
+
'aria-controls': `simple-tabpanel-${index}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const PropertiesPanel: React.FC = observer(() => {
|
|
49
|
+
const { t } = useTranslation();
|
|
50
|
+
const node = formStore.selectedNode;
|
|
51
|
+
const [tabValue, setTabValue] = React.useState(0);
|
|
52
|
+
|
|
53
|
+
const isLayout = node ? ['root', 'row', 'col', 'tabs', 'tab', 'paper', 'repeater', 'table', 'cell', 'divider'].includes(node.type) : false;
|
|
54
|
+
|
|
55
|
+
const allTabs = [
|
|
56
|
+
{ label: t("properties.tabView"), index: 0, visible: true },
|
|
57
|
+
{ label: t("properties.tabValidation"), index: 1, visible: !isLayout },
|
|
58
|
+
{ label: t("properties.tabMapping"), index: 4, visible: !isLayout }, // Added mapping tab
|
|
59
|
+
{ label: t("properties.tabLogic"), index: 2, visible: true },
|
|
60
|
+
{ label: t("properties.tabCalc"), index: 3, visible: !isLayout },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const visibleTabs = allTabs.filter(t => t.visible);
|
|
64
|
+
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
if (tabValue >= visibleTabs.length) {
|
|
67
|
+
setTabValue(0);
|
|
68
|
+
}
|
|
69
|
+
}, [node?.id, visibleTabs.length]);
|
|
70
|
+
|
|
71
|
+
if (!node) {
|
|
72
|
+
return (
|
|
73
|
+
<Box p={2}>
|
|
74
|
+
<Typography variant="body2" color="text.secondary">
|
|
75
|
+
{t('properties.noSelection')}
|
|
76
|
+
</Typography>
|
|
77
|
+
</Box>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Autocomplete State
|
|
82
|
+
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
83
|
+
const [filter, setFilter] = React.useState('');
|
|
84
|
+
const [cursorPos, setCursorPos] = React.useState(0);
|
|
85
|
+
// Track which field checks for autocomplete
|
|
86
|
+
const [activeFormulaField, setActiveFormulaField] = React.useState<'calculation' | 'defaultValue' | 'min' | 'max' | null>(null);
|
|
87
|
+
|
|
88
|
+
const activeGlobalIndex = visibleTabs[tabValue]?.index ?? 0;
|
|
89
|
+
|
|
90
|
+
const handleChange = (key: string, value: any) => {
|
|
91
|
+
formStore.updateNodeProps(node.id, { [key]: value });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleDelete = () => {
|
|
95
|
+
formStore.removeNode(node.id);
|
|
96
|
+
formStore.closePropertiesModal(); // Also close modal on delete
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
|
100
|
+
setTabValue(newValue);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const triggerAutocomplete = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, fieldType: 'calculation' | 'defaultValue' | 'min' | 'max') => {
|
|
104
|
+
const val = event.target.value;
|
|
105
|
+
const selectionStart = event.target.selectionStart || 0;
|
|
106
|
+
setCursorPos(selectionStart);
|
|
107
|
+
setActiveFormulaField(fieldType);
|
|
108
|
+
|
|
109
|
+
// Detect {{ trigger
|
|
110
|
+
const textBeforeCursor = val.slice(0, selectionStart);
|
|
111
|
+
const match = textBeforeCursor.match(/\{\{([^}]*)$/);
|
|
112
|
+
|
|
113
|
+
if (match) {
|
|
114
|
+
setFilter(match[1]);
|
|
115
|
+
setAnchorEl(event.currentTarget);
|
|
116
|
+
} else {
|
|
117
|
+
setAnchorEl(null);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleSelectField = (name: string) => {
|
|
122
|
+
if (!node) return;
|
|
123
|
+
let currentContent = '';
|
|
124
|
+
if (activeFormulaField === 'calculation') {
|
|
125
|
+
currentContent = node.calculation?.formula || '';
|
|
126
|
+
} else if (activeFormulaField === 'defaultValue') {
|
|
127
|
+
currentContent = node.defaultValue || '';
|
|
128
|
+
} else if (activeFormulaField === 'min') {
|
|
129
|
+
currentContent = String(node.validation?.min || '');
|
|
130
|
+
} else if (activeFormulaField === 'max') {
|
|
131
|
+
currentContent = String(node.validation?.max || '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const before = currentContent.slice(0, cursorPos);
|
|
135
|
+
const after = currentContent.slice(cursorPos);
|
|
136
|
+
const lastBraces = before.lastIndexOf('{{');
|
|
137
|
+
|
|
138
|
+
if (lastBraces !== -1) {
|
|
139
|
+
const newBefore = before.slice(0, lastBraces) + `{{${name}}}`;
|
|
140
|
+
const newContent = newBefore + after;
|
|
141
|
+
|
|
142
|
+
if (activeFormulaField === 'defaultValue') {
|
|
143
|
+
formStore.updateNode(node.id, { defaultValue: newContent });
|
|
144
|
+
formStore.applyDefaultValues();
|
|
145
|
+
} else if (activeFormulaField === 'calculation') {
|
|
146
|
+
formStore.updateNodeCalculation(node.id, { formula: newContent });
|
|
147
|
+
} else if (activeFormulaField === 'min') {
|
|
148
|
+
formStore.updateNodeValidation(node.id, { min: newContent });
|
|
149
|
+
} else if (activeFormulaField === 'max') {
|
|
150
|
+
formStore.updateNodeValidation(node.id, { max: newContent });
|
|
151
|
+
}
|
|
152
|
+
setCursorPos(newBefore.length);
|
|
153
|
+
}
|
|
154
|
+
setAnchorEl(null);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const FormulaHints = ({ fieldType }: { fieldType: 'calculation' | 'defaultValue' | 'min' | 'max' }) => {
|
|
158
|
+
const insertText = (text: string) => {
|
|
159
|
+
let current = '';
|
|
160
|
+
if (fieldType === 'calculation') current = node.calculation?.formula || '';
|
|
161
|
+
else if (fieldType === 'defaultValue') current = node.defaultValue || '';
|
|
162
|
+
else if (fieldType === 'min') current = String(node.validation?.min || '');
|
|
163
|
+
else if (fieldType === 'max') current = String(node.validation?.max || '');
|
|
164
|
+
|
|
165
|
+
const newVal = current + text;
|
|
166
|
+
if (fieldType === 'calculation') formStore.updateNodeCalculation(node.id, { formula: newVal });
|
|
167
|
+
else if (fieldType === 'defaultValue') {
|
|
168
|
+
formStore.updateNode(node.id, { defaultValue: newVal });
|
|
169
|
+
formStore.applyDefaultValues();
|
|
170
|
+
}
|
|
171
|
+
else if (fieldType === 'min') formStore.updateNodeValidation(node.id, { min: newVal });
|
|
172
|
+
else if (fieldType === 'max') formStore.updateNodeValidation(node.id, { max: newVal });
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Box mt={1} display="flex" flexWrap="wrap" gap={0.5}>
|
|
177
|
+
<Typography variant="caption" width="100%" color="text.secondary">{t("properties.hints")}</Typography>
|
|
178
|
+
{['today', 'now', ...formStore.getAllFieldNames()]
|
|
179
|
+
.filter(name => name !== node.props?.name)
|
|
180
|
+
.map(name => (
|
|
181
|
+
<Button
|
|
182
|
+
key={name}
|
|
183
|
+
size="small"
|
|
184
|
+
variant="outlined"
|
|
185
|
+
sx={{ textTransform: 'none', px: 1, py: 0, fontSize: '0.7rem' }}
|
|
186
|
+
onClick={() => insertText(`{{${name}}}`)}
|
|
187
|
+
>
|
|
188
|
+
{name}
|
|
189
|
+
</Button>
|
|
190
|
+
))}
|
|
191
|
+
|
|
192
|
+
<Box width="100%" mt={1}>
|
|
193
|
+
<Typography variant="caption" color="text.secondary">{t("properties.functions") || "Functions"}</Typography>
|
|
194
|
+
</Box>
|
|
195
|
+
{['IF()', 'AND()', 'OR()', 'NOT()', 'ABS()', 'ROUND()'].map(func => (
|
|
196
|
+
<Button
|
|
197
|
+
key={func}
|
|
198
|
+
size="small"
|
|
199
|
+
variant="outlined"
|
|
200
|
+
color="secondary"
|
|
201
|
+
sx={{ textTransform: 'none', px: 1, py: 0, fontSize: '0.7rem' }}
|
|
202
|
+
onClick={() => insertText(func)}
|
|
203
|
+
>
|
|
204
|
+
{func}
|
|
205
|
+
</Button>
|
|
206
|
+
))}
|
|
207
|
+
</Box>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<Box display="flex" flexDirection="column" height="100%">
|
|
213
|
+
{/* Unified Autocomplete Popper */}
|
|
214
|
+
<Popper open={Boolean(anchorEl)} anchorEl={anchorEl} placement="bottom-start" style={{ zIndex: 1500 }}>
|
|
215
|
+
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
|
|
216
|
+
<Paper sx={{ mt: 1, maxHeight: 200, overflow: 'auto', minWidth: 200 }}>
|
|
217
|
+
<List dense>
|
|
218
|
+
{['today', 'now', ...formStore.getAllFieldNames()]
|
|
219
|
+
.filter(name => name !== node.props?.name && name.toLowerCase().includes(filter.toLowerCase()))
|
|
220
|
+
.map(name => (
|
|
221
|
+
<ListItemButton key={name} onClick={() => handleSelectField(name)}>
|
|
222
|
+
<ListItemText
|
|
223
|
+
primary={name}
|
|
224
|
+
secondary={name === 'today' || name === 'now' ? 'System variable' : `{{${name}}}`}
|
|
225
|
+
primaryTypographyProps={{ variant: 'body2' }}
|
|
226
|
+
/>
|
|
227
|
+
</ListItemButton>
|
|
228
|
+
))}
|
|
229
|
+
</List>
|
|
230
|
+
</Paper>
|
|
231
|
+
</ClickAwayListener>
|
|
232
|
+
</Popper>
|
|
233
|
+
|
|
234
|
+
{/* Header always visible */}
|
|
235
|
+
<Box p={2} borderBottom={1} borderColor="divider" display="flex" alignItems="center" justifyContent="space-between" bgcolor="background.default">
|
|
236
|
+
<Box>
|
|
237
|
+
<Typography variant="subtitle1" fontWeight="bold">{t('properties.title')}</Typography>
|
|
238
|
+
<Typography variant="caption" color="text.secondary">
|
|
239
|
+
{node.type.toUpperCase()} | ID: {node.id}
|
|
240
|
+
</Typography>
|
|
241
|
+
</Box>
|
|
242
|
+
<Button
|
|
243
|
+
size="small"
|
|
244
|
+
color="error"
|
|
245
|
+
startIcon={<DeleteIcon />}
|
|
246
|
+
onClick={handleDelete}
|
|
247
|
+
>
|
|
248
|
+
{t("properties.delete")}
|
|
249
|
+
</Button>
|
|
250
|
+
</Box>
|
|
251
|
+
|
|
252
|
+
<Box borderBottom={1} borderColor="divider" mb={0.5}>
|
|
253
|
+
<Tabs value={tabValue} onChange={handleTabChange} aria-label="properties tabs" variant="fullWidth">
|
|
254
|
+
{visibleTabs.map((tab, i) => (
|
|
255
|
+
<Tab key={tab.index} label={tab.label} {...a11yProps(i)} />
|
|
256
|
+
))}
|
|
257
|
+
</Tabs>
|
|
258
|
+
</Box>
|
|
259
|
+
|
|
260
|
+
{/* View Tab */}
|
|
261
|
+
<CustomTabPanel value={activeGlobalIndex} index={0}>
|
|
262
|
+
{/* Common Props */}
|
|
263
|
+
{node.type !== 'root' && (
|
|
264
|
+
<Box mb={0.5}>
|
|
265
|
+
{formStore.availableApiColumns.length > 0 ? (
|
|
266
|
+
<FormControl fullWidth size="small">
|
|
267
|
+
<InputLabel>{t('properties.fieldId')}</InputLabel>
|
|
268
|
+
<Select
|
|
269
|
+
label={t('properties.fieldId')}
|
|
270
|
+
value={node.props?.name || ''}
|
|
271
|
+
onChange={(e) => {
|
|
272
|
+
const colId = e.target.value;
|
|
273
|
+
const col = formStore.availableApiColumns.find(c => c.id.toString() === colId);
|
|
274
|
+
if (col) {
|
|
275
|
+
formStore.updateNodeProps(node.id, {
|
|
276
|
+
name: col.id.toString(),
|
|
277
|
+
label: col.colTitle,
|
|
278
|
+
apiColumnName: col.colName
|
|
279
|
+
});
|
|
280
|
+
// Enforce numeric validation based on API type
|
|
281
|
+
if (node.type === 'number') {
|
|
282
|
+
formStore.updateNodeValidation(node.id, {
|
|
283
|
+
integer: col.colType === 'INT'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
handleChange('name', colId);
|
|
288
|
+
}
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
<MenuItem value=""><em>None</em></MenuItem>
|
|
292
|
+
{formStore.availableApiColumns
|
|
293
|
+
.filter(col => {
|
|
294
|
+
// 1. Type Compatibility Check
|
|
295
|
+
const compatibleTypes: Record<string, string[]> = {
|
|
296
|
+
'text': ['STRING'],
|
|
297
|
+
'number': ['INT', 'FLOAT'],
|
|
298
|
+
'checkbox': ['BOOLEAN'],
|
|
299
|
+
'date': ['TIMESTAMP', 'TIMESTAMP_2'],
|
|
300
|
+
'file': ['FILE', 'FILE_SET'],
|
|
301
|
+
'select': ['REF']
|
|
302
|
+
};
|
|
303
|
+
const allowed = compatibleTypes[node.type] || [];
|
|
304
|
+
if (!allowed.includes(col.colType)) return false;
|
|
305
|
+
|
|
306
|
+
// 2. Uniqueness Check: Check if this column ID is used by ANY OTHER node
|
|
307
|
+
const isUsedByOther = formStore.getAllNodes().some((n: SchemaNode) =>
|
|
308
|
+
n.id !== node.id && n.props?.name === col.id.toString()
|
|
309
|
+
);
|
|
310
|
+
return !isUsedByOther;
|
|
311
|
+
})
|
|
312
|
+
.map(col => (
|
|
313
|
+
<MenuItem key={col.id} value={col.id.toString()}>
|
|
314
|
+
{col.colTitle} ({col.colName})
|
|
315
|
+
</MenuItem>
|
|
316
|
+
))
|
|
317
|
+
}
|
|
318
|
+
</Select>
|
|
319
|
+
<Typography variant="caption" color="text.secondary">
|
|
320
|
+
{t('properties.fieldName')}
|
|
321
|
+
</Typography>
|
|
322
|
+
</FormControl>
|
|
323
|
+
) : (
|
|
324
|
+
<TextField
|
|
325
|
+
label={t('properties.fieldId')}
|
|
326
|
+
size="small"
|
|
327
|
+
fullWidth
|
|
328
|
+
value={node.props?.name || ''}
|
|
329
|
+
onChange={(e) => handleChange('name', e.target.value)}
|
|
330
|
+
helperText={t('properties.fieldName')}
|
|
331
|
+
/>
|
|
332
|
+
)}
|
|
333
|
+
</Box>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
<Box mb={0.5}>
|
|
337
|
+
<TextField
|
|
338
|
+
label={node.type === 'root' ? t("properties.formTitle") : (t('properties.label') || "Label")}
|
|
339
|
+
size="small"
|
|
340
|
+
fullWidth
|
|
341
|
+
multiline={node.type === 'label'}
|
|
342
|
+
rows={node.type === 'label' ? 3 : 1}
|
|
343
|
+
value={node.props?.label || ''}
|
|
344
|
+
onChange={(e) => handleChange('label', e.target.value)}
|
|
345
|
+
/>
|
|
346
|
+
</Box>
|
|
347
|
+
|
|
348
|
+
<Box mb={2}>
|
|
349
|
+
<Typography variant="caption" color="text.secondary" display="block" mb={0.5}>
|
|
350
|
+
{t("properties.defaultValue")}
|
|
351
|
+
</Typography>
|
|
352
|
+
{node.type === 'select' ? (
|
|
353
|
+
<FormControl fullWidth size="small">
|
|
354
|
+
<Select
|
|
355
|
+
value={node.defaultValue ?? ''}
|
|
356
|
+
onChange={(e) => {
|
|
357
|
+
formStore.updateNode(node.id, { defaultValue: e.target.value });
|
|
358
|
+
formStore.applyDefaultValues();
|
|
359
|
+
}}
|
|
360
|
+
displayEmpty
|
|
361
|
+
>
|
|
362
|
+
<MenuItem value=""><em>{t('properties.none')}</em></MenuItem>
|
|
363
|
+
{(node.props?.options || []).map((opt: any, idx: number) => (
|
|
364
|
+
<MenuItem key={idx} value={opt.value}>
|
|
365
|
+
{opt.label || <em>{t('properties.empty')}</em>}
|
|
366
|
+
</MenuItem>
|
|
367
|
+
))}
|
|
368
|
+
</Select>
|
|
369
|
+
</FormControl>
|
|
370
|
+
) : (
|
|
371
|
+
<>
|
|
372
|
+
<TextField
|
|
373
|
+
size="small"
|
|
374
|
+
fullWidth
|
|
375
|
+
value={node.defaultValue ?? ''}
|
|
376
|
+
onChange={(e) => {
|
|
377
|
+
const val = e.target.value;
|
|
378
|
+
formStore.updateNode(node.id, { defaultValue: val });
|
|
379
|
+
formStore.applyDefaultValues();
|
|
380
|
+
triggerAutocomplete(e, 'defaultValue');
|
|
381
|
+
}}
|
|
382
|
+
onKeyUp={(e) => {
|
|
383
|
+
const target = e.target as HTMLInputElement;
|
|
384
|
+
setCursorPos(target.selectionStart || 0);
|
|
385
|
+
}}
|
|
386
|
+
/>
|
|
387
|
+
<FormulaHints fieldType="defaultValue" />
|
|
388
|
+
</>
|
|
389
|
+
)}
|
|
390
|
+
</Box>
|
|
391
|
+
|
|
392
|
+
{node.type !== 'root' && (
|
|
393
|
+
<FormControl fullWidth size="small">
|
|
394
|
+
<FormLabel>{t("properties.width")}</FormLabel>
|
|
395
|
+
<Select
|
|
396
|
+
value={node.props?.width ?? node.props?.cols ?? 12}
|
|
397
|
+
onChange={(e) => handleChange('width', Number(e.target.value))}
|
|
398
|
+
>
|
|
399
|
+
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(col => (
|
|
400
|
+
<MenuItem key={col} value={col}>{col} / 12</MenuItem>
|
|
401
|
+
))}
|
|
402
|
+
</Select>
|
|
403
|
+
</FormControl>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{/* Type Specific Props */}
|
|
407
|
+
{(['text', 'number', 'select'].includes(node.type)) && (
|
|
408
|
+
<TextField
|
|
409
|
+
label={t('properties.placeholder')}
|
|
410
|
+
size="small"
|
|
411
|
+
fullWidth
|
|
412
|
+
value={node.props?.placeholder || ''}
|
|
413
|
+
onChange={(e) => handleChange('placeholder', e.target.value)}
|
|
414
|
+
/>
|
|
415
|
+
)}
|
|
416
|
+
|
|
417
|
+
{node.type === 'select' && (
|
|
418
|
+
<Box display="flex" flexDirection="column" gap={2} mt={2}>
|
|
419
|
+
<TextField
|
|
420
|
+
label={t('properties.selectOptions')}
|
|
421
|
+
multiline
|
|
422
|
+
rows={6}
|
|
423
|
+
size="small"
|
|
424
|
+
fullWidth
|
|
425
|
+
disabled={!!node.props?.dictionaryInfo}
|
|
426
|
+
value={(node.props?.options || []).map((opt: any) => opt.label).join('\n')}
|
|
427
|
+
onChange={(e) => {
|
|
428
|
+
const lines = e.target.value.split('\n');
|
|
429
|
+
// Only filter out the LAST empty line if it was just created by pressing Enter
|
|
430
|
+
// but allow internal empty lines.
|
|
431
|
+
const newOptions = lines.map(line => ({ label: line, value: line }));
|
|
432
|
+
handleChange('options', newOptions);
|
|
433
|
+
}}
|
|
434
|
+
helperText={node.props?.dictionaryInfo
|
|
435
|
+
? "Options are managed by API dictionary"
|
|
436
|
+
: t('properties.selectOptionsHelper')}
|
|
437
|
+
/>
|
|
438
|
+
<Box display="flex" alignItems="center" mt={1}>
|
|
439
|
+
<input
|
|
440
|
+
type="checkbox"
|
|
441
|
+
checked={node.props?.enableAutocomplete || false}
|
|
442
|
+
onChange={(e) => handleChange('enableAutocomplete', e.target.checked)}
|
|
443
|
+
style={{ marginRight: 8 }}
|
|
444
|
+
/>
|
|
445
|
+
<Typography variant="body2">{t('properties.enableAutocomplete')}</Typography>
|
|
446
|
+
</Box>
|
|
447
|
+
</Box>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
{node.type === 'repeater' && (
|
|
451
|
+
<TextField
|
|
452
|
+
label={t('properties.addLabel')}
|
|
453
|
+
size="small"
|
|
454
|
+
fullWidth
|
|
455
|
+
value={node.props?.addLabel || ''}
|
|
456
|
+
onChange={(e) => handleChange('addLabel', e.target.value)}
|
|
457
|
+
/>
|
|
458
|
+
)}
|
|
459
|
+
|
|
460
|
+
{node.type === 'tabs' && (
|
|
461
|
+
<Button
|
|
462
|
+
variant="outlined"
|
|
463
|
+
fullWidth
|
|
464
|
+
onClick={() => {
|
|
465
|
+
const newTabId = `tab-${Math.random().toString(36).substr(2, 9)}`;
|
|
466
|
+
formStore.addNode(node.id, {
|
|
467
|
+
id: newTabId,
|
|
468
|
+
type: 'tab',
|
|
469
|
+
props: {},
|
|
470
|
+
children: []
|
|
471
|
+
});
|
|
472
|
+
}}
|
|
473
|
+
>
|
|
474
|
+
{t("properties.addTab")}
|
|
475
|
+
</Button>
|
|
476
|
+
)}
|
|
477
|
+
|
|
478
|
+
{/* Typography / Label Props */}
|
|
479
|
+
{(node.type === 'label' || node.type === 'root') && (
|
|
480
|
+
node.type === 'label' && (
|
|
481
|
+
<Box display="flex" flexDirection="column" gap={2}>
|
|
482
|
+
<FormControl fullWidth size="small">
|
|
483
|
+
<FormLabel>{t("properties.typography")}</FormLabel>
|
|
484
|
+
<Select
|
|
485
|
+
value={node.props?.variant || 'body1'}
|
|
486
|
+
onChange={(e) => handleChange('variant', e.target.value)}
|
|
487
|
+
>
|
|
488
|
+
<MenuItem value="h1">Heading 1</MenuItem>
|
|
489
|
+
<MenuItem value="h2">Heading 2</MenuItem>
|
|
490
|
+
<MenuItem value="h3">Heading 3</MenuItem>
|
|
491
|
+
<MenuItem value="h4">Heading 4</MenuItem>
|
|
492
|
+
<MenuItem value="h5">Heading 5</MenuItem>
|
|
493
|
+
<MenuItem value="h6">Heading 6</MenuItem>
|
|
494
|
+
<MenuItem value="body1">Body 1</MenuItem>
|
|
495
|
+
<MenuItem value="body2">Body 2</MenuItem>
|
|
496
|
+
<MenuItem value="caption">Caption</MenuItem>
|
|
497
|
+
</Select>
|
|
498
|
+
</FormControl>
|
|
499
|
+
<FormControl fullWidth size="small">
|
|
500
|
+
<FormLabel>{t("properties.alignment")}</FormLabel>
|
|
501
|
+
<Select
|
|
502
|
+
value={node.props?.align || 'left'}
|
|
503
|
+
onChange={(e) => handleChange('align', e.target.value)}
|
|
504
|
+
>
|
|
505
|
+
<MenuItem value="left">{t('properties.alignLeft')}</MenuItem>
|
|
506
|
+
<MenuItem value="center">{t('properties.alignCenter')}</MenuItem>
|
|
507
|
+
<MenuItem value="right">{t('properties.alignRight')}</MenuItem>
|
|
508
|
+
<MenuItem value="justify">{t('properties.alignJustify')}</MenuItem>
|
|
509
|
+
</Select>
|
|
510
|
+
</FormControl>
|
|
511
|
+
</Box>
|
|
512
|
+
)
|
|
513
|
+
)}
|
|
514
|
+
|
|
515
|
+
{node.type === 'divider' && (
|
|
516
|
+
<Box display="flex" flexDirection="column" gap={2}>
|
|
517
|
+
<TextField
|
|
518
|
+
label={t('properties.dividerText')}
|
|
519
|
+
size="small"
|
|
520
|
+
fullWidth
|
|
521
|
+
value={node.props?.text || ''}
|
|
522
|
+
onChange={(e) => handleChange('text', e.target.value)}
|
|
523
|
+
/>
|
|
524
|
+
<FormControl fullWidth size="small">
|
|
525
|
+
<FormLabel>{t('properties.alignment')}</FormLabel>
|
|
526
|
+
<Select
|
|
527
|
+
value={node.props?.align || 'center'}
|
|
528
|
+
onChange={(e) => handleChange('align', e.target.value)}
|
|
529
|
+
>
|
|
530
|
+
<MenuItem value="left">{t('properties.alignLeft')}</MenuItem>
|
|
531
|
+
<MenuItem value="center">{t('properties.alignCenter')}</MenuItem>
|
|
532
|
+
<MenuItem value="right">{t('properties.alignRight')}</MenuItem>
|
|
533
|
+
</Select>
|
|
534
|
+
</FormControl>
|
|
535
|
+
</Box>
|
|
536
|
+
)}
|
|
537
|
+
|
|
538
|
+
{node.type === 'table' && (
|
|
539
|
+
<Box display="flex" gap={1}>
|
|
540
|
+
<TextField
|
|
541
|
+
label={t('properties.rows') || "Rows"}
|
|
542
|
+
type="number"
|
|
543
|
+
size="small"
|
|
544
|
+
fullWidth
|
|
545
|
+
value={node.props?.rows || 2}
|
|
546
|
+
onChange={(e) => {
|
|
547
|
+
const newRows = parseInt(e.target.value, 10) || 1;
|
|
548
|
+
formStore.resizeTable(node.id, newRows, node.props?.cols || 2);
|
|
549
|
+
}}
|
|
550
|
+
/>
|
|
551
|
+
<TextField
|
|
552
|
+
label={t('properties.cols') || "Cols"}
|
|
553
|
+
type="number"
|
|
554
|
+
size="small"
|
|
555
|
+
fullWidth
|
|
556
|
+
value={node.props?.cols || 2}
|
|
557
|
+
onChange={(e) => {
|
|
558
|
+
const newCols = parseInt(e.target.value, 10) || 1;
|
|
559
|
+
formStore.resizeTable(node.id, node.props?.rows || 2, newCols);
|
|
560
|
+
}}
|
|
561
|
+
/>
|
|
562
|
+
</Box>
|
|
563
|
+
)}
|
|
564
|
+
|
|
565
|
+
{['paper', 'tabs', 'tab'].includes(node.type) && (
|
|
566
|
+
<Box display="flex" flexDirection="column" gap={2}>
|
|
567
|
+
<FormControl fullWidth size="small">
|
|
568
|
+
<FormLabel>{t('properties.styleVariant')}</FormLabel>
|
|
569
|
+
<Select
|
|
570
|
+
value={node.props?.variant || 'elevation'}
|
|
571
|
+
onChange={(e) => handleChange('variant', e.target.value)}
|
|
572
|
+
>
|
|
573
|
+
<MenuItem value="elevation">{t('properties.styleElevated')}</MenuItem>
|
|
574
|
+
<MenuItem value="outlined">{t('properties.styleOutlined')}</MenuItem>
|
|
575
|
+
<MenuItem value="transparent">{t('properties.styleTransparent')}</MenuItem>
|
|
576
|
+
</Select>
|
|
577
|
+
</FormControl>
|
|
578
|
+
{(!node.props?.variant || node.props?.variant === 'elevation') && (
|
|
579
|
+
<TextField
|
|
580
|
+
label={t('properties.elevationDepth')}
|
|
581
|
+
type="number"
|
|
582
|
+
size="small"
|
|
583
|
+
fullWidth
|
|
584
|
+
value={node.props?.elevation ?? 1}
|
|
585
|
+
onChange={(e) => handleChange('elevation', Number(e.target.value))}
|
|
586
|
+
/>
|
|
587
|
+
)}
|
|
588
|
+
<TextField
|
|
589
|
+
label={t('properties.padding') || "Padding"}
|
|
590
|
+
type="number"
|
|
591
|
+
size="small"
|
|
592
|
+
fullWidth
|
|
593
|
+
value={node.props?.padding ?? 2}
|
|
594
|
+
onChange={(e) => handleChange('padding', Number(e.target.value))}
|
|
595
|
+
/>
|
|
596
|
+
</Box>
|
|
597
|
+
)}
|
|
598
|
+
</CustomTabPanel>
|
|
599
|
+
|
|
600
|
+
{/* Validation Tab */}
|
|
601
|
+
<CustomTabPanel value={activeGlobalIndex} index={1}>
|
|
602
|
+
<Box display="flex" alignItems="center" mb={1}>
|
|
603
|
+
<input
|
|
604
|
+
type="checkbox"
|
|
605
|
+
checked={node.validation?.required || false}
|
|
606
|
+
onChange={(e) => formStore.updateNodeValidation(node.id, { required: e.target.checked })}
|
|
607
|
+
style={{ marginRight: 8 }}
|
|
608
|
+
/>
|
|
609
|
+
<Typography variant="body2">{t('properties.required')}</Typography>
|
|
610
|
+
</Box>
|
|
611
|
+
|
|
612
|
+
{node.type === 'text' && (
|
|
613
|
+
<Box display="flex" alignItems="center" mb={1}>
|
|
614
|
+
<input
|
|
615
|
+
type="checkbox"
|
|
616
|
+
checked={node.validation?.email || false}
|
|
617
|
+
onChange={(e) => formStore.updateNodeValidation(node.id, { email: e.target.checked })}
|
|
618
|
+
style={{ marginRight: 8 }}
|
|
619
|
+
/>
|
|
620
|
+
<Typography variant="body2">{t("properties.emailFormat")}</Typography>
|
|
621
|
+
</Box>
|
|
622
|
+
)}
|
|
623
|
+
|
|
624
|
+
{node.type === 'number' && (
|
|
625
|
+
<Box display="flex" alignItems="center" mb={1}>
|
|
626
|
+
<input
|
|
627
|
+
type="checkbox"
|
|
628
|
+
checked={node.validation?.integer || false}
|
|
629
|
+
disabled={!!node.props?.name && formStore.availableApiColumns.some(c => c.id.toString() === node.props?.name)}
|
|
630
|
+
onChange={(e) => formStore.updateNodeValidation(node.id, { integer: e.target.checked })}
|
|
631
|
+
style={{ marginRight: 8 }}
|
|
632
|
+
/>
|
|
633
|
+
<Typography variant="body2">
|
|
634
|
+
{t("properties.integer")}
|
|
635
|
+
{node.props?.name && formStore.availableApiColumns.some(c => c.id.toString() === node.props?.name) && (
|
|
636
|
+
<Typography variant="caption" color="text.secondary" ml={1}>
|
|
637
|
+
(Enforced by API)
|
|
638
|
+
</Typography>
|
|
639
|
+
)}
|
|
640
|
+
</Typography>
|
|
641
|
+
</Box>
|
|
642
|
+
)}
|
|
643
|
+
|
|
644
|
+
{node.type === 'number' && (
|
|
645
|
+
<Box display="flex" flexDirection="column" gap={1}>
|
|
646
|
+
<Box mb={1}>
|
|
647
|
+
<TextField
|
|
648
|
+
label={t("properties.minValue")}
|
|
649
|
+
size="small"
|
|
650
|
+
fullWidth
|
|
651
|
+
value={node.validation?.min ?? ''}
|
|
652
|
+
onChange={(e) => {
|
|
653
|
+
const val = e.target.value;
|
|
654
|
+
formStore.updateNodeValidation(node.id, { min: val });
|
|
655
|
+
triggerAutocomplete(e, 'min');
|
|
656
|
+
}}
|
|
657
|
+
onKeyUp={(e) => {
|
|
658
|
+
const target = e.target as HTMLInputElement;
|
|
659
|
+
setCursorPos(target.selectionStart || 0);
|
|
660
|
+
}}
|
|
661
|
+
placeholder="e.g. 10 or {{otherField}} + 5"
|
|
662
|
+
/>
|
|
663
|
+
<FormulaHints fieldType="min" />
|
|
664
|
+
</Box>
|
|
665
|
+
<Box mb={1}>
|
|
666
|
+
<TextField
|
|
667
|
+
label={t("properties.maxValue")}
|
|
668
|
+
size="small"
|
|
669
|
+
fullWidth
|
|
670
|
+
value={node.validation?.max ?? ''}
|
|
671
|
+
onChange={(e) => {
|
|
672
|
+
const val = e.target.value;
|
|
673
|
+
formStore.updateNodeValidation(node.id, { max: val });
|
|
674
|
+
triggerAutocomplete(e, 'max');
|
|
675
|
+
}}
|
|
676
|
+
onKeyUp={(e) => {
|
|
677
|
+
const target = e.target as HTMLInputElement;
|
|
678
|
+
setCursorPos(target.selectionStart || 0);
|
|
679
|
+
}}
|
|
680
|
+
placeholder="e.g. 100 or {{otherField}} * 2"
|
|
681
|
+
/>
|
|
682
|
+
<FormulaHints fieldType="max" />
|
|
683
|
+
</Box>
|
|
684
|
+
</Box>
|
|
685
|
+
)}
|
|
686
|
+
|
|
687
|
+
{node.type === 'text' && (
|
|
688
|
+
<Box display="flex" flexDirection="column" gap={1}>
|
|
689
|
+
<TextField
|
|
690
|
+
label={t("properties.minLength")}
|
|
691
|
+
type="number"
|
|
692
|
+
size="small"
|
|
693
|
+
fullWidth
|
|
694
|
+
value={node.validation?.minLength ?? ''}
|
|
695
|
+
onChange={(e) => {
|
|
696
|
+
const val = e.target.value === '' ? undefined : Number(e.target.value);
|
|
697
|
+
formStore.updateNodeValidation(node.id, { minLength: val });
|
|
698
|
+
}}
|
|
699
|
+
/>
|
|
700
|
+
<TextField
|
|
701
|
+
label={t("properties.maxLength")}
|
|
702
|
+
type="number"
|
|
703
|
+
size="small"
|
|
704
|
+
fullWidth
|
|
705
|
+
value={node.validation?.maxLength ?? ''}
|
|
706
|
+
onChange={(e) => {
|
|
707
|
+
const val = e.target.value === '' ? undefined : Number(e.target.value);
|
|
708
|
+
formStore.updateNodeValidation(node.id, { maxLength: val });
|
|
709
|
+
}}
|
|
710
|
+
/>
|
|
711
|
+
<TextField
|
|
712
|
+
label={t("properties.regex")}
|
|
713
|
+
size="small"
|
|
714
|
+
fullWidth
|
|
715
|
+
value={node.validation?.pattern || ''}
|
|
716
|
+
onChange={(e) => formStore.updateNodeValidation(node.id, { pattern: e.target.value })}
|
|
717
|
+
/>
|
|
718
|
+
</Box>
|
|
719
|
+
)}
|
|
720
|
+
</CustomTabPanel>
|
|
721
|
+
|
|
722
|
+
{/* Logic Tab */}
|
|
723
|
+
<CustomTabPanel value={activeGlobalIndex} index={2}>
|
|
724
|
+
<Box display="flex" flexDirection="column" gap={1.5}>
|
|
725
|
+
<FormControl fullWidth size="small">
|
|
726
|
+
<FormLabel>{t('properties.fieldNameCheck')}</FormLabel>
|
|
727
|
+
<Select
|
|
728
|
+
value={node.condition?.field || ''}
|
|
729
|
+
onChange={(e) => {
|
|
730
|
+
const field = e.target.value;
|
|
731
|
+
if (!field) {
|
|
732
|
+
formStore.updateNodeCondition(node.id, undefined);
|
|
733
|
+
} else {
|
|
734
|
+
const newCond = { ...(node.condition || { op: 'eq', value: '' }), field };
|
|
735
|
+
formStore.updateNodeCondition(node.id, newCond as any);
|
|
736
|
+
}
|
|
737
|
+
}}
|
|
738
|
+
displayEmpty
|
|
739
|
+
>
|
|
740
|
+
<MenuItem value=""><em>None</em></MenuItem>
|
|
741
|
+
{formStore.getAllFieldNames().filter(name => name !== node.props?.name).map(name => (
|
|
742
|
+
<MenuItem key={name} value={name}>{name}</MenuItem>
|
|
743
|
+
))}
|
|
744
|
+
</Select>
|
|
745
|
+
<Typography variant="caption" color="text.secondary">
|
|
746
|
+
{t('properties.fieldVisibilityHelper')}
|
|
747
|
+
</Typography>
|
|
748
|
+
</FormControl>
|
|
749
|
+
|
|
750
|
+
{node.condition && (
|
|
751
|
+
<>
|
|
752
|
+
<FormControl fullWidth size="small">
|
|
753
|
+
<FormLabel>{t('properties.operator')}</FormLabel>
|
|
754
|
+
<Select
|
|
755
|
+
value={node.condition.op}
|
|
756
|
+
onChange={(e) => formStore.updateNodeCondition(node.id, { ...node.condition!, op: e.target.value as any })}
|
|
757
|
+
>
|
|
758
|
+
<MenuItem value="eq">{t('properties.opEq')}</MenuItem>
|
|
759
|
+
<MenuItem value="neq">{t('properties.opNeq')}</MenuItem>
|
|
760
|
+
<MenuItem value="gt">{t('properties.opGt')}</MenuItem>
|
|
761
|
+
<MenuItem value="lt">{t('properties.opLt')}</MenuItem>
|
|
762
|
+
<MenuItem value="contains">{t('properties.opContains')}</MenuItem>
|
|
763
|
+
</Select>
|
|
764
|
+
</FormControl>
|
|
765
|
+
|
|
766
|
+
<TextField
|
|
767
|
+
label={t("properties.value")}
|
|
768
|
+
size="small"
|
|
769
|
+
fullWidth
|
|
770
|
+
value={node.condition.value ?? ''}
|
|
771
|
+
onChange={(e) => {
|
|
772
|
+
let val: any = e.target.value;
|
|
773
|
+
// Try to convert to boolean or number if possible for easier matching
|
|
774
|
+
if (val === 'true') val = true;
|
|
775
|
+
else if (val === 'false') val = false;
|
|
776
|
+
else if (!isNaN(Number(val)) && val !== '') val = Number(val);
|
|
777
|
+
|
|
778
|
+
formStore.updateNodeCondition(node.id, { ...node.condition!, value: val });
|
|
779
|
+
}}
|
|
780
|
+
/>
|
|
781
|
+
|
|
782
|
+
<Button
|
|
783
|
+
variant="outlined"
|
|
784
|
+
size="small"
|
|
785
|
+
color="secondary"
|
|
786
|
+
onClick={() => formStore.updateNodeCondition(node.id, undefined)}
|
|
787
|
+
>
|
|
788
|
+
{t("properties.clearCondition")}
|
|
789
|
+
</Button>
|
|
790
|
+
</>
|
|
791
|
+
)}
|
|
792
|
+
</Box>
|
|
793
|
+
</CustomTabPanel>
|
|
794
|
+
|
|
795
|
+
{/* Calculation Tab */}
|
|
796
|
+
<CustomTabPanel value={activeGlobalIndex} index={3}>
|
|
797
|
+
<TextField
|
|
798
|
+
label={t("properties.formula")}
|
|
799
|
+
size="small"
|
|
800
|
+
fullWidth
|
|
801
|
+
placeholder="e.g. {{price}} * {{qty}}"
|
|
802
|
+
value={node.calculation?.formula || ''}
|
|
803
|
+
onChange={(e) => {
|
|
804
|
+
const val = e.target.value;
|
|
805
|
+
formStore.updateNodeCalculation(node.id, val ? { formula: val } : undefined);
|
|
806
|
+
triggerAutocomplete(e, 'calculation');
|
|
807
|
+
}}
|
|
808
|
+
onKeyUp={(e) => {
|
|
809
|
+
const target = e.target as HTMLInputElement;
|
|
810
|
+
setCursorPos(target.selectionStart || 0);
|
|
811
|
+
}}
|
|
812
|
+
helperText={t("properties.formulaHelper")}
|
|
813
|
+
/>
|
|
814
|
+
<FormulaHints fieldType="calculation" />
|
|
815
|
+
</CustomTabPanel>
|
|
816
|
+
|
|
817
|
+
{/* API Mapping Tab */}
|
|
818
|
+
<CustomTabPanel value={activeGlobalIndex} index={4}>
|
|
819
|
+
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
820
|
+
Map this field to an external API field for submission.
|
|
821
|
+
</Typography>
|
|
822
|
+
|
|
823
|
+
<FormControl fullWidth size="small">
|
|
824
|
+
<InputLabel>Integration</InputLabel>
|
|
825
|
+
<Select
|
|
826
|
+
label="Integration"
|
|
827
|
+
value={node.props?.apiIntegrationId || ''}
|
|
828
|
+
onChange={(e) => {
|
|
829
|
+
handleChange('apiIntegrationId', e.target.value);
|
|
830
|
+
formStore.updateNode(node.id, { apiMapping: undefined });
|
|
831
|
+
}}
|
|
832
|
+
>
|
|
833
|
+
<MenuItem value=""><em>None</em></MenuItem>
|
|
834
|
+
{(formStore.rootNode.props?.integrations || []).map((i: any) => (
|
|
835
|
+
<MenuItem key={i.id} value={i.id}>{i.name}</MenuItem>
|
|
836
|
+
))}
|
|
837
|
+
</Select>
|
|
838
|
+
</FormControl>
|
|
839
|
+
|
|
840
|
+
{node.props?.apiIntegrationId && (
|
|
841
|
+
<FormControl fullWidth size="small" sx={{ mt: 2 }}>
|
|
842
|
+
<InputLabel>API Field</InputLabel>
|
|
843
|
+
<Select
|
|
844
|
+
label="API Field"
|
|
845
|
+
value={node.apiMapping || ''}
|
|
846
|
+
onChange={(e) => formStore.updateNode(node.id, { apiMapping: e.target.value })}
|
|
847
|
+
>
|
|
848
|
+
<MenuItem value=""><em>None</em></MenuItem>
|
|
849
|
+
{(formStore.rootNode.props?.integrations?.find((i: any) => i.id === node.props?.apiIntegrationId)?.availableFields || []).map((f: any) => (
|
|
850
|
+
<MenuItem key={f.id} value={f.id}>{f.label} ({f.type})</MenuItem>
|
|
851
|
+
))}
|
|
852
|
+
</Select>
|
|
853
|
+
</FormControl>
|
|
854
|
+
)}
|
|
855
|
+
</CustomTabPanel>
|
|
856
|
+
</Box>
|
|
857
|
+
);
|
|
858
|
+
});
|