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:
Afiq 2025-05-19 17:03:00 +08:00
parent dce3e7f0f2
commit c35073f7fe
8 changed files with 1381 additions and 201 deletions

View File

@ -2,7 +2,7 @@
<div class="api-node-configuration">
<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>
<input
id="nodeLabel"
@ -10,10 +10,11 @@
type="text"
class="form-control"
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>
<textarea
id="nodeDescription"
@ -21,8 +22,9 @@
class="form-control"
placeholder="API call description"
rows="2"
@blur="saveChanges"
></textarea>
</div>
</div> -->
<div class="form-group mb-4">
<label for="apiMethod" class="form-label">HTTP Method</label>
@ -30,6 +32,7 @@
id="apiMethod"
v-model="localNodeData.apiMethod"
class="form-control"
@change="saveChanges"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
@ -47,37 +50,112 @@
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>
<textarea
id="requestBody"
v-model="localNodeData.requestBody"
class="form-control font-mono"
placeholder='{ "key": "value" }'
rows="4"
></textarea>
<small class="form-text text-muted">
You can use process variables with curly braces: { "userId": "{userId}" }
<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
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>
</div>
</div>
</div>
<!-- Variable Insertion for Headers -->
<div class="form-group mb-4">
<label for="headers" class="form-label">Headers</label>
<textarea
id="headers"
v-model="localNodeData.headers"
class="form-control font-mono"
placeholder='{ "Content-Type": "application/json" }'
rows="3"
></textarea>
<div 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
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>
</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">
@ -85,18 +163,17 @@
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>
<optgroup label="Global Variables">
<option
v-for="variable in availableVariables.global"
:key="variable.name"
:value="variable.name"
>
{{ variable.description ? `${variable.name} (${variable.description})` : variable.name }}
</option>
</optgroup>
<option
v-for="variable in availableVariables"
:key="variable.name"
:value="variable.name"
>
{{ variable.label }}
</option>
</select>
<button
@click="createGlobalVariable(localNodeData.outputVariable)"
@ -111,21 +188,7 @@
</small>
</div>
<div class="form-group mb-4">
<label class="form-label d-block">Error Handling</label>
<div class="form-check">
<input
id="continueOnError"
v-model="localNodeData.continueOnError"
type="checkbox"
class="form-check-input"
/>
<label for="continueOnError" class="form-check-label">
Continue process execution on error
</label>
</div>
</div>
<!-- Error Variable Selection -->
<div class="form-group mb-4">
<label for="errorVariable" class="form-label">Error Variable</label>
<div class="flex gap-2">
@ -133,18 +196,17 @@
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>
<optgroup label="Global Variables">
<option
v-for="variable in availableVariables.global"
:key="variable.name"
:value="variable.name"
>
{{ variable.description ? `${variable.name} (${variable.description})` : variable.name }}
</option>
</optgroup>
<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}`)"
@ -159,33 +221,82 @@
</small>
</div>
<!-- Test API Call Button -->
<div class="form-group mt-6">
<RsButton @click="testApiCall" variant="primary" :disabled="!localNodeData.apiUrl">
<Icon name="material-symbols:send" class="mr-1" />
Test API Call
</RsButton>
<!-- Test API Call Button and Results -->
<div class="form-group mt-6 space-y-4">
<div class="flex items-center gap-4">
<RsButton @click="testApiCall" variant="primary" :disabled="!localNodeData.apiUrl || isLoading">
<Icon name="material-symbols:send" class="mr-1" />
{{ isLoading ? 'Testing...' : 'Test API Call' }}
</RsButton>
<div v-if="isLoading" class="text-gray-600 text-sm flex items-center">
<Icon name="material-symbols:sync" class="animate-spin mr-2" />
Testing API endpoint...
</div>
</div>
<!-- API Test Results -->
<div v-if="testResult" :class="[
'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>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, nextTick } from 'vue';
import { useProcessBuilderStore } from '@/stores/processBuilder';
import { useVariableStore } from '@/stores/variableStore';
const props = defineProps({
nodeId: {
type: String,
nodeData: {
type: Object,
required: true
},
availableVariables: {
type: Array,
default: () => []
}
});
// Get the stores
const processStore = useProcessBuilderStore();
const emit = defineEmits(['update']);
// Get the variable store for creating variables
const variableStore = useVariableStore();
// Local state for node data
// Local state for node data - create a deep copy to avoid mutation issues
const localNodeData = ref({
label: 'API Call',
description: '',
@ -205,100 +316,97 @@ const showRequestBody = computed(() => {
// Get available variables for dropdowns
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',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, global)`
: `${v.name || 'unnamed'} (global)`,
? `${v.name} (${v.description})`
: v.name,
type: v.type || 'string',
scope: 'global'
}));
return {
global: globalVars
};
value: v.value
})) || [];
return globalVars;
});
// Load node data when component mounts or nodeId changes
watch(() => props.nodeId, () => {
loadNodeData();
}, { immediate: true });
// Watch for changes in local data and emit updates
watch(localNodeData, (newValue) => {
saveChanges();
}, { deep: true });
// Load node data from the store
function loadNodeData() {
const node = processStore.currentProcess.nodes.find(n => n.id === props.nodeId);
if (node && node.data) {
// 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: node.label || 'API Call',
description: node.data.description || '',
apiMethod: node.data.apiMethod || 'GET',
apiUrl: node.data.apiUrl || '',
requestBody: node.data.requestBody || '',
headers: node.data.headers || '{ "Content-Type": "application/json" }',
outputVariable: node.data.outputVariable || 'apiResponse',
continueOnError: node.data.continueOnError || false,
errorVariable: node.data.errorVariable || 'apiError'
label: newNodeData.label || 'API Call',
description: newNodeData.description || '',
apiMethod: newNodeData.apiMethod || 'GET',
apiUrl: newNodeData.apiUrl || '',
requestBody: newNodeData.requestBody || '',
headers: newNodeData.headers || '{ "Content-Type": "application/json" }',
outputVariable: newNodeData.outputVariable || 'apiResponse',
continueOnError: newNodeData.continueOnError || false,
errorVariable: newNodeData.errorVariable || 'apiError'
};
}
}
}, { immediate: true, deep: true });
// Function to create a new global variable
function createGlobalVariable(name, description = '') {
variableStore.addVariable({
if (!name) return;
const newVariable = {
name,
type: 'object',
scope: 'global',
value: null,
description: description || `API response from ${localNodeData.value.label}`
};
// Add the variable using the store's addVariable method
variableStore.addVariable(newVariable);
// Force a refresh of the component
nextTick(() => {
saveChanges();
});
}
// Save changes to the store
// Save changes by emitting them to parent
function saveChanges() {
const updates = {
label: localNodeData.value.label,
data: {
...localNodeData.value,
label: localNodeData.value.label // Ensure label is in both places
}
};
// Update the node in the store
processStore.updateNode(props.nodeId, updates);
// Create a clean copy of the data to avoid reactivity issues
const nodeDataCopy = JSON.parse(JSON.stringify(localNodeData.value));
// Ensure variables exist
if (localNodeData.value.outputVariable) {
// Ensure variables exist before saving
if (nodeDataCopy.outputVariable) {
variableStore.addVariableIfNotExists({
name: localNodeData.value.outputVariable,
name: nodeDataCopy.outputVariable,
type: 'object',
scope: 'global',
value: null,
description: `API response from ${localNodeData.value.label}`
description: `API response from ${nodeDataCopy.label}`
});
}
if (localNodeData.value.errorVariable) {
if (nodeDataCopy.errorVariable) {
variableStore.addVariableIfNotExists({
name: localNodeData.value.errorVariable,
name: nodeDataCopy.errorVariable,
type: 'object',
scope: 'global',
value: null,
description: `API error from ${localNodeData.value.label}`
description: `API error from ${nodeDataCopy.label}`
});
}
// Emit the updated data to parent
emit('update', nodeDataCopy);
}
// Test API call function
// Add these refs for handling the API test state
const isLoading = ref(false);
const testResult = ref(null);
// Update the testApiCall function
async function testApiCall() {
if (!localNodeData.value.apiUrl) return;
const isLoading = ref(true);
const testResult = ref(null);
isLoading.value = true;
testResult.value = null;
try {
// Get process variables for substitution
@ -307,14 +415,12 @@ async function testApiCall() {
process: {}
};
// Extract variables from the store
const allVars = variableStore.getAllVariables;
allVars.process.forEach(v => {
processVariables.process[v.name] = v.value;
});
allVars.global.forEach(v => {
processVariables.global[v.name] = v.value;
});
// Extract variables from available variables
if (props.availableVariables) {
props.availableVariables.forEach(v => {
processVariables.global[v.name] = v.value;
});
}
// Call the test API endpoint
const response = await fetch('/api/process/test-api-node', {
@ -327,7 +433,7 @@ async function testApiCall() {
apiMethod: localNodeData.value.apiMethod,
apiUrl: localNodeData.value.apiUrl,
requestBody: localNodeData.value.requestBody,
headers: localNodeData.value.headers,
headers: JSON.parse(localNodeData.value.headers || '{}'),
outputVariable: localNodeData.value.outputVariable,
errorVariable: localNodeData.value.errorVariable,
continueOnError: localNodeData.value.continueOnError
@ -337,37 +443,124 @@ async function testApiCall() {
});
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(
localNodeData.value.outputVariable,
{ value: result.data },
'global'
);
} else if (!result.success && localNodeData.value.errorVariable) {
} else if (!testResult.value.success && localNodeData.value.errorVariable) {
variableStore.updateVariable(
localNodeData.value.errorVariable,
{ value: result.error },
{ value: testResult.value.error },
'global'
);
}
} catch (error) {
console.error('API test error:', error);
testResult.value = {
success: false,
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 {
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>
<style scoped>
.api-node-configuration {
padding: 1rem;
background-color: #f8f8f8;
}
.form-group {

View 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>

View File

@ -311,48 +311,52 @@ const onDragOver = (event) => {
event.dataTransfer.dropEffect = 'copy';
};
// Add a method to update a node in the flow
const updateNode = (nodeId, updates) => {
if (!nodeId) return;
// Define methods to expose to parent components
defineExpose({
updateNode,
addNode,
removeNode,
fitView
});
// Update an existing node
function updateNode(nodeId, newData) {
const nodeToUpdate = nodes.value.find(node => node.id === nodeId);
if (!nodeToUpdate) return;
// console.log('ProcessFlowCanvas: Updating node:', nodeId, updates);
// Find the node in Vue Flow nodes
const nodeIndex = nodes.value.findIndex(n => n.id === nodeId);
if (nodeIndex === -1) {
console.warn(`Node with ID ${nodeId} not found in flow`);
return;
// Update the node properties
if (newData.label) {
nodeToUpdate.label = newData.label;
}
// Update the node with new values
const node = nodes.value[nodeIndex];
// Update the node data
if (newData.data) {
nodeToUpdate.data = {
...nodeToUpdate.data,
...newData.data
};
}
// Ensure label is consistently set in both places
const updatedLabel = updates.label || node.label;
const updatedData = {
...node.data,
...(updates.data || {}),
label: updatedLabel // Ensure label is also in data
};
// Update node internals to trigger re-render
updateNodeInternals([nodeId]);
// Update the node directly to avoid triggering watchers unnecessarily
Object.assign(nodes.value[nodeIndex], {
label: updatedLabel,
data: updatedData
});
// Notify Vue Flow to update the node's internals
updateNodeInternals(nodeId);
// console.log('Node updated:', updatedData);
return updatedData;
};
return nodeToUpdate;
}
// Expose methods to parent components
defineExpose({
updateNode
});
// Add a new node to the canvas
function addNode(node) {
addNodes([node]);
return node;
}
// Remove a node from the canvas
function removeNode(nodeId) {
const nodeToRemove = nodes.value.find(node => node.id === nodeId);
if (!nodeToRemove) return;
removeNodes([nodeToRemove]);
return nodeToRemove;
}
</script>
<template>
@ -507,6 +511,7 @@ defineExpose({
:deep(.node-gateway .custom-node-icon .material-icons) {
color: #FF9800;
font-size: 18px;
}
:deep(.node-script .custom-node-icon .material-icons) {

View File

@ -73,9 +73,50 @@ export const TaskNode = markRaw({
nodeLabel() {
// Get label from either prop or data, with fallback
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() {
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',
@ -85,12 +126,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' }, [
h('span', { class: 'node-assignee-label' }, 'Assigned to:'),
h('span', { class: 'node-assignee-value' }, this.data?.assignee || 'Unassigned')
])
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)
]),
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() {
// Create the badge content
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' : ''}`);
return h(CustomNode, {
@ -192,7 +243,11 @@ export const GatewayNode = markRaw({
icon: () => h('i', { class: 'material-icons text-orange-500' }, 'call_split'),
badge: () => badgeContent,
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 {
position: absolute;
transform: rotate(-45deg);
width: 100%;
height: 100%;
width: 120%;
height: 120%;
display: flex;
flex-direction: column;
align-items: center;
@ -627,22 +682,40 @@ export const nodeStyles = `
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.node-gateway .node-badge {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%) rotate(-45deg);
background-color: #fff7ed;
border: 1px solid #fdba74;
z-index: 10;
font-size: 11px;
padding: 2px 8px;
.node-gateway .node-description {
text-align: center;
margin-bottom: 4px;
font-size: 10px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.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;
margin-top: 8px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 10px;
max-width: 80px;
}
.node-gateway .material-icons {
font-size: 24px;
font-size: 18px;
color: #f97316;
margin-bottom: 4px;
}

View 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>

View File

@ -10,6 +10,8 @@ import GatewayConditionManager from '~/components/process-flow/GatewayConditionM
import ApiNodeConfiguration from '~/components/process-flow/ApiNodeConfiguration.vue';
import VariableManager from '~/components/process-flow/VariableManager.vue';
import { onBeforeRouteLeave } from 'vue-router';
import FormNodeConfiguration from '~/components/process-flow/FormNodeConfiguration.vue';
import TaskNodeConfiguration from '~/components/process-flow/TaskNodeConfiguration.vue';
// Define page meta
definePageMeta({
@ -624,6 +626,66 @@ const onConditionsUpdated = (conditions) => {
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>
<template>
@ -724,27 +786,28 @@ const onConditionsUpdated = (conditions) => {
<!-- Node Type Specific Properties -->
<div v-if="selectedNodeData.type === 'task'">
<label class="block text-sm font-medium text-gray-700 mb-1">Assignee</label>
<input
v-model="nodeAssignee"
type="text"
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"
<TaskNodeConfiguration
:nodeData="selectedNodeData.data"
:availableVariables="variableStore.getAllVariables.global"
@update="handleTaskNodeUpdate"
/>
</div>
<!-- Form Selection for Form Nodes -->
<div v-if="selectedNodeData.type === 'form'">
<FormSelector
@select="handleFormSelection"
@clear="clearFormSelection"
:formId="selectedNodeData.data?.formId"
<FormNodeConfiguration
:nodeData="selectedNodeData.data"
:availableVariables="variableStore.getAllVariables.global"
@update="handleFormNodeUpdate"
/>
</div>
<!-- API Configuration for API Nodes -->
<div v-if="selectedNodeData.type === 'api'">
<ApiNodeConfiguration
:nodeId="selectedNodeData.id"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@update="handleApiNodeUpdate"
/>
</div>

View 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'
};
}
});

View 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'
};
}
});