Add Form and Task Node Configuration Components
- Introduced new components, FormNodeConfiguration and TaskNodeConfiguration, for configuring form and task nodes within the process builder. - Enhanced the ProcessBuilder to support form and task node types, allowing users to define input/output mappings and assignment settings. - Implemented backend API endpoints for fetching active users and roles, improving user assignment functionality in task nodes. - Updated the ProcessFlowCanvas to handle updates for form and task nodes, ensuring consistent data management across node types. - Improved overall user experience by refining the UI for node configuration and enhancing variable handling in the process builder.
This commit is contained in:
parent
dce3e7f0f2
commit
c35073f7fe
@ -2,7 +2,7 @@
|
|||||||
<div class="api-node-configuration">
|
<div class="api-node-configuration">
|
||||||
<h3 class="text-lg font-semibold mb-4">API Call Configuration</h3>
|
<h3 class="text-lg font-semibold mb-4">API Call Configuration</h3>
|
||||||
|
|
||||||
<div class="form-group mb-4">
|
<!-- <div class="form-group mb-4">
|
||||||
<label for="nodeLabel" class="form-label">Node Label</label>
|
<label for="nodeLabel" class="form-label">Node Label</label>
|
||||||
<input
|
<input
|
||||||
id="nodeLabel"
|
id="nodeLabel"
|
||||||
@ -10,10 +10,11 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="API Call"
|
placeholder="API Call"
|
||||||
|
@blur="saveChanges"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="form-group mb-4">
|
<!-- <div class="form-group mb-4">
|
||||||
<label for="nodeDescription" class="form-label">Description</label>
|
<label for="nodeDescription" class="form-label">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="nodeDescription"
|
id="nodeDescription"
|
||||||
@ -21,8 +22,9 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="API call description"
|
placeholder="API call description"
|
||||||
rows="2"
|
rows="2"
|
||||||
|
@blur="saveChanges"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="form-group mb-4">
|
<div class="form-group mb-4">
|
||||||
<label for="apiMethod" class="form-label">HTTP Method</label>
|
<label for="apiMethod" class="form-label">HTTP Method</label>
|
||||||
@ -30,6 +32,7 @@
|
|||||||
id="apiMethod"
|
id="apiMethod"
|
||||||
v-model="localNodeData.apiMethod"
|
v-model="localNodeData.apiMethod"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
@change="saveChanges"
|
||||||
>
|
>
|
||||||
<option value="GET">GET</option>
|
<option value="GET">GET</option>
|
||||||
<option value="POST">POST</option>
|
<option value="POST">POST</option>
|
||||||
@ -47,37 +50,112 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="https://example.com/api/endpoint"
|
placeholder="https://example.com/api/endpoint"
|
||||||
|
@blur="saveChanges"
|
||||||
/>
|
/>
|
||||||
<small class="form-text text-muted">
|
<small class="form-text text-muted">
|
||||||
You can use process variables with curly braces: https://example.com/api/users/{userId}
|
You can use process variables with curly braces: https://example.com/api/users/{userId}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Variable Insertion for Request Body -->
|
||||||
<div class="form-group mb-4" v-if="showRequestBody">
|
<div class="form-group mb-4" v-if="showRequestBody">
|
||||||
<label for="requestBody" class="form-label">Request Body</label>
|
<label for="requestBody" class="form-label">Request Body</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<select
|
||||||
|
class="form-control text-sm"
|
||||||
|
@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"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:format-align-left" />
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
id="requestBody"
|
id="requestBody"
|
||||||
v-model="localNodeData.requestBody"
|
v-model="localNodeData.requestBody"
|
||||||
class="form-control font-mono"
|
class="form-control font-mono"
|
||||||
placeholder='{ "key": "value" }'
|
placeholder='{ "key": "{variableName}" }'
|
||||||
rows="4"
|
rows="6"
|
||||||
|
@blur="saveChanges"
|
||||||
></textarea>
|
></textarea>
|
||||||
<small class="form-text text-muted">
|
</div>
|
||||||
You can use process variables with curly braces: { "userId": "{userId}" }
|
<small class="form-text text-muted mt-1">
|
||||||
|
Use variables in curly braces, e.g.: { "userId": "{userId}" }
|
||||||
</small>
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Variable Insertion for Headers -->
|
||||||
<div class="form-group mb-4">
|
<div class="form-group mb-4">
|
||||||
<label for="headers" class="form-label">Headers</label>
|
<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"
|
||||||
|
>
|
||||||
|
{{ variable.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<RsButton
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
@click="formatJson('headers')"
|
||||||
|
title="Format JSON"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:format-align-left" />
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
id="headers"
|
id="headers"
|
||||||
v-model="localNodeData.headers"
|
v-model="localNodeData.headers"
|
||||||
class="form-control font-mono"
|
class="form-control font-mono"
|
||||||
placeholder='{ "Content-Type": "application/json" }'
|
placeholder='{ "Authorization": "Bearer {accessToken}" }'
|
||||||
rows="3"
|
rows="4"
|
||||||
|
@blur="saveChanges"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output Variable Selection -->
|
||||||
<div class="form-group mb-4">
|
<div class="form-group mb-4">
|
||||||
<label for="outputVariable" class="form-label">Output Variable</label>
|
<label for="outputVariable" class="form-label">Output Variable</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@ -85,18 +163,17 @@
|
|||||||
id="outputVariable"
|
id="outputVariable"
|
||||||
v-model="localNodeData.outputVariable"
|
v-model="localNodeData.outputVariable"
|
||||||
class="form-control flex-grow"
|
class="form-control flex-grow"
|
||||||
|
@change="saveChanges"
|
||||||
>
|
>
|
||||||
<option value="" disabled>Select a global variable</option>
|
<option value="" disabled>Select a global variable</option>
|
||||||
<option value="apiResponse">Create new: apiResponse</option>
|
<option value="apiResponse">Create new: apiResponse</option>
|
||||||
<optgroup label="Global Variables">
|
|
||||||
<option
|
<option
|
||||||
v-for="variable in availableVariables.global"
|
v-for="variable in availableVariables"
|
||||||
:key="variable.name"
|
:key="variable.name"
|
||||||
:value="variable.name"
|
:value="variable.name"
|
||||||
>
|
>
|
||||||
{{ variable.description ? `${variable.name} (${variable.description})` : variable.name }}
|
{{ variable.label }}
|
||||||
</option>
|
</option>
|
||||||
</optgroup>
|
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
@click="createGlobalVariable(localNodeData.outputVariable)"
|
@click="createGlobalVariable(localNodeData.outputVariable)"
|
||||||
@ -111,21 +188,7 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group mb-4">
|
<!-- Error Variable Selection -->
|
||||||
<label class="form-label d-block">Error Handling</label>
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
id="continueOnError"
|
|
||||||
v-model="localNodeData.continueOnError"
|
|
||||||
type="checkbox"
|
|
||||||
class="form-check-input"
|
|
||||||
/>
|
|
||||||
<label for="continueOnError" class="form-check-label">
|
|
||||||
Continue process execution on error
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group mb-4">
|
<div class="form-group mb-4">
|
||||||
<label for="errorVariable" class="form-label">Error Variable</label>
|
<label for="errorVariable" class="form-label">Error Variable</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@ -133,18 +196,17 @@
|
|||||||
id="errorVariable"
|
id="errorVariable"
|
||||||
v-model="localNodeData.errorVariable"
|
v-model="localNodeData.errorVariable"
|
||||||
class="form-control flex-grow"
|
class="form-control flex-grow"
|
||||||
|
@change="saveChanges"
|
||||||
>
|
>
|
||||||
<option value="" disabled>Select a global variable</option>
|
<option value="" disabled>Select a global variable</option>
|
||||||
<option value="apiError">Create new: apiError</option>
|
<option value="apiError">Create new: apiError</option>
|
||||||
<optgroup label="Global Variables">
|
|
||||||
<option
|
<option
|
||||||
v-for="variable in availableVariables.global"
|
v-for="variable in availableVariables"
|
||||||
:key="variable.name"
|
:key="variable.name"
|
||||||
:value="variable.name"
|
:value="variable.name"
|
||||||
>
|
>
|
||||||
{{ variable.description ? `${variable.name} (${variable.description})` : variable.name }}
|
{{ variable.label }}
|
||||||
</option>
|
</option>
|
||||||
</optgroup>
|
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
@click="createGlobalVariable(localNodeData.errorVariable, `API error from ${localNodeData.label}`)"
|
@click="createGlobalVariable(localNodeData.errorVariable, `API error from ${localNodeData.label}`)"
|
||||||
@ -159,33 +221,82 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Test API Call Button -->
|
<!-- Test API Call Button and Results -->
|
||||||
<div class="form-group mt-6">
|
<div class="form-group mt-6 space-y-4">
|
||||||
<RsButton @click="testApiCall" variant="primary" :disabled="!localNodeData.apiUrl">
|
<div class="flex items-center gap-4">
|
||||||
|
<RsButton @click="testApiCall" variant="primary" :disabled="!localNodeData.apiUrl || isLoading">
|
||||||
<Icon name="material-symbols:send" class="mr-1" />
|
<Icon name="material-symbols:send" class="mr-1" />
|
||||||
Test API Call
|
{{ isLoading ? 'Testing...' : 'Test API Call' }}
|
||||||
</RsButton>
|
</RsButton>
|
||||||
|
<div v-if="isLoading" class="text-gray-600 text-sm flex items-center">
|
||||||
|
<Icon name="material-symbols:sync" class="animate-spin mr-2" />
|
||||||
|
Testing API endpoint...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Test Results -->
|
||||||
|
<div v-if="testResult" :class="[
|
||||||
|
'p-4 rounded-md border',
|
||||||
|
testResult.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
|
||||||
|
]">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon
|
||||||
|
:name="testResult.success ? 'material-symbols:check-circle' : 'material-symbols:error'"
|
||||||
|
:class="testResult.success ? 'text-green-500' : 'text-red-500'"
|
||||||
|
class="w-5 h-5 mr-2"
|
||||||
|
/>
|
||||||
|
<span :class="testResult.success ? 'text-green-700' : 'text-red-700'" class="font-medium">
|
||||||
|
{{ testResult.success ? 'API Call Successful' : 'API Call Failed' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button @click="testResult = null" class="text-gray-400 hover:text-gray-600">
|
||||||
|
<Icon name="material-symbols:close" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success Response -->
|
||||||
|
<div v-if="testResult.success" class="mt-3">
|
||||||
|
<div class="text-sm text-gray-600 mb-2">Response stored in variable: {{ localNodeData.outputVariable }}</div>
|
||||||
|
<div class="bg-white border border-green-100 rounded p-3">
|
||||||
|
<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">
|
||||||
|
<pre class="text-xs font-mono whitespace-pre-wrap text-red-600">{{ JSON.stringify(testResult.error, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
import { useProcessBuilderStore } from '@/stores/processBuilder';
|
import { useProcessBuilderStore } from '@/stores/processBuilder';
|
||||||
import { useVariableStore } from '@/stores/variableStore';
|
import { useVariableStore } from '@/stores/variableStore';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
nodeId: {
|
nodeData: {
|
||||||
type: String,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
availableVariables: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the stores
|
const emit = defineEmits(['update']);
|
||||||
const processStore = useProcessBuilderStore();
|
|
||||||
|
// Get the variable store for creating variables
|
||||||
const variableStore = useVariableStore();
|
const variableStore = useVariableStore();
|
||||||
|
|
||||||
// Local state for node data
|
// Local state for node data - create a deep copy to avoid mutation issues
|
||||||
const localNodeData = ref({
|
const localNodeData = ref({
|
||||||
label: 'API Call',
|
label: 'API Call',
|
||||||
description: '',
|
description: '',
|
||||||
@ -205,100 +316,97 @@ const showRequestBody = computed(() => {
|
|||||||
|
|
||||||
// Get available variables for dropdowns
|
// Get available variables for dropdowns
|
||||||
const availableVariables = computed(() => {
|
const availableVariables = computed(() => {
|
||||||
const globalVars = variableStore.getAllVariables.global.map(v => ({
|
// Only use global variables, matching VariableManager implementation
|
||||||
|
const globalVars = props.availableVariables?.map(v => ({
|
||||||
name: v.name || 'unnamed',
|
name: v.name || 'unnamed',
|
||||||
label: v?.description
|
label: v?.description
|
||||||
? `${v.description} (${v.name || 'unnamed'}, global)`
|
? `${v.name} (${v.description})`
|
||||||
: `${v.name || 'unnamed'} (global)`,
|
: v.name,
|
||||||
type: v.type || 'string',
|
type: v.type || 'string',
|
||||||
scope: 'global'
|
value: v.value
|
||||||
}));
|
})) || [];
|
||||||
|
|
||||||
return {
|
return globalVars;
|
||||||
global: globalVars
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load node data when component mounts or nodeId changes
|
// Watch for changes from parent props
|
||||||
watch(() => props.nodeId, () => {
|
watch(() => props.nodeData, (newNodeData) => {
|
||||||
loadNodeData();
|
if (newNodeData) {
|
||||||
}, { immediate: true });
|
// Create a deep copy to break reactivity chains with parent
|
||||||
|
|
||||||
// Watch for changes in local data and emit updates
|
|
||||||
watch(localNodeData, (newValue) => {
|
|
||||||
saveChanges();
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
// Load node data from the store
|
|
||||||
function loadNodeData() {
|
|
||||||
const node = processStore.currentProcess.nodes.find(n => n.id === props.nodeId);
|
|
||||||
if (node && node.data) {
|
|
||||||
localNodeData.value = {
|
localNodeData.value = {
|
||||||
label: node.label || 'API Call',
|
label: newNodeData.label || 'API Call',
|
||||||
description: node.data.description || '',
|
description: newNodeData.description || '',
|
||||||
apiMethod: node.data.apiMethod || 'GET',
|
apiMethod: newNodeData.apiMethod || 'GET',
|
||||||
apiUrl: node.data.apiUrl || '',
|
apiUrl: newNodeData.apiUrl || '',
|
||||||
requestBody: node.data.requestBody || '',
|
requestBody: newNodeData.requestBody || '',
|
||||||
headers: node.data.headers || '{ "Content-Type": "application/json" }',
|
headers: newNodeData.headers || '{ "Content-Type": "application/json" }',
|
||||||
outputVariable: node.data.outputVariable || 'apiResponse',
|
outputVariable: newNodeData.outputVariable || 'apiResponse',
|
||||||
continueOnError: node.data.continueOnError || false,
|
continueOnError: newNodeData.continueOnError || false,
|
||||||
errorVariable: node.data.errorVariable || 'apiError'
|
errorVariable: newNodeData.errorVariable || 'apiError'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
// Function to create a new global variable
|
// Function to create a new global variable
|
||||||
function createGlobalVariable(name, description = '') {
|
function createGlobalVariable(name, description = '') {
|
||||||
variableStore.addVariable({
|
if (!name) return;
|
||||||
|
|
||||||
|
const newVariable = {
|
||||||
name,
|
name,
|
||||||
type: 'object',
|
type: 'object',
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
value: null,
|
|
||||||
description: description || `API response from ${localNodeData.value.label}`
|
description: description || `API response from ${localNodeData.value.label}`
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save changes to the store
|
|
||||||
function saveChanges() {
|
|
||||||
const updates = {
|
|
||||||
label: localNodeData.value.label,
|
|
||||||
data: {
|
|
||||||
...localNodeData.value,
|
|
||||||
label: localNodeData.value.label // Ensure label is in both places
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the node in the store
|
// Add the variable using the store's addVariable method
|
||||||
processStore.updateNode(props.nodeId, updates);
|
variableStore.addVariable(newVariable);
|
||||||
|
|
||||||
// Ensure variables exist
|
// Force a refresh of the component
|
||||||
if (localNodeData.value.outputVariable) {
|
nextTick(() => {
|
||||||
variableStore.addVariableIfNotExists({
|
saveChanges();
|
||||||
name: localNodeData.value.outputVariable,
|
|
||||||
type: 'object',
|
|
||||||
scope: 'global',
|
|
||||||
value: null,
|
|
||||||
description: `API response from ${localNodeData.value.label}`
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (localNodeData.value.errorVariable) {
|
|
||||||
variableStore.addVariableIfNotExists({
|
|
||||||
name: localNodeData.value.errorVariable,
|
|
||||||
type: 'object',
|
|
||||||
scope: 'global',
|
|
||||||
value: null,
|
|
||||||
description: `API error from ${localNodeData.value.label}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test API call function
|
// Save changes by emitting them to parent
|
||||||
|
function saveChanges() {
|
||||||
|
// Create a clean copy of the data to avoid reactivity issues
|
||||||
|
const nodeDataCopy = JSON.parse(JSON.stringify(localNodeData.value));
|
||||||
|
|
||||||
|
// Ensure variables exist before saving
|
||||||
|
if (nodeDataCopy.outputVariable) {
|
||||||
|
variableStore.addVariableIfNotExists({
|
||||||
|
name: nodeDataCopy.outputVariable,
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
value: null,
|
||||||
|
description: `API response from ${nodeDataCopy.label}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeDataCopy.errorVariable) {
|
||||||
|
variableStore.addVariableIfNotExists({
|
||||||
|
name: nodeDataCopy.errorVariable,
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
value: null,
|
||||||
|
description: `API error from ${nodeDataCopy.label}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit the updated data to parent
|
||||||
|
emit('update', nodeDataCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add these refs for handling the API test state
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const testResult = ref(null);
|
||||||
|
|
||||||
|
// Update the testApiCall function
|
||||||
async function testApiCall() {
|
async function testApiCall() {
|
||||||
if (!localNodeData.value.apiUrl) return;
|
if (!localNodeData.value.apiUrl) return;
|
||||||
|
|
||||||
const isLoading = ref(true);
|
isLoading.value = true;
|
||||||
const testResult = ref(null);
|
testResult.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get process variables for substitution
|
// Get process variables for substitution
|
||||||
@ -307,14 +415,12 @@ async function testApiCall() {
|
|||||||
process: {}
|
process: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract variables from the store
|
// Extract variables from available variables
|
||||||
const allVars = variableStore.getAllVariables;
|
if (props.availableVariables) {
|
||||||
allVars.process.forEach(v => {
|
props.availableVariables.forEach(v => {
|
||||||
processVariables.process[v.name] = v.value;
|
|
||||||
});
|
|
||||||
allVars.global.forEach(v => {
|
|
||||||
processVariables.global[v.name] = v.value;
|
processVariables.global[v.name] = v.value;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Call the test API endpoint
|
// Call the test API endpoint
|
||||||
const response = await fetch('/api/process/test-api-node', {
|
const response = await fetch('/api/process/test-api-node', {
|
||||||
@ -327,7 +433,7 @@ async function testApiCall() {
|
|||||||
apiMethod: localNodeData.value.apiMethod,
|
apiMethod: localNodeData.value.apiMethod,
|
||||||
apiUrl: localNodeData.value.apiUrl,
|
apiUrl: localNodeData.value.apiUrl,
|
||||||
requestBody: localNodeData.value.requestBody,
|
requestBody: localNodeData.value.requestBody,
|
||||||
headers: localNodeData.value.headers,
|
headers: JSON.parse(localNodeData.value.headers || '{}'),
|
||||||
outputVariable: localNodeData.value.outputVariable,
|
outputVariable: localNodeData.value.outputVariable,
|
||||||
errorVariable: localNodeData.value.errorVariable,
|
errorVariable: localNodeData.value.errorVariable,
|
||||||
continueOnError: localNodeData.value.continueOnError
|
continueOnError: localNodeData.value.continueOnError
|
||||||
@ -337,37 +443,124 @@ async function testApiCall() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
testResult.value = result;
|
|
||||||
|
|
||||||
if (result.success && localNodeData.value.outputVariable) {
|
// Store the test result
|
||||||
|
testResult.value = {
|
||||||
|
success: response.ok && !result.error,
|
||||||
|
data: result.data,
|
||||||
|
error: result.error || (response.ok ? null : { message: 'API request failed' })
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update variables in the store
|
||||||
|
if (testResult.value.success && localNodeData.value.outputVariable) {
|
||||||
variableStore.updateVariable(
|
variableStore.updateVariable(
|
||||||
localNodeData.value.outputVariable,
|
localNodeData.value.outputVariable,
|
||||||
{ value: result.data },
|
{ value: result.data },
|
||||||
'global'
|
'global'
|
||||||
);
|
);
|
||||||
} else if (!result.success && localNodeData.value.errorVariable) {
|
} else if (!testResult.value.success && localNodeData.value.errorVariable) {
|
||||||
variableStore.updateVariable(
|
variableStore.updateVariable(
|
||||||
localNodeData.value.errorVariable,
|
localNodeData.value.errorVariable,
|
||||||
{ value: result.error },
|
{ value: testResult.value.error },
|
||||||
'global'
|
'global'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('API test error:', error);
|
||||||
testResult.value = {
|
testResult.value = {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
message: error.message || 'An error occurred while testing the API call'
|
message: error.message || 'An error occurred while testing the API call',
|
||||||
|
details: error.toString()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store error in variable
|
||||||
|
if (localNodeData.value.errorVariable) {
|
||||||
|
variableStore.updateVariable(
|
||||||
|
localNodeData.value.errorVariable,
|
||||||
|
{ value: testResult.value.error },
|
||||||
|
'global'
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to insert a variable at cursor position or append to end
|
||||||
|
function insertVariable(variableName, field) {
|
||||||
|
if (!variableName) return;
|
||||||
|
|
||||||
|
const varName = `{${variableName}}`;
|
||||||
|
const textarea = document.getElementById(field);
|
||||||
|
|
||||||
|
if (textarea.selectionStart || textarea.selectionStart === 0) {
|
||||||
|
const startPos = textarea.selectionStart;
|
||||||
|
const endPos = textarea.selectionEnd;
|
||||||
|
|
||||||
|
localNodeData.value[field] =
|
||||||
|
textarea.value.substring(0, startPos) +
|
||||||
|
varName +
|
||||||
|
textarea.value.substring(endPos);
|
||||||
|
|
||||||
|
// Reset cursor position after variable
|
||||||
|
textarea.selectionStart = startPos + varName.length;
|
||||||
|
textarea.selectionEnd = startPos + varName.length;
|
||||||
|
} else {
|
||||||
|
localNodeData.value[field] += varName;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to format JSON in a field
|
||||||
|
function formatJson(field) {
|
||||||
|
try {
|
||||||
|
const value = localNodeData.value[field];
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
localNodeData.value[field] = JSON.stringify(parsed, null, 2);
|
||||||
|
saveChanges();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to format JSON for ${field}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to preview values with actual variable values
|
||||||
|
function getPreviewWithValues(field) {
|
||||||
|
try {
|
||||||
|
let text = localNodeData.value[field];
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
// Replace all variables with their current values
|
||||||
|
const variablePattern = /{([^}]+)}/g;
|
||||||
|
text = text.replace(variablePattern, (match, varName) => {
|
||||||
|
const variable = availableVariables.value.find(v => v.name === varName);
|
||||||
|
if (variable) {
|
||||||
|
return JSON.stringify(variable.value) || '(undefined)';
|
||||||
|
}
|
||||||
|
return '(undefined)';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to format as JSON if possible
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
return JSON.stringify(parsed, null, 2);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return '(Invalid format)';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.api-node-configuration {
|
.api-node-configuration {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
background-color: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
|
382
components/process-flow/FormNodeConfiguration.vue
Normal file
382
components/process-flow/FormNodeConfiguration.vue
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-node-configuration">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">Form Task 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="Form Task"
|
||||||
|
@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="Form task description"
|
||||||
|
rows="2"
|
||||||
|
@blur="saveChanges"
|
||||||
|
></textarea>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Form Selection -->
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<FormSelector
|
||||||
|
:formId="localNodeData.formId"
|
||||||
|
@select="handleFormSelection"
|
||||||
|
@clear="clearFormSelection"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!localNodeData.inputMappings || localNodeData.inputMappings.length === 0" class="text-sm text-gray-500 italic 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs mb-1">Process Variable</label>
|
||||||
|
<select
|
||||||
|
v-model="mapping.processVariable"
|
||||||
|
class="w-full p-2 border rounded text-sm"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select a variable</option>
|
||||||
|
<option
|
||||||
|
v-for="variable in availableVariables"
|
||||||
|
:key="variable.name"
|
||||||
|
:value="variable.name"
|
||||||
|
>
|
||||||
|
{{ variable.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
|
import { useVariableStore } from '@/stores/variableStore';
|
||||||
|
import FormSelector from './FormSelector.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
nodeData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
availableVariables: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
// Get the variable store for creating variables
|
||||||
|
const variableStore = useVariableStore();
|
||||||
|
|
||||||
|
// Local state for node data - create a deep copy to avoid mutation issues
|
||||||
|
const localNodeData = ref({
|
||||||
|
label: 'Form Task',
|
||||||
|
description: '',
|
||||||
|
formId: null,
|
||||||
|
formName: '',
|
||||||
|
formUuid: null,
|
||||||
|
inputMappings: [],
|
||||||
|
outputMappings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes from parent props
|
||||||
|
watch(() => props.nodeData, (newNodeData) => {
|
||||||
|
if (newNodeData) {
|
||||||
|
// Create a deep copy to break reactivity chains with parent
|
||||||
|
localNodeData.value = {
|
||||||
|
label: newNodeData.label || 'Form Task',
|
||||||
|
description: newNodeData.description || '',
|
||||||
|
formId: newNodeData.formId || null,
|
||||||
|
formName: newNodeData.formName || '',
|
||||||
|
formUuid: newNodeData.formUuid || null,
|
||||||
|
inputMappings: Array.isArray(newNodeData.inputMappings)
|
||||||
|
? [...newNodeData.inputMappings]
|
||||||
|
: [],
|
||||||
|
outputMappings: Array.isArray(newNodeData.outputMappings)
|
||||||
|
? [...newNodeData.outputMappings]
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
|
// Function to handle form selection
|
||||||
|
function handleFormSelection(form) {
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
localNodeData.value = {
|
||||||
|
...localNodeData.value,
|
||||||
|
formId: form.formID,
|
||||||
|
formName: form.formName,
|
||||||
|
formUuid: form.formUUID,
|
||||||
|
label: form.formName || 'Form Task',
|
||||||
|
description: `Form: ${form.formName}`
|
||||||
|
};
|
||||||
|
|
||||||
|
saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to clear form selection
|
||||||
|
function clearFormSelection() {
|
||||||
|
localNodeData.value = {
|
||||||
|
...localNodeData.value,
|
||||||
|
formId: null,
|
||||||
|
formName: '',
|
||||||
|
formUuid: null,
|
||||||
|
label: 'Form Task',
|
||||||
|
description: 'Form submission task',
|
||||||
|
inputMappings: [],
|
||||||
|
outputMappings: []
|
||||||
|
};
|
||||||
|
|
||||||
|
saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an input mapping
|
||||||
|
function addInputMapping() {
|
||||||
|
if (!localNodeData.value.inputMappings) {
|
||||||
|
localNodeData.value.inputMappings = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
localNodeData.value.inputMappings.push({
|
||||||
|
processVariable: '',
|
||||||
|
formField: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove an input mapping
|
||||||
|
function removeInputMapping(index) {
|
||||||
|
localNodeData.value.inputMappings.splice(index, 1);
|
||||||
|
saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an output mapping
|
||||||
|
function addOutputMapping() {
|
||||||
|
if (!localNodeData.value.outputMappings) {
|
||||||
|
localNodeData.value.outputMappings = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
localNodeData.value.outputMappings.push({
|
||||||
|
formField: '',
|
||||||
|
processVariable: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove an output mapping
|
||||||
|
function removeOutputMapping(index) {
|
||||||
|
localNodeData.value.outputMappings.splice(index, 1);
|
||||||
|
saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a variable name from a form field
|
||||||
|
function getVariableNameFromFormField(formField) {
|
||||||
|
if (!formField) return 'form_data';
|
||||||
|
|
||||||
|
// Convert the form field to a valid variable name
|
||||||
|
// Remove spaces, special characters, and convert to camelCase
|
||||||
|
return formField.replace(/[^a-zA-Z0-9_]/g, '_')
|
||||||
|
.replace(/^([A-Z])/, (match) => match.toLowerCase())
|
||||||
|
.replace(/[\s_]+(\w)/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a variable from a mapping
|
||||||
|
function createVariableFromMapping(mapping) {
|
||||||
|
if (!mapping.processVariable || !mapping.processVariable.startsWith('create_new_')) return;
|
||||||
|
|
||||||
|
const variableName = mapping.processVariable.replace('create_new_', '');
|
||||||
|
if (!variableName) return;
|
||||||
|
|
||||||
|
// Create the variable
|
||||||
|
const newVariable = {
|
||||||
|
name: variableName,
|
||||||
|
type: 'string',
|
||||||
|
scope: 'global',
|
||||||
|
description: `Form data from ${mapping.formField} in ${localNodeData.value.formName || 'form'}`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the variable
|
||||||
|
variableStore.addVariable(newVariable);
|
||||||
|
|
||||||
|
// Update the mapping to use the new variable
|
||||||
|
mapping.processVariable = variableName;
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
nextTick(() => {
|
||||||
|
saveChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes by emitting them to parent
|
||||||
|
function saveChanges() {
|
||||||
|
// Create a clean copy of the data to avoid reactivity issues
|
||||||
|
const nodeDataCopy = JSON.parse(JSON.stringify(localNodeData.value));
|
||||||
|
|
||||||
|
// Emit the updated data to parent
|
||||||
|
emit('update', nodeDataCopy);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-node-configuration {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -311,48 +311,52 @@ const onDragOver = (event) => {
|
|||||||
event.dataTransfer.dropEffect = 'copy';
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a method to update a node in the flow
|
// Define methods to expose to parent components
|
||||||
const updateNode = (nodeId, updates) => {
|
defineExpose({
|
||||||
if (!nodeId) return;
|
updateNode,
|
||||||
|
addNode,
|
||||||
|
removeNode,
|
||||||
|
fitView
|
||||||
|
});
|
||||||
|
|
||||||
// console.log('ProcessFlowCanvas: Updating node:', nodeId, updates);
|
// Update an existing node
|
||||||
|
function updateNode(nodeId, newData) {
|
||||||
|
const nodeToUpdate = nodes.value.find(node => node.id === nodeId);
|
||||||
|
if (!nodeToUpdate) return;
|
||||||
|
|
||||||
// Find the node in Vue Flow nodes
|
// Update the node properties
|
||||||
const nodeIndex = nodes.value.findIndex(n => n.id === nodeId);
|
if (newData.label) {
|
||||||
if (nodeIndex === -1) {
|
nodeToUpdate.label = newData.label;
|
||||||
console.warn(`Node with ID ${nodeId} not found in flow`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the node with new values
|
// Update the node data
|
||||||
const node = nodes.value[nodeIndex];
|
if (newData.data) {
|
||||||
|
nodeToUpdate.data = {
|
||||||
// Ensure label is consistently set in both places
|
...nodeToUpdate.data,
|
||||||
const updatedLabel = updates.label || node.label;
|
...newData.data
|
||||||
const updatedData = {
|
|
||||||
...node.data,
|
|
||||||
...(updates.data || {}),
|
|
||||||
label: updatedLabel // Ensure label is also in data
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Update the node directly to avoid triggering watchers unnecessarily
|
// Update node internals to trigger re-render
|
||||||
Object.assign(nodes.value[nodeIndex], {
|
updateNodeInternals([nodeId]);
|
||||||
label: updatedLabel,
|
|
||||||
data: updatedData
|
|
||||||
});
|
|
||||||
|
|
||||||
// Notify Vue Flow to update the node's internals
|
return nodeToUpdate;
|
||||||
updateNodeInternals(nodeId);
|
}
|
||||||
|
|
||||||
// console.log('Node updated:', updatedData);
|
// Add a new node to the canvas
|
||||||
|
function addNode(node) {
|
||||||
|
addNodes([node]);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
return updatedData;
|
// Remove a node from the canvas
|
||||||
};
|
function removeNode(nodeId) {
|
||||||
|
const nodeToRemove = nodes.value.find(node => node.id === nodeId);
|
||||||
|
if (!nodeToRemove) return;
|
||||||
|
|
||||||
// Expose methods to parent components
|
removeNodes([nodeToRemove]);
|
||||||
defineExpose({
|
return nodeToRemove;
|
||||||
updateNode
|
}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -507,6 +511,7 @@ defineExpose({
|
|||||||
|
|
||||||
:deep(.node-gateway .custom-node-icon .material-icons) {
|
:deep(.node-gateway .custom-node-icon .material-icons) {
|
||||||
color: #FF9800;
|
color: #FF9800;
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.node-script .custom-node-icon .material-icons) {
|
:deep(.node-script .custom-node-icon .material-icons) {
|
||||||
|
@ -73,9 +73,50 @@ export const TaskNode = markRaw({
|
|||||||
nodeLabel() {
|
nodeLabel() {
|
||||||
// Get label from either prop or data, with fallback
|
// Get label from either prop or data, with fallback
|
||||||
return this.label || (this.data && this.data.label) || 'Task';
|
return this.label || (this.data && this.data.label) || 'Task';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper method to get assignment display text
|
||||||
|
assignmentText() {
|
||||||
|
if (!this.data) return 'Unassigned';
|
||||||
|
|
||||||
|
const { assignmentType, assignedUsers, assignedRoles, assigneeVariable } = this.data;
|
||||||
|
|
||||||
|
if (assignmentType === 'user' && Array.isArray(assignedUsers) && assignedUsers.length > 0) {
|
||||||
|
return `${assignedUsers.length} User${assignedUsers.length > 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignmentType === 'role' && Array.isArray(assignedRoles) && assignedRoles.length > 0) {
|
||||||
|
return `${assignedRoles.length} Role${assignedRoles.length > 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignmentType === 'variable' && assigneeVariable) {
|
||||||
|
return `Variable: ${assigneeVariable}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unassigned';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper to determine priority class
|
||||||
|
priorityClass() {
|
||||||
|
if (!this.data || !this.data.priority) return '';
|
||||||
|
|
||||||
|
const priorityColors = {
|
||||||
|
low: 'text-green-500',
|
||||||
|
medium: 'text-blue-500',
|
||||||
|
high: 'text-orange-500',
|
||||||
|
urgent: 'text-red-500'
|
||||||
|
};
|
||||||
|
|
||||||
|
return priorityColors[this.data.priority] || '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render() {
|
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, {
|
return h(CustomNode, {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
type: 'task',
|
type: 'task',
|
||||||
@ -85,12 +126,22 @@ export const TaskNode = markRaw({
|
|||||||
onClick: () => this.$emit('node-click', this.id)
|
onClick: () => this.$emit('node-click', this.id)
|
||||||
}, {
|
}, {
|
||||||
icon: () => h('i', { class: 'material-icons text-blue-500' }, 'assignment'),
|
icon: () => h('i', { class: 'material-icons text-blue-500' }, 'assignment'),
|
||||||
|
badge: () => badgeContent,
|
||||||
default: () => h('div', { class: 'node-details' }, [
|
default: () => h('div', { class: 'node-details' }, [
|
||||||
h('p', { class: 'node-description' }, this.data?.description || 'A general task'),
|
h('p', { class: 'node-description' }, this.data?.description || 'A general task'),
|
||||||
h('div', { class: 'node-assignee' }, [
|
h('div', { class: 'node-assignee flex items-center justify-between text-xs' }, [
|
||||||
h('span', { class: 'node-assignee-label' }, 'Assigned to:'),
|
h('span', { class: 'node-assignee-label' }, 'Assigned:'),
|
||||||
h('span', { class: 'node-assignee-value' }, this.data?.assignee || 'Unassigned')
|
h('span', { class: 'node-assignee-value ml-1 font-medium text-blue-600' }, this.assignmentText)
|
||||||
])
|
]),
|
||||||
|
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'}`
|
||||||
|
)
|
||||||
|
]) : null
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -178,7 +229,7 @@ export const GatewayNode = markRaw({
|
|||||||
render() {
|
render() {
|
||||||
// Create the badge content
|
// Create the badge content
|
||||||
const badgeContent = h('span', {
|
const badgeContent = h('span', {
|
||||||
class: 'node-badge bg-orange-100 text-orange-600 px-1 text-xs rounded absolute -top-5 left-1/2 transform -translate-x-1/2 whitespace-nowrap'
|
class: 'node-badge bg-orange-100 text-orange-600 px-1 text-xs rounded'
|
||||||
}, `${this.totalPaths} path${this.totalPaths !== 1 ? 's' : ''}`);
|
}, `${this.totalPaths} path${this.totalPaths !== 1 ? 's' : ''}`);
|
||||||
|
|
||||||
return h(CustomNode, {
|
return h(CustomNode, {
|
||||||
@ -192,7 +243,11 @@ export const GatewayNode = markRaw({
|
|||||||
icon: () => h('i', { class: 'material-icons text-orange-500' }, 'call_split'),
|
icon: () => h('i', { class: 'material-icons text-orange-500' }, 'call_split'),
|
||||||
badge: () => badgeContent,
|
badge: () => badgeContent,
|
||||||
default: () => h('div', { class: 'gateway-details' }, [
|
default: () => h('div', { class: 'gateway-details' }, [
|
||||||
h('div', { class: 'node-conditions-value' }, this.conditionSummary)
|
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)
|
||||||
|
])
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -402,8 +457,8 @@ export const nodeStyles = `
|
|||||||
.node-gateway .custom-node-content {
|
.node-gateway .custom-node-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
width: 100%;
|
width: 120%;
|
||||||
height: 100%;
|
height: 120%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -627,22 +682,40 @@ export const nodeStyles = `
|
|||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-gateway .node-badge {
|
.node-gateway .node-description {
|
||||||
position: absolute;
|
text-align: center;
|
||||||
top: 0;
|
margin-bottom: 4px;
|
||||||
left: 50%;
|
font-size: 10px;
|
||||||
transform: translateX(-50%) rotate(-45deg);
|
overflow: hidden;
|
||||||
background-color: #fff7ed;
|
display: -webkit-box;
|
||||||
border: 1px solid #fdba74;
|
-webkit-line-clamp: 2;
|
||||||
z-index: 10;
|
-webkit-box-orient: vertical;
|
||||||
font-size: 11px;
|
}
|
||||||
padding: 2px 8px;
|
|
||||||
|
.node-gateway .node-conditions {
|
||||||
|
display: flex;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-gateway .node-conditions-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-gateway .node-conditions-value {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-top: 8px;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 10px;
|
||||||
|
max-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-gateway .material-icons {
|
.node-gateway .material-icons {
|
||||||
font-size: 24px;
|
font-size: 18px;
|
||||||
color: #f97316;
|
color: #f97316;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
391
components/process-flow/TaskNodeConfiguration.vue
Normal file
391
components/process-flow/TaskNodeConfiguration.vue
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
<template>
|
||||||
|
<div class="task-node-configuration">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">Task Configuration</h3>
|
||||||
|
|
||||||
|
<!-- <div class="form-group mb-4">
|
||||||
|
<label for="nodeLabel" class="form-label">Task Name</label>
|
||||||
|
<input
|
||||||
|
id="nodeLabel"
|
||||||
|
v-model="localNodeData.label"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Task Name"
|
||||||
|
@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="Task description"
|
||||||
|
rows="2"
|
||||||
|
@blur="saveChanges"
|
||||||
|
></textarea>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Assignment Type Selector -->
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label class="form-label">Assignment Type</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
v-model="localNodeData.assignmentType"
|
||||||
|
value="user"
|
||||||
|
class="form-radio"
|
||||||
|
@change="saveChanges"
|
||||||
|
/>
|
||||||
|
<span class="ml-2">User</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
v-model="localNodeData.assignmentType"
|
||||||
|
value="role"
|
||||||
|
class="form-radio"
|
||||||
|
@change="saveChanges"
|
||||||
|
/>
|
||||||
|
<span class="ml-2">Role</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
v-model="localNodeData.assignmentType"
|
||||||
|
value="variable"
|
||||||
|
class="form-radio"
|
||||||
|
@change="saveChanges"
|
||||||
|
/>
|
||||||
|
<span class="ml-2">Process Variable</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Assignment -->
|
||||||
|
<div v-if="localNodeData.assignmentType === 'user'" class="form-group mb-4">
|
||||||
|
<label class="form-label">Assign to Users</label>
|
||||||
|
<div v-if="loading.users" class="py-2 text-gray-500 text-sm">
|
||||||
|
Loading users...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="users.length === 0" class="py-2 text-gray-500 text-sm">
|
||||||
|
No users available
|
||||||
|
</div>
|
||||||
|
<div v-else class="max-h-60 overflow-y-auto border rounded-md divide-y">
|
||||||
|
<div
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.userID"
|
||||||
|
class="p-2 hover:bg-gray-50 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:id="`user-${user.userID}`"
|
||||||
|
:value="user.userID"
|
||||||
|
v-model="localNodeData.assignedUsers"
|
||||||
|
class="form-checkbox"
|
||||||
|
@change="saveChanges"
|
||||||
|
/>
|
||||||
|
<label :for="`user-${user.userID}`" class="ml-2 cursor-pointer flex-1">
|
||||||
|
<div class="font-medium text-sm">{{ user.userFullName || user.userUsername }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ user.userEmail }}</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role Assignment -->
|
||||||
|
<div v-if="localNodeData.assignmentType === 'role'" class="form-group mb-4">
|
||||||
|
<label class="form-label">Assign to Roles</label>
|
||||||
|
<div v-if="loading.roles" class="py-2 text-gray-500 text-sm">
|
||||||
|
Loading roles...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="roles.length === 0" class="py-2 text-gray-500 text-sm">
|
||||||
|
No roles available
|
||||||
|
</div>
|
||||||
|
<div v-else class="max-h-60 overflow-y-auto border rounded-md divide-y">
|
||||||
|
<div
|
||||||
|
v-for="role in roles"
|
||||||
|
:key="role.roleID"
|
||||||
|
class="p-2 hover:bg-gray-50 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:id="`role-${role.roleID}`"
|
||||||
|
:value="role.roleID"
|
||||||
|
v-model="localNodeData.assignedRoles"
|
||||||
|
class="form-checkbox"
|
||||||
|
@change="saveChanges"
|
||||||
|
/>
|
||||||
|
<label :for="`role-${role.roleID}`" class="ml-2 cursor-pointer flex-1">
|
||||||
|
<div class="font-medium text-sm">{{ role.roleName }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ role.roleDescription }}</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Variable Assignment -->
|
||||||
|
<div v-if="localNodeData.assignmentType === 'variable'" class="form-group mb-4">
|
||||||
|
<label class="form-label">Assignee Variable</label>
|
||||||
|
<select
|
||||||
|
v-model="localNodeData.assigneeVariable"
|
||||||
|
class="form-control"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select a variable</option>
|
||||||
|
<option
|
||||||
|
v-for="variable in availableVariables"
|
||||||
|
:key="variable.name"
|
||||||
|
:value="variable.name"
|
||||||
|
>
|
||||||
|
{{ variable.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-gray-500 text-xs mt-1 block">
|
||||||
|
The selected variable should contain a user ID or a role ID.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Task Settings -->
|
||||||
|
<div class="border-t border-gray-200 my-4 pt-4">
|
||||||
|
<h4 class="text-base font-medium mb-2">Additional Settings</h4>
|
||||||
|
|
||||||
|
<!-- Due Date -->
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label class="form-label">Due Date</label>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<select
|
||||||
|
v-model="localNodeData.dueDateType"
|
||||||
|
class="form-control"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="none">No due date</option>
|
||||||
|
<option value="fixed">Fixed duration</option>
|
||||||
|
<option value="variable">From variable</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div v-if="localNodeData.dueDateType === 'fixed'">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="localNodeData.dueDateDays"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Days"
|
||||||
|
min="0"
|
||||||
|
@blur="saveChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="localNodeData.dueDateType === 'variable'">
|
||||||
|
<select
|
||||||
|
v-model="localNodeData.dueDateVariable"
|
||||||
|
class="form-control"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select a variable</option>
|
||||||
|
<option
|
||||||
|
v-for="variable in availableVariables"
|
||||||
|
:key="variable.name"
|
||||||
|
:value="variable.name"
|
||||||
|
>
|
||||||
|
{{ variable.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Priority -->
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label class="form-label">Priority</label>
|
||||||
|
<select
|
||||||
|
v-model="localNodeData.priority"
|
||||||
|
class="form-control"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
<option value="urgent">Urgent</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Instructions -->
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label class="form-label">Task Instructions</label>
|
||||||
|
<textarea
|
||||||
|
v-model="localNodeData.instructions"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Instructions for the assignee"
|
||||||
|
@blur="saveChanges"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import { useVariableStore } from '@/stores/variableStore';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
nodeData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
availableVariables: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
// Local state
|
||||||
|
const users = ref([]);
|
||||||
|
const roles = ref([]);
|
||||||
|
const loading = ref({
|
||||||
|
users: false,
|
||||||
|
roles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Local state for node data
|
||||||
|
const localNodeData = ref({
|
||||||
|
label: 'Task',
|
||||||
|
description: '',
|
||||||
|
assignmentType: 'user', // 'user', 'role', or 'variable'
|
||||||
|
assignedUsers: [],
|
||||||
|
assignedRoles: [],
|
||||||
|
assigneeVariable: '',
|
||||||
|
dueDateType: 'none', // 'none', 'fixed', or 'variable'
|
||||||
|
dueDateDays: 3,
|
||||||
|
dueDateVariable: '',
|
||||||
|
priority: 'medium',
|
||||||
|
instructions: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the variable store for variables
|
||||||
|
const variableStore = useVariableStore();
|
||||||
|
|
||||||
|
// Watch for changes from parent props
|
||||||
|
watch(() => props.nodeData, (newNodeData) => {
|
||||||
|
if (newNodeData) {
|
||||||
|
// Create a deep copy to break reactivity chains with parent
|
||||||
|
localNodeData.value = {
|
||||||
|
label: newNodeData.label || 'Task',
|
||||||
|
description: newNodeData.description || '',
|
||||||
|
assignmentType: newNodeData.assignmentType || 'user',
|
||||||
|
assignedUsers: Array.isArray(newNodeData.assignedUsers) ? [...newNodeData.assignedUsers] : [],
|
||||||
|
assignedRoles: Array.isArray(newNodeData.assignedRoles) ? [...newNodeData.assignedRoles] : [],
|
||||||
|
assigneeVariable: newNodeData.assigneeVariable || '',
|
||||||
|
dueDateType: newNodeData.dueDateType || 'none',
|
||||||
|
dueDateDays: newNodeData.dueDateDays || 3,
|
||||||
|
dueDateVariable: newNodeData.dueDateVariable || '',
|
||||||
|
priority: newNodeData.priority || 'medium',
|
||||||
|
instructions: newNodeData.instructions || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
|
// Fetch users and roles when component is mounted
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchUsers();
|
||||||
|
await fetchRoles();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch users from API
|
||||||
|
async function fetchUsers() {
|
||||||
|
loading.value.users = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/users');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && Array.isArray(result.users)) {
|
||||||
|
users.value = result.users;
|
||||||
|
} else {
|
||||||
|
console.error('Error fetching users:', result.error || 'Unknown error');
|
||||||
|
users.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching users:', error);
|
||||||
|
users.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value.users = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch roles from API
|
||||||
|
async function fetchRoles() {
|
||||||
|
loading.value.roles = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/roles');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && Array.isArray(result.roles)) {
|
||||||
|
roles.value = result.roles;
|
||||||
|
} else {
|
||||||
|
console.error('Error fetching roles:', result.error || 'Unknown error');
|
||||||
|
roles.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching roles:', error);
|
||||||
|
roles.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value.roles = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes by emitting them to parent
|
||||||
|
function saveChanges() {
|
||||||
|
// Create a clean copy of the data to avoid reactivity issues
|
||||||
|
const nodeDataCopy = JSON.parse(JSON.stringify(localNodeData.value));
|
||||||
|
|
||||||
|
// Emit the updated data to parent
|
||||||
|
emit('update', nodeDataCopy);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.task-node-configuration {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-radio {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -10,6 +10,8 @@ import GatewayConditionManager from '~/components/process-flow/GatewayConditionM
|
|||||||
import ApiNodeConfiguration from '~/components/process-flow/ApiNodeConfiguration.vue';
|
import ApiNodeConfiguration from '~/components/process-flow/ApiNodeConfiguration.vue';
|
||||||
import VariableManager from '~/components/process-flow/VariableManager.vue';
|
import VariableManager from '~/components/process-flow/VariableManager.vue';
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
|
import FormNodeConfiguration from '~/components/process-flow/FormNodeConfiguration.vue';
|
||||||
|
import TaskNodeConfiguration from '~/components/process-flow/TaskNodeConfiguration.vue';
|
||||||
|
|
||||||
// Define page meta
|
// Define page meta
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@ -624,6 +626,66 @@ const onConditionsUpdated = (conditions) => {
|
|||||||
updateNodeInStore();
|
updateNodeInStore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle API node update
|
||||||
|
const handleApiNodeUpdate = (updatedData) => {
|
||||||
|
if (selectedNodeData.value && selectedNodeData.value.type === 'api') {
|
||||||
|
// Make sure to update the label both in data and at the root level
|
||||||
|
const newLabel = updatedData.label || 'API Call';
|
||||||
|
|
||||||
|
// Update the data
|
||||||
|
selectedNodeData.value.data = {
|
||||||
|
...updatedData,
|
||||||
|
label: newLabel // Ensure label is in data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also update the root label
|
||||||
|
selectedNodeData.value.label = newLabel;
|
||||||
|
|
||||||
|
// Update the node in store
|
||||||
|
updateNodeInStore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this function to handle form node updates
|
||||||
|
const handleFormNodeUpdate = (updatedData) => {
|
||||||
|
if (selectedNodeData.value && selectedNodeData.value.type === 'form') {
|
||||||
|
// Make sure to update the label both in data and at the root level
|
||||||
|
const newLabel = updatedData.label || 'Form Task';
|
||||||
|
|
||||||
|
// Update the data
|
||||||
|
selectedNodeData.value.data = {
|
||||||
|
...updatedData,
|
||||||
|
label: newLabel // Ensure label is in data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also update the root label
|
||||||
|
selectedNodeData.value.label = newLabel;
|
||||||
|
|
||||||
|
// Update the node in store
|
||||||
|
updateNodeInStore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this function to handle task node updates
|
||||||
|
const handleTaskNodeUpdate = (updatedData) => {
|
||||||
|
if (selectedNodeData.value && selectedNodeData.value.type === 'task') {
|
||||||
|
// Make sure to update the label both in data and at the root level
|
||||||
|
const newLabel = updatedData.label || 'Task';
|
||||||
|
|
||||||
|
// Update the data
|
||||||
|
selectedNodeData.value.data = {
|
||||||
|
...updatedData,
|
||||||
|
label: newLabel // Ensure label is in data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also update the root label
|
||||||
|
selectedNodeData.value.label = newLabel;
|
||||||
|
|
||||||
|
// Update the node in store
|
||||||
|
updateNodeInStore();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -724,27 +786,28 @@ const onConditionsUpdated = (conditions) => {
|
|||||||
|
|
||||||
<!-- Node Type Specific Properties -->
|
<!-- Node Type Specific Properties -->
|
||||||
<div v-if="selectedNodeData.type === 'task'">
|
<div v-if="selectedNodeData.type === 'task'">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Assignee</label>
|
<TaskNodeConfiguration
|
||||||
<input
|
:nodeData="selectedNodeData.data"
|
||||||
v-model="nodeAssignee"
|
:availableVariables="variableStore.getAllVariables.global"
|
||||||
type="text"
|
@update="handleTaskNodeUpdate"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Selection for Form Nodes -->
|
<!-- Form Selection for Form Nodes -->
|
||||||
<div v-if="selectedNodeData.type === 'form'">
|
<div v-if="selectedNodeData.type === 'form'">
|
||||||
<FormSelector
|
<FormNodeConfiguration
|
||||||
@select="handleFormSelection"
|
:nodeData="selectedNodeData.data"
|
||||||
@clear="clearFormSelection"
|
:availableVariables="variableStore.getAllVariables.global"
|
||||||
:formId="selectedNodeData.data?.formId"
|
@update="handleFormNodeUpdate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Configuration for API Nodes -->
|
<!-- API Configuration for API Nodes -->
|
||||||
<div v-if="selectedNodeData.type === 'api'">
|
<div v-if="selectedNodeData.type === 'api'">
|
||||||
<ApiNodeConfiguration
|
<ApiNodeConfiguration
|
||||||
:nodeId="selectedNodeData.id"
|
:nodeData="selectedNodeData.data"
|
||||||
|
:availableVariables="gatewayAvailableVariables"
|
||||||
|
@update="handleApiNodeUpdate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
36
server/api/roles/index.get.js
Normal file
36
server/api/roles/index.get.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// Get all active roles
|
||||||
|
const roles = await prisma.role.findMany({
|
||||||
|
where: {
|
||||||
|
roleStatus: 'active' // Assuming there's a status field to filter active roles
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
roleID: true,
|
||||||
|
roleName: true,
|
||||||
|
roleDescription: true
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
roleName: 'asc'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.$disconnect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
roles: roles
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching roles:', error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch roles'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
37
server/api/users/index.get.js
Normal file
37
server/api/users/index.get.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// Get all active users
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
userStatus: 'active' // Assuming there's a status field to filter active users
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userID: true,
|
||||||
|
userUsername: true,
|
||||||
|
userFullName: true,
|
||||||
|
userEmail: true
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
userFullName: 'asc'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.$disconnect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
users: users
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching users:', error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch users'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user