PluginProbe
The Innovative Form Builder – IvyForms / 0.6
The Innovative Form Builder – IvyForms v0.6
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 / stores / useFormBuilder.ts

useFormBuilder.ts in The Innovative Form Builder – IvyForms 0.6, at frontend/src/stores/useFormBuilder.ts

456 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { defineStore } from 'pinia'
2 import { ref } from 'vue'
3 import { useLabels } from '@/composables/useLabels'
4 import IvyMessage from '@/views/_components/message/ivyMessage.ts'
5 import { useRouter } from 'vue-router'
6 import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore'
7 import { useApiClient } from '@/composables/useApiClient'
8 import { useGlobalState } from '@/stores/useGlobalState'
9 import type { Field } from '@/types/field'
10 import { normalizeFieldsForBuilder, denormalizeFieldsForApi } from '@/utils/fieldsNormalization'
11 import { buildNewField } from '@/utils/addFieldUtil.ts'
12 import { duplicateFieldForStore } from '@/utils/fieldUtils'
13
14 export const useFormBuilder = defineStore('formBuilderData', () => {
15 const formId = ref<string | null>(null)
16 const isEditing = ref(false)
17 const fields = ref<Field[]>([])
18 const counterFields = ref<number>(0)
19 const selectedField = ref<Field | null>(null)
20 const activeTab = ref<string>('addField')
21 const description = ref<string | null>(null)
22 const name = ref<string | null>('Blank form')
23 const published = ref(true)
24 const showTitle = ref(false)
25 const showDescription = ref(false)
26 const storeEntries = ref(false)
27 const integrationSettings = ref<{
28 wpdatatables: { enabled: boolean }
29 [key: string]: Record<string, unknown>
30 }>({
31 wpdatatables: { enabled: true },
32 })
33 const isEditingSettingGeneral = ref(false)
34 const confirmationId = ref<string | null>(null)
35 const router = useRouter()
36 const isFormLoading = ref(false)
37
38 const { getLabel } = useLabels()
39 const { request } = useApiClient()
40 const unsavedChangesStore = useUnsavedChangesStore()
41
42 const selectField = (fieldIndex: number) => {
43 const field = fields.value.find((field) => field.fieldIndex === fieldIndex) || null
44
45 // If selecting the same field, still ensure the tab is set to options
46 if (selectedField.value?.fieldIndex === fieldIndex) {
47 activeTab.value = 'options'
48 return
49 }
50
51 selectedField.value = field
52 activeTab.value = 'options'
53 }
54 const entityManager = unsavedChangesStore.createEntityManager('formBuilder', {
55 name,
56 description,
57 published,
58 showTitle,
59 showDescription,
60 storeEntries,
61 integrationSettings,
62 fields,
63 })
64
65 function deselectField(event: Event) {
66 event.stopPropagation()
67 const clickedElement = event.target as HTMLElement
68 const fieldElements = document.querySelectorAll('.ivyforms-form-builder__list-group__item')
69 let clickedInsideField = false
70 fieldElements.forEach((el) => {
71 if (el.contains(clickedElement)) {
72 clickedInsideField = true
73 }
74 })
75 if (!clickedInsideField) {
76 selectedField.value = null
77 }
78 }
79
80 const isSelected = (fieldIndex: number) => {
81 return selectedField.value?.fieldIndex === fieldIndex
82 }
83
84 // Only update the selectedField object and the canonical entry in fields[]
85 const updateSelectedField = (key: string, value: unknown) => {
86 const sel = selectedField.value as Record<string, unknown> | null
87 if (sel) {
88 sel[key] = value
89 }
90 const index = fields.value.findIndex((f) => f.fieldIndex === selectedField.value?.fieldIndex)
91 if (index !== -1) {
92 const target = fields.value[index] as Record<string, unknown>
93 if (target !== sel) {
94 target[key] = value
95 }
96 }
97 }
98
99 function addField(fieldData: Partial<Field> & { type: string }) {
100 fields.value.push(buildNewField(fieldData, counterFields.value, getLabel))
101 updateFieldPositions()
102 }
103
104 const updateFieldPositions = () => {
105 fields.value.forEach((field, fieldIndex) => {
106 field.position = fieldIndex + 1
107 })
108 }
109
110 const increaseCounter = () => {
111 counterFields.value++
112 return counterFields.value
113 }
114
115 /**
116 * Apply beforeSaveFields hooks to allow Pro or other extensions to modify fields
117 * @param fieldsArray - The fields array to process
118 * @returns Processed fields array after all hooks have been applied
119 */
120 const applyBeforeSaveFieldsHooks = (fieldsArray: Field[]): Field[] => {
121 let fieldsToSave = [...fieldsArray]
122
123 if (typeof window !== 'undefined') {
124 const w = window as Window & {
125 IvyForms?: {
126 hooks?: {
127 beforeSaveFields?: Array<(fields: Field[]) => Field[]>
128 }
129 }
130 }
131 const hooks = w.IvyForms?.hooks?.beforeSaveFields || []
132 hooks.forEach((hook) => {
133 try {
134 fieldsToSave = hook(fieldsToSave) as Field[]
135 } catch (error) {
136 console.error(getLabel('error_in_before_save_field_hook'), error)
137 }
138 })
139 }
140
141 return fieldsToSave
142 }
143
144 // Load existing form data if formId is provided
145 const loadForm = async (id: string) => {
146 isFormLoading.value = true
147 const globalState = useGlobalState()
148
149 try {
150 const { data, error, status } = await request(`form/${id}/`, {
151 method: 'GET',
152 headers: {
153 'Content-Type': 'application/json',
154 },
155 })
156 if (!error && status === 200 && data?.data?.data) {
157 formId.value = id
158 isEditing.value = true
159 isEditingSettingGeneral.value = true
160 const responseData = data.data.data
161 name.value = responseData.name || getLabel('blank_form')
162 description.value = responseData.description || ''
163 published.value = responseData.published || false
164 showTitle.value = responseData.showTitle || false
165 showDescription.value = responseData.showDescription || false
166 storeEntries.value = responseData.storeEntries
167
168 // Load integration settings from the response
169 if (responseData.integrationSettings) {
170 integrationSettings.value = responseData.integrationSettings
171 }
172
173 confirmationId.value = responseData.confirmationId || null
174
175 const rawFields = (responseData.fields || []) as Field[]
176 fields.value = normalizeFieldsForBuilder(rawFields)
177
178 if (name.value) {
179 globalState.setPageTitle(name.value)
180 }
181 entityManager.resetUnsavedChanges()
182 counterFields.value = fields.value.length
183 ? Math.max(...fields.value.map((f) => f.fieldIndex))
184 : 0
185 } else {
186 IvyMessage({
187 message: getLabel('failed_to_tab_form'),
188 type: 'error',
189 })
190 console.error(getLabel('failed_to_tab_form'), data?.message)
191 }
192 } catch (error) {
193 IvyMessage({
194 message: getLabel('error_loading_form'),
195 type: 'error',
196 })
197 console.error(getLabel('error_loading_form'), error)
198 } finally {
199 isFormLoading.value = false
200 }
201 }
202
203 const updateForm = async () => {
204 if (!formId.value) return
205
206 // Allow Pro or other extensions to modify fields before saving
207 const fieldsToSave = applyBeforeSaveFieldsHooks(fields.value)
208
209 const denormalizedFields = denormalizeFieldsForApi(fieldsToSave)
210
211 const formData = {
212 name: name.value || getLabel('updated_form'),
213 description: description.value || '',
214 published: published.value || false,
215 showTitle: showTitle.value || false,
216 showDescription: showDescription.value || false,
217 storeEntries: storeEntries.value,
218 integrationSettings: integrationSettings.value,
219 fields: denormalizedFields,
220 }
221
222 try {
223 const { data, error, status } = await request(`form/update/${formId.value}/`, {
224 method: 'POST',
225 headers: {
226 'Content-Type': 'application/json',
227 },
228 data: formData,
229 })
230 if (!error && status === 200) {
231 IvyMessage({
232 message: getLabel('form_updated'),
233 type: 'success',
234 })
235 entityManager.resetUnsavedChanges()
236 } else {
237 IvyMessage({
238 message: getLabel('failed_to_update_form'),
239 type: 'error',
240 })
241 console.error(getLabel('failed_to_update_form'), data?.message)
242 }
243 } catch (error) {
244 IvyMessage({
245 message: getLabel('error_updating_form'),
246 type: 'error',
247 })
248 console.error(getLabel('error_updating_form'), error)
249 }
250 }
251
252 // Save form using WordPress REST API
253 const saveForm = async (options?: { template_id?: string }) => {
254 // Allow Pro or other extensions to modify fields before saving
255 const fieldsToSave = applyBeforeSaveFieldsHooks(fields.value)
256
257 const denormalizedFields = denormalizeFieldsForApi(fieldsToSave)
258 const formData = {
259 name: name.value || getLabel('blank_form'),
260 description: description.value || '',
261 showTitle: showTitle.value || false,
262 published: published.value || false,
263 showDescription: showDescription.value || false,
264 storeEntries: storeEntries.value,
265 integrationSettings: integrationSettings.value,
266 fields: denormalizedFields,
267 ...(options?.template_id && { template_id: options.template_id }),
268 }
269
270 try {
271 const { data, error, status } = await request('form/add/', {
272 method: 'POST',
273 headers: {
274 'Content-Type': 'application/json',
275 },
276 data: formData,
277 })
278 if (!error && status === 200 && data?.data?.data?.id) {
279 const responseData = data.data.data
280 formId.value = responseData.id
281 confirmationId.value = responseData.confirmationId
282 isEditing.value = true
283 isEditingSettingGeneral.value = true
284 await router.push({ hash: `manage/${formId.value}` })
285 IvyMessage({
286 message: getLabel('form_saved'),
287 type: 'success',
288 })
289 entityManager.resetUnsavedChanges()
290 } else {
291 IvyMessage({
292 message: getLabel('failed_to_save_form'),
293 type: 'error',
294 })
295 }
296 } catch (error) {
297 IvyMessage({
298 message: `${getLabel('error_save_form')} ${error}`,
299 type: 'error',
300 })
301 }
302 }
303
304 const saveFormSettings = async () => {
305 // Allow Pro or other extensions to modify fields before saving
306 const fieldsToSave = applyBeforeSaveFieldsHooks(fields.value)
307
308 const formData = {
309 name: name.value || getLabel('blank_form'),
310 description: description.value || '',
311 published: published.value || false,
312 showTitle: showTitle.value || false,
313 showDescription: showDescription.value || false,
314 storeEntries: storeEntries.value,
315 fields: denormalizeFieldsForApi(fieldsToSave),
316 }
317 try {
318 const { data, error, status } = await request('form/add/', {
319 method: 'POST',
320 headers: {
321 'Content-Type': 'application/json',
322 },
323 data: formData,
324 })
325 if (!error && status === 200 && data?.data?.data?.id) {
326 formId.value = data.data.data.id
327 isEditing.value = true
328 isEditingSettingGeneral.value = true
329 await router.push({ hash: `/settings/general/manage/${formId.value}` })
330 IvyMessage({
331 message: getLabel('form_saved'),
332 type: 'success',
333 })
334 entityManager.resetUnsavedChanges()
335 } else {
336 IvyMessage({
337 message: getLabel('failed_to_save_form'),
338 type: 'error',
339 })
340 }
341 } catch (error) {
342 IvyMessage({
343 message: `${getLabel('error_save_form')} ${error}`,
344 type: 'error',
345 })
346 }
347 }
348
349 const moveFieldUp = (fieldIndex: number) => {
350 const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
351 if (currentIndex > 0) {
352 const temp = fields.value[currentIndex]
353 fields.value[currentIndex] = fields.value[currentIndex - 1]
354 fields.value[currentIndex - 1] = temp
355 updateFieldPositions()
356 }
357 }
358
359 const moveFieldDown = (fieldIndex: number) => {
360 const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
361 if (currentIndex < fields.value.length - 1) {
362 const temp = fields.value[currentIndex]
363 fields.value[currentIndex] = fields.value[currentIndex + 1]
364 fields.value[currentIndex + 1] = temp
365 updateFieldPositions()
366 }
367 }
368
369 const deleteField = (fieldIndex: number) => {
370 const i = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
371 if (i !== -1) {
372 fields.value.splice(i, 1)
373 if (selectedField.value?.fieldIndex === fieldIndex) {
374 selectedField.value = null
375 }
376 updateFieldPositions()
377 }
378 }
379
380 const duplicateField = (fieldIndex: number) => {
381 const original = fields.value.find((f) => f.fieldIndex === fieldIndex)
382 if (!original) return
383 const newIndex = increaseCounter()
384 const copy = duplicateFieldForStore(original, newIndex, getLabel)
385 const insertAt = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
386 fields.value.splice(insertAt + 1, 0, copy)
387 updateFieldPositions()
388 }
389
390 const resetForm = () => {
391 formId.value = null
392 isEditing.value = false
393 fields.value = []
394 counterFields.value = 0
395 selectedField.value = null
396 activeTab.value = 'addField'
397 description.value = null
398 name.value = 'Blank form'
399 published.value = true
400 showTitle.value = true
401 showDescription.value = false
402 storeEntries.value = true
403 isEditingSettingGeneral.value = false
404 confirmationId.value = null
405 isFormLoading.value = false
406 }
407
408 const updateFieldOptions = (key: string, value: unknown) => {
409 if (!selectedField.value) return
410 const index = fields.value.findIndex((f) => f.fieldIndex === selectedField.value?.fieldIndex)
411 if (index !== -1) {
412 // Mutate the existing object instead of creating a new one
413 // This keeps the reference intact for selectedField
414 const target = fields.value[index] as Record<string, unknown>
415 target[key] = value
416 }
417 }
418
419 return {
420 formId,
421 fields,
422 isEditing,
423 selectedField,
424 activeTab,
425 counterFields,
426 description,
427 name,
428 published,
429 showTitle,
430 showDescription,
431 storeEntries,
432 integrationSettings,
433 isEditingSettingGeneral,
434 confirmationId,
435 isFormLoading,
436 getCurrentState: entityManager.getCurrentState,
437 saveForm,
438 loadForm,
439 updateForm,
440 increaseCounter,
441 addField,
442 selectField,
443 deselectField,
444 isSelected,
445 updateSelectedField,
446 updateFieldOptions,
447 updateFieldPositions,
448 moveFieldUp,
449 moveFieldDown,
450 deleteField,
451 duplicateField,
452 saveFormSettings,
453 resetForm,
454 }
455 })
456