@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,206 @@
|
|
|
1
|
+
import type { SchemaNode, NodeType } from '../store/FormStore';
|
|
2
|
+
import { generateId } from './idGenerator';
|
|
3
|
+
|
|
4
|
+
export interface APIColumn {
|
|
5
|
+
id: number;
|
|
6
|
+
colName: string;
|
|
7
|
+
colType: string;
|
|
8
|
+
colTitle: string;
|
|
9
|
+
required: boolean;
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface APIColumnGroup {
|
|
14
|
+
id: number;
|
|
15
|
+
title: string;
|
|
16
|
+
columns: { id: number; colName: string }[];
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface APIResponse {
|
|
21
|
+
columns: APIColumn[];
|
|
22
|
+
rootColumnGroups: APIColumnGroup[];
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const mapType = (apiType: string): NodeType => {
|
|
27
|
+
switch (apiType) {
|
|
28
|
+
case 'STRING': return 'text';
|
|
29
|
+
case 'INT':
|
|
30
|
+
case 'FLOAT': return 'number';
|
|
31
|
+
case 'BOOLEAN': return 'checkbox';
|
|
32
|
+
case 'TIMESTAMP':
|
|
33
|
+
case 'TIMESTAMP_2': return 'date';
|
|
34
|
+
case 'FILE':
|
|
35
|
+
case 'FILE_SET': return 'file';
|
|
36
|
+
case 'REF': return 'select';
|
|
37
|
+
default: return 'text';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const transformAPIDataToSchema = (apiData: APIResponse): SchemaNode => {
|
|
42
|
+
const { columns, rootColumnGroups } = apiData;
|
|
43
|
+
|
|
44
|
+
// Create a map for quick access to columns by ID
|
|
45
|
+
const columnMap = new Map<number, APIColumn>();
|
|
46
|
+
columns.forEach(col => columnMap.set(col.id, col));
|
|
47
|
+
|
|
48
|
+
// Track which columns are already placed in groups
|
|
49
|
+
const placedColumnIds = new Set<number>();
|
|
50
|
+
|
|
51
|
+
const transformColumn = (col: APIColumn): SchemaNode => {
|
|
52
|
+
const isRef = col.colType === 'REF';
|
|
53
|
+
const node: SchemaNode = {
|
|
54
|
+
id: col.id.toString(),
|
|
55
|
+
type: mapType(col.colType),
|
|
56
|
+
props: {
|
|
57
|
+
name: col.id.toString(), // Use API column ID for mapping
|
|
58
|
+
label: col.colTitle,
|
|
59
|
+
options: isRef ? [] : undefined,
|
|
60
|
+
enableAutocomplete: isRef ? true : undefined,
|
|
61
|
+
apiColumnName: col.colName // Keep the original name as meta
|
|
62
|
+
},
|
|
63
|
+
validation: {
|
|
64
|
+
required: col.required,
|
|
65
|
+
integer: col.colType === 'INT'
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (isRef && col.refForm && node.props) {
|
|
70
|
+
node.props.dictionaryInfo = {
|
|
71
|
+
tableName: col.refForm.tableName,
|
|
72
|
+
id: col.refForm.id,
|
|
73
|
+
mnemo: col.refForm.mnemo
|
|
74
|
+
};
|
|
75
|
+
// Also check for specific dictionary version mapping from apiData
|
|
76
|
+
const dicVersion = apiData.dicVersions?.find((dv: any) => dv.column.id === col.id);
|
|
77
|
+
if (dicVersion) {
|
|
78
|
+
node.props.dictionaryInfo.versionId = dicVersion.report.id;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return node;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const children: SchemaNode[] = [];
|
|
86
|
+
|
|
87
|
+
// 1. Process Groups
|
|
88
|
+
rootColumnGroups.forEach(group => {
|
|
89
|
+
const groupNode: SchemaNode = {
|
|
90
|
+
id: generateId('group'),
|
|
91
|
+
type: 'paper',
|
|
92
|
+
props: {
|
|
93
|
+
label: group.title,
|
|
94
|
+
padding: 2
|
|
95
|
+
},
|
|
96
|
+
children: []
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
group.columns.forEach(groupCol => {
|
|
100
|
+
const actualCol = columnMap.get(groupCol.id);
|
|
101
|
+
if (actualCol && actualCol.colType !== 'POSTGIS') {
|
|
102
|
+
groupNode.children?.push(transformColumn(actualCol));
|
|
103
|
+
placedColumnIds.add(actualCol.id);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (groupNode.children && groupNode.children.length > 0) {
|
|
108
|
+
children.push(groupNode);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 2. Process Remaining Columns (those not in any group)
|
|
113
|
+
columns.forEach(col => {
|
|
114
|
+
if (!placedColumnIds.has(col.id) && col.colType !== 'POSTGIS') {
|
|
115
|
+
children.push(transformColumn(col));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: 'root',
|
|
121
|
+
type: 'root',
|
|
122
|
+
children
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const transformSchemaToSubmission = (
|
|
127
|
+
rootNode: SchemaNode,
|
|
128
|
+
values: Record<string, any>,
|
|
129
|
+
sessionVars: Record<string, any> = {}
|
|
130
|
+
): any => {
|
|
131
|
+
const fieldValues: Record<string, any> = {};
|
|
132
|
+
|
|
133
|
+
const traverse = (node: SchemaNode) => {
|
|
134
|
+
if (node.props?.name) {
|
|
135
|
+
// Internal path like "4094" (field name/id)
|
|
136
|
+
const val = getDeepValue(values, node.props.name);
|
|
137
|
+
if (val !== undefined) {
|
|
138
|
+
// Map internal value to the API-based key in the output (using id_поля / name)
|
|
139
|
+
fieldValues[node.props.name] = val;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
node.children?.forEach(traverse);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
traverse(rootNode);
|
|
146
|
+
|
|
147
|
+
// Submission format as requested
|
|
148
|
+
return {
|
|
149
|
+
reportId: sessionVars.reportId || Number(rootNode.id) || 0,
|
|
150
|
+
userGroupId: sessionVars.userGroupId || 1,
|
|
151
|
+
stageStatus: sessionVars.stageStatus || "T",
|
|
152
|
+
rows: [
|
|
153
|
+
{
|
|
154
|
+
id: sessionVars.rowId || 1,
|
|
155
|
+
leafProviderId: sessionVars.leafProviderId || 0,
|
|
156
|
+
values: fieldValues
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
isFrontButton: true,
|
|
160
|
+
...sessionVars.extraData // Allow merging other session data
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Helper for deep value access (same logic as in FormStore but standalone)
|
|
165
|
+
function getDeepValue(obj: any, path: string) {
|
|
166
|
+
const normalizedPath = path.replace(/\[(\d+)\]/g, '.$1');
|
|
167
|
+
const keys = normalizedPath.split('.');
|
|
168
|
+
let current = obj;
|
|
169
|
+
for (const key of keys) {
|
|
170
|
+
if (current === undefined || current === null) return undefined;
|
|
171
|
+
current = current[key];
|
|
172
|
+
}
|
|
173
|
+
return current;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Transforms a dictionary response into Select/Autocomplete options.
|
|
178
|
+
* @param data The JSON response from the dictionary API
|
|
179
|
+
* @param labelKey Optional key to use for the label. If not provided, it tries to find the best candidate.
|
|
180
|
+
*/
|
|
181
|
+
export const transformDictionaryToOptions = (data: any, labelKey?: string): { label: string; value: any }[] => {
|
|
182
|
+
const content = data?.content || [];
|
|
183
|
+
if (!content.length) return [];
|
|
184
|
+
|
|
185
|
+
// If labelKey is not provided, try to find a candidate in the first item's values
|
|
186
|
+
let detectedLabelKey = labelKey;
|
|
187
|
+
if (!detectedLabelKey) {
|
|
188
|
+
const firstItem = content[0];
|
|
189
|
+
const values = firstItem.values || {};
|
|
190
|
+
|
|
191
|
+
// Strategy:
|
|
192
|
+
// 1. Look for common names like 'name', 'title', 'label'
|
|
193
|
+
// 2. Look for the first key that has a string value (and isn't just a number-string)
|
|
194
|
+
// In the user's example, 4092 is "один", 4091 is "1". 4092 is the best label.
|
|
195
|
+
const keys = Object.keys(values);
|
|
196
|
+
detectedLabelKey = keys.find(k => {
|
|
197
|
+
const val = values[k];
|
|
198
|
+
return typeof val === 'string' && isNaN(Number(val));
|
|
199
|
+
}) || keys[0]; // fallback to first key
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return content.map((item: any) => ({
|
|
203
|
+
label: item.values?.[detectedLabelKey!] || `Item ${item.id}`,
|
|
204
|
+
value: item.id
|
|
205
|
+
}));
|
|
206
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
// https://vite.dev/config/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
build: {
|
|
9
|
+
lib: {
|
|
10
|
+
entry: resolve(__dirname, 'src/index.ts'),
|
|
11
|
+
name: 'FormBuilder',
|
|
12
|
+
fileName: 'form-builder',
|
|
13
|
+
},
|
|
14
|
+
rollupOptions: {
|
|
15
|
+
external: (id) => {
|
|
16
|
+
const externals = [
|
|
17
|
+
'react',
|
|
18
|
+
'react-dom',
|
|
19
|
+
'mobx',
|
|
20
|
+
'mobx-react-lite',
|
|
21
|
+
'@mui/material',
|
|
22
|
+
'@mui/icons-material',
|
|
23
|
+
'@emotion/react',
|
|
24
|
+
'@emotion/styled',
|
|
25
|
+
'@dnd-kit/core',
|
|
26
|
+
'@dnd-kit/sortable',
|
|
27
|
+
'@dnd-kit/utilities',
|
|
28
|
+
'@mui/x-date-pickers',
|
|
29
|
+
'date-fns',
|
|
30
|
+
'dayjs'
|
|
31
|
+
];
|
|
32
|
+
// Match exact package name or subpaths
|
|
33
|
+
return externals.some(pkg => id === pkg || id.startsWith(pkg + '/'));
|
|
34
|
+
},
|
|
35
|
+
output: {
|
|
36
|
+
globals: {
|
|
37
|
+
react: 'React',
|
|
38
|
+
'react-dom': 'ReactDOM',
|
|
39
|
+
mobx: 'mobx',
|
|
40
|
+
'mobx-react-lite': 'mobxReactLite',
|
|
41
|
+
'@mui/material': 'MaterialUI',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
})
|