corrad-bp/components/process-flow/ScriptNodeConfiguration.vue
Md Afiq Iskandar 312e555361 Enhance Process Flow Components with Recursive Update Prevention
- 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.
2025-07-28 09:49:53 +08:00

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>