- Introduced a new ScriptNodeConfiguration component for configuring JavaScript tasks within the process builder. - Added ScriptNodeConfigurationModal for user-friendly script task setup, including input and output variable management. - Updated process management logic to handle script variables directly within the process store, improving variable management and accessibility. - Enhanced existing components to support the new script task feature, ensuring seamless integration with the process flow. - Improved overall user experience with intuitive UI elements and clear documentation for the new functionality.
453 lines
14 KiB
Vue
453 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 } 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)
|
|
}
|
|
|
|
// Watch for prop changes
|
|
watch(
|
|
() => props.nodeData,
|
|
(newData) => {
|
|
if (newData && Object.keys(newData).length > 0) {
|
|
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'
|
|
}
|
|
}
|
|
},
|
|
{ immediate: true, deep: true }
|
|
)
|
|
|
|
// Watch for changes in localNodeData and emit updates
|
|
watch(
|
|
localNodeData,
|
|
() => {
|
|
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> |