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

700 lines
20 KiB
Vue

<template>
<div class="html-node-config">
<div class="config-content">
<!-- Header -->
<div class="config-header">
<h3 class="text-lg font-semibold text-gray-800">HTML Node Configuration</h3>
<p class="text-sm text-gray-600">Configure custom HTML content for your process</p>
</div>
<!-- Basic Info Section -->
<div class="config-section">
<h4 class="section-title">Basic Information</h4>
<div class="section-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
type="text"
label="HTML Node Name"
name="label"
v-model="localNodeData.label"
help="Display name for this HTML node"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Custom Form, Information Display"
validation="required"
/>
<FormKit
type="textarea"
label="Description"
name="description"
v-model="localNodeData.description"
help="Describe what this HTML content does"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Displays a custom form for user input"
rows="2"
/>
</div>
</div>
</div>
<!-- HTML Content Section -->
<div class="config-section">
<h4 class="section-title">HTML Content</h4>
<div class="section-content">
<!-- Code Editor Tabs -->
<div class="code-editor-tabs">
<div class="tabs-header border-b border-gray-200">
<div class="flex">
<button
@click="activeTab = 'html'"
class="px-4 py-2 text-sm font-medium"
:class="activeTab === 'html' ? 'text-blue-600 border-b-2 border-blue-500' : 'text-gray-600 hover:text-gray-800'"
>
<span class="flex items-center">
<Icon name="material-symbols:code" class="w-4 h-4 mr-2" />
HTML
</span>
</button>
<button
@click="activeTab = 'css'"
class="px-4 py-2 text-sm font-medium"
:class="activeTab === 'css' ? 'text-blue-600 border-b-2 border-blue-500' : 'text-gray-600 hover:text-gray-800'"
>
<span class="flex items-center">
<Icon name="material-symbols:format-color-fill" class="w-4 h-4 mr-2" />
CSS
</span>
</button>
<button
@click="activeTab = 'js'"
class="px-4 py-2 text-sm font-medium"
:class="activeTab === 'js' ? 'text-blue-600 border-b-2 border-blue-500' : 'text-gray-600 hover:text-gray-800'"
>
<span class="flex items-center">
<Icon name="material-symbols:code-blocks" class="w-4 h-4 mr-2" />
JavaScript
</span>
</button>
</div>
</div>
<!-- HTML Tab -->
<div v-show="activeTab === 'html'" class="tab-content py-4">
<div class="html-editor-container">
<RsCodeMirror
v-model="localNodeData.htmlCode"
:options="{
mode: 'htmlmixed',
theme: 'default',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
autoCloseTags: true,
matchBrackets: true,
matchTags: true,
indentUnit: 2,
tabSize: 2
}"
class="html-editor"
placeholder="<!-- Enter your HTML code here -->
<div class='custom-html-content'>
<h2>Custom HTML Content</h2>
<p>This is a custom HTML node that can display rich content.</p>
<form>
<div class='form-group'>
<label>Name:</label>
<input type='text' class='form-control' />
</div>
<button type='button' class='btn'>Submit</button>
</form>
</div>"
/>
</div>
<p class="text-xs text-gray-500 mt-2">
💡 You can use variables in your HTML with double curly braces: <code>{{ variableSyntaxExample }}</code>
</p>
</div>
<!-- CSS Tab -->
<div v-show="activeTab === 'css'" class="tab-content py-4">
<div class="css-editor-container">
<RsCodeMirror
v-model="localNodeData.cssCode"
:options="{
mode: 'css',
theme: 'default',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2
}"
class="css-editor"
placeholder="/* Add your custom CSS styles here */
.custom-html-content {
padding: 15px;
border-radius: 5px;
background-color: #f5f5f5;
}
.form-group {
margin-bottom: 15px;
}
.form-control {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 8px 16px;
background-color: #4a6cf7;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}"
/>
</div>
<p class="text-xs text-gray-500 mt-2">
💡 Add CSS to style your HTML content. These styles will be scoped to this HTML node only.
</p>
</div>
<!-- JavaScript Tab -->
<div v-show="activeTab === 'js'" class="tab-content py-4">
<div class="js-editor-container">
<RsCodeMirror
v-model="localNodeData.jsCode"
:options="{
mode: 'javascript',
theme: 'default',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2
}"
class="js-editor"
placeholder="// Add your custom JavaScript here
// This will be executed when the HTML is rendered
document.addEventListener('DOMContentLoaded', function() {
// Access elements in your custom HTML
const button = document.querySelector('.btn');
if (button) {
button.addEventListener('click', function() {
// Handle button click
console.log('Button clicked!');
// You can access process variables like this:
// const value = processVariables.someVariable;
// And update them like this:
// processVariables.outputValue = 'New value';
});
}
});"
/>
</div>
<p class="text-xs text-gray-500 mt-2">
💡 Use <code>processVariables</code> to access and modify process data.
Available variables: {{ availableVariableNames.join(', ') || 'None' }}
</p>
</div>
</div>
<!-- Variable Integration -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<FormKit
type="checkbox"
label="Allow Variable Access"
name="allowVariableAccess"
v-model="localNodeData.allowVariableAccess"
help="If enabled, HTML can access and modify process variables"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="checkbox"
label="Auto-refresh on Variable Change"
name="autoRefresh"
v-model="localNodeData.autoRefresh"
help="If enabled, HTML content will refresh when variables change"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
</div>
</div>
<!-- Variables Section -->
<div class="config-section">
<h4 class="section-title">Variable Management</h4>
<div class="section-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Input Variables -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Input Variables
</label>
<div class="variable-list">
<div
v-for="variable in availableVariables"
:key="variable.name"
class="variable-item"
:class="{ 'selected': isInputVariable(variable.name) }"
@click="toggleInputVariable(variable.name)"
>
<div class="variable-info">
<span class="variable-name">{{ variable.name }}</span>
<span class="variable-type">{{ variable.type }}</span>
</div>
<span class="variable-description">{{ variable.description }}</span>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
Click to select variables this HTML node will read from
</p>
</div>
<!-- Output Variables -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Output Variables
</label>
<div class="space-y-2">
<div
v-for="(output, index) in localNodeData.outputVariables"
:key="index"
class="flex items-center space-x-2"
>
<FormKit
type="text"
v-model="output.name"
placeholder="Variable name"
:classes="{ outer: 'flex-1' }"
/>
<FormKit
type="select"
v-model="output.type"
:options="[
{ label: 'String', value: 'string' },
{ label: 'Number', value: 'number' },
{ label: 'Boolean', value: 'boolean' },
{ label: 'Object', value: 'object' },
{ label: 'Array', value: 'array' }
]"
:classes="{ outer: 'flex-1' }"
/>
<button
@click="removeOutputVariable(index)"
class="p-2 text-red-600 hover:bg-red-50 rounded"
>
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
</button>
</div>
<button
@click="addOutputVariable"
class="flex items-center space-x-2 px-3 py-2 text-blue-600 hover:bg-blue-50 rounded border border-dashed border-blue-300"
>
<Icon name="material-symbols:add" class="w-4 h-4" />
<span class="text-sm">Add Output Variable</span>
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Define variables this HTML node will create or modify
</p>
</div>
</div>
</div>
</div>
<!-- HTML Preview Section -->
<div class="config-section">
<h4 class="section-title">Preview</h4>
<div class="section-content">
<div class="preview-container p-4 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between mb-3">
<h5 class="font-medium text-gray-700">HTML Preview</h5>
<div class="flex items-center space-x-2">
<span class="text-xs text-gray-500">Auto-refresh</span>
<label class="switch">
<input type="checkbox" v-model="autoPreview">
<span class="slider round"></span>
</label>
<button
@click="previewHtml"
:disabled="!localNodeData.htmlCode.trim()"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
>
<Icon name="material-symbols:refresh" class="w-4 h-4 mr-1" />
Refresh Preview
</button>
</div>
</div>
<div class="preview-frame border border-gray-200 rounded bg-white p-4 min-h-[250px]">
<div v-if="!previewContent" class="flex items-center justify-center h-full text-gray-400">
<div class="text-center">
<Icon name="material-symbols:preview" class="w-8 h-8 mb-2 mx-auto" />
<p>Click "Refresh Preview" to see your HTML content</p>
</div>
</div>
<div v-else v-html="previewContent" class="html-preview"></div>
</div>
<div v-if="previewError" class="error-details mt-3 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
<div class="flex items-start">
<Icon name="material-symbols:error" class="w-5 h-5 mr-2 flex-shrink-0 text-red-500" />
<div>{{ previewError }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// Props and emits
const props = defineProps({
nodeData: {
type: Object,
default: () => ({})
},
availableVariables: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update'])
// Local state
const localNodeData = ref({
label: '',
description: '',
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
})
// Tab state
const activeTab = ref('html')
// Preview state
const autoPreview = ref(true)
const previewContent = ref(null)
const previewError = ref(null)
// Computed property to avoid template syntax issues
const variableSyntaxExample = computed(() => '{{ variableName }}')
// Computed properties
const availableVariableNames = computed(() => {
return props.availableVariables.map(v => v.name)
})
// Methods
const isInputVariable = (variableName) => {
return localNodeData.value.inputVariables.includes(variableName)
}
const toggleInputVariable = (variableName) => {
const index = localNodeData.value.inputVariables.indexOf(variableName)
if (index > -1) {
localNodeData.value.inputVariables.splice(index, 1)
} else {
localNodeData.value.inputVariables.push(variableName)
}
emitUpdate()
}
const addOutputVariable = () => {
localNodeData.value.outputVariables.push({
name: '',
type: 'string',
description: ''
})
emitUpdate()
}
const removeOutputVariable = (index) => {
localNodeData.value.outputVariables.splice(index, 1)
emitUpdate()
}
const previewHtml = () => {
try {
previewError.value = null
// Combine HTML, CSS and JS for preview
let htmlContent = localNodeData.value.htmlCode || ''
const cssCode = localNodeData.value.cssCode || ''
const jsCode = localNodeData.value.jsCode || ''
// Add CSS if provided
if (cssCode) {
htmlContent = `<style>${cssCode}</style>${htmlContent}`
}
// Add JS if provided (in a safe way)
if (jsCode) {
// We don't actually execute JS in the preview for security reasons
// Just show a placeholder that JS will be included
htmlContent += `<div class="js-indicator text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
<strong>Note:</strong> JavaScript functionality will be available when deployed.
</div>`
}
// Replace variable placeholders with mock values
if (localNodeData.value.allowVariableAccess) {
const variableRegex = /\{\{\s*([a-zA-Z0-9_\.]+)\s*\}\}/g
htmlContent = htmlContent.replace(variableRegex, (match, varName) => {
const variable = props.availableVariables.find(v => v.name === varName)
if (variable) {
return getMockValueForType(variable.type)
}
return `[${varName}]`
})
}
previewContent.value = htmlContent
} catch (error) {
previewError.value = `Preview error: ${error.message}`
previewContent.value = null
}
}
const getMockValueForType = (type) => {
switch (type) {
case 'string': return 'Sample Text'
case 'number': return '42'
case 'boolean': return 'true'
case 'object': return '{object}'
case 'array': return '[array]'
default: return 'value'
}
}
const emitUpdate = () => {
emit('update', localNodeData.value)
}
// Watch for prop changes
watch(
() => props.nodeData,
(newData) => {
if (newData && Object.keys(newData).length > 0) {
localNodeData.value = {
label: newData.label || 'HTML Node',
description: newData.description || '',
htmlCode: newData.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: newData.cssCode || '',
jsCode: newData.jsCode || '',
inputVariables: newData.inputVariables || [],
outputVariables: newData.outputVariables || [],
allowVariableAccess: newData.allowVariableAccess !== undefined ? newData.allowVariableAccess : true,
autoRefresh: newData.autoRefresh || false
}
}
},
{ immediate: true, deep: true }
)
// Watch for changes in localNodeData and emit updates
watch(
localNodeData,
() => {
emitUpdate()
},
{ deep: true }
)
// Watch for HTML, CSS, or JS changes to auto-refresh preview
watch(
[
() => localNodeData.value.htmlCode,
() => localNodeData.value.cssCode,
() => localNodeData.value.jsCode
],
() => {
if (autoPreview.value) {
previewHtml()
}
}
)
// Initialize preview on component mount
onMounted(() => {
if (autoPreview.value && localNodeData.value.htmlCode) {
previewHtml()
}
})
</script>
<style scoped>
.html-node-config {
@apply max-w-6xl mx-auto bg-white;
}
.config-content {
@apply p-6 space-y-8;
}
.config-header {
@apply border-b border-gray-200 pb-4;
}
.config-section {
@apply space-y-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.section-content {
@apply space-y-4;
}
.code-editor-tabs {
@apply border border-gray-200 rounded-lg overflow-hidden bg-white;
}
.tabs-header {
@apply bg-gray-50;
}
.tab-content {
@apply p-0;
}
.html-editor-container,
.css-editor-container,
.js-editor-container {
@apply border-0;
}
.html-editor,
.css-editor,
.js-editor {
min-height: 250px;
}
/* Toggle Switch */
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #3b82f6;
}
input:focus + .slider {
box-shadow: 0 0 1px #3b82f6;
}
input:checked + .slider:before {
transform: translateX(16px);
}
.slider.round {
border-radius: 20px;
}
.slider.round:before {
border-radius: 50%;
}
.variable-list {
@apply space-y-1 max-h-48 overflow-y-auto border border-gray-200 rounded-lg p-2;
}
.variable-item {
@apply p-2 rounded cursor-pointer hover:bg-gray-50 border border-transparent;
}
.variable-item.selected {
@apply bg-blue-50 border-blue-200;
}
.variable-info {
@apply flex items-center justify-between;
}
.variable-name {
@apply font-medium text-gray-800;
}
.variable-type {
@apply text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded;
}
.variable-description {
@apply text-xs text-gray-600 mt-1;
}
.preview-container {
@apply border border-gray-200;
}
.preview-frame {
min-height: 200px;
}
.field-wrapper {
@apply mb-0;
}
code {
@apply bg-gray-100 px-1 py-0.5 rounded text-xs font-mono;
}
.html-preview :deep(*) {
max-width: 100%;
}
</style>