fix(vcen-edit): corrección de bugs críticos en adaptadores y demo

Bugs críticos:
- stores/catalog-editor.svelte.ts: añadidos activeOptId, activeOpt y
  setActiveOpt que OptsPanel.svelte y OptDetail.svelte requerían pero
  no existían en el store — el panel de opciones estaba completamente roto.
- session.svelte.ts: createSession recibía engine donde espera glob,
  causando crash en getPrice()/resolveView(). Corregido a createSession(cat, null, engine).
- vcen/session.ts: glob ahora es optional (GlobInstance|null = null) para
  permitir sesiones de solo-evaluación sin glob; añadidos null-guards en
  getPrice, getAllPrices, resolveView, resolveVisual, resolveDefaultView.

Otras correcciones:
- comps/catalog-editor.svelte.ts + stores/catalog-editor.svelte.ts:
  updateAttDefaultValue usaba (att.value as any).value = value (mutación
  directa con cast); corregido a att.value = { ...att.value, value }.
- OptDetail.svelte: eliminado `</>` suelto dentro de la cadena de clase CSS.
- demo.store.svelte.ts: dominios option usaban estructura obsoleta
  {kind:'option', options:[]} incompatible con vcen. Refactorizado para
  usar la estructura correcta {kind:'option', mode:'list', list:[{optId}]}
  con Opt entries en cat.opts. Actualizado addAtt, updateAttDomain y
  updateAttOptions.
- demos/RuntimePanel.svelte: iteraba (domain as any).options para opciones;
  ahora itera domain.list y resuelve el valor desde cat.opts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
dev 7 months ago
parent 25ae98f16d
commit 6600f2c939

