PluginProbe
The Innovative Form Builder – IvyForms / 0.5
The Innovative Form Builder – IvyForms v0.5
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.5, at frontend/src/stores/useFormBuilder.ts

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