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

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

496 lines 15.2 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 type { Field } from '@/types/field'
4 import { useLabels } from '@/composables/useLabels'
5 import IvyMessage from '@/views/_components/message/ivyMessage.ts'
6 import { useRouter } from 'vue-router'
7 import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore'
8 import { useApiClient } from '@/composables/useApiClient'
9 import { useGlobalState } from '@/stores/useGlobalState'
10
11 export const useFormBuilder = defineStore('formBuilderData', () => {
12 const formId = ref<string | null>(null)
13 const isEditing = ref(false)
14 const fields = ref([])
15 const counterFields = ref<number>(0)
16 const selectedField = ref<Field | null>(null)
17 const activeTab = ref<string>('addField')
18 const description = ref<string | null>(null)
19 const name = ref<string | null>('Blank form')
20 const published = ref(true)
21 const showTitle = ref(false)
22 const showDescription = ref(false)
23 const storeEntries = ref(false)
24 const isEditingSettingGeneral = ref(false)
25 const confirmationId = ref<string | null>(null)
26 const router = useRouter()
27
28 const { getLabel } = useLabels()
29 const { request } = useApiClient()
30
31 // Use unsaved changes store for state management
32 const unsavedChangesStore = useUnsavedChangesStore()
33
34 // Set selected field and update the active tab
35 const selectField = (fieldIndex: number) => {
36 selectedField.value =
37 fields.value.find((field: Field) => field.fieldIndex === fieldIndex) || null
38 activeTab.value = 'options'
39 }
40
41 // Create complete entity manager for form builder
42 const entityManager = unsavedChangesStore.createEntityManager('formBuilder', {
43 name,
44 description,
45 published,
46 showTitle,
47 showDescription,
48 storeEntries,
49 fields,
50 })
51
52 // Deselect field when clicking outside of it
53 function deselectField(event: Event) {
54 event.stopPropagation() // Prevent event from bubbling up to the document
55 const clickedElement = event.target as HTMLElement
56 const fieldElements = document.querySelectorAll('.ivyforms-form-builder__list-group__item')
57
58 let clickedInsideField = false
59 fieldElements.forEach((el) => {
60 if (el.contains(clickedElement)) {
61 clickedInsideField = true
62 }
63 })
64
65 // Only deselect if clicking outside a field
66 if (!clickedInsideField) {
67 selectedField.value = null
68 }
69 }
70
71 // Check if the field is selected based on its index
72 const isSelected = (fieldIndex: number) => {
73 return selectedField.value?.fieldIndex === fieldIndex
74 }
75
76 // Update the selected field and the store
77 const updateSelectedField = (key: string, value: number) => {
78 // Find the index of the field in the store and update it
79 const index = fields.value.findIndex((f) => f.fieldIndex === selectedField.value?.fieldIndex)
80 if (index !== -1) {
81 fields.value[index][key] = value
82 }
83 }
84
85 // Handle required field change with automatic required message clearing
86 const updateRequiredField = (isRequired: boolean) => {
87 const index = fields.value.findIndex((f) => f.fieldIndex === selectedField.value?.fieldIndex)
88 if (index !== -1) {
89 fields.value[index].required = isRequired
90
91 // If required is set to false, clear the required message
92 if (!isRequired) {
93 fields.value[index].requiredMessage = ''
94 }
95 }
96 }
97
98 // Add a new field when an item is dropped or clicked
99 const addField = (fieldData: {
100 id: number
101 fieldIndex: number
102 type: string
103 label: string
104 defaultValue?: string
105 required?: boolean
106 placeholder?: string | number
107 position?: number
108 hideLabel?: boolean
109 readOnly?: boolean
110 description?: string
111 requiredMessage?: string
112 cssClasses?: string
113 }) => {
114 const newPosition = fieldData.position ?? fields.value.length + 1
115
116 fields.value.push({
117 id: fieldData.id,
118 fieldIndex: fieldData.fieldIndex,
119 type: fieldData.type,
120 label: fieldData.label,
121 defaultValue: '',
122 required: fieldData.required || false,
123 placeholder: fieldData.placeholder || '',
124 position: newPosition,
125 hideLabel: fieldData.hideLabel || false,
126 readOnly: fieldData.readOnly || false,
127 description: fieldData.description || '',
128 requiredMessage: fieldData.requiredMessage || '',
129 cssClasses: fieldData.cssClasses || '',
130 })
131
132 updateFieldPositions() // Ensure positions remain sequential
133 }
134
135 // Function to update positions after reordering
136 const updateFieldPositions = () => {
137 fields.value.forEach((field, fieldIndex) => {
138 field.position = fieldIndex + 1
139 })
140 }
141
142 // Increase the counter for new fields
143 const increaseCounter = () => {
144 counterFields.value++
145 return counterFields.value
146 }
147 // TODO add Internalization for all strings in notifications
148 // Load existing form data if formId is provided
149 const loadForm = async (id: string, options?: { normalize?: boolean; silent?: boolean }) => {
150 const globalState = useGlobalState()
151
152 try {
153 const { data, error, status } = await request(`form/${id}/`, {
154 method: 'GET',
155 headers: {
156 'Content-Type': 'application/json',
157 },
158 })
159 if (!error && status === 200 && data?.data?.data) {
160 formId.value = id
161 isEditing.value = true
162 isEditingSettingGeneral.value = true
163 const responseData = data.data.data
164 name.value = responseData.name || getLabel('blank_form')
165 description.value = responseData.description || ''
166 published.value = responseData.published || false
167 showTitle.value = responseData.showTitle || false
168 showDescription.value = responseData.showDescription || false
169 storeEntries.value = responseData.storeEntries
170 confirmationId.value = responseData.confirmationId || null
171
172 // Set page title to the loaded form name
173 if (name.value) {
174 globalState.setPageTitle(name.value)
175 }
176
177 const fieldsArray = responseData.fields
178 fields.value = []
179 counterFields.value = 0
180 fields.value = fieldsArray.map((field: Field, arrayIndex: number) => {
181 const newFieldIndex = ++counterFields.value
182 return {
183 id: field.id,
184 fieldIndex: newFieldIndex, // Use regenerated fieldIndex instead of database value
185 type: field.type,
186 label: field.label,
187 defaultValue: field.defaultValue || '',
188 required: field.required || false,
189 placeholder: field.placeholder || '',
190 position: field.position || arrayIndex + 1,
191 hideLabel: field.hideLabel || false,
192 readOnly: field.readOnly || false,
193 description: field.description || '',
194 requiredMessage: field.requiredMessage || '',
195 cssClasses: field.cssClasses || '',
196 }
197 })
198
199 entityManager.resetUnsavedChanges()
200
201 // Set counterFields to the highest fieldIndex value for future fields
202 counterFields.value = Math.max(...fields.value.map((field: Field) => field.fieldIndex))
203
204 // Optionally persist normalized state silently
205 if (options?.normalize) {
206 await updateForm({ silent: options.silent ?? true })
207 }
208 } else {
209 IvyMessage({
210 message: getLabel('failed_to_tab_form'),
211 type: 'error',
212 })
213 console.error(getLabel('failed_to_tab_form'), data?.message)
214 }
215 } catch (error) {
216 IvyMessage({
217 message: getLabel('error_loading_form'),
218 type: 'error',
219 })
220 console.error(getLabel('error_loading_form'), error)
221 }
222 }
223
224 // Update form after clicking update button
225 const updateForm = async (options?: { silent?: boolean }) => {
226 if (!formId.value) return
227 const formData = {
228 name: name.value || getLabel('updated_form'),
229 description: description.value || '',
230 published: published.value || false,
231 showTitle: showTitle.value || false,
232 showDescription: showDescription.value || false,
233 storeEntries: storeEntries.value,
234 fields: fields.value ? fields.value : [],
235 }
236
237 try {
238 const { data, error, status } = await request(`form/update/${formId.value}/`, {
239 method: 'POST',
240 headers: {
241 'Content-Type': 'application/json',
242 },
243 data: formData,
244 })
245 if (!error && status === 200) {
246 if (!options?.silent) {
247 IvyMessage({
248 message: getLabel('form_updated'),
249 type: 'success',
250 })
251 }
252 entityManager.resetUnsavedChanges()
253 } else {
254 if (!options?.silent) {
255 IvyMessage({
256 message: getLabel('failed_to_update_form'),
257 type: 'error',
258 })
259 }
260 console.error(getLabel('failed_to_update_form'), data?.message)
261 }
262 } catch (error) {
263 if (!options?.silent) {
264 IvyMessage({
265 message: getLabel('error_updating_form'),
266 type: 'error',
267 })
268 }
269 console.error(getLabel('error_updating_form'), error)
270 }
271 }
272
273 // Save form using WordPress REST API
274 const saveForm = async (options?: { template_id?: string }) => {
275 const formData = {
276 name: name.value || getLabel('blank_form'),
277 description: description.value || '',
278 showTitle: showTitle.value || false,
279 published: published.value || false,
280 showDescription: showDescription.value || false,
281 storeEntries: storeEntries.value,
282 fields: fields.value.map((field) => ({
283 id: field.id,
284 fieldIndex: field.fieldIndex,
285 type: field.type,
286 label: field.label,
287 required: field.required,
288 defaultValue: field.defaultValue,
289 placeholder: field.placeholder,
290 position: field.position,
291 hideLabel: field.hideLabel,
292 readOnly: field.readOnly,
293 description: field.description,
294 requiredMessage: field.requiredMessage,
295 cssClasses: field.cssClasses,
296 })),
297 ...(options?.template_id && { template_id: options.template_id }),
298 }
299
300 try {
301 const { data, error, status } = await request('form/add/', {
302 method: 'POST',
303 headers: {
304 'Content-Type': 'application/json',
305 },
306 data: formData,
307 })
308 if (!error && status === 200 && data?.data?.data?.id) {
309 const responseData = data.data.data
310 formId.value = responseData.id
311 confirmationId.value = responseData.confirmationId
312 isEditing.value = true
313 isEditingSettingGeneral.value = true
314 await router.push({ hash: `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 // Save form settings
335 const saveFormSettings = async () => {
336 const formData = {
337 name: name.value || getLabel('blank_form'),
338 description: description.value || '',
339 published: published.value || false,
340 showTitle: showTitle.value || false,
341 showDescription: showDescription.value || false,
342 storeEntries: storeEntries.value,
343 fields: fields.value.map((field) => ({
344 id: field.id,
345 fieldIndex: field.fieldIndex,
346 type: field.type,
347 label: field.label,
348 required: field.required,
349 defaultValue: field.defaultValue,
350 placeholder: field.placeholder,
351 position: field.position,
352 hideLabel: field.hideLabel,
353 readOnly: field.readOnly,
354 description: field.description,
355 requiredMessage: field.requiredMessage,
356 cssClasses: field.cssClasses,
357 })),
358 }
359
360 try {
361 const { data, error, status } = await request('form/add/', {
362 method: 'POST',
363 headers: {
364 'Content-Type': 'application/json',
365 },
366 data: formData,
367 })
368 if (!error && status === 200 && data?.data?.data?.id) {
369 formId.value = data.data.data.id
370 isEditing.value = true
371 isEditingSettingGeneral.value = true
372 await router.push({ hash: `/settings/general/manage/${formId.value}` })
373 IvyMessage({
374 message: getLabel('form_saved'),
375 type: 'success',
376 })
377 entityManager.resetUnsavedChanges()
378 } else {
379 IvyMessage({
380 message: getLabel('failed_to_save_form'),
381 type: 'error',
382 })
383 }
384 } catch (error) {
385 IvyMessage({
386 message: `${getLabel('error_save_form')} ${error}`,
387 type: 'error',
388 })
389 }
390 }
391
392 const moveFieldUp = (fieldIndex: number) => {
393 const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
394 if (currentIndex > 0) {
395 const temp = fields.value[currentIndex]
396 fields.value[currentIndex] = fields.value[currentIndex - 1]
397 fields.value[currentIndex - 1] = temp
398 updateFieldPositions()
399 }
400 }
401
402 const moveFieldDown = (fieldIndex: number) => {
403 const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
404 if (currentIndex < fields.value.length - 1) {
405 const temp = fields.value[currentIndex]
406 fields.value[currentIndex] = fields.value[currentIndex + 1]
407 fields.value[currentIndex + 1] = temp
408 updateFieldPositions()
409 }
410 }
411
412 // Delete a field by index
413 const deleteField = (fieldIndex: number) => {
414 const i = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
415 if (i !== -1) {
416 fields.value.splice(i, 1)
417 if (selectedField.value?.fieldIndex === fieldIndex) {
418 selectedField.value = null
419 }
420 updateFieldPositions()
421 }
422 }
423
424 // Duplicate a field
425 const duplicateField = (fieldIndex: number) => {
426 const original = fields.value.find((f) => f.fieldIndex === fieldIndex)
427 if (!original) return
428
429 const newIndex = increaseCounter()
430
431 const copy = {
432 ...JSON.parse(JSON.stringify(original)),
433 id: 0,
434 fieldIndex: newIndex,
435 label: original.label + ' (copy)',
436 }
437
438 const insertAt = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
439 fields.value.splice(insertAt + 1, 0, copy)
440 updateFieldPositions()
441 }
442
443 // Reset form to default values
444 const resetForm = () => {
445 formId.value = null
446 isEditing.value = false
447 fields.value = []
448 counterFields.value = 0
449 selectedField.value = null
450 activeTab.value = 'addField'
451 description.value = null
452 name.value = 'Blank form'
453 published.value = true
454 showTitle.value = true
455 showDescription.value = false
456 storeEntries.value = true
457 isEditingSettingGeneral.value = false
458 confirmationId.value = null
459 }
460
461 return {
462 formId,
463 fields,
464 isEditing,
465 selectedField,
466 activeTab,
467 counterFields,
468 description,
469 name,
470 published,
471 showTitle,
472 showDescription,
473 storeEntries,
474 isEditingSettingGeneral,
475 confirmationId,
476 getCurrentState: entityManager.getCurrentState,
477 saveForm,
478 loadForm,
479 updateForm,
480 increaseCounter,
481 addField,
482 selectField,
483 deselectField,
484 isSelected,
485 updateSelectedField,
486 updateRequiredField,
487 updateFieldPositions,
488 moveFieldUp,
489 moveFieldDown,
490 deleteField,
491 duplicateField,
492 saveFormSettings,
493 resetForm,
494 }
495 })
496