PluginProbe
The Innovative Form Builder – IvyForms / 0.8.1
The Innovative Form Builder – IvyForms v0.8.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / utils / fieldsNormalization.ts

fieldsNormalization.ts in The Innovative Form Builder – IvyForms 0.8.1, at frontend/src/utils/fieldsNormalization.ts

224 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import type { Field, NameSubField, AddressSubField } from '@/types/field'
2
3 function safeParse(json: string | null | undefined): Record<string, unknown> {
4 if (!json) return {}
5 try {
6 return JSON.parse(json) as Record<string, unknown>
7 } catch {
8 return {}
9 }
10 }
11
12 export function normalizeFieldsForBuilder(apiFields: Field[]): Field[] {
13 if (!Array.isArray(apiFields) || apiFields.length === 0) return []
14 const parents: Field[] = []
15 const childrenByParent: Record<number, Field[]> = {}
16
17 for (const f of apiFields) {
18 if (f.parentId && typeof f.parentId === 'number' && f.parentId > 0) {
19 if (!childrenByParent[f.parentId]) childrenByParent[f.parentId] = []
20 childrenByParent[f.parentId].push(f)
21 } else {
22 parents.push(f)
23 }
24 }
25
26 // Ensure fields have proper row/column defaults
27 parents.forEach((field, index) => {
28 // Only set defaults if ALL layout properties are missing (backward compatibility)
29 const hasLayoutInfo =
30 (field.rowIndex !== undefined && field.rowIndex !== null) ||
31 (field.columnIndex !== undefined && field.columnIndex !== null) ||
32 (field.width !== undefined && field.width !== null)
33
34 if (!hasLayoutInfo) {
35 // Old form without layout info - each field in its own row
36 field.rowIndex = index
37 field.columnIndex = 0
38 field.width = 100
39 } else {
40 // Form with layout info - preserve existing values or use sensible defaults
41 if (field.rowIndex === undefined || field.rowIndex === null) {
42 field.rowIndex = 0
43 }
44 if (field.columnIndex === undefined || field.columnIndex === null) {
45 field.columnIndex = 0
46 }
47 if (field.width === undefined || field.width === null) {
48 field.width = 100
49 }
50 }
51 })
52
53 return parents.map((parent) => {
54 if (parent.type === 'name') {
55 const children = childrenByParent[parent.id] ?? []
56 const nameFields: NameSubField[] = children.map((child) => {
57 const s = safeParse((child as Field & { settings?: string }).settings)
58 const label = (child as Partial<Field>).label ?? String(s['label'] ?? '')
59 return {
60 type: 'text',
61 id: String(child.id ?? ''),
62 modelValue: (child as Partial<Field>).defaultValue ?? '',
63 label,
64 required: !!(child as Partial<Field>).required || Boolean(s['required']),
65 optionHide: !!(child as Partial<Field>).hideLabel || Boolean(s['hideLabel']),
66 description: String((child as Partial<Field>).description ?? s['description'] ?? ''),
67 placeholder: String((child as Partial<Field>).placeholder ?? s['placeholder'] ?? ''),
68 }
69 })
70 const nameFieldTypes = nameFields.map((_, idx) => `nameField${idx + 1}`)
71 return {
72 ...parent,
73 nameFields,
74 nameFieldTypes,
75 // Preserve grid layout properties
76 rowIndex: parent.rowIndex ?? 0,
77 columnIndex: parent.columnIndex ?? 0,
78 width: parent.width ?? 100,
79 } as Field
80 }
81 if (parent.type === 'address') {
82 const children = childrenByParent[parent.id] ?? []
83 const defaultAddressOrder = [
84 'streetAddress',
85 'addressLine2',
86 'city',
87 'state',
88 'zip',
89 'country',
90 ]
91 const addressFields: AddressSubField[] = children.map((child, idx) => {
92 const s = safeParse((child as Field & { settings?: string }).settings)
93 const explicitType = (child as Partial<Field>).addressType || String(s['type'] ?? '')
94 const type =
95 explicitType && explicitType.trim() !== ''
96 ? explicitType
97 : (defaultAddressOrder[idx] ?? `addressPart${idx + 1}`)
98 return {
99 type,
100 id: String(child.id ?? ''),
101 label: (child as Partial<Field>).label ?? String(s['label'] ?? ''),
102 value: (child as Partial<Field>).defaultValue ?? String(s['value'] ?? ''),
103 placeholder: (child as Partial<Field>).placeholder ?? String(s['placeholder'] ?? ''),
104 required: !!(child as Partial<Field>).required || Boolean(s['required']),
105 hideLabel: !!(child as Partial<Field>).hideLabel || Boolean(s['hideLabel']),
106 description: String((child as Partial<Field>).description ?? s['description'] ?? ''),
107 requiredMessage: String(
108 (child as Partial<Field>).requiredMessage ?? s['requiredMessage'] ?? '',
109 ),
110 visible: !!(child as Partial<Field>).visible || Boolean(s['visible'] ?? true),
111 }
112 })
113 const addressFieldTypes = addressFields.map((af) => af.type)
114 return {
115 ...parent,
116 addressFields,
117 addressFieldTypes,
118 // Preserve grid layout properties
119 rowIndex: parent.rowIndex ?? 0,
120 columnIndex: parent.columnIndex ?? 0,
121 width: parent.width ?? 100,
122 } as Field
123 }
124 // Regular field - preserve grid layout properties
125 return {
126 ...parent,
127 rowIndex: parent.rowIndex ?? 0,
128 columnIndex: parent.columnIndex ?? 0,
129 width: parent.width ?? 100,
130 }
131 })
132 }
133
134 export function denormalizeFieldsForApi(fields: Field[]): Field[] {
135 const result: Field[] = []
136 for (const field of fields) {
137 if (field.type === 'name') {
138 if (Array.isArray(field.nameFields)) {
139 const { nameFields, ...parentRest } = field
140 // Preserve row/column/width layout for name fields
141 result.push({
142 ...parentRest,
143 type: 'name',
144 rowIndex: field.rowIndex,
145 columnIndex: field.columnIndex,
146 width: field.width,
147 } as Field)
148 nameFields.forEach((sub, idx) => {
149 result.push({
150 id: sub.id ? parseInt(sub.id, 10) || 0 : 0,
151 formId: field.formId,
152 fieldIndex: field.fieldIndex,
153 type: 'text',
154 label: sub.label,
155 defaultValue: sub.modelValue,
156 required: sub.required,
157 placeholder: sub.placeholder,
158 position: field.position,
159 value: sub.modelValue,
160 hideLabel: sub.optionHide,
161 description: sub.description,
162 requiredMessage: '',
163 parentId: field.id,
164 subFieldIndex: idx,
165 } as Field)
166 })
167 continue
168 }
169 }
170 if (field.type === 'address') {
171 if (Array.isArray(field.addressFields)) {
172 const { addressFields, ...parentRest } = field
173 // Preserve row/column/width layout for address fields
174 result.push({
175 ...parentRest,
176 rowIndex: field.rowIndex,
177 columnIndex: field.columnIndex,
178 width: field.width,
179 } as Field)
180 addressFields.forEach((sub, idx) => {
181 const settings = JSON.stringify({
182 hideLabel: sub.hideLabel,
183 description: sub.description,
184 placeholder: sub.placeholder,
185 type: sub.type,
186 requiredMessage: sub.requiredMessage,
187 visible: typeof sub.visible === 'boolean' ? sub.visible : true,
188 })
189 result.push({
190 id: sub.id ? parseInt(sub.id, 10) || 0 : 0,
191 formId: field.formId,
192 fieldIndex: field.fieldIndex,
193 type: 'text',
194 label: sub.label,
195 defaultValue: sub.value,
196 required: sub.required,
197 placeholder: sub.placeholder,
198 position: field.position,
199 value: sub.value,
200 hideLabel: sub.hideLabel,
201 description: sub.description,
202 requiredMessage: sub.requiredMessage,
203 parentId: field.id,
204 addressType: sub.type,
205 settings,
206 subFieldIndex: idx,
207 visible: sub.visible,
208 } as Field)
209 })
210 continue
211 }
212 }
213 // Regular field - preserve row/column/width layout
214 result.push({
215 ...field,
216 id: typeof field.id === 'string' ? parseInt(field.id, 10) || 0 : field.id || 0,
217 rowIndex: field.rowIndex,
218 columnIndex: field.columnIndex,
219 width: field.width,
220 } as Field)
221 }
222 return result
223 }
224