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:
parent
0b0b4a90b9
commit
dce3e7f0f2
410
components/process-flow/ApiNodeConfiguration.vue
Normal file
410
components/process-flow/ApiNodeConfiguration.vue
Normal 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>
|
@ -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',
|
type: 'gateway',
|
||||||
name: 'Decision Point',
|
name: 'Decision Point',
|
||||||
|
@ -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 the node types object to use with Vue Flow
|
||||||
export const nodeTypes = markRaw({
|
export const nodeTypes = markRaw({
|
||||||
task: TaskNode,
|
task: TaskNode,
|
||||||
@ -278,7 +326,8 @@ export const nodeTypes = markRaw({
|
|||||||
end: EndNode,
|
end: EndNode,
|
||||||
gateway: GatewayNode,
|
gateway: GatewayNode,
|
||||||
form: FormNode,
|
form: FormNode,
|
||||||
script: ScriptNode
|
script: ScriptNode,
|
||||||
|
api: ApiCallNode
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default CSS for the nodes to be imported where needed
|
// Default CSS for the nodes to be imported where needed
|
||||||
@ -297,7 +346,7 @@ export const nodeStyles = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Base styles for different node types */
|
/* Base styles for different node types */
|
||||||
.node-task, .node-form, .node-script {
|
.node-task, .node-form, .node-script, .node-api {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -394,7 +443,8 @@ export const nodeStyles = `
|
|||||||
|
|
||||||
.node-task .custom-node-title,
|
.node-task .custom-node-title,
|
||||||
.node-form .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-weight: 500;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -461,6 +511,8 @@ export const nodeStyles = `
|
|||||||
.node-assignee,
|
.node-assignee,
|
||||||
.node-form-info,
|
.node-form-info,
|
||||||
.node-script-info,
|
.node-script-info,
|
||||||
|
.node-api-info,
|
||||||
|
.node-api-method-info,
|
||||||
.node-conditions {
|
.node-conditions {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@ -470,13 +522,17 @@ export const nodeStyles = `
|
|||||||
|
|
||||||
.node-assignee-label,
|
.node-assignee-label,
|
||||||
.node-form-label,
|
.node-form-label,
|
||||||
.node-script-label {
|
.node-script-label,
|
||||||
|
.node-api-label,
|
||||||
|
.node-api-method-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-form-value,
|
.node-form-value,
|
||||||
.node-script-value,
|
.node-script-value,
|
||||||
|
.node-api-value,
|
||||||
|
.node-api-method-value,
|
||||||
.node-assignee-value {
|
.node-assignee-value {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -507,6 +563,7 @@ export const nodeStyles = `
|
|||||||
.handle-task-input,
|
.handle-task-input,
|
||||||
.handle-form-input,
|
.handle-form-input,
|
||||||
.handle-script-input,
|
.handle-script-input,
|
||||||
|
.handle-api-input,
|
||||||
.handle-gateway-input {
|
.handle-gateway-input {
|
||||||
top: -5px !important;
|
top: -5px !important;
|
||||||
width: 8px !important;
|
width: 8px !important;
|
||||||
@ -517,6 +574,7 @@ export const nodeStyles = `
|
|||||||
.handle-task-output,
|
.handle-task-output,
|
||||||
.handle-form-output,
|
.handle-form-output,
|
||||||
.handle-script-output,
|
.handle-script-output,
|
||||||
|
.handle-api-output,
|
||||||
.handle-gateway-output {
|
.handle-gateway-output {
|
||||||
bottom: -5px !important;
|
bottom: -5px !important;
|
||||||
width: 8px !important;
|
width: 8px !important;
|
||||||
|
@ -56,14 +56,6 @@
|
|||||||
<span class="font-medium text-gray-900">{{
|
<span class="font-medium text-gray-900">{{
|
||||||
variable.name
|
variable.name
|
||||||
}}</span>
|
}}</span>
|
||||||
<RsBadge
|
|
||||||
:variant="
|
|
||||||
variable.scope === 'global' ? 'primary' : 'secondary'
|
|
||||||
"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{{ variable.scope }}
|
|
||||||
</RsBadge>
|
|
||||||
<RsBadge variant="outline" size="sm" class="text-gray-500">
|
<RsBadge variant="outline" size="sm" class="text-gray-500">
|
||||||
{{ variable.type }}
|
{{ variable.type }}
|
||||||
</RsBadge>
|
</RsBadge>
|
||||||
@ -129,12 +121,11 @@
|
|||||||
label="Type"
|
label="Type"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: 'String', value: 'string' },
|
{ label: 'String', value: 'string' },
|
||||||
{ label: 'Number', value: 'number' },
|
{ label: 'Int', value: 'int' },
|
||||||
{ label: 'Boolean', value: 'boolean' },
|
{ label: 'Decimal', value: 'decimal' },
|
||||||
{ label: 'Object', value: 'object' },
|
{ label: 'Object', value: 'object' },
|
||||||
{ label: 'Array', value: 'array' },
|
{ label: 'DateTime', value: 'datetime' },
|
||||||
{ label: 'Date', value: 'date' },
|
{ label: 'Date', value: 'date' }
|
||||||
{ label: 'File', value: 'file' },
|
|
||||||
]"
|
]"
|
||||||
validation="required"
|
validation="required"
|
||||||
:validation-messages="{
|
: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
|
<FormKit
|
||||||
name="description"
|
name="description"
|
||||||
v-model="variableForm.description"
|
v-model="variableForm.description"
|
||||||
@ -166,14 +142,6 @@
|
|||||||
:rows="2"
|
: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">
|
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200">
|
||||||
<RsButton type="button" @click="closeModal" variant="tertiary">
|
<RsButton type="button" @click="closeModal" variant="tertiary">
|
||||||
Cancel
|
Cancel
|
||||||
@ -199,19 +167,14 @@ const editingVariable = ref(null);
|
|||||||
const variableForm = ref({
|
const variableForm = ref({
|
||||||
name: "",
|
name: "",
|
||||||
type: "string",
|
type: "string",
|
||||||
scope: "process",
|
scope: "global",
|
||||||
description: "",
|
description: "",
|
||||||
isRequired: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const variables = computed(() => {
|
const variables = computed(() => {
|
||||||
// This was only returning process variables, let's fix it to return both process and global variables
|
// Only return global variables
|
||||||
const allVars = [
|
return variableStore.getAllVariables.global;
|
||||||
...variableStore.getAllVariables.process,
|
|
||||||
...variableStore.getAllVariables.global,
|
|
||||||
];
|
|
||||||
return allVars;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
@ -223,7 +186,7 @@ const editVariable = (variable) => {
|
|||||||
|
|
||||||
const deleteVariable = (variable) => {
|
const deleteVariable = (variable) => {
|
||||||
if (confirm(`Are you sure you want to delete ${variable.name}?`)) {
|
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 = {
|
variableForm.value = {
|
||||||
name: "",
|
name: "",
|
||||||
type: "string",
|
type: "string",
|
||||||
scope: "process",
|
scope: "global",
|
||||||
description: "",
|
description: ""
|
||||||
isRequired: false,
|
|
||||||
};
|
};
|
||||||
editingVariable.value = null;
|
editingVariable.value = null;
|
||||||
};
|
};
|
||||||
@ -249,9 +211,8 @@ const saveVariable = async (formData) => {
|
|||||||
const newVariable = {
|
const newVariable = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
type: formData.type,
|
type: formData.type,
|
||||||
scope: formData.scope,
|
scope: "global",
|
||||||
description: formData.description,
|
description: formData.description
|
||||||
isRequired: formData.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingVariable.value) {
|
if (editingVariable.value) {
|
||||||
@ -259,7 +220,7 @@ const saveVariable = async (formData) => {
|
|||||||
variableStore.updateVariable(
|
variableStore.updateVariable(
|
||||||
editingVariable.value.name,
|
editingVariable.value.name,
|
||||||
newVariable,
|
newVariable,
|
||||||
newVariable.scope
|
"global"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Add new variable
|
// Add new variable
|
||||||
|
@ -7,6 +7,7 @@ import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue';
|
|||||||
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
|
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
|
||||||
import FormSelector from '~/components/process-flow/FormSelector.vue';
|
import FormSelector from '~/components/process-flow/FormSelector.vue';
|
||||||
import GatewayConditionManager from '~/components/process-flow/GatewayConditionManager.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 VariableManager from '~/components/process-flow/VariableManager.vue';
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
|
|
||||||
@ -74,6 +75,22 @@ const components = [
|
|||||||
iconColor: 'text-purple-500',
|
iconColor: 'text-purple-500',
|
||||||
data: { description: 'Form submission', formName: '' }
|
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',
|
type: 'gateway',
|
||||||
label: 'Decision Point',
|
label: 'Decision Point',
|
||||||
@ -724,6 +741,13 @@ const onConditionsUpdated = (conditions) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API Configuration for API Nodes -->
|
||||||
|
<div v-if="selectedNodeData.type === 'api'">
|
||||||
|
<ApiNodeConfiguration
|
||||||
|
:nodeId="selectedNodeData.id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Gateway Conditions -->
|
<!-- Gateway Conditions -->
|
||||||
<div v-if="selectedNodeData.type === 'gateway'">
|
<div v-if="selectedNodeData.type === 'gateway'">
|
||||||
<GatewayConditionManager
|
<GatewayConditionManager
|
||||||
|
73
server/api/process/test-api-node.post.js
Normal file
73
server/api/process/test-api-node.post.js
Normal 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
173
services/apiNodeService.js
Normal 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
|
||||||
|
};
|
@ -37,23 +37,40 @@ export const useVariableStore = defineStore('variables', {
|
|||||||
actions: {
|
actions: {
|
||||||
// Add a new variable
|
// Add a new variable
|
||||||
addVariable(variable) {
|
addVariable(variable) {
|
||||||
const { scope = 'process' } = variable;
|
// Set scope to global by default
|
||||||
|
const updatedVariable = {
|
||||||
|
...variable,
|
||||||
|
scope: 'global'
|
||||||
|
};
|
||||||
|
|
||||||
if (scope === 'global') {
|
// For backward compatibility, still respect the scope if it's explicitly set
|
||||||
this.globalVariables.push({
|
if (variable.scope === 'process') {
|
||||||
...variable,
|
this.processVariables.push(updatedVariable);
|
||||||
scope: 'global'
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.processVariables.push({
|
this.globalVariables.push(updatedVariable);
|
||||||
...variable,
|
}
|
||||||
scope: 'process'
|
},
|
||||||
});
|
|
||||||
|
// 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
|
// Update an existing variable
|
||||||
updateVariable(name, updates, scope = 'process') {
|
updateVariable(name, updates, scope = 'global') {
|
||||||
const variables = scope === 'global'
|
const variables = scope === 'global'
|
||||||
? this.globalVariables
|
? this.globalVariables
|
||||||
: this.processVariables;
|
: this.processVariables;
|
||||||
@ -62,13 +79,14 @@ export const useVariableStore = defineStore('variables', {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
variables[index] = {
|
variables[index] = {
|
||||||
...variables[index],
|
...variables[index],
|
||||||
...updates
|
...updates,
|
||||||
|
scope // Ensure the scope is preserved
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Delete a variable
|
// Delete a variable
|
||||||
deleteVariable(name, scope = 'process') {
|
deleteVariable(name, scope = 'global') {
|
||||||
const variables = scope === 'global'
|
const variables = scope === 'global'
|
||||||
? this.globalVariables
|
? this.globalVariables
|
||||||
: this.processVariables;
|
: this.processVariables;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user