Compare commits

...

5 Commits

Author SHA1 Message Date
Jordan Diaz
2ad2a6f87b docs: data-field-show descubrible desde el summary y el cheat-sheet
La seccion ya estaba escrita, pero el agente no llegaba a ella. Dos motivos:

- El summary del frontmatter y el parrafo de entrada de 01-builder-fields
  enumeran lo que cubre el doc y no mencionaban ni el agrupado en pestanas ni
  la visibilidad condicional. Ese summary no es decorativo: entra en el texto
  que se embebe (title + summary + primeros 2000 chars) y es lo que se ve en
  list_docs, asi que una seccion en el char 10000 sin rastro arriba es
  invisible para la busqueda semantica y para el que decide que doc abrir.

- 11b-rules-cheat-sheet es el doc de consulta rapida y ya tenia la linea de
  data-field-group; sin la hermana de data-field-show, quien mirase ahi
  concluia que las pestanas no se pueden condicionar.

Se anade la linea al cheat-sheet con la gramatica minima y los dos errores
tipicos (se referencia el nombre de variable y no el label; `campo=` es vacio).
2026-08-20 18:00:16 +00:00
Jordan Diaz
2afc2cdc34 docs: data-field-show para condicionar campos y pestanas del panel de modulos
El doc cubria data-field-group (repartir vars en pestanas) pero no habia forma
documentada de condicionar que se ve: los modulos con modos excluyentes
ensenaban al usuario todos los campos de todos los modos a la vez.

Se documentan los dos atributos nuevos (data-field-show en el campo,
data-field-group-show en una var del grupo), que llegan al builder.json por el
mismo mecanismo generico que data-field-group, con la tabla de la gramatica y
un ejemplo de las dos formas de uso.

Dos avisos que ahorran el error tipico: el campo se referencia por su NOMBRE DE
VARIABLE y no por su label (los acentos se borran, no se transliteran: "Titulo"
es `ttulo`), y `campo=` significa vacio, que es la clave de la primera opcion de
todo `list`.
2026-08-20 17:49:45 +00:00
Jordan Diaz
4835ff9467 docs: checkbox fuera de los tipos del builder y colors a las listas
El commit anterior (49b52b0) anadio un aviso de que `checkbox` no existe como
data-field-type, pero lo dejo listado como tipo valido en cuatro sitios: la
tabla de tipos de 01-builder-fields, su propio frontmatter `summary`, la tabla
de la cheat-sheet 11b y la lista del glosario. El agente consulta esas tablas
antes que el cuerpo del documento, asi que la contradiccion seguia viva:
escribe `checkbox`, el parser lo deja con type indefinido y funciones.php
descarta la variable en silencio. Era el origen del bug de tipos fantasma.

- `checkbox` fuera de las cuatro listas de data-field-type. El aviso se
  reescribe para remitir a `list` de dos opciones.
- OJO: `checkbox` SI existe como tipo de campo de TABLA del CMS
  (server/handlers/schema.py:98 y :284). Se conservan intactas sus menciones en
  05-tables-and-fields, 04-pages-and-records y la tabla de formato de datos de
  11b:88, que son otro vocabulario. El aviso lo dice explicitamente para que no
  se vuelva a borrar por error.
- `colors` entra en la tabla de 01, en la de 11b y en el glosario: tenia
  seccion propia pero no aparecia en ninguna lista, asi que quien consultaba la
  chuleta no sabia que existia. `colorpicker` se anade tambien al parrafo de
  intro de 01, que lo omitia.
- `corners` y `ratio` se dejan fuera de las tablas a proposito (decision del
  usuario: por ahora no se promocionan).
2026-08-13 20:05:21 +00:00
Jordan Diaz
49b52b0f9f docs: colors/colorpicker/corners/ratio y aviso de que checkbox no existe
01-builder-fields.md documentaba `checkbox` y `colorpicker` como tipos validos
—y usaba colorpicker en un ejemplo— pero el parser no los reconocia. Verificado
contra el parser real: ambos salian con `type` indefinido, y en ese caso
funciones.php hace `continue` y **descarta la variable en silencio**. O sea que
el doc mandaba al agente a escribir campos que nunca llegaban al builder.json.

En cambio no documentaba `colors`, `corners` ni `ratio`, que si funcionan.

- Se documentan los cuatro tipos reales, con el formato de valor de cada uno.
  `colors` es una paleta de N colores en una sola variable (JSON de pares
  nombre/color, nombres declarados en data-field-colors); `colorpicker` es un
  color suelto (hex plano).
- `colorpicker` pasa a existir de verdad: se anade al parser y al compilador
  (companion en el plugin maestro). Forge ya tenia widget para ambos —
  ColorsField y ColorField— asi que solo faltaba la pieza del parser.
- `checkbox` se marca explicitamente como inexistente, con la alternativa
  (`list` de dos opciones).
