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

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