- 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.
755 lines
28 KiB
Vue
755 lines
28 KiB
Vue
<template>
|
|
<div class="api-node-configuration">
|
|
<!-- Step 1: Basic configuration -->
|
|
<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">1</span>
|
|
</div>
|
|
<h4 class="font-medium">Basic Configuration</h4>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<!-- HTTP Method -->
|
|
<div>
|
|
<label for="apiMethod" class="block text-sm font-medium text-gray-700 mb-1">HTTP Method</label>
|
|
<select
|
|
id="apiMethod"
|
|
v-model="localNodeData.apiMethod"
|
|
class="w-full p-2 border rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 text-sm"
|
|
@change="saveChanges"
|
|
>
|
|
<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>
|
|
<option value="HEAD">HEAD</option>
|
|
<option value="OPTIONS">OPTIONS</option>
|
|
</select>
|
|
<p class="mt-1 text-xs text-gray-500">
|
|
The HTTP method determines how the API call interacts with the endpoint.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- API URL -->
|
|
<div>
|
|
<label for="apiUrl" class="block text-sm font-medium text-gray-700 mb-1">API URL</label>
|
|
<input
|
|
id="apiUrl"
|
|
v-model="localNodeData.apiUrl"
|
|
type="text"
|
|
class="w-full p-2 border rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 text-sm"
|
|
placeholder="https://example.com/api/endpoint"
|
|
@blur="saveChanges"
|
|
/>
|
|
<p class="mt-1 text-xs text-gray-500">
|
|
Use variables with <code class="bg-gray-100 px-1">{variableName}</code> syntax, e.g.: <code class="bg-gray-100 px-1">https://api.example.com/users/{userId}</code>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Authorization -->
|
|
<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">2</span>
|
|
</div>
|
|
<h4 class="font-medium">Authorization</h4>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Type</label>
|
|
<select v-if="localNodeData.authorization" v-model="localNodeData.authorization.type" class="w-48 p-2 border rounded text-sm" @change="saveChanges">
|
|
<option v-for="type in AUTH_TYPES" :key="type.value" :value="type.value">{{ type.label }}</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="localNodeData.authorization && localNodeData.authorization.type" v-for="field in getAuthFields()" :key="field.key" class="mb-2">
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">{{ field.label }}</label>
|
|
<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" />
|
|
<select v-else v-model="localNodeData.authorization[field.key]" class="w-full px-2 py-1 border rounded text-sm" @change="saveChanges">
|
|
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Params (GET, DELETE, etc.) -->
|
|
<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="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>
|
|
</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>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<!-- Output Variable -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Output Variable</label>
|
|
<div class="flex gap-2">
|
|
<VariableBrowser
|
|
v-model="localNodeData.outputVariable"
|
|
:availableVariables="availableVariables"
|
|
:allowCreate="true"
|
|
@change="saveChanges"
|
|
/>
|
|
</div>
|
|
<p class="mt-1 text-xs text-gray-500">
|
|
API response will be stored in this global variable for use in later steps
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Error Variable -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Error Variable</label>
|
|
<div class="flex gap-2">
|
|
<VariableBrowser
|
|
v-model="localNodeData.errorVariable"
|
|
:availableVariables="availableVariables"
|
|
:allowCreate="true"
|
|
@change="saveChanges"
|
|
/>
|
|
</div>
|
|
<p class="mt-1 text-xs text-gray-500">
|
|
Any API errors will be stored in this variable for error handling
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Continue on Error -->
|
|
<div class="mt-4">
|
|
<label class="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
v-model="localNodeData.continueOnError"
|
|
class="form-checkbox h-4 w-4 text-indigo-600 focus:ring-indigo-500"
|
|
@change="saveChanges"
|
|
/>
|
|
<span class="ml-2 text-sm">Continue workflow execution even if API call fails</span>
|
|
</label>
|
|
<p class="mt-1 pl-6 text-xs text-gray-500">
|
|
When enabled, the process will continue to the next step even if this API call fails
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 7: Test API Call -->
|
|
<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">7</span>
|
|
</div>
|
|
<h4 class="font-medium">Test API Call</h4>
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-600 mb-3">
|
|
Test your API configuration to verify it works as expected before saving
|
|
</p>
|
|
|
|
<div class="flex items-center gap-4">
|
|
<RsButton @click="testApiCall" variant="primary" :disabled="!localNodeData.apiUrl || isLoading">
|
|
<Icon name="material-symbols:send" class="mr-1" />
|
|
{{ isLoading ? 'Testing...' : 'Test API Call' }}
|
|
</RsButton>
|
|
<div v-if="isLoading" class="text-gray-600 text-sm flex items-center">
|
|
<Icon name="material-symbols:sync" class="animate-spin mr-2" />
|
|
Testing API endpoint...
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Test Results -->
|
|
<div v-if="testResult" class="mt-4 p-4 rounded-md border"
|
|
:class="testResult.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'"
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex items-center">
|
|
<Icon
|
|
:name="testResult.success ? 'material-symbols:check-circle' : 'material-symbols:error'"
|
|
:class="testResult.success ? 'text-green-500' : 'text-red-500'"
|
|
class="w-5 h-5 mr-2"
|
|
/>
|
|
<span :class="testResult.success ? 'text-green-700' : 'text-red-700'" class="font-medium">
|
|
{{ testResult.success ? 'API Call Successful' : 'API Call Failed' }}
|
|
</span>
|
|
</div>
|
|
<button @click="testResult = null" class="text-gray-400 hover:text-gray-600">
|
|
<Icon name="material-symbols:close" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Success Response -->
|
|
<div v-if="testResult.success" class="mt-3">
|
|
<div class="text-sm text-gray-600 mb-2">Response stored in variable: <code class="bg-green-100 px-1">{{ localNodeData.outputVariable }}</code></div>
|
|
<div class="bg-white border border-green-100 rounded p-3 max-h-60 overflow-auto">
|
|
<pre class="text-xs font-mono whitespace-pre-wrap">{{ JSON.stringify(testResult.data, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Response -->
|
|
<div v-else class="mt-3">
|
|
<div class="text-sm text-red-600 mb-2">Error stored in variable: <code class="bg-red-100 px-1">{{ localNodeData.errorVariable }}</code></div>
|
|
<div class="bg-white border border-red-100 rounded p-3 max-h-60 overflow-auto">
|
|
<pre class="text-xs font-mono whitespace-pre-wrap text-red-600">{{ JSON.stringify(testResult.error, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, nextTick } from 'vue';
|
|
import { useProcessBuilderStore } from '@/stores/processBuilder';
|
|
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({
|
|
nodeData: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
availableVariables: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['update']);
|
|
|
|
// Get the process store for creating variables
|
|
const processStore = useProcessBuilderStore();
|
|
|
|
// Local state for node data - create a deep copy to avoid mutation issues
|
|
const localNodeData = ref(migrateNodeData(props.nodeData));
|
|
|
|
// 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(() => {
|
|
// Only use global variables, matching VariableManager implementation
|
|
const globalVars = props.availableVariables?.map(v => ({
|
|
name: v.name || 'unnamed',
|
|
label: `${v.name} (${v.type || 'string'})`,
|
|
type: v.type || 'string',
|
|
value: v.value
|
|
})) || [];
|
|
|
|
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(() => props.nodeData, (newNodeData) => {
|
|
if (newNodeData) localNodeData.value = migrateNodeData(newNodeData);
|
|
}, { immediate: true, deep: true });
|
|
|
|
// Function to create a new global variable
|
|
function createGlobalVariable(name, description = '') {
|
|
if (!name) return;
|
|
|
|
const newVariable = {
|
|
name,
|
|
type: 'object',
|
|
scope: 'global',
|
|
description: description || `API response from ${localNodeData.value.label}`
|
|
};
|
|
|
|
// Add the variable using the store's addVariable method
|
|
processStore.addProcessVariable(newVariable);
|
|
|
|
// Force a refresh of the component
|
|
nextTick(() => {
|
|
saveChanges();
|
|
});
|
|
}
|
|
|
|
// Save changes by emitting them to parent
|
|
function saveChanges() {
|
|
// Create a clean copy of the data to avoid reactivity issues
|
|
const nodeDataCopy = JSON.parse(JSON.stringify(localNodeData.value));
|
|
|
|
// Ensure variables exist before saving
|
|
if (nodeDataCopy.outputVariable) {
|
|
const existingVar = processStore.getProcessVariable(nodeDataCopy.outputVariable);
|
|
if (!existingVar) {
|
|
processStore.addProcessVariable({
|
|
name: nodeDataCopy.outputVariable,
|
|
type: 'object',
|
|
scope: 'global',
|
|
value: null,
|
|
description: `API response from ${nodeDataCopy.label}`
|
|
});
|
|
}
|
|
}
|
|
|
|
if (nodeDataCopy.errorVariable) {
|
|
const existingVar = processStore.getProcessVariable(nodeDataCopy.errorVariable);
|
|
if (!existingVar) {
|
|
processStore.addProcessVariable({
|
|
name: nodeDataCopy.errorVariable,
|
|
type: 'object',
|
|
scope: 'global',
|
|
value: null,
|
|
description: `API error from ${nodeDataCopy.label}`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Emit the updated data to parent
|
|
emit('update', nodeDataCopy);
|
|
}
|
|
|
|
// Add these refs for handling the API test state
|
|
const isLoading = ref(false);
|
|
const testResult = ref(null);
|
|
|
|
// Update the testApiCall function
|
|
async function testApiCall() {
|
|
if (!localNodeData.value.apiUrl) return;
|
|
|
|
isLoading.value = true;
|
|
testResult.value = null;
|
|
|
|
try {
|
|
// Get process variables for substitution
|
|
const processVariables = {
|
|
global: {},
|
|
process: {}
|
|
};
|
|
|
|
// Extract variables from available variables
|
|
if (props.availableVariables) {
|
|
props.availableVariables.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: JSON.parse(localNodeData.value.headers || '{}'),
|
|
outputVariable: localNodeData.value.outputVariable,
|
|
errorVariable: localNodeData.value.errorVariable,
|
|
continueOnError: localNodeData.value.continueOnError
|
|
},
|
|
processVariables
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
// Store the test result
|
|
testResult.value = {
|
|
success: response.ok && !result.error,
|
|
data: result.data,
|
|
error: result.error || (response.ok ? null : { message: 'API request failed' })
|
|
};
|
|
|
|
// Update variables in the store
|
|
if (testResult.value.success && localNodeData.value.outputVariable) {
|
|
processStore.updateProcessVariable(
|
|
localNodeData.value.outputVariable,
|
|
{ value: result.data }
|
|
);
|
|
} else if (!testResult.value.success && localNodeData.value.errorVariable) {
|
|
processStore.updateProcessVariable(
|
|
localNodeData.value.errorVariable,
|
|
{ value: testResult.value.error }
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('API test error:', error);
|
|
testResult.value = {
|
|
success: false,
|
|
error: {
|
|
message: error.message || 'An error occurred while testing the API call',
|
|
details: error.toString()
|
|
}
|
|
};
|
|
|
|
// Store error in variable
|
|
if (localNodeData.value.errorVariable) {
|
|
processStore.updateProcessVariable(
|
|
localNodeData.value.errorVariable,
|
|
{ value: testResult.value.error }
|
|
);
|
|
}
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// Function to insert a variable at cursor position or append to end
|
|
function insertVariable(variableName, field) {
|
|
if (!variableName) return;
|
|
|
|
const varName = `{${variableName}}`;
|
|
const textarea = document.getElementById(field);
|
|
|
|
if (textarea.selectionStart || textarea.selectionStart === 0) {
|
|
const startPos = textarea.selectionStart;
|
|
const endPos = textarea.selectionEnd;
|
|
|
|
localNodeData.value[field] =
|
|
textarea.value.substring(0, startPos) +
|
|
varName +
|
|
textarea.value.substring(endPos);
|
|
|
|
// Reset cursor position after variable
|
|
textarea.selectionStart = startPos + varName.length;
|
|
textarea.selectionEnd = startPos + varName.length;
|
|
} else {
|
|
localNodeData.value[field] += varName;
|
|
}
|
|
|
|
saveChanges();
|
|
}
|
|
|
|
// Function to format JSON in a field
|
|
function formatJson(field) {
|
|
try {
|
|
const value = localNodeData.value[field];
|
|
if (!value) return;
|
|
|
|
const parsed = JSON.parse(value);
|
|
localNodeData.value[field] = JSON.stringify(parsed, null, 2);
|
|
saveChanges();
|
|
} catch (error) {
|
|
console.warn(`Failed to format JSON for ${field}:`, error);
|
|
}
|
|
}
|
|
|
|
// Function to preview values with actual variable values
|
|
function getPreviewWithValues(field) {
|
|
try {
|
|
let text = localNodeData.value[field];
|
|
if (!text) return '';
|
|
|
|
// Replace all variables with their current values
|
|
const variablePattern = /{([^}]+)}/g;
|
|
text = text.replace(variablePattern, (match, varName) => {
|
|
const variable = availableVariables.value.find(v => v.name === varName);
|
|
if (variable) {
|
|
// Handle different value types
|
|
if (variable.value === null || variable.value === undefined) {
|
|
return `"<${varName}>"`; // Show variable name as placeholder
|
|
}
|
|
// Return the actual value or stringify complex objects
|
|
return typeof variable.value === 'string' ? `"${variable.value}"` : JSON.stringify(variable.value);
|
|
}
|
|
return `"<${varName}>"`; // Show variable name as placeholder for missing variables
|
|
});
|
|
|
|
// Try to format as JSON if possible
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
return JSON.stringify(parsed, null, 2);
|
|
} catch {
|
|
return text;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Preview error for ${field}:`, error);
|
|
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>
|
|
|
|
<style scoped>
|
|
.form-checkbox {
|
|
@apply text-indigo-600 focus:ring-indigo-500;
|
|
}
|
|
|
|
.form-select {
|
|
@apply border rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 p-2;
|
|
}
|
|
|
|
code {
|
|
font-family: monospace;
|
|
border-radius: 0.25rem;
|
|
}
|
|
</style> |