corrad-bp/components/process-flow/ProcessBuilderComponents.vue
Md Afiq Iskandar b4eb3265c2 Enhance Process Builder with HTML and Subprocess Node Features
- Introduced new HTML and Subprocess nodes in ProcessBuilderComponents.vue, allowing users to add custom HTML content and execute subprocesses within the process flow.
- Updated ProcessFlowNodes.js to include HtmlNode and SubprocessNode components with appropriate properties and rendering logic.
- Enhanced ProcessFlowCanvas.vue to manage the new node types effectively, ensuring proper integration with existing flow functionalities.
- Improved index.vue to support configuration modals for HTML and Subprocess nodes, enhancing user interaction and customization options.
- Refactored process management logic to accommodate new node types, ensuring seamless integration and consistent user experience across the process builder.
2025-07-10 11:08:16 +08:00

419 lines
11 KiB
Vue

<template>
<div class="process-components">
<!-- Search Bar -->
<div class="search-container p-3 mb-2">
<div class="relative">
<input
type="text"
v-model="searchQuery"
placeholder="Search..."
class="w-full px-3 py-2 pl-9 bg-white border border-gray-300 rounded text-gray-700 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<Icon
name="material-symbols:search"
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"
/>
</div>
</div>
<!-- Core Components -->
<div class="component-category mb-6">
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Core Components</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 px-2">
<div
v-for="component in getComponentsByCategory('Core')"
:key="component.type"
class="component-item rounded p-3 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200 touch-manipulation"
:class="{ 'hidden': !matchesSearch(component) }"
draggable="true"
@dragstart="onDragStart($event, component)"
@click="addComponent(component)"
>
<Icon :name="component.icon" class="mb-2 w-6 h-6 text-gray-600" />
<span class="text-xs text-gray-600 text-center leading-tight">{{ component.name }}</span>
</div>
</div>
</div>
<!-- Design Elements -->
<div class="component-category mb-6">
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Design Elements</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 px-2">
<div
v-for="shape in getComponentsByCategory('Shape')"
:key="shape.type"
class="component-item rounded p-3 flex flex-col items-center justify-center cursor-grab hover:bg-blue-50 transition-colors border border-blue-200 touch-manipulation"
:class="{ 'hidden': !matchesSearch(shape) }"
draggable="true"
@dragstart="onDragStart($event, shape)"
@click="addComponent(shape)"
>
<Icon :name="shape.icon" class="mb-2 w-6 h-6 text-blue-500" />
<span class="text-xs text-blue-600 text-center leading-tight">{{ shape.name }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(['add-component']);
const searchQuery = ref('');
// Define basic process components aligned with BPMN notation
const availableComponents = [
// Core components
{
type: 'start',
name: 'Start Point',
category: 'Core',
icon: 'material-symbols:play-circle-outline',
description: 'Initiates the process flow',
defaultProps: {
label: 'Start',
data: {
description: 'Process start point'
}
}
},
{
type: 'end',
name: 'End Point',
category: 'Core',
icon: 'material-symbols:stop-circle-outline',
description: 'Terminates the process flow',
defaultProps: {
label: 'End',
data: {
description: 'Process end point'
}
}
},
{
type: 'form',
name: 'Form Task',
category: 'Core',
icon: 'material-symbols:description-outline',
description: 'Form to be filled out',
defaultProps: {
label: 'Form Task',
data: {
description: 'Form submission task',
formId: null,
formName: null
}
}
},
{
type: 'api',
name: 'API Call',
category: 'Core',
icon: 'material-symbols:api',
description: 'Make external API calls',
defaultProps: {
label: 'API Call',
data: {
description: 'External API call',
apiMethod: 'GET',
apiUrl: '',
requestBody: '',
headers: '{ "Content-Type": "application/json" }',
outputVariable: 'apiResponse',
continueOnError: false,
errorVariable: 'apiError'
}
}
},
{
type: 'gateway',
name: 'Decision Point',
category: 'Core',
icon: 'material-symbols:call-split',
description: 'Decision point for flow control',
defaultProps: {
label: 'Decision Point',
data: {
description: 'Decision point for branching the workflow',
conditions: [],
defaultPath: 'Default'
}
}
},
{
type: 'notification',
name: 'Notification',
category: 'Core',
icon: 'material-symbols:notifications-outline',
description: 'Send notifications to users',
defaultProps: {
label: 'Notification',
data: {
description: 'Send notification to users',
notificationType: 'info',
recipientType: 'user',
recipientUser: '',
recipientRole: '',
recipientVariable: '',
recipientEmail: '',
subject: '',
message: '',
priority: 'medium',
deliveryOptions: {
inApp: true,
email: false,
sms: false
},
expiration: {
enabled: false,
value: 24,
unit: 'hours'
}
}
}
},
{
type: 'business-rule',
name: 'Business Rule',
category: 'Core',
icon: 'material-symbols:rule',
description: 'Apply business rules to data',
defaultProps: {
label: 'Business Rule',
data: {
description: 'Apply business rules',
ruleGroups: [],
priority: 'medium'
}
}
},
{
type: 'script',
name: 'Script Task',
category: 'Core',
icon: 'material-symbols:code',
description: 'Execute JavaScript to transform data',
defaultProps: {
label: 'Script Task',
data: {
description: 'Execute JavaScript code',
scriptCode: '',
scriptLanguage: 'javascript',
inputVariables: [],
outputVariables: []
}
}
},
{
type: 'html',
name: 'HTML Content',
category: 'Core',
icon: 'material-symbols:code',
description: 'Display custom HTML content',
defaultProps: {
label: 'HTML Content',
data: {
description: 'Custom HTML content',
htmlCode: '<!-- Enter your HTML code here -->\n<div class="custom-html-content">\n <h2>Custom HTML Content</h2>\n <p>This is a custom HTML node that can display rich content.</p>\n</div>',
cssCode: '',
jsCode: '',
inputVariables: [],
outputVariables: [],
allowVariableAccess: true,
autoRefresh: false
}
}
},
{
type: 'subprocess',
name: 'Sub Process',
category: 'Core',
icon: 'material-symbols:hub-outline',
description: 'Execute another process as a sub-process',
defaultProps: {
label: 'Sub Process',
data: {
description: 'Executes another process',
subprocessId: null,
subprocessName: ''
}
}
},
// Design Elements / Shapes
{
type: 'swimlane-horizontal',
name: 'Horizontal Swimlane',
category: 'Shape',
icon: 'material-symbols:view-stream',
description: 'Horizontal swimlane for organizing process sections',
defaultProps: {
label: '',
data: {
description: '',
width: 600,
height: 150,
backgroundColor: '#f8fafc',
borderColor: '#e2e8f0',
textColor: '#475569',
isShape: true,
shapeType: 'swimlane-horizontal'
}
}
},
{
type: 'swimlane-vertical',
name: 'Vertical Swimlane',
category: 'Shape',
icon: 'material-symbols:view-column',
description: 'Vertical swimlane for organizing process sections',
defaultProps: {
label: '',
data: {
description: '',
width: 200,
height: 400,
backgroundColor: '#f8fafc',
borderColor: '#e2e8f0',
textColor: '#475569',
isShape: true,
shapeType: 'swimlane-vertical'
}
}
},
{
type: 'rectangle-shape',
name: 'Rectangle',
category: 'Shape',
icon: 'material-symbols:rectangle',
description: 'Rectangle shape for grouping and annotation',
defaultProps: {
label: '',
data: {
description: '',
width: 300,
height: 200,
backgroundColor: '#fefefe',
borderColor: '#d1d5db',
textColor: '#374151',
isShape: true,
shapeType: 'rectangle'
}
}
},
{
type: 'text-annotation',
name: 'Text Annotation',
category: 'Shape',
icon: 'material-symbols:text-fields',
description: 'Text annotation for adding notes and comments',
defaultProps: {
label: '',
data: {
description: '',
width: 200,
height: 80,
backgroundColor: '#fffbeb',
borderColor: '#fbbf24',
textColor: '#92400e',
isShape: true,
shapeType: 'text-annotation'
}
}
}
];
// Get components by category for rendering in sections
const getComponentsByCategory = (category) => {
return availableComponents.filter(component => component.category === category);
};
// Check if component matches search query
const matchesSearch = (component) => {
if (!searchQuery.value) return true;
const query = searchQuery.value.toLowerCase();
return (
component.name.toLowerCase().includes(query) ||
component.description.toLowerCase().includes(query) ||
component.type.toLowerCase().includes(query)
);
};
// Handle drag start event
const onDragStart = (event, component) => {
// Set the component data in the format expected by ProcessFlowCanvas
const componentData = {
type: component.type,
label: component.defaultProps.label,
data: component.defaultProps.data
};
// Set the drag data with text/plain format for better Mac compatibility
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.dropEffect = 'copy';
event.dataTransfer.setData('text/plain', JSON.stringify(componentData));
// Add visual feedback
event.target.classList.add('dragging');
};
// Add a component directly via click
const addComponent = (component) => {
return;
// Use same format as drag operation for consistency
const componentData = {
type: component.type,
label: component.defaultProps.label,
data: component.defaultProps.data
};
emit('add-component', componentData);
};
</script>
<style scoped>
.process-components {
@apply bg-white h-full;
}
.component-item {
@apply h-20;
min-height: 80px;
transition: all 0.15s ease-in-out;
}
.component-item:hover {
transform: translateY(-2px);
}
.component-item:active {
transform: scale(0.97);
}
/* Mobile touch-friendly adjustments */
@media (max-width: 768px) {
.component-item {
@apply h-24;
min-height: 96px;
}
}
/* Touch device optimizations */
@media (hover: none) and (pointer: coarse) {
.component-item {
min-height: 44px; /* iOS recommended touch target */
}
.component-item:hover {
transform: none; /* Disable hover effects on touch devices */
}
.component-item:active {
transform: scale(0.95);
background-color: #f3f4f6;
}
}
</style>