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

226 lines 8.1 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 and extract settings
125 const settings = safeParse((parent as Field & { settings?: string }).settings)
126 return {
127 ...parent,
128 htmlContent: (parent as Partial<Field>).htmlContent ?? String(settings['htmlContent'] ?? ''),
129 rowIndex: parent.rowIndex ?? 0,
130 columnIndex: parent.columnIndex ?? 0,
131 width: parent.width ?? 100,
132 }
133 })
134 }
135
136 export function denormalizeFieldsForApi(fields: Field[]): Field[] {
137 const result: Field[] = []
138 for (const field of fields) {
139 if (field.type === 'name') {
140 if (Array.isArray(field.nameFields)) {
141 const { nameFields, ...parentRest } = field
142 // Preserve row/column/width layout for name fields
143 result.push({
144 ...parentRest,
145 type: 'name',
146 rowIndex: field.rowIndex,
147 columnIndex: field.columnIndex,
148 width: field.width,
149 } as Field)
150 nameFields.forEach((sub, idx) => {
151 result.push({
152 id: sub.id ? parseInt(sub.id, 10) || 0 : 0,
153 formId: field.formId,
154 fieldIndex: field.fieldIndex,
155 type: 'text',
156 label: sub.label,
157 defaultValue: sub.modelValue,
158 required: sub.required,
159 placeholder: sub.placeholder,
160 position: field.position,
161 value: sub.modelValue,
162 hideLabel: sub.optionHide,
163 description: sub.description,
164 requiredMessage: '',
165 parentId: field.id,
166 subFieldIndex: idx,
167 } as Field)
168 })
169 continue
170 }
171 }
172 if (field.type === 'address') {
173 if (Array.isArray(field.addressFields)) {
174 const { addressFields, ...parentRest } = field
175 // Preserve row/column/width layout for address fields
176 result.push({
177 ...parentRest,
178 rowIndex: field.rowIndex,
179 columnIndex: field.columnIndex,
180 width: field.width,
181 } as Field)
182 addressFields.forEach((sub, idx) => {
183 const settings = JSON.stringify({
184 hideLabel: sub.hideLabel,
185 description: sub.description,
186 placeholder: sub.placeholder,
187 type: sub.type,
188 requiredMessage: sub.requiredMessage,
189 visible: typeof sub.visible === 'boolean' ? sub.visible : true,
190 })
191 result.push({
192 id: sub.id ? parseInt(sub.id, 10) || 0 : 0,
193 formId: field.formId,
194 fieldIndex: field.fieldIndex,
195 type: 'text',
196 label: sub.label,
197 defaultValue: sub.value,
198 required: sub.required,
199 placeholder: sub.placeholder,
200 position: field.position,
201 value: sub.value,
202 hideLabel: sub.hideLabel,
203 description: sub.description,
204 requiredMessage: sub.requiredMessage,
205 parentId: field.id,
206 addressType: sub.type,
207 settings,
208 subFieldIndex: idx,
209 visible: sub.visible,
210 } as Field)
211 })
212 continue
213 }
214 }
215 // Regular field - preserve row/column/width layout
216 result.push({
217 ...field,
218 id: typeof field.id === 'string' ? parseInt(field.id, 10) || 0 : field.id || 0,
219 rowIndex: field.rowIndex,
220 columnIndex: field.columnIndex,
221 width: field.width,
222 } as Field)
223 }
224 return result
225 }
226