corrad-bp/components/process-flow/ChildFormRenderer.vue
Md Afiq Iskandar 7df119417a Add Page Wrapper Node and Enhance Process Flow Management
- Introduced a new 'Page Wrapper' node type to allow multiple components (forms, HTML, tables) to be rendered as a single page, enhancing the flexibility of the process builder.
- Implemented child node management within the Page Wrapper, including adding, updating, and removing child nodes through the process store.
- Enhanced the ProcessFlowCanvas to support the new Page Wrapper node, including event handling for child node interactions and configuration.
- Updated the workflow page to handle submissions and interactions for Page Wrapper nodes, ensuring seamless integration with existing process flows.
- Improved documentation in CLAUDE.md to provide guidance on the new Page Wrapper architecture and its components.
- Refactored related components and store logic to accommodate the new functionality, ensuring a cohesive user experience across the application.
2025-07-31 10:28:34 +08:00

367 lines
10 KiB
Vue

<template>
<div class="child-form-renderer">
<!-- Form Header -->
<div v-if="formTitle || formDescription" class="form-header mb-4">
<h3 v-if="formTitle" class="text-lg font-semibold text-gray-900 mb-2">
{{ formTitle }}
</h3>
<p v-if="formDescription" class="text-sm text-gray-600">
{{ formDescription }}
</p>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="loading-state flex justify-center py-8">
<div class="text-center">
<Icon name="material-symbols:progress-activity" class="w-6 h-6 animate-spin text-blue-500 mx-auto mb-2" />
<p class="text-gray-500 text-sm">Loading form...</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="loadError" class="error-state bg-red-50 border border-red-200 rounded-lg p-4">
<div class="flex items-center">
<Icon name="material-symbols:error" class="w-5 h-5 text-red-600 mr-2" />
<span class="text-red-800 text-sm">{{ loadError }}</span>
</div>
</div>
<!-- Form Content -->
<div v-else-if="formComponents && formComponents.length > 0" class="form-content">
<!-- FormKit Form -->
<FormKit
ref="formRef"
type="form"
:value="localFormData"
@submit="handleFormSubmit"
@input="handleFormChange"
:actions="false"
:disabled="isSubmitting"
class="space-y-4"
>
<!-- Dynamic Components -->
<ComponentPreview
v-for="(component, index) in formComponents"
:key="`${component.props?.name || index}-${formDataKey}`"
:component="component"
:formData="localFormData"
:isPreview="false"
:fieldStates="fieldStates"
@updateFormData="handleComponentUpdate"
/>
<!-- Submit Button (if not handled by parent) -->
<div v-if="showSubmitButton" class="form-actions mt-6">
<button
type="button"
@click="submitForm"
:disabled="isSubmitting || hasValidationErrors"
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Icon v-if="isSubmitting" name="material-symbols:progress-activity" class="w-4 h-4 mr-2 animate-spin" />
{{ submitButtonText }}
</button>
</div>
</FormKit>
<!-- Conditional Logic Engine -->
<ConditionalLogicEngine
v-if="conditionalLogicScript"
:script="conditionalLogicScript"
:formData="localFormData"
:processVariables="processVariables"
@fieldStateChange="handleFieldStateChange"
/>
</div>
<!-- Empty Form State -->
<div v-else class="empty-form-state text-center py-8">
<Icon name="material-symbols:description" class="w-12 h-12 text-gray-300 mx-auto mb-2" />
<p class="text-gray-500 text-sm">No form configured for this component</p>
<p class="text-gray-400 text-xs mt-1">Component ID: {{ childNode.nodeId || 'Not set' }}</p>
</div>
<!-- Validation Errors -->
<div v-if="validationErrors && Object.keys(validationErrors).length > 0" class="validation-errors mt-4">
<div class="bg-red-50 border border-red-200 rounded-lg p-3">
<div class="flex items-start">
<Icon name="material-symbols:error" class="w-5 h-5 text-red-600 mr-2 mt-0.5 flex-shrink-0" />
<div class="flex-1">
<h4 class="text-sm font-medium text-red-800 mb-1">Please fix the following errors:</h4>
<ul class="text-sm text-red-700 space-y-1">
<li v-for="(error, field) in validationErrors" :key="field">
<strong>{{ getFieldLabel(field) }}:</strong> {{ error }}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { FormKit } from '@formkit/vue'
import { Icon } from '#components'
import ComponentPreview from '~/components/ComponentPreview.vue'
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue'
const props = defineProps({
childNode: {
type: Object,
required: true
},
processVariables: {
type: Object,
default: () => ({})
},
formData: {
type: Object,
default: () => ({})
},
showSubmitButton: {
type: Boolean,
default: false
},
submitButtonText: {
type: String,
default: 'Submit'
}
})
const emit = defineEmits(['form-submit', 'form-change', 'validation-error'])
// Reactive data
const isLoading = ref(false)
const isSubmitting = ref(false)
const loadError = ref(null)
const formComponents = ref([])
const localFormData = ref({})
const fieldStates = ref({})
const validationErrors = ref({})
const conditionalLogicScript = ref('')
const formDataKey = ref(0)
const formRef = ref(null)
// Computed properties
const formTitle = computed(() => {
return props.childNode.formTitle || props.childNode.title || null
})
const formDescription = computed(() => {
return props.childNode.formDescription || props.childNode.description || null
})
const hasValidationErrors = computed(() => {
return Object.keys(validationErrors.value).length > 0
})
// Methods
const loadFormData = async () => {
if (!props.childNode.nodeId) {
console.warn('Child node has no nodeId specified')
return
}
isLoading.value = true
loadError.value = null
try {
console.log('Loading form data for child node:', props.childNode.nodeId)
const response = await $fetch(`/api/forms/${props.childNode.nodeId}`)
if (response.success) {
const form = response.form
formComponents.value = form.formComponents || []
conditionalLogicScript.value = form.customScript || ''
console.log('Form data loaded for child:', form.formName, formComponents.value.length, 'components')
} else {
throw new Error(response.message || 'Failed to load form')
}
} catch (error) {
console.error('Error loading form data:', error)
loadError.value = error.message || 'Failed to load form data'
} finally {
isLoading.value = false
}
}
const applyInputMappings = () => {
if (!props.childNode.variableMapping || !props.childNode.variableMapping.inputs) {
return
}
const mappedData = { ...localFormData.value }
props.childNode.variableMapping.inputs.forEach(mapping => {
if (mapping.processVar && mapping.childVar) {
const processValue = props.processVariables[mapping.processVar]
if (processValue !== undefined) {
mappedData[mapping.childVar] = processValue
console.log(`Mapped process variable ${mapping.processVar} (${processValue}) to form field ${mapping.childVar}`)
}
}
})
localFormData.value = mappedData
formDataKey.value += 1 // Trigger re-render
}
const handleFormChange = (formData) => {
localFormData.value = { ...formData }
emit('form-change', formData)
}
const handleComponentUpdate = (updatedData) => {
localFormData.value = { ...localFormData.value, ...updatedData }
emit('form-change', localFormData.value)
}
const handleFormSubmit = (formData) => {
console.log('Child form submit:', props.childNode.id, formData)
// Validate form data
const errors = validateFormData(formData)
if (Object.keys(errors).length > 0) {
validationErrors.value = errors
emit('validation-error', errors)
return
}
// Clear validation errors
validationErrors.value = {}
emit('validation-error', {})
// Emit submit event
emit('form-submit', formData)
}
const submitForm = () => {
if (formRef.value && formRef.value.node && typeof formRef.value.node.submit === 'function') {
formRef.value.node.submit()
}
}
const validateFormData = (formData) => {
const errors = {}
// Basic validation - can be enhanced based on form component validation rules
formComponents.value.forEach(component => {
const field = component.props
if (!field) return
const fieldName = field.name
const fieldValue = formData[fieldName]
// Required field validation
if (field.validation && field.validation.includes('required')) {
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
errors[fieldName] = 'This field is required'
}
}
// Email validation
if (field.validation && field.validation.includes('email') && fieldValue) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(fieldValue)) {
errors[fieldName] = 'Please enter a valid email address'
}
}
// Number validation
if (field.type === 'number' && fieldValue !== undefined && fieldValue !== '') {
if (isNaN(fieldValue)) {
errors[fieldName] = 'Please enter a valid number'
}
}
})
return errors
}
const getFieldLabel = (fieldName) => {
const component = formComponents.value.find(c => c.props?.name === fieldName)
return component?.props?.label || fieldName
}
const handleFieldStateChange = (fieldName, state) => {
fieldStates.value[fieldName] = state
}
// Watch for changes in child node configuration
watch(() => props.childNode.nodeId, (newNodeId) => {
if (newNodeId) {
loadFormData()
}
}, { immediate: true })
// Watch for changes in form data prop
watch(() => props.formData, (newFormData) => {
localFormData.value = { ...newFormData }
formDataKey.value += 1
}, { deep: true, immediate: true })
// Watch for process variable changes and apply input mappings
watch(() => props.processVariables, () => {
applyInputMappings()
}, { deep: true, immediate: true })
// Apply input mappings when form loads
watch(() => formComponents.value.length, () => {
if (formComponents.value.length > 0) {
nextTick(() => {
applyInputMappings()
})
}
})
</script>
<style scoped>
.child-form-renderer {
width: 100%;
min-height: 200px;
}
.form-header {
border-bottom: 1px solid #f3f4f6;
padding-bottom: 0.75rem;
}
.loading-state,
.empty-form-state {
min-height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.form-content {
width: 100%;
}
.form-actions {
border-top: 1px solid #f3f4f6;
padding-top: 1rem;
}
.validation-errors {
margin-top: 1rem;
}
/* FormKit styling overrides */
:deep(.formkit-form) {
width: 100%;
}
:deep(.formkit-input) {
width: 100%;
}
:deep(.formkit-wrapper) {
margin-bottom: 1rem;
}
</style>