diff --git a/components/process-flow/ApiNodeConfiguration.vue b/components/process-flow/ApiNodeConfiguration.vue index 2a87f85..c5214a8 100644 --- a/components/process-flow/ApiNodeConfiguration.vue +++ b/components/process-flow/ApiNodeConfiguration.vue @@ -24,6 +24,8 @@ + +

The HTTP method determines how the API call interacts with the endpoint. @@ -48,112 +50,116 @@ - +

2
-

Request Configuration

+

Authorization

- - -
- -
-
- - - - Format - -
- -

- Set HTTP headers as a JSON object. Use variables with {variableName} syntax. -

- - -
-
Preview with Current Values:
-
-
{{ getPreviewWithValues('headers') }}
-
-
-
+
+ +
- - -
- -
-
- - - - Format - -
- -

- Request body to send with the API call. Use variables with {variableName} syntax. -

- - -
-
Preview with Current Values:
-
-
{{ getPreviewWithValues('requestBody') }}
-
-
-
+
+ + +
- -
+ +
3
+

Query Parameters

+
+ +
+ + +
+
+
+ 4 +
+

Headers

+
+ +
+ + +
+
+
+ 5 +
+

Request Body

+
+
+ + +
+
+ +
+
+ + +
+ + {{ rawBodyJsonWarning }} +
+
+
+ +
Selected: {{ localNodeData.body.data }}
+
+
No body will be sent.
+
+ + +
+
+
+ 6 +

Response Handling

@@ -208,11 +214,11 @@
- +
- 4 + 7

Test API Call

