corrad-bp/components/process-flow/PageWrapperConfigurationModal.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

226 lines
7.2 KiB
Vue

<template>
<RsModal
v-model="showModal"
title="Page Wrapper Configuration"
size="xl"
position="center"
:okCallback="saveAndClose"
okTitle="Save"
:cancelCallback="closeModal"
>
<template #body>
<div class="mb-6">
<div class="flex items-start">
<div class="mr-4 text-gray-500 flex-shrink-0 mt-1">
<Icon name="material-symbols:web-asset" class="text-2xl" />
</div>
<div>
<h3 class="text-lg font-semibold mb-1">Configure Page Wrapper</h3>
<p class="text-sm text-gray-600">
Create a multi-component page by adding forms, HTML content, and tables. Configure layout,
conditional logic, and variable mappings for each child component.
</p>
</div>
</div>
</div>
<!-- Main configuration area -->
<PageWrapperConfiguration
ref="pageWrapperConfigRef"
:nodeData="localNodeData"
:availableVariables="availableVariables"
@update="handleUpdate"
/>
<!-- Quick Reference Guide -->
<div class="mt-6 bg-blue-50 p-4 rounded-md border border-blue-100">
<h4 class="font-medium text-blue-700 mb-2 flex items-center">
<Icon name="material-symbols:info-outline" class="mr-1" />
Quick Reference Guide
</h4>
<div class="text-sm text-blue-700">
<ul class="list-disc list-inside space-y-1">
<li><strong>Grid Layout:</strong> Organize components in rows and columns</li>
<li><strong>Flex Layout:</strong> Responsive horizontal or vertical arrangement</li>
<li><strong>Stacked Layout:</strong> Components arranged vertically</li>
<li><strong>Conditional Logic:</strong> Show/hide components based on process variables</li>
<li><strong>Variable Mapping:</strong> Pass data between process and child components</li>
</ul>
</div>
</div>
<!-- Layout Preview -->
<div class="mt-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<h4 class="font-medium text-gray-700 mb-2 flex items-center">
<Icon name="material-symbols:preview" class="mr-1" />
Layout Preview
</h4>
<div class="bg-white border border-gray-200 rounded p-3 min-h-24">
<div v-if="localNodeData.childNodes && localNodeData.childNodes.length > 0">
<!-- Grid Preview -->
<div
v-if="localNodeData.layout === 'grid'"
class="grid gap-2"
:style="{
gridTemplateColumns: `repeat(${localNodeData.columns || 2}, 1fr)`,
gap: previewGap
}"
>
<div
v-for="(child, index) in localNodeData.childNodes"
:key="child.id || index"
class="bg-blue-100 border border-blue-200 rounded p-2 text-xs text-blue-700 text-center"
:style="{
gridRow: child.position?.row || 1,
gridColumn: child.position?.col || 1
}"
>
<Icon :name="getChildNodeIcon(child.type)" class="w-3 h-3 mx-auto mb-1" />
{{ getChildNodeTypeName(child.type) }}
</div>
</div>
<!-- Flex Preview -->
<div
v-else-if="localNodeData.layout === 'flex'"
class="flex flex-wrap"
:style="{ gap: previewGap }"
>
<div
v-for="(child, index) in localNodeData.childNodes"
:key="child.id || index"
class="bg-blue-100 border border-blue-200 rounded p-2 text-xs text-blue-700 text-center flex-1 min-w-32"
>
<Icon :name="getChildNodeIcon(child.type)" class="w-3 h-3 mx-auto mb-1" />
{{ getChildNodeTypeName(child.type) }}
</div>
</div>
<!-- Stacked Preview -->
<div
v-else
class="space-y-2"
:style="{ gap: previewGap }"
>
<div
v-for="(child, index) in localNodeData.childNodes"
:key="child.id || index"
class="bg-blue-100 border border-blue-200 rounded p-2 text-xs text-blue-700 text-center"
>
<Icon :name="getChildNodeIcon(child.type)" class="w-3 h-3 mx-auto mb-1" />
{{ getChildNodeTypeName(child.type) }}
</div>
</div>
</div>
<div v-else class="text-center text-gray-400 py-4">
<Icon name="material-symbols:dashboard" class="w-8 h-8 mx-auto mb-2" />
<p class="text-sm">No child components to preview</p>
</div>
</div>
</div>
</template>
</RsModal>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import PageWrapperConfiguration from './PageWrapperConfiguration.vue';
import { Icon } from '#components';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
nodeData: {
type: Object,
required: true
},
availableVariables: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue', 'update']);
const showModal = ref(props.modelValue);
const localNodeData = ref({ ...props.nodeData });
const pageWrapperConfigRef = ref(null);
// Watch for changes to modelValue prop to sync modal visibility
watch(() => props.modelValue, (value) => {
showModal.value = value;
});
// Watch for changes to showModal to emit update:modelValue
watch(() => showModal.value, (value) => {
emit('update:modelValue', value);
});
// Watch for changes to nodeData prop
watch(() => props.nodeData, (value) => {
// Create deep copy to prevent reactivity issues
localNodeData.value = {
...value,
childNodes: Array.isArray(value.childNodes)
? value.childNodes.map(child => ({ ...child }))
: []
};
}, { deep: true, immediate: true });
// Handle updates from the configuration component
const handleUpdate = (updatedData) => {
localNodeData.value = { ...localNodeData.value, ...updatedData };
};
// Save and close modal
const saveAndClose = () => {
emit('update', localNodeData.value);
showModal.value = false;
};
// Close modal without saving
const closeModal = () => {
showModal.value = false;
};
// Get child node type icon
const getChildNodeIcon = (type) => {
const iconMap = {
form: 'material-symbols:description-outline',
html: 'material-symbols:code',
table: 'material-symbols:table'
}
return iconMap[type] || 'material-symbols:extension'
}
// Get child node type display name
const getChildNodeTypeName = (type) => {
const nameMap = {
form: 'Form',
html: 'HTML',
table: 'Table'
}
return nameMap[type] || 'Unknown'
}
// Convert gap value for preview
const previewGap = computed(() => {
const gap = localNodeData.value.gap || '1rem';
// Convert rem values to pixels for preview (assuming 16px = 1rem)
if (gap.includes('rem')) {
const value = parseFloat(gap);
return `${value * 16}px`;
}
return gap;
});
</script>
<style scoped>
/* Modal content styling */
.modal-content {
max-height: 80vh;
overflow-y: auto;
}
</style>