Files
agenticSystem/docs/03-modules-and-sections.md
Jordan Diaz 9c3d9fb999 fix(mcp): las traducciones de vars de modulo van por NOMBRE de var, no columna fisica
Una sesion real del agente guardo title3/title6/title2 (columnas de
builder_custom segun varsMeta.fieldName, como decian las docs) y el front
no pintaba nada: el runtime traduce vars con t($record, $var) por nombre
de var. Corregidas descripciones de set/get_record_translations, docs
03/09/11b y ACAI_ENDPOINTS; ademas el puente PHP ahora rechaza titleN/
textN sobre builder_custom con error explicativo (guardrail).
2026-07-17 08:18:18 +00:00

238 lines
12 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.
- 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.
## 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`.