parent
850df4f151
commit
ef29d9e5dd
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VICE — Catalog Editor</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="/src/editor/main.ts"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* ============================================================================
|
||||
* VICE Catalog Editor — Root Component
|
||||
* ============================================================================
|
||||
*
|
||||
* Provee el EditorStore via contexto Svelte.
|
||||
* Todos los componentes hijos lo consumen con getContext('editor').
|
||||
*/
|
||||
import './app.css';
|
||||
|
||||
import { setContext } from 'svelte';
|
||||
import { EditorStore } from '@/editor/stores/editor.svelte';
|
||||
import EditorLayout from '@/editor/components/layout/EditorLayout.svelte';
|
||||
|
||||
const editor = new EditorStore();
|
||||
setContext('editor', editor);
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<EditorLayout />
|
||||
</main>
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
/* Smooth scroll para navegación */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* ============================================================================
|
||||
* EDITOR LAYOUT
|
||||
* ============================================================================
|
||||
*
|
||||
* Layout principal del editor:
|
||||
* TopBar | Sidebar | MainPanel
|
||||
*/
|
||||
|
||||
import { getContext } from 'svelte';
|
||||
import type { EditorStore } from '@/editor/stores/editor.svelte';
|
||||
import TopBar from './TopBar.svelte';
|
||||
import Sidebar from './Sidebar.svelte';
|
||||
/*
|
||||
import CatalogPanel from '../catalog/CatalogPanel.svelte';
|
||||
import OptionsPanel from '../options/OptionsPanel.svelte';
|
||||
import ObjectsPanel from '../objects/ObjectsPanel.svelte';
|
||||
import RulesPanel from '../rules/RulesPanel.svelte';
|
||||
import PreviewPanel from '../preview/PreviewPanel.svelte';
|
||||
import ExportPanel from '../export/ExportPanel.svelte';
|
||||
*/
|
||||
const editor = getContext<EditorStore>('editor');
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-screen bg-gray-950 text-gray-100 overflow-hidden">
|
||||
|
||||
<!-- Top Bar -->
|
||||
<TopBar />
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<Sidebar />
|
||||
|
||||
<!-- Main Panel -->
|
||||
<main class="flex-1 overflow-auto">
|
||||
<!--
|
||||
{#if editor.activePanel === 'catalog'}
|
||||
<CatalogPanel />
|
||||
{:else if editor.activePanel === 'options'}
|
||||
<OptionsPanel />
|
||||
{:else if editor.activePanel === 'objects'}
|
||||
<ObjectsPanel />
|
||||
{:else if editor.activePanel === 'rules'}
|
||||
<RulesPanel />
|
||||
{:else if editor.activePanel === 'preview'}
|
||||
<PreviewPanel />
|
||||
{:else if editor.activePanel === 'export'}
|
||||
<ExportPanel />
|
||||
{/if}
|
||||
-->
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { EditorStore } from '@/editor/stores/editor.svelte';
|
||||
|
||||
const editor = getContext<EditorStore>('editor');
|
||||
</script>
|
||||
|
||||
<header class="flex items-center justify-between px-6 py-3 bg-gray-900 border-b border-gray-800 shrink-0">
|
||||
|
||||
<!-- Logo + nombre catálogo -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-bold tracking-widest text-indigo-400 uppercase">VICE</span>
|
||||
<span class="text-gray-600">|</span>
|
||||
<span class="text-sm text-gray-300">
|
||||
{editor.catalog.name.es || 'Nuevo catálogo'}
|
||||
</span>
|
||||
{#if editor.isDirty}
|
||||
<span class="text-xs text-amber-400">● Sin guardar</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
<!-- Validación rápida -->
|
||||
{#if !editor.validation.valid}
|
||||
<span class="flex items-center gap-1 text-xs text-red-400">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-red-400 inline-block"></span>
|
||||
{editor.validation.errors.length} error{editor.validation.errors.length !== 1 ? 'es' : ''}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-1 text-xs text-emerald-400">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block"></span>
|
||||
Válido
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- Preview -->
|
||||
<button
|
||||
onclick={() => editor.setPanel('preview')}
|
||||
disabled={!editor.canPreview}
|
||||
class="px-3 py-1.5 text-xs rounded-md border border-gray-700 text-gray-300
|
||||
hover:bg-gray-800 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Vista previa
|
||||
</button>
|
||||
|
||||
<!-- Export -->
|
||||
<button
|
||||
onclick={() => editor.setPanel('export')}
|
||||
class="px-3 py-1.5 text-xs rounded-md bg-indigo-600 hover:bg-indigo-500
|
||||
text-white font-medium transition-colors"
|
||||
>
|
||||
Exportar JSON
|
||||
</button>
|
||||
|
||||
<!-- Reset -->
|
||||
<button
|
||||
onclick={() => { if (confirm('¿Resetear el editor?')) editor.reset(); }}
|
||||
class="px-3 py-1.5 text-xs rounded-md border border-gray-700 text-gray-500
|
||||
hover:text-red-400 hover:border-red-800 transition-colors"
|
||||
>
|
||||
Nuevo
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { EditorStore } from '@/editor/stores/editor.svelte';
|
||||
|
||||
const editor = getContext<EditorStore>('editor');
|
||||
</script>
|
||||
|
||||
<header class="flex items-center justify-between px-6 py-3 bg-gray-900 border-b border-gray-800 shrink-0">
|
||||
|
||||
<!-- Logo + nombre catálogo -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-bold tracking-widest text-indigo-400 uppercase">VICE</span>
|
||||
<span class="text-gray-600">|</span>
|
||||
<span class="text-sm text-gray-300">
|
||||
{editor.catalog.name.es || 'Nuevo catálogo'}
|
||||
</span>
|
||||
{#if editor.isDirty}
|
||||
<span class="text-xs text-amber-400">● Sin guardar</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
<!-- Validación rápida -->
|
||||
{#if !editor.validation.valid}
|
||||
<span class="flex items-center gap-1 text-xs text-red-400">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-red-400 inline-block"></span>
|
||||
{editor.validation.errors.length} error{editor.validation.errors.length !== 1 ? 'es' : ''}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-1 text-xs text-emerald-400">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block"></span>
|
||||
Válido
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- Preview -->
|
||||
<button
|
||||
onclick={() => editor.setPanel('preview')}
|
||||
disabled={!editor.canPreview}
|
||||
class="px-3 py-1.5 text-xs rounded-md border border-gray-700 text-gray-300
|
||||
hover:bg-gray-800 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Vista previa
|
||||
</button>
|
||||
|
||||
<!-- Export -->
|
||||
<button
|
||||
onclick={() => editor.setPanel('export')}
|
||||
class="px-3 py-1.5 text-xs rounded-md bg-indigo-600 hover:bg-indigo-500
|
||||
text-white font-medium transition-colors"
|
||||
>
|
||||
Exportar JSON
|
||||
</button>
|
||||
|
||||
<!-- Reset -->
|
||||
<button
|
||||
onclick={() => { if (confirm('¿Resetear el editor?')) editor.reset(); }}
|
||||
class="px-3 py-1.5 text-xs rounded-md border border-gray-700 text-gray-500
|
||||
hover:text-red-400 hover:border-red-800 transition-colors"
|
||||
>
|
||||
Nuevo
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* ID GENERATOR
|
||||
* ============================================================================
|
||||
*
|
||||
* Genera IDs únicos con el prefijo correcto para cada entidad.
|
||||
* Formato: prefix:timestamp_random
|
||||
* Ejemplo: op:1709123456_a3f2
|
||||
*/
|
||||
|
||||
export function generateId(prefix: 'op' | 'at' | 'sc' | 'ob' | 'rl' | 'ct' | 'vw'): string {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).slice(2, 6);
|
||||
return `${prefix}:${timestamp}_${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un code limpio a partir de un nombre.
|
||||
* 'Suelo Parquet' → 'suelo_parquet'
|
||||
*/
|
||||
export function generateCode(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '') // eliminar acentos
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_|_$/g, '');
|
||||
}
|
||||
@ -0,0 +1,209 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* CATALOG VALIDATOR
|
||||
* ============================================================================
|
||||
*
|
||||
* Valida un EditorCatalog y devuelve errores y warnings con path.
|
||||
*/
|
||||
|
||||
import type {
|
||||
EditorCatalog,
|
||||
EditorOption,
|
||||
EditorObject,
|
||||
EditorSection,
|
||||
EditorAttribute,
|
||||
EditorRule,
|
||||
ValidationResult,
|
||||
ValidationError,
|
||||
} from '../types/editor';
|
||||
|
||||
export function validateCatalog(catalog: EditorCatalog): ValidationResult {
|
||||
const errors : ValidationError[] = [];
|
||||
const warnings: ValidationError[] = [];
|
||||
|
||||
const push = (severity: 'error' | 'warning', path: string, message: string) => {
|
||||
(severity === 'error' ? errors : warnings).push({ path, message, severity });
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Catálogo
|
||||
// -------------------------------------------------------------------------
|
||||
if (!catalog.code) push('error', 'catalog.code', 'El catálogo necesita un código');
|
||||
if (!catalog.name.es) push('error', 'catalog.name.es', 'El catálogo necesita nombre en español');
|
||||
if (!catalog.basePath) push('warning', 'catalog.basePath', 'Sin basePath — las imágenes no se resolverán');
|
||||
if (catalog.objects.length === 0) push('warning', 'catalog.objects', 'El catálogo no tiene objetos');
|
||||
if (catalog.options.length === 0) push('warning', 'catalog.options', 'El catálogo no tiene opciones');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Opciones
|
||||
// -------------------------------------------------------------------------
|
||||
const optionIds = new Set(catalog.options.map(o => o.id));
|
||||
const optionCodes = new Set<string>();
|
||||
|
||||
for (const opt of catalog.options) {
|
||||
const p = `options.${opt.id}`;
|
||||
if (!opt.code) push('error', `${p}.code`, `Opción "${opt.id}" sin código`);
|
||||
if (!opt.name.es) push('error', `${p}.name.es`, `Opción "${opt.code || opt.id}" sin nombre`);
|
||||
|
||||
if (opt.code && optionCodes.has(opt.code)) {
|
||||
push('error', `${p}.code`, `Código de opción duplicado: "${opt.code}"`);
|
||||
}
|
||||
if (opt.code) optionCodes.add(opt.code);
|
||||
|
||||
if (opt.pricing.type === 'fixed' && opt.pricing.baseAmount < 0) {
|
||||
push('warning', `${p}.pricing`, `Precio negativo en opción "${opt.code}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Objetos
|
||||
// -------------------------------------------------------------------------
|
||||
const objectCodes = new Set<string>();
|
||||
|
||||
for (const obj of catalog.objects) {
|
||||
const p = `objects.${obj.id}`;
|
||||
if (!obj.code) push('error', `${p}.code`, `Objeto "${obj.id}" sin código`);
|
||||
if (!obj.name.es) push('error', `${p}.name.es`, `Objeto "${obj.code || obj.id}" sin nombre`);
|
||||
|
||||
if (obj.code && objectCodes.has(obj.code)) {
|
||||
push('error', `${p}.code`, `Código de objeto duplicado: "${obj.code}"`);
|
||||
}
|
||||
if (obj.code) objectCodes.add(obj.code);
|
||||
|
||||
if (obj.sections.length === 0) {
|
||||
push('warning', `${p}.sections`, `Objeto "${obj.code}" sin secciones`);
|
||||
}
|
||||
|
||||
// Secciones
|
||||
for (const sec of obj.sections) {
|
||||
validateSection(sec, `${p}.sections.${sec.id}`, optionIds, push);
|
||||
}
|
||||
|
||||
// Atributos globales
|
||||
for (const attr of obj.attributes) {
|
||||
validateAttribute(attr, `${p}.attributes.${attr.id}`, optionIds, push);
|
||||
}
|
||||
|
||||
// sectionOrder coherente
|
||||
for (const sid of obj.sectionOrder) {
|
||||
if (!obj.sections.find(s => s.id === sid)) {
|
||||
push('error', `${p}.sectionOrder`, `sectionOrder referencia sección inexistente: "${sid}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Reglas
|
||||
// -------------------------------------------------------------------------
|
||||
const allAttrIds = collectAllAttrIds(catalog);
|
||||
|
||||
for (const rule of catalog.rules) {
|
||||
const p = `rules.${rule.id}`;
|
||||
if (!rule.name.es) push('warning', `${p}.name`, `Regla "${rule.id}" sin nombre`);
|
||||
if (!rule.message.es) push('warning', `${p}.message`, `Regla "${rule.id}" sin mensaje`);
|
||||
|
||||
// Validar que los atributos referenciados existen
|
||||
const condAttrId = extractConditionAttrId(rule.condition);
|
||||
if (condAttrId && !allAttrIds.has(condAttrId)) {
|
||||
push('error', `${p}.condition`, `Condición referencia atributo inexistente: "${condAttrId}"`);
|
||||
}
|
||||
if (!allAttrIds.has(rule.action.targetAttrId)) {
|
||||
push('error', `${p}.action.targetAttr`, `Acción referencia atributo inexistente: "${rule.action.targetAttrId}"`);
|
||||
}
|
||||
|
||||
// Validar que las opciones referenciadas existen
|
||||
for (const optId of rule.action.values) {
|
||||
if (!optionIds.has(optId)) {
|
||||
push('error', `${p}.action.values`, `Acción referencia opción inexistente: "${optId}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
function validateSection(
|
||||
sec : EditorSection,
|
||||
path: string,
|
||||
optionIds: Set<string>,
|
||||
push: (severity: 'error' | 'warning', path: string, message: string) => void,
|
||||
): void {
|
||||
if (!sec.code) push('error', `${path}.code`, `Sección "${sec.id}" sin código`);
|
||||
if (!sec.name.es) push('error', `${path}.name.es`, `Sección "${sec.code || sec.id}" sin nombre`);
|
||||
|
||||
if (sec.kind === 'visual') {
|
||||
if (sec.views.length === 0) {
|
||||
push('warning', `${path}.views`, `Sección visual "${sec.code}" sin vistas`);
|
||||
}
|
||||
if (!sec.defaultView) {
|
||||
push('warning', `${path}.defaultView`, `Sección "${sec.code}" sin defaultView`);
|
||||
}
|
||||
for (const view of sec.views) {
|
||||
if (!view.template) {
|
||||
push('warning', `${path}.views.${view.id}.template`, `Vista "${view.code}" sin template`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sec.attributes.length === 0) {
|
||||
push('warning', `${path}.attributes`, `Sección "${sec.code}" sin atributos`);
|
||||
}
|
||||
|
||||
for (const attr of sec.attributes) {
|
||||
validateAttribute(attr, `${path}.attributes.${attr.id}`, optionIds, push);
|
||||
}
|
||||
}
|
||||
|
||||
function validateAttribute(
|
||||
attr : EditorAttribute,
|
||||
path : string,
|
||||
optionIds: Set<string>,
|
||||
push : (severity: 'error' | 'warning', path: string, message: string) => void,
|
||||
): void {
|
||||
if (!attr.code) push('error', `${path}.code`, `Atributo "${attr.id}" sin código`);
|
||||
if (!attr.name.es) push('error', `${path}.name.es`, `Atributo "${attr.code || attr.id}" sin nombre`);
|
||||
|
||||
if (attr.type === 'dynamic') {
|
||||
if (attr.options.length === 0) {
|
||||
push('warning', `${path}.options`, `Atributo "${attr.code}" sin opciones`);
|
||||
}
|
||||
for (const attrOpt of attr.options) {
|
||||
if (!optionIds.has(attrOpt.optionId)) {
|
||||
push('error', `${path}.options`, `Atributo "${attr.code}" referencia opción inexistente: "${attrOpt.optionId}"`);
|
||||
}
|
||||
}
|
||||
if (attr.defaultValue && !attr.options.some(o => o.optionId === attr.defaultValue)) {
|
||||
push('error', `${path}.defaultValue`, `Atributo "${attr.code}": defaultValue no está en las opciones`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectAllAttrIds(catalog: EditorCatalog): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const obj of catalog.objects) {
|
||||
for (const attr of obj.attributes) ids.add(attr.id);
|
||||
for (const sec of obj.sections) {
|
||||
for (const attr of sec.attributes) ids.add(attr.id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function extractConditionAttrId(condition: EditorRule['condition']): string | null {
|
||||
if (condition.type === 'attribute_equals' || condition.type === 'attribute_in') {
|
||||
return condition.attrId;
|
||||
}
|
||||
if (condition.type === 'and' || condition.type === 'or') {
|
||||
return extractConditionAttrId(condition.conditions[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* VICE Catalog Editor — Entry Point
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
import { mount } from 'svelte';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')!
|
||||
});
|
||||
|
||||
|
||||
|
||||
export default app;
|
||||
|
||||
@ -0,0 +1,587 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* EDITOR STORE — Svelte 5 (Runas)
|
||||
* ============================================================================
|
||||
*
|
||||
* Estado central del editor de catálogos.
|
||||
* Gestiona la navegación entre secciones del editor, el catálogo en edición,
|
||||
* validación en tiempo real y exportación a JSON.
|
||||
*/
|
||||
|
||||
import { generateId } from '../engines/id-generator.ts';
|
||||
import { validateCatalog } from '../engines/validator.ts';
|
||||
import type {
|
||||
EditorCatalog,
|
||||
EditorObject,
|
||||
EditorSection,
|
||||
EditorAttribute,
|
||||
EditorOption,
|
||||
EditorRule,
|
||||
ValidationResult,
|
||||
EditorPanel,
|
||||
} from '../types/editor';
|
||||
|
||||
// ============================================================================
|
||||
// EDITOR STORE
|
||||
// ============================================================================
|
||||
|
||||
export class EditorStore {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Navegación — panel activo
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Panel activo en el editor */
|
||||
activePanel = $state<EditorPanel>('catalog');
|
||||
|
||||
/** ID del objeto activo en el editor de objetos */
|
||||
activeObjectId = $state<string | null>(null);
|
||||
|
||||
/** ID de la sección activa en el editor de secciones */
|
||||
activeSectionId = $state<string | null>(null);
|
||||
|
||||
/** ID del atributo activo */
|
||||
activeAttributeId = $state<string | null>(null);
|
||||
|
||||
/** ID de la opción activa en la librería */
|
||||
activeOptionId = $state<string | null>(null);
|
||||
|
||||
/** ID de la regla activa */
|
||||
activeRuleId = $state<string | null>(null);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Catálogo en edición
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
catalog = $state<EditorCatalog>(this.#emptyCatalog());
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Estado del editor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** ¿Hay cambios sin guardar? */
|
||||
isDirty = $state<boolean>(false);
|
||||
|
||||
/** Resultado de la última validación */
|
||||
validation = $state<ValidationResult>({ valid: true, errors: [], warnings: [] });
|
||||
|
||||
/** ¿Está el panel de preview abierto? */
|
||||
previewOpen = $state<boolean>(false);
|
||||
|
||||
/** ¿Está el panel de export abierto? */
|
||||
exportOpen = $state<boolean>(false);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Derived — navegación
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
activeObject = $derived(
|
||||
this.activeObjectId
|
||||
? this.catalog.objects.find(o => o.id === this.activeObjectId) ?? null
|
||||
: null
|
||||
);
|
||||
|
||||
activeSection = $derived(
|
||||
this.activeObject && this.activeSectionId
|
||||
? this.activeObject.sections.find(s => s.id === this.activeSectionId) ?? null
|
||||
: null
|
||||
);
|
||||
|
||||
activeAttribute = $derived(
|
||||
this.activeSection && this.activeAttributeId
|
||||
? this.activeSection.attributes.find(a => a.id === this.activeAttributeId) ?? null
|
||||
: null
|
||||
);
|
||||
|
||||
activeOption = $derived(
|
||||
this.activeOptionId
|
||||
? this.catalog.options.find(o => o.id === this.activeOptionId) ?? null
|
||||
: null
|
||||
);
|
||||
|
||||
activeRule = $derived(
|
||||
this.activeRuleId
|
||||
? this.catalog.rules.find(r => r.id === this.activeRuleId) ?? null
|
||||
: null
|
||||
);
|
||||
|
||||
/** Opciones disponibles para usar en atributos — toda la librería */
|
||||
optionLibrary = $derived(this.catalog.options);
|
||||
|
||||
/** ¿El catálogo tiene suficiente información para previsualizarse? */
|
||||
canPreview = $derived(
|
||||
this.catalog.objects.length > 0 &&
|
||||
this.catalog.options.length > 0
|
||||
);
|
||||
|
||||
/** JSON exportable del catálogo */
|
||||
exportJson = $derived(
|
||||
this.exportOpen ? this.#buildExportJson() : null
|
||||
);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Navegación — API pública
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
setPanel(panel: EditorPanel): void {
|
||||
this.activePanel = panel;
|
||||
this.activeSectionId = null;
|
||||
this.activeAttributeId = null;
|
||||
}
|
||||
|
||||
setActiveObject(id: string | null): void {
|
||||
this.activeObjectId = id;
|
||||
this.activeSectionId = null;
|
||||
this.activeAttributeId = null;
|
||||
}
|
||||
|
||||
setActiveSection(id: string | null): void {
|
||||
this.activeSectionId = id;
|
||||
this.activeAttributeId = null;
|
||||
}
|
||||
|
||||
setActiveAttribute(id: string | null): void {
|
||||
this.activeAttributeId = id;
|
||||
}
|
||||
|
||||
setActiveOption(id: string | null): void {
|
||||
this.activeOptionId = id;
|
||||
}
|
||||
|
||||
setActiveRule(id: string | null): void {
|
||||
this.activeRuleId = id;
|
||||
}
|
||||
|
||||
togglePreview(): void { this.previewOpen = !this.previewOpen; }
|
||||
toggleExport(): void {
|
||||
this.exportOpen = !this.exportOpen;
|
||||
if (this.exportOpen) this.validate();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Catálogo — metadatos
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
updateCatalogMeta(patch: Partial<Pick<EditorCatalog, 'name' | 'code' | 'description' | 'basePath'>>): void {
|
||||
Object.assign(this.catalog, patch);
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Opciones — CRUD
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addOption(): EditorOption {
|
||||
const option: EditorOption = {
|
||||
id : generateId('op'),
|
||||
code : '',
|
||||
name : { es: '', en: '' },
|
||||
description: { es: '', en: '' },
|
||||
pricing : { type: 'fixed', baseAmount: 0 },
|
||||
tags : [],
|
||||
};
|
||||
this.catalog.options.push(option);
|
||||
this.activeOptionId = option.id;
|
||||
this.#markDirty();
|
||||
return option;
|
||||
}
|
||||
|
||||
updateOption(id: string, patch: Partial<EditorOption>): void {
|
||||
const idx = this.catalog.options.findIndex(o => o.id === id);
|
||||
if (idx === -1) return;
|
||||
Object.assign(this.catalog.options[idx], patch);
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
deleteOption(id: string): void {
|
||||
this.catalog.options = this.catalog.options.filter(o => o.id !== id);
|
||||
if (this.activeOptionId === id) this.activeOptionId = null;
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
duplicateOption(id: string): void {
|
||||
const src = this.catalog.options.find(o => o.id === id);
|
||||
if (!src) return;
|
||||
const copy: EditorOption = {
|
||||
...structuredClone(src),
|
||||
id : generateId('op'),
|
||||
code: src.code + '_copy',
|
||||
name: { es: src.name.es + ' (copia)', en: src.name.en + ' (copy)' },
|
||||
};
|
||||
this.catalog.options.push(copy);
|
||||
this.activeOptionId = copy.id;
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Objetos — CRUD
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addObject(): EditorObject {
|
||||
const object: EditorObject = {
|
||||
id : generateId('ob'),
|
||||
code : '',
|
||||
name : { es: '', en: '' },
|
||||
description : { es: '', en: '' },
|
||||
category : '',
|
||||
basePath : '',
|
||||
sections : [],
|
||||
sectionOrder: [],
|
||||
attributes : [],
|
||||
};
|
||||
this.catalog.objects.push(object);
|
||||
this.activeObjectId = object.id;
|
||||
this.activeSectionId = null;
|
||||
this.#markDirty();
|
||||
return object;
|
||||
}
|
||||
|
||||
updateObject(id: string, patch: Partial<EditorObject>): void {
|
||||
const obj = this.catalog.objects.find(o => o.id === id);
|
||||
if (!obj) return;
|
||||
Object.assign(obj, patch);
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
deleteObject(id: string): void {
|
||||
this.catalog.objects = this.catalog.objects.filter(o => o.id !== id);
|
||||
if (this.activeObjectId === id) {
|
||||
this.activeObjectId = null;
|
||||
this.activeSectionId = null;
|
||||
}
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
reorderSections(objectId: string, order: string[]): void {
|
||||
const obj = this.catalog.objects.find(o => o.id === objectId);
|
||||
if (!obj) return;
|
||||
obj.sectionOrder = order;
|
||||
obj.sections = order
|
||||
.map(id => obj.sections.find(s => s.id === id))
|
||||
.filter((s): s is EditorSection => s !== undefined);
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Secciones — CRUD
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addSection(objectId: string): EditorSection {
|
||||
const obj = this.catalog.objects.find(o => o.id === objectId);
|
||||
if (!obj) throw new Error(`Object ${objectId} not found`);
|
||||
|
||||
const section: EditorSection = {
|
||||
id : generateId('sc'),
|
||||
code : '',
|
||||
kind : 'visual',
|
||||
name : { es: '', en: '' },
|
||||
description: { es: '', en: '' },
|
||||
attributes : [],
|
||||
views : [],
|
||||
defaultView: null,
|
||||
availability: { mode: 'required' },
|
||||
};
|
||||
|
||||
obj.sections.push(section);
|
||||
obj.sectionOrder.push(section.id);
|
||||
this.activeSectionId = section.id;
|
||||
this.#markDirty();
|
||||
return section;
|
||||
}
|
||||
|
||||
updateSection(objectId: string, sectionId: string, patch: Partial<EditorSection>): void {
|
||||
const section = this.#findSection(objectId, sectionId);
|
||||
if (!section) return;
|
||||
Object.assign(section, patch);
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
deleteSection(objectId: string, sectionId: string): void {
|
||||
const obj = this.catalog.objects.find(o => o.id === objectId);
|
||||
if (!obj) return;
|
||||
obj.sections = obj.sections.filter(s => s.id !== sectionId);
|
||||
obj.sectionOrder = obj.sectionOrder.filter(id => id !== sectionId);
|
||||
if (this.activeSectionId === sectionId) this.activeSectionId = null;
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Atributos — CRUD
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addAttribute(objectId: string, sectionId: string): EditorAttribute {
|
||||
const section = this.#findSection(objectId, sectionId);
|
||||
if (!section) throw new Error(`Section ${sectionId} not found`);
|
||||
|
||||
const attr: EditorAttribute = {
|
||||
id : generateId('at'),
|
||||
code : '',
|
||||
name : { es: '', en: '' },
|
||||
description : { es: '', en: '' },
|
||||
type : 'dynamic',
|
||||
dataType : 'reference',
|
||||
defaultValue: null,
|
||||
options : [],
|
||||
display : {
|
||||
uiVisible : true,
|
||||
affectsVisual: true,
|
||||
affectsPrice : true,
|
||||
},
|
||||
};
|
||||
|
||||
section.attributes.push(attr);
|
||||
this.activeAttributeId = attr.id;
|
||||
this.#markDirty();
|
||||
return attr;
|
||||
}
|
||||
|
||||
updateAttribute(objectId: string, sectionId: string, attrId: string, patch: Partial<EditorAttribute>): void {
|
||||
const section = this.#findSection(objectId, sectionId);
|
||||
const attr = section?.attributes.find(a => a.id === attrId);
|
||||
if (!attr) return;
|
||||
Object.assign(attr, patch);
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
deleteAttribute(objectId: string, sectionId: string, attrId: string): void {
|
||||
const section = this.#findSection(objectId, sectionId);
|
||||
if (!section) return;
|
||||
section.attributes = section.attributes.filter(a => a.id !== attrId);
|
||||
if (this.activeAttributeId === attrId) this.activeAttributeId = null;
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
/** Añade una opción de la librería a un atributo */
|
||||
addOptionToAttribute(objectId: string, sectionId: string, attrId: string, optionId: string): void {
|
||||
const section = this.#findSection(objectId, sectionId);
|
||||
const attr = section?.attributes.find(a => a.id === attrId);
|
||||
if (!attr) return;
|
||||
if (attr.options.some(o => o.optionId === optionId)) return; // ya existe
|
||||
attr.options.push({
|
||||
optionId,
|
||||
priority : attr.options.length + 1,
|
||||
quantity : null,
|
||||
dynamicExpression: null,
|
||||
});
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
removeOptionFromAttribute(objectId: string, sectionId: string, attrId: string, optionId: string): void {
|
||||
const section = this.#findSection(objectId, sectionId);
|
||||
const attr = section?.attributes.find(a => a.id === attrId);
|
||||
if (!attr) return;
|
||||
attr.options = attr.options.filter(o => o.optionId !== optionId);
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Reglas — CRUD
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addRule(): EditorRule {
|
||||
const rule: EditorRule = {
|
||||
id : generateId('rl'),
|
||||
name : { es: '', en: '' },
|
||||
condition: { type: 'attribute_equals', attrId: '', value: '' },
|
||||
action : { type: 'forbid', targetAttrId: '', values: [] },
|
||||
severity : 'error',
|
||||
priority : this.catalog.rules.length + 1,
|
||||
message : { es: '', en: '' },
|
||||
};
|
||||
this.catalog.rules.push(rule);
|
||||
this.activeRuleId = rule.id;
|
||||
this.#markDirty();
|
||||
return rule;
|
||||
}
|
||||
|
||||
updateRule(id: string, patch: Partial<EditorRule>): void {
|
||||
const rule = this.catalog.rules.find(r => r.id === id);
|
||||
if (!rule) return;
|
||||
Object.assign(rule, patch);
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
deleteRule(id: string): void {
|
||||
this.catalog.rules = this.catalog.rules.filter(r => r.id !== id);
|
||||
if (this.activeRuleId === id) this.activeRuleId = null;
|
||||
this.#markDirty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Validación y exportación
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
validate(): ValidationResult {
|
||||
this.validation = validateCatalog(this.catalog);
|
||||
return this.validation;
|
||||
}
|
||||
|
||||
exportToJson(): string {
|
||||
this.validate();
|
||||
return JSON.stringify(this.#buildExportJson(), null, 2);
|
||||
}
|
||||
|
||||
/** Importa un catálogo desde JSON */
|
||||
importFromJson(json: string): void {
|
||||
try {
|
||||
const parsed = JSON.parse(json);
|
||||
this.catalog = parsed as EditorCatalog;
|
||||
this.validate();
|
||||
this.isDirty = false;
|
||||
} catch (e) {
|
||||
this.validation = {
|
||||
valid : false,
|
||||
errors : [{ severity: 'error', path: 'root', message: 'JSON inválido' }],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Resetea el editor a un catálogo vacío */
|
||||
reset(): void {
|
||||
this.catalog = this.#emptyCatalog();
|
||||
this.activeObjectId = null;
|
||||
this.activeSectionId = null;
|
||||
this.activeAttributeId = null;
|
||||
this.activeOptionId = null;
|
||||
this.activeRuleId = null;
|
||||
this.activePanel = 'catalog';
|
||||
this.isDirty = false;
|
||||
this.validation = { valid: true, errors: [], warnings: [] };
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers privados
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
#findSection(objectId: string, sectionId: string): EditorSection | undefined {
|
||||
return this.catalog.objects
|
||||
.find(o => o.id === objectId)
|
||||
?.sections.find(s => s.id === sectionId);
|
||||
}
|
||||
|
||||
#markDirty(): void {
|
||||
this.isDirty = true;
|
||||
// Validación incremental en background
|
||||
this.validation = validateCatalog(this.catalog);
|
||||
}
|
||||
|
||||
#emptyCatalog(): EditorCatalog {
|
||||
return {
|
||||
id : generateId('ct'),
|
||||
code : '',
|
||||
name : { es: '', en: '' },
|
||||
description: { es: '', en: '' },
|
||||
basePath : '/renders/',
|
||||
options : [],
|
||||
objects : [],
|
||||
rules : [],
|
||||
tax : null,
|
||||
};
|
||||
}
|
||||
|
||||
#buildExportJson(): object {
|
||||
// Transforma el EditorCatalog al formato ConfigurationCatalog
|
||||
const options: Record<string, object> = {};
|
||||
for (const opt of this.catalog.options) {
|
||||
options[opt.id] = {
|
||||
id : opt.id,
|
||||
code : opt.code,
|
||||
name : opt.name,
|
||||
description: opt.description,
|
||||
pricing : opt.pricing.type === 'fixed'
|
||||
? { baseAmount: opt.pricing.baseAmount }
|
||||
: { baseAmount: 0, dynamicExpression: opt.pricing.expression },
|
||||
tags : opt.tags,
|
||||
};
|
||||
}
|
||||
|
||||
const objects: Record<string, object> = {};
|
||||
for (const obj of this.catalog.objects) {
|
||||
const sections: Record<string, object> = {};
|
||||
for (const sec of obj.sections) {
|
||||
sections[sec.id] = {
|
||||
kind : sec.kind,
|
||||
id : sec.id,
|
||||
code : sec.code,
|
||||
name : sec.name,
|
||||
description : sec.description,
|
||||
attributes : sec.attributes.map(a => ({
|
||||
id : a.id,
|
||||
code : a.code,
|
||||
name : a.name,
|
||||
description : a.description,
|
||||
type : a.type,
|
||||
dataType : a.dataType,
|
||||
defaultValue: a.defaultValue,
|
||||
options : a.options,
|
||||
display : a.display,
|
||||
})),
|
||||
views : Object.fromEntries(
|
||||
sec.views.map(v => [v.id, v])
|
||||
),
|
||||
defaultView : sec.defaultView,
|
||||
availability: sec.availability,
|
||||
};
|
||||
}
|
||||
|
||||
objects[obj.id] = {
|
||||
id : obj.id,
|
||||
code : obj.code,
|
||||
name : obj.name,
|
||||
description : obj.description,
|
||||
category : obj.category,
|
||||
basePath : obj.basePath || this.catalog.basePath,
|
||||
attributes : obj.attributes,
|
||||
sections,
|
||||
sectionOrder: obj.sectionOrder,
|
||||
};
|
||||
}
|
||||
|
||||
const rules: Record<string, object> = {};
|
||||
for (const rule of this.catalog.rules) {
|
||||
rules[rule.id] = {
|
||||
id : rule.id,
|
||||
name : rule.name,
|
||||
condition: this.#ruleConditionToJsonLogic(rule.condition),
|
||||
action : {
|
||||
type : rule.action.type,
|
||||
targetAttr: rule.action.targetAttrId,
|
||||
values : rule.action.values,
|
||||
},
|
||||
priority : rule.priority,
|
||||
severity : rule.severity,
|
||||
//affects : [rule.condition.attrId, rule.action.targetAttrId],
|
||||
message : rule.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id : this.catalog.id,
|
||||
code : this.catalog.code,
|
||||
name : this.catalog.name,
|
||||
description: this.catalog.description,
|
||||
basePath : this.catalog.basePath,
|
||||
options,
|
||||
objects,
|
||||
rules,
|
||||
...(this.catalog.tax ? { tax: this.catalog.tax } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
#ruleConditionToJsonLogic(condition: EditorRule['condition']): object {
|
||||
switch (condition.type) {
|
||||
case 'attribute_equals':
|
||||
return { '==': [{ var: `attributes.${condition.attrId.replace(':', '_')}` }, condition.value] };
|
||||
case 'attribute_in':
|
||||
return { 'in': [{ var: `attributes.${condition.attrId.replace(':', '_')}` }, condition.values] };
|
||||
case 'and':
|
||||
return { 'and': condition.conditions.map(c => this.#ruleConditionToJsonLogic(c)) };
|
||||
case 'or':
|
||||
return { 'or': condition.conditions.map(c => this.#ruleConditionToJsonLogic(c)) };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* EDITOR TYPES
|
||||
* ============================================================================
|
||||
*
|
||||
* Tipos del editor — versión "plana" del catálogo optimizada para edición.
|
||||
* Arrays en lugar de Records para facilitar reordenación y CRUD.
|
||||
* Se transforma a ConfigurationCatalog al exportar.
|
||||
*/
|
||||
|
||||
export type EditorPanel = 'catalog' | 'options' | 'objects' | 'rules' | 'preview' | 'export';
|
||||
|
||||
// ============================================================================
|
||||
// I18N
|
||||
// ============================================================================
|
||||
|
||||
export interface I18nField {
|
||||
es: string;
|
||||
en: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PRICING
|
||||
// ============================================================================
|
||||
|
||||
export type EditorPricing =
|
||||
| { type: 'fixed'; baseAmount: number }
|
||||
| { type: 'dynamic'; baseAmount: number; expression: object };
|
||||
|
||||
// ============================================================================
|
||||
// OPTIONS
|
||||
// ============================================================================
|
||||
|
||||
export interface EditorOption {
|
||||
id : string;
|
||||
code : string;
|
||||
name : I18nField;
|
||||
description: I18nField;
|
||||
pricing : EditorPricing;
|
||||
tags : string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ATTRIBUTE OPTION — referencia a opción en un atributo
|
||||
// ============================================================================
|
||||
|
||||
export interface EditorAttributeOption {
|
||||
optionId : string;
|
||||
priority : number;
|
||||
quantity : EditorQuantityConfig | null;
|
||||
dynamicExpression: object | null;
|
||||
}
|
||||
|
||||
export interface EditorQuantityConfig {
|
||||
default: number;
|
||||
min : number;
|
||||
max : number;
|
||||
step : number;
|
||||
unit : I18nField;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ATTRIBUTES
|
||||
// ============================================================================
|
||||
|
||||
export interface EditorAttribute {
|
||||
id : string;
|
||||
code : string;
|
||||
name : I18nField;
|
||||
description : I18nField;
|
||||
type : 'dynamic' | 'fixed' | 'computed';
|
||||
dataType : 'reference' | 'number' | 'string' | 'boolean';
|
||||
defaultValue: string | number | null;
|
||||
options : EditorAttributeOption[]; // solo si type === 'dynamic'
|
||||
display : EditorDisplay;
|
||||
}
|
||||
|
||||
export interface EditorDisplay {
|
||||
uiVisible : boolean;
|
||||
affectsVisual: boolean;
|
||||
affectsPrice : boolean;
|
||||
readonly? : boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VIEWS
|
||||
// ============================================================================
|
||||
|
||||
export type EditorVisualStrategy = 'static_image' | 'composite_layers' | 'api_generated' | 'three_d';
|
||||
|
||||
export interface EditorView {
|
||||
id : string;
|
||||
code : string;
|
||||
name : I18nField;
|
||||
type : 'front' | 'panoramic' | 'detail';
|
||||
strategy: EditorVisualStrategy;
|
||||
template: string; // para static_image
|
||||
basePath: string;
|
||||
fallback: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SECTIONS
|
||||
// ============================================================================
|
||||
|
||||
export interface EditorSection {
|
||||
id : string;
|
||||
code : string;
|
||||
kind : 'visual' | 'config';
|
||||
name : I18nField;
|
||||
description : I18nField;
|
||||
attributes : EditorAttribute[];
|
||||
views : EditorView[];
|
||||
defaultView : string | null;
|
||||
availability: { mode: 'required' | 'optional' | 'conditional' };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OBJECTS
|
||||
// ============================================================================
|
||||
|
||||
export interface EditorObject {
|
||||
id : string;
|
||||
code : string;
|
||||
name : I18nField;
|
||||
description : I18nField;
|
||||
category : string;
|
||||
basePath : string;
|
||||
attributes : EditorAttribute[]; // atributos globales
|
||||
sections : EditorSection[];
|
||||
sectionOrder: string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RULES
|
||||
// ============================================================================
|
||||
|
||||
export type EditorRuleActionType = 'forbid' | 'allow' | 'require' | 'suggest';
|
||||
export type EditorSeverity = 'error' | 'warning' | 'info';
|
||||
|
||||
/**
|
||||
* Condición del editor — árbol de condiciones simples.
|
||||
* Se convierte a JsonLogic al exportar.
|
||||
*/
|
||||
export type EditorCondition =
|
||||
| { type: 'attribute_equals'; attrId: string; value: string }
|
||||
| { type: 'attribute_in'; attrId: string; values: string[] }
|
||||
| { type: 'and'; conditions: EditorCondition[] }
|
||||
| { type: 'or'; conditions: EditorCondition[] };
|
||||
|
||||
export interface EditorRuleAction {
|
||||
type : EditorRuleActionType;
|
||||
targetAttrId: string;
|
||||
values : string[];
|
||||
}
|
||||
|
||||
export interface EditorRule {
|
||||
id : string;
|
||||
name : I18nField;
|
||||
condition: EditorCondition;
|
||||
action : EditorRuleAction;
|
||||
severity : EditorSeverity;
|
||||
priority : number;
|
||||
message : I18nField;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TAX
|
||||
// ============================================================================
|
||||
|
||||
export interface EditorTax {
|
||||
rate : number;
|
||||
included: boolean;
|
||||
label : I18nField;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CATALOG
|
||||
// ============================================================================
|
||||
|
||||
export interface EditorCatalog {
|
||||
id : string;
|
||||
code : string;
|
||||
name : I18nField;
|
||||
description: I18nField;
|
||||
basePath : string;
|
||||
options : EditorOption[];
|
||||
objects : EditorObject[];
|
||||
rules : EditorRule[];
|
||||
tax : EditorTax | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VALIDATION
|
||||
// ============================================================================
|
||||
|
||||
export interface ValidationError {
|
||||
path : string; // ej: 'objects.ob_1.sections.sc_2.attributes.at_3'
|
||||
message: string;
|
||||
severity: 'error' | 'warning';
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid : boolean;
|
||||
errors : ValidationError[];
|
||||
warnings: ValidationError[];
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
console.log('Happy developing ✨')
|
||||
@ -0,0 +1,8 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"types": ["svelte", "vite/client"],
|
||||
"noEmit": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"moduleDetection": "force"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { resolve } from 'path';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
base: './',
|
||||
plugins: [svelte(),tailwindcss()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
Loading…
Reference in new issue