diff --git a/src/utils/condition-engine.ts b/src/utils/condition-engine.ts new file mode 100644 index 0000000..8d607ab --- /dev/null +++ b/src/utils/condition-engine.ts @@ -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, +): 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, +): 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, +): Map { + const visibility = new Map(); + + 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 = {}; + 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; +}