@@ -276,6 +282,22 @@ 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: { @@ -294,17 +316,7 @@ const emit = defineEmits(['update']); const processStore = useProcessBuilderStore(); // Local state for node data - create a deep copy to avoid mutation issues -const localNodeData = ref({ - label: 'API Call', - description: '', - apiMethod: 'GET', - apiUrl: '', - requestBody: '', - headers: '{ "Content-Type": "application/json" }', - outputVariable: 'apiResponse', - continueOnError: false, - errorVariable: 'apiError' -}); +const localNodeData = ref(migrateNodeData(props.nodeData)); // Computed for showing request body based on method const showRequestBody = computed(() => { @@ -324,22 +336,16 @@ const availableVariables = computed(() => { 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) { - // Create a deep copy to break reactivity chains with parent - localNodeData.value = { - label: newNodeData.label || 'API Call', - description: newNodeData.description || '', - apiMethod: newNodeData.apiMethod || 'GET', - apiUrl: newNodeData.apiUrl || '', - requestBody: newNodeData.requestBody || '', - headers: newNodeData.headers || '{ "Content-Type": "application/json" }', - outputVariable: newNodeData.outputVariable || 'apiResponse', - continueOnError: newNodeData.continueOnError || false, - errorVariable: newNodeData.errorVariable || 'apiError' - }; - } + if (newNodeData) localNodeData.value = migrateNodeData(newNodeData); }, { immediate: true, deep: true }); // Function to create a new global variable @@ -559,6 +565,178 @@ function getPreviewWithValues(field) { return localNodeData.value[field] || ''; } } + +// --- Authorization Fields --- +function getAuthFields() { + switch (localNodeData.value.authorization.type) { + case 'bearer': + return [ + { key: 'token', label: 'Token', type: 'text', placeholder: 'Bearer token', value: localNodeData.value.authorization.token || '' } + ]; + case 'basic': + return [ + { key: 'username', label: 'Username', type: 'text', placeholder: 'Username', value: localNodeData.value.authorization.username || '' }, + { key: 'password', label: 'Password', type: 'password', placeholder: 'Password', value: localNodeData.value.authorization.password || '' } + ]; + case 'apiKey': + return [ + { key: 'key', label: 'Key Name', type: 'text', placeholder: 'e.g. X-API-Key', value: localNodeData.value.authorization.key || '' }, + { key: 'value', label: 'Key Value', type: 'text', placeholder: 'API Key Value', value: localNodeData.value.authorization.value || '' }, + { key: 'in', label: 'Add To', type: 'select', options: [ + { value: 'header', label: 'Header' }, + { value: 'query', label: 'Query Param' } + ], value: localNodeData.value.authorization.in || 'header' } + ]; + default: + return []; + } +} +function updateAuthField(field, value) { + localNodeData.value.authorization[field] = value; + saveChanges(); +} + +// --- Body Data Handling --- +function getBodyData() { + if (['form-data', 'x-www-form-urlencoded'].includes(localNodeData.value.body.type)) { + return Array.isArray(localNodeData.value.body.data) ? localNodeData.value.body.data : []; + } + if (localNodeData.value.body.type === 'raw') { + return typeof localNodeData.value.body.data === 'string' ? localNodeData.value.body.data : ''; + } + if (localNodeData.value.body.type === 'binary') { + return localNodeData.value.body.data || null; + } + return []; +} +function setBodyData(val) { + if (['form-data', 'x-www-form-urlencoded'].includes(localNodeData.value.body.type)) { + localNodeData.value.body.data = val; + } else if (localNodeData.value.body.type === 'raw') { + localNodeData.value.body.data = val; + } else if (localNodeData.value.body.type === 'binary') { + localNodeData.value.body.data = val; + } + saveChanges(); +} + +// --- File Upload for Binary --- +function handleFileUpload(e) { + const file = e.target.files[0]; + if (file) { + localNodeData.value.body.data = file.name; + saveChanges(); + } +} + +// --- JSON Validation for Raw Body --- +const rawBodyIsJson = computed(() => { + if (bodyType.value !== 'raw') return false; + const val = localNodeData.value.body?.data || ''; + // Only check if it looks like JSON + if (!val.trim().startsWith('{') && !val.trim().startsWith('[')) return false; + try { + // Replace {variable} with a string to allow parsing + const replaced = val.replace(/\{[a-zA-Z0-9_]+\}/g, '"VAR"'); + JSON.parse(replaced); + return true; + } catch { + return false; + } +}); +const rawBodyHasVariable = computed(() => { + if (bodyType.value !== 'raw') return false; + const val = localNodeData.value.body?.data || ''; + return /\{[a-zA-Z0-9_]+\}/.test(val); +}); +const rawBodyJsonWarning = computed(() => { + if (bodyType.value !== 'raw') return ''; + const val = localNodeData.value.body?.data || ''; + if (!val.trim().startsWith('{') && !val.trim().startsWith('[')) return ''; + try { + // Replace both quoted and unquoted {variable} with "VAR" + let replaced = val + .replace(/"\{[a-zA-Z0-9_]+\}"/g, '"VAR"') // quoted + .replace(/\{[a-zA-Z0-9_]+\}/g, '"VAR"'); // unquoted + JSON.parse(replaced); + return ''; + } catch { + return 'Warning: Your request body is not valid JSON. Make sure variables are inside quotes.'; + } +}); + +// --- Variable Insertion Helper --- +function insertVariableToRaw(varName) { + if (!varName) return; + const textarea = document.querySelector('textarea[placeholder="Raw body (JSON, XML, etc.)"]'); + let val = localNodeData.value.body.data || ''; + let insertText = `{${varName}}`; + // Detect if inside quotes + let cursor = textarea ? textarea.selectionStart : val.length; + let before = val.substring(0, cursor); + let after = val.substring(cursor); + // Check if body looks like JSON + const isJson = val.trim().startsWith('{') || val.trim().startsWith('['); + // Check if cursor is inside quotes + let insideQuotes = false; + if (isJson && textarea) { + // Count quotes before cursor + const quotesBefore = before.split('"').length - 1; + insideQuotes = quotesBefore % 2 === 1; + } + if (isJson && !insideQuotes) { + insertText = `"{${varName}}"`; + } + // Insert at cursor + localNodeData.value.body.data = before + insertText + after; + saveChanges(); + // Move cursor after inserted variable + nextTick(() => { + if (textarea) { + const newPos = before.length + insertText.length; + textarea.focus(); + textarea.selectionStart = textarea.selectionEnd = newPos; + } + }); +} + +// --- UI State --- +const showParams = computed(() => ['GET', 'DELETE', 'HEAD', 'OPTIONS'].includes(localNodeData.value.apiMethod)); + +// Defensive computed for bodyType and authType +const bodyType = computed(() => localNodeData.value.body && localNodeData.value.body.type ? localNodeData.value.body.type : 'none'); +const authType = computed(() => localNodeData.value.authorization && localNodeData.value.authorization.type ? localNodeData.value.authorization.type : 'none'); + +function migrateNodeData(data) { + const migrated = { ...data }; + // Headers + if (typeof migrated.headers === 'string') { + try { + const obj = JSON.parse(migrated.headers); + migrated.headers = Object.entries(obj).map(([key, value]) => ({ key, value })); + } catch { + migrated.headers = []; + } + } else if (!Array.isArray(migrated.headers)) { + migrated.headers = []; + } + // Params + if (!Array.isArray(migrated.params)) migrated.params = []; + // Body + if (!migrated.body || typeof migrated.body !== 'object') { + migrated.body = { type: 'none', data: [] }; + } else { + if (!migrated.body.type) migrated.body.type = 'none'; + if (migrated.body.data === undefined) migrated.body.data = migrated.body.type === 'raw' ? '' : []; + } + if (typeof data.requestBody === 'string' && (!migrated.body || migrated.body.type === 'none')) { + migrated.body = { type: 'raw', data: data.requestBody }; + } + // Auth + if (!migrated.authorization || typeof migrated.authorization !== 'object') migrated.authorization = { type: 'none' }; + if (!migrated.authorization.type) migrated.authorization.type = 'none'; + return migrated; +}