You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
7.3 KiB
209 lines
7.3 KiB
|
7 months ago
|
import type { JsonLogicValue } from '@/jslg/lib';
|
||
|
7 months ago
|
import type {Obj, Opt, Sec} from '@/vcen/lib/types';
|
||
|
7 months ago
|
import type {
|
||
|
|
ScopeEntry,
|
||
|
|
ConditionNode,
|
||
|
|
ConditionError,
|
||
|
|
CompareOp,
|
||
|
|
LogicOp,
|
||
|
7 months ago
|
Operand, ResolvedOpt
|
||
|
7 months ago
|
} from '@/vcen-edit/lib/condition-editor.ts';
|
||
|
7 months ago
|
import {
|
||
|
|
buildScope,
|
||
|
|
compileCondition,
|
||
|
|
parseCondition,
|
||
|
|
validateCondition,
|
||
|
|
detectTautology,
|
||
|
|
makeLogicNode,
|
||
|
|
makeCompareNode,
|
||
|
|
replaceNode,
|
||
|
|
removeNode,
|
||
|
|
addChild,
|
||
|
|
getValidOperators,
|
||
|
7 months ago
|
} from '@/vcen-edit/lib/condition-editor.ts';
|
||
|
7 months ago
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// STORE
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Adaptador Svelte 5 para el editor de condiciones JsonLogic.
|
||
|
|
*
|
||
|
|
* Usa runas (`$state`, `$derived`) para que los componentes Svelte
|
||
|
|
* reaccionen automáticamente a los cambios del árbol de condición.
|
||
|
|
*
|
||
|
|
* ## Uso
|
||
|
|
*
|
||
|
|
* ```svelte
|
||
|
|
* <script>
|
||
|
|
* import { createConditionBuilderStore } from './condition-editor.svelte';
|
||
|
|
*
|
||
|
|
* const store = createConditionBuilderStore(obj, existingCondition);
|
||
|
|
* </script>
|
||
|
|
*
|
||
|
|
* {#if store.tautology.kind === 'always-true'}
|
||
|
|
* <Warning>Esta condición siempre es verdadera</Warning>
|
||
|
|
* {/if}
|
||
|
|
*
|
||
|
|
* <button onclick={() => store.addCompare(store.root.id)}>
|
||
|
|
* + Condición
|
||
|
|
* </button>
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export function createConditionBuilderStore(
|
||
|
|
container: Obj | Sec,
|
||
|
|
initial?: JsonLogicValue,
|
||
|
7 months ago
|
catOpts: Opt[] = [],
|
||
|
7 months ago
|
) {
|
||
|
|
// ── Estado reactivo ───────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
let root = $state<ConditionNode>(
|
||
|
|
initial
|
||
|
|
? parseCondition(initial)
|
||
|
|
: makeLogicNode('and', [])
|
||
|
|
);
|
||
|
|
|
||
|
7 months ago
|
const scope = $derived<ScopeEntry[]>(buildScope(container, catOpts));
|
||
|
7 months ago
|
|
||
|
|
const errors = $derived<ConditionError[]>(
|
||
|
|
validateCondition(root, scope)
|
||
|
|
);
|
||
|
|
|
||
|
|
const tautology = $derived(detectTautology(root));
|
||
|
|
|
||
|
|
const compiled = $derived<JsonLogicValue>(
|
||
|
|
errors.length === 0 ? compileCondition(root) : initial ?? true
|
||
|
|
);
|
||
|
|
|
||
|
|
const isValid = $derived(errors.length === 0);
|
||
|
|
|
||
|
|
// ── Mutaciones ────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
function setRoot(node: ConditionNode) {
|
||
|
|
root = node;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Añade un CompareNode vacío como hijo del LogicNode con `parentId`. */
|
||
|
|
function addCompare(parentId: string) {
|
||
|
|
const child = makeCompareNode('==');
|
||
|
|
root = addChild(root, parentId, child);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Añade un LogicNode (and/or) como hijo del LogicNode con `parentId`. */
|
||
|
|
function addLogic(parentId: string, op: LogicOp = 'and') {
|
||
|
|
const child = makeLogicNode(op, []);
|
||
|
|
root = addChild(root, parentId, child);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Elimina el nodo con `id`. Si el árbol queda vacío, resetea a and vacío. */
|
||
|
|
function remove(id: string) {
|
||
|
|
const next = removeNode(root, id);
|
||
|
|
root = next ?? makeLogicNode('and', []);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Reemplaza el operador de un CompareNode. */
|
||
|
|
function setOperator(id: string, op: CompareOp) {
|
||
|
|
const current = findNode(root, id);
|
||
|
|
if (!current || current.kind !== 'compare') return;
|
||
|
|
root = replaceNode(root, id, { ...current, op });
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Reemplaza el operando izquierdo de un CompareNode. */
|
||
|
|
function setLeft(id: string, operand: Operand) {
|
||
|
|
const current = findNode(root, id);
|
||
|
|
if (!current || current.kind !== 'compare') return;
|
||
|
|
root = replaceNode(root, id, { ...current, left: operand });
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Reemplaza el operando derecho de un CompareNode. */
|
||
|
|
function setRight(id: string, operand: Operand | [Operand, Operand]) {
|
||
|
|
const current = findNode(root, id);
|
||
|
|
if (!current || current.kind !== 'compare') return;
|
||
|
|
root = replaceNode(root, id, { ...current, right: operand });
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Cambia el operador lógico (and ↔ or) de un LogicNode. */
|
||
|
|
function setLogicOp(id: string, op: LogicOp) {
|
||
|
|
const current = findNode(root, id);
|
||
|
|
if (!current || current.kind !== 'logic') return;
|
||
|
|
root = replaceNode(root, id, { ...current, op });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Operadores válidos para el att referenciado por `varPath`.
|
||
|
|
* Útil para poblar el selector de operador en la UI.
|
||
|
|
*/
|
||
|
|
function validOperatorsFor(varPath: string): CompareOp[] {
|
||
|
|
const entry = scope.find(e => e.path === varPath);
|
||
|
|
if (!entry) return ['==', '!='];
|
||
|
|
return getValidOperators(entry.domain);
|
||
|
|
}
|
||
|
|
|
||
|
7 months ago
|
/**
|
||
|
|
* Opciones resueltas para el att referenciado por `varPath`.
|
||
|
|
* Solo tiene sentido para atts con dominio `option/list`.
|
||
|
|
* Devuelve `[]` para cualquier otro dominio.
|
||
|
|
*
|
||
|
|
* Útil para poblar el selector de valor literal en la UI
|
||
|
|
* cuando el operando derecho debe ser un OptID.
|
||
|
|
*/
|
||
|
|
function optsFor(varPath: string): ResolvedOpt[] {
|
||
|
|
const entry = scope.find(e => e.path === varPath);
|
||
|
|
return entry?.resolvedOpts ?? [];
|
||
|
|
}
|
||
|
|
|
||
|
7 months ago
|
/**
|
||
|
|
* Carga una condición existente en el editor (p.ej. al abrir una regla).
|
||
|
|
*/
|
||
|
|
function load(condition: JsonLogicValue) {
|
||
|
|
root = parseCondition(condition);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Resetea el editor a un LogicNode and vacío. */
|
||
|
|
function reset() {
|
||
|
|
root = makeLogicNode('and', []);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── API pública ───────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
return {
|
||
|
|
// Estado (reactivo)
|
||
|
|
get root() { return root; },
|
||
|
|
get scope() { return scope; },
|
||
|
|
get errors() { return errors; },
|
||
|
|
get tautology() { return tautology; },
|
||
|
|
get compiled() { return compiled; },
|
||
|
|
get isValid() { return isValid; },
|
||
|
|
|
||
|
|
// Mutaciones
|
||
|
|
setRoot,
|
||
|
|
addCompare,
|
||
|
|
addLogic,
|
||
|
|
remove,
|
||
|
|
setOperator,
|
||
|
|
setLeft,
|
||
|
|
setRight,
|
||
|
|
setLogicOp,
|
||
|
|
validOperatorsFor,
|
||
|
7 months ago
|
optsFor,
|
||
|
7 months ago
|
load,
|
||
|
|
reset,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export type ConditionBuilderStore = ReturnType<typeof createConditionBuilderStore>;
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// INTERNAL HELPERS
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
function findNode(root: ConditionNode, id: string): ConditionNode | null {
|
||
|
|
if (root.id === id) return root;
|
||
|
|
if (root.kind === 'logic') {
|
||
|
|
for (const child of root.children) {
|
||
|
|
const found = findNode(child, id);
|
||
|
|
if (found) return found;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
7 months ago
|
}
|