Enhance Process Flow Node Configuration and UI
- Updated ApiNodeConfiguration, BusinessRuleNodeConfiguration, and FormNodeConfiguration components to improve user experience with clearer layouts and enhanced styling. - Introduced Quick Reference Guides in modals for better user guidance during configuration. - Refined VariableManager with search functionality and improved variable display, allowing for easier management of process variables. - Enhanced GatewayConditionManager and related modals to support clearer decision path configurations and default path management. - Improved overall UI consistency across components, ensuring a more cohesive look and feel in the process builder.
This commit is contained in:
parent
0abb905477
commit
82bbc9ff57
@ -1,228 +1,276 @@
|
||||
<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"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</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"
|
||||
@blur="saveChanges"
|
||||
></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"
|
||||
@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>
|
||||
</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"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
<small class="form-text text-muted">
|
||||
You can use process variables with curly braces: https://example.com/api/users/{userId}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Variable Insertion for Request Body -->
|
||||
<div class="form-group mb-4" v-if="showRequestBody">
|
||||
<label for="requestBody" class="form-label">Request Body</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<!-- 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
|
||||
class="form-control text-sm"
|
||||
@change="insertVariable($event.target.value, 'requestBody')"
|
||||
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="">Insert Variable...</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
<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>
|
||||
<RsButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="formatJson('requestBody')"
|
||||
title="Format JSON"
|
||||
>
|
||||
<Icon name="material-symbols:format-align-left" />
|
||||
</RsButton>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
The HTTP method determines how the API call interacts with the endpoint.
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
id="requestBody"
|
||||
v-model="localNodeData.requestBody"
|
||||
class="form-control font-mono"
|
||||
placeholder='{ "key": "{variableName}" }'
|
||||
rows="6"
|
||||
@blur="saveChanges"
|
||||
></textarea>
|
||||
</div>
|
||||
<small class="form-text text-muted mt-1">
|
||||
Use variables in curly braces, e.g.: { "userId": "{userId}" }
|
||||
</small>
|
||||
|
||||
<!-- Request Body Preview -->
|
||||
<div v-if="localNodeData.requestBody" class="mt-3 border-t pt-3">
|
||||
<div class="text-sm font-medium text-gray-700 mb-2">Preview with Current Values:</div>
|
||||
<div class="bg-white border rounded p-3">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ getPreviewWithValues('requestBody') }}</pre>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Variable Insertion for Headers -->
|
||||
<div class="form-group mb-4">
|
||||
<label for="headers" class="form-label">Headers</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<select
|
||||
class="form-control text-sm"
|
||||
@change="insertVariable($event.target.value, 'headers')"
|
||||
>
|
||||
<option value="">Insert Variable...</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
<!-- Step 2: Headers and Body -->
|
||||
<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">Request Configuration</h4>
|
||||
</div>
|
||||
|
||||
<!-- Headers -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Headers</label>
|
||||
<div class="bg-white p-3 border rounded-md shadow-sm">
|
||||
<div class="flex gap-2 mb-2">
|
||||
<select
|
||||
class="form-select text-sm flex-grow"
|
||||
@change="insertVariable($event.target.value, 'headers')"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
<RsButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="formatJson('headers')"
|
||||
title="Format JSON"
|
||||
>
|
||||
<Icon name="material-symbols:format-align-left" />
|
||||
</RsButton>
|
||||
<option value="">Insert Variable...</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
<RsButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="formatJson('headers')"
|
||||
title="Format JSON"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
<Icon name="material-symbols:format-align-left" />
|
||||
Format
|
||||
</RsButton>
|
||||
</div>
|
||||
<textarea
|
||||
id="headers"
|
||||
v-model="localNodeData.headers"
|
||||
class="w-full p-2 border rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 text-sm font-mono"
|
||||
placeholder='{ "Authorization": "Bearer {accessToken}" }'
|
||||
rows="4"
|
||||
@blur="saveChanges"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Set HTTP headers as a JSON object. Use variables with <code class="bg-gray-100 px-1">{variableName}</code> syntax.
|
||||
</p>
|
||||
|
||||
<!-- Headers Preview -->
|
||||
<div v-if="localNodeData.headers" class="mt-3 pt-3 border-t border-gray-200">
|
||||
<div class="text-xs font-medium text-gray-700 mb-1">Preview with Current Values:</div>
|
||||
<div class="bg-gray-50 border rounded p-2">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ getPreviewWithValues('headers') }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
id="headers"
|
||||
v-model="localNodeData.headers"
|
||||
class="form-control font-mono"
|
||||
placeholder='{ "Authorization": "Bearer {accessToken}" }'
|
||||
rows="4"
|
||||
@blur="saveChanges"
|
||||
></textarea>
|
||||
</div>
|
||||
<small class="form-text text-muted mt-1">
|
||||
Use variables in curly braces, e.g.: { "Authorization": "Bearer {accessToken}" }
|
||||
</small>
|
||||
|
||||
<!-- Headers Preview -->
|
||||
<div v-if="localNodeData.headers" class="mt-3 border-t pt-3">
|
||||
<div class="text-sm font-medium text-gray-700 mb-2">Preview with Current Values:</div>
|
||||
<div class="bg-white border rounded p-3">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ getPreviewWithValues('headers') }}</pre>
|
||||
<!-- Request Body - show only for POST, PUT, PATCH -->
|
||||
<div v-if="showRequestBody" class="mt-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Request Body</label>
|
||||
<div class="bg-white p-3 border rounded-md shadow-sm">
|
||||
<div class="flex gap-2 mb-2">
|
||||
<select
|
||||
class="form-select text-sm flex-grow"
|
||||
@change="insertVariable($event.target.value, 'requestBody')"
|
||||
>
|
||||
<option value="">Insert Variable...</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
<RsButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="formatJson('requestBody')"
|
||||
title="Format JSON"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
<Icon name="material-symbols:format-align-left" />
|
||||
Format
|
||||
</RsButton>
|
||||
</div>
|
||||
<textarea
|
||||
id="requestBody"
|
||||
v-model="localNodeData.requestBody"
|
||||
class="w-full p-2 border rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 text-sm font-mono"
|
||||
placeholder='{ "key": "value", "userId": "{userId}" }'
|
||||
rows="6"
|
||||
@blur="saveChanges"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Request body to send with the API call. Use variables with <code class="bg-gray-100 px-1">{variableName}</code> syntax.
|
||||
</p>
|
||||
|
||||
<!-- Request Body Preview -->
|
||||
<div v-if="localNodeData.requestBody" class="mt-3 pt-3 border-t border-gray-200">
|
||||
<div class="text-xs font-medium text-gray-700 mb-1">Preview with Current Values:</div>
|
||||
<div class="bg-gray-50 border rounded p-2">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ getPreviewWithValues('requestBody') }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Variable Selection -->
|
||||
<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"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="" disabled>Select a global variable</option>
|
||||
<option value="apiResponse">Create new: apiResponse</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</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>
|
||||
<!-- Step 3: 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">3</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">
|
||||
<select
|
||||
v-model="localNodeData.outputVariable"
|
||||
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="" disabled>Select a global variable</option>
|
||||
<option value="apiResponse">Create new: apiResponse</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
<RsButton
|
||||
@click="createGlobalVariable(localNodeData.outputVariable)"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
title="Create a new global variable"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" />
|
||||
Create
|
||||
</RsButton>
|
||||
</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">
|
||||
<select
|
||||
v-model="localNodeData.errorVariable"
|
||||
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="" disabled>Select a global variable</option>
|
||||
<option value="apiError">Create new: apiError</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
<RsButton
|
||||
@click="createGlobalVariable(localNodeData.errorVariable, `API error from ${localNodeData.label}`)"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
title="Create a new global variable"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" />
|
||||
Create
|
||||
</RsButton>
|
||||
</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>
|
||||
<small class="form-text text-muted">
|
||||
API response will be stored in this global variable
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Error Variable Selection -->
|
||||
<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"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="" disabled>Select a global variable</option>
|
||||
<option value="apiError">Create new: apiError</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</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>
|
||||
<!-- Step 4: 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">4</span>
|
||||
</div>
|
||||
<h4 class="font-medium">Test API Call</h4>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
API errors will be stored in this global variable
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Test API Call Button and Results -->
|
||||
<div class="form-group mt-6 space-y-4">
|
||||
|
||||
<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" />
|
||||
@ -235,10 +283,9 @@
|
||||
</div>
|
||||
|
||||
<!-- API Test Results -->
|
||||
<div v-if="testResult" :class="[
|
||||
'p-4 rounded-md border',
|
||||
testResult.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
|
||||
]">
|
||||
<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
|
||||
@ -257,16 +304,16 @@
|
||||
|
||||
<!-- Success Response -->
|
||||
<div v-if="testResult.success" class="mt-3">
|
||||
<div class="text-sm text-gray-600 mb-2">Response stored in variable: {{ localNodeData.outputVariable }}</div>
|
||||
<div class="bg-white border border-green-100 rounded p-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: {{ localNodeData.errorVariable }}</div>
|
||||
<div class="bg-white border border-red-100 rounded p-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>
|
||||
@ -558,46 +605,16 @@ function getPreviewWithValues(field) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-node-configuration {
|
||||
padding: 1rem;
|
||||
background-color: #f8f8f8;
|
||||
.form-checkbox {
|
||||
@apply text-indigo-600 focus:ring-indigo-500;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
.form-select {
|
||||
@apply border rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 p-2;
|
||||
}
|
||||
|
||||
.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;
|
||||
code {
|
||||
font-family: monospace;
|
||||
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>
|
@ -2,18 +2,51 @@
|
||||
<RsModal
|
||||
v-model="showModal"
|
||||
title="API Call Configuration"
|
||||
size="lg"
|
||||
size="xl"
|
||||
position="center"
|
||||
:okCallback="saveAndClose"
|
||||
okTitle="Save"
|
||||
:cancelCallback="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="mr-4 text-indigo-500 flex-shrink-0 mt-1">
|
||||
<Icon name="material-symbols:api" class="text-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-1">Configure API Call</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
Set up an external API call to integrate with third-party services or fetch data from other systems.
|
||||
The API response will be stored in a process variable for later use.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main configuration area -->
|
||||
<ApiNodeConfiguration
|
||||
:nodeData="nodeData"
|
||||
:nodeData="localNodeData"
|
||||
:availableVariables="availableVariables"
|
||||
@update="handleUpdate"
|
||||
/>
|
||||
|
||||
<!-- Quick Reference Guide -->
|
||||
<div class="mt-6 bg-blue-50 p-4 rounded-md border border-blue-100">
|
||||
<h4 class="font-medium text-blue-700 mb-2 flex items-center">
|
||||
<Icon name="material-symbols:info-outline" class="mr-1" />
|
||||
Quick Reference Guide
|
||||
</h4>
|
||||
<div class="text-sm text-blue-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Variable placeholders can be used with <code class="bg-blue-100 px-1">{variableName}</code> syntax</li>
|
||||
<li>API responses are stored in your chosen output variable</li>
|
||||
<li>Error handling allows your process to recover from failed API calls</li>
|
||||
<li>You can test API calls directly from this configuration panel</li>
|
||||
<li>Response data structure depends on the API you're calling</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
</template>
|
||||
@ -21,6 +54,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import ApiNodeConfiguration from './ApiNodeConfiguration.vue';
|
||||
import { Icon } from '#components';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -69,4 +103,11 @@ function saveAndClose() {
|
||||
function closeModal() {
|
||||
showModal.value = false;
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
font-family: monospace;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
@ -1,59 +1,104 @@
|
||||
<template>
|
||||
<div class="business-rule-node-configuration">
|
||||
<h3 class="text-lg font-semibold mb-4">Business Rule Configuration</h3>
|
||||
|
||||
<div class="form-group mb-4">
|
||||
<label for="nodeLabel" class="form-label">Rule Name</label>
|
||||
<input
|
||||
id="nodeLabel"
|
||||
v-model="localNodeData.label"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Rule Name"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
<!-- 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-purple-100 flex items-center justify-center mr-2">
|
||||
<span class="text-xs font-semibold text-purple-600">1</span>
|
||||
</div>
|
||||
<h4 class="font-medium">Basic Configuration</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Rule Name -->
|
||||
<div>
|
||||
<label for="nodeLabel" class="block text-sm font-medium text-gray-700 mb-1">Rule Name</label>
|
||||
<input
|
||||
id="nodeLabel"
|
||||
v-model="localNodeData.label"
|
||||
type="text"
|
||||
class="w-full p-2 border rounded-md shadow-sm focus:border-purple-500 focus:ring focus:ring-purple-200 text-sm"
|
||||
placeholder="Enter a descriptive name"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
A clear name helps identify this rule in the process flow
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="nodeDescription" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
||||
<textarea
|
||||
id="nodeDescription"
|
||||
v-model="localNodeData.description"
|
||||
class="w-full p-2 border rounded-md shadow-sm focus:border-purple-500 focus:ring focus:ring-purple-200 text-sm"
|
||||
placeholder="Describe what this rule does"
|
||||
rows="2"
|
||||
@blur="saveChanges"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Optional description to explain this rule's purpose
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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="Rule description"
|
||||
rows="2"
|
||||
@blur="saveChanges"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Rule Groups (IF-THEN Rules) -->
|
||||
<div class="my-6">
|
||||
<!-- Step 2: Business Rules Definition -->
|
||||
<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-purple-100 flex items-center justify-center mr-2">
|
||||
<span class="text-xs font-semibold text-purple-600">2</span>
|
||||
</div>
|
||||
<h4 class="font-medium">Business Rules</h4>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h4 class="text-base font-medium">Business Rules</h4>
|
||||
<button
|
||||
<p class="text-sm text-gray-600">
|
||||
Define when conditions occur and what actions should be taken
|
||||
</p>
|
||||
<RsButton
|
||||
@click="addRuleGroup"
|
||||
class="btn-sm btn-primary flex items-center"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="btn-add-rule"
|
||||
>
|
||||
<i class="material-icons text-sm mr-1">add</i> Add Rule
|
||||
</button>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add Rule
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- No rules placeholder -->
|
||||
<div v-if="!localNodeData.ruleGroups || localNodeData.ruleGroups.length === 0"
|
||||
class="py-3 text-center text-gray-500 border border-dashed rounded-md">
|
||||
No business rules defined. Click "Add Rule" to create your first rule.
|
||||
class="py-6 px-4 text-center border-2 border-dashed border-purple-200 rounded-md bg-purple-50">
|
||||
<div class="text-purple-500 mb-2">
|
||||
<Icon name="material-symbols:rule" class="w-12 h-12 mx-auto" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-2">No Business Rules Defined</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Add your first rule to define when conditions occur and what actions should be taken.
|
||||
</p>
|
||||
<RsButton
|
||||
@click="addRuleGroup"
|
||||
variant="primary"
|
||||
size="md"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add First Rule
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Rules list -->
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Each rule group is an if-then rule -->
|
||||
<div
|
||||
v-for="(ruleGroup, groupIndex) in localNodeData.ruleGroups"
|
||||
:key="groupIndex"
|
||||
class="rule-group border rounded-md overflow-hidden bg-white shadow-sm"
|
||||
class="rule-group border rounded-md overflow-hidden bg-white shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<!-- Rule header -->
|
||||
<div class="rule-header bg-purple-50 px-4 py-2 flex justify-between items-center border-b">
|
||||
<h5 class="font-medium">
|
||||
<span class="text-purple-600">Rule {{ groupIndex + 1 }}:</span>
|
||||
<div class="rule-header bg-purple-50 px-4 py-3 flex justify-between items-center border-b">
|
||||
<h5 class="font-medium flex items-center">
|
||||
<Icon name="material-symbols:format-list-numbered" class="mr-1 text-purple-600" />
|
||||
<span class="text-purple-600 mr-1">Rule {{ groupIndex + 1 }}:</span>
|
||||
{{ ruleGroup.name || 'Unnamed Rule' }}
|
||||
</h5>
|
||||
<div class="flex items-center space-x-2">
|
||||
@ -66,253 +111,294 @@
|
||||
/>
|
||||
<button
|
||||
@click="removeRuleGroup(groupIndex)"
|
||||
class="text-red-500 hover:text-red-700"
|
||||
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
|
||||
title="Remove rule"
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
<Icon name="material-symbols:delete-outline" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IF section -->
|
||||
<div class="if-section p-4 bg-gray-50 border-b">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h6 class="font-medium text-gray-700">IF</h6>
|
||||
<button
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h6 class="font-medium text-gray-700 flex items-center">
|
||||
<span class="w-5 h-5 bg-purple-100 rounded-full text-xs flex items-center justify-center text-purple-800 mr-2">
|
||||
IF
|
||||
</span>
|
||||
Conditions
|
||||
</h6>
|
||||
<RsButton
|
||||
@click="addCondition(groupIndex)"
|
||||
class="btn-xs btn-secondary flex items-center"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="btn-sm-purple"
|
||||
>
|
||||
<i class="material-icons text-xs mr-1">add</i> Add Condition
|
||||
</button>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add Condition
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- No conditions placeholder -->
|
||||
<div v-if="!ruleGroup.conditions || ruleGroup.conditions.length === 0"
|
||||
class="py-2 text-center text-gray-500 text-sm border border-dashed rounded-md">
|
||||
No conditions defined. Add a condition to specify when this rule applies.
|
||||
class="py-4 text-center text-gray-500 text-sm border border-dashed rounded-md bg-white">
|
||||
<p class="mb-2">
|
||||
No conditions defined. Add a condition to specify when this rule applies.
|
||||
</p>
|
||||
<RsButton
|
||||
@click="addCondition(groupIndex)"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="btn-sm-purple"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add First Condition
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Conditions table -->
|
||||
<div v-else>
|
||||
<table class="w-full border-collapse">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Variable</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Operator</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value</th>
|
||||
<th class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white">
|
||||
<tr v-for="(condition, condIndex) in ruleGroup.conditions" :key="condIndex" class="border-t">
|
||||
<td class="px-3 py-2">
|
||||
<select
|
||||
v-model="condition.variable"
|
||||
class="form-control"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="" disabled>Select variable</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
<div class="bg-white border rounded-md overflow-hidden">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Variable</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Operator</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value</th>
|
||||
<th class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(condition, condIndex) in ruleGroup.conditions" :key="condIndex" class="border-t hover:bg-gray-50">
|
||||
<td class="px-3 py-2">
|
||||
<select
|
||||
v-model="condition.variable"
|
||||
class="form-select"
|
||||
@change="saveChanges"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<select
|
||||
v-model="condition.operator"
|
||||
class="form-control"
|
||||
@change="updateConditionOperator(groupIndex, condIndex)"
|
||||
>
|
||||
<option value="" disabled>Select operator</option>
|
||||
<option
|
||||
v-for="op in getOperatorsForType(
|
||||
availableVariables.find(v => v.name === condition.variable)?.type
|
||||
)"
|
||||
:key="op.value"
|
||||
:value="op.value"
|
||||
<option value="" disabled>Select variable</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<select
|
||||
v-model="condition.operator"
|
||||
class="form-select"
|
||||
@change="updateConditionOperator(groupIndex, condIndex)"
|
||||
>
|
||||
{{ op.label }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<template v-if="getInputType(
|
||||
availableVariables.find(v => v.name === condition.variable)?.type,
|
||||
condition.operator
|
||||
) !== 'none'">
|
||||
<input
|
||||
v-model="condition.value"
|
||||
:type="getInputType(
|
||||
availableVariables.find(v => v.name === condition.variable)?.type,
|
||||
condition.operator
|
||||
)"
|
||||
class="form-control"
|
||||
:placeholder="condition.operator?.includes('n_days') ? 'Number of days' : 'Value'"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</template>
|
||||
<span v-else class="text-gray-400 text-sm italic">N/A</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<button
|
||||
@click="removeCondition(groupIndex, condIndex)"
|
||||
class="text-red-500 hover:text-red-700"
|
||||
title="Remove condition"
|
||||
>
|
||||
<i class="material-icons text-sm">delete</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<option value="" disabled>Select operator</option>
|
||||
<option
|
||||
v-for="op in getOperatorsForType(
|
||||
availableVariables.find(v => v.name === condition.variable)?.type
|
||||
)"
|
||||
:key="op.value"
|
||||
:value="op.value"
|
||||
>
|
||||
{{ op.label }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<template v-if="getInputType(
|
||||
availableVariables.find(v => v.name === condition.variable)?.type,
|
||||
condition.operator
|
||||
) !== 'none'">
|
||||
<input
|
||||
v-model="condition.value"
|
||||
:type="getInputType(
|
||||
availableVariables.find(v => v.name === condition.variable)?.type,
|
||||
condition.operator
|
||||
)"
|
||||
class="form-control"
|
||||
:placeholder="condition.operator?.includes('n_days') ? 'Number of days' : 'Value'"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</template>
|
||||
<span v-else class="text-gray-400 text-sm italic">N/A</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<button
|
||||
@click="removeCondition(groupIndex, condIndex)"
|
||||
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
|
||||
title="Remove condition"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="text-sm" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-500 mt-2">
|
||||
<span class="font-medium">Condition Type:</span>
|
||||
<label class="inline-flex items-center ml-2">
|
||||
<input type="radio" v-model="ruleGroup.conditionType" value="all" class="form-radio" @change="saveChanges" />
|
||||
<span class="ml-1">Match All (AND)</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center ml-2">
|
||||
<input type="radio" v-model="ruleGroup.conditionType" value="any" class="form-radio" @change="saveChanges" />
|
||||
<span class="ml-1">Match Any (OR)</span>
|
||||
</label>
|
||||
<!-- Condition type selection -->
|
||||
<div class="mt-3 pt-3 border-t border-gray-200 flex flex-wrap items-center">
|
||||
<span class="text-sm font-medium text-gray-700 mr-3">Match Type:</span>
|
||||
<div class="space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="ruleGroup.conditionType"
|
||||
value="all"
|
||||
class="form-radio"
|
||||
@change="saveChanges"
|
||||
/>
|
||||
<span class="ml-2 text-sm">Match All Conditions (AND)</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="ruleGroup.conditionType"
|
||||
value="any"
|
||||
class="form-radio"
|
||||
@change="saveChanges"
|
||||
/>
|
||||
<span class="ml-2 text-sm">Match Any Condition (OR)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- THEN section -->
|
||||
<div class="then-section p-4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h6 class="font-medium text-gray-700">THEN</h6>
|
||||
<button
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h6 class="font-medium text-gray-700 flex items-center">
|
||||
<span class="w-5 h-5 bg-purple-100 rounded-full text-xs flex items-center justify-center text-purple-800 mr-2">
|
||||
THEN
|
||||
</span>
|
||||
Actions
|
||||
</h6>
|
||||
<RsButton
|
||||
@click="addAction(groupIndex)"
|
||||
class="btn-xs btn-secondary flex items-center"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="btn-sm-purple"
|
||||
>
|
||||
<i class="material-icons text-xs mr-1">add</i> Add Action
|
||||
</button>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add Action
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- No actions placeholder -->
|
||||
<div v-if="!ruleGroup.actions || ruleGroup.actions.length === 0"
|
||||
class="py-2 text-center text-gray-500 text-sm border border-dashed rounded-md">
|
||||
No actions defined. Add an action to specify what happens when conditions are met.
|
||||
class="py-4 text-center text-gray-500 text-sm border border-dashed rounded-md bg-white">
|
||||
<p class="mb-2">
|
||||
No actions defined. Add an action to specify what happens when conditions are met.
|
||||
</p>
|
||||
<RsButton
|
||||
@click="addAction(groupIndex)"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="btn-sm-purple"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add First Action
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Actions table -->
|
||||
<div v-else>
|
||||
<table class="w-full border-collapse">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action Type</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Target</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Configuration</th>
|
||||
<th class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white">
|
||||
<tr v-for="(action, actionIndex) in ruleGroup.actions" :key="actionIndex" class="border-t">
|
||||
<td class="px-3 py-2">
|
||||
<select
|
||||
v-model="action.type"
|
||||
class="form-control"
|
||||
@change="updateActionType(groupIndex, actionIndex)"
|
||||
>
|
||||
<option value="set_variable">Set Variable</option>
|
||||
<option value="calculate">Calculate Value</option>
|
||||
<option value="increment">Increment Variable</option>
|
||||
<option value="decrement">Decrement Variable</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<select
|
||||
v-model="action.variable"
|
||||
class="form-control"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="" disabled>Target variable</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div v-if="action.type === 'set_variable'">
|
||||
<input
|
||||
v-model="action.value"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Value"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="action.type === 'calculate'" class="flex items-center space-x-2">
|
||||
<div class="bg-white border rounded-md overflow-hidden">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action Type</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Target</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Configuration</th>
|
||||
<th class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(action, actionIndex) in ruleGroup.actions" :key="actionIndex" class="border-t hover:bg-gray-50">
|
||||
<td class="px-3 py-2">
|
||||
<select
|
||||
v-model="action.operator"
|
||||
class="form-control w-24"
|
||||
v-model="action.type"
|
||||
class="form-select"
|
||||
@change="updateActionType(groupIndex, actionIndex)"
|
||||
>
|
||||
<option value="set_variable">Set Variable</option>
|
||||
<option value="calculate">Calculate Value</option>
|
||||
<option value="increment">Increment Variable</option>
|
||||
<option value="decrement">Decrement Variable</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<select
|
||||
v-model="action.variable"
|
||||
class="form-select"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="add">+</option>
|
||||
<option value="subtract">-</option>
|
||||
<option value="multiply">×</option>
|
||||
<option value="divide">÷</option>
|
||||
<option value="" disabled>Target variable</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<!-- Set Variable -->
|
||||
<div v-if="action.type === 'set_variable'">
|
||||
<input
|
||||
v-model="action.value"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Value"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
v-model="action.value"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Value"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="text-gray-400 text-sm italic">
|
||||
{{ action.type === 'increment' ? 'Will increase by 1' : 'Will decrease by 1' }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<button
|
||||
@click="removeAction(groupIndex, actionIndex)"
|
||||
class="text-red-500 hover:text-red-700"
|
||||
title="Remove action"
|
||||
>
|
||||
<i class="material-icons text-sm">delete</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Calculate -->
|
||||
<div v-else-if="action.type === 'calculate'" class="flex items-center space-x-2">
|
||||
<select
|
||||
v-model="action.operator"
|
||||
class="form-select w-24"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="add">+</option>
|
||||
<option value="subtract">-</option>
|
||||
<option value="multiply">×</option>
|
||||
<option value="divide">÷</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
v-model="action.value"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Value"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Increment/Decrement -->
|
||||
<div v-else>
|
||||
<span class="text-gray-400 text-sm italic">
|
||||
{{ action.type === 'increment' ? 'Will increase by 1' : 'Will decrease by 1' }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<button
|
||||
@click="removeAction(groupIndex, actionIndex)"
|
||||
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
|
||||
title="Remove action"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="text-sm" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Settings -->
|
||||
<!-- <div class="border-t border-gray-200 my-4 pt-4">
|
||||
<h4 class="text-base font-medium mb-2"></h4>
|
||||
|
||||
<div class="form-group mb-4">
|
||||
<label class="form-label">Rule Priority</label>
|
||||
<select
|
||||
v-model="localNodeData.priority"
|
||||
class="form-control"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
<small class="text-gray-500 text-xs mt-1 block">
|
||||
Defines the execution priority of this rule when multiple rules are present.
|
||||
</small>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -320,6 +406,7 @@
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useVariableStore } from '~/stores/variableStore';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Icon } from '#components';
|
||||
|
||||
const props = defineProps({
|
||||
nodeId: {
|
||||
@ -668,40 +755,32 @@ const updateConditionOperator = (groupIndex, condIndex) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-gray-700 mb-1;
|
||||
.form-control {
|
||||
@apply block w-full p-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
@apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm;
|
||||
.form-select {
|
||||
@apply block w-full p-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
@apply h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded;
|
||||
@apply h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded;
|
||||
}
|
||||
|
||||
.form-radio {
|
||||
@apply h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300;
|
||||
@apply h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@apply px-2 py-1 text-xs font-medium rounded-md;
|
||||
.btn-add-rule {
|
||||
@apply bg-purple-600 hover:bg-purple-700 focus:ring-purple-500;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
@apply px-1.5 py-0.5 text-xs font-medium rounded;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply px-4 py-2 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply px-4 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500;
|
||||
.btn-sm-purple {
|
||||
@apply bg-white hover:bg-gray-50 text-purple-700 border-purple-300 hover:border-purple-400 focus:ring-purple-500;
|
||||
}
|
||||
|
||||
table {
|
||||
@apply border rounded-md shadow-sm;
|
||||
@apply border-collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
@ -717,10 +796,10 @@ td {
|
||||
}
|
||||
|
||||
.rule-group {
|
||||
@apply transition-all duration-200 relative;
|
||||
@apply transition-all duration-200 relative border-purple-200;
|
||||
}
|
||||
|
||||
.rule-group:hover {
|
||||
@apply shadow-md;
|
||||
@apply shadow-md border-purple-300;
|
||||
}
|
||||
</style>
|
@ -2,20 +2,53 @@
|
||||
<RsModal
|
||||
v-model="showModal"
|
||||
title="Business Rule Configuration"
|
||||
size="lg"
|
||||
size="xl"
|
||||
position="center"
|
||||
:okCallback="saveAndClose"
|
||||
okTitle="Save"
|
||||
:cancelCallback="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="mr-4 text-purple-500 flex-shrink-0 mt-1">
|
||||
<Icon name="material-symbols:rule" class="text-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-1">Configure Business Rules</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
Define conditional rules that automatically perform actions when specific conditions are met.
|
||||
Add multiple rules to handle different scenarios in your process.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main configuration area -->
|
||||
<BusinessRuleNodeConfiguration
|
||||
:nodeId="nodeId"
|
||||
:nodeData="nodeData"
|
||||
:nodeData="localNodeData"
|
||||
:availableVariables="availableVariables"
|
||||
@update="handleUpdate"
|
||||
@close="closeModal"
|
||||
/>
|
||||
|
||||
<!-- Quick Reference Guide -->
|
||||
<div class="mt-6 bg-purple-50 p-4 rounded-md border border-purple-100">
|
||||
<h4 class="font-medium text-purple-700 mb-2 flex items-center">
|
||||
<Icon name="material-symbols:info-outline" class="mr-1" />
|
||||
Quick Reference Guide
|
||||
</h4>
|
||||
<div class="text-sm text-purple-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Each rule consists of conditions (IF) and actions (THEN)</li>
|
||||
<li>Conditions can use AND logic (all must match) or OR logic (any can match)</li>
|
||||
<li>Actions will execute when all conditions are met</li>
|
||||
<li>Multiple rules allow for complex decision trees</li>
|
||||
<li>Rules are evaluated in the order they appear</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
</template>
|
||||
@ -23,6 +56,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import BusinessRuleNodeConfiguration from './BusinessRuleNodeConfiguration.vue';
|
||||
import { Icon } from '#components';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -75,4 +109,11 @@ function saveAndClose() {
|
||||
function closeModal() {
|
||||
showModal.value = false;
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
font-family: monospace;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
@ -26,55 +26,222 @@
|
||||
></textarea>
|
||||
</div> -->
|
||||
|
||||
<!-- Form Selection -->
|
||||
<div class="form-group mb-4">
|
||||
<FormSelector
|
||||
:formId="localNodeData.formId"
|
||||
@select="handleFormSelection"
|
||||
@clear="clearFormSelection"
|
||||
/>
|
||||
<!-- Step 1: Form Selection -->
|
||||
<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-emerald-100 flex items-center justify-center mr-2">
|
||||
<span class="text-xs font-semibold text-emerald-600">1</span>
|
||||
</div>
|
||||
<h4 class="font-medium">Form Selection</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Select an existing form or create a new one to use in this task.
|
||||
</p>
|
||||
<!-- Form Selector Component -->
|
||||
<div class="bg-white p-3 border rounded-md shadow-sm">
|
||||
<FormSelector
|
||||
:formId="localNodeData.formId"
|
||||
@select="handleFormSelection"
|
||||
@clear="clearFormSelection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Data Mapping Section -->
|
||||
<div v-if="localNodeData.formId" class="form-group mb-6">
|
||||
<div class="border-t border-gray-200 my-4 pt-4">
|
||||
<h4 class="text-base font-medium mb-4">Form Data Mapping</h4>
|
||||
<!-- Step 2: Form Data Mapping (only if form is selected) -->
|
||||
<div v-if="localNodeData.formId" 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-emerald-100 flex items-center justify-center mr-2">
|
||||
<span class="text-xs font-semibold text-emerald-600">2</span>
|
||||
</div>
|
||||
<h4 class="font-medium">Form Data Mapping</h4>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Configure how data flows between your process and the form.
|
||||
</p>
|
||||
|
||||
<!-- Input Variables -->
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="form-label">Input Variables (Prefill Form)</label>
|
||||
<button
|
||||
@click="addInputMapping()"
|
||||
class="px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 flex items-center"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-3.5 h-3.5 mr-1" />
|
||||
Add Mapping
|
||||
</button>
|
||||
<!-- Input Variables Mapping (Process → Form) -->
|
||||
<div class="mb-5">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<div>
|
||||
<h5 class="text-sm font-medium flex items-center">
|
||||
<span class="w-5 h-5 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center mr-2 text-xs">
|
||||
<Icon name="material-symbols:arrow-outward" />
|
||||
</span>
|
||||
Input Variables (Process → Form)
|
||||
</h5>
|
||||
<p class="text-xs text-gray-500 ml-7">Map process variables to pre-fill form fields</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!localNodeData.inputMappings || localNodeData.inputMappings.length === 0" class="text-sm text-gray-500 italic mb-2">
|
||||
<RsButton
|
||||
@click="addInputMapping()"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="btn-sm-emerald"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add Mapping
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- No input mappings placeholder -->
|
||||
<div v-if="!localNodeData.inputMappings || localNodeData.inputMappings.length === 0"
|
||||
class="py-4 text-center text-gray-500 text-sm border border-dashed rounded-md bg-white">
|
||||
<p class="mb-2">
|
||||
No input mappings defined. Form will not be prefilled with process data.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="(mapping, index) in localNodeData.inputMappings" :key="'input-' + index" class="p-3 border rounded-md bg-blue-50">
|
||||
<div class="flex justify-between mb-2">
|
||||
<h5 class="text-sm font-medium">Mapping #{{ index + 1 }}</h5>
|
||||
<button @click="removeInputMapping(index)" class="text-red-500 hover:text-red-700">
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</p>
|
||||
<RsButton
|
||||
@click="addInputMapping()"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="btn-sm-emerald"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add First Mapping
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div v-for="(mapping, index) in localNodeData.inputMappings" :key="'input-' + index"
|
||||
class="p-4 border rounded-md bg-blue-50">
|
||||
<div class="flex justify-between mb-3">
|
||||
<h5 class="text-sm font-medium flex items-center">
|
||||
<Icon name="material-symbols:arrow-outward" class="text-blue-600 mr-1" />
|
||||
Process Variable to Form Field #{{ index + 1 }}
|
||||
</h5>
|
||||
<button
|
||||
@click="removeInputMapping(index)"
|
||||
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
|
||||
title="Remove mapping"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Process Variable</label>
|
||||
<select
|
||||
v-model="mapping.processVariable"
|
||||
class="form-select"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="" disabled>Select a process variable</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
The source variable containing the data
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs mb-1">Process Variable</label>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Form Field Name</label>
|
||||
<input
|
||||
v-model="mapping.formField"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="e.g., first_name"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
The target field ID in the form
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Variables Mapping (Form → Process) -->
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<div>
|
||||
<h5 class="text-sm font-medium flex items-center">
|
||||
<span class="w-5 h-5 rounded-full bg-green-100 text-green-700 flex items-center justify-center mr-2 text-xs">
|
||||
<Icon name="material-symbols:arrow-back" />
|
||||
</span>
|
||||
Output Variables (Form → Process)
|
||||
</h5>
|
||||
<p class="text-xs text-gray-500 ml-7">Store form submission data in process variables</p>
|
||||
</div>
|
||||
<RsButton
|
||||
@click="addOutputMapping()"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="btn-sm-emerald"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add Mapping
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- No output mappings placeholder -->
|
||||
<div v-if="!localNodeData.outputMappings || localNodeData.outputMappings.length === 0"
|
||||
class="py-4 text-center text-gray-500 text-sm border border-dashed rounded-md bg-white">
|
||||
<p class="mb-2">
|
||||
No output mappings defined. Form data will not be stored in process variables.
|
||||
</p>
|
||||
<RsButton
|
||||
@click="addOutputMapping()"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="btn-sm-emerald"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" /> Add First Mapping
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div v-for="(mapping, index) in localNodeData.outputMappings" :key="'output-' + index"
|
||||
class="p-4 border rounded-md bg-green-50">
|
||||
<div class="flex justify-between mb-3">
|
||||
<h5 class="text-sm font-medium flex items-center">
|
||||
<Icon name="material-symbols:arrow-back" class="text-green-600 mr-1" />
|
||||
Form Field to Process Variable #{{ index + 1 }}
|
||||
</h5>
|
||||
<button
|
||||
@click="removeOutputMapping(index)"
|
||||
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
|
||||
title="Remove mapping"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Form Field Name</label>
|
||||
<input
|
||||
v-model="mapping.formField"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="e.g., first_name"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
The source field ID in the form
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Process Variable</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<select
|
||||
v-model="mapping.processVariable"
|
||||
class="w-full p-2 border rounded text-sm"
|
||||
class="form-select"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="" disabled>Select a variable</option>
|
||||
<option :value="'create_new_' + getVariableNameFromFormField(mapping.formField)">
|
||||
Create new variable
|
||||
</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
@ -83,91 +250,20 @@
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
<RsButton
|
||||
v-if="mapping.processVariable && mapping.processVariable.startsWith('create_new_')"
|
||||
@click="createVariableFromMapping(mapping)"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
title="Create this variable"
|
||||
class="flex-shrink-0 btn-add-var"
|
||||
>
|
||||
<Icon name="material-symbols:add" />
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs mb-1">Form Field</label>
|
||||
<input
|
||||
v-model="mapping.formField"
|
||||
type="text"
|
||||
class="w-full p-2 border rounded text-sm"
|
||||
placeholder="form_field_name"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Variables -->
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="form-label">Output Variables (Form Submissions)</label>
|
||||
<button
|
||||
@click="addOutputMapping()"
|
||||
class="px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 flex items-center"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-3.5 h-3.5 mr-1" />
|
||||
Add Mapping
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!localNodeData.outputMappings || localNodeData.outputMappings.length === 0" class="text-sm text-gray-500 italic mb-2">
|
||||
No output mappings defined. Form data will not be stored in process variables.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="(mapping, index) in localNodeData.outputMappings" :key="'output-' + index" class="p-3 border rounded-md bg-green-50">
|
||||
<div class="flex justify-between mb-2">
|
||||
<h5 class="text-sm font-medium">Mapping #{{ index + 1 }}</h5>
|
||||
<button @click="removeOutputMapping(index)" class="text-red-500 hover:text-red-700">
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs mb-1">Form Field</label>
|
||||
<input
|
||||
v-model="mapping.formField"
|
||||
type="text"
|
||||
class="w-full p-2 border rounded text-sm"
|
||||
placeholder="form_field_name"
|
||||
@blur="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs mb-1">Process Variable</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<select
|
||||
v-model="mapping.processVariable"
|
||||
class="w-full p-2 border rounded text-sm"
|
||||
@change="saveChanges"
|
||||
>
|
||||
<option value="" disabled>Select a variable</option>
|
||||
<option :value="'create_new_' + getVariableNameFromFormField(mapping.formField)">
|
||||
Create new variable
|
||||
</option>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
v-if="mapping.processVariable && mapping.processVariable.startsWith('create_new_')"
|
||||
@click="createVariableFromMapping(mapping)"
|
||||
class="shrink-0 px-2 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
title="Create this variable"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
The target variable to store form data
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -181,6 +277,7 @@
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { useVariableStore } from '@/stores/variableStore';
|
||||
import FormSelector from './FormSelector.vue';
|
||||
import { Icon } from '#components';
|
||||
|
||||
const props = defineProps({
|
||||
nodeData: {
|
||||
@ -350,10 +447,10 @@ function saveChanges() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-node-configuration {
|
||||
/* .form-node-configuration {
|
||||
padding: 1rem;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
} */
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
@ -379,4 +476,20 @@ function saveChanges() {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
@apply block w-full p-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
@apply block w-full p-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm;
|
||||
}
|
||||
|
||||
.btn-sm-emerald {
|
||||
@apply bg-white hover:bg-gray-50 text-emerald-700 border-emerald-300 hover:border-emerald-400 focus:ring-emerald-500;
|
||||
}
|
||||
|
||||
.btn-add-var {
|
||||
@apply bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500;
|
||||
}
|
||||
</style>
|
@ -2,18 +2,51 @@
|
||||
<RsModal
|
||||
v-model="showModal"
|
||||
title="Form Task Configuration"
|
||||
size="lg"
|
||||
size="xl"
|
||||
position="center"
|
||||
:okCallback="saveAndClose"
|
||||
okTitle="Save"
|
||||
:cancelCallback="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="mr-4 text-emerald-500 flex-shrink-0 mt-1">
|
||||
<Icon name="material-symbols:format-list-bulleted" class="text-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-1">Configure Form Task</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
Connect a form to your process flow to collect user input. Form submissions can be used to feed data
|
||||
into your process or to display information to users.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main configuration area -->
|
||||
<FormNodeConfiguration
|
||||
:nodeData="nodeData"
|
||||
:nodeData="localNodeData"
|
||||
:availableVariables="availableVariables"
|
||||
@update="handleUpdate"
|
||||
/>
|
||||
|
||||
<!-- Quick Reference Guide -->
|
||||
<div class="mt-6 bg-emerald-50 p-4 rounded-md border border-emerald-100">
|
||||
<h4 class="font-medium text-emerald-700 mb-2 flex items-center">
|
||||
<Icon name="material-symbols:info-outline" class="mr-1" />
|
||||
Quick Reference Guide
|
||||
</h4>
|
||||
<div class="text-sm text-emerald-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Select a form from your existing forms or create a new one</li>
|
||||
<li>Map process variables to form fields to pre-fill the form</li>
|
||||
<li>Map form fields to process variables to collect submission data</li>
|
||||
<li>The form will be displayed to users when this task is reached in the process</li>
|
||||
<li>Process execution will continue once the form is submitted</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
</template>
|
||||
@ -21,6 +54,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import FormNodeConfiguration from './FormNodeConfiguration.vue';
|
||||
import { Icon } from '#components';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -69,4 +103,11 @@ function saveAndClose() {
|
||||
function closeModal() {
|
||||
showModal.value = false;
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
font-family: monospace;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
@ -306,204 +306,267 @@ const groupedConditionText = (group) => {
|
||||
return `${c.operator.toUpperCase()} ${c.text}`;
|
||||
}).join(' ');
|
||||
};
|
||||
|
||||
// Add the following helper method to the existing script
|
||||
const getValuePlaceholder = (condition) => {
|
||||
const varType = condition.valueType?.toLowerCase();
|
||||
if (varType === 'number') return 'Enter a number';
|
||||
if (varType === 'date') return 'Select a date';
|
||||
if (varType === 'string') return 'Enter text value';
|
||||
return 'Enter value';
|
||||
};
|
||||
|
||||
// Add a method to get friendly path summary
|
||||
const getPathSummary = (group) => {
|
||||
if (!group.conditions || group.conditions.length === 0) {
|
||||
return 'No conditions defined';
|
||||
}
|
||||
|
||||
if (group.conditions.length === 1) {
|
||||
return conditionText(group.conditions[0]);
|
||||
}
|
||||
|
||||
// Build readable text with individual operators
|
||||
return group.conditions.map((condition, index) => {
|
||||
const text = conditionText(condition);
|
||||
if (index === 0) return text;
|
||||
return `${condition.logicalOperator.toUpperCase()} ${text}`;
|
||||
}).join(' ');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gateway-condition-manager">
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="text-sm font-medium text-gray-700">Decision Point Conditions</h3>
|
||||
<button
|
||||
@click="addConditionGroup"
|
||||
class="px-2 py-1 bg-orange-500 text-white text-xs rounded hover:bg-orange-600 flex items-center"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-3.5 h-3.5 mr-1" />
|
||||
Add Path
|
||||
</button>
|
||||
<!-- Empty state message when no paths are defined -->
|
||||
<div v-if="localConditions.length === 0" class="text-center p-6 border-2 border-dashed border-orange-200 rounded-lg bg-orange-50">
|
||||
<div class="text-orange-500 mb-2">
|
||||
<Icon name="material-symbols:call-split" class="w-12 h-12 mx-auto" />
|
||||
</div>
|
||||
|
||||
<div v-if="localConditions.length === 0" class="text-gray-500 text-sm italic mb-2">
|
||||
No conditions defined. Default path will be taken.
|
||||
</div>
|
||||
|
||||
<div v-else class="conditions-list space-y-3 mb-4">
|
||||
<!-- One condition group per outgoing path -->
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-2">No Decision Paths Defined</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Your process will always follow the default path: <span class="font-semibold">{{ defaultPath }}</span>
|
||||
</p>
|
||||
<button
|
||||
@click="addConditionGroup"
|
||||
class="px-4 py-2 bg-orange-500 text-white text-sm rounded-md hover:bg-orange-600 transition flex items-center mx-auto"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
|
||||
Add Your First Path
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Decision paths list -->
|
||||
<div v-else class="conditions-list space-y-4">
|
||||
<!-- Path cards - one per outgoing path -->
|
||||
<div
|
||||
v-for="(group, groupIndex) in localConditions"
|
||||
:key="group.id"
|
||||
class="condition-group border-2 rounded-lg shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-200"
|
||||
:class="{'border-orange-300 bg-orange-50': !isGroupCollapsed(group.id), 'border-gray-200': isGroupCollapsed(group.id)}"
|
||||
:data-collapsed="isGroupCollapsed(group.id)"
|
||||
>
|
||||
<!-- Path header -->
|
||||
<div
|
||||
v-for="(group, groupIndex) in localConditions"
|
||||
:key="group.id"
|
||||
class="condition-group border-2 rounded-md bg-orange-50"
|
||||
:data-collapsed="isGroupCollapsed(group.id)"
|
||||
@click.stop="isGroupCollapsed(group.id) ? toggleGroupCollapse(group.id) : null"
|
||||
class="group p-3 border-b flex items-center justify-between cursor-pointer"
|
||||
:class="{'bg-orange-100 border-orange-200': !isGroupCollapsed(group.id), 'bg-gray-50 border-gray-200 hover:bg-gray-100': isGroupCollapsed(group.id)}"
|
||||
@click="toggleGroupCollapse(group.id)"
|
||||
>
|
||||
<!-- Group header (always visible) -->
|
||||
<div class="p-3 border-b border-orange-200 flex items-center justify-between bg-orange-100">
|
||||
<div class="flex-1 mr-2">
|
||||
<div class="text-xs font-medium mb-1">
|
||||
Path Name:
|
||||
<span v-if="isGroupCollapsed(group.id)" class="ml-1 text-orange-600">
|
||||
({{ group.conditions.length }} condition{{ group.conditions.length !== 1 ? 's' : '' }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-orange-500">
|
||||
<Icon name="material-symbols:alt-route" class="w-5 h-5" />
|
||||
</div>
|
||||
<div class="font-medium">Path {{ groupIndex + 1 }}</div>
|
||||
<div
|
||||
v-if="group.output"
|
||||
class="text-sm rounded-full px-2 py-0.5 bg-orange-100 text-orange-700 border border-orange-200"
|
||||
>
|
||||
{{ group.output }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<!-- Condition count badge -->
|
||||
<div class="text-xs text-gray-500 bg-white px-2 py-0.5 rounded-full border mr-2">
|
||||
{{ group.conditions.length }} condition{{ group.conditions.length !== 1 ? 's' : '' }}
|
||||
</div>
|
||||
|
||||
<!-- Collapse/expand button -->
|
||||
<button
|
||||
class="p-1 text-gray-500 hover:text-gray-700 group-hover:text-orange-600"
|
||||
:title="isGroupCollapsed(group.id) ? 'Expand' : 'Collapse'"
|
||||
>
|
||||
<Icon
|
||||
:name="isGroupCollapsed(group.id) ? 'material-symbols:chevron-right' : 'material-symbols:expand-more'"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Delete button -->
|
||||
<button
|
||||
@click.stop="removeConditionGroup(groupIndex)"
|
||||
class="p-1 text-gray-400 hover:text-red-500"
|
||||
title="Remove path"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Path details when expanded -->
|
||||
<div v-if="!isGroupCollapsed(group.id)" class="path-details">
|
||||
<!-- Path name -->
|
||||
<div class="p-4 bg-white border-b border-orange-100">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Path Name</label>
|
||||
<div class="flex">
|
||||
<input
|
||||
type="text"
|
||||
:value="group.output"
|
||||
@input="updateConditionGroup(groupIndex, 'output', $event.target.value)"
|
||||
@blur="saveChanges"
|
||||
class="w-full p-2 border rounded text-xs"
|
||||
v-model="group.output"
|
||||
@blur="updateConditionGroup(groupIndex, 'output', group.output); saveChanges()"
|
||||
class="w-full p-2 border rounded-md shadow-sm focus:border-orange-500 focus:ring focus:ring-orange-200 text-sm"
|
||||
placeholder="Path label (e.g. 'Yes', 'Approved')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<!-- Collapse/expand button -->
|
||||
<button
|
||||
@click="toggleGroupCollapse(group.id); $event.stopPropagation();"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 mr-2"
|
||||
:title="isGroupCollapsed(group.id) ? 'Expand' : 'Collapse'"
|
||||
>
|
||||
<Icon
|
||||
:name="isGroupCollapsed(group.id) ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Delete button -->
|
||||
<button
|
||||
@click="removeConditionGroup(groupIndex); $event.stopPropagation();"
|
||||
class="p-1 text-gray-400 hover:text-red-500"
|
||||
title="Remove path"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
This name will appear on the connection line leaving this decision point.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible content -->
|
||||
<div v-if="!isGroupCollapsed(group.id)" class="p-3">
|
||||
<!-- Individual conditions in this group -->
|
||||
<div class="space-y-3">
|
||||
<!-- Conditions list -->
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h4 class="text-sm font-medium text-gray-700">Conditions for This Path</h4>
|
||||
<button
|
||||
@click.stop="addConditionToGroup(groupIndex)"
|
||||
class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded hover:bg-gray-200 border border-gray-300 flex items-center"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-3 h-3 mr-1" />
|
||||
Add Condition
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Individual conditions -->
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="(condition, conditionIndex) in group.conditions"
|
||||
:key="condition.id"
|
||||
class="condition-item p-3 border border-orange-200 rounded-md bg-white relative"
|
||||
class="condition-item p-3 border rounded-md bg-white shadow-sm hover:shadow-md transition-shadow duration-200"
|
||||
>
|
||||
<button
|
||||
v-if="group.conditions.length > 1"
|
||||
@click.stop="removeConditionFromGroup(groupIndex, conditionIndex)"
|
||||
class="absolute top-1 right-1 text-gray-400 hover:text-red-500"
|
||||
title="Remove condition"
|
||||
>
|
||||
<Icon name="material-symbols:close" class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<!-- Logical operator selector before each condition except the first one -->
|
||||
<div v-if="conditionIndex > 0" class="mb-2 pb-2 border-b border-gray-200">
|
||||
<select
|
||||
:value="condition.logicalOperator || 'and'"
|
||||
@change="updateCondition(groupIndex, conditionIndex, 'logicalOperator', $event.target.value)"
|
||||
@blur="saveChanges"
|
||||
class="w-full text-xs border border-gray-300 rounded px-2 py-1 bg-gray-50"
|
||||
<!-- Condition header with remove button -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h5 class="text-xs font-medium">Condition {{ conditionIndex + 1 }}</h5>
|
||||
<button
|
||||
v-if="group.conditions.length > 1"
|
||||
@click.stop="removeConditionFromGroup(groupIndex, conditionIndex)"
|
||||
class="text-gray-400 hover:text-red-500"
|
||||
title="Remove condition"
|
||||
>
|
||||
<option value="and">AND (Both conditions must be true)</option>
|
||||
<option value="or">OR (Either condition can be true)</option>
|
||||
</select>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<!-- Variable -->
|
||||
<select
|
||||
:value="condition.variable"
|
||||
@change="updateCondition(groupIndex, conditionIndex, 'variable', $event.target.value)"
|
||||
@blur="saveChanges"
|
||||
class="w-full p-2 border rounded text-xs"
|
||||
>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label || variable.name || 'Unnamed variable' }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Operator -->
|
||||
<select
|
||||
:value="condition.operator"
|
||||
@change="updateCondition(groupIndex, conditionIndex, 'operator', $event.target.value)"
|
||||
@blur="saveChanges"
|
||||
class="w-full p-2 border rounded text-xs"
|
||||
>
|
||||
<option
|
||||
v-for="operator in getOperatorsForType(
|
||||
props.availableVariables.find(v => v.name === condition.variable)?.type || 'string'
|
||||
)"
|
||||
:key="operator.value"
|
||||
:value="operator.value"
|
||||
>
|
||||
{{ operator.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Value -->
|
||||
<input
|
||||
v-if="condition.valueType !== 'boolean'"
|
||||
:type="getInputTypeForVarType(condition.valueType)"
|
||||
:value="condition.value"
|
||||
@input="updateCondition(groupIndex, conditionIndex, 'value', $event.target.value)"
|
||||
@blur="saveChanges"
|
||||
class="w-full p-2 border rounded text-xs"
|
||||
:placeholder="'Value'"
|
||||
/>
|
||||
<div v-else class="w-full p-2 border rounded flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="condition.value === true || condition.value === 'true'"
|
||||
@change="updateCondition(groupIndex, conditionIndex, 'value', $event.target.checked)"
|
||||
@blur="saveChanges"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-xs">{{ condition.value === true || condition.value === 'true' ? 'Yes' : 'No' }}</span>
|
||||
<!-- Logical operator for conditions after the first one -->
|
||||
<div v-if="conditionIndex > 0" class="mb-3 py-2 px-3 bg-gray-50 rounded-md border">
|
||||
<div class="text-xs text-gray-500 mb-1">Combine with previous condition using:</div>
|
||||
<div class="flex space-x-2">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
:checked="condition.logicalOperator === 'and'"
|
||||
@change="updateCondition(groupIndex, conditionIndex, 'logicalOperator', 'and')"
|
||||
class="form-radio text-orange-500 focus:ring-orange-500 h-4 w-4"
|
||||
/>
|
||||
<span class="ml-1 text-sm">AND</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
:checked="condition.logicalOperator === 'or'"
|
||||
@change="updateCondition(groupIndex, conditionIndex, 'logicalOperator', 'or')"
|
||||
class="form-radio text-orange-500 focus:ring-orange-500 h-4 w-4"
|
||||
/>
|
||||
<span class="ml-1 text-sm">OR</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Condition builder -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<!-- Variable -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Variable</label>
|
||||
<select
|
||||
v-model="condition.variable"
|
||||
@change="updateCondition(groupIndex, conditionIndex, 'variable', condition.variable)"
|
||||
class="w-full p-2 border rounded-md shadow-sm focus:border-orange-500 focus:ring focus:ring-orange-200 text-sm"
|
||||
>
|
||||
<option
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label || variable.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Operator -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Operator</label>
|
||||
<select
|
||||
v-model="condition.operator"
|
||||
@change="updateCondition(groupIndex, conditionIndex, 'operator', condition.operator)"
|
||||
class="w-full p-2 border rounded-md shadow-sm focus:border-orange-500 focus:ring focus:ring-orange-200 text-sm"
|
||||
>
|
||||
<option
|
||||
v-for="operator in getOperatorsForType(
|
||||
availableVariables.find(v => v.name === condition.variable)?.type || 'string'
|
||||
)"
|
||||
:key="operator.value"
|
||||
:value="operator.value"
|
||||
>
|
||||
{{ operator.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Value -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Value</label>
|
||||
<input
|
||||
v-if="condition.valueType !== 'boolean'"
|
||||
:type="getInputTypeForVarType(condition.valueType)"
|
||||
v-model="condition.value"
|
||||
@blur="saveChanges"
|
||||
class="w-full p-2 border rounded-md shadow-sm focus:border-orange-500 focus:ring focus:ring-orange-200 text-sm"
|
||||
:placeholder="getValuePlaceholder(condition)"
|
||||
/>
|
||||
<div v-else class="flex items-center h-[38px] p-2 border rounded-md shadow-sm bg-white">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="condition.value"
|
||||
class="form-checkbox text-orange-500 focus:ring-orange-500"
|
||||
/>
|
||||
<span class="ml-2 text-sm">{{ condition.value ? 'Yes/True' : 'No/False' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add another condition button -->
|
||||
<div class="text-center">
|
||||
<button
|
||||
@click.stop="addConditionToGroup(groupIndex)"
|
||||
class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded hover:bg-gray-200 border border-gray-300 flex items-center mx-auto"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-3 h-3 mr-1" />
|
||||
Add Another Condition
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Condition summary (always visible, even when collapsed) -->
|
||||
<div class="p-2 bg-orange-100 rounded-b-md border-t border-orange-200 text-xs text-orange-800">
|
||||
<span class="font-medium">Outcome:</span>
|
||||
<span v-if="group.conditions.length === 1">
|
||||
If {{ conditionText(group.conditions[0]) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
If {{ groupedConditionText(group) }}
|
||||
</span>
|
||||
<span class="font-medium"> → follow path "{{ group.output || 'Unlabeled path' }}"</span>
|
||||
<!-- Summary of path conditions -->
|
||||
<div class="bg-orange-50 border-t border-orange-100 p-3">
|
||||
<div class="text-sm text-gray-700 flex items-center">
|
||||
<span class="font-medium text-orange-700 mr-2">Path Summary:</span>
|
||||
{{ getPathSummary(group) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default path -->
|
||||
<div class="default-path p-3 border rounded-md bg-gray-50">
|
||||
<div class="text-xs font-medium mb-1">Default Path (when no conditions match):</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="defaultPath"
|
||||
@blur="saveChanges"
|
||||
class="w-full p-2 border rounded text-xs"
|
||||
placeholder="Default path label (e.g. 'No', 'Rejected')"
|
||||
/>
|
||||
|
||||
<!-- Path summary when collapsed -->
|
||||
<div v-if="isGroupCollapsed(group.id)" class="p-3 bg-white border-t border-gray-100 text-sm">
|
||||
<span class="font-medium">If</span> {{ getPathSummary(group) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -511,37 +574,14 @@ const groupedConditionText = (group) => {
|
||||
|
||||
<style scoped>
|
||||
.gateway-condition-manager {
|
||||
font-size: 0.875rem;
|
||||
@apply space-y-4;
|
||||
}
|
||||
|
||||
.condition-group {
|
||||
border-color: #fdba74;
|
||||
transition: all 0.2s;
|
||||
.form-radio {
|
||||
@apply text-orange-500 focus:ring-orange-500;
|
||||
}
|
||||
|
||||
.condition-group:hover {
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
.condition-item {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.condition-item:hover {
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
/* Adding new styles for collapsed state */
|
||||
.condition-group[data-collapsed="true"]:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.condition-group[data-collapsed="true"] {
|
||||
background-color: #ffedd5;
|
||||
}
|
||||
|
||||
.condition-group[data-collapsed="true"] .p-3.border-b {
|
||||
border-bottom: none;
|
||||
.form-checkbox {
|
||||
@apply text-orange-500 focus:ring-orange-500;
|
||||
}
|
||||
</style>
|
@ -2,22 +2,95 @@
|
||||
<RsModal
|
||||
v-model="showModal"
|
||||
title="Decision Point Configuration"
|
||||
size="lg"
|
||||
size="xl"
|
||||
position="center"
|
||||
:okCallback="saveAndClose"
|
||||
okTitle="Save"
|
||||
:cancelCallback="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold">Configure Decision Paths</h3>
|
||||
<p class="text-sm text-gray-600">Set up conditions to determine which path the process should follow.</p>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="mr-4 text-orange-500 flex-shrink-0 mt-1">
|
||||
<Icon name="material-symbols:call-split" class="text-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-1">Configure Decision Paths</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
Define the conditions that determine which path the process should follow.
|
||||
The decision point evaluates conditions in order and follows the first matching path.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default path configuration -->
|
||||
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-6 h-6 rounded-full bg-orange-100 flex items-center justify-center mr-2">
|
||||
<span class="text-xs font-semibold text-orange-600">1</span>
|
||||
</div>
|
||||
<h4 class="font-medium">Default Path</h4>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
This path will be taken when no conditions match. Give it a descriptive name.
|
||||
</p>
|
||||
<div class="flex items-center">
|
||||
<label class="text-sm font-medium text-gray-700 mr-2 w-24">Label:</label>
|
||||
<input
|
||||
v-model="defaultPathName"
|
||||
type="text"
|
||||
class="max-w-xs form-input rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring focus:ring-orange-200"
|
||||
placeholder="e.g., Default, Other, No Match"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Condition paths -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="w-6 h-6 rounded-full bg-orange-100 flex items-center justify-center mr-2">
|
||||
<span class="text-xs font-semibold text-orange-600">2</span>
|
||||
</div>
|
||||
<h4 class="font-medium">Define Decision Paths</h4>
|
||||
<button
|
||||
@click="addNewPath"
|
||||
class="ml-auto px-3 py-1.5 bg-orange-500 text-white text-sm rounded-md hover:bg-orange-600 transition flex items-center"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
|
||||
Add New Path
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Each path represents a possible route your process can take based on the conditions you define.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Gateway Condition Manager Component -->
|
||||
<GatewayConditionManager
|
||||
:conditions="conditions"
|
||||
:conditions="localConditions"
|
||||
:availableVariables="availableVariables"
|
||||
:defaultPath="defaultPathName"
|
||||
@update:conditions="handleConditionsUpdate"
|
||||
@update:defaultPath="updateDefaultPath"
|
||||
/>
|
||||
|
||||
<!-- Quick Reference Guide -->
|
||||
<div class="mt-6 bg-blue-50 p-4 rounded-md border border-blue-100">
|
||||
<h4 class="font-medium text-blue-700 mb-2 flex items-center">
|
||||
<Icon name="material-symbols:info-outline" class="mr-1" />
|
||||
Quick Reference Guide
|
||||
</h4>
|
||||
<div class="text-sm text-blue-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Decision points evaluate conditions in order from top to bottom</li>
|
||||
<li>The first path with matching conditions will be followed</li>
|
||||
<li>If no conditions match, the default path will be taken</li>
|
||||
<li>Each path can have multiple conditions combined with AND/OR logic</li>
|
||||
<li>Make sure to give each path a clear, descriptive name</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
</template>
|
||||
@ -25,6 +98,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import GatewayConditionManager from './GatewayConditionManager.vue';
|
||||
import { Icon } from '#components';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -38,13 +112,18 @@ const props = defineProps({
|
||||
availableVariables: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
defaultPath: {
|
||||
type: String,
|
||||
default: 'Default'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:conditions']);
|
||||
const emit = defineEmits(['update:modelValue', 'update:conditions', 'update:defaultPath']);
|
||||
|
||||
const showModal = ref(props.modelValue);
|
||||
const localConditions = ref([...props.conditions]);
|
||||
const defaultPathName = ref(props.defaultPath || 'Default');
|
||||
|
||||
// Watch for changes to modelValue prop to sync modal visibility
|
||||
watch(() => props.modelValue, (value) => {
|
||||
@ -61,17 +140,60 @@ watch(() => props.conditions, (value) => {
|
||||
localConditions.value = [...value];
|
||||
}, { deep: true });
|
||||
|
||||
// Watch for changes to defaultPath prop
|
||||
watch(() => props.defaultPath, (value) => {
|
||||
defaultPathName.value = value || 'Default';
|
||||
});
|
||||
|
||||
function handleConditionsUpdate(updatedConditions) {
|
||||
localConditions.value = [...updatedConditions];
|
||||
emit('update:conditions', updatedConditions);
|
||||
}
|
||||
|
||||
function updateDefaultPath(value) {
|
||||
defaultPathName.value = value;
|
||||
}
|
||||
|
||||
function saveAndClose() {
|
||||
emit('update:conditions', localConditions.value);
|
||||
emit('update:defaultPath', defaultPathName.value);
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
function addNewPath() {
|
||||
// Create a default new path
|
||||
const defaultVar = props.availableVariables.length > 0 ? props.availableVariables[0] : null;
|
||||
|
||||
if (!defaultVar) {
|
||||
alert('No variables available. Please add process variables before creating conditions.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newConditionGroup = {
|
||||
id: `condition-group-${Date.now()}`,
|
||||
output: `Path ${localConditions.value.length + 1}`, // Auto-numbered path
|
||||
conditions: [
|
||||
{
|
||||
id: `condition-${Date.now()}`,
|
||||
variable: defaultVar.name,
|
||||
operator: 'eq', // Default to equals
|
||||
value: '',
|
||||
valueType: defaultVar.type || 'string',
|
||||
logicalOperator: 'and'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
localConditions.value.push(newConditionGroup);
|
||||
emit('update:conditions', [...localConditions.value]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-input {
|
||||
@apply text-sm py-2 px-3;
|
||||
}
|
||||
</style>
|
@ -108,15 +108,28 @@ export const TaskNode = markRaw({
|
||||
};
|
||||
|
||||
return priorityColors[this.data.priority] || '';
|
||||
},
|
||||
|
||||
// Helper to get priority label
|
||||
priorityLabel() {
|
||||
if (!this.data || !this.data.priority) return 'None';
|
||||
return this.data.priority.charAt(0).toUpperCase() + this.data.priority.slice(1);
|
||||
},
|
||||
|
||||
// Helper for due date
|
||||
dueLabel() {
|
||||
if (!this.data || !this.data.dueDateType || this.data.dueDateType === 'none') {
|
||||
return 'Not set';
|
||||
}
|
||||
|
||||
if (this.data.dueDateType === 'fixed') {
|
||||
return `${this.data.dueDateDays || 0} days`;
|
||||
}
|
||||
|
||||
return `Variable: ${this.data.dueDateVariable || 'none'}`;
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const badgeContent = this.data?.priority ?
|
||||
h('span', {
|
||||
class: `node-badge px-1 text-xs rounded ${this.priorityClass} bg-gray-100`
|
||||
}, this.data.priority.charAt(0).toUpperCase() + this.data.priority.slice(1)) :
|
||||
null;
|
||||
|
||||
return h(CustomNode, {
|
||||
id: this.id,
|
||||
type: 'task',
|
||||
@ -126,21 +139,22 @@ export const TaskNode = markRaw({
|
||||
onClick: () => this.$emit('node-click', this.id)
|
||||
}, {
|
||||
icon: () => h('i', { class: 'material-icons text-blue-500' }, 'assignment'),
|
||||
badge: () => badgeContent,
|
||||
default: () => h('div', { class: 'node-details' }, [
|
||||
h('p', { class: 'node-description' }, this.data?.description || 'A general task'),
|
||||
h('div', { class: 'node-assignee flex items-center justify-between text-xs' }, [
|
||||
h('span', { class: 'node-assignee-label' }, 'Assigned:'),
|
||||
h('span', { class: 'node-assignee-value ml-1 font-medium text-blue-600' }, this.assignmentText)
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Assigned:'),
|
||||
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-blue-600' }, this.assignmentText)
|
||||
]),
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Priority:'),
|
||||
h('span', {
|
||||
class: `node-rule-detail-value ml-1 font-medium ${this.priorityClass}`
|
||||
}, this.priorityLabel)
|
||||
]),
|
||||
this.data?.dueDateType !== 'none' && this.data?.dueDateType ?
|
||||
h('div', { class: 'node-due-date text-xs mt-1' }, [
|
||||
h('span', { class: 'node-due-date-label' }, 'Due:'),
|
||||
h('span', { class: 'node-due-date-value ml-1' },
|
||||
this.data.dueDateType === 'fixed' ?
|
||||
`${this.data.dueDateDays || 0} days` :
|
||||
`Variable: ${this.data.dueDateVariable || 'none'}`
|
||||
)
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Due:'),
|
||||
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-blue-600' }, this.dueLabel)
|
||||
]) : null
|
||||
])
|
||||
});
|
||||
@ -224,14 +238,13 @@ export const GatewayNode = markRaw({
|
||||
.join(', ');
|
||||
|
||||
return paths || 'Unconfigured paths';
|
||||
},
|
||||
|
||||
defaultPath() {
|
||||
return this.data?.defaultPath || 'Default';
|
||||
}
|
||||
},
|
||||
render() {
|
||||
// Create the badge content
|
||||
const badgeContent = h('span', {
|
||||
class: 'node-badge bg-orange-100 text-orange-600 px-1 text-xs rounded'
|
||||
}, `${this.totalPaths} path${this.totalPaths !== 1 ? 's' : ''}`);
|
||||
|
||||
return h(CustomNode, {
|
||||
id: this.id,
|
||||
type: 'gateway',
|
||||
@ -241,12 +254,19 @@ export const GatewayNode = markRaw({
|
||||
onClick: () => this.$emit('node-click', this.id)
|
||||
}, {
|
||||
icon: () => h('i', { class: 'material-icons text-orange-500' }, 'call_split'),
|
||||
badge: () => badgeContent,
|
||||
default: () => h('div', { class: 'gateway-details' }, [
|
||||
h('p', { class: 'node-description' }, this.data?.description || 'Decision based on conditions'),
|
||||
h('div', { class: 'node-conditions flex items-center justify-between text-xs' }, [
|
||||
h('span', { class: 'node-conditions-label' }, 'Paths:'),
|
||||
h('span', { class: 'node-conditions-value ml-1 font-medium text-orange-600' }, this.conditionSummary)
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Paths:'),
|
||||
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-orange-600' },
|
||||
this.totalPaths === 0 ? 'None' : this.totalPaths
|
||||
)
|
||||
]),
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Default:'),
|
||||
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-orange-600' },
|
||||
this.defaultPath
|
||||
)
|
||||
])
|
||||
])
|
||||
});
|
||||
@ -269,10 +289,6 @@ export const FormNode = markRaw({
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const badgeContent = this.hasForm ?
|
||||
h('span', { class: 'node-badge bg-purple-100 text-purple-600 px-1 text-xs rounded' }, 'Form') :
|
||||
null;
|
||||
|
||||
return h(CustomNode, {
|
||||
id: this.id,
|
||||
type: 'form',
|
||||
@ -282,14 +298,19 @@ export const FormNode = markRaw({
|
||||
onClick: () => this.$emit('node-click', this.id)
|
||||
}, {
|
||||
icon: () => h('i', { class: 'material-icons text-purple-500' }, 'description'),
|
||||
badge: () => badgeContent,
|
||||
default: () => h('div', { class: 'node-details' }, [
|
||||
h('p', { class: 'node-description' }, this.data?.description || 'Form submission task'),
|
||||
h('div', { class: 'node-form-info' }, [
|
||||
h('span', { class: 'node-form-label' }, 'Form: '),
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Form:'),
|
||||
h('span', {
|
||||
class: this.hasForm ? 'node-form-value text-purple-600 font-medium' : 'node-form-value text-gray-400 italic'
|
||||
class: this.hasForm ? 'node-rule-detail-value ml-1 font-medium text-purple-600' : 'node-rule-detail-value ml-1 italic text-gray-400'
|
||||
}, this.formName)
|
||||
]),
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Status:'),
|
||||
h('span', {
|
||||
class: 'node-rule-detail-value ml-1 font-medium text-purple-600'
|
||||
}, this.hasForm ? 'Configured' : 'Not configured')
|
||||
])
|
||||
])
|
||||
});
|
||||
@ -303,6 +324,12 @@ export const ScriptNode = markRaw({
|
||||
nodeLabel() {
|
||||
// Get label from either prop or data, with fallback
|
||||
return this.label || (this.data && this.data.label) || 'Script';
|
||||
},
|
||||
scriptLanguage() {
|
||||
return this.data?.language || 'Not specified';
|
||||
},
|
||||
hasScript() {
|
||||
return !!this.data?.script;
|
||||
}
|
||||
},
|
||||
render() {
|
||||
@ -314,12 +341,18 @@ export const ScriptNode = markRaw({
|
||||
data: this.data,
|
||||
onClick: () => this.$emit('node-click', this.id)
|
||||
}, {
|
||||
icon: () => h('i', { class: 'material-icons script-icon' }, 'code'),
|
||||
icon: () => h('i', { class: 'material-icons text-gray-500' }, 'code'),
|
||||
default: () => h('div', { class: 'node-details' }, [
|
||||
h('p', { class: 'node-description' }, this.data?.description || 'Script execution'),
|
||||
h('div', { class: 'node-script-info' }, [
|
||||
h('span', { class: 'node-script-label' }, 'Language: '),
|
||||
h('span', { class: 'node-script-value' }, this.data?.language || 'Not specified')
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Language:'),
|
||||
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-gray-600' }, this.scriptLanguage)
|
||||
]),
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Script:'),
|
||||
h('span', {
|
||||
class: 'node-rule-detail-value ml-1 font-medium text-gray-600'
|
||||
}, this.hasScript ? 'Defined' : 'Not defined')
|
||||
])
|
||||
])
|
||||
});
|
||||
@ -338,13 +371,12 @@ export const ApiCallNode = markRaw({
|
||||
},
|
||||
apiMethod() {
|
||||
return this.data?.apiMethod || 'GET';
|
||||
},
|
||||
isConfigured() {
|
||||
return !!this.data?.apiUrl;
|
||||
}
|
||||
},
|
||||
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',
|
||||
@ -354,20 +386,20 @@ export const ApiCallNode = markRaw({
|
||||
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('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'Method:'),
|
||||
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'
|
||||
class: 'node-rule-detail-value ml-1 font-medium text-indigo-600'
|
||||
}, this.apiMethod)
|
||||
]),
|
||||
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||
h('span', { class: 'node-rule-detail-label' }, 'URL:'),
|
||||
h('span', {
|
||||
class: this.isConfigured ? 'node-rule-detail-value ml-1 font-medium text-indigo-600' : 'node-rule-detail-value ml-1 italic text-gray-400',
|
||||
style: 'max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;'
|
||||
}, this.apiUrl)
|
||||
])
|
||||
])
|
||||
});
|
||||
@ -769,28 +801,31 @@ export const nodeStyles = `
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
max-width: 90px;
|
||||
}
|
||||
|
||||
.node-gateway .node-conditions {
|
||||
.node-gateway .node-rule-detail {
|
||||
display: flex;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.node-gateway .node-conditions-label {
|
||||
.node-gateway .node-rule-detail-label {
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.node-gateway .node-conditions-value {
|
||||
.node-gateway .node-rule-detail-value {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 10px;
|
||||
max-width: 80px;
|
||||
max-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-gateway .material-icons {
|
||||
@ -798,4 +833,79 @@ export const nodeStyles = `
|
||||
color: #f97316;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Update node-specific styles to be more consistent */
|
||||
.node-form {
|
||||
width: 180px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
border: 1px solid #ddd;
|
||||
border-left: 4px solid #9333ea; /* Purple border to match icon color */
|
||||
}
|
||||
|
||||
.node-api {
|
||||
width: 180px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
border: 1px solid #ddd;
|
||||
border-left: 4px solid #6366f1; /* Indigo border to match icon color */
|
||||
}
|
||||
|
||||
/* Node details styles for consistency */
|
||||
.node-rule-detail {
|
||||
display: flex;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.node-rule-detail-label {
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.node-rule-detail-value {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Add task node specific styling to be consistent with business rule */
|
||||
.node-task {
|
||||
width: 180px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
border: 1px solid #ddd;
|
||||
border-left: 4px solid #3b82f6; /* Blue border to match icon color */
|
||||
}
|
||||
|
||||
/* Script node styling */
|
||||
.node-script {
|
||||
width: 180px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
border: 1px solid #ddd;
|
||||
border-left: 4px solid #6b7280; /* Gray border to match icon color */
|
||||
}
|
||||
`;
|
@ -353,10 +353,10 @@ function saveChanges() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-node-configuration {
|
||||
/* .task-node-configuration {
|
||||
padding: 1rem;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
} */
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
|
@ -3,11 +3,16 @@
|
||||
<!-- Header with Add Button -->
|
||||
<div class="bg-gray-50 border-b border-gray-200 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Process Variables</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Manage variables for your process flow
|
||||
</p>
|
||||
<div class="flex items-start">
|
||||
<div class="mr-4 text-blue-500 flex-shrink-0 mt-1">
|
||||
<Icon name="material-symbols:data-object" class="text-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Process Variables</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Define and manage global variables to store and pass data within your process
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<RsButton
|
||||
@click="
|
||||
@ -25,56 +30,85 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="px-4 pt-3 pb-2">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Icon name="material-symbols:search" class="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Search variables..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variable List -->
|
||||
<div class="p-4">
|
||||
<div class="p-4 overflow-auto flex-grow">
|
||||
<!-- Empty State -->
|
||||
<div v-if="!variables.length" class="text-center py-8">
|
||||
<div v-if="!variables.length" class="text-center py-10 px-4 rounded-lg border-2 border-dashed border-gray-300 bg-gray-50">
|
||||
<Icon
|
||||
name="material-symbols:data-object"
|
||||
class="w-12 h-12 mx-auto mb-3 text-gray-400"
|
||||
class="w-14 h-14 mx-auto mb-3 text-gray-400"
|
||||
/>
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-1">
|
||||
No Variables Added
|
||||
<h4 class="text-base font-medium text-gray-900 mb-1">
|
||||
No Variables Added Yet
|
||||
</h4>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Add variables to store and manage data in your process
|
||||
<p class="text-sm text-gray-500 mb-4 max-w-md mx-auto">
|
||||
Variables allow you to store and manage data in your process flow. Add your first variable to get started.
|
||||
</p>
|
||||
<RsButton
|
||||
@click="
|
||||
() => {
|
||||
resetForm();
|
||||
showAddVariable = true;
|
||||
}
|
||||
"
|
||||
variant="primary"
|
||||
size="md"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1" />
|
||||
Add First Variable
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Variable List -->
|
||||
<div v-else class="space-y-2">
|
||||
<div v-else-if="filteredVariables.length" class="space-y-3">
|
||||
<div
|
||||
v-for="variable in variables"
|
||||
v-for="variable in filteredVariables"
|
||||
:key="variable.name"
|
||||
class="variable-item"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200 hover:border-blue-200 hover:shadow-sm transition-all duration-200"
|
||||
class="flex items-center justify-between p-4 bg-white rounded-lg border border-gray-200 hover:border-blue-200 hover:shadow-sm transition-all duration-200"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-900">{{
|
||||
variable.name
|
||||
}}</span>
|
||||
<RsBadge variant="outline" size="sm" class="text-gray-500">
|
||||
<span class="font-medium text-gray-900">{{ variable.name }}</span>
|
||||
<RsBadge :variant="getTypeColor(variable.type)" size="sm">
|
||||
{{ variable.type }}
|
||||
</RsBadge>
|
||||
</div>
|
||||
<p v-if="variable.description" class="mt-1 text-sm text-gray-500">
|
||||
{{ variable.description }}
|
||||
</p>
|
||||
<div v-if="variable.value !== undefined" class="mt-2 text-xs font-mono bg-gray-50 p-2 rounded border border-gray-200 max-w-md truncate">
|
||||
{{ formatValue(variable.value, variable.type) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-4">
|
||||
<button
|
||||
@click="editVariable(variable)"
|
||||
class="p-1.5 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-md transition-colors"
|
||||
class="p-2 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-md transition-colors"
|
||||
title="Edit variable"
|
||||
>
|
||||
<Icon name="material-symbols:edit" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="deleteVariable(variable)"
|
||||
class="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
|
||||
class="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
|
||||
title="Delete variable"
|
||||
>
|
||||
<Icon name="material-symbols:delete" class="w-4 h-4" />
|
||||
@ -83,6 +117,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No search results -->
|
||||
<div v-else class="text-center py-8">
|
||||
<Icon
|
||||
name="material-symbols:search-off"
|
||||
class="w-12 h-12 mx-auto mb-3 text-gray-400"
|
||||
/>
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-1">
|
||||
No matching variables found
|
||||
</h4>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Try using different keywords or <a href="#" @click.prevent="searchQuery = ''" class="text-blue-500">clear your search</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Variable Modal -->
|
||||
@ -93,6 +141,16 @@
|
||||
:hideFooter="true"
|
||||
:overlayClose="false"
|
||||
>
|
||||
<div class="mb-4 flex items-start" v-if="!editingVariable">
|
||||
<div class="mr-3 text-blue-500 flex-shrink-0 mt-1">
|
||||
<Icon name="material-symbols:data-object" class="text-xl" />
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
Variables store data that can be used throughout your process flow. They can be updated by tasks, used in conditions,
|
||||
or displayed in forms.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
type="form"
|
||||
@submit="saveVariable"
|
||||
@ -103,8 +161,8 @@
|
||||
name="name"
|
||||
v-model="variableForm.name"
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder="Enter variable name"
|
||||
label="Variable Name"
|
||||
placeholder="Enter variable name (e.g. customerName)"
|
||||
validation="required|alpha_numeric|length:3,50"
|
||||
:validation-messages="{
|
||||
required: 'Variable name is required',
|
||||
@ -112,25 +170,20 @@
|
||||
'Variable name can only contain letters, numbers, and underscores',
|
||||
length: 'Variable name must be between 3 and 50 characters',
|
||||
}"
|
||||
help="Use a descriptive name without spaces. Example: totalAmount, customerName, orderStatus"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
name="type"
|
||||
v-model="variableForm.type"
|
||||
type="select"
|
||||
label="Type"
|
||||
:options="[
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Int', value: 'int' },
|
||||
{ label: 'Decimal', value: 'decimal' },
|
||||
{ label: 'Object', value: 'object' },
|
||||
{ label: 'DateTime', value: 'datetime' },
|
||||
{ label: 'Date', value: 'date' }
|
||||
]"
|
||||
label="Data Type"
|
||||
:options="variableTypes"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Variable type is required',
|
||||
}"
|
||||
help="Select the type of data this variable will store"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
@ -138,8 +191,9 @@
|
||||
v-model="variableForm.description"
|
||||
type="textarea"
|
||||
label="Description"
|
||||
placeholder="Enter variable description"
|
||||
placeholder="Enter a description to help others understand what this variable is used for"
|
||||
:rows="2"
|
||||
help="A clear description helps others understand the purpose of this variable"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200">
|
||||
@ -147,7 +201,7 @@
|
||||
Cancel
|
||||
</RsButton>
|
||||
<FormKit type="submit" input-class="rs-button rs-button-primary">
|
||||
{{ editingVariable ? "Update" : "Add" }}
|
||||
{{ editingVariable ? "Update Variable" : "Add Variable" }}
|
||||
</FormKit>
|
||||
</div>
|
||||
</FormKit>
|
||||
@ -164,6 +218,7 @@ const variableStore = useVariableStore();
|
||||
// State
|
||||
const showAddVariable = ref(false);
|
||||
const editingVariable = ref(null);
|
||||
const searchQuery = ref("");
|
||||
const variableForm = ref({
|
||||
name: "",
|
||||
type: "string",
|
||||
@ -171,12 +226,35 @@ const variableForm = ref({
|
||||
description: "",
|
||||
});
|
||||
|
||||
// Variable type options with descriptions
|
||||
const variableTypes = [
|
||||
{ label: 'String - Text values', value: 'string' },
|
||||
{ label: 'Int - Whole numbers', value: 'int' },
|
||||
{ label: 'Decimal - Decimal numbers', value: 'decimal' },
|
||||
{ label: 'Object - Complex data structure', value: 'object' },
|
||||
{ label: 'DateTime - Date and time values', value: 'datetime' },
|
||||
{ label: 'Date - Date values only', value: 'date' },
|
||||
{ label: 'Boolean - True/False values', value: 'boolean' }
|
||||
];
|
||||
|
||||
// Computed
|
||||
const variables = computed(() => {
|
||||
// Only return global variables
|
||||
return variableStore.getAllVariables.global;
|
||||
});
|
||||
|
||||
// Filtered variables based on search query
|
||||
const filteredVariables = computed(() => {
|
||||
if (!searchQuery.value) return variables.value;
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return variables.value.filter(variable =>
|
||||
variable.name.toLowerCase().includes(query) ||
|
||||
(variable.description && variable.description.toLowerCase().includes(query)) ||
|
||||
variable.type.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// Methods
|
||||
const editVariable = (variable) => {
|
||||
editingVariable.value = variable;
|
||||
@ -185,7 +263,7 @@ const editVariable = (variable) => {
|
||||
};
|
||||
|
||||
const deleteVariable = (variable) => {
|
||||
if (confirm(`Are you sure you want to delete ${variable.name}?`)) {
|
||||
if (confirm(`Are you sure you want to delete the variable "${variable.name}"? This might affect parts of your process that use this variable.`)) {
|
||||
variableStore.deleteVariable(variable.name, 'global');
|
||||
}
|
||||
};
|
||||
@ -234,6 +312,38 @@ const saveVariable = async (formData) => {
|
||||
// You might want to show an error message to the user here
|
||||
}
|
||||
};
|
||||
|
||||
// Get badge color based on variable type
|
||||
const getTypeColor = (type) => {
|
||||
switch (type) {
|
||||
case 'string': return 'blue';
|
||||
case 'int':
|
||||
case 'decimal': return 'purple';
|
||||
case 'object': return 'emerald';
|
||||
case 'datetime':
|
||||
case 'date': return 'amber';
|
||||
case 'boolean': return 'indigo';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
// Format variable value for display
|
||||
const formatValue = (value, type) => {
|
||||
if (value === undefined || value === null) return 'null';
|
||||
|
||||
switch (type) {
|
||||
case 'object':
|
||||
try {
|
||||
return typeof value === 'string' ? value : JSON.stringify(value);
|
||||
} catch (e) {
|
||||
return String(value);
|
||||
}
|
||||
case 'boolean':
|
||||
return value ? 'true' : 'false';
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -246,6 +356,30 @@ const saveVariable = async (formData) => {
|
||||
}
|
||||
|
||||
.variable-item:hover {
|
||||
@apply transform -translate-y-1;
|
||||
@apply transform -translate-y-0.5;
|
||||
}
|
||||
|
||||
/* Light styling for FormKit form */
|
||||
:deep(.formkit-outer) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-label) {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
:deep(.formkit-help) {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-messages) {
|
||||
font-size: 0.75rem;
|
||||
color: #ef4444;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -323,8 +323,9 @@ const handleConditionUpdate = (conditions) => {
|
||||
if (edge.source === selectedNodeData.value.id) {
|
||||
// Find matching condition group
|
||||
const matchingGroup = conditions.find(group => group.output === edge.label);
|
||||
if (!matchingGroup) {
|
||||
// If no matching group found, update edge label to default
|
||||
|
||||
// If no matching group found, and this isn't the default path, update edge label
|
||||
if (!matchingGroup && edge.label !== selectedNodeData.value.data.defaultPath) {
|
||||
return {
|
||||
...edge,
|
||||
label: selectedNodeData.value.data.defaultPath || 'Default'
|
||||
@ -708,6 +709,14 @@ const handleBusinessRuleUpdate = (data) => {
|
||||
updateNodeInStore();
|
||||
}
|
||||
};
|
||||
|
||||
// Add a method to handle default path updates
|
||||
const handleDefaultPathUpdate = (path) => {
|
||||
if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') {
|
||||
selectedNodeData.value.data.defaultPath = path;
|
||||
updateNodeInStore();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -934,7 +943,9 @@ const handleBusinessRuleUpdate = (data) => {
|
||||
v-model="showGatewayConfigModal"
|
||||
:conditions="selectedNodeData.data.conditions || []"
|
||||
:availableVariables="gatewayAvailableVariables"
|
||||
:defaultPath="selectedNodeData.data.defaultPath || 'Default'"
|
||||
@update:conditions="handleConditionUpdate"
|
||||
@update:defaultPath="handleDefaultPathUpdate"
|
||||
/>
|
||||
|
||||
<!-- Business Rule Configuration Modal -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user