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, {
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
? 'bg-indigo-600/20 border-indigo-500 text-indigo-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) {
mutate(() => {
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>
</label>
{:else if domain.kind === 'option'}
{:else if domain.kind === 'option' && domain.mode === 'list'}
{@const optList = domain.list ?? []}
<select
value={currentValue as string}
onchange={e => onChange(att.id, e.currentTarget.value)}
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"
>
{#each (domain as any).options ?? [] as opt}
<option value={opt}
disabled={es?.forbiddenValues?.includes(opt)}>
{opt}{es?.forbiddenValues?.includes(opt) ? ' ✕' : ''}
{#each optList as attOpt}
{@const globalOpt = store.cat.opts.find(o => o.id === attOpt.optId)}
{@const forbidden = es?.forbiddenValues?.includes(attOpt.optId)}
<option value={attOpt.optId} disabled={forbidden}>
{String(globalOpt?.value.value ?? attOpt.optId)}{forbidden ? ' ✕' : ''}
</option>
{/each}
</select>

@ -1,6 +1,6 @@
import { createSession } from '@/vcen/lib/session.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 { DependencyGraph } from '@/vcen/lib/types';
import type { Session } from '@/vcen/lib/session.ts';
@ -55,19 +55,31 @@ export function createDemoStore() {
function addAtt(kind: DomainKind) {
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 = {
id,
name: 'Nuevo atributo',
value: { domain: makeDomain(kind, options), value: defaultValue(kind, options) },
value: { domain: makeDomain('option', optIds), value: optIds[0] ?? null },
};
cat = {
...cat,
objs: cat.objs.map((o, i) => i === 0
? { ...o, atts: [...o.atts, att] }
: o
),
opts: [...cat.opts, ...newOpts],
objs: cat.objs.map((o, i) => i === 0 ? { ...o, atts: [...o.atts, att] } : 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;
rebuildSession();
}
@ -94,28 +106,49 @@ export function createDemoStore() {
}
function updateAttDomain(attId: string, kind: DomainKind) {
cat = updateAtt(cat, attId, a => {
const options = kind === 'option' ? ((a.value.domain as any).options ?? []) : [];
return {
if (kind === 'option') {
// Crear opts por defecto y referenciarlas en el dominio
const defaultStrs = ['opcion1', 'opcion2'];
const newOpts = defaultStrs.map(strToOpt);
const newOptIds = newOpts.map(o => o.id);
cat = {
...updateAtt(cat, attId, 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();
}
function updateAttOptions(attId: string, options: string[]) {
cat = updateAtt(cat, attId, a => {
const currentVal = a.value.value;
const value = options.includes(currentVal as string) ? currentVal : (options[0] ?? '');
return {
...a,
value: {
domain: { ...a.value.domain, options } as any,
value,
},
function updateAttOptions(attId: string, optStrs: string[]) {
// Convertir strings a Opt + ids
const existingByVal = new Map(cat.opts.map(o => [String(o.value.value), o]));
const updatedOpts: Opt[] = [];
const optIds: OptID[] = [];
for (const s of optStrs) {
const existing = existingByVal.get(s);
const opt = existing ?? strToOpt(s);
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();
}
@ -190,9 +223,18 @@ export type DemoStore = ReturnType<typeof createDemoStore>;
// ─── Helpers ──────────────────────────────────────────────────────────────────
function strToOpt(s: string): Opt {
return {
id: `op_${s.replace(/\s+/g, '_')}` as OptID,
value: { kind: 'string', value: s },
};
}
function makeFreshCat(): Cat {
const colorOpts = ['rojo', 'azul', 'negro'].map(strToOpt);
const colorIds = colorOpts.map(o => o.id);
return {
opts : [],
opts : colorOpts,
optCategories: [],
id: 'ct_demo' as Cat['id'],
name: 'Catálogo demo',
@ -204,12 +246,12 @@ function makeFreshCat(): Cat {
{
id: 'at_color' as Att['id'],
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'],
name: 'Precio',
value: { domain: { kind: 'number' } as any, value: 0 },
value: { domain: { kind: 'number', mode: 'input' }, value: 0 },
},
],
secs: [],
@ -217,15 +259,18 @@ function makeFreshCat(): Cat {
};
}
function makeDomain(kind: DomainKind, options: string[] = []): any {
if (kind === 'option') return { kind, options };
return { kind };
function makeDomain(kind: DomainKind, optIds: OptID[] = []): any {
if (kind === 'option') return { kind, mode: 'list', list: optIds.map(id => ({ optId: id })) };
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 === '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 === 'time') return '08:00';
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) {
mutate(() => {
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' };
}
// ── Opt activa (para OptsPanel / OptDetail) ──────────────────────────────
let activeOptId = $state<string | null>(null);
// ── API pública ──────────────────────────────────────────────────────────
return {
@ -463,6 +467,9 @@ export function createCatalogEditorStore(initial: Cat) {
get cat() { return cat; },
get selection() { return selection; },
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
select,

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

@ -1,4 +1,7 @@
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 type {
Cat,
@ -55,7 +58,7 @@ export class Session {
private readonly cat: Cat;
private readonly graph: DependencyGraph;
private readonly engine: JsonLogicEngine;
private readonly glob: GlobInstance;
private readonly glob: GlobOrNull;
private readonly initialResult: EvaluationResult;
private current: EvaluationResult;
private readonly listeners: Set<SessionListener> = new Set();
@ -64,7 +67,7 @@ export class Session {
constructor(
cat: Cat,
glob: GlobInstance,
glob: GlobOrNull = null,
engine: JsonLogicEngine | null = null,
) {
this.cat = cat;
@ -130,6 +133,7 @@ export class Session {
* @returns `ObjPriceResult` o `undefined` si el objId no existe en el Cat
*/
getPrice(objId: string): ObjPriceResult | undefined {
if (!this.glob) return undefined;
const obj = this.cat.objs.find(o => o.id === objId);
if (!obj) return undefined;
@ -149,8 +153,9 @@ export class Session {
* @returns Array de `ObjPriceResult`, uno por cada Obj del Cat
*/
getAllPrices(): ObjPriceResult[] {
if (!this.glob) return [];
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,
secVisual?: Visual,
objVisual?: Visual,
): ViewResolution {
): ViewResolution | undefined {
if (!this.glob) return undefined;
return resolveViewFn(
view, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
);
@ -192,6 +198,7 @@ export class Session {
secVisual?: Visual,
objVisual?: Visual,
): ViewResolution[] {
if (!this.glob) return [];
return resolveVisualFn(
visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
);
@ -209,6 +216,7 @@ export class Session {
secVisual?: Visual,
objVisual?: Visual,
): ViewResolution | undefined {
if (!this.glob) return undefined;
return resolveDefaultViewFn(
visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
);
@ -338,7 +346,7 @@ export class Session {
*/
export function createSession(
cat: Cat,
glob: GlobInstance,
glob: GlobInstance | null = null,
engine: JsonLogicEngine | null = null,
): Session {
return new Session(cat, glob, engine);

Loading…
Cancel
Save

Powered by TurnKey Linux.