@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,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,3 @@
1
+ export const generateId = (prefix: string = 'node'): string => {
2
+ return `${prefix}_${Math.random().toString(36).substr(2, 9)}`;
3
+ };
@@ -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,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -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
+ })