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

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