81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
/**
|
|
* Visibility engine for form sections and fields.
|
|
*
|
|
* Evaluates a VisibilityRule tree against the current form state.
|
|
* Form state is a plain object {fieldName: value}. Missing fields are
|
|
* treated as undefined.
|
|
*
|
|
* Rule semantics:
|
|
* - SimpleCondition: tests a single field with an operator
|
|
* - {all: [...]}: AND
|
|
* - {any: [...]}: OR
|
|
*
|
|
* Operator semantics:
|
|
* - equals : field value === rule.value
|
|
* - notEquals : field value !== rule.value
|
|
* - in : rule.values includes the field value
|
|
* - notIn : rule.values does not include the field value
|
|
* - isEmpty : field is undefined / null / "" / [] / {}
|
|
* - isNotEmpty : negation of isEmpty
|
|
*
|
|
* If a rule is malformed, evaluate returns false (field stays hidden) rather
|
|
* than throwing, so a config typo can never break the page.
|
|
*/
|
|
|
|
import type {
|
|
VisibilityRule,
|
|
SimpleCondition,
|
|
AllCondition,
|
|
AnyCondition,
|
|
} from "@/types/form";
|
|
|
|
type FormState = Record<string, unknown>;
|
|
|
|
function isSimple(r: VisibilityRule): r is SimpleCondition {
|
|
return typeof (r as SimpleCondition).field === "string";
|
|
}
|
|
|
|
function isAll(r: VisibilityRule): r is AllCondition {
|
|
return Array.isArray((r as AllCondition).all);
|
|
}
|
|
|
|
function isAny(r: VisibilityRule): r is AnyCondition {
|
|
return Array.isArray((r as AnyCondition).any);
|
|
}
|
|
|
|
function isEmpty(v: unknown): boolean {
|
|
if (v === undefined || v === null) return true;
|
|
if (typeof v === "string") return v.length === 0;
|
|
if (Array.isArray(v)) return v.length === 0;
|
|
if (typeof v === "object") return Object.keys(v as object).length === 0;
|
|
return false;
|
|
}
|
|
|
|
function evalSimple(c: SimpleCondition, state: FormState): boolean {
|
|
const v = state[c.field];
|
|
switch (c.op) {
|
|
case "equals":
|
|
return v === c.value;
|
|
case "notEquals":
|
|
return v !== c.value;
|
|
case "in":
|
|
return Array.isArray(c.values) && c.values.includes(v as string | number | boolean);
|
|
case "notIn":
|
|
return Array.isArray(c.values) && !c.values.includes(v as string | number | boolean);
|
|
case "isEmpty":
|
|
return isEmpty(v);
|
|
case "isNotEmpty":
|
|
return !isEmpty(v);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function evaluate(rule: VisibilityRule | undefined, state: FormState): boolean {
|
|
if (!rule) return true;
|
|
if (isSimple(rule)) return evalSimple(rule, state);
|
|
if (isAll(rule)) return rule.all.every((r) => evaluate(r, state));
|
|
if (isAny(rule)) return rule.any.some((r) => evaluate(r, state));
|
|
return false;
|
|
}
|