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.
812 lines
30 KiB
812 lines
30 KiB
// ─────────────────────────────────────────────────────────────────────────────
|
|
// VICE · Editor Store
|
|
//
|
|
// Store principal del editor de catálogos.
|
|
// Gestiona el estado de edición de una Collection completa.
|
|
// Usa runas de Svelte 5 ($state, $derived).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
import type {
|
|
Collection, CollectionID,
|
|
Entity, EntityID,
|
|
Group, GroupID,
|
|
Attribute, AttrID,
|
|
AttributeOption,
|
|
OptionDefinition, OptionID,
|
|
Rule,
|
|
EntityPath,
|
|
EntityPathKey,
|
|
LiteralValue,
|
|
} from '@/vice/libs/types'
|
|
import type {FixedGroup, OptionalGroup} from "@/vice/libs";
|
|
import type {LingString} from "@/ling/lib";
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// HELPERS
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
function uid(prefix: string): string {
|
|
return `${prefix}_${Math.random().toString(36).slice(2, 8)}`
|
|
}
|
|
|
|
function emptyLing(es = '', en = ''): LingString {
|
|
return { es, en }
|
|
}
|
|
|
|
function emptyCollection(): Collection {
|
|
return {
|
|
id: uid('col') as CollectionID,
|
|
code: '',
|
|
name: emptyLing(),
|
|
description: emptyLing(),
|
|
langs: ['es'],
|
|
attributes: [],
|
|
options: {} as Record<OptionID, OptionDefinition>,
|
|
entities: {} as Record<EntityID, Entity>,
|
|
}
|
|
}
|
|
|
|
function emptyEntity(id: EntityID): Entity {
|
|
return {
|
|
id,
|
|
code: '',
|
|
name: emptyLing(),
|
|
description: emptyLing(),
|
|
attributes: [],
|
|
groups: {} as Record<GroupID, Group>,
|
|
rules: [],
|
|
}
|
|
}
|
|
|
|
function emptyGroup(id: GroupID, kind: 'fixed' | 'optional' = 'fixed'): Group {
|
|
const base = {
|
|
id,
|
|
code: '',
|
|
name: emptyLing(),
|
|
description: emptyLing(),
|
|
attributes: [],
|
|
}
|
|
if (kind === 'optional') {
|
|
return { ...base, kind: 'optional', rules: [] } satisfies OptionalGroup
|
|
}
|
|
return { ...base, kind: 'fixed' } satisfies FixedGroup
|
|
}
|
|
|
|
function emptyAttribute(id: AttrID): import('@/vice/libs/types.ts').RequiredAttribute {
|
|
return {
|
|
id,
|
|
code: '',
|
|
name: emptyLing(),
|
|
description: emptyLing(),
|
|
type: 'required',
|
|
domain: { kind: 'options', options: [], default: '' as OptionID },
|
|
rules: [],
|
|
display: {},
|
|
}
|
|
}
|
|
|
|
function emptyFixedAttribute(id: AttrID): import('@/vice/libs/types.ts').FixedAttribute {
|
|
return {
|
|
id,
|
|
code: '',
|
|
name: emptyLing(),
|
|
description: emptyLing(),
|
|
type: 'fixed',
|
|
domain: { kind: 'fixed', value: { kind: 'string', value: '' } },
|
|
display: {},
|
|
}
|
|
}
|
|
|
|
function emptyOption(id: OptionID): OptionDefinition {
|
|
return {
|
|
id,
|
|
code: '',
|
|
name: emptyLing(),
|
|
description: emptyLing(),
|
|
tags: [],
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// VALIDATION
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export type EditorPanel = 'catalog' | 'objects' | 'options' | 'rules' | 'preview' | 'export'
|
|
|
|
export interface EditorIssue {
|
|
severity: 'error' | 'warning'
|
|
message: string
|
|
path: string
|
|
}
|
|
|
|
export interface EditorValidation {
|
|
valid: boolean
|
|
errors: EditorIssue[]
|
|
warnings: EditorIssue[]
|
|
}
|
|
|
|
function validateCollection(collection: Collection): EditorValidation {
|
|
const errors: EditorIssue[] = []
|
|
const warnings: EditorIssue[] = []
|
|
|
|
// Catálogo
|
|
if (!collection.code) errors.push({ severity: 'error', message: 'El catálogo necesita un código', path: 'collection.code' })
|
|
|
|
// Validar nombre en todos los langs declarados
|
|
const name = collection.name as any
|
|
for (const lang of collection.langs) {
|
|
if (!name?.[lang]) {
|
|
errors.push({ severity: 'error', message: `El catálogo necesita nombre en "${lang}"`, path: `collection.name.${lang}` })
|
|
}
|
|
}
|
|
|
|
// Opciones globales
|
|
for (const opt of Object.values(collection.options)) {
|
|
if (!opt.code) errors.push({ severity: 'error', message: `Opción "${opt.id}" sin código`, path: `options.${opt.id}.code` })
|
|
|
|
const optName = opt.name as any
|
|
for (const lang of collection.langs) {
|
|
if (!optName?.[lang]) {
|
|
warnings.push({ severity: 'warning', message: `Opción "${opt.id}" sin nombre en "${lang}"`, path: `options.${opt.id}.name.${lang}` })
|
|
}
|
|
}
|
|
}
|
|
|
|
// Entidades
|
|
for (const entity of Object.values(collection.entities)) {
|
|
if (!entity.code) errors.push({ severity: 'error', message: `Entidad "${entity.id}" sin código`, path: `entities.${entity.id}.code` })
|
|
|
|
const entityName = entity.name as any
|
|
for (const lang of collection.langs) {
|
|
if (!entityName?.[lang]) {
|
|
warnings.push({ severity: 'warning', message: `Entidad "${entity.id}" sin nombre en "${lang}"`, path: `entities.${entity.id}.name.${lang}` })
|
|
}
|
|
}
|
|
|
|
// Grupos
|
|
for (const [groupId, group] of Object.entries(entity.groups)) {
|
|
if (!group.code) errors.push({ severity: 'error', message: `Grupo "${groupId}" sin código`, path: `entities.${entity.id}.groups.${groupId}.code` })
|
|
|
|
const groupName = group.name as any
|
|
for (const lang of collection.langs) {
|
|
if (!groupName?.[lang]) {
|
|
warnings.push({ severity: 'warning', message: `Grupo "${groupId}" sin nombre en "${lang}"`, path: `entities.${entity.id}.groups.${groupId}.name.${lang}` })
|
|
}
|
|
}
|
|
|
|
// Atributos del grupo
|
|
for (const attr of group.attributes) {
|
|
validateAttribute(attr, `entities.${entity.id}.groups.${groupId}`, collection, errors, warnings)
|
|
}
|
|
}
|
|
|
|
// Atributos globales de la entidad
|
|
for (const attr of entity.attributes) {
|
|
validateAttribute(attr, `entities.${entity.id}`, collection, errors, warnings)
|
|
}
|
|
}
|
|
|
|
// Atributos globales del catálogo
|
|
for (const attr of collection.attributes) {
|
|
validateAttribute(attr, 'collection', collection, errors, warnings)
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors, warnings }
|
|
}
|
|
|
|
function validateAttribute(
|
|
attr: Attribute,
|
|
scope: string,
|
|
collection: Collection,
|
|
errors: EditorIssue[],
|
|
warnings: EditorIssue[],
|
|
): void {
|
|
const path = `${scope}.attributes.${attr.id}`
|
|
if (!attr.code) errors.push({ severity: 'error', message: `Atributo "${attr.id}" sin código`, path: `${path}.code` })
|
|
|
|
const attrName = attr.name as any
|
|
for (const lang of collection.langs) {
|
|
if (!attrName?.[lang]) {
|
|
warnings.push({ severity: 'warning', message: `Atributo "${attr.id}" sin nombre en "${lang}"`, path: `${path}.name.${lang}` })
|
|
}
|
|
}
|
|
|
|
if (attr.type !== 'fixed') {
|
|
const domain = attr.domain
|
|
if (domain.kind === 'options') {
|
|
if (domain.options.length === 0) {
|
|
warnings.push({ severity: 'warning', message: `Atributo "${attr.id}" sin opciones`, path: `${path}.domain.options` })
|
|
} else {
|
|
const hasDefault = domain.options.some(o => o.optionId === domain.default)
|
|
if (!hasDefault) {
|
|
errors.push({ severity: 'error', message: `Atributo "${attr.id}": el valor por defecto no está en las opciones`, path: `${path}.domain.default` })
|
|
}
|
|
for (const attrOpt of domain.options) {
|
|
if (!collection.options[attrOpt.optionId]) {
|
|
errors.push({ severity: 'error', message: `Atributo "${attr.id}": opción "${attrOpt.optionId}" no existe en la librería`, path: `${path}.domain.options.${attrOpt.optionId}` })
|
|
}
|
|
}
|
|
}
|
|
} else if (domain.kind === 'literals') {
|
|
if (domain.literals.length === 0) {
|
|
warnings.push({ severity: 'warning', message: `Atributo "${attr.id}" sin literales en el dominio`, path: `${path}.domain.literals` })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getLing(l: LingString): string {
|
|
return typeof l === 'string' ? l : (l as any)?.es ?? ''
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// EDITOR STORE
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export function createEditorStore(initial?: Collection) {
|
|
|
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
|
|
let collection = $state<Collection>(initial ?? emptyCollection())
|
|
let activeEntityId = $state<EntityID | null>(null)
|
|
let activeGroupId = $state<GroupID | null>(null)
|
|
let activeAttrId = $state<AttrID | null>(null)
|
|
let activeOptionId = $state<OptionID | null>(null)
|
|
let activeRuleIdx = $state<number | null>(null)
|
|
let activePanel = $state<EditorPanel>('objects')
|
|
let isDirty = $state(false)
|
|
|
|
function markDirty() { isDirty = true }
|
|
|
|
// ── Derived ────────────────────────────────────────────────────────────────
|
|
|
|
const validation = $derived(validateCollection(collection))
|
|
const canPreview = $derived(
|
|
validation.valid && Object.keys(collection.entities).length > 0
|
|
)
|
|
const activeEntity = $derived(activeEntityId ? collection.entities[activeEntityId] ?? null : null)
|
|
const activeGroup = $derived(
|
|
activeEntity && activeGroupId ? (activeEntity.groups[activeGroupId] ?? null) : null
|
|
)
|
|
const activeAttribute = $derived((() => {
|
|
if (!activeAttrId) return null
|
|
if (activeGroup) return activeGroup.attributes.find(a => a.id === activeAttrId) ?? null
|
|
if (activeEntity) return activeEntity.attributes.find(a => a.id === activeAttrId) ?? null
|
|
return collection.attributes.find(a => a.id === activeAttrId) ?? null
|
|
})())
|
|
const activeOption = $derived(activeOptionId ? collection.options[activeOptionId] ?? null : null)
|
|
|
|
// ── Navigation ─────────────────────────────────────────────────────────────
|
|
|
|
function setPanel(panel: EditorPanel) {
|
|
activePanel = panel
|
|
}
|
|
|
|
function setActiveEntity(id: EntityID | null) {
|
|
activeEntityId = id
|
|
activeGroupId = null
|
|
activeAttrId = null
|
|
}
|
|
|
|
function setActiveGroup(id: GroupID | null) {
|
|
activeGroupId = id
|
|
activeAttrId = null
|
|
}
|
|
|
|
function setActiveAttribute(id: AttrID | null) {
|
|
activeAttrId = id
|
|
}
|
|
|
|
function setActiveEntityAttribute(id: AttrID | null) {
|
|
activeGroupId = null
|
|
activeAttrId = id
|
|
}
|
|
|
|
function setActiveOption(id: OptionID | null) {
|
|
activeOptionId = id
|
|
}
|
|
|
|
// ── Collection meta ────────────────────────────────────────────────────────
|
|
|
|
function updateCollectionMeta(patch: Partial<Pick<Collection, 'code' | 'name' | 'description' | 'basePath' | 'langs'>>) {
|
|
collection = { ...collection, ...patch }
|
|
isDirty = true
|
|
}
|
|
|
|
// ── Options library ────────────────────────────────────────────────────────
|
|
|
|
function addOption(): OptionID {
|
|
const id = uid('op') as OptionID
|
|
collection = {
|
|
...collection,
|
|
options: { ...collection.options, [id]: emptyOption(id) },
|
|
}
|
|
activeOptionId = id
|
|
markDirty()
|
|
return id
|
|
}
|
|
|
|
function updateOption(id: OptionID, patch: Partial<OptionDefinition>) {
|
|
const opt = collection.options[id]
|
|
if (!opt) return
|
|
collection = {
|
|
...collection,
|
|
options: { ...collection.options, [id]: { ...opt, ...patch } },
|
|
}
|
|
markDirty()
|
|
}
|
|
|
|
function deleteOption(id: OptionID) {
|
|
const { [id]: _, ...rest } = collection.options
|
|
collection = { ...collection, options: rest as Record<OptionID, OptionDefinition> }
|
|
if (activeOptionId === id) activeOptionId = null
|
|
markDirty()
|
|
}
|
|
|
|
function duplicateOption(id: OptionID): OptionID {
|
|
const opt = collection.options[id]
|
|
if (!opt) return id
|
|
const newId = uid('op') as OptionID
|
|
const name = typeof opt.name === 'string'
|
|
? opt.name + ' (copia)'
|
|
: { ...(opt.name as any), es: ((opt.name as any).es ?? '') + ' (copia)' }
|
|
collection = {
|
|
...collection,
|
|
options: { ...collection.options, [newId]: { ...opt, id: newId, code: opt.code + '_copy', name } },
|
|
}
|
|
activeOptionId = newId
|
|
return newId
|
|
}
|
|
|
|
// ── Entities ───────────────────────────────────────────────────────────────
|
|
|
|
function addEntity(): EntityID {
|
|
const id = uid('en') as EntityID
|
|
collection = {
|
|
...collection,
|
|
entities: { ...collection.entities, [id]: emptyEntity(id) },
|
|
}
|
|
activeEntityId = id
|
|
activeGroupId = null
|
|
activeAttrId = null
|
|
markDirty()
|
|
return id
|
|
}
|
|
|
|
function updateEntity(id: EntityID, patch: Record<string, unknown>) {
|
|
const entity = collection.entities[id]
|
|
if (!entity) return
|
|
collection = {
|
|
...collection,
|
|
entities: { ...collection.entities, [id]: { ...entity, ...patch } as Entity },
|
|
}
|
|
markDirty()
|
|
}
|
|
|
|
function deleteEntity(id: EntityID) {
|
|
const { [id]: _, ...rest } = collection.entities
|
|
collection = { ...collection, entities: rest as Record<EntityID, Entity> }
|
|
if (activeEntityId === id) setActiveEntity(null)
|
|
markDirty()
|
|
}
|
|
|
|
// ── Groups ─────────────────────────────────────────────────────────────────
|
|
|
|
function addGroup(entityId: EntityID, kind: 'fixed' | 'optional' = 'fixed'): GroupID {
|
|
const entity = collection.entities[entityId]
|
|
if (!entity) return '' as GroupID
|
|
const id = uid('gr') as GroupID
|
|
collection = {
|
|
...collection,
|
|
entities: {
|
|
...collection.entities,
|
|
[entityId]: {
|
|
...entity,
|
|
groups: { ...entity.groups, [id]: emptyGroup(id, kind) },
|
|
},
|
|
},
|
|
}
|
|
activeGroupId = id
|
|
activeAttrId = null
|
|
markDirty()
|
|
return id
|
|
}
|
|
|
|
function updateGroup(entityId: EntityID, groupId: GroupID, patch: Record<string, unknown>) {
|
|
const entity = collection.entities[entityId]
|
|
const group = entity?.groups[groupId]
|
|
if (!entity || !group) return
|
|
collection = {
|
|
...collection,
|
|
entities: {
|
|
...collection.entities,
|
|
[entityId]: {
|
|
...entity,
|
|
groups: { ...entity.groups, [groupId]: { ...group, ...patch } as Group },
|
|
},
|
|
},
|
|
}
|
|
markDirty()
|
|
}
|
|
|
|
function deleteGroup(entityId: EntityID, groupId: GroupID) {
|
|
const entity = collection.entities[entityId]
|
|
if (!entity) return
|
|
const { [groupId]: _, ...rest } = entity.groups
|
|
collection = {
|
|
...collection,
|
|
entities: {
|
|
...collection.entities,
|
|
[entityId]: { ...entity, groups: rest as Record<GroupID, Group> },
|
|
},
|
|
}
|
|
if (activeGroupId === groupId) { activeGroupId = null; activeAttrId = null }
|
|
markDirty()
|
|
}
|
|
|
|
// ── Attributes ─────────────────────────────────────────────────────────────
|
|
|
|
function addAttribute(entityId: EntityID, groupId?: GroupID): AttrID {
|
|
const id = uid('at') as AttrID
|
|
const attr = emptyAttribute(id)
|
|
|
|
if (groupId) {
|
|
const entity = collection.entities[entityId]
|
|
const group = entity?.groups[groupId]
|
|
if (entity && group) {
|
|
collection = {
|
|
...collection,
|
|
entities: {
|
|
...collection.entities,
|
|
[entityId]: {
|
|
...entity,
|
|
groups: {
|
|
...entity.groups,
|
|
[groupId]: { ...group, attributes: [...group.attributes, attr] },
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
} else {
|
|
const entity = collection.entities[entityId]
|
|
if (entity) {
|
|
collection = {
|
|
...collection,
|
|
entities: {
|
|
...collection.entities,
|
|
[entityId]: { ...entity, attributes: [...entity.attributes, attr] },
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
activeAttrId = id
|
|
markDirty()
|
|
return id
|
|
}
|
|
|
|
function updateAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, patch: Record<string, unknown>) {
|
|
collection = produceAttr(collection, entityId, groupId, attrId, attr => ({ ...attr, ...patch }) as Attribute)
|
|
markDirty()
|
|
}
|
|
|
|
function deleteAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID) {
|
|
collection = produceAttr(collection, entityId, groupId, attrId, null)
|
|
if (activeAttrId === attrId) activeAttrId = null
|
|
markDirty()
|
|
}
|
|
|
|
// ── Attribute options ──────────────────────────────────────────────────────
|
|
|
|
function addOptionToAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, optionId: OptionID) {
|
|
collection = produceAttr(collection, entityId, groupId, attrId, attr => {
|
|
if (attr.type === 'fixed' || attr.type === 'literal') return attr
|
|
if (attr.options.some(o => o.optionId === optionId)) return attr
|
|
const newOpt: AttributeOption = { optionId }
|
|
const options = [...attr.options, newOpt]
|
|
const updated = { ...attr, options }
|
|
if (!attr.default && options.length === 1) (updated as any).default = optionId
|
|
return updated
|
|
})
|
|
markDirty()
|
|
}
|
|
|
|
function removeOptionFromAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, optionId: OptionID) {
|
|
collection = produceAttr(collection, entityId, groupId, attrId, attr => {
|
|
if (attr.type === 'fixed' || attr.type === 'literal' ) return attr
|
|
const options = attr.options.filter(o => o.optionId !== optionId)
|
|
const updated = { ...attr, options }
|
|
if ((attr as any).default === optionId) (updated as any).default = options[0]?.optionId ?? ''
|
|
return updated
|
|
})
|
|
markDirty()
|
|
}
|
|
|
|
// ── Catalog attributes ─────────────────────────────────────────────────────
|
|
|
|
function addCatalogAttribute(): AttrID {
|
|
const id = uid('at') as AttrID
|
|
const attr = emptyLiteralAttribute(id)
|
|
collection = { ...collection, attributes: [...collection.attributes, attr] }
|
|
activeAttrId = id
|
|
activeEntityId = null
|
|
activeGroupId = null
|
|
markDirty()
|
|
return id
|
|
}
|
|
|
|
function updateCatalogAttribute(attrId: AttrID, patch: Record<string, unknown>) {
|
|
collection = {
|
|
...collection,
|
|
attributes: collection.attributes.map(a =>
|
|
a.id === attrId ? { ...a, ...patch } as Attribute : a
|
|
),
|
|
}
|
|
}
|
|
|
|
function deleteCatalogAttribute(attrId: AttrID) {
|
|
collection = { ...collection, attributes: collection.attributes.filter(a => a.id !== attrId) }
|
|
if (activeAttrId === attrId) activeAttrId = null
|
|
}
|
|
|
|
// ── Rules ──────────────────────────────────────────────────────────────────
|
|
|
|
function addRule(
|
|
entityId: EntityID,
|
|
groupId: GroupID | undefined,
|
|
attrId: AttrID | undefined,
|
|
rule: Rule,
|
|
) {
|
|
if (attrId) {
|
|
collection = produceAttr(collection, entityId, groupId, attrId, (a) => ({
|
|
...a,
|
|
rules: [...((a as any).rules ?? []), rule],
|
|
}))
|
|
} else if (groupId) {
|
|
collection = produceGroup(collection, entityId, groupId, (g) => ({
|
|
...g,
|
|
rules: [...((g as any).rules ?? []), rule],
|
|
}))
|
|
} else {
|
|
collection = produceEntity(collection, entityId, (e) => ({
|
|
...e,
|
|
rules: [...(e.rules ?? []), rule],
|
|
}))
|
|
}
|
|
isDirty = true
|
|
}
|
|
|
|
function updateRule(
|
|
entityId: EntityID,
|
|
groupId: GroupID | undefined,
|
|
attrId: AttrID | undefined,
|
|
ruleId: string,
|
|
patch: Partial<Rule>,
|
|
) {
|
|
const mapRules = (rules: Rule[]) =>
|
|
rules.map(r => (r.id === ruleId ? { ...r, ...patch } : r))
|
|
|
|
if (attrId) {
|
|
collection = produceAttr(collection, entityId, groupId, attrId, (a) => ({
|
|
...a,
|
|
rules: mapRules((a as any).rules ?? []),
|
|
}))
|
|
} else if (groupId) {
|
|
collection = produceGroup(collection, entityId, groupId, (g) => ({
|
|
...g,
|
|
rules: mapRules((g as any).rules ?? []),
|
|
}))
|
|
} else {
|
|
collection = produceEntity(collection, entityId, (e) => ({
|
|
...e,
|
|
rules: mapRules(e.rules ?? []),
|
|
}))
|
|
}
|
|
isDirty = true
|
|
}
|
|
|
|
function deleteRule(
|
|
entityId: EntityID,
|
|
groupId: GroupID | undefined,
|
|
attrId: AttrID | undefined,
|
|
ruleId: string,
|
|
) {
|
|
const filterRules = (rules: Rule[]) =>
|
|
rules.filter(r => r.id !== ruleId)
|
|
|
|
if (attrId) {
|
|
collection = produceAttr(collection, entityId, groupId, attrId, (a) => ({
|
|
...a,
|
|
rules: filterRules((a as any).rules ?? []),
|
|
}))
|
|
} else if (groupId) {
|
|
collection = produceGroup(collection, entityId, groupId, (g) => ({
|
|
...g,
|
|
rules: filterRules((g as any).rules ?? []),
|
|
}))
|
|
} else {
|
|
collection = produceEntity(collection, entityId, (e) => ({
|
|
...e,
|
|
rules: filterRules(e.rules ?? []),
|
|
}))
|
|
}
|
|
isDirty = true
|
|
}
|
|
|
|
// ── Import / Export ────────────────────────────────────────────────────────
|
|
|
|
function exportToJson(): string {
|
|
return JSON.stringify(collection, null, 2)
|
|
}
|
|
|
|
function importFromJson(json: string) {
|
|
const parsed = JSON.parse(json)
|
|
collection = parsed
|
|
activeEntityId = null
|
|
activeGroupId = null
|
|
activeAttrId = null
|
|
activeOptionId = null
|
|
isDirty = false
|
|
}
|
|
|
|
function reset() {
|
|
collection = emptyCollection()
|
|
activeEntityId = null
|
|
activeGroupId = null
|
|
activeAttrId = null
|
|
activeOptionId = null
|
|
isDirty = false
|
|
}
|
|
|
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
|
|
return {
|
|
// State (getters)
|
|
get collection() { return collection },
|
|
get validation() { return validation },
|
|
get activePanel() { return activePanel },
|
|
get isDirty() { return isDirty },
|
|
get canPreview() { return canPreview },
|
|
get activeEntityId() { return activeEntityId },
|
|
get activeGroupId() { return activeGroupId },
|
|
get activeAttrId() { return activeAttrId },
|
|
get activeOptionId() { return activeOptionId },
|
|
get activeEntity() { return activeEntity },
|
|
get activeGroup() { return activeGroup },
|
|
get activeAttribute() { return activeAttribute },
|
|
get activeOption() { return activeOption },
|
|
|
|
// Navigation
|
|
setPanel,
|
|
setActiveEntity,
|
|
setActiveGroup,
|
|
setActiveAttribute,
|
|
setActiveEntityAttribute,
|
|
setActiveOption,
|
|
|
|
// Collection
|
|
updateCollectionMeta,
|
|
|
|
// Options
|
|
addOption,
|
|
updateOption,
|
|
deleteOption,
|
|
duplicateOption,
|
|
|
|
// Entities
|
|
addEntity,
|
|
updateEntity,
|
|
deleteEntity,
|
|
|
|
// Groups
|
|
addGroup,
|
|
updateGroup,
|
|
deleteGroup,
|
|
|
|
// Attributes
|
|
addAttribute,
|
|
updateAttribute,
|
|
deleteAttribute,
|
|
addCatalogAttribute,
|
|
updateCatalogAttribute,
|
|
deleteCatalogAttribute,
|
|
|
|
// Attribute options
|
|
addOptionToAttribute,
|
|
removeOptionFromAttribute,
|
|
|
|
// Rules
|
|
addRule,
|
|
updateRule,
|
|
deleteRule,
|
|
|
|
// Import/Export
|
|
exportToJson,
|
|
importFromJson,
|
|
reset,
|
|
}
|
|
}
|
|
|
|
export type EditorStore = ReturnType<typeof createEditorStore>
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// HELPERS INTERNOS — produce inmutable
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
function produceAttr(
|
|
collection: Collection,
|
|
entityId: EntityID,
|
|
groupId: GroupID | undefined,
|
|
attrId: AttrID,
|
|
fn: ((attr: Attribute) => Attribute) | null,
|
|
): Collection {
|
|
const entity = collection.entities[entityId]
|
|
if (!entity) return collection
|
|
|
|
function mapAttrs(attrs: Attribute[]): Attribute[] {
|
|
if (fn === null) return attrs.filter(a => a.id !== attrId)
|
|
return attrs.map(a => a.id === attrId ? fn(a) as Attribute : a)
|
|
}
|
|
|
|
if (groupId) {
|
|
const group = entity.groups[groupId]
|
|
if (!group) return collection
|
|
return {
|
|
...collection,
|
|
entities: {
|
|
...collection.entities,
|
|
[entityId]: {
|
|
...entity,
|
|
groups: {
|
|
...entity.groups,
|
|
[groupId]: { ...group, attributes: mapAttrs(group.attributes) },
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
return {
|
|
...collection,
|
|
entities: {
|
|
...collection.entities,
|
|
[entityId]: { ...entity, attributes: mapAttrs(entity.attributes) },
|
|
},
|
|
}
|
|
}
|
|
|
|
function produceEntity(
|
|
collection: Collection,
|
|
entityId: EntityID,
|
|
fn: (entity: Entity) => Entity,
|
|
): Collection {
|
|
const entity = collection.entities[entityId]
|
|
if (!entity) return collection
|
|
return {
|
|
...collection,
|
|
entities: { ...collection.entities, [entityId]: fn(entity) },
|
|
}
|
|
}
|
|
|
|
function produceGroup(
|
|
collection: Collection,
|
|
entityId: EntityID,
|
|
groupId: GroupID,
|
|
fn: (group: Group) => Group,
|
|
): Collection {
|
|
const entity = collection.entities[entityId]
|
|
if (!entity) return collection
|
|
const group = entity.groups[groupId]
|
|
if (!group) return collection
|
|
return {
|
|
...collection,
|
|
entities: {
|
|
...collection.entities,
|
|
[entityId]: {
|
|
...entity,
|
|
groups: { ...entity.groups, [groupId]: fn(group) },
|
|
},
|
|
},
|
|
}
|
|
} |