- 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.
464 lines
14 KiB
Vue
464 lines
14 KiB
Vue
<template>
|
|
<div class="script-node-config">
|
|
<div class="config-content">
|
|
<!-- Header -->
|
|
<div class="config-header">
|
|
<h3 class="text-lg font-semibold text-gray-800">Script Task Configuration</h3>
|
|
<p class="text-sm text-gray-600">Configure JavaScript code to transform data and variables</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="Script Name"
|
|
name="label"
|
|
v-model="localNodeData.label"
|
|
help="Display name for this script task"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., Process API Response, Transform Data"
|
|
validation="required"
|
|
/>
|
|
|
|
<FormKit
|
|
type="textarea"
|
|
label="Description"
|
|
name="description"
|
|
v-model="localNodeData.description"
|
|
help="Describe what this script does"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., Transforms API response data into process variables"
|
|
rows="2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Script Code Section -->
|
|
<div class="config-section">
|
|
<h4 class="section-title">Script Code</h4>
|
|
<div class="section-content">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
JavaScript Code
|
|
</label>
|
|
<div class="script-editor-container">
|
|
<RsCodeMirror
|
|
v-model="localNodeData.scriptCode"
|
|
:options="{
|
|
mode: 'javascript',
|
|
theme: 'default',
|
|
lineNumbers: true,
|
|
lineWrapping: true,
|
|
autoCloseBrackets: true,
|
|
matchBrackets: true,
|
|
indentUnit: 2,
|
|
tabSize: 2
|
|
}"
|
|
class="script-editor"
|
|
placeholder="// Available objects:
|
|
// - processVariables: object containing all process variables
|
|
// - console: for debugging (console.log)
|
|
//
|
|
// Example - Transform API response:
|
|
// const apiData = processVariables.apiResponse;
|
|
// processVariables.applicantStatus = apiData.data.status;
|
|
// processVariables.approvalRequired = apiData.data.score < 70;
|
|
//
|
|
// Example - Extract specific fields:
|
|
// processVariables.firstName = processVariables.text3?.split(' ')[0];
|
|
// processVariables.documentCount = processVariables.apiResponse?.data?.documents?.length || 0;"
|
|
/>
|
|
</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>
|
|
|
|
<!-- Error Handling -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="checkbox"
|
|
label="Continue on Error"
|
|
name="continueOnError"
|
|
v-model="localNodeData.continueOnError"
|
|
help="If enabled, process continues even if script fails"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
v-if="!localNodeData.continueOnError"
|
|
type="text"
|
|
label="Error Variable"
|
|
name="errorVariable"
|
|
v-model="localNodeData.errorVariable"
|
|
help="Variable to store error information"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., scriptError"
|
|
/>
|
|
</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 script 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 script will create or modify
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Script Testing Section -->
|
|
<div class="config-section">
|
|
<h4 class="section-title">Script Testing</h4>
|
|
<div class="section-content">
|
|
<div class="test-container p-4 bg-gray-50 rounded-lg">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h5 class="font-medium text-gray-700">Test Script Execution</h5>
|
|
<button
|
|
@click="testScript"
|
|
:disabled="!localNodeData.scriptCode.trim()"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
>
|
|
Test Script
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="testResult" class="test-result">
|
|
<div class="flex items-center space-x-2 mb-2">
|
|
<Icon
|
|
:name="testResult.success ? 'material-symbols:check-circle' : 'material-symbols:error'"
|
|
:class="testResult.success ? 'text-green-600' : 'text-red-600'"
|
|
class="w-5 h-5"
|
|
/>
|
|
<span :class="testResult.success ? 'text-green-700' : 'text-red-700'" class="font-medium">
|
|
{{ testResult.success ? 'Script executed successfully' : 'Script execution failed' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="testResult.error" class="error-details p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
|
|
{{ testResult.error }}
|
|
</div>
|
|
|
|
<div v-if="testResult.result" class="result-details">
|
|
<h6 class="font-medium text-gray-700 mb-2">Modified Variables:</h6>
|
|
<pre class="text-xs bg-white p-3 border border-gray-200 rounded overflow-auto max-h-32">{{ JSON.stringify(testResult.result, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, 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: '',
|
|
scriptCode: '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n',
|
|
scriptLanguage: 'javascript',
|
|
inputVariables: [],
|
|
outputVariables: [],
|
|
continueOnError: false,
|
|
errorVariable: 'scriptError'
|
|
})
|
|
|
|
const testResult = ref(null)
|
|
|
|
// 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 testScript = () => {
|
|
try {
|
|
// Create a test environment with mock process variables
|
|
const mockProcessVariables = {}
|
|
|
|
// Add mock values for available variables
|
|
props.availableVariables.forEach(variable => {
|
|
mockProcessVariables[variable.name] = variable.value || getMockValueForType(variable.type)
|
|
})
|
|
|
|
// Create a safe execution context
|
|
const scriptFunction = new Function('processVariables', 'console', localNodeData.value.scriptCode)
|
|
|
|
// Mock console for testing
|
|
const mockConsole = {
|
|
log: (...args) => console.log('[Script Test]', ...args),
|
|
error: (...args) => console.error('[Script Test]', ...args),
|
|
warn: (...args) => console.warn('[Script Test]', ...args)
|
|
}
|
|
|
|
// Execute the script
|
|
scriptFunction(mockProcessVariables, mockConsole)
|
|
|
|
testResult.value = {
|
|
success: true,
|
|
result: mockProcessVariables,
|
|
error: null
|
|
}
|
|
} catch (error) {
|
|
testResult.value = {
|
|
success: false,
|
|
result: null,
|
|
error: error.message
|
|
}
|
|
}
|
|
}
|
|
|
|
const getMockValueForType = (type) => {
|
|
switch (type) {
|
|
case 'string': return 'mock_value'
|
|
case 'number': return 42
|
|
case 'boolean': return true
|
|
case 'object': return { data: { field: 'value' } }
|
|
case 'array': return ['item1', 'item2']
|
|
default: return 'mock_value'
|
|
}
|
|
}
|
|
|
|
const emitUpdate = () => {
|
|
emit('update', localNodeData.value)
|
|
}
|
|
|
|
// Add a flag to prevent recursive updates
|
|
const isUpdatingFromProps = ref(false)
|
|
|
|
// Watch for prop changes
|
|
watch(
|
|
() => props.nodeData,
|
|
(newData) => {
|
|
if (newData && Object.keys(newData).length > 0) {
|
|
isUpdatingFromProps.value = true
|
|
localNodeData.value = {
|
|
label: newData.label || 'Script Task',
|
|
description: newData.description || '',
|
|
scriptCode: newData.scriptCode || '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n',
|
|
scriptLanguage: newData.scriptLanguage || 'javascript',
|
|
inputVariables: newData.inputVariables || [],
|
|
outputVariables: newData.outputVariables || [],
|
|
continueOnError: newData.continueOnError || false,
|
|
errorVariable: newData.errorVariable || 'scriptError'
|
|
}
|
|
// 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 }
|
|
)
|
|
|
|
// Reset test result when script changes
|
|
watch(
|
|
() => localNodeData.value.scriptCode,
|
|
() => {
|
|
testResult.value = null
|
|
}
|
|
)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.script-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;
|
|
}
|
|
|
|
.script-editor-container {
|
|
@apply border border-gray-300 rounded-lg overflow-hidden;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.test-container {
|
|
@apply border border-gray-200;
|
|
}
|
|
|
|
.field-wrapper {
|
|
@apply mb-0;
|
|
}
|
|
</style> |