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.
528 lines
21 KiB
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 '';
|
|
}
|
|
}
|