@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.
Files changed (50) hide show
  1. package/README.md +73 -0
  2. package/eslint.config.js +23 -0
  3. package/index.html +13 -0
  4. package/package.json +60 -0
  5. package/public/vite.svg +1 -0
  6. package/src/App.css +42 -0
  7. package/src/App.tsx +83 -0
  8. package/src/assets/react.svg +1 -0
  9. package/src/components/FieldRegistry.ts +34 -0
  10. package/src/components/FormContainer.tsx +25 -0
  11. package/src/components/FormRenderer.tsx +121 -0
  12. package/src/components/builder/DraggableTool.tsx +66 -0
  13. package/src/components/builder/DroppableCanvas.tsx +51 -0
  14. package/src/components/builder/EditorWrapper.tsx +87 -0
  15. package/src/components/builder/FormBuilder.tsx +313 -0
  16. package/src/components/builder/FormChildrenRenderer.tsx +68 -0
  17. package/src/components/builder/IntegrationSettings.tsx +110 -0
  18. package/src/components/builder/PropertiesModal.tsx +75 -0
  19. package/src/components/builder/PropertiesPanel.tsx +858 -0
  20. package/src/components/builder/SortableNode.tsx +53 -0
  21. package/src/components/builder/Toolbox.tsx +123 -0
  22. package/src/components/fields/CheckboxField.tsx +41 -0
  23. package/src/components/fields/DateField.tsx +56 -0
  24. package/src/components/fields/FileUploadField.tsx +45 -0
  25. package/src/components/fields/LabelField.tsx +20 -0
  26. package/src/components/fields/NumberField.tsx +39 -0
  27. package/src/components/fields/RichTextField.tsx +39 -0
  28. package/src/components/fields/SelectField.tsx +64 -0
  29. package/src/components/fields/TextField.tsx +44 -0
  30. package/src/components/layout/FormCol.tsx +30 -0
  31. package/src/components/layout/FormDivider.tsx +19 -0
  32. package/src/components/layout/FormPaper.tsx +85 -0
  33. package/src/components/layout/FormRepeater.tsx +130 -0
  34. package/src/components/layout/FormRow.tsx +61 -0
  35. package/src/components/layout/FormTab.tsx +33 -0
  36. package/src/components/layout/FormTable.tsx +47 -0
  37. package/src/components/layout/FormTableCell.tsx +47 -0
  38. package/src/components/layout/FormTabs.tsx +77 -0
  39. package/src/components/layout/LayoutPlaceholder.tsx +85 -0
  40. package/src/components/registerComponents.ts +30 -0
  41. package/src/index.css +75 -0
  42. package/src/index.ts +5 -0
  43. package/src/main.tsx +10 -0
  44. package/src/store/FormStore.ts +811 -0
  45. package/src/utils/apiTransformer.ts +206 -0
  46. package/src/utils/idGenerator.ts +3 -0
  47. package/tsconfig.app.json +28 -0
  48. package/tsconfig.json +7 -0
  49. package/tsconfig.node.json +26 -0
  50. package/vite.config.ts +46 -0
