Add API Call Node Configuration and Integration

- Introduced a new component, ApiNodeConfiguration, for configuring API call nodes within the process builder.
- Enhanced ProcessBuilderComponents to include the new API Call node type with default properties.
- Implemented ApiCallNode in ProcessFlowNodes for rendering API call nodes with relevant details.
- Added a backend endpoint for testing API node configurations, allowing users to validate API calls without executing the entire process.
- Updated VariableManager to default to global scope for new variables, ensuring consistency in variable management.
- Improved the overall process builder experience by integrating API call functionality and enhancing variable handling.
This commit is contained in:
Md Afiq Iskandar 2025-05-19 13:43:04 +08:00
parent 0b0b4a90b9
commit dce3e7f0f2
8 changed files with 806 additions and 69 deletions

View File

@ -0,0 +1,410 @@
<template>
<div class="api-node-configuration">
<h3 class="text-lg font-semibold mb-4">API Call Configuration</h3>
<div class="form-group mb-4">
<label for="nodeLabel" class="form-label">Node Label</label>
<input
id="nodeLabel"
v-model="localNodeData.label"
type="text"
class="form-control"
placeholder="API Call"
/>
</div>
<div class="form-group mb-4">
<label for="nodeDescription" class="form-label">Description</label>
<textarea
id="nodeDescription"
v-model="localNodeData.description"
class="form-control"
placeholder="API call description"
rows="2"
></textarea>
</div>
<div class="form-group mb-4">
<label for="apiMethod" class="form-label">HTTP Method</label>
<select
id="apiMethod"
v-model="localNodeData.apiMethod"
class="form-control"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<div class="form-group mb-4">
<label for="apiUrl" class="form-label">API URL</label>
<input
id="apiUrl"
v-model="localNodeData.apiUrl"
type="text"
class="form-control"
placeholder="https://example.com/api/endpoint"
/>
<small class="form-text text-muted">
You can use process variables with curly braces: https://example.com/api/users/{userId}
</small>
</div>
<div class="form-group mb-4" v-if="showRequestBody">
<label for="requestBody" class="form-label">Request Body</label>
<textarea
id="requestBody"
v-model="localNodeData.requestBody"
class="form-control font-mono"
placeholder='{ "key": "value" }'
rows="4"
></textarea>
<small class="form-text text-muted">
You can use process variables with curly braces: { "userId": "{userId}" }
</small>
</div>
<div class="form-group mb-4">
<label for="headers" class="form-label">Headers</label>
<textarea
id="headers"
v-model="localNodeData.headers"
class="form-control font-mono"
placeholder='{ "Content-Type": "application/json" }'
rows="3"
></textarea>
</div>
<div class="form-group mb-4">
<label for="outputVariable" class="form-label">Output Variable</label>
<div class="flex gap-2">
<select
id="outputVariable"
v-model="localNodeData.outputVariable"
class="form-control flex-grow"
>
<option value="" disabled>Select a global variable</option>
<option value="apiResponse">Create new: apiResponse</option>
<optgroup label="Global Variables">
<option
v-for="variable in availableVariables.global"
:key="variable.name"
:value="variable.name"
>
{{ variable.description ? `${variable.name} (${variable.description})` : variable.name }}
</option>
</optgroup>
</select>
<button
@click="createGlobalVariable(localNodeData.outputVariable)"
class="flex-shrink-0 px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
title="Create a new global variable"
>
<Icon name="material-symbols:add" />
</button>
</div>
<small class="form-text text-muted">
API response will be stored in this global variable
</small>
</div>
<div class="form-group mb-4">
<label class="form-label d-block">Error Handling</label>
<div class="form-check">
<input
id="continueOnError"
v-model="localNodeData.continueOnError"
type="checkbox"
class="form-check-input"
/>
<label for="continueOnError" class="form-check-label">
Continue process execution on error
</label>
</div>
</div>
<div class="form-group mb-4">
<label for="errorVariable" class="form-label">Error Variable</label>
<div class="flex gap-2">
<select
id="errorVariable"
v-model="localNodeData.errorVariable"
class="form-control flex-grow"
>
<option value="" disabled>Select a global variable</option>
<option value="apiError">Create new: apiError</option>
<optgroup label="Global Variables">
<option
v-for="variable in availableVariables.global"
:key="variable.name"
:value="variable.name"
>
{{ variable.description ? `${variable.name} (${variable.description})` : variable.name }}
</option>
</optgroup>
</select>
<button
@click="createGlobalVariable(localNodeData.errorVariable, `API error from ${localNodeData.label}`)"
class="flex-shrink-0 px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
title="Create a new global variable"
>
<Icon name="material-symbols:add" />
</button>
</div>
<small class="form-text text-muted">
API errors will be stored in this global variable
</small>
</div>
<!-- Test API Call Button -->
<div class="form-group mt-6">
<RsButton @click="testApiCall" variant="primary" :disabled="!localNodeData.apiUrl">
<Icon name="material-symbols:send" class="mr-1" />
Test API Call
</RsButton>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { useProcessBuilderStore } from '@/stores/processBuilder';
import { useVariableStore } from '@/stores/variableStore';
const props = defineProps({
nodeId: {
type: String,
required: true
}
});
// Get the stores
const processStore = useProcessBuilderStore();
const variableStore = useVariableStore();
// Local state for node data
const localNodeData = ref({
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
const showRequestBody = computed(() => {
return ['POST', 'PUT', 'PATCH'].includes(localNodeData.value.apiMethod);
});
// Get available variables for dropdowns
const availableVariables = computed(() => {
const globalVars = variableStore.getAllVariables.global.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, global)`
: `${v.name || 'unnamed'} (global)`,
type: v.type || 'string',
scope: 'global'
}));
return {
global: globalVars
};
});
// Load node data when component mounts or nodeId changes
watch(() => props.nodeId, () => {
loadNodeData();
}, { immediate: true });
// Watch for changes in local data and emit updates
watch(localNodeData, (newValue) => {
saveChanges();
}, { deep: true });
// Load node data from the store
function loadNodeData() {
const node = processStore.currentProcess.nodes.find(n => n.id === props.nodeId);
if (node && node.data) {
localNodeData.value = {
label: node.label || 'API Call',
description: node.data.description || '',
apiMethod: node.data.apiMethod || 'GET',
apiUrl: node.data.apiUrl || '',
requestBody: node.data.requestBody || '',
headers: node.data.headers || '{ "Content-Type": "application/json" }',
outputVariable: node.data.outputVariable || 'apiResponse',
continueOnError: node.data.continueOnError || false,
errorVariable: node.data.errorVariable || 'apiError'
};
}
}
// Function to create a new global variable
function createGlobalVariable(name, description = '') {
variableStore.addVariable({
name,
type: 'object',
scope: 'global',
value: null,
description: description || `API response from ${localNodeData.value.label}`
});
}
// Save changes to the store
function saveChanges() {
const updates = {
label: localNodeData.value.label,
data: {
...localNodeData.value,
label: localNodeData.value.label // Ensure label is in both places
}
};
// Update the node in the store
processStore.updateNode(props.nodeId, updates);
// Ensure variables exist
if (localNodeData.value.outputVariable) {
variableStore.addVariableIfNotExists({
name: localNodeData.value.outputVariable,
type: 'object',
scope: 'global',
value: null,
description: `API response from ${localNodeData.value.label}`
});
}
if (localNodeData.value.errorVariable) {
variableStore.addVariableIfNotExists({
name: localNodeData.value.errorVariable,
type: 'object',
scope: 'global',
value: null,
description: `API error from ${localNodeData.value.label}`
});
}
}
// Test API call function
async function testApiCall() {
if (!localNodeData.value.apiUrl) return;
const isLoading = ref(true);
const testResult = ref(null);
try {
// Get process variables for substitution
const processVariables = {
global: {},
process: {}
};
// Extract variables from the store
const allVars = variableStore.getAllVariables;
allVars.process.forEach(v => {
processVariables.process[v.name] = v.value;
});
allVars.global.forEach(v => {
processVariables.global[v.name] = v.value;
});
// Call the test API endpoint
const response = await fetch('/api/process/test-api-node', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
nodeConfig: {
apiMethod: localNodeData.value.apiMethod,
apiUrl: localNodeData.value.apiUrl,
requestBody: localNodeData.value.requestBody,
headers: localNodeData.value.headers,
outputVariable: localNodeData.value.outputVariable,
errorVariable: localNodeData.value.errorVariable,
continueOnError: localNodeData.value.continueOnError
},
processVariables
})
});
const result = await response.json();
testResult.value = result;
if (result.success && localNodeData.value.outputVariable) {
variableStore.updateVariable(
localNodeData.value.outputVariable,
{ value: result.data },
'global'
);
} else if (!result.success && localNodeData.value.errorVariable) {
variableStore.updateVariable(
localNodeData.value.errorVariable,
{ value: result.error },
'global'
);
}
} catch (error) {
testResult.value = {
success: false,
error: {
message: error.message || 'An error occurred while testing the API call'
}
};
} finally {
isLoading.value = false;
}
}
</script>
<style scoped>
.api-node-configuration {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
line-height: 1.5;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
.form-text {
display: block;
margin-top: 0.25rem;
font-size: 0.75rem;
}
.form-check {
display: flex;
align-items: center;
}
.form-check-input {
margin-right: 0.5rem;
}
.font-mono {
font-family: monospace;
}
</style>

View File

@ -99,6 +99,26 @@ const availableComponents = [
}
}
},
{
type: 'api',
name: 'API Call',
category: 'Core',
icon: 'material-symbols:api',
description: 'Make external API calls',
defaultProps: {
label: 'API Call',
data: {
description: 'External API call',
apiMethod: 'GET',
apiUrl: '',
requestBody: '',
headers: '{ "Content-Type": "application/json" }',
outputVariable: 'apiResponse',
continueOnError: false,
errorVariable: 'apiError'
}
}
},
{
type: 'gateway',
name: 'Decision Point',

View File

@ -271,6 +271,54 @@ export const ScriptNode = markRaw({
}
});
// API Call node
export const ApiCallNode = markRaw({
props: ['id', 'type', 'label', 'selected', 'data'],
computed: {
nodeLabel() {
return this.label || (this.data && this.data.label) || 'API Call';
},
apiUrl() {
return this.data?.apiUrl || 'No URL specified';
},
apiMethod() {
return this.data?.apiMethod || 'GET';
}
},
render() {
const badgeContent = h('span', {
class: 'node-badge bg-indigo-100 text-indigo-600 px-1 text-xs rounded'
}, 'API');
return h(CustomNode, {
id: this.id,
type: 'api',
label: this.nodeLabel,
selected: this.selected,
data: this.data,
onClick: () => this.$emit('node-click', this.id)
}, {
icon: () => h('i', { class: 'material-icons text-indigo-500' }, 'api'),
badge: () => badgeContent,
default: () => h('div', { class: 'node-details' }, [
h('p', { class: 'node-description' }, this.data?.description || 'External API call'),
h('div', { class: 'node-api-info' }, [
h('span', { class: 'node-api-label' }, 'URL: '),
h('span', {
class: 'node-api-value text-indigo-600 font-medium'
}, this.apiUrl)
]),
h('div', { class: 'node-api-method-info' }, [
h('span', { class: 'node-api-method-label' }, 'Method: '),
h('span', {
class: 'node-api-method-value'
}, this.apiMethod)
])
])
});
}
});
// Export the node types object to use with Vue Flow
export const nodeTypes = markRaw({
task: TaskNode,
@ -278,7 +326,8 @@ export const nodeTypes = markRaw({
end: EndNode,
gateway: GatewayNode,
form: FormNode,
script: ScriptNode
script: ScriptNode,
api: ApiCallNode
});
// Default CSS for the nodes to be imported where needed
@ -297,7 +346,7 @@ export const nodeStyles = `
}
/* Base styles for different node types */
.node-task, .node-form, .node-script {
.node-task, .node-form, .node-script, .node-api {
width: 180px;
background: white;
border-radius: 4px;
@ -394,7 +443,8 @@ export const nodeStyles = `
.node-task .custom-node-title,
.node-form .custom-node-title,
.node-script .custom-node-title {
.node-script .custom-node-title,
.node-api .custom-node-title {
font-weight: 500;
font-size: 11px;
display: flex;
@ -461,6 +511,8 @@ export const nodeStyles = `
.node-assignee,
.node-form-info,
.node-script-info,
.node-api-info,
.node-api-method-info,
.node-conditions {
display: flex;
font-size: 10px;
@ -470,13 +522,17 @@ export const nodeStyles = `
.node-assignee-label,
.node-form-label,
.node-script-label {
.node-script-label,
.node-api-label,
.node-api-method-label {
font-weight: 500;
margin-right: 4px;
}
.node-form-value,
.node-script-value,
.node-api-value,
.node-api-method-value,
.node-assignee-value {
white-space: nowrap;
overflow: hidden;
@ -507,6 +563,7 @@ export const nodeStyles = `
.handle-task-input,
.handle-form-input,
.handle-script-input,
.handle-api-input,
.handle-gateway-input {
top: -5px !important;
width: 8px !important;
@ -517,6 +574,7 @@ export const nodeStyles = `
.handle-task-output,
.handle-form-output,
.handle-script-output,
.handle-api-output,
.handle-gateway-output {
bottom: -5px !important;
width: 8px !important;

View File

@ -56,14 +56,6 @@
<span class="font-medium text-gray-900">{{
variable.name
}}</span>
<RsBadge
:variant="
variable.scope === 'global' ? 'primary' : 'secondary'
"
size="sm"
>
{{ variable.scope }}
</RsBadge>
<RsBadge variant="outline" size="sm" class="text-gray-500">
{{ variable.type }}
</RsBadge>
@ -129,12 +121,11 @@
label="Type"
:options="[
{ label: 'String', value: 'string' },
{ label: 'Number', value: 'number' },
{ label: 'Boolean', value: 'boolean' },
{ label: 'Int', value: 'int' },
{ label: 'Decimal', value: 'decimal' },
{ label: 'Object', value: 'object' },
{ label: 'Array', value: 'array' },
{ label: 'Date', value: 'date' },
{ label: 'File', value: 'file' },
{ label: 'DateTime', value: 'datetime' },
{ label: 'Date', value: 'date' }
]"
validation="required"
:validation-messages="{
@ -142,21 +133,6 @@
}"
/>
<FormKit
name="scope"
v-model="variableForm.scope"
type="select"
label="Scope"
:options="[
{ label: 'Process', value: 'process' },
{ label: 'Global', value: 'global' },
]"
validation="required"
:validation-messages="{
required: 'Variable scope is required',
}"
/>
<FormKit
name="description"
v-model="variableForm.description"
@ -166,14 +142,6 @@
:rows="2"
/>
<FormKit
name="isRequired"
v-model="variableForm.isRequired"
type="checkbox"
label="Required"
help="Mark this variable as required"
/>
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200">
<RsButton type="button" @click="closeModal" variant="tertiary">
Cancel
@ -199,19 +167,14 @@ const editingVariable = ref(null);
const variableForm = ref({
name: "",
type: "string",
scope: "process",
scope: "global",
description: "",
isRequired: false,
});
// Computed
const variables = computed(() => {
// This was only returning process variables, let's fix it to return both process and global variables
const allVars = [
...variableStore.getAllVariables.process,
...variableStore.getAllVariables.global,
];
return allVars;
// Only return global variables
return variableStore.getAllVariables.global;
});
// Methods
@ -223,7 +186,7 @@ const editVariable = (variable) => {
const deleteVariable = (variable) => {
if (confirm(`Are you sure you want to delete ${variable.name}?`)) {
variableStore.deleteVariable(variable.name, variable.scope);
variableStore.deleteVariable(variable.name, 'global');
}
};
@ -231,9 +194,8 @@ const resetForm = () => {
variableForm.value = {
name: "",
type: "string",
scope: "process",
description: "",
isRequired: false,
scope: "global",
description: ""
};
editingVariable.value = null;
};
@ -249,9 +211,8 @@ const saveVariable = async (formData) => {
const newVariable = {
name: formData.name,
type: formData.type,
scope: formData.scope,
description: formData.description,
isRequired: formData.isRequired,
scope: "global",
description: formData.description
};
if (editingVariable.value) {
@ -259,7 +220,7 @@ const saveVariable = async (formData) => {
variableStore.updateVariable(
editingVariable.value.name,
newVariable,
newVariable.scope
"global"
);
} else {
// Add new variable

View File

@ -7,6 +7,7 @@ import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue';
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
import FormSelector from '~/components/process-flow/FormSelector.vue';
import GatewayConditionManager from '~/components/process-flow/GatewayConditionManager.vue';
import ApiNodeConfiguration from '~/components/process-flow/ApiNodeConfiguration.vue';
import VariableManager from '~/components/process-flow/VariableManager.vue';
import { onBeforeRouteLeave } from 'vue-router';
@ -74,6 +75,22 @@ const components = [
iconColor: 'text-purple-500',
data: { description: 'Form submission', formName: '' }
},
{
type: 'api',
label: 'API Call',
icon: 'api',
iconColor: 'text-indigo-500',
data: {
description: 'External API call',
apiMethod: 'GET',
apiUrl: '',
requestBody: '',
headers: '{ "Content-Type": "application/json" }',
outputVariable: 'apiResponse',
continueOnError: false,
errorVariable: 'apiError'
}
},
{
type: 'gateway',
label: 'Decision Point',
@ -724,6 +741,13 @@ const onConditionsUpdated = (conditions) => {
/>
</div>
<!-- API Configuration for API Nodes -->
<div v-if="selectedNodeData.type === 'api'">
<ApiNodeConfiguration
:nodeId="selectedNodeData.id"
/>
</div>
<!-- Gateway Conditions -->
<div v-if="selectedNodeData.type === 'gateway'">
<GatewayConditionManager

View File

@ -0,0 +1,73 @@
import { executeApiCall } from '../../../services/apiNodeService';
/**
* Test API Node Endpoint
*
* This endpoint allows testing API node configurations without executing
* the entire process. It takes the node configuration and process variables
* as input and returns the API call result.
*/
export default defineEventHandler(async (event) => {
try {
// Get request body
const body = await readBody(event);
// Extract node configuration and process variables
const { nodeConfig, processVariables } = body;
// Validate input
if (!nodeConfig || !nodeConfig.apiUrl) {
return {
success: false,
error: {
message: 'Invalid API node configuration. Missing apiUrl.'
}
};
}
// Structure the variables for the API call
const structuredVariables = {
global: {},
process: {},
...processVariables
};
// Categorize variables by scope if they're not already structured
if (!processVariables.global && !processVariables.process) {
Object.entries(processVariables || {}).forEach(([name, value]) => {
// Determine which variables are global based on the presence of an actual variable in the store
// This would usually be handled by the process execution engine
if (name === nodeConfig.outputVariable || name === nodeConfig.errorVariable) {
structuredVariables.global[name] = value;
} else {
structuredVariables.process[name] = value;
}
});
}
// Execute the API call
const result = await executeApiCall(nodeConfig, structuredVariables);
// Update global variables with the result
if (result.success && nodeConfig.outputVariable) {
structuredVariables.global[nodeConfig.outputVariable] = result.data;
} else if (!result.success && nodeConfig.errorVariable) {
structuredVariables.global[nodeConfig.errorVariable] = result.error;
}
// Add the updated variables to the result
result.variables = structuredVariables;
// Return the result
return result;
} catch (error) {
// Handle errors
return {
success: false,
error: {
message: error.message || 'An error occurred while testing the API node',
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
}
};
}
});

173
services/apiNodeService.js Normal file
View File

@ -0,0 +1,173 @@
/**
* API Node Service
*
* This service handles the execution of API Call nodes in the process flow.
* It supports dynamic variable substitution in URLs, headers, and request bodies.
*/
// Helper function to substitute process variables in a string
// Example: "Hello {name}" with {name: "World"} becomes "Hello World"
function substituteVariables(text, variables) {
if (!text || typeof text !== 'string') return text;
return text.replace(/{([^{}]+)}/g, (match, variableName) => {
const variable = variables[variableName.trim()];
return variable !== undefined ? variable : match;
});
}
// Helper function to substitute variables in a JSON object or string
function substituteVariablesInObject(obj, variables) {
if (typeof obj === 'string') {
try {
// If it's a JSON string, parse it, substitute, then stringify
const parsed = JSON.parse(obj);
return JSON.stringify(substituteVariablesInObject(parsed, variables));
} catch (e) {
// If it's not valid JSON, treat as regular string
return substituteVariables(obj, variables);
}
}
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// Handle arrays
if (Array.isArray(obj)) {
return obj.map(item => substituteVariablesInObject(item, variables));
}
// Handle objects
const result = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = substituteVariablesInObject(value, variables);
}
return result;
}
// Execute an API call based on node configuration
export async function executeApiCall(nodeConfig, processVariables) {
const {
apiMethod = 'GET',
apiUrl = '',
requestBody = '',
headers = '{ "Content-Type": "application/json" }',
outputVariable = 'apiResponse',
errorVariable = 'apiError',
continueOnError = false
} = nodeConfig;
// Combine global and process variables
const allVariables = {
...processVariables.global || {},
...processVariables.process || {},
...processVariables
};
// Substitute variables in URL
const processedUrl = substituteVariables(apiUrl, allVariables);
// Parse and process headers
let processedHeaders = {};
try {
if (typeof headers === 'string') {
processedHeaders = JSON.parse(headers);
} else if (typeof headers === 'object' && headers !== null) {
processedHeaders = headers;
}
// Substitute variables in headers
processedHeaders = substituteVariablesInObject(processedHeaders, allVariables);
} catch (error) {
console.error('Error processing headers:', error);
processedHeaders = { 'Content-Type': 'application/json' };
}
// Prepare request options
const options = {
method: apiMethod,
headers: processedHeaders
};
// Add request body for appropriate methods
if (['POST', 'PUT', 'PATCH'].includes(apiMethod) && requestBody) {
let processedBody;
try {
// Try to parse as JSON if it's a string
if (typeof requestBody === 'string') {
const bodyObj = JSON.parse(requestBody);
processedBody = substituteVariablesInObject(bodyObj, allVariables);
options.body = JSON.stringify(processedBody);
} else {
// If it's already an object
processedBody = substituteVariablesInObject(requestBody, allVariables);
options.body = JSON.stringify(processedBody);
}
} catch (error) {
// If not valid JSON, treat as string with variable substitution
options.body = substituteVariables(requestBody, allVariables);
}
}
// Create result object
const result = {
outputVariable,
errorVariable,
continueOnError,
outputScope: 'global', // Specify that output should go to global variables
success: false,
data: null,
error: null
};
try {
// Make the API call
const response = await fetch(processedUrl, options);
// Get response data
let responseData;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
responseData = await response.json();
} else {
responseData = await response.text();
}
// Store result data
result.success = response.ok;
result.data = {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries([...response.headers.entries()]),
data: responseData
};
// If not successful, also store as error
if (!response.ok) {
result.error = {
message: `API call failed with status ${response.status}`,
status: response.status,
statusText: response.statusText,
data: responseData
};
}
} catch (error) {
// Handle network or other errors
result.success = false;
result.error = {
message: error.message || 'Unknown error occurred during API call',
stack: error.stack,
name: error.name
};
}
return result;
}
export default {
executeApiCall
};

View File

@ -37,23 +37,40 @@ export const useVariableStore = defineStore('variables', {
actions: {
// Add a new variable
addVariable(variable) {
const { scope = 'process' } = variable;
// Set scope to global by default
const updatedVariable = {
...variable,
scope: 'global'
};
if (scope === 'global') {
this.globalVariables.push({
...variable,
scope: 'global'
});
// For backward compatibility, still respect the scope if it's explicitly set
if (variable.scope === 'process') {
this.processVariables.push(updatedVariable);
} else {
this.processVariables.push({
...variable,
scope: 'process'
});
this.globalVariables.push(updatedVariable);
}
},
// Add a variable only if it doesn't already exist
addVariableIfNotExists(variable) {
const name = variable.name;
// Default to global scope
const scope = variable.scope || 'global';
const variables = scope === 'global'
? this.globalVariables
: this.processVariables;
// Check if variable already exists
const exists = variables.some(v => v.name === name);
// If it doesn't exist, add it
if (!exists) {
this.addVariable(variable);
}
},
// Update an existing variable
updateVariable(name, updates, scope = 'process') {
updateVariable(name, updates, scope = 'global') {
const variables = scope === 'global'
? this.globalVariables
: this.processVariables;
@ -62,13 +79,14 @@ export const useVariableStore = defineStore('variables', {
if (index !== -1) {
variables[index] = {
...variables[index],
...updates
...updates,
scope // Ensure the scope is preserved
};
}
},
// Delete a variable
deleteVariable(name, scope = 'process') {
deleteVariable(name, scope = 'global') {
const variables = scope === 'global'
? this.globalVariables
: this.processVariables;