Add Business Rule Node Configuration and Modals
- Introduced BusinessRuleNodeConfiguration and BusinessRuleNodeConfigurationModal components for configuring business rules within the process builder. - Enhanced ProcessBuilderComponents to include the new Business Rule node type with default properties. - Implemented BusinessRuleNode in ProcessFlowNodes for rendering business rule nodes with relevant details. - Updated the process builder to support business rule configurations, allowing users to define conditions and actions visually. - Improved overall user experience by refining the UI for business rule management and enhancing variable handling in the process builder.
This commit is contained in:
parent
c35073f7fe
commit
0abb905477
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<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>
|
||||||
|
72
components/process-flow/ApiNodeConfigurationModal.vue
Normal file
72
components/process-flow/ApiNodeConfigurationModal.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<RsModal
|
||||||
|
v-model="showModal"
|
||||||
|
title="API Call Configuration"
|
||||||
|
size="lg"
|
||||||
|
position="center"
|
||||||
|
:okCallback="saveAndClose"
|
||||||
|
okTitle="Save"
|
||||||
|
:cancelCallback="closeModal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<ApiNodeConfiguration
|
||||||
|
:nodeData="nodeData"
|
||||||
|
:availableVariables="availableVariables"
|
||||||
|
@update="handleUpdate"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</RsModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import ApiNodeConfiguration from './ApiNodeConfiguration.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
nodeData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
availableVariables: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'update']);
|
||||||
|
|
||||||
|
const showModal = ref(props.modelValue);
|
||||||
|
const localNodeData = ref({ ...props.nodeData });
|
||||||
|
|
||||||
|
// Watch for changes to modelValue prop to sync modal visibility
|
||||||
|
watch(() => props.modelValue, (value) => {
|
||||||
|
showModal.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to showModal to emit update:modelValue
|
||||||
|
watch(() => showModal.value, (value) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to nodeData prop
|
||||||
|
watch(() => props.nodeData, (value) => {
|
||||||
|
localNodeData.value = { ...value };
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
function handleUpdate(updatedData) {
|
||||||
|
localNodeData.value = { ...updatedData };
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAndClose() {
|
||||||
|
emit('update', localNodeData.value);
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
726
components/process-flow/BusinessRuleNodeConfiguration.vue
Normal file
726
components/process-flow/BusinessRuleNodeConfiguration.vue
Normal file
@ -0,0 +1,726 @@
|
|||||||
|
<template>
|
||||||
|
<div class="business-rule-node-configuration">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">Business Rule Configuration</h3>
|
||||||
|
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="nodeLabel" class="form-label">Rule Name</label>
|
||||||
|
<input
|
||||||
|
id="nodeLabel"
|
||||||
|
v-model="localNodeData.label"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Rule Name"
|
||||||
|
@blur="saveChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="nodeDescription" class="form-label">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="nodeDescription"
|
||||||
|
v-model="localNodeData.description"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Rule description"
|
||||||
|
rows="2"
|
||||||
|
@blur="saveChanges"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rule Groups (IF-THEN Rules) -->
|
||||||
|
<div class="my-6">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<h4 class="text-base font-medium">Business Rules</h4>
|
||||||
|
<button
|
||||||
|
@click="addRuleGroup"
|
||||||
|
class="btn-sm btn-primary flex items-center"
|
||||||
|
>
|
||||||
|
<i class="material-icons text-sm mr-1">add</i> Add Rule
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!localNodeData.ruleGroups || localNodeData.ruleGroups.length === 0"
|
||||||
|
class="py-3 text-center text-gray-500 border border-dashed rounded-md">
|
||||||
|
No business rules defined. Click "Add Rule" to create your first rule.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-6">
|
||||||
|
<!-- Each rule group is an if-then rule -->
|
||||||
|
<div
|
||||||
|
v-for="(ruleGroup, groupIndex) in localNodeData.ruleGroups"
|
||||||
|
:key="groupIndex"
|
||||||
|
class="rule-group border rounded-md overflow-hidden bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<!-- Rule header -->
|
||||||
|
<div class="rule-header bg-purple-50 px-4 py-2 flex justify-between items-center border-b">
|
||||||
|
<h5 class="font-medium">
|
||||||
|
<span class="text-purple-600">Rule {{ groupIndex + 1 }}:</span>
|
||||||
|
{{ ruleGroup.name || 'Unnamed Rule' }}
|
||||||
|
</h5>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
v-model="ruleGroup.name"
|
||||||
|
type="text"
|
||||||
|
class="form-control h-8 text-sm"
|
||||||
|
placeholder="Rule name"
|
||||||
|
@blur="saveChanges"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeRuleGroup(groupIndex)"
|
||||||
|
class="text-red-500 hover:text-red-700"
|
||||||
|
title="Remove rule"
|
||||||
|
>
|
||||||
|
<i class="material-icons">delete</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IF section -->
|
||||||
|
<div class="if-section p-4 bg-gray-50 border-b">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h6 class="font-medium text-gray-700">IF</h6>
|
||||||
|
<button
|
||||||
|
@click="addCondition(groupIndex)"
|
||||||
|
class="btn-xs btn-secondary flex items-center"
|
||||||
|
>
|
||||||
|
<i class="material-icons text-xs mr-1">add</i> Add Condition
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!ruleGroup.conditions || ruleGroup.conditions.length === 0"
|
||||||
|
class="py-2 text-center text-gray-500 text-sm border border-dashed rounded-md">
|
||||||
|
No conditions defined. Add a condition to specify when this rule applies.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead class="bg-white">
|
||||||
|
<tr>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Variable</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Operator</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value</th>
|
||||||
|
<th class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white">
|
||||||
|
<tr v-for="(condition, condIndex) in ruleGroup.conditions" :key="condIndex" class="border-t">
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<select
|
||||||
|
v-model="condition.variable"
|
||||||
|
class="form-control"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select variable</option>
|
||||||
|
<option
|
||||||
|
v-for="variable in availableVariables"
|
||||||
|
:key="variable.name"
|
||||||
|
:value="variable.name"
|
||||||
|
>
|
||||||
|
{{ variable.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<select
|
||||||
|
v-model="condition.operator"
|
||||||
|
class="form-control"
|
||||||
|
@change="updateConditionOperator(groupIndex, condIndex)"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select operator</option>
|
||||||
|
<option
|
||||||
|
v-for="op in getOperatorsForType(
|
||||||
|
availableVariables.find(v => v.name === condition.variable)?.type
|
||||||
|
)"
|
||||||
|
:key="op.value"
|
||||||
|
:value="op.value"
|
||||||
|
>
|
||||||
|
{{ op.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<template v-if="getInputType(
|
||||||
|
availableVariables.find(v => v.name === condition.variable)?.type,
|
||||||
|
condition.operator
|
||||||
|
) !== 'none'">
|
||||||
|
<input
|
||||||
|
v-model="condition.value"
|
||||||
|
:type="getInputType(
|
||||||
|
availableVariables.find(v => v.name === condition.variable)?.type,
|
||||||
|
condition.operator
|
||||||
|
)"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="condition.operator?.includes('n_days') ? 'Number of days' : 'Value'"
|
||||||
|
@blur="saveChanges"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-gray-400 text-sm italic">N/A</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 text-center">
|
||||||
|
<button
|
||||||
|
@click="removeCondition(groupIndex, condIndex)"
|
||||||
|
class="text-red-500 hover:text-red-700"
|
||||||
|
title="Remove condition"
|
||||||
|
>
|
||||||
|
<i class="material-icons text-sm">delete</i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-500 mt-2">
|
||||||
|
<span class="font-medium">Condition Type:</span>
|
||||||
|
<label class="inline-flex items-center ml-2">
|
||||||
|
<input type="radio" v-model="ruleGroup.conditionType" value="all" class="form-radio" @change="saveChanges" />
|
||||||
|
<span class="ml-1">Match All (AND)</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center ml-2">
|
||||||
|
<input type="radio" v-model="ruleGroup.conditionType" value="any" class="form-radio" @change="saveChanges" />
|
||||||
|
<span class="ml-1">Match Any (OR)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- THEN section -->
|
||||||
|
<div class="then-section p-4">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h6 class="font-medium text-gray-700">THEN</h6>
|
||||||
|
<button
|
||||||
|
@click="addAction(groupIndex)"
|
||||||
|
class="btn-xs btn-secondary flex items-center"
|
||||||
|
>
|
||||||
|
<i class="material-icons text-xs mr-1">add</i> Add Action
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!ruleGroup.actions || ruleGroup.actions.length === 0"
|
||||||
|
class="py-2 text-center text-gray-500 text-sm border border-dashed rounded-md">
|
||||||
|
No actions defined. Add an action to specify what happens when conditions are met.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead class="bg-white">
|
||||||
|
<tr>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action Type</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Target</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Configuration</th>
|
||||||
|
<th class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white">
|
||||||
|
<tr v-for="(action, actionIndex) in ruleGroup.actions" :key="actionIndex" class="border-t">
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<select
|
||||||
|
v-model="action.type"
|
||||||
|
class="form-control"
|
||||||
|
@change="updateActionType(groupIndex, actionIndex)"
|
||||||
|
>
|
||||||
|
<option value="set_variable">Set Variable</option>
|
||||||
|
<option value="calculate">Calculate Value</option>
|
||||||
|
<option value="increment">Increment Variable</option>
|
||||||
|
<option value="decrement">Decrement Variable</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<select
|
||||||
|
v-model="action.variable"
|
||||||
|
class="form-control"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Target variable</option>
|
||||||
|
<option
|
||||||
|
v-for="variable in availableVariables"
|
||||||
|
:key="variable.name"
|
||||||
|
:value="variable.name"
|
||||||
|
>
|
||||||
|
{{ variable.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<div v-if="action.type === 'set_variable'">
|
||||||
|
<input
|
||||||
|
v-model="action.value"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Value"
|
||||||
|
@blur="saveChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="action.type === 'calculate'" class="flex items-center space-x-2">
|
||||||
|
<select
|
||||||
|
v-model="action.operator"
|
||||||
|
class="form-control w-24"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="add">+</option>
|
||||||
|
<option value="subtract">-</option>
|
||||||
|
<option value="multiply">×</option>
|
||||||
|
<option value="divide">÷</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model="action.value"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Value"
|
||||||
|
@blur="saveChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span class="text-gray-400 text-sm italic">
|
||||||
|
{{ action.type === 'increment' ? 'Will increase by 1' : 'Will decrease by 1' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 text-center">
|
||||||
|
<button
|
||||||
|
@click="removeAction(groupIndex, actionIndex)"
|
||||||
|
class="text-red-500 hover:text-red-700"
|
||||||
|
title="Remove action"
|
||||||
|
>
|
||||||
|
<i class="material-icons text-sm">delete</i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Settings -->
|
||||||
|
<!-- <div class="border-t border-gray-200 my-4 pt-4">
|
||||||
|
<h4 class="text-base font-medium mb-2"></h4>
|
||||||
|
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label class="form-label">Rule Priority</label>
|
||||||
|
<select
|
||||||
|
v-model="localNodeData.priority"
|
||||||
|
class="form-control"
|
||||||
|
@change="saveChanges"
|
||||||
|
>
|
||||||
|
<option value="high">High</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-gray-500 text-xs mt-1 block">
|
||||||
|
Defines the execution priority of this rule when multiple rules are present.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
|
import { useVariableStore } from '~/stores/variableStore';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
nodeId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nodeData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
availableVariables: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'close']);
|
||||||
|
|
||||||
|
// Get the variable store for variables
|
||||||
|
const variableStore = useVariableStore();
|
||||||
|
|
||||||
|
// Create a local copy of the node data for editing
|
||||||
|
const localNodeData = ref({
|
||||||
|
label: '',
|
||||||
|
description: '',
|
||||||
|
ruleGroups: [],
|
||||||
|
priority: 'medium',
|
||||||
|
...props.nodeData
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get available variables for conditions and actions
|
||||||
|
const availableVariables = computed(() => {
|
||||||
|
const processVars = variableStore.getAllVariables.process.map(v => ({
|
||||||
|
name: v.name || 'unnamed',
|
||||||
|
label: v?.description
|
||||||
|
? `${v.description} (${v.name || 'unnamed'}, process)`
|
||||||
|
: `${v.name || 'unnamed'} (process)`,
|
||||||
|
type: v.type || 'string',
|
||||||
|
scope: 'process'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const globalVars = variableStore.getAllVariables.global.map(v => ({
|
||||||
|
name: v.name || 'unnamed',
|
||||||
|
label: v?.description
|
||||||
|
? `${v.description} (${v.name || 'unnamed'}, global)`
|
||||||
|
: `${v.name || 'unnamed'} (global)`,
|
||||||
|
type: v.type || 'string',
|
||||||
|
scope: 'global'
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...processVars, ...globalVars];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize with default values if needed
|
||||||
|
onMounted(() => {
|
||||||
|
// If we have old-style conditions/actions, migrate them to the new format
|
||||||
|
if (Array.isArray(localNodeData.value.conditions) && localNodeData.value.conditions.length > 0) {
|
||||||
|
migrateOldFormat();
|
||||||
|
} else if (!localNodeData.value.ruleGroups) {
|
||||||
|
localNodeData.value.ruleGroups = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localNodeData.value.priority) {
|
||||||
|
localNodeData.value.priority = 'medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrate old format to new format
|
||||||
|
const migrateOldFormat = () => {
|
||||||
|
// Convert old format (separate conditions and actions) to new format (rule groups)
|
||||||
|
const defaultRuleGroup = {
|
||||||
|
name: 'Rule 1',
|
||||||
|
conditions: localNodeData.value.conditions || [],
|
||||||
|
actions: localNodeData.value.actions || [],
|
||||||
|
conditionType: 'all' // Default to "AND" logic
|
||||||
|
};
|
||||||
|
|
||||||
|
localNodeData.value.ruleGroups = [defaultRuleGroup];
|
||||||
|
|
||||||
|
// Remove old properties
|
||||||
|
delete localNodeData.value.conditions;
|
||||||
|
delete localNodeData.value.actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for changes from parent
|
||||||
|
watch(() => props.nodeData, (newData) => {
|
||||||
|
if (newData) {
|
||||||
|
// Initialize with the passed data
|
||||||
|
localNodeData.value = {
|
||||||
|
label: '',
|
||||||
|
description: '',
|
||||||
|
ruleGroups: [],
|
||||||
|
priority: 'medium',
|
||||||
|
...newData
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if we need to migrate
|
||||||
|
if (Array.isArray(localNodeData.value.conditions) && !Array.isArray(localNodeData.value.ruleGroups)) {
|
||||||
|
migrateOldFormat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// Save changes to the node
|
||||||
|
const saveChanges = () => {
|
||||||
|
emit('update', localNodeData.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rule group operations
|
||||||
|
const addRuleGroup = () => {
|
||||||
|
localNodeData.value.ruleGroups.push({
|
||||||
|
name: `Rule ${localNodeData.value.ruleGroups.length + 1}`,
|
||||||
|
conditions: [],
|
||||||
|
actions: [],
|
||||||
|
conditionType: 'all'
|
||||||
|
});
|
||||||
|
saveChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRuleGroup = (groupIndex) => {
|
||||||
|
localNodeData.value.ruleGroups.splice(groupIndex, 1);
|
||||||
|
saveChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Condition operations
|
||||||
|
const addCondition = (groupIndex) => {
|
||||||
|
localNodeData.value.ruleGroups[groupIndex].conditions.push({
|
||||||
|
variable: '',
|
||||||
|
operator: 'eq',
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
saveChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCondition = (groupIndex, conditionIndex) => {
|
||||||
|
localNodeData.value.ruleGroups[groupIndex].conditions.splice(conditionIndex, 1);
|
||||||
|
saveChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action operations
|
||||||
|
const addAction = (groupIndex) => {
|
||||||
|
localNodeData.value.ruleGroups[groupIndex].actions.push({
|
||||||
|
type: 'set_variable',
|
||||||
|
variable: '',
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
saveChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAction = (groupIndex, actionIndex) => {
|
||||||
|
localNodeData.value.ruleGroups[groupIndex].actions.splice(actionIndex, 1);
|
||||||
|
saveChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update action properties based on type
|
||||||
|
const updateActionType = (groupIndex, actionIndex) => {
|
||||||
|
const action = localNodeData.value.ruleGroups[groupIndex].actions[actionIndex];
|
||||||
|
|
||||||
|
// Reset properties for the action type
|
||||||
|
if (action.type === 'set_variable') {
|
||||||
|
action.variable = action.variable || '';
|
||||||
|
action.value = action.value || '';
|
||||||
|
delete action.operator;
|
||||||
|
} else if (action.type === 'calculate') {
|
||||||
|
action.variable = action.variable || '';
|
||||||
|
action.operator = action.operator || 'add';
|
||||||
|
action.value = action.value || '';
|
||||||
|
} else if (['increment', 'decrement'].includes(action.type)) {
|
||||||
|
action.variable = action.variable || '';
|
||||||
|
delete action.value;
|
||||||
|
delete action.operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get operators based on variable type
|
||||||
|
const getOperatorsForType = (type) => {
|
||||||
|
switch (type?.toLowerCase()) {
|
||||||
|
case 'number':
|
||||||
|
case 'int':
|
||||||
|
case 'decimal':
|
||||||
|
return [
|
||||||
|
{ value: 'eq', label: '= (Equal to)' },
|
||||||
|
{ value: 'neq', label: '≠ (Not equal to)' },
|
||||||
|
{ value: 'gt', label: '> (Greater than)' },
|
||||||
|
{ value: 'gte', label: '≥ (Greater than or equal)' },
|
||||||
|
{ value: 'lt', label: '< (Less than)' },
|
||||||
|
{ value: 'lte', label: '≤ (Less than or equal)' }
|
||||||
|
];
|
||||||
|
case 'string':
|
||||||
|
return [
|
||||||
|
{ value: 'eq', label: '= (Equal to)' },
|
||||||
|
{ value: 'neq', label: '≠ (Not equal to)' },
|
||||||
|
{ value: 'contains', label: 'Contains' },
|
||||||
|
{ value: 'not_contains', label: 'Does not contain' },
|
||||||
|
{ value: 'starts_with', label: 'Starts with' },
|
||||||
|
{ value: 'ends_with', label: 'Ends with' },
|
||||||
|
{ value: 'is_empty', label: 'Is empty' },
|
||||||
|
{ value: 'is_not_empty', label: 'Is not empty' }
|
||||||
|
];
|
||||||
|
case 'datetime':
|
||||||
|
case 'date':
|
||||||
|
return [
|
||||||
|
{ value: 'eq', label: '= (Equal to)' },
|
||||||
|
{ value: 'neq', label: '≠ (Not equal to)' },
|
||||||
|
{ value: 'gt', label: '> (After)' },
|
||||||
|
{ value: 'gte', label: '≥ (On or after)' },
|
||||||
|
{ value: 'lt', label: '< (Before)' },
|
||||||
|
{ value: 'lte', label: '≤ (On or before)' },
|
||||||
|
{ value: 'is_today', label: 'Is today' },
|
||||||
|
{ value: 'is_future', label: 'Is in the future' },
|
||||||
|
{ value: 'is_past', label: 'Is in the past' },
|
||||||
|
{ value: 'last_n_days', label: 'In the last N days' },
|
||||||
|
{ value: 'next_n_days', label: 'In the next N days' }
|
||||||
|
];
|
||||||
|
case 'boolean':
|
||||||
|
return [
|
||||||
|
{ value: 'is_true', label: 'Is true' },
|
||||||
|
{ value: 'is_false', label: 'Is false' }
|
||||||
|
];
|
||||||
|
case 'object':
|
||||||
|
return [
|
||||||
|
{ value: 'eq', label: '= (Equal to)' },
|
||||||
|
{ value: 'neq', label: '≠ (Not equal to)' },
|
||||||
|
{ value: 'contains_key', label: 'Contains key' },
|
||||||
|
{ value: 'not_contains_key', label: 'Does not contain key' },
|
||||||
|
{ value: 'is_empty', label: 'Is empty' },
|
||||||
|
{ value: 'is_not_empty', label: 'Is not empty' }
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
{ value: 'eq', label: '= (Equal to)' },
|
||||||
|
{ value: 'neq', label: '≠ (Not equal to)' },
|
||||||
|
{ value: 'is_empty', label: 'Is empty' },
|
||||||
|
{ value: 'is_not_empty', label: 'Is not empty' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get input type based on variable type and operator
|
||||||
|
const getInputType = (varType, operator) => {
|
||||||
|
// Special operators that don't need value input
|
||||||
|
const noValueOperators = [
|
||||||
|
'is_empty', 'is_not_empty', 'is_true', 'is_false',
|
||||||
|
'is_today', 'is_future', 'is_past'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (noValueOperators.includes(operator)) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (varType?.toLowerCase()) {
|
||||||
|
case 'number':
|
||||||
|
case 'int':
|
||||||
|
case 'decimal':
|
||||||
|
return 'number';
|
||||||
|
case 'datetime':
|
||||||
|
return 'datetime-local';
|
||||||
|
case 'date':
|
||||||
|
return 'date';
|
||||||
|
case 'boolean':
|
||||||
|
return 'checkbox';
|
||||||
|
case 'object':
|
||||||
|
return 'text'; // For JSON input
|
||||||
|
default:
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format value based on type for display/comparison
|
||||||
|
const formatValue = (value, type, operator) => {
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
|
||||||
|
switch (type?.toLowerCase()) {
|
||||||
|
case 'datetime':
|
||||||
|
case 'date':
|
||||||
|
try {
|
||||||
|
const dt = DateTime.fromISO(value);
|
||||||
|
return type === 'datetime' ? dt.toISO() : dt.toISODate();
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
case 'number':
|
||||||
|
case 'int':
|
||||||
|
case 'decimal':
|
||||||
|
return Number(value);
|
||||||
|
case 'boolean':
|
||||||
|
return Boolean(value);
|
||||||
|
case 'object':
|
||||||
|
try {
|
||||||
|
return typeof value === 'string' ? value : JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse value from input based on type
|
||||||
|
const parseValue = (value, type) => {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
|
||||||
|
switch (type?.toLowerCase()) {
|
||||||
|
case 'datetime':
|
||||||
|
case 'date':
|
||||||
|
try {
|
||||||
|
const dt = DateTime.fromISO(value);
|
||||||
|
return type === 'datetime' ? dt.toISO() : dt.toISODate();
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
case 'number':
|
||||||
|
case 'int':
|
||||||
|
return parseInt(value);
|
||||||
|
case 'decimal':
|
||||||
|
return parseFloat(value);
|
||||||
|
case 'boolean':
|
||||||
|
return Boolean(value);
|
||||||
|
case 'object':
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add new methods for handling condition updates
|
||||||
|
const updateConditionOperator = (groupIndex, condIndex) => {
|
||||||
|
const condition = localNodeData.value.ruleGroups[groupIndex].conditions[condIndex];
|
||||||
|
const varType = availableVariables.value.find(v => v.name === condition.variable)?.type;
|
||||||
|
|
||||||
|
// Reset value if operator doesn't need it
|
||||||
|
if (getInputType(varType, condition.operator) === 'none') {
|
||||||
|
condition.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChanges();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-label {
|
||||||
|
@apply block text-sm font-medium text-gray-700 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
@apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
@apply h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-radio {
|
||||||
|
@apply h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
@apply px-2 py-1 text-xs font-medium rounded-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-xs {
|
||||||
|
@apply px-1.5 py-0.5 text-xs font-medium rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply px-4 py-2 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply px-4 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
@apply border rounded-md shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
@apply font-medium text-xs text-gray-600 bg-gray-50 py-2 px-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
@apply hover:bg-gray-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
@apply border-t border-gray-200 py-2 px-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-group {
|
||||||
|
@apply transition-all duration-200 relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-group:hover {
|
||||||
|
@apply shadow-md;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<RsModal
|
||||||
|
v-model="showModal"
|
||||||
|
title="Business Rule Configuration"
|
||||||
|
size="lg"
|
||||||
|
position="center"
|
||||||
|
:okCallback="saveAndClose"
|
||||||
|
okTitle="Save"
|
||||||
|
:cancelCallback="closeModal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<BusinessRuleNodeConfiguration
|
||||||
|
:nodeId="nodeId"
|
||||||
|
:nodeData="nodeData"
|
||||||
|
:availableVariables="availableVariables"
|
||||||
|
@update="handleUpdate"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</RsModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import BusinessRuleNodeConfiguration from './BusinessRuleNodeConfiguration.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
nodeId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nodeData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
availableVariables: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'update']);
|
||||||
|
|
||||||
|
const showModal = ref(props.modelValue);
|
||||||
|
const localNodeData = ref({ ...props.nodeData });
|
||||||
|
|
||||||
|
// Watch for changes to modelValue prop to sync modal visibility
|
||||||
|
watch(() => props.modelValue, (value) => {
|
||||||
|
showModal.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to showModal to emit update:modelValue
|
||||||
|
watch(() => showModal.value, (value) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to nodeData prop
|
||||||
|
watch(() => props.nodeData, (value) => {
|
||||||
|
localNodeData.value = { ...value };
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
function handleUpdate(updatedData) {
|
||||||
|
localNodeData.value = { ...updatedData };
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAndClose() {
|
||||||
|
emit('update', localNodeData.value);
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="form-node-configuration">
|
<div class="form-node-configuration">
|
||||||
<h3 class="text-lg font-semibold mb-4">Form Task Configuration</h3>
|
<!-- <h3 class="text-lg font-semibold mb-4">Form Task 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>
|
||||||
|
72
components/process-flow/FormNodeConfigurationModal.vue
Normal file
72
components/process-flow/FormNodeConfigurationModal.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<RsModal
|
||||||
|
v-model="showModal"
|
||||||
|
title="Form Task Configuration"
|
||||||
|
size="lg"
|
||||||
|
position="center"
|
||||||
|
:okCallback="saveAndClose"
|
||||||
|
okTitle="Save"
|
||||||
|
:cancelCallback="closeModal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<FormNodeConfiguration
|
||||||
|
:nodeData="nodeData"
|
||||||
|
:availableVariables="availableVariables"
|
||||||
|
@update="handleUpdate"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</RsModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import FormNodeConfiguration from './FormNodeConfiguration.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
nodeData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
availableVariables: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'update']);
|
||||||
|
|
||||||
|
const showModal = ref(props.modelValue);
|
||||||
|
const localNodeData = ref({ ...props.nodeData });
|
||||||
|
|
||||||
|
// Watch for changes to modelValue prop to sync modal visibility
|
||||||
|
watch(() => props.modelValue, (value) => {
|
||||||
|
showModal.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to showModal to emit update:modelValue
|
||||||
|
watch(() => showModal.value, (value) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to nodeData prop
|
||||||
|
watch(() => props.nodeData, (value) => {
|
||||||
|
localNodeData.value = { ...value };
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
function handleUpdate(updatedData) {
|
||||||
|
localNodeData.value = { ...updatedData };
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAndClose() {
|
||||||
|
emit('update', localNodeData.value);
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
77
components/process-flow/GatewayConditionManagerModal.vue
Normal file
77
components/process-flow/GatewayConditionManagerModal.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<RsModal
|
||||||
|
v-model="showModal"
|
||||||
|
title="Decision Point Configuration"
|
||||||
|
size="lg"
|
||||||
|
position="center"
|
||||||
|
:okCallback="saveAndClose"
|
||||||
|
okTitle="Save"
|
||||||
|
:cancelCallback="closeModal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-lg font-semibold">Configure Decision Paths</h3>
|
||||||
|
<p class="text-sm text-gray-600">Set up conditions to determine which path the process should follow.</p>
|
||||||
|
</div>
|
||||||
|
<GatewayConditionManager
|
||||||
|
:conditions="conditions"
|
||||||
|
:availableVariables="availableVariables"
|
||||||
|
@update:conditions="handleConditionsUpdate"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</RsModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import GatewayConditionManager from './GatewayConditionManager.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
conditions: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
availableVariables: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'update:conditions']);
|
||||||
|
|
||||||
|
const showModal = ref(props.modelValue);
|
||||||
|
const localConditions = ref([...props.conditions]);
|
||||||
|
|
||||||
|
// Watch for changes to modelValue prop to sync modal visibility
|
||||||
|
watch(() => props.modelValue, (value) => {
|
||||||
|
showModal.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to showModal to emit update:modelValue
|
||||||
|
watch(() => showModal.value, (value) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to conditions prop
|
||||||
|
watch(() => props.conditions, (value) => {
|
||||||
|
localConditions.value = [...value];
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
function handleConditionsUpdate(updatedConditions) {
|
||||||
|
localConditions.value = [...updatedConditions];
|
||||||
|
emit('update:conditions', updatedConditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAndClose() {
|
||||||
|
emit('update:conditions', localConditions.value);
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
@ -119,6 +119,22 @@ const availableComponents = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'business-rule',
|
||||||
|
name: 'Business Rule',
|
||||||
|
category: 'Core',
|
||||||
|
icon: 'material-symbols:rule',
|
||||||
|
description: 'Apply business rules to process data',
|
||||||
|
defaultProps: {
|
||||||
|
label: 'Business Rule',
|
||||||
|
data: {
|
||||||
|
description: 'Applies business rules to process data',
|
||||||
|
conditions: [],
|
||||||
|
actions: [],
|
||||||
|
priority: 'medium'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'gateway',
|
type: 'gateway',
|
||||||
name: 'Decision Point',
|
name: 'Decision Point',
|
||||||
|
@ -374,6 +374,84 @@ export const ApiCallNode = markRaw({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Business Rule node
|
||||||
|
export const BusinessRuleNode = markRaw({
|
||||||
|
props: ['id', 'type', 'label', 'selected', 'data'],
|
||||||
|
computed: {
|
||||||
|
nodeLabel() {
|
||||||
|
// Get label from either prop or data, with fallback
|
||||||
|
return this.label || (this.data && this.data.label) || 'Business Rule';
|
||||||
|
},
|
||||||
|
|
||||||
|
ruleConditionSummary() {
|
||||||
|
// First try to use the new ruleGroups structure
|
||||||
|
if (this.data && this.data.ruleGroups && Array.isArray(this.data.ruleGroups)) {
|
||||||
|
// Count total conditions across all rule groups
|
||||||
|
const totalConditions = this.data.ruleGroups.reduce((count, group) => {
|
||||||
|
return count + (Array.isArray(group.conditions) ? group.conditions.length : 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return totalConditions === 0 ? 'No conditions' :
|
||||||
|
totalConditions === 1 ? '1 condition' :
|
||||||
|
`${totalConditions} conditions`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to old structure for backward compatibility
|
||||||
|
if (this.data && this.data.conditions && Array.isArray(this.data.conditions)) {
|
||||||
|
const count = this.data.conditions.length;
|
||||||
|
return count === 1 ? '1 condition' : `${count} conditions`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'No conditions defined';
|
||||||
|
},
|
||||||
|
|
||||||
|
ruleActionSummary() {
|
||||||
|
// First try to use the new ruleGroups structure
|
||||||
|
if (this.data && this.data.ruleGroups && Array.isArray(this.data.ruleGroups)) {
|
||||||
|
// Count total actions across all rule groups
|
||||||
|
const totalActions = this.data.ruleGroups.reduce((count, group) => {
|
||||||
|
return count + (Array.isArray(group.actions) ? group.actions.length : 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return totalActions === 0 ? 'No actions' :
|
||||||
|
totalActions === 1 ? '1 action' :
|
||||||
|
`${totalActions} actions`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to old structure for backward compatibility
|
||||||
|
if (this.data && this.data.actions && Array.isArray(this.data.actions)) {
|
||||||
|
const count = this.data.actions.length;
|
||||||
|
return count === 1 ? '1 action' : `${count} actions`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'No actions defined';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h(CustomNode, {
|
||||||
|
id: this.id,
|
||||||
|
type: 'business-rule',
|
||||||
|
label: this.nodeLabel,
|
||||||
|
selected: this.selected,
|
||||||
|
data: this.data,
|
||||||
|
onClick: () => this.$emit('node-click', this.id)
|
||||||
|
}, {
|
||||||
|
icon: () => h('i', { class: 'material-icons text-purple-600' }, 'rule'),
|
||||||
|
default: () => h('div', { class: 'node-details' }, [
|
||||||
|
h('p', { class: 'node-description' }, this.data?.description || 'Applies business rules to process data'),
|
||||||
|
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||||
|
h('span', { class: 'node-rule-detail-label' }, 'Conditions:'),
|
||||||
|
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-purple-600' }, this.ruleConditionSummary)
|
||||||
|
]),
|
||||||
|
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
|
||||||
|
h('span', { class: 'node-rule-detail-label' }, 'Actions:'),
|
||||||
|
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-purple-600' }, this.ruleActionSummary)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Export the node types object to use with Vue Flow
|
// Export the node types object to use with Vue Flow
|
||||||
export const nodeTypes = markRaw({
|
export const nodeTypes = markRaw({
|
||||||
task: TaskNode,
|
task: TaskNode,
|
||||||
@ -382,6 +460,7 @@ export const nodeTypes = markRaw({
|
|||||||
gateway: GatewayNode,
|
gateway: GatewayNode,
|
||||||
form: FormNode,
|
form: FormNode,
|
||||||
script: ScriptNode,
|
script: ScriptNode,
|
||||||
|
'business-rule': BusinessRuleNode,
|
||||||
api: ApiCallNode
|
api: ApiCallNode
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,11 +7,16 @@ import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue';
|
|||||||
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
|
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
|
||||||
import FormSelector from '~/components/process-flow/FormSelector.vue';
|
import FormSelector from '~/components/process-flow/FormSelector.vue';
|
||||||
import GatewayConditionManager from '~/components/process-flow/GatewayConditionManager.vue';
|
import GatewayConditionManager from '~/components/process-flow/GatewayConditionManager.vue';
|
||||||
|
import GatewayConditionManagerModal from '~/components/process-flow/GatewayConditionManagerModal.vue';
|
||||||
import ApiNodeConfiguration from '~/components/process-flow/ApiNodeConfiguration.vue';
|
import ApiNodeConfiguration from '~/components/process-flow/ApiNodeConfiguration.vue';
|
||||||
|
import ApiNodeConfigurationModal from '~/components/process-flow/ApiNodeConfigurationModal.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 FormNodeConfiguration from '~/components/process-flow/FormNodeConfiguration.vue';
|
||||||
|
import FormNodeConfigurationModal from '~/components/process-flow/FormNodeConfigurationModal.vue';
|
||||||
import TaskNodeConfiguration from '~/components/process-flow/TaskNodeConfiguration.vue';
|
import TaskNodeConfiguration from '~/components/process-flow/TaskNodeConfiguration.vue';
|
||||||
|
import BusinessRuleNodeConfiguration from '~/components/process-flow/BusinessRuleNodeConfiguration.vue';
|
||||||
|
import BusinessRuleNodeConfigurationModal from '~/components/process-flow/BusinessRuleNodeConfigurationModal.vue';
|
||||||
|
|
||||||
// Define page meta
|
// Define page meta
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@ -54,6 +59,12 @@ const navigationConfirmed = ref(false);
|
|||||||
// Add a ref for the ProcessFlowCanvas component
|
// Add a ref for the ProcessFlowCanvas component
|
||||||
const processFlowCanvas = ref(null);
|
const processFlowCanvas = ref(null);
|
||||||
|
|
||||||
|
// Add state for node configuration modals
|
||||||
|
const showFormConfigModal = ref(false);
|
||||||
|
const showApiConfigModal = ref(false);
|
||||||
|
const showGatewayConfigModal = ref(false);
|
||||||
|
const showBusinessRuleConfigModal = ref(false);
|
||||||
|
|
||||||
// Component definitions
|
// Component definitions
|
||||||
const components = [
|
const components = [
|
||||||
{
|
{
|
||||||
@ -686,6 +697,17 @@ const handleTaskNodeUpdate = (updatedData) => {
|
|||||||
updateNodeInStore();
|
updateNodeInStore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update handler for business rule node
|
||||||
|
const handleBusinessRuleUpdate = (data) => {
|
||||||
|
if (selectedNodeData.value) {
|
||||||
|
selectedNodeData.value.data = {
|
||||||
|
...selectedNodeData.value.data,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
updateNodeInStore();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -795,29 +817,30 @@ const handleTaskNodeUpdate = (updatedData) => {
|
|||||||
|
|
||||||
<!-- Form Selection for Form Nodes -->
|
<!-- Form Selection for Form Nodes -->
|
||||||
<div v-if="selectedNodeData.type === 'form'">
|
<div v-if="selectedNodeData.type === 'form'">
|
||||||
<FormNodeConfiguration
|
<RsButton @click="showFormConfigModal = true" variant="primary" class="w-full">
|
||||||
:nodeData="selectedNodeData.data"
|
Configure Form Task
|
||||||
:availableVariables="variableStore.getAllVariables.global"
|
</RsButton>
|
||||||
@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
|
<RsButton @click="showApiConfigModal = true" variant="primary" class="w-full">
|
||||||
:nodeData="selectedNodeData.data"
|
Configure API Call
|
||||||
:availableVariables="gatewayAvailableVariables"
|
</RsButton>
|
||||||
@update="handleApiNodeUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gateway Conditions -->
|
<!-- Gateway Conditions -->
|
||||||
<div v-if="selectedNodeData.type === 'gateway'">
|
<div v-if="selectedNodeData.type === 'gateway'">
|
||||||
<GatewayConditionManager
|
<RsButton @click="showGatewayConfigModal = true" variant="primary" class="w-full">
|
||||||
:conditions="selectedNodeData.data.conditions"
|
Configure Decision Paths
|
||||||
@update:conditions="handleConditionUpdate"
|
</RsButton>
|
||||||
:availableVariables="gatewayAvailableVariables"
|
</div>
|
||||||
/>
|
|
||||||
|
<!-- Business Rule Configuration -->
|
||||||
|
<div v-if="selectedNodeData.type === 'business-rule'">
|
||||||
|
<RsButton @click="showBusinessRuleConfigModal = true" variant="primary" class="w-full">
|
||||||
|
Configure Business Rule
|
||||||
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -886,6 +909,43 @@ const handleTaskNodeUpdate = (updatedData) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</RsModal>
|
</RsModal>
|
||||||
|
|
||||||
|
<!-- Form Task Configuration Modal -->
|
||||||
|
<FormNodeConfigurationModal
|
||||||
|
v-if="selectedNodeData && selectedNodeData.type === 'form'"
|
||||||
|
v-model="showFormConfigModal"
|
||||||
|
:nodeData="selectedNodeData.data"
|
||||||
|
:availableVariables="variableStore.getAllVariables.global"
|
||||||
|
@update="handleFormNodeUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- API Call Configuration Modal -->
|
||||||
|
<ApiNodeConfigurationModal
|
||||||
|
v-if="selectedNodeData && selectedNodeData.type === 'api'"
|
||||||
|
v-model="showApiConfigModal"
|
||||||
|
:nodeData="selectedNodeData.data"
|
||||||
|
:availableVariables="gatewayAvailableVariables"
|
||||||
|
@update="handleApiNodeUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Gateway/Decision Point Configuration Modal -->
|
||||||
|
<GatewayConditionManagerModal
|
||||||
|
v-if="selectedNodeData && selectedNodeData.type === 'gateway'"
|
||||||
|
v-model="showGatewayConfigModal"
|
||||||
|
:conditions="selectedNodeData.data.conditions || []"
|
||||||
|
:availableVariables="gatewayAvailableVariables"
|
||||||
|
@update:conditions="handleConditionUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Business Rule Configuration Modal -->
|
||||||
|
<BusinessRuleNodeConfigurationModal
|
||||||
|
v-if="selectedNodeData && selectedNodeData.type === 'business-rule'"
|
||||||
|
v-model="showBusinessRuleConfigModal"
|
||||||
|
:nodeId="selectedNodeData.id"
|
||||||
|
:nodeData="selectedNodeData.data"
|
||||||
|
:availableVariables="gatewayAvailableVariables"
|
||||||
|
@update="handleBusinessRuleUpdate"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -942,6 +1002,13 @@ const handleTaskNodeUpdate = (updatedData) => {
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.node-business-rule) {
|
||||||
|
min-width: 160px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-left: 4px solid #9333ea; /* Purple border to match our icon color */
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.node-details) {
|
:deep(.node-details) {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user