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:
Luca Oelfke 2026-02-13 20:35:17 +01:00
parent e334ad7aab
commit c494a75242

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