- set_module_example_data documenta el formato de ambos, que el agente no podia
  adivinar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:50:03 +00:00
Jordan Diaz
76221ee1d4 refactor: set_module_example_data escribe via el server Python
La tool llamaba a `action_ws=setStaticVars`, que hace un file_put_contents
del builder.json ENTERO en la web para cambiar una sola clave. Eso se
saltaba las dos garantias que protegen ese fichero: el bloqueo de escritura
de la API de ficheros (BLOCKED_GENERATED_FILENAMES en handlers/files.py) y
la allowlist de /api/modules/update-metadata.

El riesgo no era teorico. El builder.json es la unica memoria de que
variable vive en que columna de builder_custom: si una compilacion caia
entre la lectura y la escritura de setStaticVars, esta devolvia el mapeo
var->columna anterior encima del recien generado y el contenido guardado
quedaba colgado de la variable equivocada en TODAS las paginas que usan el
modulo.

Ahora delega en /api/modules/update-metadata, que ya es la via quirurgica
para la metadata y acepta staticVars en su allowlist (objeto, <=64KB,
<=200 claves).

Se aprovecha para quitar el volcado del schema y del payload completos por
consola en cada llamada.

Companion en el repo de Forge: compiler.js reenvia staticVars en cada
compilacion. Antes no lo mandaba, asi que el CMS lo tiraba al regenerar el
builder.json — solo 5 de 4870 modulos lo conservaban.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:21:16 +00:00
4 changed files with 133 additions and 36 deletions

View File

@@ -3,11 +3,11 @@ title: "Campos editables del builder"
tags: [builder, twig, html, modules]
load_priority: 80
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
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
@@ -39,7 +39,7 @@ Reglas obligatorias:
| `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) |
| `colors` | `<div data-field-colors="fondo,titulo">` | Objeto de N colores por nombre: `{{ colores.fondo }}` |
| `colorpicker` | `<div>` | Hex color string |
### textfield
@@ -210,13 +210,54 @@ Uso en Twig:
{% 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`)
@@ -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).
- 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
### `c-if` — Renderizado condicional

View File

@@ -3,7 +3,7 @@ title: "Reglas inmutables y cheat-sheet de tipos"
tags: [reference, rules, cheat]
load_priority: 90
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
@@ -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` (tabla) | `<div data-list-table="...">` | `num` del registro |
| `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 |
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
| Atributo | Uso | Ejemplo |

View File

@@ -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.
**`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** `==`.

View File

@@ -1,7 +1,17 @@
import { z } from "zod";
import { withAuth, getSessionCredentials, getApiClient, getCommonParams } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { withAuth } from "../../auth/index.js";
import { handleToolError, validateRequired } from "../helpers/errorHandler.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) {
server.tool(
@@ -10,6 +20,8 @@ export function registerSetModuleExampleDataTool(server) {
Reglas críticas:
- 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.
- 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).
@@ -50,42 +62,36 @@ Si dudas del formato exacto, lee 'read_doc({ name: "01-builder-fields" })'.`,
}
}
const credentials = await getSessionCredentials(extra.sessionId);
const client = await getApiClient(extra.sessionId);
console.error(`[set_module_example_data] Module ID: ${moduleId}, vars: ${Object.keys(exampleData).length}`);
// Log data for debugging
console.error(`[set_module_example_data] Module ID: ${moduleId}`);
console.error(`[set_module_example_data] Module Schema:`, JSON.stringify(moduleSchema, null, 2));
console.error(`[set_module_example_data] Example Data:`, JSON.stringify(exampleData, null, 2));
// Prepare payload for setStaticVars action
const payload = await getCommonParams(extra.sessionId, {
action_ws: "setStaticVars",
moduleId: moduleId,
const { projectSlug } = getCurrentProjectInfo();
const result = await pythonPost("/api/modules/update-metadata", {
project: projectSlug,
module: moduleId,
staticVars: exampleData,
schema: moduleSchema
});
console.error(`[set_module_example_data] Full Payload:`, JSON.stringify(payload, null, 2));
// Send to viewer_functions
const response = await client.post("/cms/lib/viewer_functions.php", payload);
console.error(`[set_module_example_data] Response:`, JSON.stringify(response.data, null, 2));
// Check for API errors in response
const apiError = handleApiResponse(response.data, 'set_module_example_data');
if (apiError) return apiError;
if (!result?.success) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: result?.error || "Could not set module example data",
}),
}],
isError: true,
};
}
return {
content: [{
type: "text", text: JSON.stringify({
success: true,
message: `Example data set successfully for module '${moduleId}'`,
moduleId: moduleId,
moduleId: result.module || moduleId,
dataCount: Object.keys(exampleData).length,
schemaVarsCount: moduleSchema?.codeVars ? Object.keys(moduleSchema.codeVars).length : 0,
response: response.data
}, null, 2)
}],
};