import { ActionType, ConditionOperator, ConditionSetOperator, ConditionType, CMSType } from '@/zeus/index.js'; import { FieldFrontType } from '@/graphql/core/selectors.js'; import get from 'lodash-es/get'; import { TailwindPlayCommonParams } from '@/TailwindPlay/Fields/types.js'; export const NON_INTERACTIVE_ACTIONS = [ActionType.SET_DISABLED, ActionType.DISPLAY_VALUE]; type Rule = NonNullable['rules']>[number]; type Condition = Rule['conditions'][number]; type ValueType = string | number | boolean | null | undefined; function buildCandidatePaths(path: string, prefix: string): string[] { // Normalize prefix (strip trailing dot for segment math) const normPrefix = prefix.endsWith('.') ? prefix.slice(0, -1) : prefix; const segments = normPrefix.split('.').filter(Boolean); const lastSeg = segments[segments.length - 1]; const firstSeg = segments[0]; const candidates: string[] = []; // 1) If already absolute under current prefix, prefer as-is if (prefix && path.startsWith(prefix)) candidates.push(path); // 2) If path repeats the last segment of prefix (e.g., prefix: pricing.wrapper., path: wrapper.foo) // de-duplicate that segment when prefixing if (prefix && lastSeg && path.startsWith(`${lastSeg}.`)) { candidates.push(`${prefix}${path.slice(lastSeg.length + 1)}`); } // 3) When inside nested shapes (e.g., prefix: nowy.wrapper., path: wrapper.foo), // try prepending only the root shape (nowy.wrapper.foo) if (firstSeg && segments.length > 1) { candidates.push(`${firstSeg}.${path}`); } // 4) Naive prefixed variant (kept as a fallback) if (prefix) candidates.push(`${prefix}${path}`); // 5) Raw path as a final fallback (may be absolute from root or other scope) candidates.push(path); // Ensure uniqueness while preserving order return Array.from(new Set(candidates)); } export function evaluateConditionNode( field: FieldFrontType, condition: Condition, data: Record, modelData: Record, prefix: string = '', ): boolean { if (condition.type === ConditionType.SET && Array.isArray(condition.children)) { const results = condition.children.map((child) => evaluateConditionNode(field, child, data, modelData, prefix)); const out = condition.setOperator === ConditionSetOperator.OR ? results.some(Boolean) : results.every(Boolean); return out; } if (!condition.path) return false; const candidatePaths = buildCandidatePaths(condition.path, prefix); let actualValue: ValueType = undefined; for (const p of candidatePaths) { // Support both flattened keys ("a.b.c") and nested objects if (Object.prototype.hasOwnProperty.call(data, p)) { actualValue = (data as Record)[p]; } else { actualValue = get(data, p); } if (typeof actualValue !== 'undefined') { break; } } if (typeof actualValue === 'undefined') { for (const p of candidatePaths) { const modelValue = get(modelData, p) as { defaultValue?: ValueType } | undefined; if (modelValue && typeof modelValue === 'object' && 'defaultValue' in modelValue) { actualValue = modelValue.defaultValue; break; } } } // Also probe common nested locations for defaults (e.g., path.defaultValue, path.variable.defaultValue) if (typeof actualValue === 'undefined') { for (const p of candidatePaths) { const directDefault = get(modelData, `${p}.defaultValue`); if (typeof directDefault !== 'undefined') { actualValue = directDefault as ValueType; break; } const fromVariable = get(modelData, `${p}.variable.defaultValue`); if (typeof fromVariable !== 'undefined') { actualValue = fromVariable as ValueType; break; } } } // Normalize missing boolean-like values on initial render to behave as false // Handles both string and boolean rule values (e.g., 'false' or false) if (typeof actualValue === 'undefined') { const raw = condition.value as unknown as string | boolean | null | undefined; const expectedTmp = typeof raw === 'string' ? parseValue(raw) : raw; if (expectedTmp === true || expectedTmp === false || raw === 'true' || raw === 'false') { actualValue = false; } } if (actualValue === 'false' || actualValue === 'true') { actualValue = actualValue === 'true'; } if ( condition.operator !== ConditionOperator.REGEX && condition.operator !== ConditionOperator.NOT_REGEX && typeof actualValue === 'string' && !isNaN(Number(actualValue)) ) { actualValue = Number(actualValue); } const expectedValue = parseValue(condition.value); switch (condition.operator) { case ConditionOperator.REGEX: if (typeof actualValue !== 'string' || typeof expectedValue !== 'string') return false; try { const pattern = expectedValue.replace(/^\/|\/$/g, ''); const regex = new RegExp(pattern); return regex.test(actualValue); } catch (e) { console.error('Invalid regex pattern:', expectedValue, e); return false; } case ConditionOperator.NOT_REGEX: if (typeof actualValue !== 'string' || typeof expectedValue !== 'string') return false; try { const pattern = expectedValue.replace(/^\/|\/$/g, ''); const regex = new RegExp(pattern); return !regex.test(actualValue); } catch (e) { console.error('Invalid regex pattern:', expectedValue, e); return false; } case ConditionOperator.EQUAL: { const res = actualValue === expectedValue; return res; } case ConditionOperator.NOT_EQUAL: { const res = actualValue !== expectedValue; return res; } case ConditionOperator.GREATER: { const res = compareValues(actualValue, expectedValue, '>'); return res; } case ConditionOperator.GREATER_EQUAL: { const res = compareValues(actualValue, expectedValue, '>='); return res; } case ConditionOperator.LESS: { const res = compareValues(actualValue, expectedValue, '<'); return res; } case ConditionOperator.LESS_EQUAL: { const res = compareValues(actualValue, expectedValue, '<='); return res; } case ConditionOperator.EXISTS: { // treat boolean false as an existing value const exists = !(actualValue === undefined || actualValue === null || actualValue === ''); return exists; } case ConditionOperator.NOT_EXISTS: { const exists = !(actualValue === undefined || actualValue === null || actualValue === ''); const res = !exists; return res; } default: return false; } } function compareValues(a: ValueType, b: ValueType, op: '>' | '>=' | '<' | '<='): boolean { if (a === undefined || a === null || a === '') a = 0; if (b === undefined || b === null || b === '') b = 0; if (typeof a === 'string' && typeof b === 'string') { switch (op) { case '>': return a.localeCompare(b, undefined, { numeric: true }) > 0; case '>=': return a.localeCompare(b, undefined, { numeric: true }) >= 0; case '<': return a.localeCompare(b, undefined, { numeric: true }) < 0; case '<=': return a.localeCompare(b, undefined, { numeric: true }) <= 0; } } if (typeof a === 'number' && typeof b === 'number') { switch (op) { case '>': return a > b; case '>=': return a >= b; case '<': return a < b; case '<=': return a <= b; } } if (typeof a === 'string' && typeof b === 'number') { switch (op) { case '>': return a.length > b; case '>=': return a.length >= b; case '<': return a.length < b; case '<=': return a.length <= b; } } if (typeof a === 'number' && typeof b === 'string') { switch (op) { case '>': return a > b.length; case '>=': return a >= b.length; case '<': return a < b.length; case '<=': return a <= b.length; } } return false; } export function parseValue(value: string | undefined | null): ValueType { if (value?.startsWith('/') && value?.endsWith('/')) { try { const pattern = value.slice(1, -1); const regex = new RegExp(pattern); if (regex) return pattern; } catch (e) { console.error('Invalid regex pattern:', value, e); } } if (value === undefined || value === null) return null; if (value === 'true') return true; if (value === 'false') return false; if (!isNaN(Number(value))) return Number(value); try { return JSON.parse(value); } catch { return value; } } export function extractConditionPathsAndValues(fields: FieldFrontType[], type: CMSType) { const pairs: Array<{ path: string; value: ValueType }> = []; fields.forEach((f) => { if (f.type === type && f.visual?.rules) { f.visual.rules.forEach((rule) => { rule.conditions?.forEach((cond) => { if (cond.path) { pairs.push({ path: cond.path, value: cond.value }); } }); }); } }); return Array.from(new Map(pairs.map((p) => [p.path + ':' + p.value, p])).values()); } /** * Evaluates rules with SET_VALUE actions and returns the computed value. * Finds the first matching rule and extracts its SET_VALUE action value. * Returns undefined if no rules match. */ export function evaluateSetValueRules( field: FieldFrontType, actionType: ActionType, state: Record, modelData: Record, prefix: string = '', ) { const rules = (field.visual?.rules || []).filter( (rule) => rule.actions && rule.actions.some((a) => a.type === actionType), ); for (const rule of rules) { const allConditionsMatch = rule.conditions?.every((condition) => evaluateConditionNode(field, condition, state, modelData, prefix), ); if (allConditionsMatch) { const setValueAction = rule.actions?.find((a) => a.type === actionType); if (setValueAction && setValueAction.value !== undefined) { const val = setValueAction.value; // Parse string boolean values to actual booleans if (typeof val === 'string') { if (val === 'true') return true; if (val === 'false') return false; } return val; } } } return undefined; }