Enhance API Node Configuration Component with Authorization and Request Features

- Added support for additional HTTP methods (HEAD, OPTIONS) in the API node configuration.
- Refactored the request configuration section to focus on authorization, including dynamic fields for various auth types (Bearer, Basic, API Key).
- Introduced a new KeyValueTable component for managing query parameters and headers, improving user experience in configuring API requests.
- Updated the request body handling to support multiple body types (form-data, x-www-form-urlencoded, raw, binary) with appropriate UI elements.
- Enhanced response handling and testing steps to align with the new configuration structure, ensuring a seamless API interaction experience.
This commit is contained in:
Md Afiq Iskandar 2025-07-16 08:39:43 +08:00
parent 406bcaea08
commit 03000b710b
5 changed files with 994 additions and 175 deletions

View File

@ -24,6 +24,8 @@
<option value="PUT">PUT</option> <option value="PUT">PUT</option>
<option value="PATCH">PATCH</option> <option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option> <option value="DELETE">DELETE</option>
<option value="HEAD">HEAD</option>
<option value="OPTIONS">OPTIONS</option>
</select> </select>
<p class="mt-1 text-xs text-gray-500"> <p class="mt-1 text-xs text-gray-500">
The HTTP method determines how the API call interacts with the endpoint. The HTTP method determines how the API call interacts with the endpoint.
@ -48,112 +50,116 @@
</div> </div>
</div> </div>
<!-- Step 2: Headers and Body --> <!-- Step 2: Authorization -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200"> <div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2"> <div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-indigo-600">2</span> <span class="text-xs font-semibold text-indigo-600">2</span>
</div> </div>
<h4 class="font-medium">Request Configuration</h4> <h4 class="font-medium">Authorization</h4>
</div> </div>
<div class="mb-2">
<!-- Headers --> <label class="block text-sm font-medium text-gray-700 mb-1">Type</label>
<div class="mb-4"> <select v-if="localNodeData.authorization" v-model="localNodeData.authorization.type" class="w-48 p-2 border rounded text-sm" @change="saveChanges">
<label class="block text-sm font-medium text-gray-700 mb-1">Headers</label> <option v-for="type in AUTH_TYPES" :key="type.value" :value="type.value">{{ type.label }}</option>
<div class="bg-white p-3 border rounded-md shadow-sm"> </select>
<div class="flex gap-2 mb-2">
<VariableBrowser
:availableVariables="availableVariables"
placeholder="Insert Variable into Headers..."
:allowCreate="false"
@update:modelValue="insertVariable($event, 'headers')"
class="flex-grow"
/>
<RsButton
variant="secondary"
size="sm"
@click="formatJson('headers')"
title="Format JSON"
class="flex-shrink-0"
>
<Icon name="material-symbols:format-align-left" />
Format
</RsButton>
</div>
<textarea
id="headers"
v-model="localNodeData.headers"
class="w-full p-2 border rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 text-sm font-mono"
placeholder='{ "Authorization": "Bearer {accessToken}" }'
rows="4"
@blur="saveChanges"
></textarea>
<p class="mt-1 text-xs text-gray-500">
Set HTTP headers as a JSON object. Use variables with <code class="bg-gray-100 px-1">{variableName}</code> syntax.
</p>
<!-- Headers Preview -->
<div v-if="localNodeData.headers" class="mt-3 pt-3 border-t border-gray-200">
<div class="text-xs font-medium text-gray-700 mb-1">Preview with Current Values:</div>
<div class="bg-gray-50 border rounded p-2">
<pre class="text-xs font-mono whitespace-pre-wrap">{{ getPreviewWithValues('headers') }}</pre>
</div>
</div>
</div>
</div> </div>
<div v-if="localNodeData.authorization && localNodeData.authorization.type" v-for="field in getAuthFields()" :key="field.key" class="mb-2">
<!-- Request Body - show only for POST, PUT, PATCH --> <label class="block text-xs font-medium text-gray-600 mb-1">{{ field.label }}</label>
<div v-if="showRequestBody" class="mt-4"> <input v-if="field.type !== 'select'" :type="field.type" v-model="localNodeData.authorization[field.key]" :placeholder="field.placeholder" class="w-full px-2 py-1 border rounded text-sm" @input="saveChanges" />
<label class="block text-sm font-medium text-gray-700 mb-1">Request Body</label> <select v-else v-model="localNodeData.authorization[field.key]" class="w-full px-2 py-1 border rounded text-sm" @change="saveChanges">
<div class="bg-white p-3 border rounded-md shadow-sm"> <option v-for="opt in field.options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
<div class="flex gap-2 mb-2"> </select>
<VariableBrowser
:availableVariables="availableVariables"
placeholder="Insert Variable into Request Body..."
:allowCreate="false"
@update:modelValue="insertVariable($event, 'requestBody')"
class="flex-grow"
/>
<RsButton
variant="secondary"
size="sm"
@click="formatJson('requestBody')"
title="Format JSON"
class="flex-shrink-0"
>
<Icon name="material-symbols:format-align-left" />
Format
</RsButton>
</div>
<textarea
id="requestBody"
v-model="localNodeData.requestBody"
class="w-full p-2 border rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 text-sm font-mono"
placeholder='{ "key": "value", "userId": "{userId}" }'
rows="6"
@blur="saveChanges"
></textarea>
<p class="mt-1 text-xs text-gray-500">
Request body to send with the API call. Use variables with <code class="bg-gray-100 px-1">{variableName}</code> syntax.
</p>
<!-- Request Body Preview -->
<div v-if="localNodeData.requestBody" class="mt-3 pt-3 border-t border-gray-200">
<div class="text-xs font-medium text-gray-700 mb-1">Preview with Current Values:</div>
<div class="bg-gray-50 border rounded p-2">
<pre class="text-xs font-mono whitespace-pre-wrap">{{ getPreviewWithValues('requestBody') }}</pre>
</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- Step 3: Response Handling --> <!-- Step 3: Params (GET, DELETE, etc.) -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200"> <div v-if="showParams" class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2"> <div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-indigo-600">3</span> <span class="text-xs font-semibold text-indigo-600">3</span>
</div> </div>
<h4 class="font-medium">Query Parameters</h4>
</div>
<KeyValueTable
v-model="localNodeData.params"
:variables="processVariables"
:allow-variable-insert="true"
placeholder-key="Param Key"
placeholder-value="Param Value"
@update:modelValue="saveChanges"
/>
</div>
<!-- Step 4: Headers -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-indigo-600">4</span>
</div>
<h4 class="font-medium">Headers</h4>
</div>
<KeyValueTable
v-model="localNodeData.headers"
:variables="processVariables"
:allow-variable-insert="true"
placeholder-key="Header Name"
placeholder-value="Header Value"
@update:modelValue="saveChanges"
/>
</div>
<!-- Step 5: Body (POST/PUT/PATCH) -->
<div v-if="showRequestBody && localNodeData.body && bodyType" class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-indigo-600">5</span>
</div>
<h4 class="font-medium">Request Body</h4>
</div>
<div class="mb-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Body Type</label>
<select v-model="localNodeData.body.type" class="w-64 p-2 border rounded text-sm" @change="saveChanges">
<option v-for="type in BODY_TYPES" :key="type.value" :value="type.value">{{ type.label }}</option>
</select>
</div>
<div v-if="['form-data', 'x-www-form-urlencoded'].includes(bodyType)">
<KeyValueTable
:model-value="getBodyData()"
:variables="processVariables"
:allow-variable-insert="true"
placeholder-key="Field Name"
placeholder-value="Field Value"
@update:modelValue="setBodyData"
/>
</div>
<div v-else-if="bodyType === 'raw'">
<textarea v-model="localNodeData.body.data" rows="6" class="w-full p-2 border rounded font-mono text-sm" placeholder="Raw body (JSON, XML, etc.)" @input="saveChanges"></textarea>
<VariableBrowser
:available-variables="processVariables"
:allow-create="false"
:placeholder="'Insert variable...'"
@update:modelValue="insertVariableToRaw"
class="w-48 mt-2"
/>
<div v-if="rawBodyJsonWarning && rawBodyHasVariable" class="mt-2 text-xs text-red-600 flex items-center gap-1">
<Icon name="material-symbols:warning" class="w-4 h-4" />
{{ rawBodyJsonWarning }}
</div>
</div>
<div v-else-if="bodyType === 'binary'">
<input type="file" @change="handleFileUpload" />
<div v-if="localNodeData.body.data" class="mt-2 text-xs text-gray-600">Selected: {{ localNodeData.body.data }}</div>
</div>
<div v-else class="text-xs text-gray-500">No body will be sent.</div>
</div>
<!-- Step 6: Response Handling -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-indigo-600">6</span>
</div>
<h4 class="font-medium">Response Handling</h4> <h4 class="font-medium">Response Handling</h4>
</div> </div>
@ -208,11 +214,11 @@
</div> </div>
</div> </div>
<!-- Step 4: Test API Call --> <!-- Step 7: Test API Call -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200"> <div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2"> <div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-indigo-600">4</span> <span class="text-xs font-semibold text-indigo-600">7</span>
</div> </div>
<h4 class="font-medium">Test API Call</h4> <h4 class="font-medium">Test API Call</h4>
</div> </div>
@ -276,6 +282,22 @@
import { ref, computed, watch, nextTick } from 'vue'; import { ref, computed, watch, nextTick } from 'vue';
import { useProcessBuilderStore } from '@/stores/processBuilder'; import { useProcessBuilderStore } from '@/stores/processBuilder';
import VariableBrowser from './VariableBrowser.vue'; import VariableBrowser from './VariableBrowser.vue';
import KeyValueTable from './KeyValueTable.vue';
const AUTH_TYPES = [
{ value: 'none', label: 'None' },
{ value: 'bearer', label: 'Bearer Token' },
{ value: 'basic', label: 'Basic Auth' },
{ value: 'apiKey', label: 'API Key' }
];
const BODY_TYPES = [
{ value: 'none', label: 'None' },
{ value: 'form-data', label: 'form-data' },
{ value: 'x-www-form-urlencoded', label: 'x-www-form-urlencoded' },
{ value: 'raw', label: 'raw' },
{ value: 'binary', label: 'binary' }
];
const props = defineProps({ const props = defineProps({
nodeData: { nodeData: {
@ -294,17 +316,7 @@ const emit = defineEmits(['update']);
const processStore = useProcessBuilderStore(); const processStore = useProcessBuilderStore();
// Local state for node data - create a deep copy to avoid mutation issues // Local state for node data - create a deep copy to avoid mutation issues
const localNodeData = ref({ const localNodeData = ref(migrateNodeData(props.nodeData));
label: 'API Call',
description: '',
apiMethod: 'GET',
apiUrl: '',
requestBody: '',
headers: '{ "Content-Type": "application/json" }',
outputVariable: 'apiResponse',
continueOnError: false,
errorVariable: 'apiError'
});
// Computed for showing request body based on method // Computed for showing request body based on method
const showRequestBody = computed(() => { const showRequestBody = computed(() => {
@ -324,22 +336,16 @@ const availableVariables = computed(() => {
return globalVars; return globalVars;
}); });
// Get process variables for substitution
const processVariables = computed(() => {
// Use processStore.currentProcess.variables (object) and convert to array
const varsObj = processStore.currentProcess?.variables || {};
return Object.values(varsObj);
});
// Watch for changes from parent props // Watch for changes from parent props
watch(() => props.nodeData, (newNodeData) => { watch(() => props.nodeData, (newNodeData) => {
if (newNodeData) { if (newNodeData) localNodeData.value = migrateNodeData(newNodeData);
// Create a deep copy to break reactivity chains with parent
localNodeData.value = {
label: newNodeData.label || 'API Call',
description: newNodeData.description || '',
apiMethod: newNodeData.apiMethod || 'GET',
apiUrl: newNodeData.apiUrl || '',
requestBody: newNodeData.requestBody || '',
headers: newNodeData.headers || '{ "Content-Type": "application/json" }',
outputVariable: newNodeData.outputVariable || 'apiResponse',
continueOnError: newNodeData.continueOnError || false,
errorVariable: newNodeData.errorVariable || 'apiError'
};
}
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
// Function to create a new global variable // Function to create a new global variable
@ -559,6 +565,178 @@ function getPreviewWithValues(field) {
return localNodeData.value[field] || ''; return localNodeData.value[field] || '';
} }
} }
// --- Authorization Fields ---
function getAuthFields() {
switch (localNodeData.value.authorization.type) {
case 'bearer':
return [
{ key: 'token', label: 'Token', type: 'text', placeholder: 'Bearer token', value: localNodeData.value.authorization.token || '' }
];
case 'basic':
return [
{ key: 'username', label: 'Username', type: 'text', placeholder: 'Username', value: localNodeData.value.authorization.username || '' },
{ key: 'password', label: 'Password', type: 'password', placeholder: 'Password', value: localNodeData.value.authorization.password || '' }
];
case 'apiKey':
return [
{ key: 'key', label: 'Key Name', type: 'text', placeholder: 'e.g. X-API-Key', value: localNodeData.value.authorization.key || '' },
{ key: 'value', label: 'Key Value', type: 'text', placeholder: 'API Key Value', value: localNodeData.value.authorization.value || '' },
{ key: 'in', label: 'Add To', type: 'select', options: [
{ value: 'header', label: 'Header' },
{ value: 'query', label: 'Query Param' }
], value: localNodeData.value.authorization.in || 'header' }
];
default:
return [];
}
}
function updateAuthField(field, value) {
localNodeData.value.authorization[field] = value;
saveChanges();
}
// --- Body Data Handling ---
function getBodyData() {
if (['form-data', 'x-www-form-urlencoded'].includes(localNodeData.value.body.type)) {
return Array.isArray(localNodeData.value.body.data) ? localNodeData.value.body.data : [];
}
if (localNodeData.value.body.type === 'raw') {
return typeof localNodeData.value.body.data === 'string' ? localNodeData.value.body.data : '';
}
if (localNodeData.value.body.type === 'binary') {
return localNodeData.value.body.data || null;
}
return [];
}
function setBodyData(val) {
if (['form-data', 'x-www-form-urlencoded'].includes(localNodeData.value.body.type)) {
localNodeData.value.body.data = val;
} else if (localNodeData.value.body.type === 'raw') {
localNodeData.value.body.data = val;
} else if (localNodeData.value.body.type === 'binary') {
localNodeData.value.body.data = val;
}
saveChanges();
}
// --- File Upload for Binary ---
function handleFileUpload(e) {
const file = e.target.files[0];
if (file) {
localNodeData.value.body.data = file.name;
saveChanges();
}
}
// --- JSON Validation for Raw Body ---
const rawBodyIsJson = computed(() => {
if (bodyType.value !== 'raw') return false;
const val = localNodeData.value.body?.data || '';
// Only check if it looks like JSON
if (!val.trim().startsWith('{') && !val.trim().startsWith('[')) return false;
try {
// Replace {variable} with a string to allow parsing
const replaced = val.replace(/\{[a-zA-Z0-9_]+\}/g, '"VAR"');
JSON.parse(replaced);
return true;
} catch {
return false;
}
});
const rawBodyHasVariable = computed(() => {
if (bodyType.value !== 'raw') return false;
const val = localNodeData.value.body?.data || '';
return /\{[a-zA-Z0-9_]+\}/.test(val);
});
const rawBodyJsonWarning = computed(() => {
if (bodyType.value !== 'raw') return '';
const val = localNodeData.value.body?.data || '';
if (!val.trim().startsWith('{') && !val.trim().startsWith('[')) return '';
try {
// Replace both quoted and unquoted {variable} with "VAR"
let replaced = val
.replace(/"\{[a-zA-Z0-9_]+\}"/g, '"VAR"') // quoted
.replace(/\{[a-zA-Z0-9_]+\}/g, '"VAR"'); // unquoted
JSON.parse(replaced);
return '';
} catch {
return 'Warning: Your request body is not valid JSON. Make sure variables are inside quotes.';
}
});
// --- Variable Insertion Helper ---
function insertVariableToRaw(varName) {
if (!varName) return;
const textarea = document.querySelector('textarea[placeholder="Raw body (JSON, XML, etc.)"]');
let val = localNodeData.value.body.data || '';
let insertText = `{${varName}}`;
// Detect if inside quotes
let cursor = textarea ? textarea.selectionStart : val.length;
let before = val.substring(0, cursor);
let after = val.substring(cursor);
// Check if body looks like JSON
const isJson = val.trim().startsWith('{') || val.trim().startsWith('[');
// Check if cursor is inside quotes
let insideQuotes = false;
if (isJson && textarea) {
// Count quotes before cursor
const quotesBefore = before.split('"').length - 1;
insideQuotes = quotesBefore % 2 === 1;
}
if (isJson && !insideQuotes) {
insertText = `"{${varName}}"`;
}
// Insert at cursor
localNodeData.value.body.data = before + insertText + after;
saveChanges();
// Move cursor after inserted variable
nextTick(() => {
if (textarea) {
const newPos = before.length + insertText.length;
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = newPos;
}
});
}
// --- UI State ---
const showParams = computed(() => ['GET', 'DELETE', 'HEAD', 'OPTIONS'].includes(localNodeData.value.apiMethod));
// Defensive computed for bodyType and authType
const bodyType = computed(() => localNodeData.value.body && localNodeData.value.body.type ? localNodeData.value.body.type : 'none');
const authType = computed(() => localNodeData.value.authorization && localNodeData.value.authorization.type ? localNodeData.value.authorization.type : 'none');
function migrateNodeData(data) {
const migrated = { ...data };
// Headers
if (typeof migrated.headers === 'string') {
try {
const obj = JSON.parse(migrated.headers);
migrated.headers = Object.entries(obj).map(([key, value]) => ({ key, value }));
} catch {
migrated.headers = [];
}
} else if (!Array.isArray(migrated.headers)) {
migrated.headers = [];
}
// Params
if (!Array.isArray(migrated.params)) migrated.params = [];
// Body
if (!migrated.body || typeof migrated.body !== 'object') {
migrated.body = { type: 'none', data: [] };
} else {
if (!migrated.body.type) migrated.body.type = 'none';
if (migrated.body.data === undefined) migrated.body.data = migrated.body.type === 'raw' ? '' : [];
}
if (typeof data.requestBody === 'string' && (!migrated.body || migrated.body.type === 'none')) {
migrated.body = { type: 'raw', data: data.requestBody };
}
// Auth
if (!migrated.authorization || typeof migrated.authorization !== 'object') migrated.authorization = { type: 'none' };
if (!migrated.authorization.type) migrated.authorization.type = 'none';
return migrated;
}
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,68 @@
<template>
<div>
<div v-for="(row, idx) in rows" :key="idx" class="flex items-center gap-2 mb-2">
<input
v-model="row.key"
:placeholder="placeholderKey"
class="w-32 px-2 py-1 border rounded text-sm"
@input="emitChange"
/>
<input
v-model="row.value"
:placeholder="placeholderValue"
class="flex-1 px-2 py-1 border rounded text-sm"
@input="emitChange"
:ref="el => valueInputs[idx] = el"
/>
<VariableBrowser
v-if="allowVariableInsert && variables && variables.length"
:available-variables="variables"
:placeholder="'Insert variable...'"
:allow-create="false"
@update:modelValue="varName => insertVariable(idx, varName)"
class="w-32"
/>
<button @click="removeRow(idx)" class="px-2 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200"></button>
</div>
<button @click="addRow" class="mt-1 px-3 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200">+ Add</button>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import VariableBrowser from './VariableBrowser.vue';
const props = defineProps({
modelValue: { type: Array, default: () => [] },
placeholderKey: { type: String, default: 'Key' },
placeholderValue: { type: String, default: 'Value' },
allowVariableInsert: { type: Boolean, default: false },
variables: { type: Array, default: () => [] }
});
const emit = defineEmits(['update:modelValue']);
const rows = ref(props.modelValue.map(r => ({ ...r })));
const valueInputs = ref([]);
watch(() => props.modelValue, v => { rows.value = v.map(r => ({ ...r })); });
function emitChange() { emit('update:modelValue', rows.value.map(r => ({ ...r }))); }
function addRow() { rows.value.push({ key: '', value: '' }); emitChange(); }
function removeRow(idx) { rows.value.splice(idx, 1); emitChange(); }
function insertVariable(idx, varName) {
if (!varName) return;
const input = valueInputs.value[idx];
if (input && input.selectionStart !== undefined) {
const start = input.selectionStart;
const end = input.selectionEnd;
const before = rows.value[idx].value.substring(0, start);
const after = rows.value[idx].value.substring(end);
rows.value[idx].value = before + `{${varName}}` + after;
emitChange();
// Move cursor after inserted variable
nextTick(() => {
input.focus();
input.selectionStart = input.selectionEnd = start + varName.length + 2;
});
} else {
rows.value[idx].value += `{${varName}}`;
emitChange();
}
}
</script>

View File

@ -121,14 +121,21 @@
{ {
"id": "api-1752550319410", "id": "api-1752550319410",
"data": { "data": {
"body": { "data": "{ \"title\" : \"{todoTitle}\"}", "type": "raw" },
"label": "API Call", "label": "API Call",
"apiUrl": "https://jsonplaceholder.typicode.com/todos/1", "shape": "rectangle",
"headers": "{ \"Content-Type\": \"application/json\" }", "apiUrl": "https://jsonplaceholder.typicode.com/posts",
"apiMethod": "GET", "params": [],
"headers": [{ "key": "Content-Type", "value": "application/json" }],
"apiMethod": "POST",
"textColor": "#1e40af",
"borderColor": "#3b82f6",
"description": "External API call", "description": "External API call",
"requestBody": "", "requestBody": "",
"authorization": { "type": "none" },
"errorVariable": "apiError", "errorVariable": "apiError",
"outputVariable": "apiResponse", "outputVariable": "apiResponse",
"backgroundColor": "#eff6ff",
"continueOnError": false "continueOnError": false
}, },
"type": "api", "type": "api",
@ -199,8 +206,8 @@
} }
], ],
"viewport": { "viewport": {
"x": -572.7398904538663, "x": -118.4524312896406,
"y": 253.200827139402, "y": 314.4180761099366,
"zoom": 0.8444612090080732 "zoom": 0.6437632135306554
} }
} }

View File

@ -0,0 +1,288 @@
# Process Variables System Documentation
## Overview
Process variables serve as the **central nervous system** for data flow and state management in the BPM system. They enable dynamic behavior, external system integration, and stateful execution across multiple user interactions and system boundaries.
## Architecture Components
### 1. VariableManager.vue - Central Variable Management
**Location**: `components/process-flow/VariableManager.vue`
**Primary Functions:**
- **Variable Creation & Management**: Add, edit, delete process variables with comprehensive type support
- **Usage Tracking**: Analyzes where variables are used across all node types (form, API, script, gateway, etc.)
- **Variable Navigation**: Click-to-navigate from variable usage to specific nodes in the process
- **Type Organization**: Groups variables by data type (string, int, decimal, boolean, date, object, etc.)
**Key Features:**
- **14+ Variable Types**: string, int, decimal, boolean, date, datetime, object, array, currency, email, UUID, etc.
- **Deep Usage Analysis**: Scans all node data for variable references in multiple formats:
- Template placeholders: `{variableName}`, `{{variableName}}`
- JavaScript references: `${variableName}`, `processVariables.variableName`
- Direct value assignments and API outputs
- **Cross-Node Detection**: Finds variables in API URLs, form mappings, script code, business rules, notifications
### 2. VariableBrowser.vue - Variable Selection Component
**Location**: `components/process-flow/VariableBrowser.vue`
**Purpose**: Used throughout the process builder for variable selection in node configurations
**Key Features:**
- **Grouped Selection**: Variables organized by type with optgroups
- **Live Preview**: Shows current variable values in selection dropdown
- **Inline Creation**: "+" button to create new variables on-the-fly
- **Type Filtering**: Can restrict to specific variable types
- **Validation**: Checks for variable existence and naming conflicts
- **Rich Type Support**: 20+ variable types including special types (currency, email, UUID, etc.)
**Integration Points:**
- Form node configuration (input/output mappings)
- API node configuration (URL parameters, request body)
- Script node configuration (input/output variables)
- Business rule conditions and actions
- Gateway decision logic
- Notification recipient and content variables
### 3. variableStore.js - Legacy Store
**Location**: `stores/variableStore.js`
**Note**: This appears to be a legacy store with basic functionality. The actual variable management is handled primarily by the `processBuilder` store.
**Features:**
- **Scope Management**: Global vs process-level variables
- **Basic CRUD**: Add, update, delete variables
- **Simple Structure**: Arrays for global and process variables
- **Node Variables**: Input/output tracking for individual nodes
## Variable Lifecycle & Data Flow
### 1. Variable Creation
- **Primary Source**: VariableManager component for explicit creation
- **Secondary Source**: VariableBrowser inline creation during node configuration
- **Auto-Creation**: Form nodes can auto-create variables from mappings (`createVariableFromMapping`)
### 2. Variable Usage Across Node Types
#### Form Nodes (`FormNodeConfiguration.vue`)
- **Input Mappings**: Process variables → Form field pre-filling
- **Output Mappings**: Form submission data → Process variables
- **Field Conditions**: Process variables control field visibility/behavior
- **Assignment Variables**: Dynamic task assignment based on variable values
**Example Configuration:**
```javascript
{
inputMappings: [
{
processVariable: "customerName",
formField: "name"
}
],
outputMappings: [
{
formField: "email",
processVariable: "customerEmail"
}
],
fieldConditions: [
{
processVariable: "userType",
operator: "equals",
value: "premium",
targetField: "specialOffers",
action: "show"
}
]
}
```
#### API Nodes
- **URL Interpolation**: Variables embedded in API endpoints
- **Request Body**: Variables in JSON payloads
- **Headers**: Dynamic authentication tokens/values
- **Output Variables**: API responses stored as process variables
- **Error Handling**: Error data captured in designated variables
**Example Configuration:**
```javascript
{
apiUrl: "https://api.example.com/users/{userId}",
requestBody: "{\"name\": \"{customerName}\", \"email\": \"{customerEmail}\"}",
headers: "{\"Authorization\": \"Bearer {authToken}\"}",
outputVariable: "apiResponse",
errorVariable: "apiError"
}
```
#### Script Nodes
- **Input Variables**: Pass process variables to script execution
- **Output Variables**: Script results stored back to process variables
- **Script Code**: Direct variable references in JavaScript code
**Example Configuration:**
```javascript
{
inputVariables: ["customerAge", "orderAmount"],
outputVariables: [
{
name: "discount",
type: "decimal",
description: "Calculated discount amount"
}
],
scriptCode: `
const discount = customerAge > 65 ? orderAmount * 0.1 : 0;
return { discount };
`
}
```
#### Business Rule Nodes
- **Conditions**: Variables used in rule evaluation logic
- **Actions**: Variables updated based on rule outcomes
- **Complex Logic**: AND/OR conditions with multiple variable checks
#### Gateway/Decision Nodes
- **Decision Logic**: Variables determine process flow paths
- **Condition Evaluation**: Dynamic routing based on variable values
### 3. Variable Reference Patterns
The system supports multiple variable reference formats:
- **Template**: `{variableName}`, `{{variableName}}`
- **JavaScript**: `${variableName}`, `processVariables.variableName`
- **JSON**: Direct string/object references
- **API Integration**: URL parameters, headers, body content
### 4. Variable Tracking & Dependencies
- **Usage Analysis**: Real-time tracking of where variables are used
- **Dependency Management**: Prevents deletion of variables in use
- **Navigation Support**: Click-to-navigate from variable to usage location
- **Reference Counting**: Shows total references per variable
### 5. Process Execution Context
Variables serve as the **data backbone** of process execution:
- **State Management**: Track process state between nodes
- **Data Transformation**: Convert and format data as it flows
- **Decision Making**: Enable dynamic process paths
- **Integration**: Bridge between external systems and process logic
- **User Interaction**: Store and recall user input across process steps
## Variable Types
### Basic Types
- **string**: Text values
- **int**: Whole numbers
- **decimal**: Decimal numbers
- **boolean**: True/False values
- **date**: Date values only
- **datetime**: Date and time values
### Complex Types
- **object**: JSON objects and complex data structures
- **array**: Lists of values
- **map**: Key-value pairs
### Special Types
- **currency**: Monetary values
- **percentage**: Percentage values
- **email**: Email addresses
- **url**: URLs
- **phone**: Phone numbers
- **uuid**: Unique identifiers
- **file**: File references
- **image**: Image references
- **binary**: Binary data
## Form Integration
### Form Node Configuration Steps
The form node configuration provides a comprehensive 4-step process:
#### Step 1: Form Selection
- Select existing forms or create new ones
- Integration with FormSelector component
#### Step 2: Form Data Mapping
- **Input Mappings**: Process variables → Form field pre-filling
- **Output Mappings**: Form submission data → Process variables
- Bidirectional data flow configuration
#### Step 3: Field Conditions
- Configure conditional field behavior based on process variables
- Supported operators: equals, not_equals, is_true, is_false, is_empty, etc.
- Actions: readonly, hide, required, optional, show, enable
#### Step 4: Task Assignment
- **Public**: Anyone can complete
- **Specific Users**: Select individual users
- **Role-based**: Assign to user roles
- **Dynamic**: Use process variables to determine assignee
## API Integration
Variables enable seamless integration with external APIs:
- **Dynamic URLs**: Embed variables in API endpoints
- **Request Customization**: Variables in headers, body, and parameters
- **Response Handling**: Store API responses in process variables
- **Error Management**: Capture and handle API errors
## Best Practices
### Variable Naming
- Use descriptive names (e.g., `customerEmail` instead of `email`)
- Follow camelCase convention
- Avoid spaces and special characters
- Use consistent prefixes for related variables
### Type Selection
- Choose the most specific type for better validation
- Use `object` for complex data structures
- Use `array` for lists of items
- Use special types (email, currency) for semantic clarity
### Scope Management
- Prefer process-level variables for data that persists across nodes
- Use global variables sparingly for truly global data
- Document variable purposes and expected values
### Dependency Management
- Review variable usage before deletion
- Use the usage tracking feature to understand dependencies
- Plan variable lifecycle aligned with process flow
## Development Notes
### Store Integration
Variables are primarily managed through the `processBuilder` store:
- `processStore.addProcessVariable()`
- `processStore.getProcessVariables()`
- `processStore.updateProcessVariable()`
- `processStore.deleteProcessVariable()`
### Component Integration
- VariableBrowser is used throughout node configuration modals
- VariableManager provides comprehensive variable management UI
- Real-time validation and error handling
### Performance Considerations
- Variable usage analysis runs on-demand to avoid performance issues
- Deep object cloning used to prevent reactivity issues
- Efficient grouping and filtering for large variable sets
## Related Documentation
- [Form Builder System](./form-builder-system.md)
- [Process Definition Schema](./json/process-builder/processDefinition.json)
- [Process Variables Example](./json/process-builder/processVariables.json)

View File

@ -36,6 +36,7 @@ const currentForm = ref(null);
const tasks = ref([]); const tasks = ref([]);
const conditionalLogicScript = ref(''); const conditionalLogicScript = ref('');
const combinedScript = ref(''); const combinedScript = ref('');
const fieldStates = ref({});
// Get process ID from route // Get process ID from route
const processId = computed(() => route.params.id); const processId = computed(() => route.params.id);
@ -246,17 +247,31 @@ const moveToNextStep = () => {
const handleFormSubmit = async () => { const handleFormSubmit = async () => {
try { try {
stepLoading.value = true; stepLoading.value = true;
console.log('[Workflow] Form submitted. Data:', formData.value); console.log('[Workflow] Form submitted. Data:', formData.value);
// Save form data to process variables
Object.assign(processVariables.value, formData.value);
// Move to next step // Apply output mappings if configured
if (currentNode.value.data?.outputMappings?.length > 0) {
applyOutputMappings(
currentNode.value.data,
formData.value,
processVariables.value
);
} else {
// Fallback to current flat assignment behavior
Object.assign(processVariables.value, formData.value);
}
// Handle assignment variables
if (currentNode.value.data?.assignmentVariable) {
processVariables.value[currentNode.value.data.assignmentVariable] = {
completedBy: 'currentUser', // TODO: Get from auth context
completedAt: new Date().toISOString(),
formData: { ...formData.value }
};
}
moveToNextStep(); moveToNextStep();
console.log('[Workflow] After form submit, current node:', currentNode.value); console.log('[Workflow] After form submit, current node:', currentNode.value);
// If next step is API or script, execute it automatically
if (currentNode.value && ['api', 'script'].includes(currentNode.value.type)) { if (currentNode.value && ['api', 'script'].includes(currentNode.value.type)) {
await executeCurrentStep(); await executeCurrentStep();
} }
@ -268,52 +283,227 @@ const handleFormSubmit = async () => {
} }
}; };
// Execute current step (for API/script nodes) // --- Utility: Substitute variables in a string ---
function substituteVariables(str, variables) {
if (typeof str !== 'string') return str;
return str.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => {
const value = variables[varName];
if (value === undefined || value === null) return '';
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
});
}
// --- Variable Mapping Functions ---
// Apply input mappings to pre-fill form with process variables
function applyInputMappings(nodeData, processVars, formData) {
const { inputMappings = [] } = nodeData;
console.log('[Workflow] Applying input mappings:', inputMappings);
inputMappings.forEach(mapping => {
const { processVariable, formField } = mapping;
if (processVars[processVariable] !== undefined) {
formData[formField] = processVars[processVariable];
console.log(`[Workflow] Mapped ${processVariable} -> ${formField}:`, processVars[processVariable]);
}
});
}
// Apply output mappings to save form data to process variables
function applyOutputMappings(nodeData, formData, processVars) {
const { outputMappings = [] } = nodeData;
console.log('[Workflow] Applying output mappings:', outputMappings);
outputMappings.forEach(mapping => {
const { formField, processVariable } = mapping;
if (formData[formField] !== undefined) {
processVars[processVariable] = formData[formField];
console.log(`[Workflow] Mapped ${formField} -> ${processVariable}:`, formData[formField]);
}
});
}
// Apply field conditions for dynamic form behavior
function applyFieldConditions(nodeData, processVars) {
const { fieldConditions = [] } = nodeData;
const fieldStates = {};
fieldConditions.forEach(condition => {
const { processVariable, operator, value, targetField, action } = condition;
if (evaluateCondition({ variable: processVariable, operator, value }, processVars)) {
fieldStates[targetField] = {
...fieldStates[targetField],
[action]: true
};
console.log(`[Workflow] Field condition applied: ${targetField} -> ${action}`);
}
});
return fieldStates;
}
// --- Utility: Build API request from node data ---
function buildApiRequest(nodeData, variables) {
// 1. URL (with param substitution)
let url = substituteVariables(nodeData.apiUrl, variables);
// 2. Params (for GET, DELETE, etc.)
let params = Array.isArray(nodeData.params) ? nodeData.params : [];
if (params.length) {
const query = params
.filter(p => p.key)
.map(p => `${encodeURIComponent(substituteVariables(p.key, variables))}=${encodeURIComponent(substituteVariables(p.value, variables))}`)
.join('&');
if (query) {
url += (url.includes('?') ? '&' : '?') + query;
}
}
// 3. Headers
let headers = {};
if (Array.isArray(nodeData.headers)) {
nodeData.headers.forEach(h => {
if (h.key) headers[substituteVariables(h.key, variables)] = substituteVariables(h.value, variables);
});
} else if (typeof nodeData.headers === 'object') {
headers = { ...nodeData.headers };
}
// 4. Authorization
if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') {
const auth = nodeData.authorization;
if (auth.type === 'bearer' && auth.token) {
headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`;
} else if (auth.type === 'basic' && auth.username && auth.password) {
const token = btoa(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`);
headers['Authorization'] = `Basic ${token}`;
} else if (auth.type === 'apiKey' && auth.key && auth.value) {
if (auth.in === 'header') {
headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables);
} else if (auth.in === 'query') {
url += (url.includes('?') ? '&' : '?') + `${encodeURIComponent(substituteVariables(auth.key, variables))}=${encodeURIComponent(substituteVariables(auth.value, variables))}`;
}
}
}
// 5. Body
let body;
if (nodeData.body && nodeData.body.type && nodeData.body.type !== 'none') {
if (['form-data', 'x-www-form-urlencoded'].includes(nodeData.body.type)) {
const dataArr = Array.isArray(nodeData.body.data) ? nodeData.body.data : [];
if (nodeData.body.type === 'form-data') {
body = new FormData();
dataArr.forEach(item => {
if (item.key) body.append(substituteVariables(item.key, variables), substituteVariables(item.value, variables));
});
// Remove content-type header for FormData (browser will set it)
if (headers['Content-Type']) delete headers['Content-Type'];
} else {
// x-www-form-urlencoded
body = dataArr
.filter(item => item.key)
.map(item => `${encodeURIComponent(substituteVariables(item.key, variables))}=${encodeURIComponent(substituteVariables(item.value, variables))}`)
.join('&');
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
} else if (nodeData.body.type === 'raw') {
body = substituteVariables(nodeData.body.data, variables);
// Try to detect JSON
if (body && body.trim().startsWith('{')) {
headers['Content-Type'] = 'application/json';
}
} else if (nodeData.body.type === 'binary') {
// Not supported in workflow execution context
body = undefined;
}
}
return { url, headers, body };
}
// Update executeCurrentStep for API node
const executeCurrentStep = async () => { const executeCurrentStep = async () => {
try { try {
stepLoading.value = true; stepLoading.value = true;
console.log('[Workflow] Executing current step:', currentNode.value); console.log('[Workflow] Executing current step:', currentNode.value);
if (currentNode.value?.type === 'api') { if (currentNode.value?.type === 'api') {
console.log(`[Workflow] Executing API node: ${currentNode.value.data?.label || currentNode.value.label}`); console.log(`[Workflow] Executing API node: ${currentNode.value.data?.label || currentNode.value.label}`);
// Enhanced API node execution const nodeData = currentNode.value.data || {};
const { // Use new structure if present
apiUrl, if (nodeData.body || nodeData.headers || nodeData.params || nodeData.authorization) {
apiMethod = 'GET', const { url, headers, body } = buildApiRequest(nodeData, processVariables.value);
headers = '{}', const apiMethod = nodeData.apiMethod || 'GET';
requestBody = '', const outputVariable = nodeData.outputVariable || 'apiResponse';
outputVariable = 'apiResponse', const errorVariable = nodeData.errorVariable || 'apiError';
errorVariable = 'apiError', const continueOnError = nodeData.continueOnError || false;
continueOnError = false try {
} = currentNode.value.data || {}; const response = await $fetch(url, {
try { method: apiMethod,
const response = await $fetch(apiUrl, { headers,
method: apiMethod, body: ['GET', 'HEAD'].includes(apiMethod) ? undefined : body,
headers: headers ? JSON.parse(headers) : {}, });
body: requestBody ? JSON.parse(requestBody) : undefined, processVariables.value[outputVariable] = response;
}); processVariables.value[errorVariable] = null;
processVariables.value[outputVariable] = response; console.log('[Workflow] API call success. Output variable set:', outputVariable, response);
processVariables.value[errorVariable] = null;
console.log('[Workflow] API call success. Output variable set:', outputVariable, response);
// Only auto-progress if there's a single outgoing edge
if (canAutoProgress(currentNode.value)) {
moveToNextStep();
} else {
console.log('[Workflow] API completed, multiple paths available - waiting for user choice');
}
} catch (err) {
processVariables.value[errorVariable] = err;
console.error('[Workflow] API call failed:', err);
if (continueOnError) {
if (canAutoProgress(currentNode.value)) { if (canAutoProgress(currentNode.value)) {
moveToNextStep(); moveToNextStep();
} else { } else {
console.log('[Workflow] API failed but continuing, multiple paths available - waiting for user choice'); console.log('[Workflow] API completed, multiple paths available - waiting for user choice');
}
} catch (err) {
processVariables.value[errorVariable] = err;
console.error('[Workflow] API call failed:', err);
if (continueOnError) {
if (canAutoProgress(currentNode.value)) {
moveToNextStep();
} else {
console.log('[Workflow] API failed but continuing, multiple paths available - waiting for user choice');
}
} else {
error.value = 'API call failed: ' + (err.message || err);
}
}
} else {
// Fallback: old structure
const {
apiUrl,
apiMethod = 'GET',
headers = '{}',
requestBody = '',
outputVariable = 'apiResponse',
errorVariable = 'apiError',
continueOnError = false
} = nodeData;
try {
const response = await $fetch(apiUrl, {
method: apiMethod,
headers: headers ? JSON.parse(headers) : {},
body: requestBody ? JSON.parse(requestBody) : undefined,
});
processVariables.value[outputVariable] = response;
processVariables.value[errorVariable] = null;
console.log('[Workflow] API call success. Output variable set:', outputVariable, response);
if (canAutoProgress(currentNode.value)) {
moveToNextStep();
} else {
console.log('[Workflow] API completed, multiple paths available - waiting for user choice');
}
} catch (err) {
processVariables.value[errorVariable] = err;
console.error('[Workflow] API call failed:', err);
if (continueOnError) {
if (canAutoProgress(currentNode.value)) {
moveToNextStep();
} else {
console.log('[Workflow] API failed but continuing, multiple paths available - waiting for user choice');
}
} else {
error.value = 'API call failed: ' + (err.message || err);
} }
} else {
error.value = 'API call failed: ' + (err.message || err);
} }
} }
} else if (currentNode.value?.type === 'script') { } else if (currentNode.value?.type === 'script') {
@ -461,6 +651,24 @@ watch(currentStep, async (newStep) => {
const formId = currentNode.value.data?.formId; const formId = currentNode.value.data?.formId;
if (formId) { if (formId) {
currentForm.value = await loadFormData(formId); currentForm.value = await loadFormData(formId);
// Apply input mappings to pre-fill form
if (currentNode.value.data?.inputMappings) {
applyInputMappings(
currentNode.value.data,
processVariables.value,
formData.value
);
}
// Apply field conditions
if (currentNode.value.data?.fieldConditions) {
fieldStates.value = applyFieldConditions(
currentNode.value.data,
processVariables.value
);
}
// Update form store with form components and data for ComponentPreview // Update form store with form components and data for ComponentPreview
if (currentForm.value?.formComponents) { if (currentForm.value?.formComponents) {
formStore.formComponents = currentForm.value.formComponents; formStore.formComponents = currentForm.value.formComponents;
@ -480,6 +688,13 @@ watch(formData, (newData) => {
formStore.updatePreviewFormData(newData); formStore.updatePreviewFormData(newData);
}, { deep: true }); }, { deep: true });
// Watch for process variable changes and re-evaluate field conditions
watch(processVariables, (newVars) => {
if (currentNode.value?.type === 'form' && currentNode.value.data?.fieldConditions) {
fieldStates.value = applyFieldConditions(currentNode.value.data, newVars);
}
}, { deep: true });
// Add FormKit form ref // Add FormKit form ref
const formRef = ref(null); const formRef = ref(null);
@ -532,11 +747,15 @@ const makeDecision = (targetNodeId) => {
// Helper: Interpolate process variables in HTML content // Helper: Interpolate process variables in HTML content
const interpolateHtmlContent = (htmlContent) => { const interpolateHtmlContent = (htmlContent) => {
if (!htmlContent || typeof htmlContent !== 'string') return htmlContent; if (!htmlContent || typeof htmlContent !== 'string') return htmlContent;
// Replace {{ processVariables.variableName }} with actual values // Replace {{ processVariables.variableName }} with actual values
return htmlContent.replace(/\{\{\s*processVariables\.(\w+)\s*\}\}/g, (match, variableName) => { return htmlContent.replace(/\{\{\s*processVariables\.(\w+)\s*\}\}/g, (match, variableName) => {
const value = processVariables.value[variableName]; const value = processVariables.value[variableName];
return value !== undefined && value !== null ? String(value) : ''; if (value === undefined || value === null) return '';
if (typeof value === 'object') {
// Pretty print object as JSON, wrapped in <pre> if not already inside <pre>
return `<pre style='display:inline;white-space:pre-wrap;'>${JSON.stringify(value, null, 2)}</pre>`;
}
return String(value);
}); });
}; };
@ -621,6 +840,21 @@ onUnmounted(() => {
currentStyleElement = null; currentStyleElement = null;
} }
}); });
// Helper: Get next node object for single-path nodes
function getNextNodeObject(currentNode) {
if (!currentNode || !workflowData.value?.nodes) return null;
const edges = getOutgoingEdges(currentNode.id);
if (edges.length !== 1) return null;
const nextNodeId = edges[0].target;
return workflowData.value.nodes.find(n => n.id === nextNodeId) || null;
}
// Helper: Get next node label by node ID
function getNodeLabel(nodeId) {
const node = workflowData.value.nodes.find(n => n.id === nodeId);
return node?.data?.label || node?.label || 'Next Step';
}
</script> </script>
<template> <template>
@ -780,7 +1014,8 @@ onUnmounted(() => {
<template v-for="(component, index) in currentForm.formComponents" :key="index"> <template v-for="(component, index) in currentForm.formComponents" :key="index">
<ComponentPreview <ComponentPreview
:component="component" :component="component"
:is-preview="false" :is-preview="false"
:field-states="fieldStates"
/> />
</template> </template>
<div class="flex justify-end pt-6 border-t border-gray-200"> <div class="flex justify-end pt-6 border-t border-gray-200">
@ -865,7 +1100,7 @@ onUnmounted(() => {
variant="outline" variant="outline"
class="justify-between p-3" class="justify-between p-3"
> >
<span>{{ edge.label || 'Continue' }}</span> <span>Continue to: {{ getNodeLabel(edge.target) }}</span>
<Icon name="material-symbols:arrow-right-alt" /> <Icon name="material-symbols:arrow-right-alt" />
</RsButton> </RsButton>
</template> </template>
@ -908,7 +1143,12 @@ onUnmounted(() => {
variant="primary" variant="primary"
> >
<Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" /> <Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" />
{{ stepLoading ? 'Processing...' : 'Continue' }} <template v-if="!stepLoading">
Continue to: {{ getNextNodeObject(currentNode)?.data?.label || getNextNodeObject(currentNode)?.label || 'Next Step' }}
</template>
<template v-else>
Processing...
</template>
</RsButton> </RsButton>
</div> </div>
@ -922,7 +1162,7 @@ onUnmounted(() => {
variant="outline" variant="outline"
class="justify-between p-3" class="justify-between p-3"
> >
<span>{{ edge.label || 'Continue' }}</span> <span>Continue to: {{ getNodeLabel(edge.target) }}</span>
<Icon name="material-symbols:arrow-right-alt" /> <Icon name="material-symbols:arrow-right-alt" />
</RsButton> </RsButton>
</template> </template>
@ -995,8 +1235,46 @@ onUnmounted(() => {
</RsButton> </RsButton>
</div> </div>
<!-- Variable Mapping Debug (only in development) -->
<div v-if="currentNode.type === 'form'" class="bg-gray-100 rounded-lg p-4">
<details>
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Variable Mapping Debug</summary>
<div class="space-y-3 text-xs">
<!-- Input Mappings -->
<div v-if="currentNode.data?.inputMappings?.length">
<p class="font-medium text-gray-700">Input Mappings (Process Form):</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.inputMappings, null, 2) }}</pre>
</div>
<!-- Output Mappings -->
<div v-if="currentNode.data?.outputMappings?.length">
<p class="font-medium text-gray-700">Output Mappings (Form Process):</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.outputMappings, null, 2) }}</pre>
</div>
<!-- Field Conditions -->
<div v-if="currentNode.data?.fieldConditions?.length">
<p class="font-medium text-gray-700">Field Conditions:</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.fieldConditions, null, 2) }}</pre>
</div>
<!-- Current Field States -->
<div v-if="Object.keys(fieldStates).length">
<p class="font-medium text-gray-700">Active Field States:</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(fieldStates, null, 2) }}</pre>
</div>
<!-- Form Data -->
<div v-if="Object.keys(formData).length">
<p class="font-medium text-gray-700">Current Form Data:</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(formData, null, 2) }}</pre>
</div>
</div>
</details>
</div>
<!-- Process Variables Debug (only in development) --> <!-- Process Variables Debug (only in development) -->
<div v-if="Object.keys(processVariables).length > 0" class="bg-gray-100 rounded-lg p-4"> <div v-if="Object.keys(processVariables).length > 0" class="bg-gray-100 rounded-lg p-4 mt-4">
<details> <details>
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Process Variables (Debug)</summary> <summary class="font-medium text-gray-700 cursor-pointer mb-2">Process Variables (Debug)</summary>
<pre class="text-xs text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(processVariables, null, 2) }}</pre> <pre class="text-xs text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(processVariables, null, 2) }}</pre>