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:
Chirag Chhatrala 2025-04-28 22:11:39 +05:30 committed by GitHub
parent f344764f52
commit 204724e34c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 283 additions and 17 deletions

View File

@ -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>

View File

@ -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

141
client/composables/forms/useFormLogic.js vendored Normal file
View File

@ -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
}
}

View File

@ -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
}