corrad-bp/components/process-flow/BusinessRuleNodeConfiguration.vue
Md Afiq Iskandar 75e5e6f97e Refactor Variable Selection in Process Flow Components
- Replaced select dropdowns with VariableBrowser component in ApiNodeConfiguration, BusinessRuleNodeConfiguration, FormNodeConfiguration, GatewayConditionManager, NotificationNodeConfiguration, and other relevant components for improved variable management.
- Enhanced user experience by allowing variable creation directly within the VariableBrowser, streamlining the process of selecting and managing variables.
- Updated related methods to ensure proper handling of variable selection and insertion across components, maintaining consistency in functionality.
- Improved code clarity and maintainability by consolidating variable selection logic into the new VariableBrowser component.
2025-07-07 17:53:20 +08:00

1479 lines
54 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="business-rule-node-configuration">
<!-- Step 1: Basic Configuration -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-purple-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-purple-600">1</span>
</div>
<h4 class="font-medium">Basic Configuration</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Rule Name -->
<div>
<label for="nodeLabel" class="block text-sm font-medium text-gray-700 mb-1">Rule Name</label>
<input
id="nodeLabel"
v-model="localNodeData.label"
type="text"
class="w-full p-2 border rounded-md shadow-sm focus:border-purple-500 focus:ring focus:ring-purple-200 text-sm"
placeholder="Enter a descriptive name"
@blur="saveChanges"
/>
<p class="mt-1 text-xs text-gray-500">
A clear name helps identify this rule in the process flow
</p>
</div>
<!-- Description -->
<div>
<label for="nodeDescription" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
id="nodeDescription"
v-model="localNodeData.description"
class="w-full p-2 border rounded-md shadow-sm focus:border-purple-500 focus:ring focus:ring-purple-200 text-sm"
placeholder="Describe what this rule does"
rows="2"
@blur="saveChanges"
></textarea>
<p class="mt-1 text-xs text-gray-500">
Optional description to explain this rule's purpose
</p>
</div>
</div>
</div>
<!-- Step 2: Business Rules Definition -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-purple-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-purple-600">2</span>
</div>
<h4 class="font-medium">Business Rules</h4>
</div>
<!-- Mode Toggle -->
<div class="flex justify-between items-center mb-4">
<div class="flex items-center space-x-4">
<p class="text-sm text-gray-600">
Define when conditions occur and what actions should be taken
</p>
<div class="flex items-center bg-white rounded-lg border border-gray-200 p-1">
<button
@click="editorMode = 'visual'"
:class="[
'px-3 py-1 text-xs font-medium rounded-md transition-colors',
editorMode === 'visual'
? 'bg-purple-100 text-purple-700'
: 'text-gray-500 hover:text-gray-700'
]"
>
<Icon name="material-symbols:dashboard" class="w-4 h-4 mr-1" />
Visual Builder
</button>
<button
@click="editorMode = 'pseudocode'"
:class="[
'px-3 py-1 text-xs font-medium rounded-md transition-colors',
editorMode === 'pseudocode'
? 'bg-purple-100 text-purple-700'
: 'text-gray-500 hover:text-gray-700'
]"
>
<Icon name="material-symbols:code" class="w-4 h-4 mr-1" />
Pseudocode
</button>
</div>
</div>
<RsButton
v-if="editorMode === 'visual'"
@click="addRuleGroup"
variant="primary"
size="sm"
class="btn-add-rule"
>
<Icon name="material-symbols:add" class="mr-1" /> Add Rule
</RsButton>
</div>
<!-- Pseudocode Editor -->
<div v-if="editorMode === 'pseudocode'" class="space-y-4">
<!-- Pseudocode Help -->
<div class="bg-blue-50 border border-blue-200 rounded-md p-3">
<div class="flex items-start">
<Icon name="material-symbols:info" class="w-4 h-4 text-blue-600 mt-0.5 mr-2 flex-shrink-0" />
<div class="text-sm">
<p class="font-medium text-blue-800 mb-1">Pseudocode Syntax Guide:</p>
<div class="text-blue-700 space-y-1">
<p><code class="bg-blue-100 px-1 rounded">IF variable operator value THEN action</code></p>
<p><code class="bg-blue-100 px-1 rounded">IF age &gt; 18 AND status = "active" THEN set eligibility = "approved"</code></p>
<p><code class="bg-blue-100 px-1 rounded">IF amount &lt; 1000 THEN set discount = 0.1</code></p>
</div>
</div>
</div>
</div>
<!-- Pseudocode Editor -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Write your business rules in pseudocode:
</label>
<div class="border border-gray-300 rounded-md overflow-hidden">
<RsCodeMirror
v-model="pseudocodeText"
language="javascript"
height="300px"
:theme="'oneDark'"
class="pseudocode-editor"
@update:modelValue="onPseudocodeChange"
/>
</div>
<div class="mt-2 text-xs text-gray-500">
<p><strong>Quick Start:</strong> Type your rules in natural language format</p>
<p class="mt-1">Example: <code class="bg-gray-100 px-1 rounded">IF age &gt; 18 THEN SET canVote = true</code></p>
</div>
</div>
<!-- Pseudocode Actions -->
<div class="flex justify-between items-center">
<div class="flex space-x-2">
<RsButton @click="generatePseudocode" variant="secondary" size="sm">
<Icon name="material-symbols:refresh" class="mr-1" />
Generate from Rules
</RsButton>
<RsButton @click="parsePseudocode" variant="primary" size="sm">
<Icon name="material-symbols:rule" class="mr-1" />
Convert to Rules
</RsButton>
</div>
<RsButton @click="showPseudocodeExamples = !showPseudocodeExamples" variant="tertiary" size="sm">
<Icon name="material-symbols:help" class="mr-1" />
Examples
</RsButton>
</div>
<!-- Pseudocode Examples Modal -->
<div v-if="showPseudocodeExamples" class="bg-gray-50 border border-gray-200 rounded-md p-4">
<h4 class="font-medium text-gray-800 mb-3">Pseudocode Examples</h4>
<div class="space-y-3 text-sm">
<div>
<p class="font-medium text-gray-700">Basic Condition:</p>
<pre class="bg-white p-2 rounded border text-xs font-mono">IF age &gt;= 18 THEN SET canVote = true</pre>
</div>
<div>
<p class="font-medium text-gray-700">Multiple Conditions (AND):</p>
<pre class="bg-white p-2 rounded border text-xs font-mono">IF income &gt; 50000 AND creditScore &gt;= 700 THEN SET loanApproved = true</pre>
</div>
<div>
<p class="font-medium text-gray-700">Multiple Conditions (OR):</p>
<pre class="bg-white p-2 rounded border text-xs font-mono">IF isPremium = true OR yearsActive &gt; 5 THEN SET discount = 0.15</pre>
</div>
<div>
<p class="font-medium text-gray-700">Multiple Actions:</p>
<pre class="bg-white p-2 rounded border text-xs font-mono">IF orderAmount &gt; 100 THEN
SET freeShipping = true
SET priority = "high"
SET notification = "Thank you for your large order!"
END</pre>
</div>
</div>
<div class="mt-3 pt-3 border-t border-gray-300">
<RsButton @click="showPseudocodeExamples = false" variant="tertiary" size="sm">
<Icon name="material-symbols:close" class="mr-1" />
Close Examples
</RsButton>
</div>
</div>
<!-- Parse Errors -->
<div v-if="pseudocodeErrors.length > 0" class="bg-red-50 border border-red-200 rounded-md p-3">
<div class="flex items-start">
<Icon name="material-symbols:error" class="w-4 h-4 text-red-600 mt-0.5 mr-2 flex-shrink-0" />
<div>
<p class="font-medium text-red-800 mb-1">Pseudocode Errors:</p>
<ul class="text-sm text-red-700 space-y-1">
<li v-for="(error, index) in pseudocodeErrors" :key="index">{{ error }}</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Visual Builder -->
<div v-if="editorMode === 'visual'">
<!-- No rules placeholder -->
<div v-if="!localNodeData.ruleGroups || localNodeData.ruleGroups.length === 0"
class="py-6 px-4 text-center border-2 border-dashed border-purple-200 rounded-md bg-purple-50">
<div class="text-purple-500 mb-2">
<Icon name="material-symbols:rule" class="w-12 h-12 mx-auto" />
</div>
<h3 class="text-lg font-medium text-gray-700 mb-2">No Business Rules Defined</h3>
<p class="text-sm text-gray-600 mb-4">
Add your first rule to define when conditions occur and what actions should be taken.
</p>
<RsButton
@click="addRuleGroup"
variant="primary"
size="md"
>
<Icon name="material-symbols:add" class="mr-1" /> Add First Rule
</RsButton>
</div>
<!-- Rules list -->
<div v-else class="space-y-6">
<!-- Each rule group is an if-then rule -->
<div
v-for="(ruleGroup, groupIndex) in localNodeData.ruleGroups"
:key="groupIndex"
class="rule-group border rounded-md overflow-hidden bg-white shadow-sm transition-all hover:shadow-md"
>
<!-- Rule header -->
<div class="rule-header bg-purple-50 px-4 py-3 flex justify-between items-center border-b">
<h5 class="font-medium flex items-center">
<Icon name="material-symbols:format-list-numbered" class="mr-1 text-purple-600" />
<span class="text-purple-600 mr-1">Rule {{ groupIndex + 1 }}:</span>
{{ ruleGroup.name || 'Unnamed Rule' }}
</h5>
<div class="flex items-center space-x-2">
<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 p-1 rounded hover:bg-red-50"
title="Remove rule"
>
<Icon name="material-symbols:delete-outline" />
</button>
</div>
</div>
<!-- IF section -->
<div class="if-section p-4 bg-gray-50 border-b">
<div class="flex justify-between items-center mb-3">
<h6 class="font-medium text-gray-700 flex items-center">
<span class="w-5 h-5 bg-purple-100 rounded-full text-xs flex items-center justify-center text-purple-800 mr-2">
IF
</span>
Conditions
</h6>
<RsButton
@click="addCondition(groupIndex)"
variant="secondary"
size="sm"
class="btn-sm-purple"
>
<Icon name="material-symbols:add" class="mr-1" /> Add Condition
</RsButton>
</div>
<!-- No conditions placeholder -->
<div v-if="!ruleGroup.conditions || ruleGroup.conditions.length === 0"
class="py-4 text-center text-gray-500 text-sm border border-dashed rounded-md bg-white">
<p class="mb-2">
No conditions defined. Add a condition to specify when this rule applies.
</p>
<RsButton
@click="addCondition(groupIndex)"
variant="secondary"
size="sm"
class="btn-sm-purple"
>
<Icon name="material-symbols:add" class="mr-1" /> Add First Condition
</RsButton>
</div>
<!-- Conditions table -->
<div v-else>
<div class="bg-white border rounded-md overflow-hidden">
<table class="w-full border-collapse">
<thead>
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Variable</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Operator</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value</th>
<th class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(condition, condIndex) in ruleGroup.conditions" :key="condIndex" class="border-t hover:bg-gray-50">
<td class="px-3 py-2">
<VariableBrowser
v-model="condition.variable"
:availableVariables="props.availableVariables"
:allowCreate="true"
@change="updateConditionVariable(groupIndex, condIndex)"
/>
</td>
<td class="px-3 py-2">
<select
v-model="condition.operator"
class="form-select"
@change="updateConditionOperator(groupIndex, condIndex)"
>
<option value="" disabled>Select operator</option>
<option
v-for="op in getOperatorsForType(
props.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(
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'none'">
<div class="flex items-center text-gray-400 text-sm italic">
<Icon name="material-symbols:info" class="w-4 h-4 mr-1" />
No value required
</div>
</template>
<!-- Range inputs for between operators -->
<template v-else-if="getInputType(
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'range'">
<div class="space-y-1">
<div class="grid grid-cols-2 gap-1">
<input
v-model="condition.minValue"
:type="props.availableVariables.find(v => v.name === condition.variable)?.type === 'date' ? 'date' :
props.availableVariables.find(v => v.name === condition.variable)?.type === 'datetime' ? 'datetime-local' : 'number'"
class="form-control text-xs"
placeholder="Min"
@blur="saveChanges"
/>
<input
v-model="condition.maxValue"
:type="props.availableVariables.find(v => v.name === condition.variable)?.type === 'date' ? 'date' :
props.availableVariables.find(v => v.name === condition.variable)?.type === 'datetime' ? 'datetime-local' : 'number'"
class="form-control text-xs"
placeholder="Max"
@blur="saveChanges"
/>
</div>
<p class="text-xs text-gray-500">Range values</p>
</div>
</template>
<!-- Weekday selector -->
<template v-else-if="getInputType(
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'weekday'">
<select v-model="condition.value" class="form-select" @change="saveChanges">
<option value="1">Monday</option>
<option value="2">Tuesday</option>
<option value="3">Wednesday</option>
<option value="4">Thursday</option>
<option value="5">Friday</option>
<option value="6">Saturday</option>
<option value="7">Sunday</option>
</select>
</template>
<!-- Month selector -->
<template v-else-if="getInputType(
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'month'">
<select v-model="condition.value" class="form-select" @change="saveChanges">
<option value="1">January</option>
<option value="2">February</option>
<option value="3">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
<option value="7">July</option>
<option value="8">August</option>
<option value="9">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
</template>
<!-- Regular input -->
<template v-else>
<input
v-model="condition.value"
:type="getInputType(
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
)"
class="form-control"
:placeholder="getValuePlaceholder(condition)"
:step="props.availableVariables.find(v => v.name === condition.variable)?.type === 'decimal' ? '0.01' : undefined"
@blur="saveChanges"
/>
</template>
</td>
<td class="px-3 py-2 text-center">
<button
@click="removeCondition(groupIndex, condIndex)"
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
title="Remove condition"
>
<Icon name="material-symbols:delete-outline" class="text-sm" />
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Condition type selection -->
<div class="mt-3 pt-3 border-t border-gray-300 flex flex-wrap items-center">
<span class="text-sm font-medium text-gray-700 mr-3">Match Type:</span>
<div class="space-x-4">
<label class="inline-flex items-center">
<input
type="radio"
v-model="ruleGroup.conditionType"
value="all"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2 text-sm">Match All Conditions (AND)</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="ruleGroup.conditionType"
value="any"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2 text-sm">Match Any Condition (OR)</span>
</label>
</div>
</div>
</div>
</div>
<!-- THEN section -->
<div class="then-section p-4">
<div class="flex justify-between items-center mb-3">
<h6 class="font-medium text-gray-700 flex items-center">
<span class="w-5 h-5 bg-purple-100 rounded-full text-xs flex items-center justify-center text-purple-800 mr-2">
THEN
</span>
Actions
</h6>
<RsButton
@click="addAction(groupIndex)"
variant="secondary"
size="sm"
class="btn-sm-purple"
>
<Icon name="material-symbols:add" class="mr-1" /> Add Action
</RsButton>
</div>
<!-- No actions placeholder -->
<div v-if="!ruleGroup.actions || ruleGroup.actions.length === 0"
class="py-4 text-center text-gray-500 text-sm border border-dashed rounded-md bg-white">
<p class="mb-2">
No actions defined. Add an action to specify what happens when conditions are met.
</p>
<RsButton
@click="addAction(groupIndex)"
variant="secondary"
size="sm"
class="btn-sm-purple"
>
<Icon name="material-symbols:add" class="mr-1" /> Add First Action
</RsButton>
</div>
<!-- Actions table -->
<div v-else>
<div class="bg-white border rounded-md overflow-hidden">
<table class="w-full border-collapse">
<thead>
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action Type</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Target</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Configuration</th>
<th class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(action, actionIndex) in ruleGroup.actions" :key="actionIndex" class="border-t hover:bg-gray-50">
<td class="px-3 py-2">
<select
v-model="action.type"
class="form-select"
@change="updateActionType(groupIndex, actionIndex)"
>
<option value="set_variable">Set Variable</option>
<option value="calculate">Calculate Value</option>
<option value="increment">Increment Variable</option>
<option value="decrement">Decrement Variable</option>
</select>
</td>
<td class="px-3 py-2">
<VariableBrowser
v-model="action.variable"
:availableVariables="props.availableVariables"
placeholder="Select target variable"
:allowCreate="true"
@update:modelValue="saveChanges"
/>
</td>
<td class="px-3 py-2">
<!-- Set Variable -->
<div v-if="action.type === 'set_variable'">
<input
v-model="action.value"
type="text"
class="form-control"
placeholder="Value"
@blur="saveChanges"
/>
</div>
<!-- Calculate -->
<div v-else-if="action.type === 'calculate'" class="flex items-center space-x-2">
<select
v-model="action.operator"
class="form-select w-24"
@change="saveChanges"
>
<option value="add">+</option>
<option value="subtract">-</option>
<option value="multiply">×</option>
<option value="divide">÷</option>
</select>
<input
v-model="action.value"
type="text"
class="form-control"
placeholder="Value"
@blur="saveChanges"
/>
</div>
<!-- Increment/Decrement -->
<div v-else>
<span class="text-gray-400 text-sm italic">
{{ action.type === 'increment' ? 'Will increase by 1' : 'Will decrease by 1' }}
</span>
</div>
</td>
<td class="px-3 py-2 text-center">
<button
@click="removeAction(groupIndex, actionIndex)"
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
title="Remove action"
>
<Icon name="material-symbols:delete-outline" class="text-sm" />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div> <!-- End Visual Builder -->
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { useProcessBuilderStore } from '~/stores/processBuilder';
import { DateTime } from 'luxon';
import { Icon } from '#components';
import RsCodeMirror from '~/components/RsCodeMirror.vue';
import VariableBrowser from './VariableBrowser.vue';
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 processStore = useProcessBuilderStore();
// Create a local copy of the node data for editing
const localNodeData = ref({
label: '',
description: '',
ruleGroups: [],
priority: 'medium',
...props.nodeData
});
// Use the availableVariables prop instead of creating a computed property
// The prop is already properly formatted from the parent component
// Pseudocode functionality
const editorMode = ref('visual');
const pseudocodeText = ref('');
const pseudocodeErrors = ref([]);
const showPseudocodeExamples = ref(false);
// 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';
}
// Initialize pseudocode text with examples
if (!pseudocodeText.value) {
generatePseudocode();
}
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 'int':
case 'decimal':
case 'number':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' },
{ value: 'gt', label: '> (Greater than)' },
{ value: 'gte', label: '≥ (Greater than or equal to)' },
{ value: 'lt', label: '< (Less than)' },
{ value: 'lte', label: '≤ (Less than or equal to)' },
{ value: 'between', label: 'Between (inclusive)' },
{ value: 'not_between', label: 'Not between' },
{ value: 'is_even', label: 'Is even number' },
{ value: 'is_odd', label: 'Is odd number' },
{ value: 'is_positive', label: 'Is positive' },
{ value: 'is_negative', label: 'Is negative' },
{ value: 'is_zero', label: 'Is zero' }
];
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' },
{ value: 'regex', label: 'Matches pattern (regex)' },
{ value: 'length_eq', label: 'Length equals' },
{ value: 'length_gt', label: 'Length greater than' },
{ value: 'length_lt', label: 'Length less than' }
];
case 'boolean':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'is_true', label: 'Is True' },
{ value: 'is_false', label: 'Is False' }
];
case 'date':
case 'datetime':
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: 'between', label: 'Between dates' },
{ value: 'is_today', label: 'Is today' },
{ value: 'is_yesterday', label: 'Is yesterday' },
{ value: 'is_tomorrow', label: 'Is tomorrow' },
{ value: 'is_this_week', label: 'Is this week' },
{ value: 'is_this_month', label: 'Is this month' },
{ value: 'is_this_year', label: 'Is this year' },
{ 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' },
{ value: 'weekday_eq', label: 'Day of week equals' },
{ value: 'month_eq', label: 'Month equals' }
];
case 'object':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' },
{ value: 'has_property', label: 'Has property' },
{ value: 'property_equals', label: 'Property equals' },
{ value: 'property_contains', label: 'Property contains' },
{ value: 'is_empty', label: 'Is empty/null' },
{ value: 'is_not_empty', label: 'Is not empty/null' },
{ value: 'property_count', label: 'Property count equals' }
];
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_yesterday', 'is_tomorrow', 'is_this_week', 'is_this_month', 'is_this_year',
'is_future', 'is_past', 'is_even', 'is_odd', 'is_positive', 'is_negative', 'is_zero'
];
if (noValueOperators.includes(operator)) {
return 'none';
}
// Special operators that need specific input types
if (operator === 'between' || operator === 'not_between') {
return 'range';
}
// Operators that need number input regardless of base type
if (['last_n_days', 'next_n_days', 'length_eq', 'length_gt', 'length_lt', 'property_count'].includes(operator)) {
return 'number';
}
// Day/month selection
if (operator === 'weekday_eq') {
return 'weekday';
}
if (operator === 'month_eq') {
return 'month';
}
switch (varType?.toLowerCase()) {
case 'int':
case 'decimal':
case 'number':
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;
}
};
// Get placeholder text based on variable type and operator
const getValuePlaceholder = (condition) => {
const varType = props.availableVariables.find(v => v.name === condition.variable)?.type?.toLowerCase();
const operator = condition.operator;
// Handle operators that don't need values
if (getInputType(varType, operator) === 'none') {
return 'No value needed';
}
// Handle special operators
if (operator === 'between' || operator === 'not_between') {
if (varType === 'int' || varType === 'decimal' || varType === 'number') {
return 'Enter range: min,max (e.g., 10,50)';
}
if (varType === 'date' || varType === 'datetime') {
return 'Enter date range: start,end';
}
}
if (operator === 'last_n_days' || operator === 'next_n_days') {
return 'Enter number of days (e.g., 7)';
}
if (operator === 'length_eq' || operator === 'length_gt' || operator === 'length_lt') {
return 'Enter length value (e.g., 5)';
}
if (operator === 'property_count') {
return 'Enter expected count (e.g., 3)';
}
if (operator === 'has_property' || operator === 'property_equals' || operator === 'property_contains') {
return 'Enter property path (e.g., user.email)';
}
if (operator === 'regex') {
return 'Enter regex pattern (e.g., ^[A-Z]+$)';
}
// Type-specific placeholders
switch (varType) {
case 'int':
return 'Enter a whole number (e.g., 42)';
case 'decimal':
case 'number':
return 'Enter a number (e.g., 3.14)';
case 'date':
return 'Select a date';
case 'datetime':
return 'Select date and time';
case 'string':
return operator === 'contains' || operator === 'not_contains'
? 'Enter text to search for'
: 'Enter text value';
case 'boolean':
return 'true or false';
case 'object':
return 'Enter JSON object or property path';
default:
return 'Enter value';
}
};
// Add new methods for handling condition updates
const updateConditionOperator = (groupIndex, condIndex) => {
const condition = localNodeData.value.ruleGroups[groupIndex].conditions[condIndex];
const varType = props.availableVariables.find(v => v.name === condition.variable)?.type;
// Reset values when operator changes
if (getInputType(varType, condition.operator) === 'none') {
condition.value = null;
condition.minValue = null;
condition.maxValue = null;
} else if (getInputType(varType, condition.operator) === 'range') {
condition.value = null;
condition.minValue = condition.minValue || '';
condition.maxValue = condition.maxValue || '';
} else {
condition.minValue = null;
condition.maxValue = null;
condition.value = condition.value || '';
}
saveChanges();
};
// Update condition variable
const updateConditionVariable = (groupIndex, condIndex) => {
const condition = localNodeData.value.ruleGroups[groupIndex].conditions[condIndex];
const selectedVar = props.availableVariables.find(v => v.name === condition.variable);
if (selectedVar) {
// Reset operator to a valid one for this type
const operators = getOperatorsForType(selectedVar.type);
condition.operator = operators.length > 0 ? operators[0].value : 'eq';
// Reset values
condition.value = '';
condition.minValue = '';
condition.maxValue = '';
}
saveChanges();
};
// Pseudocode functionality methods
const generatePseudocode = () => {
try {
let pseudocode = '';
if (!localNodeData.value.ruleGroups || localNodeData.value.ruleGroups.length === 0) {
pseudocodeText.value = `// No rules defined yet
// You can either:
// 1. Add rules in the visual builder first, then generate pseudocode
// 2. Write pseudocode directly here and convert to visual rules
// Example pseudocode:
IF age >= 18 AND status = "active" THEN
SET eligibility = "approved"
SET discountRate = 0.1
END
IF orderAmount > 1000 THEN
SET priority = "high"
SET freeShipping = true
END`;
return;
}
localNodeData.value.ruleGroups.forEach((ruleGroup, index) => {
if (index > 0) pseudocode += '\n\n';
// Rule comment
pseudocode += `// ${ruleGroup.name || `Rule ${index + 1}`}\n`;
// Build conditions
if (ruleGroup.conditions && ruleGroup.conditions.length > 0) {
pseudocode += 'IF ';
ruleGroup.conditions.forEach((condition, condIndex) => {
if (condIndex > 0) {
pseudocode += ruleGroup.conditionType === 'any' ? ' OR ' : ' AND ';
}
const variable = props.availableVariables.find(v => v.name === condition.variable);
const varName = variable ? variable.name : condition.variable;
const operator = condition.operator;
let value = condition.value;
// Convert operators to readable format
let readableOperator = operator;
switch (operator) {
case 'eq': readableOperator = '='; break;
case 'neq': readableOperator = '≠'; break;
case 'gt': readableOperator = '>'; break;
case 'gte': readableOperator = '≥'; break;
case 'lt': readableOperator = '<'; break;
case 'lte': readableOperator = '≤'; break;
case 'contains': readableOperator = 'contains'; break;
case 'starts_with': readableOperator = 'starts with'; break;
case 'ends_with': readableOperator = 'ends with'; break;
case 'is_empty': readableOperator = 'is empty'; value = ''; break;
case 'is_not_empty': readableOperator = 'is not empty'; value = ''; break;
case 'is_true': readableOperator = 'is'; value = 'true'; break;
case 'is_false': readableOperator = 'is'; value = 'false'; break;
}
if (value && variable?.type === 'string' && !['true', 'false'].includes(value.toString().toLowerCase())) {
value = `"${value}"`;
}
pseudocode += `${varName} ${readableOperator}`;
if (value !== '') {
pseudocode += ` ${value}`;
}
});
pseudocode += ' THEN\n';
// Build actions
if (ruleGroup.actions && ruleGroup.actions.length > 0) {
ruleGroup.actions.forEach(action => {
const targetVar = props.availableVariables.find(v => v.name === action.variable);
const varName = targetVar ? targetVar.name : action.variable;
switch (action.type) {
case 'set_variable':
let setValue = action.value;
if (setValue && targetVar?.type === 'string' && !['true', 'false'].includes(setValue.toString().toLowerCase())) {
setValue = `"${setValue}"`;
}
pseudocode += ` SET ${varName} = ${setValue}\n`;
break;
case 'calculate':
const op = action.operator === 'add' ? '+' : action.operator === 'subtract' ? '-' : action.operator === 'multiply' ? '*' : '/';
pseudocode += ` SET ${varName} = ${varName} ${op} ${action.value}\n`;
break;
case 'increment':
pseudocode += ` SET ${varName} = ${varName} + 1\n`;
break;
case 'decrement':
pseudocode += ` SET ${varName} = ${varName} - 1\n`;
break;
}
});
} else {
pseudocode += ' // No actions defined\n';
}
pseudocode += 'END';
} else {
pseudocode += '// No conditions defined';
}
});
pseudocodeText.value = pseudocode;
pseudocodeErrors.value = [];
} catch (error) {
pseudocodeErrors.value = [`Failed to generate pseudocode: ${error.message}`];
}
};
const parsePseudocode = () => {
try {
pseudocodeErrors.value = [];
if (!pseudocodeText.value.trim()) {
pseudocodeErrors.value = ['Pseudocode is empty'];
return;
}
const lines = pseudocodeText.value.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('//'));
const newRuleGroups = [];
let currentRule = null;
let inRule = false;
let ruleIndex = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.toUpperCase().startsWith('IF ')) {
// Start new rule
currentRule = {
name: `Rule ${++ruleIndex}`,
conditions: [],
actions: [],
conditionType: 'all'
};
inRule = true;
// Parse conditions
const conditionPart = line.substring(3).replace(/\s+THEN\s*$/i, '');
const conditions = parseConditions(conditionPart);
currentRule.conditions = conditions.conditions;
currentRule.conditionType = conditions.type;
if (line.toUpperCase().includes(' THEN')) {
// Single line IF-THEN, look for actions on next lines
continue;
}
} else if (line.toUpperCase().startsWith('SET ') && inRule && currentRule) {
// Parse action
const action = parseAction(line);
if (action) {
currentRule.actions.push(action);
}
} else if (line.toUpperCase() === 'END' && inRule && currentRule) {
// End of rule
newRuleGroups.push(currentRule);
currentRule = null;
inRule = false;
} else if (line.toUpperCase().includes(' THEN ')) {
// Single line IF-THEN-SET
const parts = line.split(/\s+THEN\s+/i);
if (parts.length === 2) {
const conditionPart = parts[0].replace(/^IF\s+/i, '');
const actionPart = parts[1];
currentRule = {
name: `Rule ${++ruleIndex}`,
conditions: [],
actions: [],
conditionType: 'all'
};
const conditions = parseConditions(conditionPart);
currentRule.conditions = conditions.conditions;
currentRule.conditionType = conditions.type;
const action = parseAction(actionPart);
if (action) {
currentRule.actions.push(action);
}
newRuleGroups.push(currentRule);
currentRule = null;
}
}
}
// Add any remaining rule
if (currentRule && inRule) {
newRuleGroups.push(currentRule);
}
if (newRuleGroups.length === 0) {
pseudocodeErrors.value = ['No valid rules found in pseudocode'];
return;
}
// Update the rules
localNodeData.value.ruleGroups = newRuleGroups;
saveChanges();
// Switch back to visual mode to see the results
editorMode.value = 'visual';
} catch (error) {
pseudocodeErrors.value = [`Failed to parse pseudocode: ${error.message}`];
}
};
const parseConditions = (conditionText) => {
const conditions = [];
let conditionType = 'all'; // Default to AND
// Check if it's OR logic
if (conditionText.toUpperCase().includes(' OR ')) {
conditionType = 'any';
const parts = conditionText.split(/\s+OR\s+/i);
parts.forEach(part => {
const condition = parseSingleCondition(part.trim());
if (condition) conditions.push(condition);
});
} else if (conditionText.toUpperCase().includes(' AND ')) {
const parts = conditionText.split(/\s+AND\s+/i);
parts.forEach(part => {
const condition = parseSingleCondition(part.trim());
if (condition) conditions.push(condition);
});
} else {
// Single condition
const condition = parseSingleCondition(conditionText.trim());
if (condition) conditions.push(condition);
}
return { conditions, type: conditionType };
};
const parseSingleCondition = (conditionText) => {
// Handle different operators
const operators = [
{ pattern: /\s+(>=|≥)\s+/, value: 'gte' },
{ pattern: /\s+(<=|≤)\s+/, value: 'lte' },
{ pattern: /\s+(!=|≠)\s+/, value: 'neq' },
{ pattern: /\s+(>)\s+/, value: 'gt' },
{ pattern: /\s+(<)\s+/, value: 'lt' },
{ pattern: /\s+(=)\s+/, value: 'eq' },
{ pattern: /\s+contains\s+/i, value: 'contains' },
{ pattern: /\s+starts\s+with\s+/i, value: 'starts_with' },
{ pattern: /\s+ends\s+with\s+/i, value: 'ends_with' },
{ pattern: /\s+is\s+empty/i, value: 'is_empty' },
{ pattern: /\s+is\s+not\s+empty/i, value: 'is_not_empty' },
{ pattern: /\s+is\s+true/i, value: 'is_true' },
{ pattern: /\s+is\s+false/i, value: 'is_false' },
{ pattern: /\s+is\s+/i, value: 'eq' }
];
for (const op of operators) {
const match = conditionText.match(op.pattern);
if (match) {
const variable = conditionText.substring(0, match.index).trim();
const value = conditionText.substring(match.index + match[0].length).trim();
// Clean up quotes
let cleanValue = value.replace(/^["']|["']$/g, '');
// Handle special cases
if (['is_empty', 'is_not_empty', 'is_true', 'is_false'].includes(op.value)) {
cleanValue = '';
}
return {
variable: variable,
operator: op.value,
value: cleanValue
};
}
}
return null;
};
const parseAction = (actionText) => {
if (actionText.toUpperCase().startsWith('SET ')) {
const actionPart = actionText.substring(4).trim();
// Handle different action types
if (actionPart.includes(' = ')) {
const [variable, expression] = actionPart.split(' = ').map(s => s.trim());
// Check if it's a calculation
if (expression.includes(variable)) {
// It's a calculation like "amount = amount + 10"
const calcMatch = expression.match(new RegExp(`${variable}\\s*([+\\-*/])\\s*(.+)`));
if (calcMatch) {
const operator = calcMatch[1] === '+' ? 'add' : calcMatch[1] === '-' ? 'subtract' : calcMatch[1] === '*' ? 'multiply' : 'divide';
return {
type: 'calculate',
variable: variable,
operator: operator,
value: calcMatch[2].trim()
};
}
// Check for increment/decrement
if (expression === `${variable} + 1`) {
return { type: 'increment', variable: variable };
}
if (expression === `${variable} - 1`) {
return { type: 'decrement', variable: variable };
}
}
// Regular assignment
let value = expression.replace(/^["']|["']$/g, ''); // Remove quotes
return {
type: 'set_variable',
variable: variable,
value: value
};
}
}
return null;
};
const onPseudocodeChange = () => {
// Clear errors when user starts typing
if (pseudocodeErrors.value.length > 0) {
pseudocodeErrors.value = [];
}
};
</script>
<style scoped>
.pseudocode-editor {
/* Custom styling for the pseudocode editor */
}
.pseudocode-editor :deep(.cm-editor) {
border: none;
}
.pseudocode-editor :deep(.cm-focused) {
outline: none;
}
.form-control {
@apply block w-full p-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm;
}
.form-select {
@apply block w-full p-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm;
}
.form-checkbox {
@apply h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded;
}
.form-radio {
@apply h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300;
}
.btn-add-rule {
@apply bg-purple-600 hover:bg-purple-700 focus:ring-purple-500;
}
.btn-sm-purple {
@apply bg-white hover:bg-gray-50 text-purple-700 border-purple-300 hover:border-purple-400 focus:ring-purple-500;
}
table {
@apply border-collapse;
}
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 border-purple-200;
}
.rule-group:hover {
@apply shadow-md border-purple-300;
}
</style>