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

457 lines 13.8 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 // Add a new field when an item is dropped or clicked
86 const addField = (fieldData: {
87 id: number
88 fieldIndex: number
89 type: string
90 label: string
91 defaultValue?: string
92 required?: boolean
93 placeholder?: string | number
94 position?: number
95 }) => {
96 const newPosition = fieldData.position ?? fields.value.length + 1
97
98 fields.value.push({
99 id: fieldData.id,
100 fieldIndex: fieldData.fieldIndex,
101 type: fieldData.type,
102 label: fieldData.label,
103 defaultValue: '',
104 required: fieldData.required || false,
105 placeholder: fieldData.placeholder || '',
106 position: newPosition,
107 })
108
109 updateFieldPositions() // Ensure positions remain sequential
110 }
111
112 // Function to update positions after reordering
113 const updateFieldPositions = () => {
114 fields.value.forEach((field, fieldIndex) => {
115 field.position = fieldIndex + 1
116 })
117 }
118
119 // Increase the counter for new fields
120 const increaseCounter = () => {
121 counterFields.value++
122 return counterFields.value
123 }
124 // TODO add Internalization for all strings in notifications
125 // Load existing form data if formId is provided
126 const loadForm = async (id: string, options?: { normalize?: boolean; silent?: boolean }) => {
127 const globalState = useGlobalState()
128
129 try {
130 const { data, error, status } = await request(`form/${id}/`, {
131 method: 'GET',
132 headers: {
133 'Content-Type': 'application/json',
134 },
135 })
136 if (!error && status === 200 && data?.data?.data) {
137 formId.value = id
138 isEditing.value = true
139 isEditingSettingGeneral.value = true
140 const responseData = data.data.data
141 name.value = responseData.name || getLabel('blank_form')
142 description.value = responseData.description || ''
143 published.value = responseData.published || false
144 showTitle.value = responseData.showTitle || false
145 showDescription.value = responseData.showDescription || false
146 storeEntries.value = responseData.storeEntries
147 confirmationId.value = responseData.confirmationId || null
148
149 // Set page title to the loaded form name
150 if (name.value) {
151 globalState.setPageTitle(name.value)
152 }
153
154 const fieldsArray = responseData.fields
155 fields.value = []
156 counterFields.value = 0
157 fields.value = fieldsArray.map((field: Field, arrayIndex: number) => {
158 const newFieldIndex = ++counterFields.value
159 return {
160 id: field.id,
161 fieldIndex: newFieldIndex, // Use regenerated fieldIndex instead of database value
162 type: field.type,
163 label: field.label,
164 defaultValue: field.defaultValue || '',
165 required: field.required || false,
166 placeholder: field.placeholder || '',
167 position: field.position || arrayIndex + 1,
168 }
169 })
170
171 entityManager.resetUnsavedChanges()
172
173 // Set counterFields to the highest fieldIndex value for future fields
174 counterFields.value = Math.max(...fields.value.map((field: Field) => field.fieldIndex))
175
176 // Optionally persist normalized state silently
177 if (options?.normalize) {
178 await updateForm({ silent: options.silent ?? true })
179 }
180 } else {
181 IvyMessage({
182 message: getLabel('failed_to_tab_form'),
183 type: 'error',
184 })
185 console.error(getLabel('failed_to_tab_form'), data?.message)
186 }
187 } catch (error) {
188 IvyMessage({
189 message: getLabel('error_loading_form'),
190 type: 'error',
191 })
192 console.error(getLabel('error_loading_form'), error)
193 }
194 }
195
196 // Update form after clicking update button
197 const updateForm = async (options?: { silent?: boolean }) => {
198 if (!formId.value) return
199 const formData = {
200 name: name.value || getLabel('updated_form'),
201 description: description.value || '',
202 published: published.value || false,
203 showTitle: showTitle.value || false,
204 showDescription: showDescription.value || false,
205 storeEntries: storeEntries.value,
206 fields: fields.value ? fields.value : [],
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 if (!options?.silent) {
219 IvyMessage({
220 message: getLabel('form_updated'),
221 type: 'success',
222 })
223 }
224 entityManager.resetUnsavedChanges()
225 } else {
226 if (!options?.silent) {
227 IvyMessage({
228 message: getLabel('failed_to_update_form'),
229 type: 'error',
230 })
231 }
232 console.error(getLabel('failed_to_update_form'), data?.message)
233 }
234 } catch (error) {
235 if (!options?.silent) {
236 IvyMessage({
237 message: getLabel('error_updating_form'),
238 type: 'error',
239 })
240 }
241 console.error(getLabel('error_updating_form'), error)
242 }
243 }
244
245 // Save form using WordPress REST API
246 const saveForm = async (options?: { template_id?: string }) => {
247 const formData = {
248 name: name.value || getLabel('blank_form'),
249 description: description.value || '',
250 showTitle: showTitle.value || false,
251 published: published.value || false,
252 showDescription: showDescription.value || false,
253 storeEntries: storeEntries.value,
254 fields: fields.value.map((field) => ({
255 id: field.id,
256 fieldIndex: field.fieldIndex,
257 type: field.type,
258 label: field.label,
259 required: field.required,
260 defaultValue: field.defaultValue,
261 placeholder: field.placeholder,
262 position: field.position,
263 })),
264 ...(options?.template_id && { template_id: options.template_id }),
265 }
266
267 try {
268 const { data, error, status } = await request('form/add/', {
269 method: 'POST',
270 headers: {
271 'Content-Type': 'application/json',
272 },
273 data: formData,
274 })
275 if (!error && status === 200 && data?.data?.data?.id) {
276 const responseData = data.data.data
277 formId.value = responseData.id
278 confirmationId.value = responseData.confirmationId
279 isEditing.value = true
280 isEditingSettingGeneral.value = true
281 await router.push({ hash: `manage/${formId.value}` })
282 IvyMessage({
283 message: getLabel('form_saved'),
284 type: 'success',
285 })
286 entityManager.resetUnsavedChanges()
287 } else {
288 IvyMessage({
289 message: getLabel('failed_to_save_form'),
290 type: 'error',
291 })
292 }
293 } catch (error) {
294 IvyMessage({
295 message: `${getLabel('error_save_form')} ${error}`,
296 type: 'error',
297 })
298 }
299 }
300
301 // Save form settings
302 const saveFormSettings = async () => {
303 const formData = {
304 name: name.value || getLabel('blank_form'),
305 description: description.value || '',
306 published: published.value || false,
307 showTitle: showTitle.value || false,
308 showDescription: showDescription.value || false,
309 storeEntries: storeEntries.value,
310 fields: fields.value.map((field) => ({
311 id: field.id,
312 fieldIndex: field.fieldIndex,
313 type: field.type,
314 label: field.label,
315 required: field.required,
316 defaultValue: field.defaultValue,
317 placeholder: field.placeholder,
318 position: field.position,
319 })),
320 }
321
322 try {
323 const { data, error, status } = await request('form/add/', {
324 method: 'POST',
325 headers: {
326 'Content-Type': 'application/json',
327 },
328 data: formData,
329 })
330 if (!error && status === 200 && data?.data?.data?.id) {
331 formId.value = data.data.data.id
332 isEditing.value = true
333 isEditingSettingGeneral.value = true
334 await router.push({ hash: `/settings/general/manage/${formId.value}` })
335 IvyMessage({
336 message: getLabel('form_saved'),
337 type: 'success',
338 })
339 entityManager.resetUnsavedChanges()
340 } else {
341 IvyMessage({
342 message: getLabel('failed_to_save_form'),
343 type: 'error',
344 })
345 }
346 } catch (error) {
347 IvyMessage({
348 message: `${getLabel('error_save_form')} ${error}`,
349 type: 'error',
350 })
351 }
352 }
353
354 const moveFieldUp = (fieldIndex: number) => {
355 const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
356 if (currentIndex > 0) {
357 const temp = fields.value[currentIndex]
358 fields.value[currentIndex] = fields.value[currentIndex - 1]
359 fields.value[currentIndex - 1] = temp
360 updateFieldPositions()
361 }
362 }
363
364 const moveFieldDown = (fieldIndex: number) => {
365 const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
366 if (currentIndex < fields.value.length - 1) {
367 const temp = fields.value[currentIndex]
368 fields.value[currentIndex] = fields.value[currentIndex + 1]
369 fields.value[currentIndex + 1] = temp
370 updateFieldPositions()
371 }
372 }
373
374 // Delete a field by index
375 const deleteField = (fieldIndex: number) => {
376 const i = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
377 if (i !== -1) {
378 fields.value.splice(i, 1)
379 if (selectedField.value?.fieldIndex === fieldIndex) {
380 selectedField.value = null
381 }
382 updateFieldPositions()
383 }
384 }
385
386 // Duplicate a field
387 const duplicateField = (fieldIndex: number) => {
388 const original = fields.value.find((f) => f.fieldIndex === fieldIndex)
389 if (!original) return
390
391 const newIndex = increaseCounter()
392
393 const copy = {
394 ...JSON.parse(JSON.stringify(original)),
395 id: 0,
396 fieldIndex: newIndex,
397 label: original.label + ' (copy)',
398 }
399
400 const insertAt = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
401 fields.value.splice(insertAt + 1, 0, copy)
402 updateFieldPositions()
403 }
404
405 // Reset form to default values
406 const resetForm = () => {
407 formId.value = null
408 isEditing.value = false
409 fields.value = []
410 counterFields.value = 0
411 selectedField.value = null
412 activeTab.value = 'addField'
413 description.value = null
414 name.value = 'Blank form'
415 published.value = true
416 showTitle.value = true
417 showDescription.value = false
418 storeEntries.value = true
419 isEditingSettingGeneral.value = false
420 confirmationId.value = null
421 }
422
423 return {
424 formId,
425 fields,
426 isEditing,
427 selectedField,
428 activeTab,
429 counterFields,
430 description,
431 name,
432 published,
433 showTitle,
434 showDescription,
435 storeEntries,
436 isEditingSettingGeneral,
437 confirmationId,
438 getCurrentState: entityManager.getCurrentState,
439 saveForm,
440 loadForm,
441 updateForm,
442 increaseCounter,
443 addField,
444 selectField,
445 deselectField,
446 isSelected,
447 updateSelectedField,
448 updateFieldPositions,
449 moveFieldUp,
450 moveFieldDown,
451 deleteField,
452 duplicateField,
453 saveFormSettings,
454 resetForm,
455 }
456 })
457