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.
v/src/vcen-edit/stores/catalog-editor.svelte.ts

528 lines
21 KiB

import type { Cat, Obj, Sec , Att, AttDomain, AttValue,
Opt, OptCategory, AttOpt,
OptID, OptCatID , RuleSet, Rule, RuleAction,
RuleID, AttState, RelativePath,
DomainValueRef, DomainValueState,
RuleLevel , CatID, ObjID, SecID, AttID } from '@/vcen/lib';
// ─────────────────────────────────────────────────────────────────────────────
// SELECTION — nodo activo en el árbol
// ─────────────────────────────────────────────────────────────────────────────
export type SelectionKind = 'cat' | 'obj' | 'sec' | 'att' | 'opts';
export type Selection =
| { kind: 'cat' }
| { kind: 'opts' } // panel Opts/OptCategories
| { kind: 'obj'; objId: ObjID }
| { kind: 'sec'; objId: ObjID; secId: SecID }
| { kind: 'att'; objId: ObjID; secId?: SecID; attId: AttID };
// ─────────────────────────────────────────────────────────────────────────────
// ID GENERATORS
// ─────────────────────────────────────────────────────────────────────────────
let _seq = Date.now();
function nextId<T extends string>(prefix: string): T {
return `${prefix}:${(++_seq).toString(36)}` as T;
}
// ─────────────────────────────────────────────────────────────────────────────
// DEFAULT FACTORIES
// ─────────────────────────────────────────────────────────────────────────────
function defaultAttValue(): AttValue {
return { domain: { kind: 'string', mode: 'input' }, value: '' };
}
function defaultAtt(name = 'Nuevo atributo'): Att {
return { id: nextId<AttID>('at'), name, value: defaultAttValue() };
}
function defaultSec(name = 'Nueva sección'): Sec {
return { id: nextId<SecID>('sc'), name, atts: [] };
}
function defaultObj(name = 'Nuevo objeto'): Obj {
return { id: nextId<ObjID>('ob'), name, atts: [], secs: [] };
}
function defaultOpt(value = ''): Opt {
return {
id: nextId<OptID>('op'),
value: { kind: 'string', value },
};
}
function defaultOptCategory(name = 'Nueva categoría'): OptCategory {
return { id: nextId<OptCatID>('oc'), name };
}
function defaultRule(): Rule {
return {
id: nextId<RuleID>('rl'),
priority: 10,
condition: true,
actions: [],
};
}
// ─────────────────────────────────────────────────────────────────────────────
// STORE
// ─────────────────────────────────────────────────────────────────────────────
/**
* Store Svelte 5 para el editor de catálogo.
*
* Gestiona el Cat en modo borrador con mutaciones granulares.
* La UI lee `cat` directamente (reactivo) y llama a las mutaciones
* para cada operación de edición.
*
* ## Uso
*
* ```svelte
* <script>
* import { createCatalogEditorStore } from './catalog-editor.svelte';
* const editor = createCatalogEditorStore(cat);
* </script>
*
* <CatalogTree {editor} />
* ```
*/
export function createCatalogEditorStore(initial: Cat) {
let cat = $state<Cat>(structuredClone(initial));
let selection = $state<Selection>({ kind: 'cat' });
let dirty = $state(false);
// ── Helpers internos ────────────────────────────────────────────────────
function mutate(fn: () => void) {
fn();
dirty = true;
}
function findObj(objId: ObjID): Obj | undefined {
return cat.objs.find(o => o.id === objId);
}
function findSec(objId: ObjID, secId: SecID): Sec | undefined {
return findObj(objId)?.secs.find(s => s.id === secId);
}
function findAtt(objId: ObjID, secId: SecID | undefined, attId: AttID): Att | undefined {
if (secId) return findSec(objId, secId)?.atts.find(a => a.id === attId);
return findObj(objId)?.atts.find(a => a.id === attId);
}
function findCatAtt(attId: AttID): Att | undefined {
return cat.atts.find(a => a.id === attId);
}
// ── Selección ───────────────────────────────────────────────────────────
function select(s: Selection) {
selection = s;
}
// ── Cat ─────────────────────────────────────────────────────────────────
function renameCat(name: string) {
mutate(() => { cat.name = name; });
}
// ── Obj ─────────────────────────────────────────────────────────────────
function addObj(name?: string): ObjID {
const obj = defaultObj(name);
mutate(() => { cat.objs.push(obj); });
select({ kind: 'obj', objId: obj.id });
return obj.id;
}
function renameObj(objId: ObjID, name: string) {
mutate(() => {
const obj = findObj(objId);
if (obj) obj.name = name;
});
}
function removeObj(objId: ObjID) {
mutate(() => {
cat.objs = cat.objs.filter(o => o.id !== objId);
});
// Limpiar selección si apuntaba al obj eliminado
if (selection.kind === 'obj' && selection.objId === objId) select({ kind: 'cat' });
if ((selection.kind === 'sec' || selection.kind === 'att') && selection.objId === objId) select({ kind: 'cat' });
}
function moveObj(objId: ObjID, direction: 'up' | 'down') {
mutate(() => {
const idx = cat.objs.findIndex(o => o.id === objId);
if (idx === -1) return;
const target = direction === 'up' ? idx - 1 : idx + 1;
if (target < 0 || target >= cat.objs.length) return;
[cat.objs[idx], cat.objs[target]] = [cat.objs[target], cat.objs[idx]];
});
}
// ── Sec ─────────────────────────────────────────────────────────────────
function addSec(objId: ObjID, name?: string): SecID {
const sec = defaultSec(name);
mutate(() => {
const obj = findObj(objId);
if (obj) obj.secs.push(sec);
});
select({ kind: 'sec', objId, secId: sec.id });
return sec.id;
}
function renameSec(objId: ObjID, secId: SecID, name: string) {
mutate(() => {
const sec = findSec(objId, secId);
if (sec) sec.name = name;
});
}
function removeSec(objId: ObjID, secId: SecID) {
mutate(() => {
const obj = findObj(objId);
if (obj) obj.secs = obj.secs.filter(s => s.id !== secId);
});
if (selection.kind === 'sec' && selection.secId === secId) select({ kind: 'obj', objId });
if (selection.kind === 'att' && selection.secId === secId) select({ kind: 'obj', objId });
}
function moveSec(objId: ObjID, secId: SecID, direction: 'up' | 'down') {
mutate(() => {
const obj = findObj(objId);
if (!obj) return;
const idx = obj.secs.findIndex(s => s.id === secId);
if (idx === -1) return;
const target = direction === 'up' ? idx - 1 : idx + 1;
if (target < 0 || target >= obj.secs.length) return;
[obj.secs[idx], obj.secs[target]] = [obj.secs[target], obj.secs[idx]];
});
}
function setSecRuleSet(objId: ObjID, secId: SecID, ruleSet: RuleSet | undefined) {
mutate(() => {
const sec = findSec(objId, secId);
if (sec) sec.rules = ruleSet;
});
}
// ── Att (en Obj o Sec) ──────────────────────────────────────────────────
function addAtt(objId: ObjID, secId: SecID | undefined, name?: string): AttID {
const att = defaultAtt(name);
mutate(() => {
if (secId) {
const sec = findSec(objId, secId);
if (sec) sec.atts.push(att);
} else {
const obj = findObj(objId);
if (obj) obj.atts.push(att);
}
});
select({ kind: 'att', objId, secId, attId: att.id });
return att.id;
}
function addCatAtt(name?: string): AttID {
const att = defaultAtt(name);
mutate(() => { cat.atts.push(att); });
select({ kind: 'att', objId: '' as ObjID, secId: undefined, attId: att.id });
return att.id;
}
function renameAtt(objId: ObjID, secId: SecID | undefined, attId: AttID, name: string) {
mutate(() => {
const att = findAtt(objId, secId, attId);
if (att) att.name = name;
});
}
function removeAtt(objId: ObjID, secId: SecID | undefined, attId: AttID) {
mutate(() => {
if (secId) {
const sec = findSec(objId, secId);
if (sec) sec.atts = sec.atts.filter(a => a.id !== attId);
} else {
const obj = findObj(objId);
if (obj) obj.atts = obj.atts.filter(a => a.id !== attId);
}
});
if (selection.kind === 'att' && selection.attId === attId) {
select(secId ? { kind: 'sec', objId, secId } : { kind: 'obj', objId });
}
}
function moveAtt(objId: ObjID, secId: SecID | undefined, attId: AttID, direction: 'up' | 'down') {
mutate(() => {
const list = secId ? findSec(objId, secId)?.atts : findObj(objId)?.atts;
if (!list) return;
const idx = list.findIndex(a => a.id === attId);
if (idx === -1) return;
const target = direction === 'up' ? idx - 1 : idx + 1;
if (target < 0 || target >= list.length) return;
[list[idx], list[target]] = [list[target], list[idx]];
});
}
function updateAttDomain(objId: ObjID, secId: SecID | undefined, attId: AttID, domain: AttDomain) {
mutate(() => {
const att = findAtt(objId, secId, attId);
if (!att) return;
// Cambiar dominio resetea el valor al default del nuevo kind
att.value = { domain, value: defaultValueForDomain(domain) } as AttValue;
});
}
function updateAttDefaultValue(objId: ObjID, secId: SecID | undefined, attId: AttID, value: unknown) {
mutate(() => {
const att = findAtt(objId, secId, attId);
if (att) att.value = { ...att.value, value } as typeof att.value;
});
}
function setAttRuleSet(objId: ObjID, secId: SecID | undefined, attId: AttID, ruleSet: RuleSet | undefined) {
mutate(() => {
const att = findAtt(objId, secId, attId);
if (att) att.rules = ruleSet;
});
}
function setObjRuleSet(objId: ObjID, ruleSet: RuleSet | undefined) {
mutate(() => {
const obj = findObj(objId);
if (obj) obj.rules = ruleSet;
});
}
// ── Opts globales ───────────────────────────────────────────────────────
function addOpt(value = '', categoryId?: OptCatID): OptID {
const opt = defaultOpt(value);
if (categoryId) opt.categoryId = categoryId;
mutate(() => { cat.opts.push(opt); });
return opt.id;
}
function updateOpt(optId: OptID, patch: Partial<Pick<Opt, 'value' | 'label' | 'categoryId' | 'rules'>>) {
mutate(() => {
const opt = cat.opts.find(o => o.id === optId);
if (!opt) return;
if (patch.value !== undefined) opt.value = patch.value;
if (patch.label !== undefined) opt.label = patch.label;
if (patch.categoryId !== undefined) opt.categoryId = patch.categoryId;
if (patch.rules !== undefined) opt.rules = patch.rules;
});
}
function removeOpt(optId: OptID) {
mutate(() => {
cat.opts = cat.opts.filter(o => o.id !== optId);
// Limpiar referencias en dominios option/list de todos los atts
removeOptFromAllDomains(optId);
});
}
function removeOptFromAllDomains(optId: OptID) {
// Cat-level atts
for (const att of cat.atts) pruneAttOpt(att, optId);
// Obj/Sec atts
for (const obj of cat.objs) {
for (const att of obj.atts) pruneAttOpt(att, optId);
for (const sec of obj.secs) {
for (const att of sec.atts) pruneAttOpt(att, optId);
}
}
}
function pruneAttOpt(att: Att, optId: OptID) {
if (att.value.domain.kind === 'option' && att.value.domain.mode === 'list') {
(att.value.domain as any).list = (att.value.domain as any).list.filter(
(ao: AttOpt) => ao.optId !== optId
);
}
}
function addAttOpt(objId: ObjID, secId: SecID | undefined, attId: AttID, optId: OptID) {
mutate(() => {
const att = findAtt(objId, secId, attId);
if (!att || att.value.domain.kind !== 'option' || att.value.domain.mode !== 'list') return;
const list = (att.value.domain as any).list as AttOpt[];
if (!list.find(ao => ao.optId === optId)) {
list.push({ optId });
}
});
}
function removeAttOpt(objId: ObjID, secId: SecID | undefined, attId: AttID, optId: OptID) {
mutate(() => {
const att = findAtt(objId, secId, attId);
if (!att || att.value.domain.kind !== 'option' || att.value.domain.mode !== 'list') return;
(att.value.domain as any).list = ((att.value.domain as any).list as AttOpt[])
.filter(ao => ao.optId !== optId);
});
}
// ── OptCategories ───────────────────────────────────────────────────────
function addOptCategory(name?: string): OptCatID {
const oc = defaultOptCategory(name);
mutate(() => { cat.optCategories.push(oc); });
return oc.id;
}
function renameOptCategory(ocId: OptCatID, name: string) {
mutate(() => {
const oc = cat.optCategories.find(c => c.id === ocId);
if (oc) oc.name = name;
});
}
function removeOptCategory(ocId: OptCatID) {
mutate(() => {
cat.optCategories = cat.optCategories.filter(c => c.id !== ocId);
// Opts que apuntaban a esta categoría quedan sin categoría
for (const opt of cat.opts) {
if (opt.categoryId === ocId) delete opt.categoryId;
}
});
}
// ── RuleSet helpers ─────────────────────────────────────────────────────
function addRule(ruleSet: RuleSet): RuleID {
const rule = defaultRule();
mutate(() => { ruleSet.rules.push(rule); });
return rule.id;
}
function updateRule(ruleSet: RuleSet, ruleId: RuleID, patch: Partial<Pick<Rule, 'condition' | 'priority'>>) {
mutate(() => {
const rule = ruleSet.rules.find(r => r.id === ruleId);
if (!rule) return;
if (patch.condition !== undefined) rule.condition = patch.condition;
if (patch.priority !== undefined) rule.priority = patch.priority;
});
}
function removeRule(ruleSet: RuleSet, ruleId: RuleID) {
mutate(() => {
ruleSet.rules = ruleSet.rules.filter(r => r.id !== ruleId);
});
}
function addAction(ruleSet: RuleSet, ruleId: RuleID, action: RuleAction) {
mutate(() => {
const rule = ruleSet.rules.find(r => r.id === ruleId);
if (rule) rule.actions.push(action);
});
}
function updateAction(ruleSet: RuleSet, ruleId: RuleID, actionIdx: number, action: RuleAction) {
mutate(() => {
const rule = ruleSet.rules.find(r => r.id === ruleId);
if (rule && actionIdx < rule.actions.length) rule.actions[actionIdx] = action;
});
}
function removeAction(ruleSet: RuleSet, ruleId: RuleID, actionIdx: number) {
mutate(() => {
const rule = ruleSet.rules.find(r => r.id === ruleId);
if (rule) rule.actions.splice(actionIdx, 1);
});
}
// ── Persistencia ────────────────────────────────────────────────────────
/** Devuelve el Cat en estado actual. Útil para guardar. */
function getCat(): Cat {
return cat;
}
/** Marca el store como limpio (p.ej. tras guardar). */
function markClean() {
dirty = false;
}
/** Descarta todos los cambios y vuelve al Cat inicial. */
function discard() {
cat = structuredClone(initial);
dirty = false;
selection = { kind: 'cat' };
}
// ── Opt activa (para OptsPanel / OptDetail) ──────────────────────────────
let activeOptId = $state<string | null>(null);
// ── API pública ──────────────────────────────────────────────────────────
return {
// Estado reactivo
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,
// Cat
renameCat,
// Obj
addObj, renameObj, removeObj, moveObj,
setObjRuleSet,
// Sec
addSec, renameSec, removeSec, moveSec,
setSecRuleSet,
// Att
addAtt, addCatAtt, renameAtt, removeAtt, moveAtt,
updateAttDomain, updateAttDefaultValue, setAttRuleSet,
// Opts globales
addOpt, updateOpt, removeOpt, addAttOpt, removeAttOpt,
// OptCategories
addOptCategory, renameOptCategory, removeOptCategory,
// RuleSet helpers (operan sobre cualquier RuleSet pasado por ref)
addRule, updateRule, removeRule,
addAction, updateAction, removeAction,
// Persistencia
getCat, markClean, discard,
// Acceso a finders para los paneles
findObj, findSec, findAtt, findCatAtt,
};
}
export type CatalogEditorStore = ReturnType<typeof createCatalogEditorStore>;
// ─────────────────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────────────────
function defaultValueForDomain(domain: AttDomain): unknown {
switch (domain.kind) {
case 'string': return '';
case 'number': return 0;
case 'currency': return 0;
case 'boolean': return false;
case 'option': return null;
case 'date': return '';
case 'time': return '';
case 'datetime': return '';
}
}

Powered by TurnKey Linux.