import { defineStore } from 'pinia' import { ref } from 'vue' import type { Field } from '@/types/field' import { useLabels } from '@/composables/useLabels' import IvyMessage from '@/views/_components/message/ivyMessage.ts' import { useRouter } from 'vue-router' import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore' import { useApiClient } from '@/composables/useApiClient' export const useFormBuilder = defineStore('formBuilderData', () => { const formId = ref(null) const isEditing = ref(false) const fields = ref([]) const counterFields = ref(0) const selectedField = ref(null) const activeTab = ref('addField') const description = ref(null) const name = ref('Blank form') const published = ref(true) const showTitle = ref(true) const showDescription = ref(false) const isEditingSettingGeneral = ref(false) const confirmationId = ref(null) const router = useRouter() const { getLabel } = useLabels() const { request } = useApiClient() // Use unsaved changes store for state management const unsavedChangesStore = useUnsavedChangesStore() // Set selected field and update the active tab const selectField = (fieldIndex: number) => { selectedField.value = fields.value.find((field: Field) => field.fieldIndex === fieldIndex) || null activeTab.value = 'options' } // Create complete entity manager for form builder const entityManager = unsavedChangesStore.createEntityManager('formBuilder', { name, description, published, showTitle, showDescription, fields, }) // Deselect field when clicking outside of it function deselectField(event: Event) { event.stopPropagation() // Prevent event from bubbling up to the document const clickedElement = event.target as HTMLElement const fieldElements = document.querySelectorAll('.ivyforms-form-builder__list-group__item') let clickedInsideField = false fieldElements.forEach((el) => { if (el.contains(clickedElement)) { clickedInsideField = true } }) // Only deselect if clicking outside a field if (!clickedInsideField) { selectedField.value = null } } // Check if the field is selected based on its index const isSelected = (fieldIndex: number) => { return selectedField.value?.fieldIndex === fieldIndex } // Update the selected field and the store const updateSelectedField = (key: string, value: number) => { // Find the index of the field in the store and update it const index = fields.value.findIndex((f) => f.fieldIndex === selectedField.value?.fieldIndex) if (index !== -1) { fields.value[index][key] = value } } // Add a new field when an item is dropped or clicked const addField = (fieldData: { id: number fieldIndex: number type: string label: string defaultValue?: string required?: boolean placeholder?: string | number position?: number }) => { const newPosition = fieldData.position ?? fields.value.length + 1 fields.value.push({ id: fieldData.id, fieldIndex: fieldData.fieldIndex, type: fieldData.type, label: fieldData.label, defaultValue: '', required: fieldData.required || false, placeholder: fieldData.placeholder || '', position: newPosition, }) updateFieldPositions() // Ensure positions remain sequential } // Function to update positions after reordering const updateFieldPositions = () => { fields.value.forEach((field, fieldIndex) => { field.position = fieldIndex + 1 }) } // Increase the counter for new fields const increaseCounter = () => { counterFields.value++ return counterFields.value } // TODO add Internalization for all strings in notifications // Load existing form data if formId is provided const loadForm = async (id: string) => { try { const { data, error, status } = await request(`form/${id}/`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }) if (!error && status === 200 && data?.data?.data) { formId.value = id isEditing.value = true isEditingSettingGeneral.value = true const responseData = data.data.data name.value = responseData.name || getLabel('blank_form') description.value = responseData.description || '' published.value = responseData.published || false showTitle.value = responseData.showTitle || false showDescription.value = responseData.showDescription || false confirmationId.value = responseData.confirmationId || null const fieldsArray = responseData.fields fields.value = fieldsArray.map((field: Field, fieldIndex: number) => ({ id: field.id, fieldIndex: field.fieldIndex, type: field.type, label: field.label, defaultValue: field.defaultValue || '', required: field.required || false, placeholder: field.placeholder || '', position: field.position || fieldIndex + 1, })) entityManager.resetUnsavedChanges() counterFields.value = 0 if (fieldsArray.length) counterFields.value = Math.max(...fieldsArray.map((field: Field) => field.fieldIndex)) } else { IvyMessage({ message: getLabel('failed_to_tab_form'), type: 'error', }) console.error(getLabel('failed_to_tab_form'), data?.message) } } catch (error) { IvyMessage({ message: getLabel('error_loading_form'), type: 'error', }) console.error(getLabel('error_loading_form'), error) } } // Update form after clicking update button const updateForm = async () => { if (!formId.value) return const formData = { name: name.value || getLabel('updated_form'), description: description.value || '', published: published.value || false, showTitle: showTitle.value || false, showDescription: showDescription.value || false, fields: fields.value ? fields.value : [], } try { const { data, error, status } = await request(`form/update/${formId.value}/`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, data: formData, }) if (!error && status === 200) { IvyMessage({ message: getLabel('form_updated'), type: 'success', }) entityManager.resetUnsavedChanges() } else { IvyMessage({ message: getLabel('failed_to_update_form'), type: 'error', }) console.error(getLabel('failed_to_update_form'), data?.message) } } catch (error) { IvyMessage({ message: getLabel('error_updating_form'), type: 'error', }) console.error(getLabel('error_updating_form'), error) } } // Save form using WordPress REST API const saveForm = async () => { const formData = { name: name.value || getLabel('blank_form'), description: description.value || '', showTitle: showTitle.value || false, published: published.value || false, showDescription: showDescription.value || false, fields: fields.value.map((field) => ({ id: field.id, fieldIndex: field.fieldIndex, type: field.type, label: field.label, required: field.required, defaultValue: field.defaultValue, placeholder: field.placeholder, position: field.position, })), } try { const { data, error, status } = await request('form/add/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, data: formData, }) if (!error && status === 200 && data?.data?.data?.id) { const responseData = data.data.data formId.value = responseData.id confirmationId.value = responseData.confirmationId isEditing.value = true isEditingSettingGeneral.value = true await router.push({ hash: `manage/${formId.value}` }) IvyMessage({ message: getLabel('form_saved'), type: 'success', }) entityManager.resetUnsavedChanges() } else { IvyMessage({ message: getLabel('failed_to_save_form'), type: 'error', }) } } catch (error) { IvyMessage({ message: `${getLabel('error_save_form')} ${error}`, type: 'error', }) } } // Save form settings const saveFormSettings = async () => { const formData = { name: name.value || getLabel('blank_form'), description: description.value || '', published: published.value || false, showTitle: showTitle.value || false, showDescription: showDescription.value || false, fields: fields.value.map((field) => ({ id: field.id, fieldIndex: field.fieldIndex, type: field.type, label: field.label, required: field.required, defaultValue: field.defaultValue, placeholder: field.placeholder, position: field.position, })), } try { const { data, error, status } = await request('form/add/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, data: formData, }) if (!error && status === 200 && data?.data?.data?.id) { formId.value = data.data.data.id isEditing.value = true isEditingSettingGeneral.value = true await router.push({ hash: `/settings/general/manage/${formId.value}` }) IvyMessage({ message: getLabel('form_saved'), type: 'success', }) entityManager.resetUnsavedChanges() } else { IvyMessage({ message: getLabel('failed_to_save_form'), type: 'error', }) } } catch (error) { IvyMessage({ message: `${getLabel('error_save_form')} ${error}`, type: 'error', }) } } const moveFieldUp = (fieldIndex: number) => { const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex) if (currentIndex > 0) { const temp = fields.value[currentIndex] fields.value[currentIndex] = fields.value[currentIndex - 1] fields.value[currentIndex - 1] = temp updateFieldPositions() } } const moveFieldDown = (fieldIndex: number) => { const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex) if (currentIndex < fields.value.length - 1) { const temp = fields.value[currentIndex] fields.value[currentIndex] = fields.value[currentIndex + 1] fields.value[currentIndex + 1] = temp updateFieldPositions() } } // Delete a field by index const deleteField = (fieldIndex: number) => { const i = fields.value.findIndex((f) => f.fieldIndex === fieldIndex) if (i !== -1) { fields.value.splice(i, 1) if (selectedField.value?.fieldIndex === fieldIndex) { selectedField.value = null } updateFieldPositions() } } // Duplicate a field const duplicateField = (fieldIndex: number) => { const original = fields.value.find((f) => f.fieldIndex === fieldIndex) if (!original) return const newIndex = increaseCounter() const copy = { ...JSON.parse(JSON.stringify(original)), id: 0, fieldIndex: newIndex, label: original.label + ' (copy)', } const insertAt = fields.value.findIndex((f) => f.fieldIndex === fieldIndex) fields.value.splice(insertAt + 1, 0, copy) updateFieldPositions() } // Reset form to default values const resetForm = () => { formId.value = null isEditing.value = false fields.value = [] counterFields.value = 0 selectedField.value = null activeTab.value = 'addField' description.value = null name.value = 'Blank form' published.value = true showTitle.value = true showDescription.value = false isEditingSettingGeneral.value = false confirmationId.value = null } return { formId, fields, isEditing, selectedField, activeTab, counterFields, description, name, published, showTitle, showDescription, isEditingSettingGeneral, confirmationId, getCurrentState: entityManager.getCurrentState, saveForm, loadForm, updateForm, increaseCounter, addField, selectField, deselectField, isSelected, updateSelectedField, updateFieldPositions, moveFieldUp, moveFieldDown, deleteField, duplicateField, saveFormSettings, resetForm, } })