Add LogicConfirmationModal Component and Integrate Logic Validation (#742)
* Add LogicConfirmationModal Component and Integrate Logic Validation - Introduced LogicConfirmationModal component to display validation errors related to form logic. - Integrated logic validation and cleaning functionalities into FormEditor, prompting users when invalid logic is detected. - Added useLogicValidation and useLogicCleaner composables for error checking and logic correction. - Removed deprecated validatePropertiesLogic function to streamline logic handling. These changes enhance user experience by providing clear feedback on form logic issues and ensuring that invalid logic is addressed before form submission. * Refactor Logic Handling in Form Components - Updated LogicConfirmationModal to improve user feedback by changing the title and icon colors, enhancing clarity on incomplete form logic. - Replaced useLogicValidation and useLogicCleaner with a new useFormLogic composable in FormEditor for streamlined logic validation and cleaning processes. - Removed deprecated useLogicCleaner and useLogicValidation files to simplify the codebase and improve maintainability. These changes enhance the user experience by providing clearer messages regarding form logic issues and ensuring that invalid logic is effectively managed before form submission. --------- Co-authored-by: JhumanJ <julien@nahum.net>
This commit is contained in:
parent
f344764f52
commit
204724e34c
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<Modal
|
||||
:show="isVisible"
|
||||
compact-header
|
||||
icon-color="yellow"
|
||||
@close="$emit('cancel')"
|
||||
>
|
||||
<template #title>
|
||||
Incomplete Form Logic
|
||||
</template>
|
||||
<template #icon>
|
||||
<Icon
|
||||
name="heroicons:exclamation-triangle"
|
||||
class="h-7 w-7"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<p class=" text-gray-700">
|
||||
Some logic rules are incomplete or invalid and will be cleaned up to ensure that the form works correctly.
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<template
|
||||
v-for="error in groupedErrors"
|
||||
:key="error.fieldId"
|
||||
>
|
||||
<div class="rounded-lg bg-yellow-50 p-3">
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
name="heroicons:exclamation-triangle"
|
||||
class="h-5 w-5 text-yellow-400"
|
||||
/>
|
||||
<h4 class="ml-2 text-sm font-medium text-yellow-800">
|
||||
Field: {{ error.fieldName }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-yellow-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li
|
||||
v-for="(message, index) in error.messages"
|
||||
:key="index"
|
||||
>
|
||||
{{ message }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<UButton
|
||||
variant="outline"
|
||||
@click="$emit('cancel')"
|
||||
>
|
||||
Cancel
|
||||
</UButton>
|
||||
<UButton
|
||||
color="blue"
|
||||
@click="$emit('confirm')"
|
||||
>
|
||||
Save Anyway (Remove Invalid Logic)
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
errors: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['cancel', 'confirm'])
|
||||
|
||||
// Group errors by field and format messages
|
||||
const groupedErrors = computed(() => {
|
||||
const errorMap = new Map()
|
||||
|
||||
props.errors.forEach(error => {
|
||||
if (!errorMap.has(error.fieldId)) {
|
||||
errorMap.set(error.fieldId, {
|
||||
fieldId: error.fieldId,
|
||||
fieldName: error.fieldName,
|
||||
messages: []
|
||||
})
|
||||
}
|
||||
|
||||
const group = errorMap.get(error.fieldId)
|
||||
group.messages.push(error.message)
|
||||
})
|
||||
|
||||
return Array.from(errorMap.values())
|
||||
})
|
||||
</script>
|
||||
|
|
@ -84,6 +84,14 @@
|
|||
:validation-error-response="validationErrorResponse"
|
||||
@close="showFormErrorModal = false"
|
||||
/>
|
||||
|
||||
<!-- Logic Confirmation Modal -->
|
||||
<LogicConfirmationModal
|
||||
:is-visible="showLogicConfirmationModal"
|
||||
:errors="logicErrors"
|
||||
@cancel="handleLogicConfirmationCancel"
|
||||
@confirm="handleLogicConfirmationConfirm"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
|
@ -100,13 +108,14 @@ import FormErrorModal from "./form-components/FormErrorModal.vue"
|
|||
import FormFieldsEditor from './FormFieldsEditor.vue'
|
||||
import FormCustomization from "./form-components/FormCustomization.vue"
|
||||
import FormEditorPreview from "./form-components/FormEditorPreview.vue"
|
||||
import { validatePropertiesLogic } from "~/composables/forms/validatePropertiesLogic.js"
|
||||
import { useFormLogic } from "~/composables/forms/useFormLogic.js"
|
||||
import opnformConfig from "~/opnform.config.js"
|
||||
import { captureException } from "@sentry/core"
|
||||
import FormSettings from './form-components/FormSettings.vue'
|
||||
import FormEditorErrorHandler from '~/components/open/forms/components/FormEditorErrorHandler.vue'
|
||||
import { setFormDefaults } from '~/composables/forms/initForm.js'
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
import LogicConfirmationModal from '~/components/forms/LogicConfirmationModal.vue'
|
||||
|
||||
export default {
|
||||
name: "FormEditor",
|
||||
|
|
@ -118,7 +127,8 @@ export default {
|
|||
FormCustomization,
|
||||
FormFieldsEditor,
|
||||
FormErrorModal,
|
||||
FormSettings
|
||||
FormSettings,
|
||||
LogicConfirmationModal
|
||||
},
|
||||
props: {
|
||||
isEdit: {
|
||||
|
|
@ -179,9 +189,11 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
showFormErrorModal: false,
|
||||
showLogicConfirmationModal: false,
|
||||
validationErrorResponse: null,
|
||||
updateFormLoading: false,
|
||||
createdFormSlug: null,
|
||||
logicErrors: [],
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -238,7 +250,24 @@ export default {
|
|||
const defaultedData = setFormDefaults(this.form.data())
|
||||
this.form.fill(defaultedData)
|
||||
|
||||
this.form.properties = validatePropertiesLogic(this.form.properties)
|
||||
// Check for logic errors
|
||||
const { getLogicErrors } = useFormLogic()
|
||||
this.logicErrors = getLogicErrors(this.form.properties)
|
||||
|
||||
if (this.logicErrors.length > 0) {
|
||||
this.showLogicConfirmationModal = true
|
||||
return
|
||||
}
|
||||
|
||||
this.proceedWithSave()
|
||||
},
|
||||
proceedWithSave() {
|
||||
if (this.logicErrors.length > 0) {
|
||||
// Clean invalid logic before saving using the comprehensive validator
|
||||
const { validatePropertiesLogic } = useFormLogic()
|
||||
this.form.properties = validatePropertiesLogic(this.form.properties)
|
||||
}
|
||||
|
||||
if (this.isGuest) {
|
||||
this.saveFormGuest()
|
||||
} else if (this.isEdit) {
|
||||
|
|
@ -247,6 +276,13 @@ export default {
|
|||
this.saveFormCreate()
|
||||
}
|
||||
},
|
||||
handleLogicConfirmationCancel() {
|
||||
this.showLogicConfirmationModal = false
|
||||
},
|
||||
handleLogicConfirmationConfirm() {
|
||||
this.showLogicConfirmationModal = false
|
||||
this.proceedWithSave()
|
||||
},
|
||||
saveFormEdit() {
|
||||
if (this.updateFormLoading) return
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
import FormPropertyLogicRule from '~/lib/forms/FormPropertyLogicRule.js'
|
||||
|
||||
export const useFormLogic = () => {
|
||||
const validateCondition = (condition, properties, fieldId, errors, parentIndex = null) => {
|
||||
if (condition.operatorIdentifier) {
|
||||
// This is a group condition (and/or)
|
||||
if (!Array.isArray(condition.children)) {
|
||||
errors.push({
|
||||
fieldId,
|
||||
error: 'INVALID_CONDITION_GROUP',
|
||||
message: 'Condition group must have children array'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
condition.children.forEach((childCondition, index) => {
|
||||
validateCondition(childCondition, properties, fieldId, errors, parentIndex || index)
|
||||
})
|
||||
} else {
|
||||
// This is a leaf condition
|
||||
const referencedField = properties.find(p => p.id === condition.value?.property_meta?.id)
|
||||
if (!referencedField) {
|
||||
errors.push({
|
||||
fieldId,
|
||||
ruleIndex: parentIndex,
|
||||
error: 'INVALID_FIELD_REFERENCE',
|
||||
referencedFieldId: condition.value?.property_meta?.id,
|
||||
message: `Referenced field ${condition.value?.property_meta?.id} no longer exists`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getLogicErrors = (properties) => {
|
||||
const errors = []
|
||||
|
||||
properties.forEach((field) => {
|
||||
// Skip if field has no logic configured at all
|
||||
if (!field.logic) return
|
||||
|
||||
// Skip if field has default empty logic (null conditions and empty actions)
|
||||
if (field.logic.conditions === null && (!field.logic.actions || field.logic.actions.length === 0)) return
|
||||
|
||||
// Now validate if logic is configured but missing required parts
|
||||
if (!field.logic.conditions) {
|
||||
errors.push({
|
||||
fieldId: field.id,
|
||||
fieldName: field.name,
|
||||
error: 'MISSING_CONDITIONS',
|
||||
message: 'No conditions specified'
|
||||
})
|
||||
}
|
||||
|
||||
if (!field.logic.actions || field.logic.actions.length === 0) {
|
||||
errors.push({
|
||||
fieldId: field.id,
|
||||
fieldName: field.name,
|
||||
error: 'MISSING_ACTIONS',
|
||||
message: 'No actions specified'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate conditions structure and field references
|
||||
if (field.logic.conditions) {
|
||||
validateCondition(field.logic.conditions, properties, field.id, errors)
|
||||
}
|
||||
|
||||
// Apply comprehensive validation from FormPropertyLogicRule
|
||||
const logicRule = new FormPropertyLogicRule(field)
|
||||
if (!logicRule.isValid()) {
|
||||
errors.push({
|
||||
fieldId: field.id,
|
||||
fieldName: field.name,
|
||||
error: 'INVALID_LOGIC_RULE',
|
||||
message: logicRule.isConditionCorrect ?
|
||||
'Invalid action configuration' :
|
||||
'Invalid condition configuration'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const cleanInvalidLogic = (properties) => {
|
||||
// Create a deep copy to avoid mutating the original
|
||||
const cleanedProperties = JSON.parse(JSON.stringify(properties))
|
||||
|
||||
// Get all validation errors
|
||||
const errors = getLogicErrors(cleanedProperties)
|
||||
|
||||
// Group errors by fieldId for efficient processing
|
||||
const errorsByField = errors.reduce((acc, error) => {
|
||||
if (!acc[error.fieldId]) {
|
||||
acc[error.fieldId] = []
|
||||
}
|
||||
acc[error.fieldId].push(error)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Clean invalid logic for each field
|
||||
cleanedProperties.forEach((field) => {
|
||||
const fieldErrors = errorsByField[field.id]
|
||||
|
||||
// If field has any errors, reset its logic
|
||||
if (fieldErrors?.length > 0) {
|
||||
field.logic = {
|
||||
conditions: null,
|
||||
actions: [],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return cleanedProperties
|
||||
}
|
||||
|
||||
// Direct implementation of the previous validatePropertiesLogic function
|
||||
const validatePropertiesLogic = (properties) => {
|
||||
// Create a deep copy to avoid mutating the original
|
||||
const validatedProperties = JSON.parse(JSON.stringify(properties))
|
||||
|
||||
validatedProperties.forEach((field) => {
|
||||
const isValid = new FormPropertyLogicRule(field).isValid()
|
||||
if (!isValid) {
|
||||
field.logic = {
|
||||
conditions: null,
|
||||
actions: [],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return validatedProperties
|
||||
}
|
||||
|
||||
return {
|
||||
validateCondition,
|
||||
getLogicErrors,
|
||||
cleanInvalidLogic,
|
||||
validatePropertiesLogic
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import FormPropertyLogicRule from "~/lib/forms/FormPropertyLogicRule.js"
|
||||
|
||||
export const validatePropertiesLogic = (properties) => {
|
||||
properties.forEach((field) => {
|
||||
const isValid = new FormPropertyLogicRule(field).isValid()
|
||||
if (!isValid) {
|
||||
field.logic = {
|
||||
conditions: null,
|
||||
actions: [],
|
||||
}
|
||||
}
|
||||
})
|
||||
return properties
|
||||
}
|
||||
Loading…
Reference in New Issue