@@ -0,0 +1,87 @@
1
+ import React from 'react';
2
+ import { Box, IconButton } from '@mui/material';
3
+ import SettingsIcon from '@mui/icons-material/Settings';
4
+ import { observer } from 'mobx-react-lite';
5
+ import { formStore, type SchemaNode } from '../../store/FormStore';
6
+
7
+ interface EditorWrapperProps {
8
+ node: SchemaNode;
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ export const EditorWrapper: React.FC<EditorWrapperProps> = observer(({ node, children }) => {
13
+ const isSelected = formStore.selectedNodeId === node.id;
14
+
15
+ const handleClick = (e: React.MouseEvent) => {
16
+ e.stopPropagation();
17
+ formStore.selectNode(node.id);
18
+ };
19
+
20
+ return (
21
+ <Box
22
+ onClick={handleClick}
23
+ onDoubleClick={(e) => {
24
+ e.stopPropagation();
25
+ formStore.openPropertiesModal(node.id);
26
+ }}
27
+ sx={{
28
+ display: 'flex',
29
+ flexDirection: 'column',
30
+ flexGrow: 1,
31
+ height: '100%',
32
+ position: 'relative',
33
+ outline: isSelected ? '2px solid #1976d2' : '1px dashed transparent',
34
+ outlineOffset: '-2px', // Inset outline to avoid overflow issues
35
+ cursor: 'pointer',
36
+ '&:hover': {
37
+ outline: !isSelected ? '1px dashed #999' : '2px solid #1976d2',
38
+ backgroundColor: !isSelected ? 'rgba(0, 0, 0, 0.02)' : undefined,
39
+ },
40
+ transition: 'all 0.2s',
41
+ }}
42
+ >
43
+ {isSelected && (
44
+ <Box
45
+ sx={{
46
+ position: 'absolute',
47
+ top: -24,
48
+ left: -2,
49
+ backgroundColor: '#1976d2',
50
+ color: 'white',
51
+ padding: '2px 8px',
52
+ borderRadius: '4px 4px 0 0',
53
+ fontSize: '0.75rem',
54
+ fontWeight: 'bold',
55
+ zIndex: 10,
56
+ textTransform: 'uppercase',
57
+ boxShadow: 1
58
+ }}
59
+ >
60
+ {node.type}
61
+ </Box>
62
+ )}
63
+ {isSelected && (
64
+ <Box
65
+ sx={{
66
+ position: 'absolute',
67
+ top: -15,
68
+ right: -10,
69
+ zIndex: 11,
70
+ backgroundColor: 'background.paper',
71
+ borderRadius: '50%',
72
+ boxShadow: 2
73
+ }}
74
+ onClick={(e) => {
75
+ e.stopPropagation();
76
+ formStore.openPropertiesModal(node.id);
77
+ }}
78
+ >
79
+ <IconButton size="small" color="primary">
80
+ <SettingsIcon fontSize="small" />
81
+ </IconButton>
82
+ </Box>
83
+ )}
84
+ {children}
85
+ </Box>
86
+ );
87
+ });
@@ -0,0 +1,313 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Box, Paper, Typography, AppBar, Toolbar, Button, Tabs, Tab } from '@mui/material';
4
+ import SettingsIcon from '@mui/icons-material/Settings';
5
+ import SaveIcon from '@mui/icons-material/Save';
6
+ import FileOpenIcon from '@mui/icons-material/FileOpen';
7
+ import DownloadIcon from '@mui/icons-material/Download';
8
+ import { observer } from 'mobx-react-lite';
9
+ import {
10
+ DndContext,
11
+ type DragEndEvent,
12
+ type DragStartEvent,
13
+ pointerWithin,
14
+ PointerSensor,
15
+ KeyboardSensor,
16
+ useSensor,
17
+ useSensors,
18
+ DragOverlay,
19
+ defaultDropAnimationSideEffects
20
+ } from '@dnd-kit/core';
21
+ import {
22
+ sortableKeyboardCoordinates
23
+ } from '@dnd-kit/sortable';
24
+ import {
25
+ formStore,
26
+ type NodeType
27
+ } from '../../store/FormStore';
28
+ import { FormRenderer } from '../FormRenderer';
29
+ import { Toolbox } from './Toolbox';
30
+ import { IntegrationSettings } from './IntegrationSettings';
31
+ import { PropertiesModal } from './PropertiesModal';
32
+ import { generateId } from '../../utils/idGenerator';
33
+ import { DroppableCanvas } from './DroppableCanvas';
34
+
35
+ interface FormBuilderProps {
36
+ onCancel?: () => void;
37
+ }
38
+
39
+ export const FormBuilder: React.FC<FormBuilderProps> = observer(({ onCancel }) => {
40
+ const { t } = useTranslation();
41
+ const [activeId, setActiveId] = React.useState<string | null>(null);
42
+ const [activeData, setActiveData] = React.useState<any>(null);
43
+ const [leftTab, setLeftTab] = React.useState<'toolbox' | 'integrations'>('toolbox');
44
+
45
+ const sensors = useSensors(
46
+ useSensor(PointerSensor, {
47
+ activationConstraint: {
48
+ distance: 10,
49
+ },
50
+ }),
51
+ useSensor(KeyboardSensor, {
52
+ coordinateGetter: sortableKeyboardCoordinates,
53
+ })
54
+ );
55
+
56
+ const handleDragStart = (event: DragStartEvent) => {
57
+ setActiveId(event.active.id as string);
58
+ setActiveData(event.active.data.current);
59
+ };
60
+
61
+ const getDropTarget = (overId: string | undefined): { parentId: string; index?: number } | null => {
62
+ if (!overId) return null;
63
+ if (overId === 'canvas-drop-zone') return { parentId: 'root' };
64
+
65
+ const overNode = formStore.findNode(formStore.rootNode, overId);
66
+ if (!overNode) return null;
67
+
68
+ const containerTypes: NodeType[] = ['root', 'row', 'col', 'tabs', 'tab', 'paper', 'repeater', 'table', 'cell'];
69
+
70
+ if (containerTypes.includes(overNode.type)) {
71
+ // If dropping on a container, check if it has children.
72
+ // If it's empty, we append (index undefined).
73
+ // If it's not empty, it's safer to append, unless we hit a specific child.
74
+ // But if we are ON the container itself, appending is the most natural behavior.
75
+ return { parentId: overId, index: -1 }; // -1 signifies "at the end" for my indicator logic
76
+ } else {
77
+ const parentNode = formStore.findParent(formStore.rootNode, overId);
78
+ if (parentNode) {
79
+ const overIndex = parentNode.children?.findIndex(c => c.id === overId) ?? -1;
80
+ return { parentId: parentNode.id, index: overIndex !== -1 ? overIndex : undefined };
81
+ }
82
+ }
83
+ return null;
84
+ };
85
+
86
+ const handleDragOver = (event: any) => {
87
+ const { over } = event;
88
+ const target = getDropTarget(over?.id);
89
+ if (target) {
90
+ formStore.setDropIndicator({ parentId: target.parentId, index: target.index ?? -1 });
91
+ } else {
92
+ formStore.setDropIndicator(null);
93
+ }
94
+ };
95
+
96
+ const handleDragEnd = (event: DragEndEvent) => {
97
+ const { active, over } = event;
98
+ setActiveId(null);
99
+ setActiveData(null);
100
+ formStore.setDropIndicator(null);
101
+
102
+ if (!over) return;
103
+
104
+ const target = getDropTarget(over.id as string);
105
+ if (!target) return;
106
+
107
+ const isToolboxItem = active.data.current?.isToolboxItem;
108
+
109
+ if (isToolboxItem) {
110
+ const toolData = active.data.current as { type: NodeType; defaultProps: Record<string, any> };
111
+ const { type, defaultProps } = toolData;
112
+
113
+ const newNode: any = {
114
+ id: generateId(type),
115
+ type: type,
116
+ props: { width: 6, ...defaultProps, name: generateId('field'), label: t(`toolbox.${type}`) },
117
+ children: []
118
+ };
119
+
120
+ // Special initialization for Table
121
+ if (type === 'table') {
122
+ if (!newNode.props.width) newNode.props.width = 12;
123
+ const rows = defaultProps?.rows || 2;
124
+ const cols = defaultProps?.cols || 2;
125
+ const cells = [];
126
+ for (let i = 0; i < rows * cols; i++) {
127
+ cells.push({ id: generateId('cell'), type: 'cell', children: [] });
128
+ }
129
+ newNode.children = cells;
130
+ }
131
+
132
+ formStore.addNode(target.parentId, newNode, target.index === -1 ? undefined : target.index);
133
+ } else if (active.id !== over.id) {
134
+ formStore.moveNode(active.id as string, over.id === 'canvas-drop-zone' ? 'root' : over.id as string);
135
+ }
136
+ };
137
+
138
+
139
+
140
+
141
+ return (
142
+ <DndContext
143
+ sensors={sensors}
144
+ collisionDetection={pointerWithin}
145
+ onDragStart={handleDragStart}
146
+ onDragOver={handleDragOver}
147
+ onDragEnd={handleDragEnd}
148
+ >
149
+ <Box display="flex" flexDirection="column" height="100vh">
150
+ <AppBar position="static" color="default" elevation={1}>
151
+ <Toolbar variant="dense">
152
+ {/* Title removed per request */}
153
+ <Box sx={{ display: 'flex', mx: 2, p: 0.5, backgroundColor: 'action.hover', borderRadius: 1 }}>
154
+ <Button
155
+ onClick={() => formStore.setEditMode(true)}
156
+ variant={formStore.isEditMode ? 'contained' : 'text'}
157
+ size="small"
158
+ sx={{ px: 2 }}
159
+ >
160
+ {t('app.builder')}
161
+ </Button>
162
+ <Button
163
+ onClick={() => formStore.setEditMode(false)}
164
+ variant={!formStore.isEditMode ? 'contained' : 'text'}
165
+ size="small"
166
+ sx={{ px: 2 }}
167
+ >
168
+ {t('app.preview')}
169
+ </Button>
170
+ </Box>
171
+
172
+ <Box sx={{ flexGrow: 1 }} />
173
+
174
+ <Button
175
+ color="inherit"
176
+ startIcon={<SettingsIcon />}
177
+ onClick={() => formStore.openPropertiesModal('root')}
178
+ >
179
+ {t("app.formSettings")}
180
+ </Button>
181
+ <Button color="inherit" startIcon={<FileOpenIcon />} onClick={() => formStore.loadFromStorage()}>
182
+ {t("app.load")}
183
+ </Button>
184
+ {onCancel && (
185
+ <Button color="inherit" onClick={onCancel} sx={{ ml: 1 }}>
186
+ {t("app.cancel") || "Cancel"}
187
+ </Button>
188
+ )}
189
+ <Button color="primary" variant="contained" startIcon={<SaveIcon />} onClick={() => formStore.saveToStorage()} sx={{ ml: 1 }}>
190
+ {t("app.save")}
191
+ </Button>
192
+ <Button color="inherit" startIcon={<DownloadIcon />} onClick={() => {
193
+ const data = JSON.stringify(formStore.rootNode, null, 2);
194
+ const blob = new Blob([data], { type: 'application/json' });
195
+ const url = URL.createObjectURL(blob);
196
+ const a = document.createElement('a');
197
+ a.href = url;
198
+ a.download = 'form-schema.json';
199
+ document.body.appendChild(a);
200
+ a.click();
201
+ document.body.removeChild(a);
202
+ URL.revokeObjectURL(url);
203
+ }}>
204
+ {t("app.exportJson")}
205
+ </Button>
206
+ </Toolbar>
207
+ </AppBar>
208
+
209
+ <Box display="flex" flexGrow={1} overflow="hidden">
210
+ {/* Left Sidebar with Tabs */}
211
+ {formStore.isEditMode && (
212
+ <Paper elevation={2} sx={{ width: 320, display: 'flex', flexDirection: 'column', zIndex: 1, borderRight: 1, borderColor: 'divider' }}>
213
+ <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
214
+ <Tabs value={leftTab} onChange={(_, v) => setLeftTab(v)} variant="fullWidth">
215
+ <Tab label={t("app.toolbox")} value="toolbox" />
216
+ <Tab label={t("app.integrations")} value="integrations" />
217
+ </Tabs>
218
+ </Box>
219
+ <Box sx={{ flexGrow: 1, overflow: 'auto' }}>
220
+ {leftTab === 'toolbox' ? <Toolbox /> : <IntegrationSettings />}
221
+ </Box>
222
+ </Paper>
223
+ )}
224
+
225
+ {/* Canvas - Center Area */}
226
+ <Box flexGrow={1} bgcolor="#f0f2f5" p={4} overflow="auto" display="flex" justifyContent="center">
227
+ <Paper
228
+ elevation={formStore.isEditMode ? 0 : 3}
229
+ sx={{
230
+ width: '100%',
231
+ maxWidth: 1400,
232
+ minHeight: '100%',
233
+ p: formStore.isEditMode ? 4 : 6,
234
+ px: formStore.isEditMode ? 6 : 8,
235
+ bgcolor: 'background.paper',
236
+ borderRadius: 2
237
+ }}
238
+ >
239
+ {formStore.isEditMode ? (
240
+ <DroppableCanvas>
241
+ <FormRenderer node={formStore.rootNode} path="" isEditing={true} />
242
+ </DroppableCanvas>
243
+ ) : (
244
+ <Box>
245
+ <FormRenderer node={formStore.rootNode} path="" isEditing={false} />
246
+ <Box mt={6} pt={3} borderTop={1} borderColor="divider" display="flex" justifyContent="flex-end" gap={2}>
247
+ {onCancel && (
248
+ <Button
249
+ variant="outlined"
250
+ size="large"
251
+ sx={{ px: 4, borderRadius: 2 }}
252
+ onClick={onCancel}
253
+ >
254
+ {t('app.cancel') || "Cancel"}
255
+ </Button>
256
+ )}
257
+ <Button
258
+ variant="contained"
259
+ color="success"
260
+ size="large"
261
+ sx={{ px: 4, borderRadius: 2 }}
262
+ onClick={async () => {
263
+ try {
264
+ await formStore.submitForm();
265
+ alert(t('app.submissionSuccess'));
266
+ } catch (e) {
267
+ console.error(e);
268
+ alert(t('app.submissionFailed'));
269
+ }
270
+ }}
271
+ >
272
+ {t('app.submitForm')}
273
+ </Button>
274
+ </Box>
275
+ </Box>
276
+ )}
277
+ </Paper>
278
+ </Box>
279
+
280
+ {/* Properties Modal (Replaces Sidebar) */}
281
+ <PropertiesModal />
282
+ </Box>
283
+ </Box>
284
+ <DragOverlay dropAnimation={{
285
+ sideEffects: defaultDropAnimationSideEffects({
286
+ styles: {
287
+ active: {
288
+ opacity: '0.5',
289
+ },
290
+ },
291
+ }),
292
+ }}>
293
+ {activeId ? (
294
+ <Box sx={{
295
+ p: 2,
296
+ bgcolor: 'background.paper',
297
+ border: '2px solid #1976d2',
298
+ borderRadius: 1,
299
+ boxShadow: 3,
300
+ opacity: 0.9,
301
+ minWidth: 100
302
+ }}>
303
+ {activeData?.isToolboxItem ? (
304
+ <Typography>{activeData.type.toUpperCase()}</Typography>
305
+ ) : (
306
+ <Typography>{t("app.movingElement")}</Typography>
307
+ )}
308
+ </Box>
309
+ ) : null}
310
+ </DragOverlay>
311
+ </DndContext>
312
+ );
313
+ });
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { Box, type SxProps, type Theme } from '@mui/material';
4
+ import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
5
+ import { formStore, type SchemaNode } from '../../store/FormStore';
6
+ import { FormRenderer, DropIndicator } from '../FormRenderer';
7
+
8
+ interface FormChildrenRendererProps {
9
+ parentId: string;
10
+ children: SchemaNode[] | undefined;
11
+ isEditing: boolean;
12
+ layout?: 'horizontal' | 'vertical';
13
+ path?: string;
14
+ }
15
+
16
+ export const FormChildrenRenderer: React.FC<FormChildrenRendererProps> = observer(({
17
+ parentId,
18
+ children,
19
+ isEditing,
20
+ layout = 'horizontal',
21
+ path
22
+ }) => {
23
+ const childIds = children?.map(c => c.id) || [];
24
+ const { dropIndicator } = formStore;
25
+
26
+ const isHorizontal = layout === 'horizontal';
27
+
28
+ const containerSx: SxProps<Theme> = {
29
+ display: 'flex',
30
+ flexWrap: isHorizontal ? 'wrap' : 'nowrap',
31
+ flexDirection: isHorizontal ? 'row' : 'column',
32
+ width: '100%',
33
+ m: 0,
34
+ gap: 0,
35
+ };
36
+
37
+ if (!isEditing) {
38
+ return (
39
+ <Box sx={containerSx}>
40
+ {children?.map(child => (
41
+ <FormRenderer key={child.id} node={child} path={path} isEditing={false} />
42
+ ))}
43
+ </Box>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <Box sx={containerSx}>
49
+ <SortableContext items={childIds} strategy={rectSortingStrategy}>
50
+ {children?.map((child, index) => (
51
+ <React.Fragment key={child.id}>
52
+ {dropIndicator?.parentId === parentId && dropIndicator.index === index && (
53
+ <Box sx={{ width: isHorizontal ? '100%' : 'auto', p: isHorizontal ? 1 : 0 }}>
54
+ <DropIndicator />
55
+ </Box>
56
+ )}
57
+ <FormRenderer node={child} path={path} isEditing={true} />
58
+ </React.Fragment>
59
+ ))}
60
+ {dropIndicator?.parentId === parentId && dropIndicator.index === -1 && (
61
+ <Box sx={{ width: isHorizontal ? '100%' : 'auto', p: isHorizontal ? 1 : 0 }}>
62
+ <DropIndicator />
63
+ </Box>
64
+ )}
65
+ </SortableContext>
66
+ </Box>
67
+ );
68
+ });
@@ -0,0 +1,110 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ Box, Button, TextField, Typography, List, ListItem,
4
+ ListItemText, ListItemSecondaryAction, IconButton,
5
+ Dialog, DialogTitle, DialogContent, DialogActions,
6
+ Select, MenuItem, FormControl, InputLabel
7
+ } from '@mui/material';
8
+ import DeleteIcon from '@mui/icons-material/Delete';
9
+ import AddIcon from '@mui/icons-material/Add';
10
+ import SyncIcon from '@mui/icons-material/Sync';
11
+ import { observer } from 'mobx-react-lite';
12
+ import { formStore, type APIIntegration } from '../../store/FormStore';
13
+
14
+ export const IntegrationSettings: React.FC = observer(() => {
15
+ const [open, setOpen] = useState(false);
16
+ const [currentIntegration, setCurrentIntegration] = useState<Partial<APIIntegration>>({
17
+ method: 'POST',
18
+ headers: {}
19
+ });
20
+
21
+ const handleAdd = () => {
22
+ setCurrentIntegration({ id: Math.random().toString(36).substr(2, 9), method: 'POST', headers: {}, name: '', fetchFieldsURL: '', submitURL: '', availableFields: [] });
23
+ setOpen(true);
24
+ };
25
+
26
+ const handleSave = () => {
27
+ if (currentIntegration.id) {
28
+ formStore.addIntegration(currentIntegration as APIIntegration);
29
+ setOpen(false);
30
+ }
31
+ };
32
+
33
+ const integrations = formStore.rootNode.props?.integrations || [];
34
+
35
+ return (
36
+ <Box sx={{ p: 2 }}>
37
+ <Typography variant="h6" gutterBottom>API Integrations</Typography>
38
+ <Button
39
+ variant="contained"
40
+ startIcon={<AddIcon />}
41
+ onClick={handleAdd}
42
+ sx={{ mb: 2 }}
43
+ >
44
+ Add Integration
45
+ </Button>
46
+
47
+ <List>
48
+ {integrations.map((integration: APIIntegration) => (
49
+ <ListItem key={integration.id} divider>
50
+ <ListItemText
51
+ primary={integration.name}
52
+ secondary={`${integration.method} ${integration.submitURL}`}
53
+ />
54
+ <ListItemSecondaryAction>
55
+ <IconButton onClick={() => formStore.fetchExternalFields(integration.id)} title="Fetch Fields">
56
+ <SyncIcon />
57
+ </IconButton>
58
+ <IconButton edge="end" onClick={() => formStore.removeIntegration(integration.id)}>
59
+ <DeleteIcon />
60
+ </IconButton>
61
+ </ListItemSecondaryAction>
62
+ </ListItem>
63
+ ))}
64
+ </List>
65
+
66
+ <Dialog open={open} onClose={() => setOpen(false)} fullWidth maxWidth="sm">
67
+ <DialogTitle>Add API Integration</DialogTitle>
68
+ <DialogContent>
69
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
70
+ <TextField
71
+ label="Name"
72
+ fullWidth
73
+ value={currentIntegration.name}
74
+ onChange={(e) => setCurrentIntegration({ ...currentIntegration, name: e.target.value })}
75
+ />
76
+ <TextField
77
+ label="Fetch Fields URL"
78
+ fullWidth
79
+ value={currentIntegration.fetchFieldsURL}
80
+ onChange={(e) => setCurrentIntegration({ ...currentIntegration, fetchFieldsURL: e.target.value })}
81
+ placeholder="http://localhost:8000/api/mock-external/fields"
82
+ />
83
+ <TextField
84
+ label="Submit URL"
85
+ fullWidth
86
+ value={currentIntegration.submitURL}
87
+ onChange={(e) => setCurrentIntegration({ ...currentIntegration, submitURL: e.target.value })}
88
+ placeholder="http://localhost:8000/api/mock-external/submit"
89
+ />
90
+ <FormControl fullWidth>
91
+ <InputLabel>Method</InputLabel>
92
+ <Select
93
+ value={currentIntegration.method}
94
+ label="Method"
95
+ onChange={(e) => setCurrentIntegration({ ...currentIntegration, method: e.target.value as any })}
96
+ >
97
+ <MenuItem value="POST">POST</MenuItem>
98
+ <MenuItem value="PUT">PUT</MenuItem>
99
+ </Select>
100
+ </FormControl>
101
+ </Box>
102
+ </DialogContent>
103
+ <DialogActions>
104
+ <Button onClick={() => setOpen(false)}>Cancel</Button>
105
+ <Button onClick={handleSave} variant="contained">Save</Button>
106
+ </DialogActions>
107
+ </Dialog>
108
+ </Box>
109
+ );
110
+ });
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+ import {
3
+ Dialog,
4
+ DialogTitle,
5
+ DialogContent,
6
+ DialogActions,
7
+ Button,
8
+ Box,
9
+ Typography,
10
+ IconButton,
11
+ useTheme,
12
+ useMediaQuery
13
+ } from '@mui/material';
14
+ import { observer } from 'mobx-react-lite';
15
+ import { useTranslation } from 'react-i18next';
16
+ import { formStore } from '../../store/FormStore';
17
+ import CloseIcon from '@mui/icons-material/Close';
18
+ import { PropertiesPanel } from './PropertiesPanel'; // We reuse the inner content for now
19
+ // Actually PropertiesPanel has its own layout, we should extract the content
20
+ // OR just render PropertiesPanel inside the modal and strip its outer layout in refactor.
21
+ // For now, let's wrap PropertiesPanel and modify it to not have the header?
22
+ // Or better: Clone the logic from PropertiesPanel into here or make PropertiesPanel adapting.
23
+ // Let's modify PropertiesPanel to accept a "mode" or just use it as content.
24
+
25
+ export const PropertiesModal: React.FC = observer(() => {
26
+ const { t } = useTranslation();
27
+ const theme = useTheme();
28
+ const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
29
+ const isOpen = formStore.isPropertiesModalOpen;
30
+ const node = formStore.selectedNode;
31
+
32
+ if (!node) return null;
33
+
34
+ const handleClose = () => {
35
+ formStore.closePropertiesModal();
36
+ };
37
+
38
+ return (
39
+ <Dialog
40
+ open={isOpen}
41
+ onClose={handleClose}
42
+ fullScreen={fullScreen}
43
+ maxWidth="sm"
44
+ fullWidth
45
+ scroll="paper"
46
+ PaperProps={{
47
+ sx: { minHeight: '600px', maxHeight: '80vh' }
48
+ }}
49
+ >
50
+ <DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
51
+ <Typography variant="h6" component="div">
52
+ {node.type === 'root' ? t('app.formSettings') : t('app.elementSettings')}
53
+ </Typography>
54
+ <IconButton
55
+ aria-label="close"
56
+ onClick={handleClose}
57
+ sx={{ color: (theme) => theme.palette.grey[500] }}
58
+ >
59
+ <CloseIcon />
60
+ </IconButton>
61
+ </DialogTitle>
62
+ <DialogContent dividers sx={{ p: 0 }}>
63
+ {/* We render the existing panel. We might need to adjust styling later to remove duplicate headers */}
64
+ <Box sx={{ '& > div': { height: 'auto', border: 'none' } }}>
65
+ <PropertiesPanel />
66
+ </Box>
67
+ </DialogContent>
68
+ <DialogActions>
69
+ <Button onClick={handleClose}>
70
+ {t('properties.delete').toLowerCase() === 'delete' ? 'Close' : 'Закрыть'}
71
+ </Button>
72
+ </DialogActions>
73
+ </Dialog>
74
+ );
75
+ });