Compare commits
7 Commits
5c011ab7ef
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ad2a6f87b | ||
|
|
2afc2cdc34 | ||
|
|
4835ff9467 | ||
|
|
49b52b0f9f | ||
|
|
76221ee1d4 | ||
|
|
f7e950694e | ||
|
|
bdee7665ec |
@@ -3,11 +3,11 @@ title: "Campos editables del builder"
|
|||||||
tags: [builder, twig, html, modules]
|
tags: [builder, twig, html, modules]
|
||||||
load_priority: 80
|
load_priority: 80
|
||||||
load_when: [always]
|
load_when: [always]
|
||||||
summary: "Atributos data-field-* (textfield, headfield, link, upload, list, multiv2, checkbox), agrupar campos en pestañas con data-field-group, c-if/c-for/c-class, c-form, componentes built-in del builder Acai."
|
summary: "Atributos data-field-* (textfield, headfield, link, upload, list, multiv2, colors, colorpicker), agrupar campos en pestañas con data-field-group, mostrar u ocultar campos y pestañas segun otro campo con data-field-show y data-field-group-show, c-if/c-for/c-class, c-form, componentes built-in del builder Acai."
|
||||||
---
|
---
|
||||||
# Builder Fields — Campos editables del index-base.tpl
|
# 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`.
|
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`, `colors`, `colorpicker`, `corners`, `ratio`), la regla `data-field-label` → nombre de variable, el reparto en pestañas (`data-field-group`) y su visibilidad condicional (`data-field-show`, `data-field-group-show`), 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
|
## Reglas de nomenclatura de variables
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Reglas obligatorias:
|
|||||||
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
||||||
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
||||||
| `multiv2` | `<li>` wrapper | Array de objetos repetibles |
|
| `multiv2` | `<li>` wrapper | Array de objetos repetibles |
|
||||||
| `checkbox` | `<div>` o `<input>` | `1` o `0` (número) |
|
| `colors` | `<div data-field-colors="fondo,titulo">` | Objeto de N colores por nombre: `{{ colores.fondo }}` |
|
||||||
| `colorpicker` | `<div>` | Hex color string |
|
| `colorpicker` | `<div>` | Hex color string |
|
||||||
|
|
||||||
### textfield
|
### textfield
|
||||||
@@ -210,13 +210,54 @@ Uso en Twig:
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
```
|
```
|
||||||
|
|
||||||
### checkbox
|
### colors — paleta de colores
|
||||||
|
|
||||||
Devuelve `1` o `0` (número), nunca `true`/`false`.
|
Un solo campo que agrupa VARIOS colores. Los nombres de cada color se declaran en
|
||||||
|
`data-field-colors`, separados por comas:
|
||||||
|
|
||||||
### colorpicker
|
```html
|
||||||
|
<div c-hidden="true">
|
||||||
|
<div data-field-type="colors"
|
||||||
|
data-field-label="Colores"
|
||||||
|
data-field-colors="fondo,titulo,boton"></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
Devuelve un string hexadecimal (`#ff0000`). Almacenado en config-vars (no en `builder_custom`).
|
El valor guardado es un JSON con pares nombre/valor. Cada color puede ser sólido o un
|
||||||
|
gradiente lineal:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"fondo": "#ffffff", "titulo": "#111111", "boton": "linear-gradient(90deg, #aaa, #000)"}
|
||||||
|
```
|
||||||
|
|
||||||
|
En Twig se accede a cada color por su nombre: `{{ colores.fondo }}`.
|
||||||
|
|
||||||
|
### colorpicker — un solo color
|
||||||
|
|
||||||
|
Cuando solo necesitas UN color, no una paleta. El valor es un string hexadecimal plano:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div c-hidden="true">
|
||||||
|
<div data-field-type="colorpicker" data-field-label="Color de fondo"></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Valor guardado: `#ff0000`. En Twig: `{{ colordefondo }}`.
|
||||||
|
|
||||||
|
Usa `colorpicker` para un color suelto y `colors` cuando el módulo tenga varios: `colors`
|
||||||
|
gasta UNA sola variable para N colores, mientras que N `colorpicker` gastan N.
|
||||||
|
|
||||||
|
### corners — radios de esquina
|
||||||
|
|
||||||
|
Selector de radio de borde en escala Tailwind. Mismo patrón que `colors`.
|
||||||
|
|
||||||
|
### ratio — proporción
|
||||||
|
|
||||||
|
Selector de proporción de imagen (16/9, 4/3, 1/1...).
|
||||||
|
|
||||||
|
> Para un valor booleano usa `list` con dos opciones: el builder no tiene un tipo de casilla.
|
||||||
|
> (No lo confundas con el tipo `checkbox` de los campos de TABLA del CMS, que sí existe y se
|
||||||
|
> documenta en `05-tables-and-fields.md`. Son dos vocabularios distintos.)
|
||||||
|
|
||||||
## Agrupar campos en pestañas (`data-field-group`)
|
## Agrupar campos en pestañas (`data-field-group`)
|
||||||
|
|
||||||
@@ -253,6 +294,54 @@ Recomendación de uso:
|
|||||||
- Reparto habitual: **Contenido** (textos e imágenes), **Estilos** (colores, alineación, bordes), **Ajustes** (opciones de comportamiento, límites, enlaces).
|
- Reparto habitual: **Contenido** (textos e imágenes), **Estilos** (colores, alineación, bordes), **Ajustes** (opciones de comportamiento, límites, enlaces).
|
||||||
- Mantén los nombres de grupo consistentes entre módulos y en español. Son literales: `Estilos` y `estilos` generan dos pestañas distintas.
|
- Mantén los nombres de grupo consistentes entre módulos y en español. Son literales: `Estilos` y `estilos` generan dos pestañas distintas.
|
||||||
|
|
||||||
|
## Mostrar u ocultar campos y pestañas (`data-field-show`)
|
||||||
|
|
||||||
|
Un campo o una pestaña entera pueden depender del valor de otro campo del mismo módulo. Se declara con dos atributos, que viajan al `builder.json` por el mismo mecanismo genérico que `data-field-group` (`customDataField.show` y `customDataField['group-show']`):
|
||||||
|
|
||||||
|
- `data-field-show="campo=valor"` — en el elemento del campo que se quiere condicionar.
|
||||||
|
- `data-field-group-show="campo=valor"` — en **una** var del grupo; condiciona la pestaña completa.
|
||||||
|
|
||||||
|
Gramática:
|
||||||
|
|
||||||
|
| Condición | Se muestra cuando |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| `modo=1` | el valor es `1` |
|
||||||
|
| `modo=1,2` | el valor es `1` o `2` |
|
||||||
|
| `modo=` | el campo está **vacío** — es la clave de la primera opción de todo `list` |
|
||||||
|
| `modo!=1` | el valor NO es `1` |
|
||||||
|
| `modo=1;otro=2` | se cumplen ambas (AND) |
|
||||||
|
|
||||||
|
El campo se referencia por su **nombre de variable**, no por su label: se aplican las [reglas de nomenclatura](#reglas-de-nomenclatura-de-variables), así que `Mostrar Estilos` se referencia como `mostrarestilos` y `Título` como `ttulo` (los acentos se borran, no se transliteran). Los valores no pueden contener coma ni punto y coma.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div c-hidden="true">
|
||||||
|
<!-- Pestaña completa: "Estilos" solo aparece si el usuario activa el switch -->
|
||||||
|
<div data-field-type="list" data-field-label="Mostrar Estilos" data-list-options="|No,1|Si"></div>
|
||||||
|
<div data-field-type="textfield"
|
||||||
|
data-field-label="Texto de Estilos"
|
||||||
|
data-field-group="Estilos"
|
||||||
|
data-field-group-show="mostrarestilos=1"></div>
|
||||||
|
|
||||||
|
<!-- Campo a campo: cada opción del list muestra su propio campo -->
|
||||||
|
<div data-field-type="list"
|
||||||
|
data-field-label="Modo Modulo"
|
||||||
|
data-list-options="|Opcion 1,1|Opcion 2,2|Opcion 3"
|
||||||
|
data-field-group="Contenido"></div>
|
||||||
|
<div data-field-type="textfield" data-field-label="Opcion 1" data-field-group="Contenido" data-field-show="modomodulo="></div>
|
||||||
|
<div data-field-type="textfield" data-field-label="Opcion 2" data-field-group="Contenido" data-field-show="modomodulo=1"></div>
|
||||||
|
<div data-field-type="textfield" data-field-label="Opcion 3" data-field-group="Contenido" data-field-show="modomodulo=2"></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Comportamiento del panel:
|
||||||
|
- **Ocultar no borra.** El valor sigue guardado y sigue llegando al Twig; si el usuario vuelve a mostrar el campo, lo escrito sigue ahí.
|
||||||
|
- **Por eso la plantilla debe repetir la condición con `c-if`.** Ocultar el campo en el panel NO lo quita de la web: si `Texto de Estilos` no debe pintarse cuando `mostrarestilos` está a `0`, el `index-base.tpl` necesita su propio `c-if="mostrarestilos = '1'"`.
|
||||||
|
- Una pestaña se oculta sola cuando todos sus campos han quedado ocultos por su `show`; en la mayoría de casos basta con condicionar los campos y no hace falta `data-field-group-show`.
|
||||||
|
- Si varias vars del mismo grupo declaran `data-field-group-show`, **gana la primera** y las demás se ignoran.
|
||||||
|
- Si una condición apunta a un campo que no existe (typo, o un label renombrado que cambió el nombre de variable), el campo **se muestra igualmente** y el aviso queda en la consola del navegador. Nunca desaparece en silencio.
|
||||||
|
- Dentro de `multiv2` la condición se evalúa contra los valores **de ese item**, no contra los del módulo.
|
||||||
|
- En modo traducción las condiciones se evalúan contra los valores del **idioma base**.
|
||||||
|
|
||||||
## Atributos Acai
|
## Atributos Acai
|
||||||
|
|
||||||
### `c-if` — Renderizado condicional
|
### `c-if` — Renderizado condicional
|
||||||
@@ -500,3 +589,4 @@ Valores comunes: `fade-up`, `fade-down`, `fade-left`, `fade-right`, `zoom-in`, `
|
|||||||
8. Checkbox guarda `1` o `0` (número), nunca `true`/`false`.
|
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`.
|
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-*`.
|
10. `script.js` y `style.css` son estáticos: NO uses sintaxis Twig dentro. Pasa valores dinámicos vía `data-*`.
|
||||||
|
11. Si el módulo lista registros de una tabla del CMS (`c-for` sobre ella, o su `hook.php` la consulta), declara esa tabla con `update_module_metadata({ cmsTables: [...] })`. No es un `data-field-*`: no se deduce del HTML, hay que declararla. Ver `03-modules-and-sections.md`.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ Componentes visuales reutilizables. Viven en `template/estandar/modulos/<module-
|
|||||||
|
|
||||||
Reglas duras:
|
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.
|
- **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**.
|
- 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-*`.
|
- `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.
|
- `index-base.tpl` solo contiene HTML/Twig. **Nunca** embebas etiquetas `<script>` con lógica del módulo, **nunca** PHP.
|
||||||
@@ -203,6 +204,32 @@ Acceso en Twig:
|
|||||||
|
|
||||||
Las variables son **propiedades del objeto iterado**, no variables sueltas.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Reglas:
|
|||||||
| `check_module_usage` | Lista páginas que usan el módulo | **OBLIGATORIO antes de `delete_module`** |
|
| `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 |
|
| `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 |
|
| `set_module_example_data` | Define datos de ejemplo para preview en el editor | Pasar valores para TODAS las variables del schema |
|
||||||
|
| `update_module_metadata` | Edita metadata del `builder.json` | `label`, `description`, `onlyAdminModule`, `MJMLModule`, `cmsTables`. NO renombra el módulo. Rechaza los módulos de layout (`custom-header/footer[-twig]`) |
|
||||||
|
|
||||||
### Registros (records)
|
### Registros (records)
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ Reglas:
|
|||||||
| `remove_module_from_record` | Quita módulo de la página | Por `sectionId` (preferido) o `modulePosition` |
|
| `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` |
|
| `reorder_module` | Mueve módulo a otra posición | `fromPosition` → `toPosition` |
|
||||||
| `toggle_module_visibility` | Muestra/oculta sin borrar | Por `sectionId` |
|
| `toggle_module_visibility` | Muestra/oculta sin borrar | Por `sectionId` |
|
||||||
| `get_module_config_vars` | Lee valores actuales de las variables | Por `tableName` + `recordNum` + `sectionId` |
|
| `get_module_config_vars` | Lee valores actuales de las variables | Por `tableName` + `recordNum` + `sectionId`. Devuelve además `cmsTables`: las tablas cuyo contenido MUESTRA el módulo (ahí están sus registros, no en las vars) |
|
||||||
| `set_module_config_vars` | Escribe variables del módulo | Devuelve `uploadFields` con `recordNum`+`fieldName` listos para subir imágenes |
|
| `set_module_config_vars` | Escribe variables del módulo | Devuelve `uploadFields` con `recordNum`+`fieldName` listos para subir imágenes |
|
||||||
|
|
||||||
### Tablas y campos (schema)
|
### Tablas y campos (schema)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: "Reglas inmutables y cheat-sheet de tipos"
|
|||||||
tags: [reference, rules, cheat]
|
tags: [reference, rules, cheat]
|
||||||
load_priority: 90
|
load_priority: 90
|
||||||
load_when: [cheatsheet]
|
load_when: [cheatsheet]
|
||||||
summary: "Reglas no negociables (cms_, num, _num, upload arrays, c-if/{% if %}), tipos de builder field, atributos Acai, filtros Twig, formato de datos para insert/update, errores comunes."
|
summary: "Reglas no negociables (cms_, num, _num, upload arrays, c-if/{% if %}), tipos de builder field, pestañas y visibilidad condicional de campos (data-field-group, data-field-show), atributos Acai, filtros Twig, formato de datos para insert/update, errores comunes."
|
||||||
---
|
---
|
||||||
# Reglas inmutables y cheat-sheet
|
# Reglas inmutables y cheat-sheet
|
||||||
|
|
||||||
@@ -42,11 +42,13 @@ Resumen ejecutable de reglas críticas, tipos de campo, filtros y formatos de da
|
|||||||
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
||||||
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
||||||
| `multiv2` | `<li>` wrapper | Array de objetos |
|
| `multiv2` | `<li>` wrapper | Array de objetos |
|
||||||
| `checkbox` | `<input>` o `<div>` | `1` / `0` |
|
| `colors` | `<div data-field-colors="fondo,titulo">` | Objeto de N colores por nombre |
|
||||||
| `colorpicker` | `<div>` | Hex color |
|
| `colorpicker` | `<div>` | Hex color |
|
||||||
|
|
||||||
Pestañas en el panel del módulo: `data-field-group="Estilos"` en el elemento del campo (sin group → pestaña "Principal").
|
Pestañas en el panel del módulo: `data-field-group="Estilos"` en el elemento del campo (sin group → pestaña "Principal").
|
||||||
|
|
||||||
|
Visibilidad condicional: `data-field-show="modo=1"` en el campo, y `data-field-group-show="modo=1"` en **una** var del grupo para condicionar la pestaña entera. Se referencia el NOMBRE DE VARIABLE, no el label. `modo=` significa vacío (clave de la primera opción de todo `list`), `,` es OR, `!=` niega y `;` encadena condiciones (AND).
|
||||||
|
|
||||||
## Atributos Acai
|
## Atributos Acai
|
||||||
|
|
||||||
| Atributo | Uso | Ejemplo |
|
| Atributo | Uso | Ejemplo |
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Definiciones cortas de los términos que aparecen en docs y prompts. Si te pierd
|
|||||||
|
|
||||||
**`c-form`** — atributo que convierte un `<form>` en un formulario que persiste a una tabla del CMS. Sintaxis: `<c-form tableName="'contacto'" captcha="true">`. Se renderiza como form HTML con submit a un endpoint Acai.
|
**`c-form`** — atributo que convierte un `<form>` en un formulario que persiste a una tabla del CMS. Sintaxis: `<c-form tableName="'contacto'" captcha="true">`. Se renderiza como form HTML con submit a un endpoint Acai.
|
||||||
|
|
||||||
**`data-field-*`** — familia de atributos que marca un elemento como editable en el builder visual. Tipos: `textfield`, `headfield`, `textbox`, `wysiwyg`, `link`, `upload`, `uploadMulti`, `list`, `multiv2`, `checkbox`, `colorpicker`.
|
**`data-field-*`** — familia de atributos que marca un elemento como editable en el builder visual. Tipos: `textfield`, `headfield`, `textbox`, `wysiwyg`, `link`, `upload`, `uploadMulti`, `list`, `multiv2`, `colors`, `colorpicker`.
|
||||||
|
|
||||||
**`c-if`, `c-for`, `c-class`, `c-hidden`, `c-required`** — atributos de lógica visual. **`c-if` usa un solo `=`** (`c-if="x = 1"`), Twig `{% if %}` usa **doble** `==`.
|
**`c-if`, `c-for`, `c-class`, `c-hidden`, `c-required`** — atributos de lógica visual. **`c-if` usa un solo `=`** (`c-if="x = 1"`), Twig `{% if %}` usa **doble** `==`.
|
||||||
|
|
||||||
|
|||||||
@@ -129,16 +129,130 @@ Examples:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerGetHookEntryParamsTool(server) {
|
||||||
|
server.tool(
|
||||||
|
"get_hook_entryparams",
|
||||||
|
`Read the declared entry parameters (entryParams) of a global hook. entryParams are the input parameters a hook expects — each has a required 'variable' name plus optional 'value' and 'valueType'. They are passed to the hook when it is invoked.
|
||||||
|
|
||||||
|
Use this when the user asks about a hook's inputs, or to inspect the current params before editing them.
|
||||||
|
|
||||||
|
hookEndPoint format: starts and ends with '/', with '/' as separator. E.g. "/hooks/appListado/".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- entryParams: array of { variable, value?, valueType? }.
|
||||||
|
Example: [{ "variable": "action" }, { "variable": "data", "value": "", "valueType": "Integer" }].`,
|
||||||
|
withAuthParams({
|
||||||
|
hookEndPoint: z.string().describe('Hook endpoint path, e.g. "/hooks/appListado/"'),
|
||||||
|
}),
|
||||||
|
{ readOnlyHint: true, destructiveHint: false },
|
||||||
|
withAuth(async ({ hookEndPoint }, extra) => {
|
||||||
|
try {
|
||||||
|
const { projectSlug } = getCurrentProjectInfo();
|
||||||
|
const result = await pythonGet("/api/creator/hook-entryparams", {
|
||||||
|
project: projectSlug,
|
||||||
|
endPoint: hookEndPoint,
|
||||||
|
});
|
||||||
|
if (!result?.success) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: result?.error || "No se pudieron leer los entryParams",
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
exists: !!result.exists,
|
||||||
|
entryParams: result.entryParams || [],
|
||||||
|
hookEndPoint,
|
||||||
|
}, null, 2),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleToolError(error, "get_hook_entryparams", { hookEndPoint });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerSetHookEntryParamsTool(server) {
|
||||||
|
server.tool(
|
||||||
|
"set_hook_entryparams",
|
||||||
|
`Set the declared entry parameters (entryParams) of a global hook. entryParams describe the inputs a hook expects — each has a required 'variable' name plus optional 'value' and 'valueType'.
|
||||||
|
|
||||||
|
IMPORTANT: this OVERWRITES the entire entryParams list of the hook (it does NOT merge). You must pass the COMPLETE set of params every time, because the whole array is replaced.
|
||||||
|
|
||||||
|
Use this AFTER creating or editing the hook file (via acai-write) to declare which inputs it accepts.
|
||||||
|
|
||||||
|
entryParams format: array of { variable, value?, valueType? }.
|
||||||
|
Example: [{ "variable": "action" }, { "variable": "data", "value": "", "valueType": "Integer" }].`,
|
||||||
|
withAuthParams({
|
||||||
|
hookEndPoint: z.string().describe('Hook endpoint path, e.g. "/hooks/appListado/"'),
|
||||||
|
entryParams: z.array(z.object({
|
||||||
|
variable: z.string(),
|
||||||
|
value: z.string().optional(),
|
||||||
|
valueType: z.string().optional(),
|
||||||
|
})).describe('Complete list of entry params. Each item: { variable (required), value? (string), valueType? (string) }. Replaces the whole array. E.g. [{"variable":"action"},{"variable":"data","value":"","valueType":"Integer"}]'),
|
||||||
|
}),
|
||||||
|
{ readOnlyHint: false, destructiveHint: false },
|
||||||
|
withAuth(async ({ hookEndPoint, entryParams }, extra) => {
|
||||||
|
try {
|
||||||
|
const { projectSlug } = getCurrentProjectInfo();
|
||||||
|
const result = await pythonPost("/api/creator/hook-entryparams", {
|
||||||
|
project: projectSlug,
|
||||||
|
endPoint: hookEndPoint,
|
||||||
|
entryParams,
|
||||||
|
});
|
||||||
|
if (!result?.success) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: result?.error || "No se pudo guardar",
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: result.message || "entryParams actualizados",
|
||||||
|
entryParams: result.entryParams || [],
|
||||||
|
hookEndPoint,
|
||||||
|
}, null, 2),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleToolError(error, "set_hook_entryparams", { hookEndPoint, entryParams });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registra las tools de configuracion de hooks globales.
|
* Registra las tools de configuracion de hooks globales.
|
||||||
*
|
*
|
||||||
* `get_hook_middleware` es de solo lectura y se registra siempre. El set
|
* Las tools de solo lectura (`get_hook_middleware`, `get_hook_entryparams`) se
|
||||||
* modifica el layout y solo se expone si el rol puede editar codigo — sigue
|
* registran siempre. Las de escritura modifican el layout y solo se exponen si
|
||||||
* el mismo criterio que otras tools de escritura (ver project/index.js).
|
* el rol puede editar codigo — sigue el mismo criterio que otras tools de
|
||||||
|
* escritura (ver project/index.js).
|
||||||
*/
|
*/
|
||||||
export function registerHookTools(server) {
|
export function registerHookTools(server) {
|
||||||
registerGetHookMiddlewareTool(server);
|
registerGetHookMiddlewareTool(server);
|
||||||
|
registerGetHookEntryParamsTool(server);
|
||||||
if (canEditCode()) {
|
if (canEditCode()) {
|
||||||
registerSetHookMiddlewareTool(server);
|
registerSetHookMiddlewareTool(server);
|
||||||
|
registerSetHookEntryParamsTool(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { withAuth, getSessionCredentials, getApiClient, getCommonParams } from "../../auth/index.js";
|
import { withAuth } from "../../auth/index.js";
|
||||||
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
|
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||||
import { withAuthParams } from "../helpers/authSchema.js";
|
import { withAuthParams } from "../helpers/authSchema.js";
|
||||||
|
import { pythonPost } from "../helpers/pythonServerClient.js";
|
||||||
|
import { getCurrentProjectInfo } from "../files/helpers.js";
|
||||||
|
|
||||||
|
// Antes esta tool llamaba a `action_ws=setStaticVars`, que hacia un
|
||||||
|
// file_put_contents del builder.json ENTERO en la web para cambiar una sola
|
||||||
|
// clave. Eso se saltaba el bloqueo de escritura de builder.json y, si caia una
|
||||||
|
// compilacion entre su lectura y su escritura, devolvia el mapeo var->columna
|
||||||
|
// anterior encima del recien generado (contenido rotado en todas las paginas
|
||||||
|
// que usan el modulo). Ahora delega en el endpoint quirurgico de Forge, que
|
||||||
|
// escribe SOLO las claves de su allowlist.
|
||||||
|
|
||||||
export function registerSetModuleExampleDataTool(server) {
|
export function registerSetModuleExampleDataTool(server) {
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -10,6 +20,8 @@ export function registerSetModuleExampleDataTool(server) {
|
|||||||
|
|
||||||
Reglas críticas:
|
Reglas críticas:
|
||||||
- Uploads SIEMPRE como [{ urlPath: "..." }] (nunca strings ni objetos sueltos).
|
- Uploads SIEMPRE como [{ urlPath: "..." }] (nunca strings ni objetos sueltos).
|
||||||
|
- 'colors' como STRING con un JSON de pares nombre/color, usando los nombres declarados en data-field-colors: "{\\"fondo\\":\\"#ffffff\\",\\"titulo\\":\\"#111111\\"}". Cada color admite hex o linear-gradient(...).
|
||||||
|
- 'colorpicker' como un hex plano ("#ff0000"), no como objeto.
|
||||||
- 'multiv2' como array con al menos 2 items para que el preview se vea representativo.
|
- 'multiv2' como array con al menos 2 items para que el preview se vea representativo.
|
||||||
- Los nombres de variables se derivan de 'data-field-label' (minúsculas, sin espacios ni acentos).
|
- Los nombres de variables se derivan de 'data-field-label' (minúsculas, sin espacios ni acentos).
|
||||||
- Para URLs de imagen usa 'generate_image' o un placeholder (e.g. https://placehold.co/800x600).
|
- Para URLs de imagen usa 'generate_image' o un placeholder (e.g. https://placehold.co/800x600).
|
||||||
@@ -50,42 +62,36 @@ Si dudas del formato exacto, lee 'read_doc({ name: "01-builder-fields" })'.`,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = await getSessionCredentials(extra.sessionId);
|
console.error(`[set_module_example_data] Module ID: ${moduleId}, vars: ${Object.keys(exampleData).length}`);
|
||||||
const client = await getApiClient(extra.sessionId);
|
|
||||||
|
|
||||||
// Log data for debugging
|
const { projectSlug } = getCurrentProjectInfo();
|
||||||
console.error(`[set_module_example_data] Module ID: ${moduleId}`);
|
const result = await pythonPost("/api/modules/update-metadata", {
|
||||||
console.error(`[set_module_example_data] Module Schema:`, JSON.stringify(moduleSchema, null, 2));
|
project: projectSlug,
|
||||||
console.error(`[set_module_example_data] Example Data:`, JSON.stringify(exampleData, null, 2));
|
module: moduleId,
|
||||||
|
|
||||||
// Prepare payload for setStaticVars action
|
|
||||||
const payload = await getCommonParams(extra.sessionId, {
|
|
||||||
action_ws: "setStaticVars",
|
|
||||||
moduleId: moduleId,
|
|
||||||
staticVars: exampleData,
|
staticVars: exampleData,
|
||||||
schema: moduleSchema
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.error(`[set_module_example_data] Full Payload:`, JSON.stringify(payload, null, 2));
|
if (!result?.success) {
|
||||||
|
return {
|
||||||
// Send to viewer_functions
|
content: [{
|
||||||
const response = await client.post("/cms/lib/viewer_functions.php", payload);
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
console.error(`[set_module_example_data] Response:`, JSON.stringify(response.data, null, 2));
|
success: false,
|
||||||
|
error: result?.error || "Could not set module example data",
|
||||||
// Check for API errors in response
|
}),
|
||||||
const apiError = handleApiResponse(response.data, 'set_module_example_data');
|
}],
|
||||||
if (apiError) return apiError;
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: "text", text: JSON.stringify({
|
type: "text", text: JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Example data set successfully for module '${moduleId}'`,
|
message: `Example data set successfully for module '${moduleId}'`,
|
||||||
moduleId: moduleId,
|
moduleId: result.module || moduleId,
|
||||||
dataCount: Object.keys(exampleData).length,
|
dataCount: Object.keys(exampleData).length,
|
||||||
schemaVarsCount: moduleSchema?.codeVars ? Object.keys(moduleSchema.codeVars).length : 0,
|
schemaVarsCount: moduleSchema?.codeVars ? Object.keys(moduleSchema.codeVars).length : 0,
|
||||||
response: response.data
|
|
||||||
}, null, 2)
|
}, null, 2)
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,22 +7,27 @@ import { getCurrentProjectInfo } from "../files/helpers.js";
|
|||||||
|
|
||||||
// Tool: update_module_metadata
|
// Tool: update_module_metadata
|
||||||
// Edita la metadata de builder.json de un modulo (label, description,
|
// Edita la metadata de builder.json de un modulo (label, description,
|
||||||
// onlyAdminModule, MJMLModule) delegando en /api/modules/update-metadata.
|
// onlyAdminModule, MJMLModule, cmsTables) delegando en
|
||||||
// El id/carpeta del modulo NO se puede renombrar y el endpoint rechaza los
|
// /api/modules/update-metadata. El id/carpeta del modulo NO se puede renombrar
|
||||||
// modulos generados del layout (custom-header/footer[-twig]).
|
// y el endpoint rechaza los modulos generados del layout
|
||||||
|
// (custom-header/footer[-twig]).
|
||||||
|
|
||||||
// Claves editables del builder.json. Se envian solo las que llegan definidas,
|
// Claves editables del builder.json. Se envian solo las que llegan definidas,
|
||||||
// de modo que el endpoint hace un merge parcial y no pisa el resto.
|
// de modo que el endpoint hace un merge parcial y no pisa el resto.
|
||||||
const EDITABLE_KEYS = ["label", "description", "onlyAdminModule", "MJMLModule"];
|
const EDITABLE_KEYS = ["label", "description", "onlyAdminModule", "MJMLModule", "cmsTables"];
|
||||||
|
|
||||||
export function registerUpdateModuleMetadataTool(server) {
|
export function registerUpdateModuleMetadataTool(server) {
|
||||||
server.tool(
|
server.tool(
|
||||||
"update_module_metadata",
|
"update_module_metadata",
|
||||||
`Edit a module's metadata in its builder.json: 'label' (display name shown in the visual builder), 'description' (short help text for editors), 'onlyAdminModule' (true = the module is only visible to admin users in the builder), 'MJMLModule' (true = the module is an email/MJML module).
|
`Edit a module's metadata in its builder.json: 'label' (display name shown in the visual builder), 'description' (short help text for editors), 'onlyAdminModule' (true = the module is only visible to admin users in the builder), 'MJMLModule' (true = the module is an email/MJML module), 'cmsTables' (CMS tables whose content this module displays).
|
||||||
|
|
||||||
|
Use 'cmsTables' when the module renders records from a CMS table — a news list, a product grid, a blog carousel. Declare the tables it reads (e.g. ["noticias"]). The editor then shows a direct link to each of those tables from any page that includes the module, so whoever edits that page can reach the content without hunting for it. A module that only shows its own variables (a banner with a title and an image) needs no cmsTables.
|
||||||
|
|
||||||
|
Do NOT confuse 'cmsTables' with the 'tables' key of builder.json: 'tables' is where the module's VARIABLE VALUES are stored (always builder_custom) and is managed by the compiler. 'cmsTables' is what content the module DISPLAYS, and is chosen by a human or by you.
|
||||||
|
|
||||||
The module id (its folder name under template/estandar/modulos/) CANNOT be renamed with this tool — there is no rename option at all. Only the fields above change; everything else in builder.json is preserved.
|
The module id (its folder name under template/estandar/modulos/) CANNOT be renamed with this tool — there is no rename option at all. Only the fields above change; everything else in builder.json is preserved.
|
||||||
|
|
||||||
At least one of label, description, onlyAdminModule or MJMLModule is required. Fields you omit are left untouched.
|
At least one of label, description, onlyAdminModule, MJMLModule or cmsTables is required. Fields you omit are left untouched. Passing cmsTables replaces the whole list, so include the tables you want to keep; pass [] to clear it.
|
||||||
|
|
||||||
Not applicable to the generated layout modules (custom-header, custom-footer, custom-header-twig, custom-footer-twig): those are artifacts of the global layout and the request will be rejected — use set_layout_field for them.`,
|
Not applicable to the generated layout modules (custom-header, custom-footer, custom-header-twig, custom-footer-twig): those are artifacts of the global layout and the request will be rejected — use set_layout_field for them.`,
|
||||||
withAuthParams({
|
withAuthParams({
|
||||||
@@ -31,11 +36,12 @@ Not applicable to the generated layout modules (custom-header, custom-footer, cu
|
|||||||
description: z.string().optional().describe("Short description shown to editors in the builder"),
|
description: z.string().optional().describe("Short description shown to editors in the builder"),
|
||||||
onlyAdminModule: z.boolean().optional().describe("If true, the module is only visible to admin users in the builder"),
|
onlyAdminModule: z.boolean().optional().describe("If true, the module is only visible to admin users in the builder"),
|
||||||
MJMLModule: z.boolean().optional().describe("If true, the module is treated as an email (MJML) module"),
|
MJMLModule: z.boolean().optional().describe("If true, the module is treated as an email (MJML) module"),
|
||||||
|
cmsTables: z.array(z.string()).optional().describe("CMS tables whose records this module displays, e.g. [\"noticias\"]. Replaces the whole list; [] clears it. Only real tables of the project — they become direct links to the CMS."),
|
||||||
}),
|
}),
|
||||||
{ readOnlyHint: false, destructiveHint: false },
|
{ readOnlyHint: false, destructiveHint: false },
|
||||||
withAuth(async ({ module, label, description, onlyAdminModule, MJMLModule }, _extra) => {
|
withAuth(async ({ module, label, description, onlyAdminModule, MJMLModule, cmsTables }, _extra) => {
|
||||||
try {
|
try {
|
||||||
const provided = { label, description, onlyAdminModule, MJMLModule };
|
const provided = { label, description, onlyAdminModule, MJMLModule, cmsTables };
|
||||||
|
|
||||||
// Validacion temprana: sin ninguna clave editable no tiene
|
// Validacion temprana: sin ninguna clave editable no tiene
|
||||||
// sentido llamar al endpoint. Ojo con los booleanos false —
|
// sentido llamar al endpoint. Ojo con los booleanos false —
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function registerGetModuleConfigVarsTool(server) {
|
|||||||
- vars: resolved values (text, HTML, etc.) for simple vars and arrays for multi/repeater vars
|
- vars: resolved values (text, HTML, etc.) for simple vars and arrays for multi/repeater vars
|
||||||
- varsMeta: per-var physical location { tableName: 'builder_custom', recordNum, fieldName, type }. USE THIS to know exactly which row + column to update with create_or_update_record. The variable's display name (e.g. 'titulo') is NOT the same as the physical column name (e.g. 'title2'). Uploads inside a 'multi' var carry their labels in subFields.<subVar>.infoLabels.
|
- varsMeta: per-var physical location { tableName: 'builder_custom', recordNum, fieldName, type }. USE THIS to know exactly which row + column to update with create_or_update_record. The variable's display name (e.g. 'titulo') is NOT the same as the physical column name (e.g. 'title2'). Uploads inside a 'multi' var carry their labels in subFields.<subVar>.infoLabels.
|
||||||
- uploadFields: per-var upload location for upload_record_image / replace_record_image / set_upload_info. Each entry includes infoLabels when the module defines them (array; position 0 = info1).
|
- uploadFields: per-var upload location for upload_record_image / replace_record_image / set_upload_info. Each entry includes infoLabels when the module defines them (array; position 0 = info1).
|
||||||
|
- cmsTables: CMS tables whose records this module DISPLAYS (e.g. ["noticias"]), or [] if it only shows its own variables. This is where the module's visible content really lives: if the user asks to change what a news-list module shows, the records are in those tables, NOT in the module's vars. Use list_table_records / create_or_update_record against them. Empty list means nothing to look up elsewhere.
|
||||||
- moduleId, sectionId
|
- moduleId, sectionId
|
||||||
|
|
||||||
Required params:
|
Required params:
|
||||||
|
|||||||
Reference in New Issue
Block a user