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

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