ajustes
This commit is contained in:
444
docs/01-builder-fields.md
Normal file
444
docs/01-builder-fields.md
Normal file
@@ -0,0 +1,444 @@
|
||||
# Builder Fields — Campos editables del index-base.tpl
|
||||
|
||||
Este documento define los campos editables que el usuario rellena desde el panel del builder de Acai. Cubre el atributo `data-field-type` con todos sus tipos (`textfield`, `headfield`, `textbox`, `wysiwyg`, `link`, `upload`, `uploadMulti`, `list`, `multiv2`, `checkbox`, `colorpicker`), la regla `data-field-label` → nombre de variable, los atributos Acai (`c-if`, `c-else`, `c-for`, `c-class`, `c-hidden`, `c-required`), el tag `<set>`, la inclusión de módulos, los formularios `c-form` y los componentes built-in. Léelo antes de crear o modificar cualquier `index-base.tpl`.
|
||||
|
||||
## Reglas de nomenclatura de variables
|
||||
|
||||
El atributo `data-field-label` se convierte automáticamente en el nombre de variable Twig: se ponen minúsculas y se eliminan espacios y caracteres especiales.
|
||||
|
||||
| Label | Variable resultante |
|
||||
|-------|---------------------|
|
||||
| `Categoría Noticia` | `categoranoticia` |
|
||||
| `Color Principal` | `colorprincipal` |
|
||||
| `Título Producto` | `ttuloproducto` |
|
||||
|
||||
Reglas obligatorias:
|
||||
- Todo elemento con `data-field-type` DEBE incluir también `data-field-label`.
|
||||
- Sin `data-field-label`, el builder genera variables temporales o incorrectas y el módulo queda mal configurado.
|
||||
- Usa labels descriptivos y estables; no dejes labels vacíos ni genéricos como "Campo" o "Texto".
|
||||
- En `index-base.tpl` evita clases Tailwind con valores arbitrarios (`text-[44px]`, `font-['Cinzel']`, `leading-[1.1]`) — pueden romper el parseo. Muévelas a `style.css`.
|
||||
|
||||
## Tipos de campo (`data-field-type`)
|
||||
|
||||
| Tipo | Elemento HTML | Devuelve |
|
||||
|------|---------------|----------|
|
||||
| `textfield` | `<p>` | String |
|
||||
| `headfield` | `<h1>`–`<h6>` | String + variable extra `_tag` con la etiqueta elegida |
|
||||
| `textbox` | `<div>` | String multilínea |
|
||||
| `wysiwyg` | `<div class="wysiwyg">` | String HTML |
|
||||
| `link` | `<a>` | URL string (ya incluye barras) |
|
||||
| `upload` | `<img>` | **Array** de `{urlPath, info1, info2, info3, info4}` |
|
||||
| `uploadMulti` | `<li>` | Itera sobre archivos subidos |
|
||||
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
||||
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
||||
| `multiv2` | `<li>` wrapper | Array de objetos repetibles |
|
||||
| `checkbox` | `<div>` o `<input>` | `1` o `0` (número) |
|
||||
| `colorpicker` | `<div>` | Hex color string |
|
||||
|
||||
### textfield
|
||||
|
||||
```html
|
||||
<p data-field-type="textfield" data-field-label="Título">
|
||||
Elemento editable
|
||||
</p>
|
||||
```
|
||||
|
||||
### headfield
|
||||
|
||||
Genera 2 variables: la estándar y `_tag` con la etiqueta elegida (h1…h6).
|
||||
|
||||
```html
|
||||
<{{ titulo_tag | default('h2') }}
|
||||
data-field-type="headfield"
|
||||
data-field-label="Título Sección"
|
||||
class="text-3xl font-bold">
|
||||
Título de la sección
|
||||
</{{ titulo_tag | default('h2') }}>
|
||||
```
|
||||
|
||||
### textbox
|
||||
|
||||
```html
|
||||
<div data-field-type="textbox" data-field-label="Descripción">
|
||||
Texto largo editable
|
||||
</div>
|
||||
```
|
||||
|
||||
### wysiwyg
|
||||
|
||||
Editor de texto enriquecido. Acceder con `| raw` para no escapar el HTML.
|
||||
|
||||
```html
|
||||
<div class="wysiwyg" data-field-type="wysiwyg" data-field-label="Contenido Enriquecido">
|
||||
<p>Texto con <strong>estilos</strong> editables</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### link
|
||||
|
||||
El campo `enlace` de Acai ya incluye las barras necesarias — nunca añadas barras extra.
|
||||
|
||||
```html
|
||||
<a data-field-type="link" data-field-label="Enlace Principal" href="#">
|
||||
Haz clic aquí
|
||||
</a>
|
||||
```
|
||||
|
||||
### upload
|
||||
|
||||
Devuelve un array. Acceso en Twig: `{{ imagen[0].urlPath }}`.
|
||||
|
||||
```html
|
||||
<div class="p-1/6 relative">
|
||||
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
|
||||
data-field-type="upload"
|
||||
data-field-label="Imagen Principal"
|
||||
data-lazy="true"
|
||||
data-field-info1="titulo"
|
||||
data-field-width="1400"
|
||||
alt="">
|
||||
</div>
|
||||
```
|
||||
|
||||
Atributos disponibles:
|
||||
- `data-lazy="true"` — carga perezosa
|
||||
- `data-field-width="1400"` — ancho máximo sugerido
|
||||
- `data-field-info1="titulo"` — campo de información adicional (típicamente alt)
|
||||
|
||||
### uploadMulti
|
||||
|
||||
Itera sobre todas las imágenes subidas. Variable iteradora: `uploadMulti`.
|
||||
|
||||
```html
|
||||
<li data-field-type="uploadMulti" data-field-label="Galería" data-field-info1="titulo">
|
||||
<div class="relative min-h-screen">
|
||||
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
|
||||
data-src="{{ uploadMulti.urlPath | imagec(2100) }}"
|
||||
alt="{{ uploadMulti.info1 }}">
|
||||
</div>
|
||||
</li>
|
||||
```
|
||||
|
||||
### list (opciones fijas)
|
||||
|
||||
```html
|
||||
<div data-field-type="list"
|
||||
data-field-label="Color Producto"
|
||||
data-list-options="Rojo,Azul,|Verde,3|Amarillo">
|
||||
</div>
|
||||
```
|
||||
|
||||
Formato `data-list-options`:
|
||||
- `opcion1,opcion2` → la opción es etiqueta y valor a la vez
|
||||
- `|valor3,etiqueta3` → separa valor de etiqueta con `|`
|
||||
|
||||
### list (tabla)
|
||||
|
||||
Selecciona un registro de otra tabla. Devuelve el `num`.
|
||||
|
||||
```html
|
||||
<div data-field-type="list"
|
||||
data-field-label="Noticia Destacada"
|
||||
data-list-table="noticias"
|
||||
data-list-value="num"
|
||||
data-list-label="titulo">
|
||||
{{ record.titulo }}
|
||||
</div>
|
||||
```
|
||||
|
||||
- `data-list-table` — nombre de tabla **sin prefijo `cms_`**
|
||||
- `data-list-value` — campo a usar como valor (normalmente `num`)
|
||||
- `data-list-label` — campo a mostrar como label
|
||||
|
||||
### multiv2 — Campos repetibles
|
||||
|
||||
Crea grupos de campos repetibles. La variable resultante es un array de objetos.
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li data-field-type="multiv2" data-field-label="Productos">
|
||||
<div data-field-type="textfield" data-field-label="Nombre">
|
||||
Nombre del producto
|
||||
</div>
|
||||
<div data-field-type="textbox" data-field-label="Descripción">
|
||||
Descripción del producto
|
||||
</div>
|
||||
<div class="p-1/6 relative">
|
||||
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
|
||||
data-field-type="upload"
|
||||
data-field-label="Imagen"
|
||||
data-lazy="true"
|
||||
data-field-width="800"
|
||||
alt="">
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Uso en Twig:
|
||||
|
||||
```twig
|
||||
{% for record in productos %}
|
||||
<div class="producto">
|
||||
<h3>{{ record.nombre }}</h3>
|
||||
<p>{{ record.descripcion }}</p>
|
||||
<img src="{{ record.imagen[0].urlPath }}" alt="">
|
||||
</div>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### checkbox
|
||||
|
||||
Devuelve `1` o `0` (número), nunca `true`/`false`.
|
||||
|
||||
### colorpicker
|
||||
|
||||
Devuelve un string hexadecimal (`#ff0000`). Almacenado en config-vars (no en `builder_custom`).
|
||||
|
||||
## Atributos Acai
|
||||
|
||||
### `c-if` — Renderizado condicional
|
||||
|
||||
Usa `=` (un solo igual) para comparaciones, no `==`.
|
||||
|
||||
```html
|
||||
<div c-if="subtitle">{{ subtitle }}</div>
|
||||
<div c-if="layout = 'grid'">Grid layout</div>
|
||||
```
|
||||
|
||||
### `c-else`
|
||||
|
||||
Va inmediatamente después del elemento `c-if`.
|
||||
|
||||
```html
|
||||
<div c-if="image">
|
||||
<img src="{{ image[0].urlPath }}">
|
||||
</div>
|
||||
<div c-else>
|
||||
<p>No image available</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-for` — Iteración sobre array
|
||||
|
||||
```html
|
||||
<div c-for="item in record.features">
|
||||
<h3>{{ item.title }}</h3>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-for` — Iteración sobre tabla de BD
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li c-for="producto in productos"
|
||||
c-where="'visible=1'"
|
||||
c-order="'num desc'"
|
||||
c-limit="10">
|
||||
{{ producto.title }}
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Parámetros opcionales: `c-where` (string SQL), `c-order` (string de orden), `c-limit` (entero).
|
||||
|
||||
Equivalente Twig:
|
||||
```twig
|
||||
{% for producto in 'productos' | get('visible=1','num desc',10) %}
|
||||
<li>{{ producto.title }}</li>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Variables del loop: `loop.index` (1-based), `loop.index is odd`, `loop.index is even`.
|
||||
|
||||
### `c-class` — Clases CSS condicionales
|
||||
|
||||
```html
|
||||
<!-- Simple -->
|
||||
<div c-class="{ 'text-center': alineacion == '1', 'text-right': alineacion == '2' }">
|
||||
|
||||
<!-- Múltiples condiciones -->
|
||||
<div c-class="{
|
||||
'flex-row-reverse': orden == '1',
|
||||
'cursor-pointer click-a-child': record.enlace_anchor,
|
||||
'rounded-xl': radioborde == '4'
|
||||
}">
|
||||
|
||||
<!-- Con loop -->
|
||||
<div c-class="{
|
||||
'md:order-1': loop.index is odd,
|
||||
'md:pl-6': loop.index is even
|
||||
}">
|
||||
|
||||
<!-- Combinado con clases estáticas -->
|
||||
<div class="flex items-center" c-class="{ 'justify-center': centrado }">
|
||||
```
|
||||
|
||||
### `c-hidden` — Variables ocultas
|
||||
|
||||
Elemento que NO se renderiza pero SÍ declara variables builder. Patrón típico para colores y opciones de configuración.
|
||||
|
||||
```html
|
||||
<div c-hidden="true">
|
||||
<input data-field-type="textfield" data-field-label="Color de fondo" value="">
|
||||
<div data-field-type="list"
|
||||
data-field-label="Color titulo resaltado"
|
||||
data-list-options="|Main color,1|Main color light,2|Main color dark"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-required` — Validación condicional
|
||||
|
||||
```html
|
||||
<input type="text" name="telefono"
|
||||
c-required="'2' not in camposquitar"
|
||||
placeholder="Teléfono">
|
||||
```
|
||||
|
||||
## Tag `<set>` — Definir variables
|
||||
|
||||
```html
|
||||
<!-- Obtener configuración de la BD -->
|
||||
<set :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
|
||||
|
||||
<!-- Construir URLs dinámicas -->
|
||||
<set :logo="tienda.logo.0.urlPath
|
||||
? 'https://' ~ server.HTTP_HOST ~ tienda.logo.0.urlPath
|
||||
: 'https://' ~ server.HTTP_HOST ~ '/template/estandar/images/logo.png'">
|
||||
</set>
|
||||
|
||||
<!-- Twig set para expresiones complejas -->
|
||||
{% set gracias = 'apartados' | get('num = 20').0 %}
|
||||
```
|
||||
|
||||
## Incluir módulos
|
||||
|
||||
Para incluir un módulo dentro de otro módulo o dentro de una sección general, usa el `moduleId` como etiqueta HTML:
|
||||
|
||||
```html
|
||||
<module_id :param1="value1" :param2="'string value'"></module_id>
|
||||
```
|
||||
|
||||
Ejemplos:
|
||||
```html
|
||||
<header_menu :showLogo="true" :menuItems="items"></header_menu>
|
||||
<product_card :product="selectedProduct" :showPrice="true"></product_card>
|
||||
```
|
||||
|
||||
El módulo hijo recibe los parámetros como variables en su contexto.
|
||||
|
||||
## Formularios — `c-form`
|
||||
|
||||
Maneja automáticamente validación, almacenamiento en BD y envío de emails.
|
||||
|
||||
```html
|
||||
<c-form
|
||||
class="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow"
|
||||
tableName="'solicitudes'"
|
||||
mailRecord="['correos', 'CONTACTO']"
|
||||
sendTo="'contacto@empresa.com'"
|
||||
sendToClient="'email'"
|
||||
captcha="true"
|
||||
honeypot="true"
|
||||
messageOK="'¡Gracias! Te contactaremos pronto'"
|
||||
messageKO="'Por favor, completa todos los campos'"
|
||||
redirect="'/gracias'"
|
||||
attachFiles="true">
|
||||
|
||||
<input name="nombre" type="text" required class="w-full p-2 border rounded">
|
||||
<input name="email" type="email" required class="w-full p-2 border rounded">
|
||||
<textarea name="mensaje" required class="w-full p-2 border rounded" rows="5"></textarea>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input name="acepto_politica" type="checkbox" class="mr-2" required>
|
||||
<span>Acepto la política de privacidad</span>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="bg-teal-500 text-white px-6 py-2 rounded">Enviar</button>
|
||||
<captcha/>
|
||||
</c-form>
|
||||
```
|
||||
|
||||
### Atributos `c-form`
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| `tableName="'tabla'"` | Tabla destino (sin `cms_`) |
|
||||
| `mailRecord="['correos', 'ID']"` | Template de email en tabla `correos` |
|
||||
| `sendTo="'email@dominio.com'"` | Destinatarios (separados por coma) |
|
||||
| `sendToClient="'campo_email'"` | Campo del formulario con email del cliente para auto-reply |
|
||||
| `captcha="true"` | Activa Google reCAPTCHA |
|
||||
| `honeypot="true"` | Campo oculto anti-spam |
|
||||
| `messageOK="'texto'"` | Mensaje al enviar correctamente |
|
||||
| `messageKO="'texto'"` | Mensaje al fallar validación |
|
||||
| `redirect="'/ruta/'"` | Redirección tras envío correcto |
|
||||
| `attachFiles="true"` | Adjuntar archivos al email |
|
||||
| `showImages="true"` | Mostrar thumbnails en email |
|
||||
| `emailMode="'twig'"` | Email en formato Twig |
|
||||
| `header="'<div>...'"` | HTML cabecera del email |
|
||||
| `footer="'<div>...'"` | HTML footer del email |
|
||||
| `styles="'body { ... }'"` | CSS del email |
|
||||
|
||||
Para formularios estándar (contacto, postulación), prefiere `c-form` antes que crear lógica custom de POST/hook. Solo crea una tabla propia si necesitas gestionar esos registros desde el admin.
|
||||
|
||||
## Componentes built-in
|
||||
|
||||
### Carousel — `c-tns-wrapper`
|
||||
|
||||
```html
|
||||
<div class="c-tns-wrapper"
|
||||
data-responsive='{"0":1,"768":2,"1024":3}'
|
||||
data-speed="400"
|
||||
data-nav="true"
|
||||
data-autoplay-timeout="3000">
|
||||
<div c-for="slide in record.slides">
|
||||
<img src="{{ slide.image[0].urlPath }}">
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Lightbox
|
||||
|
||||
```html
|
||||
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
|
||||
<img src="{{ image[0].urlPath | imagec(400) }}">
|
||||
</a>
|
||||
```
|
||||
|
||||
### Breadcrumb
|
||||
|
||||
```html
|
||||
<breadCrumb/>
|
||||
<breadCrumb class="bg-gray-200 p-3 rounded" c-prevlinks="null"></breadCrumb>
|
||||
```
|
||||
|
||||
### Animate On Scroll (AOS)
|
||||
|
||||
```html
|
||||
<div data-aos="fade-up" data-aos-delay="200" data-aos-duration="800">
|
||||
Contenido animado
|
||||
</div>
|
||||
```
|
||||
|
||||
Valores comunes: `fade-up`, `fade-down`, `fade-left`, `fade-right`, `zoom-in`, `zoom-in-up`, `fade-up-right`, `fade-up-left`. Tras cambios dinámicos en JS: `AOS.refresh()`.
|
||||
|
||||
### Lazy loading
|
||||
|
||||
```html
|
||||
<img class="lazyload" data-src="{{ image[0].urlPath }}">
|
||||
<!-- O en builder field: -->
|
||||
<img data-field-type="upload" data-field-label="Imagen" data-lazy="true">
|
||||
```
|
||||
|
||||
## Reglas críticas
|
||||
|
||||
1. Todo `data-field-type` exige `data-field-label`.
|
||||
2. `data-field-label` se transforma a variable: minúsculas, sin espacios ni caracteres especiales.
|
||||
3. Campos `upload` retornan **arrays** — usa `imagen[0].urlPath`, nunca `imagen`.
|
||||
4. Variables dentro de `multiv2` son propiedades del objeto iterado (`record.nombre`).
|
||||
5. `c-if` usa `=` (un igual). `{% if %}` usa `==` (doble igual).
|
||||
6. `c-for` con tabla: nombre **sin prefijo `cms_`**.
|
||||
7. `enlace` ya incluye las barras — no añadas slashes extra.
|
||||
8. Checkbox guarda `1` o `0` (número), nunca `true`/`false`.
|
||||
9. Evita Tailwind arbitrary-value en `index-base.tpl` — muévelos a `style.css`.
|
||||
10. `script.js` y `style.css` son estáticos: NO uses sintaxis Twig dentro. Pasa valores dinámicos vía `data-*`.
|
||||
260
docs/02-twig.md
Normal file
260
docs/02-twig.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Twig — Filtros personalizados de Acai
|
||||
|
||||
Este documento describe los filtros Twig propios de Acai (`get`, `queryDB`, `hook`, `module`, `imagec`, `translate`) y los filtros estándar más usados (`raw`, `truncate`, `json_decode`, `split`, `filter`). Acai usa **filtros con pipe `|`**, nunca funciones. Léelo antes de escribir cualquier expresión Twig dentro de `index-base.tpl` o de una sección general. Cubre también la concatenación con `~`, los ternarios, el operador `default` y la diferencia entre `c-if` (=) y `{% if %}` (==).
|
||||
|
||||
## `get` — Consultar tabla de BD
|
||||
|
||||
```twig
|
||||
{{ 'table_name' | get(where, order, limit) }}
|
||||
```
|
||||
|
||||
- `table_name`: **sin prefijo `cms_`**
|
||||
- `where`: string SQL u objeto (opcional)
|
||||
- `order`: string de orden (opcional)
|
||||
- `limit`: entero (opcional)
|
||||
|
||||
```twig
|
||||
{# Todos los registros #}
|
||||
{% set products = 'productos' | get() %}
|
||||
|
||||
{# Con WHERE string #}
|
||||
{% set active = 'productos' | get('activo=1') %}
|
||||
|
||||
{# Con WHERE objeto #}
|
||||
{% set active = 'productos' | get({activo: 1}) %}
|
||||
|
||||
{# Con WHERE + ORDER + LIMIT #}
|
||||
{% set latest = 'noticias' | get('publicado=1', 'fecha DESC', 6) %}
|
||||
|
||||
{# Single record (primer resultado) #}
|
||||
{% set product = 'productos' | get({num: 42}) %}
|
||||
{{ product[0].nombre }}
|
||||
```
|
||||
|
||||
Iterar resultados:
|
||||
```twig
|
||||
{% for producto in 'productos' | get('activo=1', 'num DESC', 10) %}
|
||||
<h3>{{ producto.titulo }}</h3>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Concatenar valor dinámico en WHERE — usa el operador `~`:
|
||||
```twig
|
||||
{% set stock = 'stocks' | get('producto_num=' ~ producto.num) %}
|
||||
```
|
||||
|
||||
## `queryDB` — SQL directo
|
||||
|
||||
Usa el nombre de tabla **completo CON prefijo `cms_`**. Solo cuando `get` no sea suficiente (JOINs, agregaciones complejas).
|
||||
|
||||
```twig
|
||||
{% set results = 'SELECT * FROM cms_productos WHERE precio > 100 ORDER BY precio ASC' | queryDB() %}
|
||||
|
||||
{# JOIN complejo #}
|
||||
{% set top = 'SELECT p.*, COUNT(v.num) as ventas
|
||||
FROM cms_productos p
|
||||
LEFT JOIN cms_ventas v ON v.producto_num = p.num
|
||||
GROUP BY p.num
|
||||
ORDER BY ventas DESC
|
||||
LIMIT 5' | queryDB() %}
|
||||
```
|
||||
|
||||
## `hook` — Ejecutar PHP hook
|
||||
|
||||
```twig
|
||||
{# Llamar y mostrar resultado #}
|
||||
{{ 'hooks/module_id/' | hook({param1: 'value', param2: variable}) }}
|
||||
|
||||
{# Capturar en variable #}
|
||||
{% set result = 'hooks/calcular_precio/' | hook({cantidad: 5, tipo: 'mayoreo'}) %}
|
||||
<p>Total: ${{ result.total }}</p>
|
||||
```
|
||||
|
||||
El primer argumento es la ruta del endpoint del hook (`hooks/<id>/`). El objeto pasa los parámetros, que el PHP recibe como variables (`$cantidad`, `$tipo`).
|
||||
|
||||
## `module` — Renderizar otro módulo
|
||||
|
||||
```twig
|
||||
{{ 'other_module_id' | module({param1: value1}) }}
|
||||
|
||||
{# Capturar en variable #}
|
||||
{% set carrito = 'carrito_compras' | module({usuario_id: 123}) %}
|
||||
```
|
||||
|
||||
Equivale a `<other_module_id :param1="value1"></other_module_id>`.
|
||||
|
||||
## `imagec` — Optimizar imágenes
|
||||
|
||||
```twig
|
||||
{# Redimensionar a ancho específico #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(400) }}">
|
||||
|
||||
{# Con srcset #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(800) }}"
|
||||
srcset="{{ record.image[0].urlPath | imagec(400) }} 400w,
|
||||
{{ record.image[0].urlPath | imagec(800) }} 800w">
|
||||
```
|
||||
|
||||
Acai genera versiones optimizadas (webp + tamaños) y las cachea. Usa siempre `imagec` para imágenes en producción.
|
||||
|
||||
## `translate` — Texto editable y traducción
|
||||
|
||||
Cualquier string con `| translate` se resuelve contra la tabla `textos_generales` del proyecto. Cumple **dos funciones a la vez**:
|
||||
|
||||
1. **Traducción**: cada fila guarda la versión del texto por cada idioma habilitado.
|
||||
2. **Edición de contenidos**: es el canal oficial para que el usuario final modifique esos textos sin tocar código. `| translate` no es solo i18n — es el mecanismo por el que un texto "hardcodeado" se vuelve editable desde el CMS.
|
||||
|
||||
```twig
|
||||
{{ 'Bienvenido' | translate }}
|
||||
{{ variable | translate }}
|
||||
{{ 'Contáctanos' | translate | raw }}
|
||||
```
|
||||
|
||||
Cómo funciona:
|
||||
- Los strings envueltos en `| translate` se buscan en `textos_generales`.
|
||||
- Si existe la fila, devuelve el valor guardado (en el idioma activo).
|
||||
- Si no existe, devuelve el texto original tal cual (fallback).
|
||||
- Las filas se editan desde el admin del CMS o vía `CmsApi` (update sobre `textos_generales`).
|
||||
|
||||
Reglas críticas:
|
||||
- **NO crees archivos JSON de traducciones, `.po`, ni ningún sistema i18n externo.** El único sistema de textos traducibles/editables es la tabla `textos_generales`.
|
||||
- **NO hardcodees textos en el código del módulo** si el usuario debe poder editarlos. Envuélvelos en `| translate`.
|
||||
- Para **cambiar un texto** (traducir o editar), edita la fila correspondiente en `textos_generales` — nunca modifiques el código.
|
||||
- Para **añadir un texto nuevo editable**, basta con escribir el string con `| translate` en el código; el sistema lo recoge y el usuario lo puede editar desde el admin.
|
||||
|
||||
## Filtros estándar
|
||||
|
||||
### `raw` — Renderizar HTML sin escapar
|
||||
|
||||
```twig
|
||||
{{ record.description | raw }}
|
||||
```
|
||||
|
||||
Imprescindible para `wysiwyg` y para HTML construido en variables.
|
||||
|
||||
### `truncate` — Truncar texto
|
||||
|
||||
```twig
|
||||
{{ record.description | truncate(150) }}
|
||||
```
|
||||
|
||||
### `json_decode` — Parsear JSON
|
||||
|
||||
```twig
|
||||
{% set data = jsonString | json_decode %}
|
||||
{{ data.key }}
|
||||
```
|
||||
|
||||
### `split`, `filter`, `length`, `default`, `lower`, `upper`, `trim`, `replace`
|
||||
|
||||
Funcionan igual que en Twig estándar.
|
||||
|
||||
```twig
|
||||
{{ title | default('Sin título') }}
|
||||
{{ items | length }}
|
||||
{{ name | upper }}
|
||||
```
|
||||
|
||||
## Operadores y sintaxis
|
||||
|
||||
### Concatenación con `~`
|
||||
|
||||
Twig usa `~` (no `.` ni `+`):
|
||||
|
||||
```twig
|
||||
{{ 'Hello ' ~ name ~ '!' }}
|
||||
{% set url = '/products/' ~ product.slug ~ '/' %}
|
||||
```
|
||||
|
||||
En filtros:
|
||||
```twig
|
||||
{% set stock = 'stocks' | get('producto_num=' ~ producto.num) %}
|
||||
```
|
||||
|
||||
### Ternario
|
||||
|
||||
```twig
|
||||
{{ isActive ? 'active' : 'inactive' }}
|
||||
{{ title | default('Default Title') }}
|
||||
```
|
||||
|
||||
### Comparaciones
|
||||
|
||||
| Contexto | Igualdad |
|
||||
|----------|----------|
|
||||
| `c-if` | `=` (un solo igual) |
|
||||
| `{% if %}` | `==` (doble igual) |
|
||||
|
||||
```twig
|
||||
{# Atributo Acai - un igual #}
|
||||
<div c-if="layout = 'grid'">
|
||||
|
||||
{# Twig estándar - doble igual #}
|
||||
{% if type == 'premium' %}
|
||||
{% if items | length > 0 %}
|
||||
{% if name is not empty %}
|
||||
```
|
||||
|
||||
## Ejemplos complejos
|
||||
|
||||
### Galería con productos y stock
|
||||
|
||||
```twig
|
||||
{% for producto in 'productos' | get('destacado=1', 'num DESC', 12) %}
|
||||
<div class="producto-card">
|
||||
<img src="{{ producto.imagen[0].urlPath | imagec(400) }}" alt="{{ producto.titulo }}">
|
||||
<h3>{{ producto.titulo }}</h3>
|
||||
<p>{{ producto.descripcion | truncate(100) }}</p>
|
||||
|
||||
{% set stock = 'stocks' | get('producto_num=' ~ producto.num) %}
|
||||
<span>Stock: {{ stock[0].cantidad }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Múltiples filtros combinados
|
||||
|
||||
```twig
|
||||
{% set categorias = 'categorias' | get() %}
|
||||
{% set productos = 'productos' | get('activo=1', 'titulo ASC', 20) %}
|
||||
{% set stats = 'hooks/obtener_stats/' | hook({fecha_inicio: '2024-01-01'}) %}
|
||||
|
||||
<h1>{{ stats.titulo | translate }}</h1>
|
||||
|
||||
<nav>
|
||||
{% for cat in categorias %}
|
||||
<a href="{{ cat.enlace }}">{{ cat.nombre }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{% for prod in productos %}
|
||||
<div>
|
||||
<img src="{{ prod.imagen[0].urlPath | imagec(300) }}" alt="">
|
||||
<h3>{{ prod.titulo }}</h3>
|
||||
</div>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Composición con `<set>` y configuración global
|
||||
|
||||
```twig
|
||||
<set :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
|
||||
{% set logoUrl = tienda.logo.0.urlPath
|
||||
? 'https://' ~ server.HTTP_HOST ~ tienda.logo.0.urlPath
|
||||
: 'https://' ~ server.HTTP_HOST ~ '/template/estandar/images/logo.png' %}
|
||||
|
||||
<img src="{{ logoUrl }}" alt="{{ tienda.nombre }}">
|
||||
```
|
||||
|
||||
## Reglas críticas
|
||||
|
||||
1. **Solo filtros, nunca funciones.** `'tabla' | get()`, no `get('tabla')`.
|
||||
2. **Tablas sin prefijo `cms_`** en `get()`. **Con prefijo `cms_`** en `queryDB()`.
|
||||
3. **Upload fields son arrays.** `record.imagen[0].urlPath`, no `record.imagen`.
|
||||
4. **Concatenación con `~`**, no con `.` ni `+`.
|
||||
5. **`c-if` usa `=`**, **`{% if %}` usa `==`**.
|
||||
6. **Foreign keys con sufijo `_num`**: `categoria_num`, no `categoria_id`.
|
||||
7. **`enlace` ya tiene barras** — no las añadas.
|
||||
8. **PK siempre es `num`**, nunca `id`.
|
||||
9. **`| translate` para textos editables** — nunca crees JSONs de i18n.
|
||||
10. Usa `imagec(width)` para imágenes en producción.
|
||||
200
docs/03-modules-and-sections.md
Normal file
200
docs/03-modules-and-sections.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Módulos y Secciones Generales
|
||||
|
||||
Este documento explica el sistema modular de Acai: la diferencia entre **módulos** (componentes visuales reutilizables que el usuario coloca en páginas Builder) y **secciones generales** (plantillas ligadas a una tabla que se renderizan automáticamente al acceder al `enlace` de un registro). Cubre la estructura de archivos de un módulo, las reglas obligatorias sobre `index-base.tpl`, las variables globales (`section_id`, `interno`, `server.HTTP_HOST`, `loop`), la convención `custom-{tableName}` para detalles de registro, la inclusión de un módulo dentro de otro y el uso de `thisrecord` en secciones generales. Léelo antes de crear, mover o editar cualquier carpeta dentro de `template/estandar/modulos/`.
|
||||
|
||||
## Módulos
|
||||
|
||||
Componentes visuales reutilizables. Viven en `template/estandar/modulos/<module-id>/`. El usuario los arrastra al builder de una página y rellena sus variables.
|
||||
|
||||
### Estructura de archivos
|
||||
|
||||
```
|
||||
<module-id>/
|
||||
├── index-base.tpl # Plantilla source (Twig + atributos Acai) — EDITA ESTE
|
||||
├── index.tpl # Compilado (auto-generado) — NO TOCAR
|
||||
├── index-twig.tpl # Compilado Twig (auto-generado) — NO TOCAR
|
||||
├── builder.json # Variables del builder (auto-generado) — NO TOCAR
|
||||
├── style.css # CSS del módulo (estático)
|
||||
├── script.js # JS del módulo (estático)
|
||||
└── hook.php # Hook PHP propio del módulo (opcional)
|
||||
```
|
||||
|
||||
Reglas duras:
|
||||
- **Solo se edita `index-base.tpl`.** `index.tpl`, `index-twig.tpl` y `builder.json` los genera el compilador y se sobrescriben automáticamente.
|
||||
- Editar `index-base.tpl` con `acai-write` o `acai-line-replace` **dispara la compilación automática**.
|
||||
- `script.js` y `style.css` son **estáticos** — NO uses sintaxis Twig dentro. Pasa valores dinámicos vía atributos `data-*`.
|
||||
- `index-base.tpl` solo contiene HTML/Twig. **Nunca** embebas etiquetas `<script>` con lógica del módulo, **nunca** PHP.
|
||||
|
||||
### Sintaxis del template
|
||||
|
||||
Híbrido Twig + atributos Acai. Ver `01-builder-fields.md` para el catálogo completo de campos editables (`data-field-type`) y atributos (`c-if`, `c-for`, etc.).
|
||||
|
||||
```html
|
||||
<section class="hero-section" id="{{ section_id }}">
|
||||
<div class="container mx-auto px-4">
|
||||
<h2 data-field-type="headfield" data-field-label="Titulo" class="text-3xl font-bold">
|
||||
Title here
|
||||
</h2>
|
||||
<p data-field-type="textbox" data-field-label="Descripcion" class="text-lg text-gray-600">
|
||||
Description text
|
||||
</p>
|
||||
<img data-field-type="upload" data-field-label="Imagen" src="placeholder.jpg" class="w-full rounded-lg">
|
||||
<a data-field-type="link" data-field-label="Boton" href="#" class="btn">Call to action</a>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Incluir un módulo dentro de otro
|
||||
|
||||
```html
|
||||
<module_id :param1="value1" :param2="'string value'"></module_id>
|
||||
```
|
||||
|
||||
El módulo hijo recibe los parámetros como variables en su contexto. Ejemplos típicos: incluir un formulario dentro del detalle de un registro, anidar un `bloqueproducto` dentro de un listado.
|
||||
|
||||
```html
|
||||
<bloqueproducto_i7aunn :producto="selectedProduct" :showPrice="true"></bloqueproducto_i7aunn>
|
||||
<form_postular :vacante_num="thisrecord.num"></form_postular>
|
||||
```
|
||||
|
||||
### Variables globales
|
||||
|
||||
| Variable | Descripción |
|
||||
|----------|-------------|
|
||||
| `section_id` | ID único por instancia del módulo. Úsalo para anchors, IDs HTML, scoping de JS |
|
||||
| `interno` | `true` cuando se renderiza en el editor del CMS, `false` en el sitio público |
|
||||
| `server.HTTP_HOST` | Dominio actual (sin protocolo) |
|
||||
| `loop.index` | Índice 1-based dentro de un `c-for` o `{% for %}` |
|
||||
| `loop.index is odd` / `is even` | Para layouts alternados (zigzag) |
|
||||
| `thisrecord` | El registro actual (solo en secciones generales / detalle) |
|
||||
|
||||
Patrón canónico para `section_id`:
|
||||
```html
|
||||
<div id="{{section_id}}"></div>
|
||||
<section id="id_{{ section_id }}" class="relative">
|
||||
<!-- contenido -->
|
||||
</section>
|
||||
```
|
||||
|
||||
## Secciones Generales
|
||||
|
||||
Plantillas ligadas a una tabla. Cuando el usuario navega al `enlace` de un registro, Acai busca la sección general correspondiente y la renderiza pasándole el registro como `thisrecord`.
|
||||
|
||||
Se construyen como **módulos especiales** dentro de `template/estandar/modulos/`. La diferencia clave: NO se colocan vía drag-and-drop; el CMS las enlaza por convención de nombre.
|
||||
|
||||
### Convención `custom-{tableName}` (REGLA DURA)
|
||||
|
||||
Toda tabla con campo `enlace` tiene automáticamente una sección general en:
|
||||
|
||||
```
|
||||
template/estandar/modulos/custom-{tableName}/
|
||||
```
|
||||
|
||||
El nombre es **literalmente** `custom-` seguido del `tableName`. Cuando el cliente accede a la URL de cualquier registro de esa tabla, Acai renderiza esa sección pasándole el registro como `thisrecord`.
|
||||
|
||||
Ejemplos:
|
||||
- Tabla `vacantes` → `template/estandar/modulos/custom-vacantes/index-base.tpl`
|
||||
- Tabla `productos` → `template/estandar/modulos/custom-productos/index-base.tpl`
|
||||
- Tabla `noticias` → `template/estandar/modulos/custom-noticias/index-base.tpl`
|
||||
|
||||
Reglas duras:
|
||||
- El CMS lo enlaza **automáticamente por convención de nombre**. NO existe ni se configura `_detailPage`.
|
||||
- Se crea/edita como cualquier otro módulo: `acai-write` sobre `index-base.tpl` dispara el compile.
|
||||
- Dentro del Twig, el registro actual está en `thisrecord` (`thisrecord.titulo`, `thisrecord.descripcion`, `thisrecord.imagen[0].urlPath`).
|
||||
- **NO crees una página por registro en `apartados`** ni una página "detalle" genérica. El detalle ya lo resuelve la sección general.
|
||||
- **NO uses ni configures `_detailPage`** — no existe.
|
||||
- **NO construyas URLs con query params** (`?id=5`) ni hagas fetch desde JS para cargar el registro.
|
||||
- **NO uses hooks para cargar el registro** — `thisrecord` ya está disponible.
|
||||
- **NO inventes otro nombre de módulo** para el detalle: debe ser `custom-{tableName}` exacto.
|
||||
|
||||
### Acceso a datos via `thisrecord`
|
||||
|
||||
```html
|
||||
<article class="product-card">
|
||||
<img src="{{ thisrecord.imagen[0].urlPath }}"
|
||||
alt="{{ thisrecord.imagen[0].info1 }}"
|
||||
class="w-full h-64 object-cover">
|
||||
<h3 class="text-xl font-semibold">{{ thisrecord.nombre }}</h3>
|
||||
<p class="text-gray-600">{{ thisrecord.descripcion | raw }}</p>
|
||||
<span class="text-2xl font-bold">{{ thisrecord.precio }}€</span>
|
||||
</article>
|
||||
```
|
||||
|
||||
Particularidades:
|
||||
- Upload fields retornan **arrays**: `thisrecord.imagen[0].urlPath`
|
||||
- Metadatos del upload: `info1` (alt típico), `info2`, `info3`, `info4`
|
||||
- Foreign keys con sufijo `_num`: `thisrecord.categoria_num`
|
||||
- Si la FK tiene relación cargada, también aparece como objeto: `thisrecord.categoria_bd[0].nombre`
|
||||
|
||||
### Embeber formularios en el detalle
|
||||
|
||||
Si un detalle necesita un formulario (postular, pedir info), **embebe el módulo del formulario dentro de la sección general** pasándole el `num` del registro actual:
|
||||
|
||||
```html
|
||||
<form_postular :vacante_num="thisrecord.num"></form_postular>
|
||||
```
|
||||
|
||||
NO pongas el formulario como sección suelta del listado.
|
||||
|
||||
### Definir variables con `<set>`
|
||||
|
||||
```html
|
||||
<set :categories="'categorias' | get()"></set>
|
||||
<set :featured="'productos' | get('destacado=1', 'orden ASC', 3)"></set>
|
||||
<set :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
|
||||
```
|
||||
|
||||
## Flujo canónico para una funcionalidad tipo "vacantes"
|
||||
|
||||
1. **Crear la tabla** con `enlace=true` (`create_table`) y añadir los campos (`create_field`).
|
||||
2. **Crear la sección general** `template/estandar/modulos/custom-{tableName}/index-base.tpl` con el Twig que renderiza `thisrecord.*`. Añade `style.css` y `script.js` si hace falta.
|
||||
3. (Opcional) **Crear un módulo de listado** `template/estandar/modulos/{tableName}_listado/` que consulte los registros y enlace a cada `enlace`.
|
||||
4. (Opcional) **Crear la página índice** `/{tableName}/` como registro normal en `apartados` (tipo Builder) y añadirle el módulo de listado.
|
||||
|
||||
## multiv2 — Contenido repetible dentro del módulo
|
||||
|
||||
El tipo `multiv2` permite que el usuario repita un grupo de campos editables dentro de un mismo módulo. Útil para FAQs, listados de servicios, slides, etc.
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li data-field-type="multiv2" data-field-label="Records">
|
||||
<h3 data-field-type="textfield" data-field-label="Titulo">Título por defecto</h3>
|
||||
<p data-field-type="textbox" data-field-label="Descripcion">Descripción</p>
|
||||
<img data-field-type="upload" data-field-label="Imagen" src="">
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Acceso en Twig:
|
||||
```twig
|
||||
{% for record in records %}
|
||||
<h3>{{ record.titulo }}</h3>
|
||||
<p>{{ record.descripcion }}</p>
|
||||
<img src="{{ record.imagen[0].urlPath }}">
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Las variables son **propiedades del objeto iterado**, no variables sueltas.
|
||||
|
||||
## Layout global vs módulos
|
||||
|
||||
`header`, `footer`, `style` global y `javascript` global NO son módulos normales. Viven en `cms/lib/plugins/builder_saas/layout.json` y se editan con tools dedicadas (`get_layout_field` / `set_layout_field`). Ver `08-layout-and-libraries.md`.
|
||||
|
||||
NUNCA edites directamente:
|
||||
- `cms/lib/plugins/builder_saas/layout.json`
|
||||
- `template/estandar/modulos/custom-header-twig/*`
|
||||
- `template/estandar/modulos/custom-footer-twig/*`
|
||||
- `template/estandar/modulos/custom-header/*`
|
||||
- `template/estandar/modulos/custom-footer/*`
|
||||
|
||||
Estos son artefactos generados a partir de `layout.json`.
|
||||
|
||||
## Reglas críticas
|
||||
|
||||
1. Solo se edita `index-base.tpl`. Los demás archivos `.tpl` y `builder.json` son auto-generados.
|
||||
2. Editar `index-base.tpl` con `acai-write` / `acai-line-replace` compila automáticamente.
|
||||
3. `script.js` y `style.css` son estáticos: nunca uses Twig ni atributos builder dentro.
|
||||
4. Detalle de registro = `template/estandar/modulos/custom-{tableName}/`. Nada de `_detailPage`, nada de páginas duplicadas.
|
||||
5. `thisrecord` solo existe en secciones generales — en módulos normales no aparece.
|
||||
6. Para incluir un módulo: `<module_id :param="value"></module_id>` o `'module_id' | module({param: value})`.
|
||||
7. Layout global (header/footer) NO se edita por archivos — usa `get_layout_field`/`set_layout_field`.
|
||||
186
docs/04-pages-and-records.md
Normal file
186
docs/04-pages-and-records.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# Páginas y Registros
|
||||
|
||||
Este documento explica cómo Acai modela las páginas del sitio: toda fila con campo `enlace` es una página, y según el campo `controlador` puede ser **Builder** (modular, contenido por módulos) o **Standard** (contenido directo en los campos del registro). Cubre los tipos de tabla por `menuType` (`category`, `multi`, `single`, `separador`), las particularidades de la tabla `apartados`, los campos de visibilidad (`visible_en_el_menu` vs `visible`), las reglas inviolables sobre `enlace` y `controlador`, y el patrón canónico para implementar el detalle de un registro vía sección general `custom-{tableName}`. Léelo antes de crear, modificar o eliminar cualquier registro de tabla con `enlace`.
|
||||
|
||||
## Tipos de página
|
||||
|
||||
Todo registro con campo `enlace` es una **página pública**. El campo `controlador` decide cómo se renderiza.
|
||||
|
||||
### Builder (modular)
|
||||
|
||||
- `controlador` = `cms/lib/plugins/builder_saas/controlador.php`
|
||||
- El contenido se construye con **módulos** (drag & drop)
|
||||
- El campo `builder` contiene un array JSON de instancias de módulos
|
||||
- Tools: `add_module_to_record`, `set_module_config_vars`, `list_page_modules`, `reorder_module`, `toggle_module_visibility`
|
||||
- La página renderiza los módulos en orden
|
||||
|
||||
### Standard (campos directos)
|
||||
|
||||
- `controlador` = `cms/lib/plugins/builder_saas/controlador_tabla.php`
|
||||
- El contenido vive en los campos del registro (`content`, `titulo_alternativo`, etc.)
|
||||
- El campo `content` es HTML (wysiwyg)
|
||||
- Tool: `create_or_update_record` para editar el contenido directamente
|
||||
- No se usan módulos del builder
|
||||
|
||||
### Cómo detectar el tipo
|
||||
|
||||
**Siempre comprueba el campo `controlador` del registro**:
|
||||
- Contiene `controlador.php` (sin `_tabla`) → **Builder**
|
||||
- Contiene `controlador_tabla.php` → **Standard**
|
||||
|
||||
## Tipos de tabla con páginas (sections)
|
||||
|
||||
Las tablas con páginas se llaman **secciones**. El campo `menuType` del schema define el tipo:
|
||||
|
||||
### `menuType = "category"`
|
||||
|
||||
- **Jerárquico** — páginas con relaciones padre/hijo
|
||||
- Campos especiales: `parentNum`, `depth`, `globalOrder`, `lineage`, `siblingOrder`, `breadcrumb`
|
||||
- Ejemplo: `apartados` (páginas principales del sitio)
|
||||
- Visibilidad: campo `visible_en_el_menu` (1 visible, 0 oculto)
|
||||
- Orden: `globalOrder`
|
||||
|
||||
### `menuType = "multi"`
|
||||
|
||||
- **Lista plana** — sin jerarquía
|
||||
- Orden: `dragSortOrder`
|
||||
- Ejemplos: `blog`, `noticias`, `productos`, `vacantes`, `travesias`
|
||||
- Visibilidad: campo `visible` (1 visible, 0 oculto)
|
||||
|
||||
### `menuType = "single"`
|
||||
|
||||
- Página única (un solo registro)
|
||||
- Ejemplos: home, about us
|
||||
- No requiere campos de orden
|
||||
|
||||
### `menuType = "separador"`
|
||||
|
||||
- Separador visual en el menú del admin (no es una tabla con datos)
|
||||
|
||||
## La tabla `apartados`
|
||||
|
||||
La tabla principal de páginas en la mayoría de sitios Acai. Características:
|
||||
|
||||
| Campo | Descripción |
|
||||
|-------|-------------|
|
||||
| `num` | Primary key |
|
||||
| `name` | Nombre de la página (campo título) |
|
||||
| `enlace` | URL pública (ya con barras: `/servicios/`) |
|
||||
| `controlador` | Define Builder vs Standard |
|
||||
| `menuType` | `"category"` (jerárquico) |
|
||||
| `parentNum` | `num` del padre (`0` = raíz) |
|
||||
| `depth` | Nivel de anidamiento (`0` raíz, `1` hijo, `2` nieto) |
|
||||
| `globalOrder` | Orden global en el árbol |
|
||||
| `visible_en_el_menu` | `1` visible, `0` oculto |
|
||||
| `breadcrumb` | Auto-generado |
|
||||
| `builder` | JSON con instancias de módulos (si es Builder) |
|
||||
| `content` | HTML del contenido (si es Standard) |
|
||||
|
||||
Cada registro de `apartados` puede ser Builder o Standard — comprueba `controlador` por registro.
|
||||
|
||||
## Reglas críticas para páginas
|
||||
|
||||
### NUNCA cambies el campo `enlace`
|
||||
|
||||
A menos que el usuario te lo pida explícitamente, **nunca modifiques `enlace`**. Cambiarlo:
|
||||
- Rompe enlaces externos (SEO, marcadores, otros sitios)
|
||||
- Rompe enlaces internos del propio sitio
|
||||
- Genera 404 hasta que se regeneren los aliases
|
||||
|
||||
Si el usuario sí pide cambiarlo, considera usar la tool `regenerate_enlaces` con `generateAlias=true` para mantener los enlaces antiguos como redirects.
|
||||
|
||||
### NUNCA cambies el campo `controlador`
|
||||
|
||||
`controlador` define si la página es Builder o Standard. Cambiarlo a posteriori rompe el render. Solo se setea durante la creación.
|
||||
|
||||
### Campos de visibilidad
|
||||
|
||||
Comprueba siempre qué campo tiene la tabla antes de cambiar visibilidad:
|
||||
- `apartados` y otras categorías → `visible_en_el_menu`
|
||||
- `blog`, `travesias`, `noticias` y otras multi → `visible`
|
||||
|
||||
### Campos de título
|
||||
|
||||
- Algunas tablas usan `name` (e.g. `apartados`)
|
||||
- Otras usan `title` (e.g. `blog`, `travesias`)
|
||||
- Consulta el schema (`get_table_schema`) antes de asumir.
|
||||
|
||||
## Trabajar con páginas Builder
|
||||
|
||||
### Añadir contenido a una página Builder nueva
|
||||
|
||||
1. Listar módulos disponibles con `acai-glob` (`template/estandar/modulos/*/builder.json`).
|
||||
2. `add_module_to_record` (uno cada vez, en orden). Devuelve `sectionId`.
|
||||
3. `set_module_config_vars` con el `sectionId` para rellenar variables. Devuelve `uploadFields`.
|
||||
4. Imágenes: `upload_record_image` o `generate_image` usando los `recordNum` y `fieldName` de `uploadFields`.
|
||||
5. `navigate_browser` para que el usuario vea el resultado.
|
||||
|
||||
### Editar una página Builder existente
|
||||
|
||||
1. `list_page_modules` — ver el layout actual (sectionIds, posiciones, visibilidades).
|
||||
2. `get_module_config_vars` — leer las variables actuales del módulo a modificar.
|
||||
3. `set_module_config_vars` — actualizar valores.
|
||||
4. O editar el template del módulo: `acai-view` + `acai-line-replace` sobre `index-base.tpl` (compila automáticamente).
|
||||
5. `reorder_module` para mover módulos, `toggle_module_visibility` para ocultar/mostrar.
|
||||
|
||||
## Trabajar con páginas Standard
|
||||
|
||||
Usa `create_or_update_record`. Campos típicos:
|
||||
|
||||
| Campo | Descripción |
|
||||
|-------|-------------|
|
||||
| `content` | HTML del contenido (wysiwyg) |
|
||||
| `titulo_alternativo` | Título mostrado en la página |
|
||||
| `titulo_de_pagina` | `<title>` del navegador (SEO) |
|
||||
| `metatag_descripcion` | Meta description (SEO) |
|
||||
|
||||
```
|
||||
create_or_update_record:
|
||||
tableName: "apartados"
|
||||
recordId: "87"
|
||||
fields:
|
||||
content: "<h2>Nuestros Servicios</h2><p>Ofrecemos…</p>"
|
||||
titulo_de_pagina: "Servicios | Mi Sitio"
|
||||
metatag_descripcion: "Descubre nuestros servicios…"
|
||||
```
|
||||
|
||||
## Patrón canónico — Detalle de registro
|
||||
|
||||
Para cualquier tabla con campo `enlace` (productos, noticias, vacantes, servicios), **el detalle se resuelve por convención** vía sección general `custom-{tableName}`. Ver `03-modules-and-sections.md` para detalles.
|
||||
|
||||
Reglas duras:
|
||||
- **NO** crees una página por registro en `apartados`.
|
||||
- **NO** uses ni configures `_detailPage` — no existe.
|
||||
- **NO** uses query params (`?id=5`) en URLs.
|
||||
- **NO** uses hooks para cargar el registro — `thisrecord` ya existe.
|
||||
- El nombre del módulo es **literalmente** `custom-` + `tableName`.
|
||||
|
||||
Flujo para una funcionalidad tipo "vacantes":
|
||||
1. `create_table` con `enlace=true`
|
||||
2. `create_field` para los campos necesarios
|
||||
3. Crear `template/estandar/modulos/custom-vacantes/index-base.tpl` que use `thisrecord.*`
|
||||
4. (Opcional) Módulo de listado `vacantes_listado` que consulte y enlace
|
||||
5. (Opcional) Página índice `/vacantes/` en `apartados` con el módulo de listado
|
||||
|
||||
## Campos típicos en tablas "publicables"
|
||||
|
||||
Cuando creas una tabla con `enlace` (noticias, vacantes, blog), añade por defecto:
|
||||
|
||||
| Campo | Tipo | Uso |
|
||||
|-------|------|-----|
|
||||
| `fecha_publicacion` | date | Ordenar y filtrar |
|
||||
| `fecha_expiracion` | date (opcional) | Oculta el registro automáticamente cuando caduca |
|
||||
| `visible` | checkbox | Control manual |
|
||||
|
||||
NO añadas un campo "estado" calculado si ya tienes `visible` + fechas.
|
||||
|
||||
## Reglas críticas
|
||||
|
||||
1. **Toda tabla con `enlace` produce páginas públicas.** Comprueba el `controlador` para saber si son Builder o Standard.
|
||||
2. **NUNCA modifiques `enlace`** salvo petición explícita del usuario.
|
||||
3. **NUNCA modifiques `controlador`** de un registro existente.
|
||||
4. **PK siempre es `num`**, nunca `id`. Foreign keys con sufijo `_num`.
|
||||
5. **Visibilidad**: `visible_en_el_menu` para `category`, `visible` para `multi`. Comprueba el schema.
|
||||
6. **Detalle de registro = `custom-{tableName}`**, nunca página duplicada.
|
||||
7. **Tablas sin prefijo `cms_`** en todas las llamadas a tools.
|
||||
8. **Para formularios estándar usa `c-form`**, no construyas POST/hook custom.
|
||||
272
docs/05-tables-and-fields.md
Normal file
272
docs/05-tables-and-fields.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Tablas y Campos
|
||||
|
||||
Este documento explica cómo gestionar tablas y campos en Acai usando las tools del MCP. Cubre: cómo se almacena el schema (`cms/data/schema/{tabla}.ini.php`), los `menuType` (`multi`, `single`, `category`, `separador`), el flag `enlace` para tablas públicas, todos los tipos de campo (`textfield`, `textbox`, `wysiwyg`, `codigo`, `date`, `list`, `checkbox`, `upload`, `multitext`, `separator`), los props comunes (`isRequired`, `defaultValue`, `optionsType`, etc.), la diferencia entre operaciones reversibles e irreversibles (`dropData`, `dropColumn`, rename), y el flujo correcto para crear una funcionalidad nueva. Léelo antes de usar cualquier tool del grupo `tables/`.
|
||||
|
||||
## Schemas
|
||||
|
||||
Cada tabla tiene un schema en `cms/data/schema/{tabla}.ini.php`. Define:
|
||||
- Nombres y tipos de campo
|
||||
- Reglas de validación
|
||||
- Relaciones (foreign keys)
|
||||
- Configuración de display (orden, ancho, etc.)
|
||||
- Bloque `[meta]` con `menuName`, `menuType`, `menuOrder`, `controller`, etc.
|
||||
|
||||
Antes de operar sobre una tabla, **siempre** consulta el schema:
|
||||
- `list_tables` — inventario rápido del proyecto
|
||||
- `get_table_schema` con `tableName` (sin `cms_`) — schema completo
|
||||
- `get_table_schema` con `minimal=true` — solo nombres + tipos + labels (ahorra tokens)
|
||||
- `get_table_schema` con `filterFields="galeria|foto|image"` — filtra por palabras clave
|
||||
|
||||
**NUNCA inventes nombres de campos o tablas.** Siempre confirma con el schema.
|
||||
|
||||
## Convenciones inmutables
|
||||
|
||||
| Regla | Valor correcto |
|
||||
|-------|----------------|
|
||||
| Nombres de tabla en tools/Twig/CmsApi | sin prefijo `cms_` |
|
||||
| Nombres en `queryDB` | con prefijo `cms_` |
|
||||
| Primary key | `num` (siempre) |
|
||||
| Foreign key | `<entidad>_num` (e.g. `categoria_num`) |
|
||||
| Upload field | array `[{urlPath, info1, info2, info3, info4}]` |
|
||||
|
||||
## Crear una tabla — `create_table`
|
||||
|
||||
```
|
||||
create_table({
|
||||
tableName: "vacantes", // sin cms_, lowercase + underscores
|
||||
menuName: "Vacantes", // display en sidebar admin
|
||||
menuType: "multi", // multi | single | category | separador
|
||||
enlace: true, // ¿es tabla pública con URLs?
|
||||
seoMetas: true, // añade campos SEO meta (default false)
|
||||
menuOrder: 5 // opcional, orden en sidebar
|
||||
})
|
||||
```
|
||||
|
||||
### Decisiones obligatorias antes de llamar
|
||||
|
||||
- **`enlace: true|false`** es una decisión de arquitectura. **PREGUNTA AL USUARIO** antes de llamar:
|
||||
- `true` → la tabla genera URLs públicas, automáticamente añade campo `enlace` + slug. Cada registro será una página y puede tener detalle vía `custom-{tableName}`.
|
||||
- `false` → tabla puramente administrativa (categorías internas, configuraciones, logs).
|
||||
- **`menuType`**:
|
||||
- `multi` → lista plana (productos, noticias, vacantes)
|
||||
- `single` → un único registro (home, configuración, about us)
|
||||
- `category` → contenedor jerárquico que agrupa otras tablas en el menú
|
||||
- `separador` → solo un separador visual
|
||||
|
||||
### Después de crear la tabla
|
||||
|
||||
1. Añade los campos necesarios con `create_field` (uno por uno).
|
||||
2. Si la tabla tiene `enlace: true`, considera crear la sección general `custom-{tableName}` para el detalle (ver `03-modules-and-sections.md`).
|
||||
3. Si la tabla quiere ordenar/filtrar por fechas, añade campos `fecha_publicacion`, `fecha_expiracion`, `visible` (ver `04-pages-and-records.md`).
|
||||
|
||||
## Crear un campo — `create_field`
|
||||
|
||||
```
|
||||
create_field({
|
||||
tableName: "vacantes",
|
||||
fieldName: "salario_minimo", // identificador SQL-safe
|
||||
label: "Salario Mínimo", // display en formulario admin
|
||||
type: "textfield",
|
||||
initialProps: { // opcional — overrides de defaults
|
||||
isRequired: 1,
|
||||
maxLength: 100
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Tipos de campo
|
||||
|
||||
| Tipo | Uso |
|
||||
|------|-----|
|
||||
| `textfield` | Texto de una línea |
|
||||
| `textbox` | Texto multilínea plano |
|
||||
| `wysiwyg` | Editor de texto enriquecido |
|
||||
| `codigo` | Editor de código (HTML/JS/CSS snippet) |
|
||||
| `date` | Selector de fecha o datetime |
|
||||
| `list` | Select / radio / checkboxes (necesita `listType` + `optionsType` en `initialProps`) |
|
||||
| `checkbox` | Booleano (1/0) |
|
||||
| `upload` | Subida de archivos (imágenes, docs) |
|
||||
| `multitext` | Repetidor de entradas de texto |
|
||||
| `separator` | Separador visual en el formulario (sin columna en BD) |
|
||||
|
||||
### Props comunes (`initialProps`)
|
||||
|
||||
Pasa solo los que quieres sobrescribir; el resto usa defaults.
|
||||
|
||||
| Prop | Aplica a | Descripción |
|
||||
|------|----------|-------------|
|
||||
| `isRequired` | todos | `1` o `0` |
|
||||
| `isUnique` | textfield, textbox | `1` o `0` |
|
||||
| `defaultValue` | todos | Valor por defecto |
|
||||
| `description` | todos | Texto de ayuda en el formulario |
|
||||
| `minLength` / `maxLength` | textfield, textbox, wysiwyg | Longitud min/max |
|
||||
| `listType` | list | `select`, `radio`, `checkboxes` |
|
||||
| `optionsType` | list | `text` (opciones fijas) o `tablename` (opciones desde otra tabla) |
|
||||
| `optionsText` | list (text) | `"opcion1,opcion2,|valor3,etiqueta3"` |
|
||||
| `optionsTablename` | list (tablename) | Tabla origen (sin `cms_`) |
|
||||
| `optionsValueField` | list (tablename) | Campo del valor (típico: `num`) |
|
||||
| `optionsLabelField` | list (tablename) | Campo de la etiqueta |
|
||||
| `optionsQuery` | list (tablename) | Filtro WHERE adicional |
|
||||
| `filterField` | list (tablename) | Filtro dinámico por valor de otro campo |
|
||||
| `allowedExtensions` | upload | `"jpg,png,webp,pdf"` |
|
||||
| `maxUploads` | upload | Número máximo de archivos |
|
||||
| `createThumbnails` | upload | `1` o `0` |
|
||||
| `maxThumbnailWidth` / `maxThumbnailHeight` | upload | px |
|
||||
| `fieldWidth` / `fieldHeight` | upload | px sugeridos al builder |
|
||||
| `adminOnly` | todos | `1` oculta el campo en formularios públicos |
|
||||
| `charsetRule` | textfield | Restricciones de caracteres |
|
||||
| `tipoTags` | wysiwyg | Tags HTML permitidos |
|
||||
|
||||
### Ejemplo: lista desde tabla
|
||||
|
||||
```
|
||||
create_field({
|
||||
tableName: "vacantes",
|
||||
fieldName: "categoria_num",
|
||||
label: "Categoría",
|
||||
type: "list",
|
||||
initialProps: {
|
||||
listType: "select",
|
||||
optionsType: "tablename",
|
||||
optionsTablename: "categorias",
|
||||
optionsValueField: "num",
|
||||
optionsLabelField: "nombre"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Actualizar un campo — `update_field`
|
||||
|
||||
```
|
||||
update_field({
|
||||
tableName: "vacantes",
|
||||
fieldName: "descripcion",
|
||||
newFieldName: "descripcion_corta", // OPCIONAL — renombra columna MySQL
|
||||
props: {
|
||||
label: "Descripción Corta",
|
||||
maxLength: 200
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Casos destructivos
|
||||
|
||||
- **`newFieldName`** renombra la columna MySQL. Los datos se preservan, pero **rompe cualquier referencia hardcodeada** (Twig, hooks, JS, queryDB). Audita el código antes de renombrar.
|
||||
- **Cambiar `type`** puede coercer/truncar datos (ej. `wysiwyg` → `textfield` elimina HTML). El backend devuelve `warnings` en la respuesta — **muéstralos al usuario**.
|
||||
|
||||
## Borrar un campo — `delete_field`
|
||||
|
||||
```
|
||||
delete_field({
|
||||
tableName: "vacantes",
|
||||
fieldName: "campo_obsoleto",
|
||||
dropColumn: false // default
|
||||
})
|
||||
```
|
||||
|
||||
- `dropColumn: false` → solo elimina del schema. Si la columna MySQL tiene datos, el backend rechaza y devuelve `dataCount` para que avises al usuario.
|
||||
- `dropColumn: true` → `ALTER TABLE DROP COLUMN`. **Los datos de esa columna se pierden permanentemente.**
|
||||
|
||||
## Borrar una tabla — `delete_table`
|
||||
|
||||
```
|
||||
delete_table({
|
||||
tableName: "tabla_obsoleta",
|
||||
dropData: false, // default
|
||||
dryRun: true // pre-flight check
|
||||
})
|
||||
```
|
||||
|
||||
- `dryRun: true` → no borra nada, solo reporta `recordCount`. **Úsalo siempre antes de pedir confirmación al usuario.**
|
||||
- `dropData: false` → solo borra el schema (`.ini.php`). Si la tabla MySQL tiene registros, el backend rechaza.
|
||||
- `dropData: true` → `DROP TABLE` + delete schema. **Datos perdidos permanentemente.**
|
||||
|
||||
## Reordenar — `reorder_tables`, `reorder_fields`
|
||||
|
||||
Pasa la lista completa ordenada de nombres. Solo cambia el orden visual, los datos no se tocan.
|
||||
|
||||
```
|
||||
reorder_tables({ order: ["apartados", "blog", "productos", "vacantes"] })
|
||||
|
||||
reorder_fields({
|
||||
tableName: "vacantes",
|
||||
order: ["titulo", "descripcion", "salario_minimo", "fecha_publicacion", "visible"]
|
||||
})
|
||||
```
|
||||
|
||||
Los campos del sistema (`num`, `creationDate`, etc.) se ignoran automáticamente.
|
||||
|
||||
## Actualizar metadata — `update_table_metadata`
|
||||
|
||||
Modifica el bloque `[meta]` del schema.
|
||||
|
||||
```
|
||||
update_table_metadata({
|
||||
tableName: "vacantes",
|
||||
newTableName: "ofertas_empleo", // OPCIONAL — renombra tabla MySQL
|
||||
meta: {
|
||||
menuName: "Ofertas de Empleo",
|
||||
menuOrder: 3,
|
||||
listPageFields: "titulo,fecha_publicacion,visible",
|
||||
breadcrumbField: "titulo"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Keys aceptadas en `meta`:
|
||||
`menuName`, `menuDesc`, `menuType`, `menuOrder`, `menuDisplay`, `menuHidden`, `controller`, `breadcrumbField`, `breadcrumbByLink`, `breadcrumbParentNum`, `listPageFields` (csv), `listPageOrder`, `listPageSearchFields`.
|
||||
|
||||
**`newTableName` renombra la tabla MySQL** y rompe cualquier referencia hardcodeada (controllers custom, módulos con SQL embebido, queryDB en plantillas). Audita el código antes y avisa al usuario.
|
||||
|
||||
## Regenerar enlaces — `regenerate_enlaces`
|
||||
|
||||
Regenera el campo `enlace` (slug) de todos los registros de una tabla. **Cambia URLs públicas** — todo lo que apunte a las antiguas dará 404 a menos que actives los aliases.
|
||||
|
||||
```
|
||||
regenerate_enlaces({
|
||||
tableName: "vacantes",
|
||||
generateAlias: true // recomendado si la tabla ya es pública
|
||||
})
|
||||
```
|
||||
|
||||
- `generateAlias: false` (default) — solo actualiza `enlace`. URLs antiguas → 404.
|
||||
- `generateAlias: true` — escribe entradas en `alias_urls` para redirigir las URLs antiguas a las nuevas. Más seguro.
|
||||
|
||||
## Listar tablas — `list_tables`
|
||||
|
||||
Devuelve todas las tablas con su `menuName`, `menuType`, `menuOrder` y `tableName`. Sin prefijo `cms_`. Úsalo cuando necesites un inventario rápido.
|
||||
|
||||
## Tipos de campo y formato al insertar/actualizar registros
|
||||
|
||||
Al usar `create_or_update_record`, cada tipo espera un formato específico:
|
||||
|
||||
| Tipo | Formato | Ejemplo |
|
||||
|------|---------|---------|
|
||||
| `textfield` | String | `"Texto"` |
|
||||
| `textbox` | String multilínea | `"Línea 1\nLínea 2"` |
|
||||
| `date`/datetime | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` |
|
||||
| `wysiwyg` | String HTML | `"<p>Texto</p>"` |
|
||||
| `list` | String o número | `"activo"` o `"1"` (num si es FK) |
|
||||
| `checkbox` | Número 1/0 | `1` o `0` |
|
||||
| `multitext` | String JSON | `"[{\"item\":\"valor\"}]"` |
|
||||
| `upload` | **NO enviar** | Usa `upload_record_image` después de crear el registro |
|
||||
|
||||
Ver `06-hooks-and-cmsapi.md` para los detalles de `CmsApi::insert` / `update`.
|
||||
|
||||
## Flujo canónico — Funcionalidad nueva tipo "vacantes"
|
||||
|
||||
1. `create_table({ tableName: "vacantes", menuType: "multi", enlace: true, seoMetas: true })` — pregunta al usuario si quiere `enlace` y `seoMetas`.
|
||||
2. `create_field` para cada campo: `titulo`, `descripcion` (wysiwyg), `salario_minimo` (textfield), `categoria_num` (list desde tabla), `fecha_publicacion` (date), `fecha_expiracion` (date), `visible` (checkbox), `imagen_destacada` (upload).
|
||||
3. Crear sección general `template/estandar/modulos/custom-vacantes/index-base.tpl` con `acai-write` (compila automáticamente).
|
||||
4. (Opcional) Módulo de listado `vacantes_listado_xxxxxx` que liste registros con `'vacantes' | get('visible=1', 'fecha_publicacion DESC', 20)`.
|
||||
5. (Opcional) Página índice `/vacantes/` en `apartados` con el módulo de listado.
|
||||
|
||||
## Reglas críticas
|
||||
|
||||
1. **Tabla sin prefijo `cms_`** en todas las tools. PK siempre `num`.
|
||||
2. **Antes de cualquier operación**: `get_table_schema` para confirmar nombres y tipos de campo.
|
||||
3. **Pregunta al usuario antes de `create_table`** sobre `enlace` y `seoMetas` (decisiones de arquitectura).
|
||||
4. **`dropData`, `dropColumn`, `newFieldName`, `newTableName`** son destructivos o irreversibles — pide confirmación explícita.
|
||||
5. **`regenerate_enlaces`**: usa `generateAlias: true` si la tabla ya tiene tráfico público.
|
||||
6. **Surfacea los `warnings`** que el backend devuelve (cambios de tipo, renames, conteos de datos en riesgo).
|
||||
7. **Upload fields no se setean en insert/update** — usa `upload_record_image` después.
|
||||
383
docs/06-hooks-and-cmsapi.md
Normal file
383
docs/06-hooks-and-cmsapi.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# Hooks y CmsApi (server-side)
|
||||
|
||||
Este documento describe cómo crear y consumir hooks PHP en Acai (lógica server-side) y cómo usar `CmsApi` (alias de `CocoDB`) para acceder a la base de datos. Cubre las dos ubicaciones válidas para un hook (global en `hooks/hooks.<id>.php` o propio de módulo en `template/estandar/modulos/<id>/hook.php`), las cuatro formas de invocarlo (filtro Twig, etiqueta `<hook>`, JS `CmsApi.hook`, `c-form`), las reglas obligatorias (devolver array, no `echo`, no `exit`), la API completa de `CmsApi::get/insert/update/delete` con sus opciones (`uploads`, `relations`, `translates`, `groupBy`, `aggregates`), y la tool `set_hook_middleware` para que un hook global se ejecute automáticamente antes de renderizar páginas. Léelo antes de crear cualquier `.php` de hook.
|
||||
|
||||
## Hooks — qué son y dónde viven
|
||||
|
||||
Hooks son archivos PHP que ejecutan lógica server-side. Hay dos ubicaciones válidas:
|
||||
|
||||
### Hook global
|
||||
|
||||
- Archivo: `hooks/hooks.<hook-id>.php`
|
||||
- Endpoint: `/hooks/<hook-id>/`
|
||||
- Úsalo cuando la lógica se reutiliza entre módulos, páginas o formularios.
|
||||
|
||||
Ejemplo: `hooks/hooks.calcular_precio.php` → endpoint `/hooks/calcular_precio/`
|
||||
|
||||
### Hook propio de módulo
|
||||
|
||||
- Archivo: `template/estandar/modulos/<module-id>/hook.php`
|
||||
- Endpoint: `/hooks/<module-id>/`
|
||||
- Úsalo cuando la lógica pertenece solo a ese módulo.
|
||||
|
||||
Ejemplo: `template/estandar/modulos/buscadorapartados_hjd8s/hook.php` → endpoint `/hooks/buscadorapartados_hjd8s/`
|
||||
|
||||
### Regla práctica
|
||||
|
||||
- Si la lógica solo sirve a un módulo → `hook.php` dentro del módulo.
|
||||
- Si varias piezas la consumen → hook global en `hooks/`.
|
||||
|
||||
## Reglas obligatorias
|
||||
|
||||
- Devuelve datos con `return [...]`. **NUNCA** uses `echo json_encode(...)` ni `exit`.
|
||||
- Para leer parámetros usa `$_REQUEST[...]` o las variables ya inyectadas (los parámetros pasados al llamar el hook se convierten en variables PHP del mismo nombre).
|
||||
- En hooks usa `CmsApi::get()` o `CocoDB::get()` como primera opción.
|
||||
- **NO uses `CocoDB::getInstance()`** salvo necesidad excepcional.
|
||||
- **NO escribas SQL manual con `prepare()/bind_param()`** salvo que de verdad no haya alternativa con `CmsApi` o `CocoDB`.
|
||||
|
||||
### Estructura de un hook
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Si llamas el hook con { cantidad: 10, tipo: 'mayoreo' },
|
||||
// recibes $cantidad = 10 y $tipo = 'mayoreo' como variables.
|
||||
|
||||
$precioUnitario = 50;
|
||||
|
||||
if ($tipo === 'mayoreo' && $cantidad > 10) {
|
||||
$precioUnitario *= 0.85; // 15% descuento
|
||||
}
|
||||
|
||||
return [
|
||||
"success" => true,
|
||||
"precioUnitario" => round($precioUnitario, 2),
|
||||
"total" => round($precioUnitario * $cantidad, 2),
|
||||
"descuento" => $tipo === 'mayoreo' ? 15 : 0
|
||||
];
|
||||
```
|
||||
|
||||
## Cómo invocar un hook
|
||||
|
||||
### Desde Twig (filtro `hook`)
|
||||
|
||||
```twig
|
||||
{% set resultado = 'hooks/calcular_precio/' | hook({cantidad: 5, tipo: 'mayoreo'}) %}
|
||||
<p>Total: {{ resultado.total }}€</p>
|
||||
```
|
||||
|
||||
### Desde HTML (etiqueta `<hook>`)
|
||||
|
||||
```html
|
||||
<hook result="precio" endpoint="/hooks/calcular_precio/" :cantidad="10" :tipo="'mayoreo'"></hook>
|
||||
<p>{{ precio.message }}</p>
|
||||
```
|
||||
|
||||
### Desde JavaScript
|
||||
|
||||
```js
|
||||
CmsApi.hook('/hooks/calcular_precio/', { cantidad: 10, tipo: 'mayoreo' }, (data) => {
|
||||
console.log(data.total);
|
||||
});
|
||||
```
|
||||
|
||||
Si llamas a un hook propio del módulo desde su `script.js`, usa el `module-id` real (`/hooks/buscadorapartados_hjd8s/`). NO construyas el endpoint con Twig dentro de `script.js` — pásalo desde `index-base.tpl` vía `data-hook-endpoint`.
|
||||
|
||||
### Desde otro hook PHP
|
||||
|
||||
```php
|
||||
$result = hook("/hooks/calcular_precio/", ["cantidad" => 5, "tipo" => "mayoreo"]);
|
||||
$mensaje = $result["message"];
|
||||
```
|
||||
|
||||
### Desde `c-form`
|
||||
|
||||
Los hooks se ejecutan automáticamente al enviar el formulario si están configurados en los atributos del `c-form`.
|
||||
|
||||
### Testing manual
|
||||
|
||||
Con Docker corriendo:
|
||||
```bash
|
||||
curl {ACAI_WEB_URL}/hooks/calcular_precio/
|
||||
```
|
||||
Usa la URL real del proyecto (devuélvela con `get_web_url`), no `localhost:8080`. En desarrollo local no necesitas `X-Hooks-Token`.
|
||||
|
||||
## CmsApi (PHP)
|
||||
|
||||
API server-side para BD. Disponible en todos los hooks. Alias de `CocoDB`.
|
||||
|
||||
### Read — `CmsApi::get()`
|
||||
|
||||
```php
|
||||
// Todos los registros
|
||||
$products = CmsApi::get("productos");
|
||||
|
||||
// Con WHERE string
|
||||
$active = CmsApi::get("productos", "activo=1");
|
||||
|
||||
// Con orden y límite
|
||||
$latest = CmsApi::get("noticias", "", "fecha DESC", 5);
|
||||
|
||||
// Condiciones complejas
|
||||
$caros = CmsApi::get("productos", "precio > 100");
|
||||
$resultados = CmsApi::get("productos", "activo = 1 AND stock > 0");
|
||||
|
||||
// Operadores
|
||||
$expensive = CmsApi::get("productos", "precio >= 100");
|
||||
$search = CmsApi::get("productos", "nombre LIKE '%keyword%'");
|
||||
$inList = CmsApi::get("productos", "categoria_num IN (1, 2, 3)");
|
||||
|
||||
// Con opciones
|
||||
$datos = CmsApi::get("productos", "", "", "", [
|
||||
'translates' => true,
|
||||
'uploads' => true,
|
||||
'relations' => true,
|
||||
'relationsDepth' => 2
|
||||
]);
|
||||
```
|
||||
|
||||
#### Opciones
|
||||
|
||||
| Option | Tipo | Default | Descripción |
|
||||
|--------|------|---------|-------------|
|
||||
| `uploads` | bool | `true` | Incluir datos de upload fields |
|
||||
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['categoria']` |
|
||||
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
|
||||
| `translates` | string | idioma actual | Código de idioma para `| translate` |
|
||||
| `groupBy` | string | null | Cláusula GROUP BY |
|
||||
| `aggregates` | array | `[]` | Funciones de agregación |
|
||||
| `onlyFields` | array | null | Seleccionar solo ciertos campos |
|
||||
| `debug` | bool | false | Imprime el SQL generado |
|
||||
| `redis` | bool | null | Forzar cache Redis |
|
||||
| `redis_expire` | int | 60 | TTL del cache (segundos) |
|
||||
|
||||
### Insert — `CmsApi::insert()`
|
||||
|
||||
```php
|
||||
// Un registro
|
||||
CmsApi::insert('contacto', [
|
||||
["nombre" => "John", "email" => "john@example.com", "mensaje" => "Hola"]
|
||||
]);
|
||||
|
||||
// Múltiples registros
|
||||
CmsApi::insert('productos', [
|
||||
["nombre" => "Producto A", "precio" => 100],
|
||||
["nombre" => "Producto B", "precio" => 200]
|
||||
]);
|
||||
|
||||
// Retornar el last id
|
||||
$result = CmsApi::insert('productos',
|
||||
[["nombre" => "Nuevo", "precio" => 150]],
|
||||
[],
|
||||
['return_last_id' => true]
|
||||
);
|
||||
$nuevoNum = $result['lastId'];
|
||||
```
|
||||
|
||||
#### Opciones de insert
|
||||
|
||||
| Option | Descripción |
|
||||
|--------|-------------|
|
||||
| `forceNum` | Permite setear el campo `num` manualmente |
|
||||
| `ignoreSchema` | Saltar validación de schema |
|
||||
| `ignoreFields` | Array de campos a ignorar |
|
||||
| `return_last_id` | Devuelve el `num` del último insert |
|
||||
|
||||
### Update — `CmsApi::update()`
|
||||
|
||||
```php
|
||||
// Con WHERE string
|
||||
CmsApi::update('productos', ["precio" => 150], "num=1");
|
||||
|
||||
// Con WHERE array
|
||||
CmsApi::update('productos',
|
||||
["activo" => 1],
|
||||
[["column" => "num", "operator" => "=", "value" => 1]]
|
||||
);
|
||||
|
||||
// Múltiples registros
|
||||
CmsApi::update('productos', ["activo" => 0], "precio < 50");
|
||||
```
|
||||
|
||||
### Delete — `CmsApi::delete()`
|
||||
|
||||
```php
|
||||
CmsApi::delete('productos', "num=5");
|
||||
|
||||
CmsApi::delete('productos',
|
||||
[["column" => "activo", "operator" => "=", "value" => 0]]
|
||||
);
|
||||
```
|
||||
|
||||
### Reglas CmsApi
|
||||
|
||||
- Nombres de tabla **sin prefijo `cms_`**
|
||||
- Primary key siempre es **`num`**
|
||||
- Foreign keys: **`<entidad>_num`**
|
||||
- Upload fields **NO se setean** vía insert/update — usa `upload_record_image` después.
|
||||
- Operadores soportados: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
|
||||
|
||||
## CocoDB (low-level)
|
||||
|
||||
Capa de BD que usa internamente `CmsApi`. Misma API que `CmsApi` pero con métodos verbosos:
|
||||
- `CocoDB::get($table, $where, $order, $limit, $options)` — idéntico a `CmsApi::get`
|
||||
- `CocoDB::insertRecords($table, $records, $functions, $options)` — idéntico a `CmsApi::insert`
|
||||
- `CocoDB::updateRecords($table, $records, $where, $functions, $options)` — idéntico a `CmsApi::update`
|
||||
- `CocoDB::deleteRecords($table, $where, $options)` — idéntico a `CmsApi::delete`
|
||||
|
||||
Para uso normal, `CmsApi` es suficiente. Acude a `CocoDB` solo para SQL más bajo nivel cuando `CmsApi` no cubra el caso.
|
||||
|
||||
## CmsApi (JavaScript — Client-Side)
|
||||
|
||||
```js
|
||||
// Llamar hook
|
||||
CmsApi.hook('/hooks/calcular_precio/', { cantidad: 10 }, (response) => {
|
||||
console.log(response);
|
||||
});
|
||||
|
||||
// Leer registros (si está expuesto vía hooks)
|
||||
CmsApi.get('productos', { where: 'activo=1' }, (records) => {
|
||||
console.log(records);
|
||||
});
|
||||
```
|
||||
|
||||
## Hook middleware — auto-ejecutar antes de páginas
|
||||
|
||||
Un hook global puede configurarse como **middleware**: se ejecuta automáticamente antes de renderizar ciertas páginas (o todas). Útil para inyección de variables, redirecciones condicionales, control de acceso, parseo de URLs, etc.
|
||||
|
||||
### Tools
|
||||
|
||||
- `get_hook_middleware({ hookEndPoint: "/hooks/parse_styles/" })` — leer config actual
|
||||
- `set_hook_middleware({ hookEndPoint: "/hooks/parse_styles/", middleWare: [...] })` — escribir config
|
||||
|
||||
### Valores de `middleWare`
|
||||
|
||||
| Valor | Comportamiento |
|
||||
|-------|----------------|
|
||||
| `[]` | Hook solo se ejecuta cuando se llama explícitamente (default). |
|
||||
| `["allurls"]` | Se ejecuta antes de **cada página** del sitio. |
|
||||
| `["<tableName>-<num>", ...]` | Se ejecuta antes de **registros específicos**. Ejemplo: `["cms_apartados-2"]` para la home. |
|
||||
|
||||
> Nota: en el array de `middleWare` los `tableName` van **con** prefijo `cms_` (es la representación interna de `layout.json`).
|
||||
|
||||
### Ejemplos
|
||||
|
||||
```
|
||||
// Lógica de redirect que solo afecta a la home (apartados num=2)
|
||||
set_hook_middleware({
|
||||
hookEndPoint: "/hooks/redirect_home/",
|
||||
middleWare: ["cms_apartados-2"]
|
||||
})
|
||||
|
||||
// Inyección global (analytics, vars de sitio)
|
||||
set_hook_middleware({
|
||||
hookEndPoint: "/hooks/global_vars/",
|
||||
middleWare: ["allurls"]
|
||||
})
|
||||
|
||||
// Hook utilitario que se llama desde módulos — sin middleware
|
||||
set_hook_middleware({
|
||||
hookEndPoint: "/hooks/calcular_precio/",
|
||||
middleWare: []
|
||||
})
|
||||
```
|
||||
|
||||
### Reglas
|
||||
|
||||
- El middleware se aplica **solo a hooks globales**, no a hooks de módulo.
|
||||
- Crear el `.php` con `acai-write` **NO** activa middleware automáticamente — hay que llamar `set_hook_middleware` explícitamente.
|
||||
- Lee `get_hook_middleware` antes de modificar para no sobrescribir configuraciones existentes.
|
||||
|
||||
## Schemas y formato de datos al insertar
|
||||
|
||||
Antes de un `CmsApi::insert`/`update` o de un `create_or_update_record` desde MCP, consulta el schema (`get_table_schema`). Tipos de campo y formato esperado:
|
||||
|
||||
| Tipo | Formato | Ejemplo |
|
||||
|------|---------|---------|
|
||||
| `textfield` | String | `"Texto"` |
|
||||
| `textbox` | String multilínea | `"Línea 1\nLínea 2"` |
|
||||
| `date`/datetime | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` |
|
||||
| `wysiwyg` | HTML string | `"<p class=\"font-bold\">Texto</p>"` |
|
||||
| `list` | String o número | `"activo"` o `"1"` |
|
||||
| `checkbox` | Número 1/0 | `1` o `0` |
|
||||
| `multitext` | String JSON | `"[{\"item\":\"valor\"}]"` |
|
||||
| `upload` | NO enviar | Usa `upload_record_image` después |
|
||||
|
||||
## Ejemplos prácticos
|
||||
|
||||
### Hook con operaciones de BD
|
||||
|
||||
```php
|
||||
<?php
|
||||
// hook.php del módulo "procesar_compra"
|
||||
$producto = CmsApi::get("productos", "num=" . intval($producto_id));
|
||||
|
||||
if (empty($producto)) {
|
||||
return ["success" => false, "message" => "Producto no encontrado"];
|
||||
}
|
||||
|
||||
$total = $producto[0]['precio'] * $cantidad;
|
||||
|
||||
// Crear venta
|
||||
$result = CmsApi::insert('ventas', [[
|
||||
"usuario_num" => $usuario_id,
|
||||
"producto_num" => $producto_id,
|
||||
"cantidad" => $cantidad,
|
||||
"total" => $total,
|
||||
"fecha" => date('Y-m-d H:i:s')
|
||||
]], [], ['return_last_id' => true]);
|
||||
|
||||
// Actualizar stock
|
||||
$stock = CmsApi::get("stocks", "producto_num=" . intval($producto_id));
|
||||
if (!empty($stock)) {
|
||||
CmsApi::update('stocks',
|
||||
["cantidad" => $stock[0]['cantidad'] - $cantidad],
|
||||
"producto_num=$producto_id"
|
||||
);
|
||||
}
|
||||
|
||||
return ["success" => true, "ventaNum" => $result['lastId'], "total" => $total];
|
||||
```
|
||||
|
||||
### Hook con relaciones cargadas
|
||||
|
||||
```php
|
||||
<?php
|
||||
$productos = CmsApi::get("productos", "activo=1", "globalOrder ASC", 10, [
|
||||
'uploads' => true,
|
||||
'relations' => true,
|
||||
'relationsDepth' => 1
|
||||
]);
|
||||
|
||||
return [
|
||||
"success" => true,
|
||||
"productos" => $productos
|
||||
];
|
||||
```
|
||||
|
||||
### Hook con búsqueda dinámica
|
||||
|
||||
```php
|
||||
<?php
|
||||
$where = "1=1";
|
||||
if (!empty($termino)) {
|
||||
$termino = addslashes($termino);
|
||||
$where .= " AND (titulo LIKE '%$termino%' OR descripcion LIKE '%$termino%')";
|
||||
}
|
||||
if (!empty($categoria)) {
|
||||
$where .= " AND categoria_num=" . intval($categoria);
|
||||
}
|
||||
|
||||
$resultados = CmsApi::get("productos", $where, "fecha DESC", 20);
|
||||
|
||||
return ["success" => true, "count" => count($resultados), "resultados" => $resultados];
|
||||
```
|
||||
|
||||
## Reglas críticas
|
||||
|
||||
1. Hook devuelve `return [...]` — **nunca** `echo`/`exit`.
|
||||
2. Tablas **sin prefijo `cms_`** en `CmsApi`/`CocoDB`. PK siempre `num`.
|
||||
3. Foreign keys con sufijo `_num` (`categoria_num`, `usuario_num`).
|
||||
4. Upload fields **no** se setean por insert/update.
|
||||
5. Sanea variables externas — `intval()`, `addslashes()` o usa parámetros tipados antes de concatenar en SQL.
|
||||
6. Antes de usar `set_hook_middleware`, lee con `get_hook_middleware` para no sobrescribir.
|
||||
7. Hooks de módulo: endpoint = `/hooks/<module-id>/` (no `/hooks/hook.<id>/`).
|
||||
283
docs/07-css-js-conventions.md
Normal file
283
docs/07-css-js-conventions.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# CSS y JavaScript — Convenciones del Módulo
|
||||
|
||||
Este documento define cómo escribir CSS, JavaScript y, cuando hace falta, Vue 3 dentro de un módulo Acai. Cubre la regla "Tailwind first" + BEM para CSS custom, las clases utilitarias propias de Acai (`transition3s`, `click-a-child`, `line-clamp2`, `lazyload`, `bg-main-color`, etc.), las CSS variables del tema (`--main-color`), el patrón obligatorio de **scoping** vía la clase raíz del módulo, la regla dura de que `script.js` y `style.css` son **archivos estáticos** (sin Twig dentro), cómo pasar valores dinámicos desde `index-base.tpl` a JS vía `data-*`, cuándo usar Vue 3 y cómo integrarlo evitando conflicto de delimiters con Twig, y los componentes nativos del builder (Carousel `c-tns-wrapper`, Lightbox, Breadcrumb, AOS, Lazy loading). Léelo antes de escribir cualquier `style.css` o `script.js`.
|
||||
|
||||
## Estructura del módulo
|
||||
|
||||
- Cada módulo genera HTML + CSS + JS (y opcionalmente Vue 3).
|
||||
- Define una **clase raíz en kebab-case** específica del módulo: `product-card`, `hero-section`, `buscador-apartados`.
|
||||
- **Todo el CSS y JS deben quedar scopeados bajo esa clase raíz.**
|
||||
|
||||
## CSS
|
||||
|
||||
### Tailwind first
|
||||
|
||||
Usa TailwindCSS como método principal. Reserva CSS custom solo cuando Tailwind no cubra el caso (estados complejos, transiciones específicas, animaciones).
|
||||
|
||||
```html
|
||||
<div class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Title</h2>
|
||||
</div>
|
||||
```
|
||||
|
||||
### BEM para CSS custom
|
||||
|
||||
Cuando necesites CSS propio, scopealo bajo la clase raíz con BEM:
|
||||
|
||||
```css
|
||||
.hero-section { }
|
||||
.hero-section__title { }
|
||||
.hero-section__image { }
|
||||
.hero-section--dark { }
|
||||
```
|
||||
|
||||
Nunca uses clases globales sin prefijo de módulo.
|
||||
|
||||
### CSS variables del tema
|
||||
|
||||
| Variable | Descripción |
|
||||
|----------|-------------|
|
||||
| `var(--main-color)` | Color de marca principal |
|
||||
| `var(--main-color-light)` | Variante clara |
|
||||
| `var(--main-color-dark)` | Variante oscura |
|
||||
|
||||
### Estilos inline con fallbacks
|
||||
|
||||
Patrón estándar para colores configurables por el usuario (variables del builder):
|
||||
|
||||
```html
|
||||
<div style="background-color: {{ colordefondo ? colordefondo : 'transparent' }}">
|
||||
<p style="color: {{ colordeltexto ? colordeltexto : '#111827' }}">
|
||||
```
|
||||
|
||||
### Clases utilitarias de Acai
|
||||
|
||||
| Clase | Descripción |
|
||||
|-------|-------------|
|
||||
| `transition3s` | Transición suave 0.3s |
|
||||
| `click-a-child` | Hace al padre clickeable vía el primer `<a>` hijo |
|
||||
| `line-clamp2` / `line-clamp3` / `line-clamp5` | Truncar texto a N líneas |
|
||||
| `filter-white` | Filtro CSS para teñir imágenes/iconos en blanco |
|
||||
| `lazyload` | Activa lazy loading (usar con `data-src`) |
|
||||
| `text-shadow` | Sombra de texto para legibilidad sobre imágenes |
|
||||
| `wysiwyg` | Wrapper para contenido rico (estilos coherentes) |
|
||||
| `bg-main-color` / `bg-main-color-light` / `bg-main-color-dark` | Fondos con color primario |
|
||||
| `text-main-color` / `text-main-color-light` / `text-main-color-dark` | Texto con color primario |
|
||||
| `titulo-main-color` / `titulo-white` | Estilos de título resaltado |
|
||||
| `c-tns-wrapper` / `c-tns-container` / `c-tns-nav-container` | Carousel built-in |
|
||||
| `glightbox` | Activa lightbox |
|
||||
|
||||
### Reglas para `style.css`
|
||||
|
||||
`style.css` es **estático**. Reglas duras:
|
||||
- **NO** uses sintaxis Twig (`{{ ... }}`, `{% ... %}`) ni atributos builder (`c-if`, `c-for`).
|
||||
- **NO** escribas selectores que dependan de `{{ section_id }}` — scopea con la clase raíz del módulo.
|
||||
|
||||
```css
|
||||
/* CORRECTO — scopeado con clase raíz */
|
||||
.product-card { }
|
||||
.product-card__title { color: var(--main-color); }
|
||||
|
||||
/* INCORRECTO — Twig inside */
|
||||
#{{ section_id }} h3 { }
|
||||
```
|
||||
|
||||
## JavaScript
|
||||
|
||||
### `script.js` es estático
|
||||
|
||||
Igual que `style.css`: **NO** uses sintaxis Twig dentro. Si necesitas valores dinámicos, pásalos desde `index-base.tpl` vía atributos `data-*`.
|
||||
|
||||
#### Patrón correcto
|
||||
|
||||
`index-base.tpl`:
|
||||
```html
|
||||
<section class="buscador-apartados"
|
||||
id="{{ section_id }}"
|
||||
data-section-id="{{ section_id }}"
|
||||
data-hook-endpoint="/hooks/buscadorapartados_hjd8s/"
|
||||
data-limit="{{ limite | default('10') }}">
|
||||
<input class="buscador-apartados__input" type="text">
|
||||
<ul class="buscador-apartados__results"></ul>
|
||||
</section>
|
||||
```
|
||||
|
||||
`script.js`:
|
||||
```js
|
||||
document.querySelectorAll('.buscador-apartados').forEach((section) => {
|
||||
const sectionId = section.dataset.sectionId;
|
||||
const hookEndpoint = section.dataset.hookEndpoint;
|
||||
const limit = parseInt(section.dataset.limit, 10);
|
||||
const input = section.querySelector('.buscador-apartados__input');
|
||||
const results = section.querySelector('.buscador-apartados__results');
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
CmsApi.hook(hookEndpoint, { termino: e.target.value, limite: limit }, (data) => {
|
||||
results.innerHTML = data.items.map((it) => `<li>${it.titulo}</li>`).join('');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
NUNCA hagas `CmsApi.hook('/hooks/{{ moduleId }}/', ...)` dentro de `script.js`. Si necesitas el endpoint, pásalo por `data-hook-endpoint`.
|
||||
|
||||
### CmsApi (cliente)
|
||||
|
||||
```js
|
||||
// Llamar hook
|
||||
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, (response) => {
|
||||
console.log(response);
|
||||
});
|
||||
|
||||
// Si llamas un hook propio del módulo, usa el module-id real:
|
||||
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, (response) => {
|
||||
console.log(response);
|
||||
});
|
||||
```
|
||||
|
||||
### Embeber `<script>` directamente — NO
|
||||
|
||||
No embebas `<script>` con lógica del módulo dentro de `index-base.tpl`. La lógica vive en `script.js`. La excepción: scripts mínimos para inicializar Vue 3 (ver más abajo) que sí necesitan los delimitadores `{{ section_id }}` para mountear sobre IDs únicos.
|
||||
|
||||
## Cuándo usar Vue 3
|
||||
|
||||
Usa Vue 3 (vía CDN) cuando la lógica requiera:
|
||||
- Doble binding / reactividad
|
||||
- Solicitudes asíncronas complejas con UI reactiva
|
||||
- Componentes reutilizables dentro del módulo
|
||||
- Gestión de estado local
|
||||
- Ciclos de vida (mounted, unmounted, etc.)
|
||||
|
||||
Para lógica simple (listeners, fetch único, toggle de clases), usa JavaScript vanilla.
|
||||
|
||||
### Integración Vue 3
|
||||
|
||||
```html
|
||||
<div id="app-{{ section_id }}">
|
||||
<p>${ message }</p>
|
||||
<button @click="increment">${ count }</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref } = Vue;
|
||||
createApp({
|
||||
delimiters: ['${', '}'], // OBLIGATORIO — evita conflicto con Twig {{ }}
|
||||
setup() {
|
||||
const message = ref('Hello');
|
||||
const count = ref(0);
|
||||
const increment = () => count.value++;
|
||||
return { message, count, increment };
|
||||
}
|
||||
}).mount('#app-{{ section_id }}');
|
||||
</script>
|
||||
```
|
||||
|
||||
Reglas:
|
||||
- **Siempre** redefine `delimiters` a `['${', '}']` para no chocar con Twig.
|
||||
- Mountea sobre un id único usando `section_id`: `#app-{{ section_id }}`.
|
||||
- Vue se carga como librería global vía `add_global_library` o ya incluida en el proyecto (ver `08-layout-and-libraries.md`).
|
||||
|
||||
## Variables globales disponibles en JS / Twig
|
||||
|
||||
| Variable | Descripción | Ejemplo |
|
||||
|----------|-------------|---------|
|
||||
| `section_id` | ID único por instancia del módulo | `<div id="{{section_id}}">` |
|
||||
| `server.HTTP_HOST` | Dominio actual | `https://{{ server.HTTP_HOST }}/path` |
|
||||
| `loop.index` | Índice 1-based en `c-for`/`{% for %}` | `{{ loop.index }}` |
|
||||
| `loop.index is odd` / `is even` | Para layouts alternados | zigzag |
|
||||
| `interno` | `true` dentro del editor CMS | `c-class="{'editor-mode': interno}"` |
|
||||
|
||||
### Patrón `section_id`
|
||||
|
||||
Cada instancia de un módulo recibe un `section_id` único. Úsalo para anchors, IDs HTML y selector raíz de JS:
|
||||
|
||||
```html
|
||||
<div id="{{section_id}}"></div>
|
||||
<section id="id_{{ section_id }}" class="relative">
|
||||
<!-- contenido -->
|
||||
</section>
|
||||
```
|
||||
|
||||
## Componentes nativos
|
||||
|
||||
### Carousel — `c-tns-wrapper`
|
||||
|
||||
```html
|
||||
<div class="c-tns-wrapper"
|
||||
data-responsive='{"0":1,"768":2,"1024":3}'
|
||||
data-autoplay-timeout="5000"
|
||||
data-mode="carousel"
|
||||
data-speed="400"
|
||||
data-nav="true">
|
||||
<ul class="c-tns-container">
|
||||
<li data-field-type="multiv2" data-field-label="Slides" class="px-2">
|
||||
<!-- contenido del slide -->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| `data-responsive` | Items por breakpoint. JSON `{"0":1,"768":2,"1024":3}` o sintaxis corta `"sm:2, md:3, lg:4"` |
|
||||
| `data-autoplay-timeout` | Intervalo autoplay (ms) |
|
||||
| `data-mode` | `"gallery"` o `"carousel"` |
|
||||
| `data-speed` | Velocidad de transición (ms) |
|
||||
| `data-nav` | `"true"` para mostrar dots |
|
||||
|
||||
Dots de navegación custom:
|
||||
```html
|
||||
<div class="c-tns-nav-container absolute bottom-4 left-0 w-full flex justify-center items-end z-20">
|
||||
<div c-for="item in records"
|
||||
class="pointer-events-auto cursor-pointer rounded-full border-2 border-white w-4 h-4 mx-1 bg-black bg-opacity-50">
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Lightbox
|
||||
|
||||
```html
|
||||
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
|
||||
<img src="{{ image[0].urlPath | imagec(400) }}">
|
||||
</a>
|
||||
```
|
||||
|
||||
### Breadcrumb
|
||||
|
||||
```html
|
||||
<breadCrumb class="bg-gray-200 p-3 rounded" c-prevlinks="null"></breadCrumb>
|
||||
```
|
||||
|
||||
### Animate On Scroll (AOS)
|
||||
|
||||
```html
|
||||
<div data-aos="fade-up" data-aos-duration="800">Contenido</div>
|
||||
```
|
||||
|
||||
Valores comunes: `fade-up`, `fade-down`, `fade-left`, `fade-right`, `zoom-in`, `zoom-in-up`, `fade-up-right`, `fade-up-left`.
|
||||
|
||||
Tras cambios dinámicos en JS:
|
||||
```js
|
||||
AOS.refresh();
|
||||
```
|
||||
|
||||
### Lazy loading
|
||||
|
||||
```html
|
||||
<!-- Builder var con lazy automático -->
|
||||
<img data-field-type="upload" data-field-label="Imagen" data-lazy="true" data-field-width="800" alt="">
|
||||
|
||||
<!-- Manual en plantillas -->
|
||||
<img class="lazyload" data-src="{{ record.imagen[0].urlPath | imagec(800) }}" alt="">
|
||||
```
|
||||
|
||||
## Reglas críticas
|
||||
|
||||
1. `script.js` y `style.css` son **estáticos** — sin sintaxis Twig dentro.
|
||||
2. Para valores dinámicos en JS, pásalos desde `index-base.tpl` vía atributos `data-*`.
|
||||
3. Define una **clase raíz en kebab-case** por módulo. Scopea TODO el CSS/JS bajo ella.
|
||||
4. Tailwind first; CSS custom solo donde Tailwind no llegue, siempre con BEM.
|
||||
5. Vue 3: redefine `delimiters: ['${', '}']` para evitar conflicto con Twig.
|
||||
6. Mountea Vue sobre `#app-{{ section_id }}`.
|
||||
7. Usa las clases utilitarias de Acai (`transition3s`, `lazyload`, `bg-main-color`, etc.) antes de inventar utilidades.
|
||||
8. NO embebas lógica `<script>` dentro de `index-base.tpl` (Vue init es la única excepción común).
|
||||
212
docs/08-layout-and-libraries.md
Normal file
212
docs/08-layout-and-libraries.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Layout Global y Librerías Globales
|
||||
|
||||
Este documento explica cómo gestionar los **4 campos globales del proyecto** (`style` CSS global, `javascript` JS global, `header` Twig del header del sitio, `footer` Twig del footer) y las **librerías globales** (CSS/JS/fonts inyectadas en `<head>` o antes de `</body>`). Cubre la regla crítica de NO editar nunca `cms/lib/plugins/builder_saas/layout.json` ni los `.tpl` de `custom-header-twig` / `custom-footer-twig` directamente, las tools `get_layout_field` / `set_layout_field` (única vía válida para editar header/footer/style/javascript) y las tools `list_global_libraries` / `add_global_library` / `remove_global_library` / `set_global_libraries` para gestionar las URLs de librerías. Léelo antes de tocar cualquier cosa relacionada con header, footer, CSS global o librerías externas (jQuery, Vue CDN, Google Fonts, etc.).
|
||||
|
||||
## Layout global
|
||||
|
||||
Los 4 campos globales del proyecto viven en `cms/lib/plugins/builder_saas/layout.json`:
|
||||
|
||||
| Campo | Contenido |
|
||||
|-------|-----------|
|
||||
| `style` | CSS global del proyecto (se inyecta en todas las páginas) |
|
||||
| `javascript` | JS global del proyecto (se inyecta en todas las páginas) |
|
||||
| `header` | Twig del header del sitio (se renderiza arriba de cada página) |
|
||||
| `footer` | Twig del footer del sitio (se renderiza al final de cada página) |
|
||||
|
||||
Los campos `header` y `footer` son **Twig** — se beneficias de filtros (`| get`, `| translate`, etc.) y atributos (`c-if`, `c-for`).
|
||||
|
||||
### REGLA CRÍTICA — Nunca edites estos archivos directamente
|
||||
|
||||
**Está prohibido** usar `acai-view`, `acai-line-replace`, `acai-write` ni `acai-delete` sobre:
|
||||
|
||||
- `cms/lib/plugins/builder_saas/layout.json`
|
||||
- `template/estandar/modulos/custom-header-twig/*`
|
||||
- `template/estandar/modulos/custom-footer-twig/*`
|
||||
- `template/estandar/modulos/custom-header/*`
|
||||
- `template/estandar/modulos/custom-footer/*`
|
||||
|
||||
Estos ficheros son **artefactos generados** a partir de `layout.json`. Editarlos directamente provoca:
|
||||
- Desincronización con `layout.json.{header,footer}ModuleCustom.htmlParsed`.
|
||||
- Sobrescritura de tus cambios cuando el usuario abre el builder visual y guarda.
|
||||
- Comportamiento inconsistente entre el render público y el builder.
|
||||
|
||||
**El backend protege estas rutas** — las tools de archivo devolverán error si intentas tocarlas.
|
||||
|
||||
### Workflow correcto
|
||||
|
||||
#### Leer
|
||||
|
||||
```
|
||||
get_layout_field({ field: "header" }) // Twig source del header
|
||||
get_layout_field({ field: "footer" }) // Twig source del footer
|
||||
get_layout_field({ field: "style" }) // CSS global
|
||||
get_layout_field({ field: "javascript" }) // JS global
|
||||
```
|
||||
|
||||
#### Escribir
|
||||
|
||||
```
|
||||
set_layout_field({
|
||||
field: "footer",
|
||||
content: "<footer class='bg-gray-900 text-white py-10'>…nuevo HTML/Twig…</footer>"
|
||||
})
|
||||
```
|
||||
|
||||
`set_layout_field` ejecuta una pipeline atómica:
|
||||
1. Escribe el source en `layout.json.{field}`.
|
||||
2. Sincroniza `layout.json.{field}ModuleCustom.htmlParsed`.
|
||||
3. Regenera los `.tpl` de `custom-{field}-twig/`.
|
||||
4. Compila el Twig a PHP.
|
||||
|
||||
Es **destructivo** — sobrescribe el contenido completo. **Pair con `get_layout_field` primero** para leer el actual y modificarlo, no escribirlo desde cero.
|
||||
|
||||
### Ejemplos de uso
|
||||
|
||||
#### Cambiar el copyright del footer
|
||||
|
||||
```
|
||||
// 1. Leer
|
||||
get_layout_field({ field: "footer" })
|
||||
// devuelve: <footer><p>© 2024 Mi Sitio</p>…</footer>
|
||||
|
||||
// 2. Modificar localmente y reescribir entero
|
||||
set_layout_field({
|
||||
field: "footer",
|
||||
content: "<footer><p>© 2025 Mi Sitio. Todos los derechos reservados.</p>…</footer>"
|
||||
})
|
||||
```
|
||||
|
||||
#### Añadir un menú al header
|
||||
|
||||
```
|
||||
// 1. Leer source actual
|
||||
get_layout_field({ field: "header" })
|
||||
|
||||
// 2. Escribir nueva versión con el menú añadido
|
||||
set_layout_field({
|
||||
field: "header",
|
||||
content: "<header>…<nav>{% for item in 'apartados' | get('parentNum=0 AND visible_en_el_menu=1', 'globalOrder ASC') %}<a href='{{ item.enlace }}'>{{ item.name }}</a>{% endfor %}</nav>…</header>"
|
||||
})
|
||||
```
|
||||
|
||||
#### Añadir CSS global
|
||||
|
||||
```
|
||||
// 1. Leer
|
||||
get_layout_field({ field: "style" })
|
||||
|
||||
// 2. Reescribir con la regla añadida
|
||||
set_layout_field({
|
||||
field: "style",
|
||||
content: ":root { --main-color: #2563eb; }\n.btn-primary { … }\n…"
|
||||
})
|
||||
```
|
||||
|
||||
## Librerías globales
|
||||
|
||||
`layout.json["libraries"]` define una lista de URLs (CSS, JS, fonts) que el CMS inyecta en cada página. Hay **dos secciones**:
|
||||
|
||||
| Sección | Posición | Uso típico |
|
||||
|---------|----------|------------|
|
||||
| `top` | Dentro de `<head>` | CSS, fonts (Google Fonts), JS crítico (preload) |
|
||||
| `bottom` | Antes de `</body>` | La mayoría de JS (jQuery, Vue, sliders, etc.) |
|
||||
|
||||
### Tools
|
||||
|
||||
#### Listar — `list_global_libraries`
|
||||
|
||||
```
|
||||
list_global_libraries()
|
||||
```
|
||||
|
||||
Devuelve:
|
||||
```json
|
||||
{
|
||||
"top": [
|
||||
{ "num": 1, "url": "https://fonts.googleapis.com/css2?family=Inter" },
|
||||
{ "num": 2, "url": "/css/extras.css" }
|
||||
],
|
||||
"bottom": [
|
||||
{ "num": 1, "url": "https://unpkg.com/vue@3/dist/vue.global.prod.js" },
|
||||
{ "num": 2, "url": "/js/main.js" }
|
||||
],
|
||||
"layoutExists": true
|
||||
}
|
||||
```
|
||||
|
||||
Llama a esta tool **antes** de añadir/quitar para no duplicar entradas.
|
||||
|
||||
#### Añadir — `add_global_library`
|
||||
|
||||
Idempotente: si la URL ya existe en esa sección, devuelve `added: false`.
|
||||
|
||||
```
|
||||
add_global_library({
|
||||
section: "bottom",
|
||||
url: "https://unpkg.com/vue@3/dist/vue.global.prod.js"
|
||||
})
|
||||
|
||||
add_global_library({
|
||||
section: "top",
|
||||
url: "https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
|
||||
})
|
||||
|
||||
// Rutas relativas al proyecto también valen
|
||||
add_global_library({
|
||||
section: "bottom",
|
||||
url: "/js/custom.js"
|
||||
})
|
||||
```
|
||||
|
||||
#### Eliminar — `remove_global_library`
|
||||
|
||||
Idempotente: si la URL no existe, devuelve `removed: false`.
|
||||
|
||||
```
|
||||
remove_global_library({
|
||||
section: "bottom",
|
||||
url: "https://unpkg.com/vue@3/dist/vue.global.prod.js"
|
||||
})
|
||||
```
|
||||
|
||||
#### Reemplazar entera — `set_global_libraries`
|
||||
|
||||
**Destructivo** — sobrescribe la lista completa de la sección. Úsalo solo para reordenar masivamente o reemplazar el conjunto. Para cambios incrementales prefiere `add_global_library` / `remove_global_library`.
|
||||
|
||||
```
|
||||
set_global_libraries({
|
||||
section: "bottom",
|
||||
libraries: [
|
||||
{ url: "/js/jquery.min.js" },
|
||||
{ url: "/js/main.js" },
|
||||
{ url: "https://unpkg.com/vue@3/dist/vue.global.prod.js" }
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Convenciones
|
||||
|
||||
- Para **librerías populares**, prefiere CDN oficial (`unpkg.com`, `cdn.jsdelivr.net`, `cdnjs.cloudflare.com`).
|
||||
- Para **assets propios** del proyecto, usa rutas relativas (`/js/main.js`, `/css/extras.css`).
|
||||
- Si añades una librería que ya tiene equivalente cargada (ej. dos versiones de jQuery), elimina la antigua antes de añadir la nueva.
|
||||
- El **orden** importa: las dependencias deben ir antes que sus consumidores. `set_global_libraries` permite reordenar.
|
||||
|
||||
## Decisión: ¿layout global o módulo?
|
||||
|
||||
| Caso | Solución |
|
||||
|------|----------|
|
||||
| Header/footer del sitio | Layout global (`set_layout_field` con `header`/`footer`) |
|
||||
| CSS aplicado a todo el sitio | Layout global (`set_layout_field` con `style`) o librería global |
|
||||
| JS aplicado a todo el sitio | Layout global (`set_layout_field` con `javascript`) o librería global |
|
||||
| Componente reutilizable en páginas | Módulo en `template/estandar/modulos/` |
|
||||
| Detalle de un registro | Sección general `custom-{tableName}` |
|
||||
| Bloque visual específico | Módulo |
|
||||
|
||||
## Reglas críticas
|
||||
|
||||
1. **NUNCA edites directamente** `layout.json`, `custom-header-twig/*`, `custom-footer-twig/*`, `custom-header/*`, `custom-footer/*`. Las tools de archivo te devolverán error si lo intentas.
|
||||
2. Para header/footer/style/javascript globales, **única vía**: `get_layout_field` + `set_layout_field`.
|
||||
3. `set_layout_field` es **destructivo** — siempre lee primero, modifica, escribe.
|
||||
4. Para librerías globales: `list_global_libraries` → `add_global_library` / `remove_global_library`. `set_global_libraries` solo para reordenar/reemplazar masivamente.
|
||||
5. Inyección automática: `top` va en `<head>`, `bottom` antes de `</body>`. Decide según el tipo de asset.
|
||||
6. El orden de las librerías importa para dependencias.
|
||||
245
docs/09-mcp-tools-reference.md
Normal file
245
docs/09-mcp-tools-reference.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# MCP Tools — Referencia Completa
|
||||
|
||||
Este documento es el **inventario canónico** de todas las tools MCP disponibles para el agente Acai. Está agrupado por categoría (archivos, módulos, registros, tablas, layout, librerías, hooks, media, navegación, proyecto, git, autenticación, docs) y describe para cada tool su propósito, parámetros clave, qué devuelve y cuándo usarla. Incluye además los **workflows canónicos** para las operaciones más comunes (crear módulo, editar módulo, crear funcionalidad nueva con tabla + detalle, gestionar imágenes de un módulo, editar header/footer, configurar middleware de hook). Léelo antes de cualquier tarea para elegir la secuencia correcta de tools.
|
||||
|
||||
## Inventario por categoría
|
||||
|
||||
### Archivos
|
||||
|
||||
| Tool | Acción | Notas |
|
||||
|------|--------|-------|
|
||||
| `acai-view` | Lee un archivo (con rango de líneas opcional) | Ahorra tokens — usa `start_line`/`end_line` |
|
||||
| `acai-glob` | Encuentra archivos por patrón glob | `template/estandar/modulos/**/index-base.tpl` |
|
||||
| `acai-grep` | Busca texto/regex en archivos | Soporta filtro por glob |
|
||||
| `acai-write` | Crea o reescribe un archivo completo | **Editar `index-base.tpl` compila automáticamente** |
|
||||
| `acai-line-replace` | Reemplaza un bloque de líneas validado | Preferida sobre `acai-write` para edits puntuales |
|
||||
| `acai-delete` | Elimina un archivo | Destructivo — confirma primero |
|
||||
|
||||
Reglas:
|
||||
- Rutas siempre relativas al proyecto.
|
||||
- Editar `index-base.tpl` → compila automáticamente. No necesitas llamar a `compile_module`.
|
||||
- **NO toques** `index.tpl`, `index-twig.tpl`, `builder.json` (autogenerados) ni los archivos de layout protegidos (ver `08-layout-and-libraries.md`).
|
||||
|
||||
### Módulos
|
||||
|
||||
| Tool | Acción | Notas |
|
||||
|------|--------|-------|
|
||||
| `create_module` | Crea un módulo nuevo (carpeta + archivos + compile) | Alternativa a hacer `acai-write` manual del `index-base.tpl`. Genera `moduleId` único añadiendo sufijo aleatorio. |
|
||||
| `compile_module` | Recompila manualmente (rescate) | Solo si los archivos generados están desincronizados. Editar `index-base.tpl` con tools de archivo ya compila. |
|
||||
| `check_module` | Preview del render con datos de ejemplo | Devuelve preview (50 líneas) por defecto; `fullRender:true` para todo |
|
||||
| `check_module_usage` | Lista páginas que usan el módulo | **OBLIGATORIO antes de `delete_module`** |
|
||||
| `delete_module` | Elimina la carpeta del módulo | Destructivo. Si `inUse=true`, deniega — el usuario debe quitarlo de las páginas primero |
|
||||
| `set_module_example_data` | Define datos de ejemplo para preview en el editor | Pasar valores para TODAS las variables del schema |
|
||||
|
||||
### Registros (records)
|
||||
|
||||
| Tool | Acción | Notas |
|
||||
|------|--------|-------|
|
||||
| `list_table_records` | Lista registros con filtros, paginación | Usa `fields` y `truncateText` para ahorrar tokens |
|
||||
| `get_record` | Obtiene un registro completo por `num` | Carga uploads + relations por defecto |
|
||||
| `create_or_update_record` | Crea o actualiza registros | Sin `recordId` → crea. Con `recordId` → actualiza. Acepta array para batch |
|
||||
| `delete_table_records` | Elimina registros (por IDs o `deleteAll`) | **Destructivo permanente** |
|
||||
| `list_page_modules` | Lista módulos colocados en una página Builder | Devuelve sectionIds, posiciones, visibilidad, configVars |
|
||||
| `add_module_to_record` | Añade módulo a una página Builder | Devuelve `sectionId` — úsalo en `set_module_config_vars` |
|
||||
| `remove_module_from_record` | Quita módulo de la página | Por `sectionId` (preferido) o `modulePosition` |
|
||||
| `reorder_module` | Mueve módulo a otra posición | `fromPosition` → `toPosition` |
|
||||
| `toggle_module_visibility` | Muestra/oculta sin borrar | Por `sectionId` |
|
||||
| `get_module_config_vars` | Lee valores actuales de las variables | Por `tableName` + `recordNum` + `sectionId` |
|
||||
| `set_module_config_vars` | Escribe variables del módulo | Devuelve `uploadFields` con `recordNum`+`fieldName` listos para subir imágenes |
|
||||
|
||||
### Tablas y campos (schema)
|
||||
|
||||
Ver `05-tables-and-fields.md` para detalles. Tools:
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `list_tables` | Inventario de tablas (sin `cms_`) |
|
||||
| `get_table_schema` | Schema completo. Soporta `minimal:true` y `filterFields:"..."` para ahorrar tokens |
|
||||
| `create_table` | Crea tabla nueva. PREGUNTA al usuario sobre `enlace`/`seoMetas` antes |
|
||||
| `update_table_metadata` | Actualiza el `[meta]`. `newTableName` renombra MySQL (destructivo) |
|
||||
| `delete_table` | Borra tabla. Usa `dryRun:true` primero. `dropData:true` borra datos |
|
||||
| `reorder_tables` | Reordena sidebar admin |
|
||||
| `create_field` | Añade campo a tabla |
|
||||
| `update_field` | Actualiza props. `newFieldName` renombra columna (destructivo). Cambios de `type` pueden truncar datos — surfacea `warnings` |
|
||||
| `delete_field` | Borra campo. `dropColumn:true` borra datos permanentemente |
|
||||
| `reorder_fields` | Reordena los campos del formulario admin |
|
||||
| `regenerate_enlaces` | Regenera URLs de la tabla. `generateAlias:true` para preservar redirects |
|
||||
|
||||
### Layout global
|
||||
|
||||
Ver `08-layout-and-libraries.md`.
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `get_layout_field` | Lee `style`/`javascript`/`header`/`footer` del `layout.json` |
|
||||
| `set_layout_field` | Escribe el campo (atómico: actualiza json + regenera tpl + compila). Destructivo |
|
||||
|
||||
### Librerías globales
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `list_global_libraries` | Lista las URLs `top` (head) y `bottom` (antes de `</body>`) |
|
||||
| `add_global_library` | Añade URL idempotente |
|
||||
| `remove_global_library` | Quita URL idempotente |
|
||||
| `set_global_libraries` | Reemplaza la lista de la sección. Destructivo |
|
||||
|
||||
### Hooks (middleware)
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `get_hook_middleware` | Lee la config `middleWare` de un hook global |
|
||||
| `set_hook_middleware` | Configura cuándo se ejecuta automáticamente: `[]`, `["allurls"]`, `["<tableName>-<num>"]` |
|
||||
|
||||
Ver `06-hooks-and-cmsapi.md` para uso. Crear/editar el `.php` del hook se hace con `acai-write`.
|
||||
|
||||
### Media
|
||||
|
||||
| Tool | Acción | Notas |
|
||||
|------|--------|-------|
|
||||
| `generate_image` | Genera imagen con IA y la guarda en `cms/uploads/generated/` | Devuelve `dockerUrl` y `uploadUrl`/`fullUrl`. **En Forge prefiere `uploadUrl`/`fullUrl`** sobre `dockerUrl` para `upload_record_image` |
|
||||
| `upload_record_image` | Sube imagen a un campo de un registro | Necesita `tableName`, `recordId` (num), `fieldName` real (de relations o `uploadFields`) |
|
||||
| `upload_image_to_assets` | Sube imagen a `/images/` del template (assets globales) | Acepta base64, data URI, URL. Permite resize/quality/format |
|
||||
|
||||
### Navegación
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `navigate_browser` | Navega el browser preview del usuario a un `enlace` (e.g. `/servicios/`) |
|
||||
|
||||
### Proyecto
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `get_web_url` | URL del sitio en desarrollo. **OBLIGATORIO** antes de fetch/Playwright. Acuérdate de añadir `?pruebas=1` |
|
||||
| `save_project_styles` | Guarda resumen de estilos en `docs/project-styles.md` |
|
||||
|
||||
### Git
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `list_git_log` | Lista los últimos commits para que el usuario elija un id de rollback |
|
||||
| (rollback) | Tool de rescate; pide confirmación al usuario |
|
||||
|
||||
### Autenticación
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `refresh_acai_token` | Renueva el JWT cuando expira (errores 403) |
|
||||
|
||||
### Documentación
|
||||
|
||||
| Tool | Acción |
|
||||
|------|--------|
|
||||
| `read_doc` | Lee un doc del knowledge base completo o por sección. Útil cuando un doc no fue cargado por relevancia o necesitas una sección puntual |
|
||||
| `list_docs` | Lista los docs disponibles con sus títulos y summaries |
|
||||
|
||||
## Workflows canónicos
|
||||
|
||||
### 1. Crear un módulo nuevo
|
||||
|
||||
1. **Estilo del proyecto**: si existe `docs/project-styles.md` léelo. Si no, explora 3-4 módulos representativos (no `custom-*`) y guarda con `save_project_styles`.
|
||||
2. **Lee la doc relevante** según contenido: `01-builder-fields.md` siempre; `07-css-js-conventions.md` si lleva JS; `06-hooks-and-cmsapi.md` si lleva hook PHP; `02-twig.md` si usa filtros.
|
||||
3. **`acai-write`** sobre `template/estandar/modulos/<moduleId>_xxxxxx/index-base.tpl` con el HTML/Twig. Si necesita CSS/JS/PHP, escribe también `style.css`, `script.js`, `hook.php`.
|
||||
4. **Compilación automática** al escribir `index-base.tpl`. Si por algún motivo necesitas forzarla sin tocar el archivo: `compile_module`.
|
||||
5. **`add_module_to_record`** para colocarlo en una página. Devuelve `sectionId`.
|
||||
6. **`set_module_config_vars`** para rellenar variables (textos, listas, etc.). Devuelve `uploadFields` con `recordNum`+`fieldName` por cada upload.
|
||||
7. **Imágenes**: `generate_image` o `upload_record_image` usando el `recordNum` y `fieldName` del paso 6.
|
||||
8. **`navigate_browser`** al `enlace` de la página para que el usuario vea el resultado.
|
||||
|
||||
### 2. Editar un módulo existente
|
||||
|
||||
1. `get_module_config_vars` — leer estado actual (vars + recordNums).
|
||||
2. `acai-view` — leer el rango concreto de `index-base.tpl` que vas a tocar.
|
||||
3. `acai-line-replace` — modificar el bloque (compila automáticamente). Usa `acai-write` solo si el cambio es masivo.
|
||||
4. Si cambian variables: `set_module_config_vars` para actualizar valores.
|
||||
|
||||
### 3. Gestionar imágenes de un módulo
|
||||
|
||||
**Tras `set_module_config_vars`** (recomendado):
|
||||
1. La respuesta incluye `uploadFields` con `{ fieldName, recordNum }` por cada variable upload.
|
||||
2. Para multi vars con uploads: `uploadFields["records.imagen"]` es array `[{index, fieldName, recordNum}]`.
|
||||
3. `upload_record_image` con `tableName: "builder_custom"`, `recordId` y `fieldName` de `uploadFields`.
|
||||
|
||||
**Sin `set_module_config_vars` previo**:
|
||||
1. `get_module_config_vars` — obtiene `recordNum` de builder_custom.
|
||||
2. Lee `builder.json` del módulo para encontrar el `fieldName` real (de `vars.NOMBRE.relations.builder_custom`, ej. `image1` — **NO** uses el nombre de la variable).
|
||||
3. `upload_record_image` con `tableName: "builder_custom"`, `recordId` (recordNum del paso 1), `fieldName` (de relations).
|
||||
|
||||
Generar imagen primero:
|
||||
1. `generate_image` con `prompt` + `style`.
|
||||
2. Usa la URL recomendada que devuelve (`uploadUrl` o `fullUrl` en Forge; `dockerUrl` solo en local).
|
||||
3. `upload_record_image` con esa URL.
|
||||
|
||||
### 4. Crear funcionalidad nueva con tabla + detalle
|
||||
|
||||
Ejemplo: implementar "Vacantes".
|
||||
|
||||
1. **`create_table`** con `tableName: "vacantes"`, `menuType: "multi"`, `enlace: true`, `seoMetas: true`. Pregunta al usuario antes los flags.
|
||||
2. **`create_field`** para cada campo: `titulo`, `descripcion` (wysiwyg), `salario_minimo` (textfield), `categoria_num` (list desde tabla), `fecha_publicacion` (date), `fecha_expiracion` (date), `visible` (checkbox), `imagen_destacada` (upload).
|
||||
3. **`acai-write`** sobre `template/estandar/modulos/custom-vacantes/index-base.tpl` con el Twig que usa `thisrecord.*` (sección general que renderiza el detalle de cada registro).
|
||||
4. (Opcional) **Módulo de listado** `vacantes_listado_xxxxxx` con `'vacantes' | get('visible=1', 'fecha_publicacion DESC', 20)`.
|
||||
5. (Opcional) **Página índice** `/vacantes/` en `apartados` (Builder) con el módulo de listado dentro.
|
||||
6. **`navigate_browser`** a un detalle creado para verificar.
|
||||
|
||||
### 5. Editar header / footer del sitio
|
||||
|
||||
Ver `08-layout-and-libraries.md`.
|
||||
|
||||
1. `get_layout_field({ field: "header" })` — lee Twig actual.
|
||||
2. Modifica localmente.
|
||||
3. `set_layout_field({ field: "header", content: "..." })` — atómico. Sobrescribe `layout.json`, regenera `.tpl` y compila.
|
||||
|
||||
**NUNCA** uses `acai-write` sobre `custom-header-twig/index-base.tpl` ni `layout.json`.
|
||||
|
||||
### 6. Añadir librería global (jQuery, Vue CDN, Google Fonts)
|
||||
|
||||
1. `list_global_libraries` — comprueba si ya existe.
|
||||
2. `add_global_library({ section: "bottom", url: "..." })` para JS, o `top` para CSS/fonts.
|
||||
|
||||
Para reordenar dependencias: `set_global_libraries` con la lista completa.
|
||||
|
||||
### 7. Hook con middleware (auto-ejecutar antes de páginas)
|
||||
|
||||
1. **`acai-write`** sobre `hooks/hooks.<id>.php` con la lógica.
|
||||
2. **`get_hook_middleware`** sobre `/hooks/<id>/` para ver config actual.
|
||||
3. **`set_hook_middleware`** con el nuevo `middleWare`:
|
||||
- `[]` → solo cuando se llama explícitamente
|
||||
- `["allurls"]` → antes de cada página
|
||||
- `["cms_apartados-2"]` → solo antes del registro num=2 de apartados
|
||||
|
||||
### 8. Gestionar registros de una tabla
|
||||
|
||||
1. `list_table_records` con `where`/`order`/`limit`/`fields` (sin `cms_`).
|
||||
2. `get_record` para uno completo (`tableName`+`recordNum`).
|
||||
3. `create_or_update_record` para crear/actualizar. Antes consulta el schema con `get_table_schema`.
|
||||
4. `delete_table_records` para borrar (destructivo permanente).
|
||||
|
||||
### 9. Explorar el sitio
|
||||
|
||||
- `list_table_records` sobre `apartados` para ver páginas.
|
||||
- `list_page_modules` sobre una página para ver módulos.
|
||||
- `get_module_config_vars` para ver datos de un módulo.
|
||||
- `check_module` para preview con datos custom.
|
||||
|
||||
### 10. Consultar la documentación bajo demanda
|
||||
|
||||
Si el knowledge_base no cargó un doc relevante (lo verás en "Other Available Docs") o necesitas una sección puntual con detalle:
|
||||
|
||||
```
|
||||
list_docs() // todos los docs con summary
|
||||
read_doc({ name: "05-tables-and-fields" }) // doc completo
|
||||
read_doc({ name: "06-hooks-and-cmsapi", section: "Hook middleware" }) // sección por heading H2
|
||||
```
|
||||
|
||||
## Reglas globales para todas las tools
|
||||
|
||||
1. **`tableName` siempre SIN prefijo `cms_`** (excepto en `queryDB` Twig y en el `middleWare` de `set_hook_middleware`).
|
||||
2. **PK siempre `num`**, nunca `id`. Foreign keys con sufijo `_num`.
|
||||
3. **Upload fields son arrays** — accede con `[0].urlPath`.
|
||||
4. **`fieldName` para imágenes** viene de `builder.json` → `vars.NOMBRE.relations.builder_custom` (ej. `image1`), NO del nombre de la variable.
|
||||
5. **`recordId` para imágenes de módulo** es el `num` de `builder_custom`, NO el `sectionId`.
|
||||
6. Tras `set_module_config_vars`, **TODAS** las variables (incluidos uploads) reciben `recordNum`+`fieldName` en `uploadFields`.
|
||||
7. Si un token JWT expira (error 403): `refresh_acai_token` y reintentar.
|
||||
8. Al pedir URLs del sitio: `get_web_url` SIEMPRE primero. Añade `?pruebas=1` para modo desarrollo. Nunca uses dominios de producción ni `localhost:8080`.
|
||||
9. Antes de crear archivos, **lee la doc** relevante (`read_doc` si no está en el KB cargado).
|
||||
10. Operaciones destructivas (`delete_*`, `dropColumn`, `dropData`, `dropTable`, `newTableName`, `newFieldName`, `regenerate_enlaces` sin alias, `set_global_libraries`, `set_layout_field`): **pide confirmación al usuario** si no es trivial.
|
||||
@@ -1,12 +1,10 @@
|
||||
# Patrones de Producción
|
||||
|
||||
Patrones reales usados en módulos y secciones generales de producción. Usar como referencia al crear nuevos módulos.
|
||||
Este documento recoge patrones reales usados en módulos y secciones generales de proyectos Acai en producción. Cada patrón incluye el HTML/Twig listo para reutilizar y notas sobre cuándo aplicarlo. Cubre: cabecera de sección con colores configurables, layout zigzag (imagen + texto alternado), acordeón FAQ, formulario de contacto completo con `c-form`, compartir en redes sociales, sección general de detalle de producto, galería con carousel modo `gallery`. Léelo cuando vayas a crear un módulo y quieras evitar reinventar patrones que ya tienen una versión canónica testeada en producción.
|
||||
|
||||
---
|
||||
## 1. Cabecera de sección (Pretítulo + Título + Subtítulo)
|
||||
|
||||
## Patrón 1: Cabecera de Sección (Pretítulo + Título + Subtítulo)
|
||||
|
||||
Bloque de cabecera con colores y alineación configurables. Casi todos los módulos lo usan:
|
||||
Bloque de cabecera con colores y alineación configurables. Casi todos los módulos lo usan como inicio.
|
||||
|
||||
```html
|
||||
<div c-hidden="true">
|
||||
@@ -52,11 +50,9 @@ Bloque de cabecera con colores y alineación configurables. Casi todos los módu
|
||||
</section>
|
||||
```
|
||||
|
||||
---
|
||||
## 2. Layout Zigzag (imagen + texto alternado)
|
||||
|
||||
## Patrón 2: Layout Zigzag/Ajedrez (Imagen + Texto alternado)
|
||||
|
||||
Usa `loop.index is odd/even` para alternar:
|
||||
Usa `loop.index is odd/even` para alternar la dirección.
|
||||
|
||||
```html
|
||||
<li c-for="record in records" class="w-full py-6">
|
||||
@@ -80,9 +76,7 @@ Usa `loop.index is odd/even` para alternar:
|
||||
</li>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 3: Acordeón FAQ
|
||||
## 3. Acordeón FAQ
|
||||
|
||||
```html
|
||||
<li data-field-type="multiv2" data-field-label="Records" data-aos="fade-up" data-aos-duration="800">
|
||||
@@ -101,27 +95,27 @@ Usa `loop.index is odd/even` para alternar:
|
||||
</li>
|
||||
```
|
||||
|
||||
JavaScript para toggle:
|
||||
```javascript
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll(".faq-page").forEach(faq => {
|
||||
faq.addEventListener("click", function () {
|
||||
`script.js`:
|
||||
```js
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll(".faq-page").forEach((faq) => {
|
||||
faq.addEventListener("click", () => {
|
||||
const body = faq.nextElementSibling;
|
||||
const isActive = faq.classList.toggle("active");
|
||||
body.classList.toggle("hidden", !isActive);
|
||||
AOS.refresh();
|
||||
if (typeof AOS !== "undefined") AOS.refresh();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 4: Formulario de Contacto Completo
|
||||
## 4. Formulario de contacto completo
|
||||
|
||||
```html
|
||||
<set :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
|
||||
<set :logo="tienda.logo.0.urlPath ? 'https://' ~ server.HTTP_HOST ~ tienda.logo.0.urlPath : 'https://' ~ server.HTTP_HOST ~ '/template/estandar/images/logo.png'"></set>
|
||||
<set :logo="tienda.logo.0.urlPath
|
||||
? 'https://' ~ server.HTTP_HOST ~ tienda.logo.0.urlPath
|
||||
: 'https://' ~ server.HTTP_HOST ~ '/template/estandar/images/logo.png'"></set>
|
||||
{% set imagen = '<img src="' ~ logo ~ '" style="max-height:150px; display: block; margin: 0 auto;">' %}
|
||||
{% set gracias = 'apartados' | get('num = 20').0 %}
|
||||
|
||||
@@ -136,19 +130,19 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
<input type="text" name="nombre" required
|
||||
placeholder="{{ 'Nombre' | translate }}"
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1" />
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1">
|
||||
<input type="email" name="email" required
|
||||
placeholder="{{ 'Email' | translate }}"
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1" />
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1">
|
||||
<input type="text" name="telefono" required
|
||||
placeholder="{{ 'Teléfono' | translate }}"
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1" />
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1">
|
||||
<textarea name="comentario" cols="30" rows="5"
|
||||
placeholder="{{ 'Escribe aquí tu comentario' | translate }}..."
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl resize-none px-6 py-2 my-1"></textarea>
|
||||
|
||||
<label class="w-full flex items-start mt-4">
|
||||
<input required type="checkbox" class="mt-1" />
|
||||
<input required type="checkbox" class="mt-1">
|
||||
<span class="text-xs sm:text-sm ml-3">{{ 'Acepto las condiciones legales' | translate | raw }}</span>
|
||||
</label>
|
||||
|
||||
@@ -163,9 +157,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</c-form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 5: Compartir en Redes Sociales
|
||||
## 5. Compartir en redes sociales
|
||||
|
||||
```html
|
||||
<ul class="flex flex-wrap -mx-1 mt-4">
|
||||
@@ -184,12 +176,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</ul>
|
||||
```
|
||||
|
||||
---
|
||||
## 6. Sección general — Detalle de producto
|
||||
|
||||
## Patrón 6: Sección General — Detalle de Producto
|
||||
Patrón completo para `template/estandar/modulos/custom-productos/index-base.tpl`:
|
||||
|
||||
```html
|
||||
<set :tienda="'configuracion_tienda' | get('num !=0')[0]"></set>
|
||||
<set :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
|
||||
|
||||
<section class="detalle-producto">
|
||||
<div class="container mx-auto max-w-7xl px-6 2xl:px-0 mt-20 mb-10">
|
||||
@@ -238,9 +230,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</section>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 7: Galería con Carousel (modo Gallery)
|
||||
## 7. Galería con carousel — modo Gallery
|
||||
|
||||
```html
|
||||
<div class="c-tns-wrapper" data-autoplay-timeout="8000" data-mode="gallery" data-speed="400" data-nav="true">
|
||||
@@ -260,3 +250,49 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 8. Listado con filtros y paginación
|
||||
|
||||
```html
|
||||
<set :categorias="'categorias' | get('visible=1', 'orden ASC')"></set>
|
||||
{% set perPage = 12 %}
|
||||
{% set page = pagina | default(1) %}
|
||||
{% set offset = (page - 1) * perPage %}
|
||||
|
||||
<div class="container mx-auto max-w-7xl px-6 py-10">
|
||||
<!-- Filtros -->
|
||||
<nav class="flex flex-wrap gap-2 mb-8">
|
||||
<a href="?cat=" class="px-4 py-2 rounded-full bg-gray-200">{{ 'Todas' | translate }}</a>
|
||||
{% for cat in categorias %}
|
||||
<a href="?cat={{ cat.num }}" class="px-4 py-2 rounded-full bg-gray-200">{{ cat.nombre }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
<!-- Listado -->
|
||||
{% set where = 'visible=1' %}
|
||||
{% if cat_filter %}
|
||||
{% set where = where ~ ' AND categoria_num=' ~ cat_filter %}
|
||||
{% endif %}
|
||||
|
||||
{% set productos = 'productos' | get(where, 'orden ASC', perPage) %}
|
||||
<ul class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{% for prod in productos %}
|
||||
<li class="bg-white rounded-xl shadow p-4">
|
||||
<a href="{{ prod.enlace }}">
|
||||
<img src="{{ prod.imagen[0].urlPath | imagec(400) }}" alt="{{ prod.nombre }}" class="w-full aspect-video object-cover rounded">
|
||||
<h3 class="text-lg font-semibold mt-3">{{ prod.nombre }}</h3>
|
||||
<p class="text-gray-600">{{ prod.descripcion | truncate(100) }}</p>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Reglas de aplicación
|
||||
|
||||
- Estos patrones son **referencia** — adáptalos al estilo del proyecto (ver `docs/project-styles.md` si existe).
|
||||
- Reutiliza **clases utilitarias de Acai** (`bg-main-color`, `transition3s`, `lazyload`, `glightbox`, `c-tns-wrapper`) antes de inventar.
|
||||
- Para campos editables siempre añade `data-field-label` (ver `01-builder-fields.md`).
|
||||
- Para `c-form`, prefiere usarlo antes de construir POST/hook custom.
|
||||
- Para detalle de registro usa **siempre** la convención `custom-{tableName}/`.
|
||||
128
docs/11-quick-reference.md
Normal file
128
docs/11-quick-reference.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Quick Reference — Cheat sheet
|
||||
|
||||
Este documento es un **resumen ejecutable** de las reglas críticas, los tipos de campo, los filtros Twig, los formatos de datos para insert/update y las variables globales. Es la **fuente única de verdad** para resolver dudas rápidas sin tener que abrir los docs largos. Léelo antes de cualquier operación cuando quieras refrescar las reglas; el resto de docs (`01`–`10`) profundizan en cada tema.
|
||||
|
||||
## Reglas inmutables
|
||||
|
||||
| Regla | Correcto | Incorrecto |
|
||||
|-------|----------|------------|
|
||||
| Nombres de tabla en tools/Twig/CmsApi | `'productos'` | `'cms_productos'` |
|
||||
| Nombres en `queryDB` | `cms_productos` | `productos` |
|
||||
| Primary key | `record.num` | `record.id` |
|
||||
| Foreign keys | `categoria_num` | `categoria_id` |
|
||||
| Upload fields | `record.imagen[0].urlPath` | `record.imagen` |
|
||||
| Optimizar imagen | `imagen[0].urlPath \| imagec(800)` | `imagen.url` |
|
||||
| Filtros Twig | `{{ 'tabla' \| get() }}` | `{{ get('tabla') }}` |
|
||||
| Campo enlace | `{{ producto.enlace }}` (ya tiene barras) | `"/{{ producto.enlace }}/"` |
|
||||
| Builder var name | `data-field-label` → minúsculas, sin espacios | Mantener casing original |
|
||||
| Checkbox | `1` o `0` (número) | `true` / `false` |
|
||||
| Formato fecha | `YYYY-MM-DD HH:mm:ss` | Cualquier otro |
|
||||
| `c-if` igualdad | `c-if="x = 'valor'"` (un `=`) | `c-if="x == 'valor'"` |
|
||||
| Twig `{% if %}` | `{% if x == 'valor' %}` (doble `==`) | `{% if x = 'valor' %}` |
|
||||
| Concatenación Twig | `'value=' ~ variable` | `'value=' + variable` |
|
||||
|
||||
## Tipos de builder field (`data-field-type`)
|
||||
|
||||
| Tipo | Elemento | Devuelve |
|
||||
|------|----------|----------|
|
||||
| `textfield` | `<p>` | String |
|
||||
| `headfield` | `<h1>`–`<h6>` | String + variable `_tag` |
|
||||
| `textbox` | `<div>` | String multilínea |
|
||||
| `wysiwyg` | `<div class="wysiwyg">` | HTML string |
|
||||
| `link` | `<a>` | URL string |
|
||||
| `upload` | `<img>` | Array `[{urlPath, info1, info2, info3, info4}]` |
|
||||
| `uploadMulti` | `<li>` | Itera archivos subidos |
|
||||
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
||||
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
||||
| `multiv2` | `<li>` wrapper | Array de objetos |
|
||||
| `checkbox` | `<input>` o `<div>` | `1` / `0` |
|
||||
| `colorpicker` | `<div>` | Hex color |
|
||||
|
||||
## Atributos Acai
|
||||
|
||||
| Atributo | Uso | Ejemplo |
|
||||
|----------|-----|---------|
|
||||
| `c-if` | Condicional | `<p c-if="activo = 1">` |
|
||||
| `c-else` | Rama else | `<p c-else>` |
|
||||
| `c-for` | Loop array | `<li c-for="item in items">` |
|
||||
| `c-for` (tabla) | Loop BD | `<li c-for="p in productos" c-where="'activo=1'" c-limit="10">` |
|
||||
| `c-hidden` | Variable oculta | `<div c-hidden="true">` |
|
||||
| `c-class` | Clase condicional | `<div c-class="{ 'bg-red': color == '1' }">` |
|
||||
| `c-required` | Required condicional | `c-required="'2' not in camposquitar"` |
|
||||
| `c-form` | Formulario | `<c-form tableName="'contacto'" captcha="true">` |
|
||||
|
||||
## Filtros Twig
|
||||
|
||||
| Filtro | Uso |
|
||||
|--------|-----|
|
||||
| `get` | `'tabla' \| get(where, order, limit)` |
|
||||
| `queryDB` | `'SELECT ... FROM cms_tabla' \| queryDB()` |
|
||||
| `hook` | `'hooks/module_id/' \| hook({params})` |
|
||||
| `module` | `'module_id' \| module({params})` |
|
||||
| `imagec` | `path \| imagec(width)` |
|
||||
| `translate` | `'texto' \| translate` (tabla `textos_generales`) |
|
||||
| `raw` | `variable \| raw` |
|
||||
| `truncate` | `text \| truncate(100)` |
|
||||
| `json_decode` | `'json_string' \| json_decode` |
|
||||
| `default` | `variable \| default('fallback')` |
|
||||
| `length`, `upper`, `lower`, `trim`, `replace`, `split`, `filter` | Estándar Twig |
|
||||
|
||||
## Formato de datos para insert/update
|
||||
|
||||
| Tipo | Formato | Ejemplo |
|
||||
|------|---------|---------|
|
||||
| `textfield` | String | `"Texto"` |
|
||||
| `textbox` | String multilínea | `"Línea 1\nLínea 2"` |
|
||||
| `date`/datetime | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` |
|
||||
| `wysiwyg` | HTML string | `"<p>Texto</p>"` |
|
||||
| `list` | String o número | `"activo"` o `"1"` |
|
||||
| `checkbox` | Número 1/0 | `1` o `0` |
|
||||
| `multitext` | String JSON | `"[{\"item\":\"valor\"}]"` |
|
||||
| `upload` | NO enviar — usar `upload_record_image` después |
|
||||
|
||||
## Variables globales
|
||||
|
||||
| Variable | Descripción |
|
||||
|----------|-------------|
|
||||
| `section_id` | ID único por instancia del módulo |
|
||||
| `interno` | `true` dentro del editor CMS |
|
||||
| `server.HTTP_HOST` | Dominio actual (sin protocolo) |
|
||||
| `loop.index` | Índice 1-based en `c-for`/`{% for %}` |
|
||||
| `loop.index is odd` / `is even` | Layouts alternados |
|
||||
| `thisrecord` | Registro actual (solo en secciones generales) |
|
||||
|
||||
## Decisión rápida — qué tool usar
|
||||
|
||||
| Intención | Tool / workflow |
|
||||
|-----------|-----------------|
|
||||
| Crear módulo nuevo | `acai-write` `index-base.tpl` → `add_module_to_record` → `set_module_config_vars` |
|
||||
| Editar template de módulo | `acai-view` → `acai-line-replace` |
|
||||
| Ver datos de un módulo en una página | `get_module_config_vars` |
|
||||
| Cambiar valores de un módulo | `set_module_config_vars` |
|
||||
| Subir imagen a un módulo | Usa `uploadFields` de `set_module_config_vars` → `upload_record_image` (`tableName: "builder_custom"`) |
|
||||
| Crear tabla nueva | `create_table` (pregunta `enlace`/`seoMetas` antes) → `create_field` |
|
||||
| Crear detalle de registro | Sección general en `template/estandar/modulos/custom-{tableName}/` |
|
||||
| Editar header / footer | `get_layout_field` → `set_layout_field` (NUNCA edites los `.tpl` directamente) |
|
||||
| Añadir librería global | `list_global_libraries` → `add_global_library` (`top` o `bottom`) |
|
||||
| Hook que se ejecuta antes de cada página | `acai-write` el `.php` → `set_hook_middleware({ middleWare: ["allurls"] })` |
|
||||
| Generar imagen IA | `generate_image` → `upload_record_image` con `uploadUrl`/`fullUrl` |
|
||||
| Buscar archivos | `acai-glob` |
|
||||
| Buscar texto en archivos | `acai-grep` |
|
||||
| URL del proyecto | `get_web_url` (añade `?pruebas=1`) |
|
||||
| Navegar el preview del usuario | `navigate_browser` |
|
||||
| Token JWT expirado (403) | `refresh_acai_token` |
|
||||
| Necesito un doc no cargado | `read_doc({ name: "..." })` |
|
||||
| Listado de docs | `list_docs()` |
|
||||
|
||||
## Errores comunes a evitar
|
||||
|
||||
- Editar `index.tpl`, `index-twig.tpl` o `builder.json` (autogenerados).
|
||||
- Editar `layout.json` o `custom-header-twig/*` directamente (usa `set_layout_field`).
|
||||
- Usar el `sectionId` como `recordId` para subir imágenes (es el `num` de `builder_custom`).
|
||||
- Usar el nombre de la variable como `fieldName` (es el campo de relations: `image1`, no `imagenes`).
|
||||
- Crear página por registro en `apartados` para detalles (usa `custom-{tableName}/`).
|
||||
- Cambiar `enlace` o `controlador` de un registro existente.
|
||||
- Usar `localhost:8080` o dominios de producción (siempre `get_web_url` + `?pruebas=1`).
|
||||
- Crear archivos JSON de i18n (usa `| translate` + tabla `textos_generales`).
|
||||
- Usar Twig dentro de `script.js` o `style.css` (estáticos — pasa valores via `data-*`).
|
||||
- Llamar `mkdir` (usa `acai-write` directamente — crea el directorio padre).
|
||||
@@ -1,481 +0,0 @@
|
||||
# Builder Fields & Acai Attributes
|
||||
|
||||
## Nombres de variables
|
||||
|
||||
El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas).
|
||||
|
||||
Reglas obligatorias:
|
||||
- Todo elemento editable con `data-field-type` debe incluir también `data-field-label`
|
||||
- Si falta `data-field-label`, el builder puede generar variables temporales o incorrectas y el módulo queda mal configurado
|
||||
- Usa labels descriptivos y estables; no dejes labels vacíos ni genéricos como "Campo" o "Texto"
|
||||
|
||||
Evita también en `index-base.tpl` las clases Tailwind con valores arbitrarios como `text-[44px]`, `font-['Cinzel']` o `leading-[1.1]`. En este stack pueden romper el parseo/compilación del template. Muévelas a `style.css`.
|
||||
|
||||
| Label | Variable |
|
||||
|-------|----------|
|
||||
| Categoría Noticia | `categoranoticia` |
|
||||
| Color Principal | `colorprincipal` |
|
||||
| Título Producto | `ttuloproducto` |
|
||||
|
||||
---
|
||||
|
||||
## Field Types (`data-field-type`)
|
||||
|
||||
| Type | Element | Returns |
|
||||
|------|---------|---------|
|
||||
| `textfield` | `<p>` | String |
|
||||
| `headfield` | `<h1>`-`<h6>` | String + variable `_tag` con la etiqueta elegida |
|
||||
| `textbox` | `<div>` | String multi-línea |
|
||||
| `wysiwyg` | `<div class="wysiwyg">` | HTML string |
|
||||
| `link` | `<a>` | URL string (ya incluye barras) |
|
||||
| `upload` | `<img>` | **Array** de `{urlPath, info1, info2, info3, info4}` |
|
||||
| `uploadMulti` | `<li>` | Itera sobre archivos subidos |
|
||||
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
||||
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
||||
| `multiv2` | `<li>` wrapper | Array de objetos |
|
||||
|
||||
### textfield
|
||||
|
||||
```html
|
||||
<p data-field-type="textfield" data-field-label="Título">
|
||||
Elemento editable
|
||||
</p>
|
||||
```
|
||||
|
||||
### headfield
|
||||
|
||||
Genera 2 variables: la estándar y otra con sufijo `_tag` con la etiqueta elegida por el usuario.
|
||||
|
||||
```html
|
||||
<{{ title_tag | default('h2') }} data-field-type="headfield" data-field-label="Título Sección" class="text-3xl font-bold">
|
||||
Título de la sección
|
||||
</{{ title_tag | default('h2') }}>
|
||||
```
|
||||
|
||||
### textbox
|
||||
|
||||
```html
|
||||
<div data-field-type="textbox" data-field-label="Descripción">
|
||||
Texto largo editable
|
||||
</div>
|
||||
```
|
||||
|
||||
### wysiwyg
|
||||
|
||||
```html
|
||||
<div class="wysiwyg" data-field-type="wysiwyg" data-field-label="Contenido Enriquecido">
|
||||
<p>Texto con <strong>estilos</strong> editables</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### link
|
||||
|
||||
```html
|
||||
<a data-field-type="link" data-field-label="Enlace Principal" href="#">
|
||||
Haz clic aquí
|
||||
</a>
|
||||
```
|
||||
|
||||
### upload
|
||||
|
||||
```html
|
||||
<div class="p-1/6 relative">
|
||||
<img
|
||||
class="absolute top-0 left-0 w-full h-full object-cover object-center lazyload"
|
||||
data-field-type="upload"
|
||||
data-field-label="Imagen Principal"
|
||||
data-lazy="true"
|
||||
data-field-info1="titulo"
|
||||
data-field-width="1400"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
```
|
||||
|
||||
Atributos disponibles:
|
||||
- `data-lazy="true"`: Carga perezosa
|
||||
- `data-field-width="1400"`: Ancho máximo sugerido
|
||||
- `data-field-info1="titulo"`: Campo de información adicional (usado como alt)
|
||||
|
||||
Acceso en Twig: `{{ imagen[0].urlPath }}`, `{{ imagen[0].info1 }}`
|
||||
|
||||
### uploadMulti
|
||||
|
||||
Itera sobre todas las imágenes subidas:
|
||||
|
||||
```html
|
||||
<li data-field-type="uploadMulti" data-field-label="Galería" data-field-info1="titulo">
|
||||
<div class="relative min-h-screen">
|
||||
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
|
||||
data-src="{{ uploadMulti.urlPath | imagec(2100) }}"
|
||||
alt="{{ uploadMulti.info1 }}">
|
||||
</div>
|
||||
</li>
|
||||
```
|
||||
|
||||
### list (opciones fijas)
|
||||
|
||||
```html
|
||||
<div
|
||||
data-field-type="list"
|
||||
data-field-label="Color Producto"
|
||||
data-list-options="Rojo,Azul,|Verde,3|Amarillo"
|
||||
>
|
||||
</div>
|
||||
```
|
||||
|
||||
Formato de opciones: `opcion1,opcion2,|opcion3,valor3|opcion4`
|
||||
|
||||
### list (tabla)
|
||||
|
||||
```html
|
||||
<div
|
||||
data-field-type="list"
|
||||
data-field-label="Noticia Destacada"
|
||||
data-list-table="noticias"
|
||||
data-list-value="num"
|
||||
data-list-label="titulo"
|
||||
>
|
||||
{{ record.titulo }}
|
||||
</div>
|
||||
```
|
||||
|
||||
- `data-list-table`: Nombre de tabla sin prefijo `cms_`
|
||||
- `data-list-value`: Campo a usar como valor (generalmente `num`)
|
||||
- `data-list-label`: Campo a mostrar como label
|
||||
|
||||
### multiv2 — Campos repetibles
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li data-field-type="multiv2" data-field-label="Productos">
|
||||
<div data-field-type="textfield" data-field-label="Nombre">
|
||||
Nombre del producto
|
||||
</div>
|
||||
<div data-field-type="textbox" data-field-label="Descripción">
|
||||
Descripción del producto
|
||||
</div>
|
||||
<div class="p-1/6 relative">
|
||||
<img
|
||||
class="absolute top-0 left-0 w-full h-full object-cover lazyload"
|
||||
data-field-type="upload"
|
||||
data-field-label="Imagen"
|
||||
data-lazy="true"
|
||||
data-field-width="800"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Uso en Twig — las variables son propiedades del objeto iterado:
|
||||
|
||||
```twig
|
||||
{% for record in productos %}
|
||||
<div class="producto">
|
||||
<h3>{{ record.nombre }}</h3>
|
||||
<p>{{ record.descripcion }}</p>
|
||||
<img src="{{ record.imagen[0].urlPath }}" alt="">
|
||||
</div>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acai Attributes
|
||||
|
||||
### `c-if` — Renderizado condicional
|
||||
|
||||
```html
|
||||
<!-- Verificar existencia de variable -->
|
||||
<div c-if="subtitle">{{ subtitle }}</div>
|
||||
|
||||
<!-- Comparación de valores (usa = no ==) -->
|
||||
<div c-if="layout = 'grid'">Grid layout</div>
|
||||
```
|
||||
|
||||
### `c-else`
|
||||
|
||||
Debe ir inmediatamente después del elemento `c-if`:
|
||||
|
||||
```html
|
||||
<div c-if="image">
|
||||
<img src="{{ image[0].urlPath }}" />
|
||||
</div>
|
||||
<div c-else>
|
||||
<p>No image available</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-for` — Iteración sobre array
|
||||
|
||||
```html
|
||||
<div c-for="item in record.features">
|
||||
<h3>{{ item.title }}</h3>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-for` — Iteración sobre tabla de BD
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li c-for="producto in productos" c-where="'visible=1'" c-order="'num desc'" c-limit="10">
|
||||
{{ producto.title }}
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Parámetros opcionales: `c-where` (condición SQL), `c-order` (orden), `c-limit` (límite).
|
||||
|
||||
Equivalente en Twig:
|
||||
```twig
|
||||
{% for producto in 'productos' | get('visible=1','num desc',10) %}
|
||||
<li>{{ producto.title }}</li>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Dentro del loop: `loop.index` (1-based), `loop.index is odd`, `loop.index is even`
|
||||
|
||||
### `c-class` — Clases CSS condicionales
|
||||
|
||||
```html
|
||||
<!-- Simple -->
|
||||
<div c-class="{ 'text-center': alineacion == '1', 'text-right': alineacion == '2' }">
|
||||
|
||||
<!-- Múltiples condiciones -->
|
||||
<div c-class="{
|
||||
'flex-row-reverse': orden == '1',
|
||||
'cursor-pointer click-a-child': record.enlace_anchor,
|
||||
'rounded-xl': radioborde == '4'
|
||||
}">
|
||||
|
||||
<!-- Con expresiones Twig (loop) -->
|
||||
<div c-class="{
|
||||
'md:order-1': loop.index is odd,
|
||||
'md:pl-6': loop.index is even
|
||||
}">
|
||||
|
||||
<!-- Combinado con clases estáticas -->
|
||||
<div class="flex items-center" c-class="{ 'justify-center': centrado }">
|
||||
```
|
||||
|
||||
### `c-hidden` — Elementos ocultos
|
||||
|
||||
Elemento que no se renderiza pero puede declarar variables builder:
|
||||
|
||||
```html
|
||||
<div c-hidden="true">
|
||||
<input data-field-type="textfield" data-field-label="Config" value="default" />
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-required` — Campos requeridos condicionales
|
||||
|
||||
```html
|
||||
<input type="text" name="telefono" c-required="'2' not in camposquitar" placeholder="Teléfono">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Definiendo variables con `<set>`
|
||||
|
||||
```html
|
||||
<!-- Obtener configuración de la BD -->
|
||||
<set :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
|
||||
|
||||
<!-- Construir URLs dinámicas -->
|
||||
<set :logo="tienda.logo.0.urlPath ? 'https://' ~ server.HTTP_HOST ~ tienda.logo.0.urlPath : 'https://' ~ server.HTTP_HOST ~ '/template/estandar/images/logo.png'"></set>
|
||||
|
||||
<!-- Twig set para expresiones complejas -->
|
||||
{% set gracias = 'apartados' | get('num = 20').0 %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Incluyendo módulos
|
||||
|
||||
Para incluir un módulo dentro de otro módulo o sección general, usa el ID del módulo como etiqueta HTML:
|
||||
|
||||
```html
|
||||
<module_id :param1="value1" :param2="value2"></module_id>
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
```html
|
||||
<header_menu :showLogo="true" :menuItems="items"></header_menu>
|
||||
<product_card :product="selectedProduct" :showPrice="true"></product_card>
|
||||
```
|
||||
|
||||
El módulo hijo recibe los parámetros como variables en su contexto.
|
||||
|
||||
---
|
||||
|
||||
## Formularios (`c-form`)
|
||||
|
||||
Manejo automático de validación, almacenamiento en BD y envío de emails.
|
||||
|
||||
```html
|
||||
<c-form
|
||||
class="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow"
|
||||
tableName="'solicitudes'"
|
||||
mailRecord="['correos', 'CONTACTO']"
|
||||
sendTo="'contacto@empresa.com'"
|
||||
sendToClient="'email'"
|
||||
captcha="true"
|
||||
honeypot="true"
|
||||
messageOK="'¡Gracias! Te contactaremos pronto'"
|
||||
messageKO="'Por favor, completa todos los campos'"
|
||||
redirect="'/gracias'"
|
||||
attachFiles="true"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2">Nombre</label>
|
||||
<input name="nombre" type="text" class="w-full p-2 border rounded" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2">Email</label>
|
||||
<input name="email" type="text" class="w-full p-2 border rounded" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2">Mensaje</label>
|
||||
<textarea name="mensaje" class="w-full p-2 border rounded" rows="5" required></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center">
|
||||
<input name="acepto_politica" type="checkbox" class="mr-2" required>
|
||||
<span>Acepto la política de privacidad</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="bg-teal-500 text-white px-6 py-2 rounded hover:bg-teal-600">Enviar</button>
|
||||
<captcha/>
|
||||
</c-form>
|
||||
```
|
||||
|
||||
### Atributos de c-form
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| `tableName="'table'"` | Tabla donde almacenar registros |
|
||||
| `mailRecord="['correos', 'ID']"` | Template de email de la tabla `correos` |
|
||||
| `sendTo="'email@domain.com'"` | Destinatarios (separados por coma) |
|
||||
| `sendToClient="'campo_email'"` | Campo con email del cliente para auto-reply |
|
||||
| `captcha="true"` | Google reCAPTCHA |
|
||||
| `honeypot="true"` | Campo oculto anti-spam |
|
||||
| `messageOK="'texto'"` | Mensaje de éxito |
|
||||
| `messageKO="'texto'"` | Mensaje de error |
|
||||
| `redirect="'/path/'"` | Redirección tras envío exitoso |
|
||||
| `attachFiles="true"` | Adjuntar archivos al email |
|
||||
| `showImages="true"` | Mostrar thumbnails en email |
|
||||
| `emailMode="'twig'"` | Email en formato Twig |
|
||||
| `header="'<div>...</div>'"` | HTML cabecera del email |
|
||||
| `footer="'<div>...</div>'"` | HTML footer del email |
|
||||
| `styles="'body { ... }'"` | CSS para el email |
|
||||
|
||||
---
|
||||
|
||||
## Componentes Built-in
|
||||
|
||||
### Carousel (`c-tns-wrapper`)
|
||||
|
||||
```html
|
||||
<div class="c-tns-wrapper"
|
||||
data-responsive='{"0":1,"768":2,"1024":3}'
|
||||
data-speed="400"
|
||||
data-nav="true"
|
||||
data-autoplay-timeout="3000">
|
||||
<div c-for="slide in record.slides">
|
||||
<img src="{{ slide.image[0].urlPath }}" />
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Lightbox
|
||||
|
||||
```html
|
||||
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
|
||||
<img src="{{ image[0].urlPath | imagec(400) }}" />
|
||||
</a>
|
||||
```
|
||||
|
||||
### Breadcrumb
|
||||
|
||||
```html
|
||||
<breadCrumb/>
|
||||
```
|
||||
|
||||
### Animate On Scroll (AOS)
|
||||
|
||||
```html
|
||||
<div data-aos="fade-up" data-aos-delay="200">
|
||||
Animated content
|
||||
</div>
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```html
|
||||
<img class="lazyload" data-src="{{ image[0].urlPath }}" />
|
||||
<!-- o -->
|
||||
<img data-lazy="true" src="{{ image[0].urlPath }}" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Puntos importantes
|
||||
|
||||
1. **Nombres de variables:** `data-field-label` → sin espacios ni caracteres especiales, minúsculas
|
||||
2. **Variables en multiv2:** Son propiedades del objeto iterado (`record.nombre`)
|
||||
3. **Campos upload:** Retornan arrays, no strings (`imagen[0].urlPath`, no `imagen`)
|
||||
4. **c-if usa `=` no `==`:** `c-if="layout = 'grid'"` (un solo igual)
|
||||
5. **c-for tabla:** El nombre de tabla va sin prefijo `cms_`
|
||||
6. **Enlace:** Ya incluye barras, no añadir extras
|
||||
7. **Checkbox:** Valores `1` o `0`, no `true`/`false`
|
||||
|
||||
---
|
||||
|
||||
## MCP Tools: Config Vars e Imágenes de Módulos
|
||||
|
||||
### Regla importante: Siempre rellenar variables al añadir un módulo
|
||||
Cuando se añade un módulo a una página (con `add_module_to_record`), este queda vacío y no muestra nada visible. **SIEMPRE** hay que llamar a `set_module_config_vars` inmediatamente después para rellenar las variables con contenido de ejemplo coherente con el contexto del sitio. Incluir:
|
||||
- Textos (títulos, descripciones, pretítulos) con contenido relevante al sitio
|
||||
- Valores de listas/selects con una opción válida
|
||||
- Para variables multi (records), crear al menos 2-3 items de ejemplo
|
||||
- Para variables de imagen (upload), usar `generate_image` o `upload_record_image` para que el módulo se vea completo
|
||||
|
||||
Un módulo sin variables configuradas es invisible en la web.
|
||||
|
||||
### Leer variables de un módulo
|
||||
Antes de modificar cualquier módulo, usar `get_module_config_vars` para conocer el estado actual:
|
||||
- **tableName**: tabla del registro padre (ej: `apartados`), SIN prefijo `cms_`
|
||||
- **recordNum**: campo `num` del registro padre (ej: `2`)
|
||||
- **sectionId**: el `section_id` de la instancia del módulo (ej: `6c6d8`)
|
||||
|
||||
### Escribir variables de un módulo
|
||||
Usar `set_module_config_vars` con los mismos tableName, recordNum y sectionId. Pasar todos los valores como strings.
|
||||
|
||||
La respuesta incluye `configVars` con el `recordNum` del registro `builder_custom` creado/actualizado y `uploadFields` para imágenes.
|
||||
|
||||
**Tipos de almacenamiento (manejado automáticamente):**
|
||||
- `headfield`, `textfield`, `link`, `textbox`, `wysiwyg`, `upload` → se guardan en tabla `builder_custom`
|
||||
- `list`, `checkbox`, `colorpicker` → se guardan directamente en el JSON config-vars (no en builder_custom)
|
||||
|
||||
No necesitas preocuparte por esto — `set_module_config_vars` lo maneja internamente. Solo pasa los valores como strings.
|
||||
|
||||
### Subir imágenes a un módulo
|
||||
El nombre del campo de imagen viene de `builder.json` → `vars.NOMBRE.relations.builder_custom` (ej: `"image1"`). NO es el nombre de la variable (ej: NO `"imagenes"`).
|
||||
|
||||
**Flujo correcto:**
|
||||
1. `get_module_config_vars` → obtener el `recordNum` en builder_custom de la variable de imagen
|
||||
2. `upload_record_image` con:
|
||||
- `tableName`: `"builder_custom"` (siempre, sin prefijo cms_)
|
||||
- `recordId`: el `recordNum` del paso 1 (ej: `"778"`)
|
||||
- `fieldName`: el campo de relations del builder.json (ej: `"image1"`)
|
||||
- `imageUrl`: URL completa accesible desde Docker
|
||||
3. `reorder_record_uploads` si es necesario — pasar array de upload IDs en el orden deseado
|
||||
4. `list_record_uploads` para verificar
|
||||
|
||||
**Errores comunes a evitar:**
|
||||
- NO usar el sectionId como recordId — usar el `num` de builder_custom
|
||||
- NO usar el nombre de la variable como fieldName — usar el campo de relations del builder.json (ej: `image1`, no `imagenes`)
|
||||
- NO poner prefijo `cms_` en tableName
|
||||
@@ -1,261 +0,0 @@
|
||||
# CSS & JavaScript Conventions
|
||||
|
||||
## Estructura del módulo
|
||||
|
||||
- Genera HTML + CSS + JS (o Vue 3 si es necesario)
|
||||
- Define una clase raíz en kebab-case: `product-card`, `hero-section`, etc.
|
||||
- Todo el CSS y JS scopeado bajo esa clase raíz
|
||||
|
||||
---
|
||||
|
||||
## CSS
|
||||
|
||||
### Tailwind First
|
||||
|
||||
Usar TailwindCSS como método principal. Solo CSS custom cuando Tailwind no cubra el estilo o se necesiten estados complejos/transiciones específicas.
|
||||
|
||||
```html
|
||||
<div class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Title</h2>
|
||||
</div>
|
||||
```
|
||||
|
||||
### BEM para CSS Custom
|
||||
|
||||
Cuando se necesite CSS personalizado, siempre scopeado bajo la clase raíz con BEM:
|
||||
|
||||
```css
|
||||
.hero-section { }
|
||||
.hero-section__title { }
|
||||
.hero-section__image { }
|
||||
.hero-section--dark { }
|
||||
```
|
||||
|
||||
Nunca usar clases globales sin prefijo de módulo.
|
||||
|
||||
### CSS Variables del tema
|
||||
|
||||
```css
|
||||
var(--main-color) /* Color de marca primario */
|
||||
var(--main-color-light) /* Variante clara */
|
||||
var(--main-color-dark) /* Variante oscura */
|
||||
```
|
||||
|
||||
### Estilos inline con fallbacks
|
||||
|
||||
Patrón para colores configurables por el usuario:
|
||||
|
||||
```html
|
||||
<div style="background-color: {{ colordefondo ? colordefondo : 'transparent' }}">
|
||||
<p style="color: {{ colordeltexto ? colordeltexto : '#111827' }}">
|
||||
```
|
||||
|
||||
### Clases utilitarias de Acai
|
||||
|
||||
| Clase | Descripción |
|
||||
|-------|-------------|
|
||||
| `transition3s` | Transición suave 0.3s |
|
||||
| `click-a-child` | Hace el padre clickeable via primer `<a>` hijo |
|
||||
| `line-clamp2` / `line-clamp3` / `line-clamp5` | Truncar texto a N líneas |
|
||||
| `filter-white` | Filtro CSS para hacer imágenes/iconos blancos |
|
||||
| `lazyload` | Lazy loading (usar con `data-src`) |
|
||||
| `text-shadow` | Sombra de texto para legibilidad sobre imágenes |
|
||||
| `wysiwyg` | Wrapper para contenido de texto enriquecido |
|
||||
| `bg-main-color` / `bg-main-color-light` / `bg-main-color-dark` | Fondos con color primario |
|
||||
| `text-main-color` / `text-main-color-light` / `text-main-color-dark` | Texto con color primario |
|
||||
|
||||
---
|
||||
|
||||
## JavaScript
|
||||
|
||||
### Module Scripts (`script.js`)
|
||||
|
||||
JavaScript scopeado al módulo usando `section_id`:
|
||||
|
||||
This is the default and expected place for module JavaScript.
|
||||
Do NOT embed `<script>` tags directly inside `index-base.tpl` for normal module behavior.
|
||||
Keep `index-base.tpl` for HTML/Twig markup and put interactive logic in `script.js`.
|
||||
`script.js` is a static file: do NOT use Twig syntax inside it.
|
||||
Do NOT write `{{ section_id }}`, `{{ variable }}`, `{% if ... %}`, or builder attributes inside `script.js`.
|
||||
|
||||
If JavaScript needs dynamic values, pass them from `index-base.tpl` through `data-*` attributes:
|
||||
|
||||
```html
|
||||
<section
|
||||
class="buscador-apartados"
|
||||
data-section-id="{{ section_id }}"
|
||||
data-hook-endpoint="/hooks/buscadorapartados_hjd8s/">
|
||||
</section>
|
||||
```
|
||||
|
||||
```js
|
||||
document.querySelectorAll('.buscador-apartados').forEach((section) => {
|
||||
const sectionId = section.dataset.sectionId;
|
||||
const hookEndpoint = section.dataset.hookEndpoint;
|
||||
const buttons = section.querySelectorAll('.btn');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### CmsApi (Client-Side)
|
||||
|
||||
```js
|
||||
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
|
||||
console.log(response);
|
||||
});
|
||||
```
|
||||
|
||||
If you are calling a hook that belongs to the current module, the endpoint must use the real module id:
|
||||
|
||||
```js
|
||||
// Module folder: template/estandar/modulos/buscadorapartados_hjd8s/
|
||||
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, function(response) {
|
||||
console.log(response);
|
||||
});
|
||||
```
|
||||
|
||||
Do not try to build this endpoint with Twig inside `script.js`. Put the final endpoint in a `data-hook-endpoint` attribute in `index-base.tpl` if needed.
|
||||
|
||||
### Module Styles (`style.css`)
|
||||
|
||||
`style.css` is also a static file.
|
||||
Do NOT use Twig syntax or builder attributes inside it.
|
||||
Do NOT write selectors or values that depend on `{{ section_id }}`.
|
||||
|
||||
Scope styles with the module root class instead of dynamic Twig ids.
|
||||
|
||||
### Cuándo usar Vue 3
|
||||
|
||||
Usar Vue 3 CDN cuando la lógica requiera:
|
||||
- Doble binding / reactividad
|
||||
- Solicitudes asíncronas complejas
|
||||
- Componentes reutilizables
|
||||
- Gestión de estado local
|
||||
- Ciclos de vida
|
||||
|
||||
Para lógica simple, usar JavaScript vanilla.
|
||||
|
||||
### Vue 3 Integration
|
||||
|
||||
```html
|
||||
<div id="app-{{ section_id }}">
|
||||
<p>${ message }</p>
|
||||
<button @click="increment">${ count }</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref } = Vue;
|
||||
createApp({
|
||||
delimiters: ['${', '}'], // Evitar conflicto con Twig {{ }}
|
||||
setup() {
|
||||
const message = ref('Hello');
|
||||
const count = ref(0);
|
||||
const increment = () => count.value++;
|
||||
return { message, count, increment };
|
||||
}
|
||||
}).mount('#app-{{ section_id }}');
|
||||
</script>
|
||||
```
|
||||
|
||||
Siempre usar `'${'` y `'}'` como delimitadores Vue para evitar conflicto con Twig.
|
||||
|
||||
---
|
||||
|
||||
## Variables Globales Disponibles
|
||||
|
||||
| Variable | Descripción | Ejemplo |
|
||||
|----------|-------------|---------|
|
||||
| `section_id` | ID único por instancia del módulo | `<div id="{{section_id}}">` |
|
||||
| `server.HTTP_HOST` | Dominio actual | `https://{{ server.HTTP_HOST }}/path` |
|
||||
| `loop.index` | Índice de iteración (1-based) en c-for/for | `{{ loop.index }}` |
|
||||
| `loop.index is odd` | True en iteraciones impares | Layouts alternados |
|
||||
| `loop.index is even` | True en iteraciones pares | Patrones zigzag |
|
||||
| `interno` | True dentro del editor CMS | `c-class="{'editor-mode': interno}"` |
|
||||
|
||||
### Patrón section_id
|
||||
|
||||
Cada instancia de módulo recibe un `section_id` único. Usar para navigation anchor e IDs:
|
||||
|
||||
```html
|
||||
<div id="{{section_id}}"></div>
|
||||
<section id="id_{{ section_id }}" class="relative">
|
||||
<!-- contenido del módulo -->
|
||||
</section>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Componentes Nativos
|
||||
|
||||
### Carousel (`c-tns-wrapper`)
|
||||
|
||||
```html
|
||||
<div class="c-tns-wrapper" data-responsive="sm:2, md:3, lg:4" data-speed="1000" data-nav="true">
|
||||
<ul class="c-tns-container">
|
||||
<li data-field-type="multiv2" data-field-label="Slides" class="px-2">
|
||||
<!-- contenido del slide -->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
| Atributo | Descripción | Ejemplo |
|
||||
|----------|-------------|---------|
|
||||
| `data-responsive` | Items por breakpoint | `"sm:2, md:3, lg:4"` |
|
||||
| `data-autoplay-timeout` | Intervalo autoplay (ms) | `"5000"` |
|
||||
| `data-mode` | Modo de transición | `"gallery"` o `"carousel"` |
|
||||
| `data-speed` | Velocidad de transición (ms) | `"400"` |
|
||||
| `data-nav` | Puntos de navegación | `"true"` |
|
||||
|
||||
Dots de navegación custom:
|
||||
```html
|
||||
<div class="c-tns-nav-container absolute bottom-4 left-0 w-full flex justify-center items-end z-20">
|
||||
<div c-for="item in records"
|
||||
class="pointer-events-auto cursor-pointer rounded-full border-2 border-white w-4 h-4 mx-1 bg-black bg-opacity-50">
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Lightbox
|
||||
|
||||
```html
|
||||
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
|
||||
<img src="{{ image[0].urlPath | imagec(400) }}" />
|
||||
</a>
|
||||
```
|
||||
|
||||
### Breadcrumb
|
||||
|
||||
```html
|
||||
<breadCrumb class="bg-gray-200 p-3 rounded" c-prevlinks="null"></breadCrumb>
|
||||
```
|
||||
|
||||
### AOS (Animate On Scroll)
|
||||
|
||||
```html
|
||||
<div data-aos="fade-up" data-aos-duration="800">Contenido</div>
|
||||
```
|
||||
|
||||
Valores comunes: `fade-up`, `fade-down`, `fade-left`, `fade-right`, `zoom-in`, `zoom-in-up`, `fade-up-right`, `fade-up-left`
|
||||
|
||||
Después de cambios dinámicos: `AOS.refresh()` en JavaScript.
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```html
|
||||
<!-- Builder var con lazy loading -->
|
||||
<img data-field-type="upload" data-field-label="Imagen" data-lazy="true" data-field-width="800" alt="">
|
||||
|
||||
<!-- Manual en templates -->
|
||||
<img class="lazyload" data-src="{{ record.imagen[0].urlPath | imagec(800) }}" alt="">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Buenas prácticas
|
||||
|
||||
- HTML/Twig semántico
|
||||
- Código limpio y organizado
|
||||
- Evitar dependencias externas innecesarias
|
||||
- Evitar estilos inline salvo casos justificados (colores dinámicos del usuario)
|
||||
- No usar clases globales sin prefijo de módulo
|
||||
@@ -1,389 +0,0 @@
|
||||
# Hooks & Server-Side API
|
||||
|
||||
## Hooks
|
||||
|
||||
Hooks son archivos PHP que ejecutan lógica server-side. Pueden existir en dos sitios:
|
||||
- Hooks globales en `hooks/hooks.<hook-id>.php`
|
||||
- Hooks propios de módulo en `template/estandar/modulos/<module-id>/hook.php`
|
||||
|
||||
### Tipos de hooks
|
||||
|
||||
**1. Hook global**
|
||||
- Archivo: `hooks/hooks.<hook-id>.php`
|
||||
- Endpoint: `/hooks/<hook-id>/`
|
||||
- Úsalo cuando la lógica se reutiliza entre módulos, páginas o formularios
|
||||
|
||||
**2. Hook propio de módulo**
|
||||
- Archivo: `template/estandar/modulos/<module-id>/hook.php`
|
||||
- Endpoint: `/hooks/<module-id>/`
|
||||
- Úsalo cuando la lógica pertenece solo a ese módulo
|
||||
|
||||
Ejemplos:
|
||||
- `hooks/hooks.buscar_barcos.php` -> `/hooks/buscar_barcos/`
|
||||
- `template/estandar/modulos/hero_banner/hook.php` -> `/hooks/hero_banner/`
|
||||
|
||||
Regla práctica:
|
||||
- Si el hook solo sirve a un módulo, créalo dentro del módulo
|
||||
- Si varias piezas del proyecto lo van a consumir, créalo como hook global
|
||||
|
||||
## Reglas obligatorias para hooks
|
||||
|
||||
- Un hook debe devolver datos con `return [...]`
|
||||
- No uses `echo json_encode(...)`
|
||||
- No uses `exit`
|
||||
- Para leer parámetros, usa `$_REQUEST[...]` o las variables ya inyectadas por el sistema
|
||||
- En hooks, usa `CmsApi::get()` o `CocoDB::get()` como primera opción
|
||||
- No uses `CocoDB::getInstance()` salvo necesidad real muy excepcional
|
||||
- No escribas SQL manual con `prepare()/bind_param()` salvo que no exista forma razonable de resolverlo con `CmsApi` o `CocoDB`
|
||||
|
||||
### Estructura de un Hook
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Los parámetros se reciben como variables directamente
|
||||
// Ejemplo: Si llamas hook con {param1: 100}, tendrás $param1 = 100
|
||||
|
||||
$resultado = $param1 * 2;
|
||||
|
||||
// Retornar un array (se convierte a JSON)
|
||||
return [
|
||||
"success" => true,
|
||||
"message" => "Valor procesado: " . $resultado,
|
||||
"value" => $resultado
|
||||
];
|
||||
?>
|
||||
```
|
||||
|
||||
### Testing Hooks
|
||||
|
||||
El Docker debe estar corriendo. Hacer curl al endpoint del hook:
|
||||
|
||||
```bash
|
||||
curl {ACAI_WEB_URL}/hooks/example_hook/
|
||||
```
|
||||
(Use the project's actual URL, never localhost:8080)
|
||||
|
||||
No usar X-Hooks-Token en desarrollo local.
|
||||
|
||||
### Cómo Llamar Hooks
|
||||
|
||||
La referencia siempre se hace con el endpoint `/hooks/<id>/`:
|
||||
- Para hooks globales, `<id>` es el nombre lógico del hook sin `hooks.` ni `.php`
|
||||
- Para hooks de módulo, `<id>` es el `module-id` de la carpeta del módulo
|
||||
|
||||
**Desde HTML (recomendado para módulos):**
|
||||
```html
|
||||
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
|
||||
<p>{{ myVar.message }}</p>
|
||||
```
|
||||
|
||||
**Desde Twig:**
|
||||
```twig
|
||||
{% set resultado = 'hooks/mimodulo/' | hook({param1: 100, param2: 'texto'}) %}
|
||||
<p>{{ resultado.message }}</p>
|
||||
```
|
||||
|
||||
**Desde JavaScript:**
|
||||
```js
|
||||
CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
|
||||
console.log(data.message);
|
||||
});
|
||||
```
|
||||
|
||||
**Ejemplo real para hook de módulo:**
|
||||
```js
|
||||
// Módulo: template/estandar/modulos/buscadorapartados_hjd8s/
|
||||
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, (data) => {
|
||||
console.log(data);
|
||||
});
|
||||
```
|
||||
|
||||
**Desde otro Hook PHP:**
|
||||
```php
|
||||
<?php
|
||||
$result = hook("/hooks/mimodulo/", ["param1" => 100, "param2" => "texto"]);
|
||||
$mensaje = $result["message"];
|
||||
?>
|
||||
```
|
||||
|
||||
**Desde c-form:** Los hooks se ejecutan automáticamente al enviar el formulario si están configurados.
|
||||
|
||||
---
|
||||
|
||||
## CmsApi (PHP)
|
||||
|
||||
API server-side para operaciones de base de datos. Disponible en todos los hooks.
|
||||
|
||||
### Read — `CmsApi::get()`
|
||||
|
||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/<nombre_de_tabla>.ini.php
|
||||
|
||||
```php
|
||||
// Todos los registros
|
||||
$products = CmsApi::get("productos");
|
||||
|
||||
// Con condición WHERE en string
|
||||
$active = CmsApi::get("productos", "active=1");
|
||||
|
||||
// Con orden y límite
|
||||
$latest = CmsApi::get("noticias", "", "fecha DESC", 5);
|
||||
|
||||
// Con condición string
|
||||
$activos = CmsApi::get("productos", "activo=1");
|
||||
|
||||
// Condición compleja
|
||||
$caros = CmsApi::get("productos", "precio > 100");
|
||||
|
||||
// Múltiples condiciones (AND)
|
||||
$resultados = CmsApi::get("productos", "activo = 1 AND stock > 0");
|
||||
|
||||
// Con operadores
|
||||
$expensive = CmsApi::get("productos", "precio >= 100");
|
||||
$search = CmsApi::get("productos", "nombre LIKE '%keyword%'");
|
||||
$inList = CmsApi::get("productos", "categoria_num IN (1, 2, 3)");
|
||||
|
||||
#### Opciones de `get()`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `uploads` | bool | `true` | Incluir datos de upload fields |
|
||||
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` |
|
||||
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
|
||||
| `translates` | string | current lang | Código de idioma |
|
||||
| `groupBy` | string | null | GROUP BY clause |
|
||||
| `aggregates` | array | `[]` | Funciones de agregación |
|
||||
| `onlyFields` | array | null | Seleccionar solo campos específicos |
|
||||
| `debug` | bool | false | Mostrar SQL query |
|
||||
| `redis` | bool | null | Forzar cache Redis |
|
||||
| `redis_expire` | int | 60 | TTL de cache Redis (segundos) |
|
||||
|
||||
// Con opciones
|
||||
$datos = CmsApi::get("productos", "", "", "", [
|
||||
'translates' => true,
|
||||
'uploads' => true,
|
||||
'relations' => true,
|
||||
'relationsDepth' => 2
|
||||
]);
|
||||
```
|
||||
|
||||
### Insert — `CmsApi::insert()`
|
||||
|
||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/<nombre_de_tabla>.ini.php
|
||||
|
||||
```php
|
||||
// Un registro
|
||||
CmsApi::insert('contacto', [
|
||||
["nombre" => "John", "email" => "john@example.com", "mensaje" => "Hello"]
|
||||
]);
|
||||
|
||||
// Múltiples registros
|
||||
CmsApi::insert('productos', [
|
||||
["nombre" => "Producto A", "precio" => 100],
|
||||
["nombre" => "Producto B", "precio" => 200]
|
||||
]);
|
||||
|
||||
// Con retorno del último ID
|
||||
CmsApi::insert('productos',
|
||||
[["nombre" => "Nuevo", "precio" => 150]],
|
||||
[],
|
||||
['return_last_id' => true]
|
||||
);
|
||||
```
|
||||
|
||||
#### Opciones de insert
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `forceNum` | Permite setear el campo `num` manualmente |
|
||||
| `ignoreSchema` | Saltar validación de schema |
|
||||
| `ignoreFields` | Array de campos a ignorar |
|
||||
|
||||
|
||||
### Update — `CmsApi::update()`
|
||||
|
||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/<nombre_de_tabla>.ini.php
|
||||
|
||||
```php
|
||||
// Con condición string
|
||||
CmsApi::update('productos', ["precio" => 150], "num=1");
|
||||
|
||||
// Con condición array
|
||||
CmsApi::update('productos',
|
||||
["activo" => 1],
|
||||
[["column" => "num", "operator" => "=", "value" => 1]]
|
||||
);
|
||||
|
||||
// Múltiples registros
|
||||
CmsApi::update('productos', ["activo" => 0], "precio < 50");
|
||||
```
|
||||
|
||||
#### Opciones de update
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `forceNum` | Permite setear el campo `num` manualmente |
|
||||
| `ignoreSchema` | Saltar validación de schema |
|
||||
| `ignoreFields` | Array de campos a ignorar |
|
||||
|
||||
|
||||
### Delete — `CmsApi::delete()`
|
||||
|
||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/<nombre_de_tabla>.ini.php
|
||||
|
||||
```php
|
||||
CmsApi::delete('productos', "num=5");
|
||||
|
||||
CmsApi::delete('productos',
|
||||
[["column" => "activo", "operator" => "=", "value" => 0]]
|
||||
);
|
||||
```
|
||||
|
||||
### Reglas importantes
|
||||
|
||||
- Nombres de tabla **sin** prefijo `cms_`
|
||||
- Primary key siempre es `num`, nunca `id`
|
||||
- Foreign keys: `categoria_num`, no `categoria_id`
|
||||
- Upload fields: no se manejan via insert/update
|
||||
- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
|
||||
|
||||
---
|
||||
|
||||
## CmsApi (JavaScript — Client-Side)
|
||||
|
||||
```js
|
||||
// Llamar hook
|
||||
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
|
||||
// response es la salida del hook
|
||||
});
|
||||
|
||||
// Leer registros (si está expuesto via hooks)
|
||||
CmsApi.get('tableName', { where: conditions }, function(records) {
|
||||
// records array
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CocoDB
|
||||
|
||||
Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control.
|
||||
|
||||
Para búsquedas y lecturas habituales, prioriza:
|
||||
1. `CmsApi::get()`
|
||||
2. `CocoDB::get()`
|
||||
3. SQL manual solo si de verdad no hay alternativa razonable
|
||||
|
||||
### `CocoDB::get($table, $where, $order, $limit, $options)`
|
||||
## Funcionalidad exactamente igual a CmsApi::get ( ver referencia CmsApi::get )
|
||||
|
||||
|
||||
### `CocoDB::insertRecords($table, $records, $functions, $options)`
|
||||
## Funcionalidad exactamente igual a CmsApi::insert ( ver referencia CmsApi::insert )
|
||||
|
||||
### `CocoDB::updateRecords($table, $records, $where, $functions, $options)`
|
||||
## Funcionalidad exactamente igual a CmsApi::update ( ver referencia CmsApi::update )
|
||||
|
||||
|
||||
### `CocoDB::deleteRecords($table, $where, $options)`
|
||||
## Funcionalidad exactamente igual a CmsApi::delete ( ver referencia CmsApi::delete )
|
||||
|
||||
---
|
||||
|
||||
## Creación y Actualización de Registros
|
||||
|
||||
### Flujo correcto
|
||||
|
||||
1. Consultar el esquema de la tabla (leer `cms/data/schema/{tabla}.ini.php`)
|
||||
2. Revisar los tipos de campo
|
||||
3. Rellenar según el tipo de dato
|
||||
4. Enviar con la estructura correcta
|
||||
|
||||
### Tipos de campo y formato
|
||||
|
||||
| Tipo | Formato | Ejemplo |
|
||||
|------|---------|---------|
|
||||
| **Text field** | String | `"Texto"` |
|
||||
| **Text box** | String multilínea | `"Línea 1\nLínea 2"` |
|
||||
| **Date/time** | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` |
|
||||
| **Wysiwyg** | String HTML | `"<p class=\"font-bold\">Texto</p>"` |
|
||||
| **List** | String o número | `"activo"` o `"1"` (num si es foreign key) |
|
||||
| **Checkbox** | Número 1/0 | `1` o `0` |
|
||||
| **Multivalores** | String JSON | `"[{\"producto\":\"1\"}]"` |
|
||||
| **Upload** | **NO enviar** — usar `upload_record_image` después de crear el registro |
|
||||
|
||||
---
|
||||
|
||||
## Table Schemas
|
||||
|
||||
Los schemas están en `cms/data/schema/` como archivos `.ini.php`. Definen:
|
||||
- Nombres y tipos de campo
|
||||
- Reglas de validación
|
||||
- Relaciones (foreign keys)
|
||||
- Configuración de display
|
||||
|
||||
---
|
||||
|
||||
## Ejemplos Prácticos
|
||||
|
||||
### Hook de Cálculo de Precio
|
||||
|
||||
```php
|
||||
<?php
|
||||
// hook.php del módulo "calcular_precio"
|
||||
$precioUnitario = 50;
|
||||
|
||||
if ($tipo === 'mayoreo' && $cantidad > 10) {
|
||||
$precioUnitario *= 0.85; // 15% descuento
|
||||
}
|
||||
|
||||
return [
|
||||
"success" => true,
|
||||
"precioUnitario" => round($precioUnitario, 2),
|
||||
"total" => round($precioUnitario * $cantidad, 2),
|
||||
"descuento" => $tipo === 'mayoreo' ? 15 : 0
|
||||
];
|
||||
?>
|
||||
```
|
||||
|
||||
```html
|
||||
<hook result="precio" endpoint="/hooks/calcular_precio/" :cantidad="10" :tipo="'mayoreo'"></hook>
|
||||
<p>Total: ${{ precio.total }}</p>
|
||||
```
|
||||
|
||||
En este ejemplo, el endpoint usa `calcular_precio` porque el archivo vive en:
|
||||
`template/estandar/modulos/calcular_precio/hook.php`
|
||||
|
||||
### Hook con Operaciones de BD
|
||||
|
||||
```php
|
||||
<?php
|
||||
// hook.php del módulo "procesar_compra"
|
||||
$producto = CmsApi::get("productos", "num=" . $producto_id);
|
||||
|
||||
if (empty($producto)) {
|
||||
return ["success" => false, "message" => "Producto no encontrado"];
|
||||
}
|
||||
|
||||
$total = $producto[0]['precio'] * $cantidad;
|
||||
|
||||
// Crear venta
|
||||
CmsApi::insert('ventas', [[
|
||||
"usuario_num" => $usuario_id,
|
||||
"producto_num" => $producto_id,
|
||||
"cantidad" => $cantidad,
|
||||
"total" => $total,
|
||||
"fecha" => date('Y-m-d H:i:s')
|
||||
]], [], ['return_last_id' => true]);
|
||||
|
||||
// Actualizar stock
|
||||
$stock = CmsApi::get("stocks", "producto_num=" . $producto_id);
|
||||
if (!empty($stock)) {
|
||||
CmsApi::update('stocks',
|
||||
["cantidad" => $stock[0]['cantidad'] - $cantidad],
|
||||
"producto_num=$producto_id"
|
||||
);
|
||||
}
|
||||
|
||||
return ["success" => true, "total" => $total];
|
||||
?>
|
||||
```
|
||||
@@ -1,159 +0,0 @@
|
||||
# Acai CMS — Project Instructions
|
||||
|
||||
This is an Acai CMS website project. Follow these instructions when working with the codebase.
|
||||
|
||||
## Environment
|
||||
|
||||
- The site runs in Docker. **Before using fetch or Playwright, call the `get_web_url` tool** to get the correct development URL. Never hardcode or guess URLs.
|
||||
- **ALWAYS append `?pruebas=1`** to any URL you visit (e.g. `http://.../?pruebas=1`). This query param is required to view the site in development mode.
|
||||
- **NEVER navigate to the production domain** (e.g. `keepsailing.es`, `tienda.com`). The production site is NOT your development environment.
|
||||
- **NEVER use localhost:8080 or https://** — use HTTP (not HTTPS) with the URL from `get_web_url`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── template/estandar/
|
||||
│ ├── modulos/ # Builder modules (visual components)
|
||||
│ │ └── <module-id>/
|
||||
│ │ ├── index-base.tpl # Twig template (source — EDIT THIS)
|
||||
│ │ ├── style.css # Module styles
|
||||
│ │ └── script.js # Module JavaScript
|
||||
│ │ ├── index.tpl # Compiled (auto-generated, do NOT edit)
|
||||
│ │ ├── index-twig.tpl # Compiled (auto-generated, do NOT edit)
|
||||
│ │ └── builder.json # Compiled builder vars (auto-generated, do NOT edit)
|
||||
│ ├── css/ # Global CSS
|
||||
│ └── js/ # Global JavaScript
|
||||
├── hooks/ # PHP hooks (server-side logic)
|
||||
├── cms/
|
||||
│ ├── data/schema/ # Database table schemas (JSON)
|
||||
│ ├── lib/plugins/ # CMS plugins
|
||||
│ └── uploads/ # Uploaded media files
|
||||
├── .acai # Project config (domain, tokens, DB credentials)
|
||||
├── .docker/
|
||||
│ ├── .env # Docker environment (DB credentials)
|
||||
│ ├── docker-compose.yml
|
||||
│ ├── tunnel-url.txt # Public tunnel URL (if active)
|
||||
│ └── bore-db-url.txt # Database tunnel URL (if active)
|
||||
└── database.sql # Database dump
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Modules (`template/estandar/modulos/`)
|
||||
Visual components that the site builder uses. Each module is a self-contained unit with its own template (Twig + Acai attributes), CSS, and JS. Modules are placed on pages via the drag-and-drop builder. The editable file is always `index-base.tpl`.
|
||||
|
||||
- Include other modules: `<module_id :param1="value1"></module_id>`
|
||||
- Each module instance gets a unique `section_id` variable for anchors/scoping
|
||||
- Use `interno` variable to detect CMS editor mode vs public view
|
||||
|
||||
See [docs/modular-system.md](docs/modular-system.md) for detailed rules.
|
||||
|
||||
### Pages
|
||||
Every record with an `enlace` field is a page. Pages are either **Builder** (modular) or **Standard**:
|
||||
|
||||
- **Builder**: `controlador` = `cms/lib/plugins/builder_saas/controlador.php` — content via modules
|
||||
- **Standard**: `controlador` = `cms/lib/plugins/builder_saas/controlador_tabla.php` — content in record fields
|
||||
|
||||
**Critical**: Never change `enlace` or `controlador` of existing pages unless explicitly asked.
|
||||
|
||||
See [docs/pages-and-records.md](docs/pages-and-records.md) for full details.
|
||||
|
||||
### General Sections
|
||||
Database-backed templates (headers, footers, record views) that use the `thisrecord` variable to access record fields. They use the same Twig + Acai attribute engine as modules.
|
||||
|
||||
- Upload fields return arrays: `thisrecord.image[0].urlPath`
|
||||
- Foreign keys use `_num` suffix: `category_num`
|
||||
|
||||
See [docs/modular-system.md](docs/modular-system.md) for details.
|
||||
|
||||
### Hooks (`hooks/`)
|
||||
PHP files that execute server-side logic. Triggered by:
|
||||
- Twig filter: `'hooks/module_id/' | hook({param: value})`
|
||||
- HTML tag: `<hook result="var" endpoint="/hooks/module_id/" :param="value"></hook>`
|
||||
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
|
||||
- Form action: via `c-form` attribute
|
||||
|
||||
There are two valid hook locations:
|
||||
- Global hooks in `hooks/hooks.<hook-id>.php` for reusable/shared server-side logic
|
||||
- Module-specific hooks in `template/estandar/modulos/<module-id>/hook.php` for logic owned by a single module
|
||||
|
||||
How to reference them:
|
||||
- Global hook `hooks/hooks.calcular_precio.php` -> endpoint `/hooks/calcular_precio/`
|
||||
- Module hook `template/estandar/modulos/hero_banner/hook.php` -> endpoint `/hooks/hero_banner/`
|
||||
- Module hook `template/estandar/modulos/buscadorapartados_hjd8s/hook.php` -> endpoint `/hooks/buscadorapartados_hjd8s/`
|
||||
|
||||
Rule of thumb:
|
||||
- If the logic is only used by one module, prefer that module's `hook.php`
|
||||
- If the logic will be reused by several modules/pages, create a global hook in `hooks/`
|
||||
- Return arrays from hooks; do not use `echo json_encode(...)` or `exit`
|
||||
|
||||
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
|
||||
|
||||
## Database Access
|
||||
|
||||
When the site is running in Docker, you can connect to the database:
|
||||
|
||||
- **Host:** `127.0.0.1`
|
||||
- **Port:** Check `.docker/docker-compose.yml` for the mapped port (usually 3307+)
|
||||
- **Credentials:** Read from `.docker/.env`:
|
||||
- `DB_USERNAME`
|
||||
- `DB_PASSWORD`
|
||||
- `DB_DATABASE`
|
||||
|
||||
```bash
|
||||
docker exec -it dw-<project-name>-db mysql -u root -p<password> <database>
|
||||
```
|
||||
|
||||
**Important:** Table names in CmsApi/Twig do NOT use the `cms_` prefix. The primary key is always `num`, never `id`.
|
||||
|
||||
## Acai Core (web-base)
|
||||
|
||||
The project workspace contains only the **customization layer** (modules, hooks, schemas, uploads). The CMS core (routing, rendering engine, admin panel, APIs) lives in a separate directory called **web-base** that is mounted as a Docker volume.
|
||||
|
||||
The web-base path can be obtained via: `GET http://localhost:9090/api/web-base-path`
|
||||
|
||||
Do NOT modify web-base files — they are shared across all projects.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Before working with any area (hooks, modules, templates, CSS/JS, etc.), read the corresponding documentation in `docs/` first.** Do not guess or assume — always consult the docs before taking action.
|
||||
2. **NEVER use `mkdir` to create directories.** Instead, use `acai-write` to create the first file inside the directory — this creates parent directories automatically. For example, to create a new module, directly write the `index-base.tpl` file.
|
||||
3. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
|
||||
4. Editing or creating any `index-base.tpl` through `acai-write` or `acai-line-replace` triggers automatic compilation. `compile_module` is only for manual recovery when you need to force a recompile without changing the file.
|
||||
5. `script.js` and `style.css` are static files — do NOT use Twig syntax inside them. Pass dynamic values from `index-base.tpl` via `data-*` attributes.
|
||||
6. Use Twig **filters** (with `|`), never Twig functions
|
||||
7. Table names without `cms_` prefix everywhere
|
||||
8. Primary key is `num`, never `id`
|
||||
9. Upload fields are arrays — access with `[0].urlPath`
|
||||
10. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
|
||||
11. Twig concatenation uses `~` operator: `'value=' ~ variable`
|
||||
12. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked
|
||||
13. **NEVER modify the `controlador` field** of existing records — it defines whether a page is Builder or Standard
|
||||
|
||||
## MCP Tools
|
||||
|
||||
This project has MCP tools for managing modules, records, media, and more. **Before starting any task, consult the tools reference for the correct workflow.**
|
||||
|
||||
See [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) for the complete list of available tools and step-by-step workflows.
|
||||
|
||||
Key workflows:
|
||||
- **Create module**: Read [docs/module-creation-guide.md](docs/module-creation-guide.md) first → create files with `acai-write` / refine with `acai-line-replace` → automatic compile on `index-base.tpl` → `add_module_to_record` (returns sectionId) → `set_module_config_vars` (returns uploadFields) → images via uploadFields. Use `compile_module` only if you need a manual recompile without editing the file.
|
||||
- **Edit module**: `acai-view` → `acai-line-replace` (or `acai-write` for full rewrites) → automatic compile on `index-base.tpl`
|
||||
- **Add images**: use `uploadFields` from `set_module_config_vars` response → `upload_record_image`
|
||||
- **Generate images**: `generate_image` → `upload_record_image` with returned URL
|
||||
|
||||
## Documentation
|
||||
|
||||
- [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, global variables
|
||||
- [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, Acai attributes, c-form, components
|
||||
- [docs/twig-filters.md](docs/twig-filters.md) — Twig filters reference (get, hook, module, queryDB, etc.)
|
||||
- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, record creation
|
||||
- [docs/css-js-conventions.md](docs/css-js-conventions.md) — CSS/JS/Vue 3, Tailwind, BEM, native components
|
||||
- [docs/quick-reference.md](docs/quick-reference.md) — Cheat sheet: domain rules, field types, filters
|
||||
- [docs/production-patterns.md](docs/production-patterns.md) — Real production patterns (header, zigzag, FAQ, forms)
|
||||
- [docs/vue-builder-rules.md](docs/vue-builder-rules.md) — CMS-VUE rules (tabs, colorpicker, components)
|
||||
- [docs/vue-builder-examples.md](docs/vue-builder-examples.md) — Vue builder examples (Banner Slideshow, etc.)
|
||||
- [docs/pages-and-records.md](docs/pages-and-records.md) — Page types (Builder vs Standard), sections, visibility, critical rules
|
||||
- [docs/module-creation-guide.md](docs/module-creation-guide.md) — Module creation workflow, style reference, field types
|
||||
- [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) — MCP tools reference, available tools, workflows
|
||||
@@ -1,159 +0,0 @@
|
||||
# MCP Tools Reference
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Tool | Categoría | Acción |
|
||||
|------|-----------|--------|
|
||||
| `compile_module` | Módulos | Recompilación manual de rescate cuando necesitas forzar la compilación sin editar el archivo |
|
||||
| `check_module` | Módulos | Preview de cómo renderiza un módulo |
|
||||
| `check_module_usage` | Módulos | Qué páginas usan un módulo |
|
||||
| `acai-view` | Archivos | Lee un archivo del proyecto por tramos |
|
||||
| `acai-glob` | Archivos | Busca rutas de archivos por patrón |
|
||||
| `acai-grep` | Archivos | Busca texto en archivos del proyecto |
|
||||
| `acai-line-replace` | Archivos | Reemplaza un bloque concreto en un archivo existente |
|
||||
| `acai-write` | Archivos | Crea o reescribe un archivo completo. Antes de usarlo, lee la doc correspondiente según el tipo de archivo (`module-creation-guide`, `builder-fields`, `css-js-conventions`, `hooks-and-api`) |
|
||||
| `acai-delete` | Archivos | Borra un archivo del proyecto |
|
||||
| `list_page_modules` | Registros | Lista módulos de una página |
|
||||
| `add_module_to_record` | Registros | Añade módulo a una página |
|
||||
| `remove_module_from_record` | Registros | Elimina módulo de una página |
|
||||
| `reorder_module` | Registros | Cambia posición de un módulo |
|
||||
| `toggle_module_visibility` | Registros | Muestra/oculta módulo |
|
||||
| `get_module_config_vars` | Registros | Lee variables de un módulo |
|
||||
| `set_module_config_vars` | Registros | Escribe variables de un módulo |
|
||||
| `list_table_records` | Registros | Buscar/listar registros con filtros |
|
||||
| `get_record` | Registros | Obtener un registro por num |
|
||||
| `create_or_update_record` | Registros | Crear o actualizar registros |
|
||||
| `delete_table_records` | Registros | Eliminar registros (destructivo) |
|
||||
| `upload_record_image` | Media | Subir imagen a campo de registro (desde URL) |
|
||||
| `generate_image` | Media | Generar imagen con IA y guardar en uploads |
|
||||
| `upload_image_to_assets` | Media | Subir imagen a /images/ del template |
|
||||
| `list_record_uploads` | Media | Listar uploads de un campo |
|
||||
| `replace_record_image` | Media | Reemplazar imagen existente |
|
||||
| `delete_record_upload` | Media | Borrar upload |
|
||||
| `reorder_record_uploads` | Media | Reordenar imágenes de un campo |
|
||||
| `refresh_acai_token` | Auth | Renovar token JWT expirado |
|
||||
| `navigate_browser` | Navegación | Navegar el browser del frontend a una URL |
|
||||
| `save_project_styles` | Proyecto | Guardar resumen de estilos en docs/project-styles.md |
|
||||
| `rollback_git` | Git | Recuperar cambios de git remoto |
|
||||
| `get_layout_field` | Layout | Lee el source de los campos globales del layout.json: style, javascript, header, footer |
|
||||
| `set_layout_field` | Layout | Reemplaza un campo global del layout.json. **USA ESTA TOOL** para editar header/footer — NO toques los .tpl directos |
|
||||
|
||||
## Flujos de trabajo
|
||||
|
||||
### Crear un módulo nuevo desde cero
|
||||
|
||||
1. `acai-write` — Crea `index-base.tpl`, `style.css`, `script.js` y cualquier hook necesario con rutas relativas al proyecto
|
||||
2. `acai-write` o `acai-line-replace` compilan automáticamente al tocar `index-base.tpl`
|
||||
3. `add_module_to_record` — Añade el módulo a una página (tabla padre, ej: `apartados`)
|
||||
4. `set_module_config_vars` — Rellena las variables con contenido (textos, colores, opciones). **OBLIGATORIO** — sin esto el módulo no muestra nada. Devuelve:
|
||||
- `configVars`: mapa de variables → recordNums
|
||||
- `uploadFields`: mapa de variables upload → `{ fieldName, recordNum }` — **usa estos directamente** para subir imágenes sin necesidad de leer builder.json
|
||||
- Para vars multi con uploads: `uploadFields["varName.subVarName"]` es un array con `[{ index, fieldName, recordNum }]`
|
||||
5. Para imágenes: `generate_image` o `upload_record_image` usando el `recordNum` y `fieldName` del `uploadFields` devuelto en el paso 4
|
||||
6. Verificar con `check_module` o recargando la página
|
||||
|
||||
### Editar un módulo existente
|
||||
|
||||
1. `get_module_config_vars` — Leer el estado actual del módulo (variables, recordNums)
|
||||
2. `acai-view` — Leer solo el tramo de `index-base.tpl` que se va a modificar
|
||||
3. `acai-line-replace` — Editar el bloque concreto. Usa `acai-write` solo si el archivo es nuevo o el cambio es masivo
|
||||
4. La compilación es automática tras cada edición de `index-base.tpl`
|
||||
5. Si cambias variables: `set_module_config_vars` para actualizar valores
|
||||
|
||||
### Editar archivos del proyecto con bajo consumo de tokens
|
||||
|
||||
1. `acai-view` — Leer el archivo o un rango de líneas
|
||||
2. `acai-glob` — Encontrar archivos relevantes por ruta cuando no conoces el path exacto
|
||||
3. `acai-grep` — Buscar texto o atributos concretos dentro de archivos del proyecto
|
||||
4. `acai-line-replace` — Reemplazar el bloque exacto en archivos existentes
|
||||
5. `acai-write` — Crear archivos nuevos o reescribirlos por completo si es necesario
|
||||
6. `acai-delete` — Borrar archivos solo cuando sea explícitamente necesario
|
||||
|
||||
Reglas:
|
||||
- Usa siempre rutas relativas al proyecto
|
||||
- No edites `index.tpl`, `index-twig.tpl` ni `builder.json` — son auto-generados
|
||||
- Tras editar cualquier `index-base.tpl` con las file tools, la compilación se ejecuta automáticamente
|
||||
|
||||
### Añadir/modificar imágenes de un módulo
|
||||
|
||||
**Tras `set_module_config_vars`** (método recomendado — sin pasos extra):
|
||||
1. El response de `set_module_config_vars` incluye `uploadFields` con los `recordNum` y `fieldName` de cada variable upload
|
||||
2. `upload_record_image` con `tableName: "builder_custom"`, `recordId` y `fieldName` del `uploadFields`
|
||||
3. Para uploads dentro de vars multi: `uploadFields["records.imagen"]` devuelve array con `{ index, fieldName, recordNum }` por cada record
|
||||
|
||||
**Sin haber llamado a `set_module_config_vars`**:
|
||||
1. `get_module_config_vars` — Obtener el `recordNum` de builder_custom
|
||||
2. Leer `builder.json` del módulo para encontrar el `fieldName` real (ej: `image1`, NO el nombre de la variable)
|
||||
3. `upload_record_image` con:
|
||||
- `tableName`: `"builder_custom"` (siempre sin cms_)
|
||||
- `recordId`: el recordNum del paso 1
|
||||
- `fieldName`: el campo de relations del builder.json (ej: `image1`)
|
||||
- `imageUrl`: usa la URL recomendada por la tool que generó/subió la imagen. En Forge, si `generate_image` devuelve `uploadUrl` o `fullUrl`, priorízala frente a `dockerUrl`
|
||||
|
||||
### Generar imagen con IA
|
||||
|
||||
1. `generate_image` con prompt descriptivo + style (photographic, digital-art, minimalist...)
|
||||
2. La imagen se guarda en `cms/uploads/generated/` y devuelve una URL local de preview (`dockerUrl`) y, cuando aplica, una URL recomendada para subida (`uploadUrl` / `fullUrl`)
|
||||
3. Para `upload_record_image`, usa la URL recomendada por la tool. En Forge, prioriza `uploadUrl` o `fullUrl` si están presentes
|
||||
|
||||
### Gestionar registros de una tabla
|
||||
|
||||
1. `list_table_records` — Buscar registros con filtros (`where`, `order`, `limit`)
|
||||
2. `get_record` — Obtener un registro completo por num
|
||||
3. `create_or_update_record` — Crear o actualizar (la tabla sin prefijo `cms_`, PK es `num`)
|
||||
4. `delete_table_records` — Eliminar por IDs
|
||||
|
||||
### Explorar el sitio
|
||||
|
||||
1. `list_page_modules` — Ver qué módulos tiene cada página
|
||||
2. `get_module_config_vars` — Ver los datos de cada módulo
|
||||
3. `check_module` — Preview de cómo renderiza
|
||||
|
||||
## Reglas importantes para todas las tools
|
||||
|
||||
1. **tableName** siempre SIN prefijo `cms_` (ej: `apartados`, no `cms_apartados`)
|
||||
2. **Primary key** es siempre `num`, nunca `id`
|
||||
3. **Uploads** son arrays — acceder con `[0].urlPath`
|
||||
4. **fieldName de imágenes** viene de `builder.json` → `vars.NOMBRE.relations.builder_custom` (ej: `image1`), NO del nombre de la variable
|
||||
5. **recordId para imágenes** es el `num` de `builder_custom`, NO el sectionId del módulo
|
||||
6. Tras `set_module_config_vars`, TODAS las variables del módulo (incluyendo upload) reciben config-vars automáticamente
|
||||
7. Si el token expira (error 403), usar `refresh_acai_token`
|
||||
|
||||
## Layout global (header, footer, style, javascript)
|
||||
|
||||
Los 4 campos globales del proyecto (`style.css`, `script.js`, `header`, `footer`) viven en `cms/lib/plugins/builder_saas/layout.json`.
|
||||
|
||||
### REGLA CRÍTICA
|
||||
|
||||
**NUNCA uses `acai-view`, `acai-line-replace`, `acai-write` ni `acai-delete` sobre**:
|
||||
- `cms/lib/plugins/builder_saas/layout.json`
|
||||
- `template/estandar/modulos/custom-header-twig/*`
|
||||
- `template/estandar/modulos/custom-footer-twig/*`
|
||||
- `template/estandar/modulos/custom-header/*`
|
||||
- `template/estandar/modulos/custom-footer/*`
|
||||
|
||||
Esos ficheros son **artefactos generados** a partir del `layout.json`. Editarlos directamente provoca:
|
||||
- Desincronización con `layout.json.{header,footer}ModuleCustom.htmlParsed`.
|
||||
- Sobrescritura de tus cambios cuando el usuario abre el builder visual y guarda.
|
||||
- Comportamiento inconsistente entre el render público y el builder.
|
||||
|
||||
### Workflow correcto
|
||||
|
||||
Para leer:
|
||||
```
|
||||
get_layout_field({ field: "header" }) // devuelve el source Twig del header
|
||||
get_layout_field({ field: "footer" })
|
||||
get_layout_field({ field: "style" }) // CSS global
|
||||
get_layout_field({ field: "javascript" }) // JS global
|
||||
```
|
||||
|
||||
Para editar:
|
||||
```
|
||||
set_layout_field({ field: "footer", content: "<footer>...nuevo HTML/Twig...</footer>" })
|
||||
```
|
||||
|
||||
El backend:
|
||||
1. Escribe el source en `layout.json.{field}`.
|
||||
2. Sincroniza `layout.json.{field}ModuleCustom.htmlParsed`.
|
||||
3. Regenera los `.tpl` del módulo `custom-{field}-twig/`.
|
||||
4. Compila el Twig a PHP.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Acai Modular System
|
||||
|
||||
## Modules
|
||||
|
||||
Modules are the visual building blocks of Acai websites. Each module lives in `template/estandar/modulos/<module-id>/`.
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
<module-id>/
|
||||
├── index-base.tpl # Source template (EDIT THIS)
|
||||
├── index.tpl # Compiled output (auto-generated, do NOT edit)
|
||||
├── index-twig.tpl # Compiled Twig output (auto-generated, do NOT edit)
|
||||
├── builder.json # Compiled builder vars (auto-generated, do NOT edit)
|
||||
├── style.css # Module-scoped styles
|
||||
└── script.js # Module JavaScript
|
||||
```
|
||||
|
||||
### Template Syntax
|
||||
|
||||
Templates use a hybrid of **Twig** and **Acai attributes**. The source file is always `index-base.tpl`.
|
||||
|
||||
```html
|
||||
<section class="hero-section" id="{{ section_id }}">
|
||||
<div class="container mx-auto px-4">
|
||||
<h2 data-field-type="headfield" class="text-3xl font-bold">
|
||||
Title here
|
||||
</h2>
|
||||
<p data-field-type="textbox" class="text-lg text-gray-600">
|
||||
Description text
|
||||
</p>
|
||||
<img data-field-type="upload" src="placeholder.jpg" class="w-full rounded-lg" />
|
||||
<a data-field-type="link" href="#" class="btn">Call to action</a>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Including Modules from Other Modules
|
||||
|
||||
```html
|
||||
<module_id :param1="value1" :param2="'string value'"></module_id>
|
||||
```
|
||||
|
||||
Parameters are received as variables inside the included module.
|
||||
|
||||
### Global Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `section_id` | Unique ID per module instance (use for anchors, JS scoping) |
|
||||
| `interno` | `true` when viewing in CMS editor, `false` on public site |
|
||||
| `server.HTTP_HOST` | Current domain |
|
||||
| `loop.index` | 1-based iteration index (inside `c-for`) |
|
||||
| `loop.index is odd` / `loop.index is even` | For alternating layouts |
|
||||
|
||||
|
||||
## General Sections
|
||||
|
||||
General sections are database-backed templates used for record views, headers, footers, and reusable layouts. They use the same template engine as modules.
|
||||
|
||||
### Key Differences from Modules
|
||||
|
||||
- Access record data via the `thisrecord` variable
|
||||
- Upload fields return **arrays**: `thisrecord.image[0].urlPath`
|
||||
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
|
||||
- Foreign key fields use `_num` suffix: `thisrecord.category_num`
|
||||
- Saved via `save_general_section()` (not `save_module()`)
|
||||
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
|
||||
|
||||
### Example: Record Template
|
||||
|
||||
```html
|
||||
<article class="product-card">
|
||||
<img src="{{ thisrecord.imagen[0].urlPath }}"
|
||||
alt="{{ thisrecord.imagen[0].info1 }}"
|
||||
class="w-full h-64 object-cover" />
|
||||
<h3 class="text-xl font-semibold">{{ thisrecord.nombre }}</h3>
|
||||
<p class="text-gray-600">{{ thisrecord.descripcion | raw }}</p>
|
||||
<span class="text-2xl font-bold">{{ thisrecord.precio }}€</span>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Variable Assignment
|
||||
|
||||
Use `<set>` tag to create variables from queries:
|
||||
|
||||
```html
|
||||
<set :categories="'categorias' | get()"></set>
|
||||
<set :featured="'productos' | get({destacado: 1}, 'orden ASC', 3)"></set>
|
||||
```
|
||||
|
||||
|
||||
## Repeatable Content (multiv2)
|
||||
|
||||
The `multiv2` builder field type creates repeatable groups of fields:
|
||||
|
||||
```html
|
||||
<div c-for="item in record.items">
|
||||
<h3 data-field-type="textfield">{{ item.title }}</h3>
|
||||
<p data-field-type="textbox">{{ item.description }}</p>
|
||||
<img data-field-type="upload" src="{{ item.image }}" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Access individual items: `record.items[0].title`, `record.items[1].image`, etc.
|
||||
@@ -1,100 +0,0 @@
|
||||
# Module Creation Guide
|
||||
|
||||
## Style Reference
|
||||
|
||||
When creating new modules, you MUST match the visual style of the existing project. Follow these steps IN ORDER:
|
||||
|
||||
### Step 1: Check for `docs/project-styles.md`
|
||||
- If the file exists → read it and use it as your style reference. DONE — skip to module creation.
|
||||
- If the file does NOT exist → continue to Step 2.
|
||||
|
||||
### Step 2: Determine if exploration is needed
|
||||
- Count modules in `template/estandar/modulos/` that have `builder.json` and do NOT start with `custom-`
|
||||
- If 3+ qualifying modules exist → continue to Step 3
|
||||
- If fewer than 3 → skip exploration, create the module based on the user's description. The style will be defined as modules are created.
|
||||
|
||||
### Step 3: Explore and GENERATE the style guide (MANDATORY)
|
||||
- Read `index-base.tpl` and `style.css` of 3-4 representative modules (only those with `builder.json`, skip `custom-*`)
|
||||
- **You MUST then call `save_project_styles`** with a markdown summary including:
|
||||
- Primary/secondary/accent colors (hex values)
|
||||
- Font families and sizes used
|
||||
- Spacing scale (padding/margin patterns)
|
||||
- Common Tailwind classes and custom CSS patterns
|
||||
- Button styles, card styles, section layouts
|
||||
- Any recurring design patterns (gradients, shadows, borders, etc.)
|
||||
- This saves `docs/project-styles.md` which will be read by future module creation tasks — no re-exploration needed.
|
||||
|
||||
**After creating a module:** if `docs/project-styles.md` does not exist yet and there are now 3+ modules, call `save_project_styles`.
|
||||
|
||||
## Module Structure
|
||||
|
||||
Each module lives in `template/estandar/modulos/<moduleId>/` with:
|
||||
- `index-base.tpl` — Twig template (source — EDIT THIS)
|
||||
- `style.css` — Module styles
|
||||
- `script.js` — Module JavaScript
|
||||
- `builder.json` — Compiled builder vars (auto-generated, do NOT edit)
|
||||
- `index.tpl` / `index-twig.tpl` — Compiled (auto-generated, do NOT edit)
|
||||
|
||||
## Creating a Module — Full Workflow
|
||||
|
||||
If the module needs JavaScript, you MUST read `docs/css-js-conventions.md` before writing `index-base.tpl` or `script.js`.
|
||||
If the module needs server-side logic, dynamic data processing, form handling, or reusable backend behavior, you MUST read `docs/hooks-and-api.md` before creating `hook.php` or any global hook.
|
||||
If the module will call hooks from Twig, also review `docs/twig-filters.md` for the `hook` filter syntax.
|
||||
If `index-base.tpl` contains builder fields (`data-field-type`), you MUST review `docs/builder-fields.md` before writing the template.
|
||||
|
||||
Hard rules for module files:
|
||||
- `index-base.tpl` is for HTML/Twig only
|
||||
- `script.js` is for module JavaScript
|
||||
- `style.css` is for module CSS
|
||||
- `hook.php` is for server-side logic
|
||||
- Do NOT embed `<script>` tags in `index-base.tpl`
|
||||
- Do NOT put PHP logic in `index-base.tpl`
|
||||
- `script.js` and `style.css` are static files: do NOT use Twig syntax inside them
|
||||
- Do NOT use `{{ ... }}`, `{% ... %}`, `c-if`, `c-for`, or builder attributes inside `script.js` or `style.css`
|
||||
- If JavaScript needs dynamic values such as `section_id` or a hook endpoint, expose them from `index-base.tpl` via `data-*` attributes
|
||||
- Every editable builder field with `data-field-type` MUST also define `data-field-label`
|
||||
- Avoid Tailwind arbitrary-value syntax such as `text-[...]`, `font-[...]`, `leading-[...]`, `bg-[...]` inside `index-base.tpl`; move those styles to `style.css`
|
||||
|
||||
1. **Read style reference** (steps above)
|
||||
2. **`acai-write`** — Create the module files directly (`index-base.tpl`, `style.css`, `script.js`, optional `hook.php`) using project-relative paths and complete file contents.
|
||||
- If the server-side logic belongs only to that module, create `template/estandar/modulos/<module-id>/hook.php`
|
||||
- If the logic should be reused across modules/pages, create a global hook in `hooks/hooks.<hook-id>.php`
|
||||
- Inside the module, reference its own hook with `/hooks/<module-id>/`
|
||||
- Example: module folder `template/estandar/modulos/buscadorapartados_hjd8s/` -> hook endpoint `/hooks/buscadorapartados_hjd8s/`
|
||||
3. **Automatic compile** — Writing `index-base.tpl` automatically creates the generated template placeholders and triggers compilation. `compile_module` is only a manual recovery tool if you need to force a recompile without changing the file.
|
||||
4. **`add_module_to_record`** — Adds the module to a page. Response includes `sectionId` — use it directly in the next step.
|
||||
5. **`set_module_config_vars`** — Fill variables with content. Response includes `uploadFields` with `{ fieldName, recordNum }` for each upload variable.
|
||||
6. **Upload images** — Use `generate_image` then `upload_record_image` with the `recordNum` and `fieldName` from step 5's `uploadFields`. No need to read builder.json or call get_module_config_vars.
|
||||
7. **`navigate_browser`** — Navigate to the page so the user can see the result.
|
||||
|
||||
## HTML Field Types
|
||||
|
||||
Use these `data-field-type` attributes in `index-base.tpl`:
|
||||
|
||||
| Attribute | Purpose | Example |
|
||||
|-----------|---------|---------|
|
||||
| `headfield` | Editable heading | `<h2 data-field-type="headfield">Title</h2>` |
|
||||
| `textfield` | Short editable text | `<span data-field-type="textfield">Text</span>` |
|
||||
| `wysiwyg` | Rich text editor | `<div data-field-type="wysiwyg">Content</div>` |
|
||||
| `upload` | Image upload | `<img data-field-type="upload" src="...">` |
|
||||
| `list` | Select dropdown | `<div data-field-type="list" data-options="opt1,opt2">` |
|
||||
| `multiv2` | Repeater/records | `<div data-field-type="multiv2">...</div>` |
|
||||
| `checkbox` | Toggle | `<div data-field-type="checkbox">` |
|
||||
| `colorpicker` | Color picker | `<div data-field-type="colorpicker">` |
|
||||
|
||||
## MJML Modules
|
||||
|
||||
Modules with `MJMLModule: true` in their schema are email modules:
|
||||
- Only appear when the page table is `mail_marketing`
|
||||
- For `mail_marketing` tables, only MJML modules are shown
|
||||
- Use MJML markup instead of standard HTML
|
||||
|
||||
## Key Rules
|
||||
|
||||
- Always use Tailwind CSS as primary styling
|
||||
- Use `section_id` variable for unique anchors/scoping
|
||||
- Use `interno` variable to detect CMS editor vs public view
|
||||
- Include other modules with: `<module_id :param1="value1"></module_id>`
|
||||
- Editing `index-base.tpl` with `acai-write` or `acai-line-replace` compiles automatically
|
||||
- Twig uses filters (with `|`), never functions
|
||||
- Twig concatenation uses `~`: `'value=' ~ variable`
|
||||
@@ -1,104 +0,0 @@
|
||||
# Pages & Records Guide
|
||||
|
||||
## Page Types
|
||||
|
||||
Every CMS record that has an `enlace` (URL) field is a **page**. Pages come in two types determined by the `controlador` field:
|
||||
|
||||
### Builder (Modular) Pages
|
||||
- `controlador` = `cms/lib/plugins/builder_saas/controlador.php`
|
||||
- Content is built from **modules** (drag & drop components)
|
||||
- The `builder` field contains a JSON array of module instances
|
||||
- Use MCP tools: `add_module_to_record`, `set_module_config_vars`, etc.
|
||||
- The page template renders modules in order from the builder JSON
|
||||
|
||||
### Standard Pages
|
||||
- `controlador` = `cms/lib/plugins/builder_saas/controlador_tabla.php`
|
||||
- Content lives directly in the record fields (`content`, `titulo_alternativo`, etc.)
|
||||
- The `content` field is HTML (wysiwyg)
|
||||
- Use `create_or_update_record` to edit content directly
|
||||
- No modules involved
|
||||
|
||||
### How to determine page type
|
||||
**Always check the `controlador` field** of the record:
|
||||
- Contains `controlador.php` (without `_tabla`) → **Builder**
|
||||
- Contains `controlador_tabla.php` → **Standard**
|
||||
|
||||
## Table Types (Sections)
|
||||
|
||||
Tables with pages are called **sections**. There are two section types defined by `menuType` in the schema:
|
||||
|
||||
### Category (`menuType = "category"`)
|
||||
- **Hierarchical** — pages have parent/child relationships
|
||||
- Fields: `parentNum`, `depth`, `globalOrder`, `lineage`, `siblingOrder`
|
||||
- Example: `apartados` (main site pages)
|
||||
- Uses `visible_en_el_menu` field for menu visibility
|
||||
- Ordered by `globalOrder`
|
||||
|
||||
### Multi (`menuType = "multi"`)
|
||||
- **Flat list** — no hierarchy
|
||||
- Uses `dragSortOrder` for ordering
|
||||
- Example: `blog`, `travesias`
|
||||
- Typically uses `visible` field (not `visible_en_el_menu`)
|
||||
|
||||
## Critical Rules for Pages
|
||||
|
||||
### NEVER change the `enlace` field
|
||||
Unless the user explicitly asks to change a page URL, **never modify the `enlace` field**. Changing it breaks existing links, SEO, and navigation. The enlace is set when the page is created and should remain stable.
|
||||
|
||||
### NEVER change the `controlador` field
|
||||
The controlador defines whether the page is Builder or Standard. Changing it breaks the page rendering. Only set it during page creation.
|
||||
|
||||
### Visibility fields
|
||||
- `apartados` and other category tables use: `visible_en_el_menu` (1 = visible, 0 = hidden)
|
||||
- `blog`, `travesias` and other multi tables use: `visible` (1 = visible, 0 = hidden)
|
||||
- Always check which field the table has before toggling visibility
|
||||
|
||||
### Name/Title fields
|
||||
- Some tables use `name` (e.g. `apartados`)
|
||||
- Others use `title` (e.g. `blog`, `travesias`)
|
||||
- Check the schema to know which one to use
|
||||
|
||||
## Working with Builder Pages
|
||||
|
||||
### Adding content to a new Builder page
|
||||
1. List available modules: `list_available_modules`
|
||||
2. Add modules: `add_module_to_record` (one at a time, in order)
|
||||
3. Configure each module: `set_module_config_vars` with content
|
||||
4. Add images if needed: `upload_record_image` or `generate_image`
|
||||
|
||||
### Editing an existing Builder page
|
||||
1. List current modules: `list_page_modules`
|
||||
2. Get module vars: `get_module_config_vars`
|
||||
3. Update vars: `set_module_config_vars`
|
||||
4. Or edit the module template: edit `index-base.tpl` with `acai-line-replace` or `acai-write` → compilation runs automatically
|
||||
|
||||
## Working with Standard Pages
|
||||
|
||||
### Adding content to a Standard page
|
||||
Use `create_or_update_record` to set:
|
||||
- `content` — HTML content (main body)
|
||||
- `titulo_alternativo` — alternative title shown on the page
|
||||
- `titulo_de_pagina` — browser tab title (SEO)
|
||||
- `metatag_descripcion` — meta description (SEO)
|
||||
|
||||
### Example: Update a standard page
|
||||
```
|
||||
create_or_update_record with:
|
||||
tableName: "apartados"
|
||||
recordNum: "87"
|
||||
fields:
|
||||
content: "<h2>Our Services</h2><p>We offer...</p>"
|
||||
titulo_de_pagina: "Services | My Site"
|
||||
metatag_descripcion: "Discover our services..."
|
||||
```
|
||||
|
||||
## The `apartados` Table (Special)
|
||||
|
||||
The `apartados` table is the main pages table in most Acai sites. Key characteristics:
|
||||
- `menuType = "category"` — hierarchical with parent/child
|
||||
- `parentNum` — the num of the parent page (0 = root level)
|
||||
- `depth` — nesting level (0 = root, 1 = child, 2 = grandchild)
|
||||
- `globalOrder` — display order across the entire tree
|
||||
- `visible_en_el_menu` — whether the page shows in the navigation menu
|
||||
- `breadcrumb` — auto-generated breadcrumb path
|
||||
- Pages can be either Builder or Standard (check `controlador` field per record)
|
||||
@@ -1,85 +0,0 @@
|
||||
# Quick Reference
|
||||
|
||||
## Reglas Críticas
|
||||
|
||||
| Regla | Correcto | Incorrecto |
|
||||
|-------|----------|------------|
|
||||
| Nombres de tabla | `'productos'` | `'cms_productos'` |
|
||||
| Primary key | `record.num` | `record.id` |
|
||||
| Foreign keys | `categoria_num` | `categoria_id` |
|
||||
| Upload fields | `record.imagen[0].urlPath` | `record.imagen` |
|
||||
| Optimizar imagen | `record.imagen[0].urlPath \| imagec(800)` | `record.imagen.url` |
|
||||
| Filtros Twig | `{{ 'table' \| get() }}` | `{{ get('table') }}` |
|
||||
| Campo enlace | `{{ producto.enlace }}` (ya tiene barras) | `"/{{ producto.enlace }}/"` |
|
||||
| Nombres builder vars | `data-field-label` → sin espacios/especiales, minúsculas | Mantener casing original |
|
||||
| Checkbox | `1` o `0` (número) | `true`/`false` |
|
||||
| Formato fecha | `YYYY-MM-DD HH:mm:ss` | Cualquier otro formato |
|
||||
| c-if igualdad | `c-if="x = 'valor'"` (un `=`) | `c-if="x == 'valor'"` |
|
||||
| Twig if igualdad | `{% if x == 'valor' %}` (doble `==`) | `{% if x = 'valor' %}` |
|
||||
| queryDB tablas | `SELECT * FROM cms_tabla` (con prefijo) | `SELECT * FROM tabla` |
|
||||
| get tablas | `'tabla' \| get()` (sin prefijo) | `'cms_tabla' \| get()` |
|
||||
|
||||
## Builder Variable Types
|
||||
|
||||
| Type | Elemento | Retorna |
|
||||
|------|----------|---------|
|
||||
| `textfield` | `<p>` | String |
|
||||
| `headfield` | `<h1>`-`<h6>` | String + var `_tag` |
|
||||
| `textbox` | `<div>` | String multilínea |
|
||||
| `wysiwyg` | `<div class="wysiwyg">` | HTML string |
|
||||
| `link` | `<a>` | URL string |
|
||||
| `upload` | `<img>` | Array de `{urlPath, info1}` |
|
||||
| `uploadMulti` | `<li>` | Itera archivos subidos |
|
||||
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
||||
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
||||
| `multiv2` | `<li>` wrapper | Array de objetos |
|
||||
|
||||
## Acai HTML Attributes
|
||||
|
||||
| Atributo | Uso | Ejemplo |
|
||||
|----------|-----|---------|
|
||||
| `c-if` | Condicional | `<p c-if="activo = 1">` |
|
||||
| `c-else` | Rama else | `<p c-else>` |
|
||||
| `c-for` | Loop array | `<li c-for="item in items">` |
|
||||
| `c-for` | Loop tabla | `<li c-for="p in productos" c-where="'activo=1'" c-limit="10">` |
|
||||
| `c-hidden` | Variable oculta | `<p c-hidden="true" data-field-type="textfield">` |
|
||||
| `c-class` | Clase condicional | `<div c-class="{ 'bg-red': color == '1' }">` |
|
||||
| `c-form` | Formulario | `<c-form tableName="'contacto'" captcha="true">` |
|
||||
|
||||
## Twig Filters
|
||||
|
||||
| Filtro | Uso |
|
||||
|--------|-----|
|
||||
| `get` | `'table' \| get(where, order, limit)` |
|
||||
| `hook` | `'hooks/module_id/' \| hook({params})` |
|
||||
| `module` | `'module_id' \| module({params})` |
|
||||
| `queryDB` | `'SELECT ...' \| queryDB()` |
|
||||
| `imagec` | `path \| imagec(width)` |
|
||||
| `translate` | `'text' \| translate` — tabla `textos_generales` (editar/traducir), no crear JSONs i18n |
|
||||
| `json_decode` | `'json_string' \| json_decode` |
|
||||
| `raw` | `variable \| raw` |
|
||||
| `truncate` | `text \| truncate(100)` |
|
||||
|
||||
## Formato de datos para registros
|
||||
|
||||
| Tipo | Formato | Ejemplo |
|
||||
|------|---------|---------|
|
||||
| Text field | String | `"Texto"` |
|
||||
| Text box | String multilínea | `"Línea 1\nLínea 2"` |
|
||||
| Date/time | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` |
|
||||
| Wysiwyg | HTML string | `"<p class=\"font-bold\">Texto</p>"` |
|
||||
| List | String o número | `"activo"` o `"1"` |
|
||||
| Checkbox | Número 1/0 | `1` o `0` |
|
||||
| Multivalores | String JSON | `"[{\"producto\":\"1\"}]"` |
|
||||
| Upload | NO enviar — subir imagen después de crear registro |
|
||||
|
||||
## Variables globales
|
||||
|
||||
| Variable | Descripción |
|
||||
|----------|-------------|
|
||||
| `section_id` | ID único por instancia del módulo |
|
||||
| `server.HTTP_HOST` | Dominio actual |
|
||||
| `loop.index` | Índice de iteración (1-based) |
|
||||
| `loop.index is odd/even` | Para layouts alternados |
|
||||
| `interno` | True dentro del editor CMS |
|
||||
| `thisrecord` | Registro actual (en secciones generales) |
|
||||
@@ -1,242 +0,0 @@
|
||||
# Twig Filters Reference
|
||||
|
||||
Acai usa filtros Twig con sintaxis `|`. No usar funciones Twig — solo filtros.
|
||||
|
||||
## `get` — Consultar tabla de BD
|
||||
|
||||
```twig
|
||||
{{ 'table_name' | get(where, order, limit) }}
|
||||
```
|
||||
|
||||
- `table_name`: sin prefijo `cms_`
|
||||
- `where`: string SQL o objeto (opcional)
|
||||
- `order`: string de orden (opcional)
|
||||
- `limit`: int (opcional)
|
||||
|
||||
```twig
|
||||
{# Todos los registros #}
|
||||
{% set products = 'productos' | get() %}
|
||||
|
||||
{# Con WHERE string #}
|
||||
{% set active = 'productos' | get('activo=1') %}
|
||||
|
||||
{# Con WHERE objeto #}
|
||||
{% set active = 'productos' | get({activo: 1}) %}
|
||||
|
||||
{# Con WHERE + ORDER + LIMIT #}
|
||||
{% set latest = 'noticias' | get('publicado=1', 'fecha DESC', 6) %}
|
||||
|
||||
{# Completo #}
|
||||
{% set caros = 'productos' | get('precio > 100', 'precio DESC', 20) %}
|
||||
|
||||
{# Single record (primer resultado) #}
|
||||
{% set product = 'productos' | get({num: 42}) %}
|
||||
{{ product[0].nombre }}
|
||||
```
|
||||
|
||||
Iterar resultados:
|
||||
```twig
|
||||
{% for producto in 'productos' | get('activo=1', 'num DESC', 10) %}
|
||||
<h3>{{ producto.titulo }}</h3>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
## `queryDB` — SQL directo
|
||||
|
||||
Usa nombre de tabla completo WITH prefijo `cms_`.
|
||||
|
||||
```twig
|
||||
{% set results = 'SELECT * FROM cms_productos WHERE precio > 100 ORDER BY precio ASC' | queryDB() %}
|
||||
|
||||
{# JOIN complejo #}
|
||||
{% set top = 'SELECT p.*, COUNT(v.num) as ventas
|
||||
FROM cms_productos p
|
||||
LEFT JOIN cms_ventas v ON v.producto_num = p.num
|
||||
GROUP BY p.num
|
||||
ORDER BY ventas DESC
|
||||
LIMIT 5' | queryDB() %}
|
||||
```
|
||||
|
||||
Usar solo cuando `get` no sea suficiente.
|
||||
|
||||
## `hook` — Ejecutar PHP Hook
|
||||
|
||||
```twig
|
||||
{# Llamar y mostrar resultado #}
|
||||
{{ 'hooks/module_id/' | hook({param1: 'value', param2: variable}) }}
|
||||
|
||||
{# Capturar en variable #}
|
||||
{% set result = 'hooks/calcular_precio/' | hook({cantidad: 5, tipo: 'mayoreo'}) %}
|
||||
<p>Total: ${{ result.total }}</p>
|
||||
```
|
||||
|
||||
## `module` — Renderizar otro módulo
|
||||
|
||||
```twig
|
||||
{{ 'other_module_id' | module({param1: value1}) }}
|
||||
|
||||
{# Capturar en variable #}
|
||||
{% set carrito = 'carrito_compras' | module({usuario_id: 123}) %}
|
||||
```
|
||||
|
||||
## `imagec` — Optimizar/redimensionar imágenes
|
||||
|
||||
```twig
|
||||
{# Redimensionar a ancho #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(400) }}" />
|
||||
|
||||
{# En srcset #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(800) }}"
|
||||
srcset="{{ record.image[0].urlPath | imagec(400) }} 400w,
|
||||
{{ record.image[0].urlPath | imagec(800) }} 800w" />
|
||||
```
|
||||
|
||||
## `translate` — Texto editable y traducción
|
||||
|
||||
Cualquier string con `| translate` se resuelve contra la tabla
|
||||
`textos_generales` del proyecto. Esta tabla cumple **dos funciones a la vez**:
|
||||
|
||||
1. **Traducción**: cada fila guarda la versión del texto por cada idioma
|
||||
habilitado del proyecto.
|
||||
2. **Edición de contenidos**: es el canal oficial para que el usuario final
|
||||
(o el agente) **modifique esos textos sin tocar código**. El filtro
|
||||
`| translate` no es solo i18n — es el mecanismo por el que un texto
|
||||
"hardcodeado" en una plantilla se vuelve editable desde el CMS.
|
||||
|
||||
```twig
|
||||
{{ 'Bienvenido' | translate }}
|
||||
{{ variable | translate }}
|
||||
```
|
||||
|
||||
**Cómo funciona:**
|
||||
- Los strings envueltos en `| translate` en las plantillas o en el código de
|
||||
los módulos se buscan en `textos_generales`.
|
||||
- Si existe la fila, devuelve el valor guardado (en el idioma activo).
|
||||
- Si no existe, devuelve el texto original tal cual (fallback).
|
||||
- Las filas se editan desde el admin del CMS o via `cmsApi` (update sobre
|
||||
`textos_generales`).
|
||||
|
||||
**Reglas críticas para el agente:**
|
||||
- **No crees archivos JSON de traducciones, `.po`, ni ningún sistema i18n
|
||||
externo**. El único sistema de textos traducibles/editables es la tabla
|
||||
`textos_generales`.
|
||||
- **No hardcodees los textos en el código del módulo** si se espera que el
|
||||
usuario pueda cambiarlos. Envuélvelos siempre en `| translate`:
|
||||
`{{ 'Contáctanos' | translate }}` en vez de `Contáctanos`.
|
||||
- Para **cambiar un texto** (traducirlo o editarlo), el flujo correcto es
|
||||
editar la fila correspondiente en `textos_generales` — nunca modificar
|
||||
el código de la plantilla.
|
||||
- Para **añadir un texto nuevo editable**, basta con escribir el string en
|
||||
el código con `| translate`; el sistema lo recogerá y el usuario podrá
|
||||
editarlo desde el admin. No hace falta insertar la fila manualmente
|
||||
(aunque se puede via `cmsApi` si quieres pre-cargar traducciones).
|
||||
|
||||
## `raw` — Renderizar HTML sin escapar
|
||||
|
||||
```twig
|
||||
{{ record.description | raw }}
|
||||
```
|
||||
|
||||
## `truncate` — Truncar texto
|
||||
|
||||
```twig
|
||||
{{ record.description | truncate(150) }}
|
||||
```
|
||||
|
||||
## `json_decode` — Parsear JSON
|
||||
|
||||
```twig
|
||||
{% set data = jsonString | json_decode %}
|
||||
{{ data.key }}
|
||||
```
|
||||
|
||||
## `split`, `filter` — Filtros estándar Twig
|
||||
|
||||
Misma funcionalidad que Twig estándar.
|
||||
|
||||
---
|
||||
|
||||
## Operadores y Sintaxis
|
||||
|
||||
### Concatenación
|
||||
|
||||
Twig usa `~` (no `.` ni `+`):
|
||||
|
||||
```twig
|
||||
{{ 'Hello ' ~ name ~ '!' }}
|
||||
{% set url = '/products/' ~ product.slug ~ '/' %}
|
||||
```
|
||||
|
||||
### Concatenar en filtros
|
||||
|
||||
```twig
|
||||
{% set stock = 'stocks' | get('producto_num=' ~ producto.num) %}
|
||||
```
|
||||
|
||||
### Ternario / Default
|
||||
|
||||
```twig
|
||||
{{ title | default('Default Title') }}
|
||||
{{ isActive ? 'active' : 'inactive' }}
|
||||
```
|
||||
|
||||
### Comparaciones
|
||||
|
||||
```twig
|
||||
{% if items | length > 0 %}
|
||||
{% if type == 'premium' %}
|
||||
{% if name is not empty %}
|
||||
```
|
||||
|
||||
En `c-if` usar `=` (simple). En `{% if %}` usar `==` (doble).
|
||||
|
||||
---
|
||||
|
||||
## Ejemplos complejos
|
||||
|
||||
### Galería con productos y stock
|
||||
|
||||
```twig
|
||||
{% for producto in 'productos' | get('destacado=1', 'num DESC', 12) %}
|
||||
<div class="producto-card">
|
||||
<img src="{{ producto.imagen[0].urlPath | imagec(400) }}" alt="{{ producto.titulo }}">
|
||||
<h3>{{ producto.titulo }}</h3>
|
||||
<p>{{ producto.descripcion | truncate(100) }}</p>
|
||||
|
||||
{% set stock = 'stocks' | get('producto_num=' ~ producto.num) %}
|
||||
<span>Stock: {{ stock[0].cantidad }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Múltiples filtros combinados
|
||||
|
||||
```twig
|
||||
{% set categorias = 'categorias' | get() %}
|
||||
{% set productos = 'productos' | get('activo=1', 'titulo ASC', 20) %}
|
||||
{% set stats = 'hooks/obtener_stats/' | hook({fecha_inicio: '2024-01-01'}) %}
|
||||
|
||||
<h1>{{ stats.titulo | translate }}</h1>
|
||||
|
||||
<nav>
|
||||
{% for cat in categorias %}
|
||||
<a href="">{{ cat.nombre }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{% for prod in productos %}
|
||||
<div>
|
||||
<img src="{{ prod.imagen[0].urlPath | imagec(300) }}" alt="">
|
||||
<h3>{{ prod.titulo }}</h3>
|
||||
</div>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Puntos importantes
|
||||
|
||||
1. **Solo filtros, no funciones:** `'tabla' | get()` no `get('tabla')`
|
||||
2. **Upload fields son arrays:** `record.imagen[0].urlPath`, no `record.imagen`
|
||||
3. **Tablas sin prefijo `cms_`** en `get()`. Con prefijo en `queryDB()`
|
||||
4. **Concatenar con `~`:** `'stocks' | get('producto_num=' ~ producto.num)`
|
||||
Reference in New Issue
Block a user