- 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.
700 lines
20 KiB
Vue
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> |