@ -103,7 +103,7 @@ const labelStr = $derived(typeof opt.label === 'string' ? opt.label : (opt.lab
onclick={() => editor.updateOpt(opt.id, { onclick={() => editor.updateOpt(opt.id, {
value: { domain: { kind, mode: 'input' as any }, value: kind === 'number' ? 0 : kind === 'boolean' ? false : '' } value: { domain: { kind, mode: 'input' as any }, value: kind === 'number' ? 0 : kind === 'boolean' ? false : '' }
})} })}
class="px-3 py-1.5 text-xs rounded border transition-colors</> class="px-3 py-1.5 text-xs rounded border transition-colors
{opt.value.kind === kind {opt.value.kind === kind
? 'bg-indigo-600/20 border-indigo-500 text-indigo-300' ? 'bg-indigo-600/20 border-indigo-500 text-indigo-300'
: 'border-gray-700 text-gray-500 hover:border-gray-600 hover:text-gray-300'}" : 'border-gray-700 text-gray-500 hover:border-gray-600 hover:text-gray-300'}"

@ -283,7 +283,7 @@ export function createCatalogEditorStore(initial: Cat) {
function updateAttDefaultValue(objId: ObjID, secId: SecID | undefined, attId: AttID, value: unknown) { function updateAttDefaultValue(objId: ObjID, secId: SecID | undefined, attId: AttID, value: unknown) {
mutate(() => { mutate(() => {
const att = findAtt(objId, secId, attId); const att = findAtt(objId, secId, attId);
if (att) (att.value as any).value = value; if (att) att.value = { ...att.value, value } as typeof att.value;
}); });
} }

@ -96,17 +96,19 @@
</span> </span>
</label> </label>
{:else if domain.kind === 'option'} {:else if domain.kind === 'option' && domain.mode === 'list'}
{@const optList = domain.list ?? []}
<select <select
value={currentValue as string} value={currentValue as string}
onchange={e => onChange(att.id, e.currentTarget.value)} onchange={e => onChange(att.id, e.currentTarget.value)}
class="flex-1 rounded border border-gray-700 bg-gray-800 px-2 py-1.5 class="flex-1 rounded border border-gray-700 bg-gray-800 px-2 py-1.5
text-sm text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500" text-sm text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
{#each (domain as any).options ?? [] as opt} {#each optList as attOpt}
<option value={opt} {@const globalOpt = store.cat.opts.find(o => o.id === attOpt.optId)}
disabled={es?.forbiddenValues?.includes(opt)}> {@const forbidden = es?.forbiddenValues?.includes(attOpt.optId)}
{opt}{es?.forbiddenValues?.includes(opt) ? ' ✕' : ''} <option value={attOpt.optId} disabled={forbidden}>
{String(globalOpt?.value.value ?? attOpt.optId)}{forbidden ? ' ✕' : ''}
</option> </option>
{/each} {/each}
</select> </select>

@ -1,6 +1,6 @@
import { createSession } from '@/vcen/lib/session.ts'; import { createSession } from '@/vcen/lib/session.ts';
import { buildDependencyGraph } from '@/vcen/lib/graph.ts'; import { buildDependencyGraph } from '@/vcen/lib/graph.ts';
import type { Cat, Obj, Att, Sec } from '@/vcen/lib/types'; import type { Cat, Obj, Att, Sec, Opt, OptID } from '@/vcen/lib/types';
import type { Rule } from '@/vcen/lib/types'; import type { Rule } from '@/vcen/lib/types';
import type { DependencyGraph } from '@/vcen/lib/types'; import type { DependencyGraph } from '@/vcen/lib/types';
import type { Session } from '@/vcen/lib/session.ts'; import type { Session } from '@/vcen/lib/session.ts';
@ -55,19 +55,31 @@ export function createDemoStore() {
function addAtt(kind: DomainKind) { function addAtt(kind: DomainKind) {
const id = `at_${Date.now()}` as Att['id']; const id = `at_${Date.now()}` as Att['id'];
const options: string[] = kind === 'option' ? ['opcion1', 'opcion2'] : []; if (kind === 'option') {
const defaultStrs = ['opcion1', 'opcion2'];
const newOpts = defaultStrs.map(strToOpt);
const optIds = newOpts.map(o => o.id);
const att: Att = { const att: Att = {
id, id,
name: 'Nuevo atributo', name: 'Nuevo atributo',
value: { domain: makeDomain(kind, options), value: defaultValue(kind, options) }, value: { domain: makeDomain('option', optIds), value: optIds[0] ?? null },
}; };
cat = { cat = {
...cat, ...cat,
objs: cat.objs.map((o, i) => i === 0 opts: [...cat.opts, ...newOpts],
? { ...o, atts: [...o.atts, att] } objs: cat.objs.map((o, i) => i === 0 ? { ...o, atts: [...o.atts, att] } : o),
: o
),
}; };
} else {
const att: Att = {
id,
name: 'Nuevo atributo',
value: { domain: makeDomain(kind, []), value: defaultValue(kind) },
};
cat = {
...cat,
objs: cat.objs.map((o, i) => i === 0 ? { ...o, atts: [...o.atts, att] } : o),
};
}
selectedAttId = id; selectedAttId = id;
rebuildSession(); rebuildSession();
} }
@ -94,28 +106,49 @@ export function createDemoStore() {
} }
function updateAttDomain(attId: string, kind: DomainKind) { function updateAttDomain(attId: string, kind: DomainKind) {
cat = updateAtt(cat, attId, a => { if (kind === 'option') {
const options = kind === 'option' ? ((a.value.domain as any).options ?? []) : []; // Crear opts por defecto y referenciarlas en el dominio
return { const defaultStrs = ['opcion1', 'opcion2'];
const newOpts = defaultStrs.map(strToOpt);
const newOptIds = newOpts.map(o => o.id);
cat = {
...updateAtt(cat, attId, a => ({
...a, ...a,
value: { domain: makeDomain(kind, options), value: defaultValue(kind, options) }, value: { domain: makeDomain('option', newOptIds), value: newOptIds[0] ?? null },
})),
opts: [...cat.opts, ...newOpts],
}; };
}); } else {
cat = updateAtt(cat, attId, a => ({
...a,
value: { domain: makeDomain(kind, []), value: defaultValue(kind) },
}));
}
rebuildSession(); rebuildSession();
} }
function updateAttOptions(attId: string, options: string[]) { function updateAttOptions(attId: string, optStrs: string[]) {
cat = updateAtt(cat, attId, a => { // Convertir strings a Opt + ids
const currentVal = a.value.value; const existingByVal = new Map(cat.opts.map(o => [String(o.value.value), o]));
const value = options.includes(currentVal as string) ? currentVal : (options[0] ?? ''); const updatedOpts: Opt[] = [];
return { const optIds: OptID[] = [];
...a, for (const s of optStrs) {
value: { const existing = existingByVal.get(s);
domain: { ...a.value.domain, options } as any, const opt = existing ?? strToOpt(s);
value, updatedOpts.push(opt);
}, optIds.push(opt.id);
}
// Merge: mantener opts no afectadas + agregar/sustituir las de este att
const affectedIds = new Set(optIds);
const otherOpts = cat.opts.filter(o => !affectedIds.has(o.id));
cat = {
...updateAtt(cat, attId, a => {
const currentVal = a.value.value as OptID | null;
const value = optIds.includes(currentVal as OptID) ? currentVal : (optIds[0] ?? null);
return { ...a, value: { domain: makeDomain('option', optIds), value } };
}),
opts: [...otherOpts, ...updatedOpts],
}; };
});
rebuildSession(); rebuildSession();
} }
@ -190,9 +223,18 @@ export type DemoStore = ReturnType<typeof createDemoStore>;
// ─── Helpers ────────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────────
function strToOpt(s: string): Opt {
return {
id: `op_${s.replace(/\s+/g, '_')}` as OptID,
value: { kind: 'string', value: s },
};
}
function makeFreshCat(): Cat { function makeFreshCat(): Cat {
const colorOpts = ['rojo', 'azul', 'negro'].map(strToOpt);
const colorIds = colorOpts.map(o => o.id);
return { return {
opts : [], opts : colorOpts,
optCategories: [], optCategories: [],
id: 'ct_demo' as Cat['id'], id: 'ct_demo' as Cat['id'],
name: 'Catálogo demo', name: 'Catálogo demo',
@ -204,12 +246,12 @@ function makeFreshCat(): Cat {
{ {
id: 'at_color' as Att['id'], id: 'at_color' as Att['id'],
name: 'Color', name: 'Color',
value: { domain: { kind: 'option', options: ['rojo', 'azul', 'negro'] } as any, value: 'rojo' }, value: { domain: { kind: 'option', mode: 'list', list: colorIds.map(id => ({ optId: id })) }, value: colorIds[0] },
}, },
{ {
id: 'at_precio' as Att['id'], id: 'at_precio' as Att['id'],
name: 'Precio', name: 'Precio',
value: { domain: { kind: 'number' } as any, value: 0 }, value: { domain: { kind: 'number', mode: 'input' }, value: 0 },
}, },
], ],
secs: [], secs: [],
@ -217,15 +259,18 @@ function makeFreshCat(): Cat {
}; };
} }
function makeDomain(kind: DomainKind, options: string[] = []): any { function makeDomain(kind: DomainKind, optIds: OptID[] = []): any {
if (kind === 'option') return { kind, options }; if (kind === 'option') return { kind, mode: 'list', list: optIds.map(id => ({ optId: id })) };
return { kind }; if (kind === 'number' || kind === 'currency') return { kind, mode: 'input' };
if (kind === 'boolean') return { kind, mode: 'input' };
if (kind === 'date' || kind === 'time' || kind === 'datetime') return { kind, mode: 'input' };
return { kind, mode: 'input' };
} }
function defaultValue(kind: DomainKind, options: string[] = []): any { function defaultValue(kind: DomainKind): any {
if (kind === 'number' || kind === 'currency') return 0; if (kind === 'number' || kind === 'currency') return 0;
if (kind === 'boolean') return false; if (kind === 'boolean') return false;
if (kind === 'option') return options[0] ?? ''; if (kind === 'option') return null;
if (kind === 'date') return new Date().toISOString().split('T')[0]; if (kind === 'date') return new Date().toISOString().split('T')[0];
if (kind === 'time') return '08:00'; if (kind === 'time') return '08:00';
if (kind === 'datetime') return new Date().toISOString().slice(0, 16); if (kind === 'datetime') return new Date().toISOString().slice(0, 16);

@ -282,7 +282,7 @@ export function createCatalogEditorStore(initial: Cat) {
function updateAttDefaultValue(objId: ObjID, secId: SecID | undefined, attId: AttID, value: unknown) { function updateAttDefaultValue(objId: ObjID, secId: SecID | undefined, attId: AttID, value: unknown) {
mutate(() => { mutate(() => {
const att = findAtt(objId, secId, attId); const att = findAtt(objId, secId, attId);
if (att) (att.value as any).value = value; if (att) att.value = { ...att.value, value } as typeof att.value;
}); });
} }
@ -456,6 +456,10 @@ export function createCatalogEditorStore(initial: Cat) {
selection = { kind: 'cat' }; selection = { kind: 'cat' };
} }
// ── Opt activa (para OptsPanel / OptDetail) ──────────────────────────────
let activeOptId = $state<string | null>(null);
// ── API pública ────────────────────────────────────────────────────────── // ── API pública ──────────────────────────────────────────────────────────
return { return {
@ -463,6 +467,9 @@ export function createCatalogEditorStore(initial: Cat) {
get cat() { return cat; }, get cat() { return cat; },
get selection() { return selection; }, get selection() { return selection; },
get dirty() { return dirty; }, get dirty() { return dirty; },
get activeOptId() { return activeOptId; },
get activeOpt() { return activeOptId ? cat.opts.find(o => o.id === activeOptId) ?? null : null; },
setActiveOpt(id: string) { activeOptId = id; },
// Selección // Selección
select, select,

@ -55,7 +55,7 @@ export function createSessionStore(
cat: Cat, cat: Cat,
engine: JsonLogicEngine | null = null, engine: JsonLogicEngine | null = null,
) { ) {
const session = createSession(cat, engine); const session = createSession(cat, null, engine);
// ── Estado reactivo ─────────────────────────────────────────────────────── // ── Estado reactivo ───────────────────────────────────────────────────────
// Un único $state que contiene el EvaluationResult completo. // Un único $state que contiene el EvaluationResult completo.

@ -1,4 +1,7 @@
import type {GlobInstance} from "@/glob/lib"; import type {GlobInstance} from "@/glob/lib";
// GlobInstance puede ser null cuando la sesión se usa solo para evaluación
// (sin pricing ni view resolution)
type GlobOrNull = GlobInstance | null;
import { JsonLogicEngine } from '@/jslg/lib'; import { JsonLogicEngine } from '@/jslg/lib';
import type { import type {
Cat, Cat,
@ -55,7 +58,7 @@ export class Session {
private readonly cat: Cat; private readonly cat: Cat;
private readonly graph: DependencyGraph; private readonly graph: DependencyGraph;
private readonly engine: JsonLogicEngine; private readonly engine: JsonLogicEngine;
private readonly glob: GlobInstance; private readonly glob: GlobOrNull;
private readonly initialResult: EvaluationResult; private readonly initialResult: EvaluationResult;
private current: EvaluationResult; private current: EvaluationResult;
private readonly listeners: Set<SessionListener> = new Set(); private readonly listeners: Set<SessionListener> = new Set();
@ -64,7 +67,7 @@ export class Session {
constructor( constructor(
cat: Cat, cat: Cat,
glob: GlobInstance, glob: GlobOrNull = null,
engine: JsonLogicEngine | null = null, engine: JsonLogicEngine | null = null,
) { ) {
this.cat = cat; this.cat = cat;
@ -130,6 +133,7 @@ export class Session {
* @returns `ObjPriceResult` o `undefined` si el objId no existe en el Cat * @returns `ObjPriceResult` o `undefined` si el objId no existe en el Cat
*/ */
getPrice(objId: string): ObjPriceResult | undefined { getPrice(objId: string): ObjPriceResult | undefined {
if (!this.glob) return undefined;
const obj = this.cat.objs.find(o => o.id === objId); const obj = this.cat.objs.find(o => o.id === objId);
if (!obj) return undefined; if (!obj) return undefined;
@ -149,8 +153,9 @@ export class Session {
* @returns Array de `ObjPriceResult`, uno por cada Obj del Cat * @returns Array de `ObjPriceResult`, uno por cada Obj del Cat
*/ */
getAllPrices(): ObjPriceResult[] { getAllPrices(): ObjPriceResult[] {
if (!this.glob) return [];
return this.cat.objs.map(obj => return this.cat.objs.map(obj =>
computeObjPrice(obj, this.cat.opts, this.cat.pricingDefaults, this.configState, this.glob, this.engine) computeObjPrice(obj, this.cat.opts, this.cat.pricingDefaults, this.configState, this.glob!, this.engine)
); );
} }
@ -172,7 +177,8 @@ export class Session {
obj: Obj, obj: Obj,
secVisual?: Visual, secVisual?: Visual,
objVisual?: Visual, objVisual?: Visual,
): ViewResolution { ): ViewResolution | undefined {
if (!this.glob) return undefined;
return resolveViewFn( return resolveViewFn(
view, obj, secVisual, objVisual, this.cat, this.configState, this.engine, view, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
); );
@ -192,6 +198,7 @@ export class Session {
secVisual?: Visual, secVisual?: Visual,
objVisual?: Visual, objVisual?: Visual,
): ViewResolution[] { ): ViewResolution[] {
if (!this.glob) return [];
return resolveVisualFn( return resolveVisualFn(
visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine, visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
); );
@ -209,6 +216,7 @@ export class Session {
secVisual?: Visual, secVisual?: Visual,
objVisual?: Visual, objVisual?: Visual,
): ViewResolution | undefined { ): ViewResolution | undefined {
if (!this.glob) return undefined;
return resolveDefaultViewFn( return resolveDefaultViewFn(
visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine, visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
); );
@ -338,7 +346,7 @@ export class Session {
*/ */
export function createSession( export function createSession(
cat: Cat, cat: Cat,
glob: GlobInstance, glob: GlobInstance | null = null,
engine: JsonLogicEngine | null = null, engine: JsonLogicEngine | null = null,
): Session { ): Session {
return new Session(cat, glob, engine); return new Session(cat, glob, engine);

Loading…
Cancel
Save

Powered by TurnKey Linux.