Las cmsTables (tablas del CMS cuyo contenido MUESTRA un modulo) ya se definen desde Forge y desde el CMS legacy; faltaba que el agente las entendiera. - update_module_metadata acepta cmsTables. El endpoint del server ya la validaba, asi que solo habia que declararla en el schema Zod. - get_module_config_vars la devuelve: sale gratis porque el handler ya resolvia el schema del modulo para uploadFields/varsMeta. - Docs (01, 03, 09) con el matiz que mas se puede confundir: `tables` es donde viven los VALORES de las vars (siempre builder_custom, lo pone el compilador) y `cmsTables` es que contenido MUESTRA el modulo. Para el agente lo util no es solo escribirlas: al recibirlas sabe donde esta de verdad el contenido visible. Si le piden cambiar lo que muestra un listado de noticias, los registros estan en esa tabla, no en las vars del modulo. Se documenta ademas que la metadata del builder.json es la UNICA parte editable del fichero (y solo con esta tool): el resto lo regenera el compilador en cada compilacion.
265 lines
14 KiB
Markdown
265 lines
14 KiB
Markdown
---
|
|
title: "Módulos y Secciones Generales"
|
|
tags: [modules, sections, structure, twig, html]
|
|
load_priority: 75
|
|
load_when: [always]
|
|
summary: "Módulos (carpetas en template/estandar/modulos), index-base.tpl, secciones generales (custom-{tableName}/), thisrecord, gestión de uploads de un registro."
|
|
---
|
|
# 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.
|
|
- **Excepción del `builder.json`: la metadata SÍ se edita, con `update_module_metadata`** (`label`, `description`, `onlyAdminModule`, `MJMLModule`, `cmsTables`). Nunca la escribas con tools de archivo: el compilador regenera el fichero entero y perderías el cambio.
|
|
- 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`
|
|
|
|
### Gestionar la galería / uploads de un registro
|
|
|
|
Cuando el usuario pide "cambia la imagen de la vacante 12", "borra la 2ª foto de la galería", "reordena las imágenes" — **NO uses `upload_record_image` para reemplazar**: eso añade un upload nuevo encima sin borrar el viejo y deja basura. Usa el flow de gestión:
|
|
|
|
```
|
|
list_record_uploads({ tableName, recordId, fieldName })
|
|
→ [{ uploadId, urlPath, info1, ... }, ...]
|
|
```
|
|
|
|
Y según el caso:
|
|
|
|
| Acción del usuario | Tool a usar |
|
|
|---|---|
|
|
| Reemplazar una imagen concreta por otra | `replace_record_image({ tableName, recordId, fieldName, uploadId, imageUrl })` |
|
|
| Borrar una imagen | `delete_record_upload({ uploadId })` |
|
|
| Cambiar el orden | `reorder_record_uploads({ tableName, recordId, fieldName, uploadIds: [...] })` |
|
|
| Añadir una imagen NUEVA (sin tocar las existentes) | `upload_record_image({ tableName, recordId, fieldName, imageUrl })` |
|
|
|
|
En modo producción todas estas tools sincronizan automáticamente con el servidor real. Si solo conoces el `recordId` y necesitas saber qué `fieldName` tiene uploads, llama antes a `get_table_schema({ minimal: true })` y filtra los campos `type: "upload"`.
|
|
|
|
### 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.
|
|
|
|
## `cmsTables` — qué tablas del CMS muestra el módulo
|
|
|
|
Un módulo puede pintar contenido que NO vive en sus variables: un listado de noticias, una parrilla de productos, un carrusel del blog. Esos registros están en tablas del CMS, y `cmsTables` es donde el módulo declara cuáles.
|
|
|
|
```json
|
|
{ "label": "Listado de noticias", "cmsTables": ["noticias"] }
|
|
```
|
|
|
|
**No confundir con `tables`**, que también está en el `builder.json` y se parece demasiado:
|
|
|
|
| Clave | Qué es | Quién la pone |
|
|
|-------|--------|---------------|
|
|
| `tables` | Dónde se guardan los VALORES de las variables del módulo. Siempre `["builder_custom"]` | El compilador. No la toques |
|
|
| `cmsTables` | Qué contenido MUESTRA el módulo | Una persona desde el editor, o tú con `update_module_metadata` |
|
|
|
|
Para qué sirve: el editor pinta un acceso directo al CMS de cada tabla declarada desde cualquier página que incluya el módulo, para que quien edita esa página llegue al contenido sin buscarlo.
|
|
|
|
Cuándo declararla:
|
|
- **Sí**: el módulo hace `c-for` sobre registros de una tabla, o su `hook.php` los consulta.
|
|
- **No**: el módulo solo muestra sus propias variables (un banner con título e imagen). Deja la lista vacía.
|
|
|
|
Cómo usarla tú:
|
|
- Al crear un módulo que lista contenido, decláralas: `update_module_metadata({ module, cmsTables: ["noticias"] })`.
|
|
- Al recibirlas en `get_module_config_vars`, te dicen **dónde está de verdad el contenido visible**. Si el usuario pide cambiar lo que muestra un listado de noticias, los registros están en `noticias` — no en las variables del módulo. Ve a esa tabla con `list_table_records` / `create_or_update_record`.
|
|
- Pasar `cmsTables` reemplaza la lista entera: incluye las que quieras conservar. `[]` la vacía.
|
|
|
|
## Traducir las variables de un módulo
|
|
|
|
Los valores textuales de las variables de un módulo (títulos, descripciones, wysiwyg, etc.) NO se guardan en la fila de la página, sino en la tabla `builder_custom`. Por eso una traducción de módulo apunta siempre a `builder_custom`, no a `apartados` ni a la tabla de la página.
|
|
|
|
Para traducir las vars de una instancia de módulo:
|
|
1. `get_module_config_vars({ tableName, recordNum, sectionId })` devuelve `varsMeta`: de ahí interesa el `recordNum` (la fila de `builder_custom` de la instancia; en vars multi, uno por item).
|
|
2. `set_record_translations({ tableName: "builder_custom", recordNum: <de varsMeta>, prefix, fields: { <NOMBRE de la var>: "texto traducido" } })` — p.ej. `{ titulo: "...", subtitulo: "..." }`.
|
|
|
|
CRÍTICO: la clave de traducción es el NOMBRE de la var del builder.json (`titulo`, `subtitulo`, `enlace_anchor`...), NUNCA la columna física de `builder_custom` (`title3`, `text1`...). El runtime traduce con `t($record, $var)` por nombre de var; con la columna física se guarda pero no se pinta jamás (la action lo rechaza con 400). Tampoco añadas filtros `| translate` a las plantillas del módulo: el motor traduce las vars automáticamente. Ver el workflow completo en `09-mcp-tools-reference.md`.
|
|
|
|
## 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`.
|