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

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