PluginProbe
The Innovative Form Builder – IvyForms / 1.1.1
The Innovative Form Builder – IvyForms v1.1.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 1.1.1, at frontend/src/utils/fieldsNormalization.ts

286 lines 10.4 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 import {
3 assignLegacyNameFieldKeysIfMissing,
4 getChildFieldSettings,
5 isNameFieldKey,
6 resolveNameFieldKey,
7 sortChildrenByDisplayOrder,
8 } from '@/utils/nameFieldKeyUtils'
9
10 function safeParse(json: string | null | undefined): Record<string, unknown> {
11 if (!json) return {}
12 try {
13 return JSON.parse(json) as Record<string, unknown>
14 } catch {
15 return {}
16 }
17 }
18
19 export function normalizeFieldsForBuilder(apiFields: Field[]): Field[] {
20 if (!Array.isArray(apiFields) || apiFields.length === 0) return []
21 const parents: Field[] = []
22 const childrenByParent: Record<number, Field[]> = {}
23
24 for (const f of apiFields) {
25 // Parse settings if it's a string (from database)
26 if (typeof f.settings === 'string') {
27 const parsed = safeParse(f.settings)
28 f.settings = parsed as Record<string, unknown>
29 }
30
31 if (f.parentId && typeof f.parentId === 'number' && f.parentId > 0) {
32 if (!childrenByParent[f.parentId]) childrenByParent[f.parentId] = []
33 childrenByParent[f.parentId].push(f)
34 } else {
35 parents.push(f)
36 }
37 }
38
39 // Ensure fields have proper row/column defaults
40 parents.forEach((field, index) => {
41 // Only set defaults if ALL layout properties are missing (backward compatibility)
42 const hasLayoutInfo =
43 (field.rowIndex !== undefined && field.rowIndex !== null) ||
44 (field.columnIndex !== undefined && field.columnIndex !== null) ||
45 (field.width !== undefined && field.width !== null)
46
47 if (!hasLayoutInfo) {
48 // Old form without layout info - each field in its own row
49 field.rowIndex = index
50 field.columnIndex = 0
51 field.width = 100
52 } else {
53 // Form with layout info - preserve existing values or use sensible defaults
54 if (field.rowIndex === undefined || field.rowIndex === null) {
55 field.rowIndex = 0
56 }
57 if (field.columnIndex === undefined || field.columnIndex === null) {
58 field.columnIndex = 0
59 }
60 if (field.width === undefined || field.width === null) {
61 field.width = 100
62 }
63 }
64
65 // Extract pageId from settings if it exists
66 if (field.settings && typeof field.settings === 'object' && 'pageId' in field.settings) {
67 const settingsObj = field.settings as Record<string, unknown>
68 if (settingsObj.pageId && typeof settingsObj.pageId === 'string') {
69 field.pageId = settingsObj.pageId
70 }
71 }
72 })
73
74 return parents.map((parent) => {
75 if (parent.type === 'name') {
76 const childrenList = childrenByParent[parent.id] ?? []
77 const childOrder = new Map<Field, number>()
78 childrenList.forEach((child, index) => childOrder.set(child, index))
79 const children = sortChildrenByDisplayOrder(childrenList, childOrder)
80 const childSettingsList = children.map((child) => getChildFieldSettings(child))
81 const nameFields: NameSubField[] = assignLegacyNameFieldKeysIfMissing(
82 children.map((child, idx) => {
83 const settings = childSettingsList[idx]
84 const label = (child as Partial<Field>).label ?? String(settings.label ?? '')
85 return {
86 type: isNameFieldKey(child.nameFieldType) ? child.nameFieldType : undefined,
87 id: String(child.id ?? ''),
88 modelValue: (child as Partial<Field>).defaultValue ?? '',
89 label,
90 required: !!(child as Partial<Field>).required || Boolean(settings.required),
91 optionHide: !!(child as Partial<Field>).hideLabel || Boolean(settings.hideLabel),
92 description: String(
93 (child as Partial<Field>).description ?? settings.description ?? '',
94 ),
95 placeholder: String(
96 (child as Partial<Field>).placeholder ?? settings.placeholder ?? '',
97 ),
98 }
99 }),
100 childSettingsList,
101 )
102 const nameFieldTypes = nameFields.map((subfield) => subfield.type)
103 return {
104 ...parent,
105 nameFields,
106 nameFieldTypes,
107 // Preserve grid layout properties
108 rowIndex: parent.rowIndex ?? 0,
109 columnIndex: parent.columnIndex ?? 0,
110 width: parent.width ?? 100,
111 } as Field
112 }
113 if (parent.type === 'address') {
114 const children = childrenByParent[parent.id] ?? []
115 const defaultAddressOrder = [
116 'streetAddress',
117 'addressLine2',
118 'city',
119 'state',
120 'zip',
121 'country',
122 ]
123 const addressFields: AddressSubField[] = children.map((child, idx) => {
124 // Handle both string and object settings
125 let s: Record<string, unknown>
126 if (typeof child.settings === 'string') {
127 s = safeParse(child.settings)
128 } else if (typeof child.settings === 'object' && child.settings !== null) {
129 s = child.settings as Record<string, unknown>
130 } else {
131 s = {}
132 }
133 const explicitType = (child as Partial<Field>).addressType || String(s['type'] ?? '')
134 const type =
135 explicitType && explicitType.trim() !== ''
136 ? explicitType
137 : (defaultAddressOrder[idx] ?? `addressPart${idx + 1}`)
138 return {
139 type,
140 id: String(child.id ?? ''),
141 label: (child as Partial<Field>).label ?? String(s['label'] ?? ''),
142 value: (child as Partial<Field>).defaultValue ?? String(s['value'] ?? ''),
143 placeholder: (child as Partial<Field>).placeholder ?? String(s['placeholder'] ?? ''),
144 required: !!(child as Partial<Field>).required || Boolean(s['required']),
145 hideLabel: !!(child as Partial<Field>).hideLabel || Boolean(s['hideLabel']),
146 description: String((child as Partial<Field>).description ?? s['description'] ?? ''),
147 requiredMessage: String(
148 (child as Partial<Field>).requiredMessage ?? s['requiredMessage'] ?? '',
149 ),
150 visible:
151 typeof (child as Partial<Field>).visible === 'boolean'
152 ? Boolean((child as Partial<Field>).visible)
153 : Boolean(s['visible'] ?? true),
154 }
155 })
156 const addressFieldTypes = addressFields.map((af) => af.type)
157 return {
158 ...parent,
159 addressFields,
160 addressFieldTypes,
161 // Preserve grid layout properties
162 rowIndex: parent.rowIndex ?? 0,
163 columnIndex: parent.columnIndex ?? 0,
164 width: parent.width ?? 100,
165 } as Field
166 }
167 // Regular field - preserve grid layout properties and extract settings
168 const settings = safeParse((parent as Field & { settings?: string }).settings)
169 return {
170 ...parent,
171 htmlContent: (parent as Partial<Field>).htmlContent ?? String(settings['htmlContent'] ?? ''),
172 rowIndex: parent.rowIndex ?? 0,
173 columnIndex: parent.columnIndex ?? 0,
174 width: parent.width ?? 100,
175 }
176 })
177 }
178
179 export function denormalizeFieldsForApi(fields: Field[]): Field[] {
180 const result: Field[] = []
181 for (const field of fields) {
182 if (field.type === 'name') {
183 if (Array.isArray(field.nameFields)) {
184 const { nameFields, ...parentRest } = field
185 // Preserve row/column/width layout for name fields
186 const parentSettings = field.pageId ? { pageId: field.pageId } : {}
187 result.push({
188 ...parentRest,
189 type: 'name',
190 rowIndex: field.rowIndex,
191 columnIndex: field.columnIndex,
192 width: field.width,
193 settings: parentSettings,
194 } as Field)
195 nameFields.forEach((sub, idx) => {
196 const nameFieldType = isNameFieldKey(sub.type)
197 ? sub.type
198 : resolveNameFieldKey(sub, undefined, idx)
199 result.push({
200 id: sub.id ? parseInt(sub.id, 10) || 0 : 0,
201 formId: field.formId,
202 fieldIndex: field.fieldIndex,
203 type: 'text',
204 label: sub.label,
205 defaultValue: sub.modelValue,
206 required: sub.required,
207 placeholder: sub.placeholder,
208 position: field.position,
209 value: sub.modelValue,
210 hideLabel: sub.optionHide,
211 description: sub.description,
212 requiredMessage: '',
213 parentId: field.id,
214 nameFieldType,
215 subFieldIndex: idx,
216 pageId: field.pageId,
217 settings: {
218 nameFieldType,
219 subFieldIndex: idx,
220 ...(field.pageId && { pageId: field.pageId }),
221 },
222 } as Field)
223 })
224 continue
225 }
226 }
227 if (field.type === 'address') {
228 if (Array.isArray(field.addressFields)) {
229 const { addressFields, ...parentRest } = field
230 // Preserve row/column/width layout for address fields
231 const parentSettings = field.pageId ? { pageId: field.pageId } : {}
232 result.push({
233 ...parentRest,
234 rowIndex: field.rowIndex,
235 columnIndex: field.columnIndex,
236 width: field.width,
237 settings: parentSettings,
238 } as Field)
239 addressFields.forEach((sub, idx) => {
240 result.push({
241 id: sub.id ? parseInt(sub.id, 10) || 0 : 0,
242 formId: field.formId,
243 fieldIndex: field.fieldIndex,
244 type: 'text',
245 label: sub.label,
246 defaultValue: sub.value,
247 required: sub.required,
248 placeholder: sub.placeholder,
249 position: field.position,
250 value: sub.value,
251 hideLabel: sub.hideLabel,
252 description: sub.description,
253 requiredMessage: sub.requiredMessage,
254 parentId: field.id,
255 addressType: sub.type,
256 subFieldIndex: idx,
257 visible: sub.visible,
258 pageId: field.pageId,
259 settings: {
260 hideLabel: sub.hideLabel,
261 description: sub.description,
262 placeholder: sub.placeholder,
263 type: sub.type,
264 requiredMessage: sub.requiredMessage,
265 visible: typeof sub.visible === 'boolean' ? sub.visible : true,
266 ...(field.pageId && { pageId: field.pageId }),
267 },
268 } as Field)
269 })
270 continue
271 }
272 }
273 // Regular field - preserve row/column/width layout
274 const settings = field.pageId ? { pageId: field.pageId } : {}
275 result.push({
276 ...field,
277 id: typeof field.id === 'string' ? parseInt(field.id, 10) || 0 : field.id || 0,
278 rowIndex: field.rowIndex,
279 columnIndex: field.columnIndex,
280 width: field.width,
281 settings,
282 } as Field)
283 }
284 return result
285 }
286