feat: add condition evaluation engine with operator logic and visibility computation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e334ad7aab
commit
c494a75242
1 changed files with 120 additions and 0 deletions
120
src/utils/condition-engine.ts
Normal file
120
src/utils/condition-engine.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { FieldConditions, ConditionRule, ConditionOperator, FieldType } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate whether a field should be visible based on its conditions
|
||||||
|
* and the current form values.
|
||||||
|
* Returns true if the field should be shown, false if hidden.
|
||||||
|
*/
|
||||||
|
export function evaluateConditions(
|
||||||
|
conditions: FieldConditions,
|
||||||
|
values: Record<string, unknown>,
|
||||||
|
): boolean {
|
||||||
|
if (conditions.rules.length === 0) return true;
|
||||||
|
|
||||||
|
const results = conditions.rules.map((rule) => evaluateRule(rule, values));
|
||||||
|
|
||||||
|
return conditions.logic === 'and'
|
||||||
|
? results.every(Boolean)
|
||||||
|
: results.some(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateRule(
|
||||||
|
rule: ConditionRule,
|
||||||
|
values: Record<string, unknown>,
|
||||||
|
): boolean {
|
||||||
|
const value = values[rule.fieldId];
|
||||||
|
|
||||||
|
switch (rule.operator) {
|
||||||
|
case 'is_empty':
|
||||||
|
return isEmpty(value);
|
||||||
|
case 'is_not_empty':
|
||||||
|
return !isEmpty(value);
|
||||||
|
case 'equals':
|
||||||
|
return String(value ?? '') === String(rule.value ?? '');
|
||||||
|
case 'not_equals':
|
||||||
|
return String(value ?? '') !== String(rule.value ?? '');
|
||||||
|
case 'contains':
|
||||||
|
return checkContains(value, rule.value);
|
||||||
|
case 'not_contains':
|
||||||
|
return !checkContains(value, rule.value);
|
||||||
|
case 'greater_than':
|
||||||
|
return toNum(value) > toNum(rule.value);
|
||||||
|
case 'less_than':
|
||||||
|
return toNum(value) < toNum(rule.value);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmpty(value: unknown): boolean {
|
||||||
|
if (value === null || value === undefined || value === '') return true;
|
||||||
|
if (Array.isArray(value) && value.length === 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkContains(value: unknown, search: unknown): boolean {
|
||||||
|
const searchStr = String(search ?? '');
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.some((item) => String(item) === searchStr);
|
||||||
|
}
|
||||||
|
return String(value ?? '').includes(searchStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNum(value: unknown): number {
|
||||||
|
return parseFloat(String(value ?? '')) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the set of operators that are valid for a given field type.
|
||||||
|
*/
|
||||||
|
export function getOperatorsForType(type: FieldType): ConditionOperator[] {
|
||||||
|
const base: ConditionOperator[] = ['equals', 'not_equals', 'is_empty', 'is_not_empty'];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'text':
|
||||||
|
case 'textarea':
|
||||||
|
return [...base, 'contains', 'not_contains'];
|
||||||
|
case 'tags':
|
||||||
|
return [...base, 'contains', 'not_contains'];
|
||||||
|
case 'number':
|
||||||
|
case 'rating':
|
||||||
|
case 'slider':
|
||||||
|
return [...base, 'greater_than', 'less_than'];
|
||||||
|
case 'toggle':
|
||||||
|
// Toggle always has a value (true/false), so is_empty/is_not_empty don't apply
|
||||||
|
return ['equals', 'not_equals'];
|
||||||
|
default:
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute visibility for all fields in a form.
|
||||||
|
* Fields can only depend on fields before them (prevents cycles).
|
||||||
|
* Returns a Map of fieldId -> isVisible.
|
||||||
|
*/
|
||||||
|
export function computeVisibility(
|
||||||
|
fields: { id: string; conditions?: FieldConditions }[],
|
||||||
|
values: Record<string, unknown>,
|
||||||
|
): Map<string, boolean> {
|
||||||
|
const visibility = new Map<string, boolean>();
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!field.conditions || field.conditions.rules.length === 0) {
|
||||||
|
visibility.set(field.id, true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only evaluate against fields that are themselves visible
|
||||||
|
const visibleValues: Record<string, unknown> = {};
|
||||||
|
for (const [id, val] of Object.entries(values)) {
|
||||||
|
if (visibility.get(id) !== false) {
|
||||||
|
visibleValues[id] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visibility.set(field.id, evaluateConditions(field.conditions, visibleValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue