- 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.
1479 lines
54 KiB
Vue
1479 lines
54 KiB
Vue
<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 > 18 AND status = "active" THEN set eligibility = "approved"</code></p>
|
||
<p><code class="bg-blue-100 px-1 rounded">IF amount < 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 > 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 >= 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 > 50000 AND creditScore >= 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 > 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 > 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> |