- 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.
226 lines
7.2 KiB
Vue
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> |