- Updated HtmlNodeConfiguration.vue and ScriptNodeConfiguration.vue to prevent recursive updates when props change by introducing a flag and utilizing nextTick for state management. - Improved keyboard shortcut handling in index.vue to block shortcuts when modals are open or when input elements are focused, enhancing user experience during process building.
711 lines
21 KiB
Vue
711 lines
21 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, nextTick } 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'
|
|
}
|
|
}
|
|
|
|
// Add a flag to prevent recursive updates
|
|
const isUpdatingFromProps = ref(false)
|
|
|
|
const emitUpdate = () => {
|
|
emit('update', localNodeData.value)
|
|
}
|
|
|
|
// Watch for prop changes
|
|
watch(
|
|
() => props.nodeData,
|
|
(newData) => {
|
|
if (newData && Object.keys(newData).length > 0) {
|
|
isUpdatingFromProps.value = true
|
|
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
|
|
}
|
|
// Reset the flag after the update
|
|
nextTick(() => {
|
|
isUpdatingFromProps.value = false
|
|
})
|
|
}
|
|
},
|
|
{ immediate: true, deep: true }
|
|
)
|
|
|
|
// Watch for changes in localNodeData and emit updates
|
|
watch(
|
|
localNodeData,
|
|
() => {
|
|
// Only emit if we're not currently updating from props
|
|
if (!isUpdatingFromProps.value) {
|
|
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> |