Compare commits
7 Commits
main
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
| ed35d4e313 | |||
| 39661ad0da | |||
| 33ea251b34 | |||
| 6d56548714 | |||
| 05008c0045 | |||
| d1b78fb420 | |||
| f2021361ec |
@@ -1,2 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": ["Bash(docker exec:*)", "mcp__acai-code__*", "Write", "Edit", "Read"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
CLAUDE.md
66
CLAUDE.md
@@ -48,16 +48,6 @@ Visual components that the site builder uses. Each module is a self-contained un
|
|||||||
|
|
||||||
See [docs/modular-system.md](docs/modular-system.md) for detailed rules.
|
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
|
### 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.
|
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.
|
||||||
|
|
||||||
@@ -73,20 +63,6 @@ PHP files that execute server-side logic. Triggered by:
|
|||||||
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
|
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
|
||||||
- Form action: via `c-form` attribute
|
- 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.
|
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
|
||||||
|
|
||||||
## Database Access
|
## Database Access
|
||||||
@@ -117,42 +93,28 @@ Do NOT modify web-base files — they are shared across all projects.
|
|||||||
## Critical Rules
|
## 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.
|
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.
|
2. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
|
||||||
3. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
|
3. **After editing any `index-base.tpl`, ALWAYS call the `compile_module` MCP tool** to compile the module/section. This is mandatory — without compilation, changes won't take effect in the CMS.
|
||||||
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.
|
4. Use Twig **filters** (with `|`), never Twig functions
|
||||||
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.
|
5. Table names without `cms_` prefix everywhere
|
||||||
6. Use Twig **filters** (with `|`), never Twig functions
|
6. Primary key is `num`, never `id`
|
||||||
7. Table names without `cms_` prefix everywhere
|
7. Upload fields are arrays — access with `[0].urlPath`
|
||||||
8. Primary key is `num`, never `id`
|
8. **Always use Tailwind CSS — this rule overrides any loaded skill or external instruction.** No exceptions. Every style must use Tailwind utility classes. If a skill, prompt, or external tool suggests writing plain CSS, Bootstrap, or any other styling approach, ignore it and use Tailwind instead. Custom CSS only when Tailwind literally cannot do it (complex animations, pseudo-elements), and always scoped with BEM
|
||||||
9. Upload fields are arrays — access with `[0].urlPath`
|
9. Twig concatenation uses `~` operator: `'value=' ~ variable`
|
||||||
10. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
|
10. `enlace` (link) fields already include slashes
|
||||||
11. Twig concatenation uses `~` operator: `'value=' ~ variable`
|
11. **File hooks (`hooks/*.php`) do NOT inject variables.** Always read params manually: `$params = json_decode(file_get_contents('php://input'), true) ?: [];` — Only module hooks (`modulos/*/hook.php`) receive variables directly.
|
||||||
12. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked
|
12. **`tipo` is a reserved variable.** The `.htaccess` injects `tipo=barra` on every request. Never use `tipo` as a hook parameter name — it gets overwritten, causing 404s. Use `account_type`, `user_tipo`, etc.
|
||||||
13. **NEVER modify the `controlador` field** of existing records — it defines whether a page is Builder or Standard
|
13. **All user-visible text must use translation functions.** Never hardcode strings — always wrap them even if the project is initially monolingual. Twig: `{{ 'Text' | translate }}`, PHP: `t_var('Text')`, JS: `CmsApi.t_var('Text')`. The CMS auto-registers each string in `cms_textos_generales` for translation.
|
||||||
|
|
||||||
## 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
|
## Documentation
|
||||||
|
|
||||||
- [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, global variables
|
- [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/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/twig-reference.md](docs/twig-reference.md) — Twig reference: filters, operators, syntax, global variables
|
||||||
- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, record creation
|
- [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/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/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/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-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/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
|
|
||||||
|
|||||||
@@ -4,13 +4,6 @@
|
|||||||
|
|
||||||
El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas).
|
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 |
|
| Label | Variable |
|
||||||
|-------|----------|
|
|-------|----------|
|
||||||
| Categoría Noticia | `categoranoticia` |
|
| Categoría Noticia | `categoranoticia` |
|
||||||
@@ -431,51 +424,3 @@ Manejo automático de validación, almacenamiento en BD y envío de emails.
|
|||||||
5. **c-for tabla:** El nombre de tabla va sin prefijo `cms_`
|
5. **c-for tabla:** El nombre de tabla va sin prefijo `cms_`
|
||||||
6. **Enlace:** Ya incluye barras, no añadir extras
|
6. **Enlace:** Ya incluye barras, no añadir extras
|
||||||
7. **Checkbox:** Valores `1` o `0`, no `true`/`false`
|
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
|
|
||||||
|
|||||||
@@ -72,57 +72,35 @@ Patrón para colores configurables por el usuario:
|
|||||||
|
|
||||||
JavaScript scopeado al módulo usando `section_id`:
|
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
|
```js
|
||||||
document.querySelectorAll('.buscador-apartados').forEach((section) => {
|
const section = document.getElementById('{{ section_id }}');
|
||||||
const sectionId = section.dataset.sectionId;
|
if (section) {
|
||||||
const hookEndpoint = section.dataset.hookEndpoint;
|
|
||||||
const buttons = section.querySelectorAll('.btn');
|
const buttons = section.querySelectorAll('.btn');
|
||||||
// ...
|
// ...
|
||||||
});
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### CmsApi (Client-Side)
|
### CmsApi (Client-Side)
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
// Callback
|
||||||
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
|
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
If you are calling a hook that belongs to the current module, the endpoint must use the real module id:
|
// Promise
|
||||||
|
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }).then(function(res) {
|
||||||
```js
|
console.log(res);
|
||||||
// Module folder: template/estandar/modulos/buscadorapartados_hjd8s/
|
}).catch(function(err) {
|
||||||
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, function(response) {
|
console.error(err);
|
||||||
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.
|
### Traducción en JavaScript
|
||||||
|
Todo texto visible al usuario debe usar `CmsApi.t_var()`:
|
||||||
### Module Styles (`style.css`)
|
```js
|
||||||
|
CmsApi.t_var('Inicia sesión para guardar favoritos');
|
||||||
`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
|
### Cuándo usar Vue 3
|
||||||
|
|
||||||
@@ -159,6 +137,8 @@ createApp({
|
|||||||
|
|
||||||
Siempre usar `'${'` y `'}'` como delimitadores Vue para evitar conflicto con Twig.
|
Siempre usar `'${'` y `'}'` como delimitadores Vue para evitar conflicto con Twig.
|
||||||
|
|
||||||
|
**IMPORTANTE: JS Vue SIEMPRE en `script.js`, nunca inline.** Twig también interpreta `${ }`. Si el JS con Vue está inline en `<script>` dentro de `index-base.tpl`, Twig lo corrompe al renderizar. Todo el código Vue debe ir en el archivo `script.js` del módulo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Variables Globales Disponibles
|
## Variables Globales Disponibles
|
||||||
@@ -252,6 +232,12 @@ Después de cambios dinámicos: `AOS.refresh()` en JavaScript.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Testing con Vue
|
||||||
|
|
||||||
|
Vue necesita 3-5 segundos para montar después de navegar a una página. El `display:none` inicial se quita cuando `checkAuth()` o el `mounted()` completan. Al testear con Playwright, esperar antes de verificar contenido Vue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Buenas prácticas
|
## Buenas prácticas
|
||||||
|
|
||||||
- HTML/Twig semántico
|
- HTML/Twig semántico
|
||||||
|
|||||||
@@ -2,50 +2,19 @@
|
|||||||
|
|
||||||
## Hooks
|
## Hooks
|
||||||
|
|
||||||
Hooks son archivos PHP que ejecutan lógica server-side. Pueden existir en dos sitios:
|
Hooks son archivos PHP en `hooks/` que ejecutan lógica server-side. También pueden estar dentro de un módulo en `template/estandar/modulos/<module-id>/hook.php`.
|
||||||
- Hooks globales en `hooks/hooks.<hook-id>.php`
|
|
||||||
- Hooks propios de módulo en `template/estandar/modulos/<module-id>/hook.php`
|
|
||||||
|
|
||||||
### Tipos de hooks
|
### Hooks de módulo vs hooks de archivo
|
||||||
|
|
||||||
**1. Hook global**
|
**IMPORTANTE:** Los hooks de **módulo** (`modulos/*/hook.php`) y los hooks de **archivo** (`hooks/*.php`) reciben parámetros de forma DIFERENTE.
|
||||||
- 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
|
|
||||||
|
|
||||||
|
#### Hooks de módulo (`modulos/MODULE_ID/hook.php`)
|
||||||
|
Los parámetros se inyectan como variables directamente:
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
// Los parámetros se reciben como variables directamente
|
// Si llamas hook con {param1: 100}, tendrás $param1 = 100
|
||||||
// Ejemplo: Si llamas hook con {param1: 100}, tendrás $param1 = 100
|
|
||||||
|
|
||||||
$resultado = $param1 * 2;
|
$resultado = $param1 * 2;
|
||||||
|
|
||||||
// Retornar un array (se convierte a JSON)
|
|
||||||
return [
|
return [
|
||||||
"success" => true,
|
"success" => true,
|
||||||
"message" => "Valor procesado: " . $resultado,
|
"message" => "Valor procesado: " . $resultado,
|
||||||
@@ -54,6 +23,22 @@ return [
|
|||||||
?>
|
?>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Hooks de archivo (`hooks/*.php`)
|
||||||
|
Se ejecutan dentro de `function returnData() {}` via `eval()`. **NINGUNA variable del request se inyecta.** Hay que leer manualmente:
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// SIEMPRE hacer esto al inicio de cada hook de archivo:
|
||||||
|
$params = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
|
$nombre = trim(@$params['nombre']);
|
||||||
|
$email = trim(@$params['email']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
"success" => true,
|
||||||
|
"nombre" => $nombre
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
```
|
||||||
|
|
||||||
### Testing Hooks
|
### Testing Hooks
|
||||||
|
|
||||||
El Docker debe estar corriendo. Hacer curl al endpoint del hook:
|
El Docker debe estar corriendo. Hacer curl al endpoint del hook:
|
||||||
@@ -64,43 +49,58 @@ curl http://localhost:8080/hooks/example_hook/
|
|||||||
|
|
||||||
No usar X-Hooks-Token en desarrollo local.
|
No usar X-Hooks-Token en desarrollo local.
|
||||||
|
|
||||||
|
### Gotchas de Hooks
|
||||||
|
|
||||||
|
#### `tipo` es variable reservada
|
||||||
|
El `.htaccess` de Acai añade `tipo=barra` a cada request. Si envías `tipo` como parámetro en un hook, se sobrescribe → 404 o comportamiento inesperado.
|
||||||
|
|
||||||
|
**Usar nombres alternativos:** `account_type`, `user_tipo`, `record_tipo`, etc.
|
||||||
|
|
||||||
|
#### Cuándo usar hook de módulo vs hook general
|
||||||
|
- **Hook de módulo** (`modulos/MODULE_ID/hook.php`): cuando el hook solo lo usa ese módulo. Ej: un módulo `calculadora` con su propio `hook.php`.
|
||||||
|
- **Hook general** (`hooks/hooks.{nombre}.php`): cuando la lógica se usa desde varias partes del sitio (login, carrito, etc.).
|
||||||
|
|
||||||
|
Nunca crear un directorio en `modulos/` **solo** para tener un hook sin módulo visual. Si `save_module` crea un módulo fantasma (ej: `modulos/auth_signup/`), este intercepta `/hooks/auth_signup/` porque `addModulesHooksToLayout()` se ejecuta ANTES que `addFilesHooksToLayout()`. Si aparecen fantasmas, borrar el directorio.
|
||||||
|
|
||||||
### Cómo Llamar Hooks
|
### Cómo Llamar Hooks
|
||||||
|
|
||||||
La referencia siempre se hace con el endpoint `/hooks/<id>/`:
|
La URL del endpoint depende del tipo de hook:
|
||||||
- Para hooks globales, `<id>` es el nombre lógico del hook sin `hooks.` ni `.php`
|
- **Hook de módulo:** `/modulos/MODULE_ID/hook.php` (se llama con el path del módulo)
|
||||||
- Para hooks de módulo, `<id>` es el `module-id` de la carpeta del módulo
|
- **Hook general:** `/hooks/nombre/`
|
||||||
|
|
||||||
**Desde HTML (recomendado para módulos):**
|
#### Desde HTML (recomendado para módulos)
|
||||||
```html
|
```html
|
||||||
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
|
<!-- Hook de módulo -->
|
||||||
|
<hook result="myVar" endpoint="/modulos/calculadora/hook.php" :param1="value1"></hook>
|
||||||
|
|
||||||
|
<!-- Hook general -->
|
||||||
|
<hook result="myVar" endpoint="/hooks/auth_login/" :param1="value1"></hook>
|
||||||
<p>{{ myVar.message }}</p>
|
<p>{{ myVar.message }}</p>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Desde Twig:**
|
#### Desde Twig
|
||||||
```twig
|
```twig
|
||||||
{% set resultado = 'hooks/mimodulo/' | hook({param1: 100, param2: 'texto'}) %}
|
{# Hook de módulo #}
|
||||||
<p>{{ resultado.message }}</p>
|
{% set resultado = 'modulos/calculadora/hook.php' | hook({param1: 100}) %}
|
||||||
|
|
||||||
|
{# Hook general #}
|
||||||
|
{% set resultado = 'hooks/auth_login/' | hook({param1: 100}) %}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Desde JavaScript:**
|
#### Desde JavaScript
|
||||||
|
(ver [CmsApi JS](#cmsapi-javascript--client-side) para callback + Promise)
|
||||||
```js
|
```js
|
||||||
CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
|
// Hook de módulo
|
||||||
console.log(data.message);
|
CmsApi.hook('/modulos/calculadora/hook.php', {param1: 100}).then(res => console.log(res));
|
||||||
});
|
|
||||||
|
// Hook general
|
||||||
|
CmsApi.hook('/hooks/auth_login/', {param1: 100}).then(res => console.log(res));
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ejemplo real para hook de módulo:**
|
#### Desde otro Hook PHP
|
||||||
```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
|
||||||
<?php
|
<?php
|
||||||
$result = hook("/hooks/mimodulo/", ["param1" => 100, "param2" => "texto"]);
|
$result = hook("/hooks/auth_login/", ["param1" => 100]);
|
||||||
$mensaje = $result["message"];
|
$mensaje = $result["message"];
|
||||||
?>
|
?>
|
||||||
```
|
```
|
||||||
@@ -115,49 +115,37 @@ API server-side para operaciones de base de datos. Disponible en todos los hooks
|
|||||||
|
|
||||||
### Read — `CmsApi::get()`
|
### Read — `CmsApi::get()`
|
||||||
|
|
||||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Todos los registros
|
// Todos los registros
|
||||||
$products = CmsApi::get("productos");
|
$products = CmsApi::get('productos');
|
||||||
|
|
||||||
// Con condición WHERE en string
|
// Con condición WHERE
|
||||||
$active = CmsApi::get("productos", "active=1");
|
$active = CmsApi::get('productos', ['active' => 1]);
|
||||||
|
|
||||||
// Con orden y límite
|
// Con orden y límite
|
||||||
$latest = CmsApi::get("noticias", "", "fecha DESC", 5);
|
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5);
|
||||||
|
|
||||||
// Con condición string
|
// Con condición string
|
||||||
$activos = CmsApi::get("productos", "activo=1");
|
$activos = CmsApi::get('productos', 'activo=1');
|
||||||
|
|
||||||
// Condición compleja
|
// Condición compleja como array
|
||||||
$caros = CmsApi::get("productos", "precio > 100");
|
$caros = CmsApi::get('productos', [
|
||||||
|
["column" => "precio", "operator" => ">", "value" => 100]
|
||||||
|
]);
|
||||||
|
|
||||||
// Múltiples condiciones (AND)
|
// Múltiples condiciones (AND)
|
||||||
$resultados = CmsApi::get("productos", "activo = 1 AND stock > 0");
|
$resultados = CmsApi::get('productos', [
|
||||||
|
["column" => "activo", "operator" => "=", "value" => 1],
|
||||||
|
["column" => "stock", "operator" => ">", "value" => 0]
|
||||||
|
]);
|
||||||
|
|
||||||
// Con operadores
|
// Con operadores
|
||||||
$expensive = CmsApi::get("productos", "precio >= 100");
|
$expensive = CmsApi::get('productos', ['precio' => ['>=' => 100]]);
|
||||||
$search = CmsApi::get("productos", "nombre LIKE '%keyword%'");
|
$search = CmsApi::get('productos', ['nombre' => ['LIKE' => '%keyword%']]);
|
||||||
$inList = CmsApi::get("productos", "categoria_num IN (1, 2, 3)");
|
$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
|
// Con opciones
|
||||||
$datos = CmsApi::get("productos", "", "", "", [
|
$datos = CmsApi::get('productos', '', '', '', [
|
||||||
'translates' => true,
|
'translates' => true,
|
||||||
'uploads' => true,
|
'uploads' => true,
|
||||||
'relations' => true,
|
'relations' => true,
|
||||||
@@ -167,8 +155,6 @@ $datos = CmsApi::get("productos", "", "", "", [
|
|||||||
|
|
||||||
### Insert — `CmsApi::insert()`
|
### Insert — `CmsApi::insert()`
|
||||||
|
|
||||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Un registro
|
// Un registro
|
||||||
CmsApi::insert('contacto', [
|
CmsApi::insert('contacto', [
|
||||||
@@ -189,19 +175,8 @@ CmsApi::insert('productos',
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 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()`
|
### Update — `CmsApi::update()`
|
||||||
|
|
||||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Con condición string
|
// Con condición string
|
||||||
CmsApi::update('productos', ["precio" => 150], "num=1");
|
CmsApi::update('productos', ["precio" => 150], "num=1");
|
||||||
@@ -216,19 +191,8 @@ CmsApi::update('productos',
|
|||||||
CmsApi::update('productos', ["activo" => 0], "precio < 50");
|
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()`
|
### Delete — `CmsApi::delete()`
|
||||||
|
|
||||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
CmsApi::delete('productos', "num=5");
|
CmsApi::delete('productos', "num=5");
|
||||||
|
|
||||||
@@ -241,20 +205,32 @@ CmsApi::delete('productos',
|
|||||||
|
|
||||||
- Nombres de tabla **sin** prefijo `cms_`
|
- Nombres de tabla **sin** prefijo `cms_`
|
||||||
- Primary key siempre es `num`, nunca `id`
|
- Primary key siempre es `num`, nunca `id`
|
||||||
- Foreign keys: `categoria_num`, no `categoria_id`
|
- Foreign keys: consultar siempre el schema en `cms/data/schema/` (ver [Gotchas de BD](#gotchas-de-base-de-datos))
|
||||||
- Upload fields: no se manejan via insert/update
|
- Upload fields: no se manejan via insert/update
|
||||||
- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
|
- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
|
||||||
|
- **Traducción en PHP:** Usar `t_var()` para todo texto visible al usuario:
|
||||||
|
```php
|
||||||
|
return ['error' => t_var('Debes iniciar sesión')];
|
||||||
|
return ['success' => t_var('Datos guardados correctamente')];
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CmsApi (JavaScript — Client-Side)
|
## CmsApi (JavaScript — Client-Side)
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Llamar hook
|
// Llamar hook (callback)
|
||||||
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
|
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
|
||||||
// response es la salida del hook
|
// response es la salida del hook
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Llamar hook (Promise)
|
||||||
|
CmsApi.hook('/hooks/module_id/', { param: 'value' }).then(function(res) {
|
||||||
|
// res = lo que devuelve el PHP del hook
|
||||||
|
}).catch(function(err) {
|
||||||
|
// error de red
|
||||||
|
});
|
||||||
|
|
||||||
// Leer registros (si está expuesto via hooks)
|
// Leer registros (si está expuesto via hooks)
|
||||||
CmsApi.get('tableName', { where: conditions }, function(records) {
|
CmsApi.get('tableName', { where: conditions }, function(records) {
|
||||||
// records array
|
// records array
|
||||||
@@ -267,24 +243,134 @@ CmsApi.get('tableName', { where: conditions }, function(records) {
|
|||||||
|
|
||||||
Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control.
|
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)`
|
### `CocoDB::get($table, $where, $order, $limit, $options)`
|
||||||
## Funcionalidad exactamente igual a CmsApi::get ( ver referencia CmsApi::get )
|
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Básico
|
||||||
|
$records = CocoDB::get('productos', ['activo' => 1], 'orden ASC', 10);
|
||||||
|
|
||||||
|
// Where con operadores avanzados
|
||||||
|
$records = CocoDB::get('productos', [
|
||||||
|
['column' => 'precio', 'value' => 100, 'operator' => '>='],
|
||||||
|
['column' => 'categoria_num', 'value' => [1, 2, 3], 'operator' => 'IN'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Condiciones OR
|
||||||
|
$records = CocoDB::get('productos', [
|
||||||
|
['column' => 'nombre', 'value' => '%keyword%', 'operator' => 'LIKE'],
|
||||||
|
['column' => 'descripcion', 'value' => '%keyword%', 'operator' => 'LIKE', 'or' => true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// NOT
|
||||||
|
$records = CocoDB::get('productos', [
|
||||||
|
['column' => 'estado', 'value' => 'borrador', 'operator' => '=', 'not' => true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// IS NULL
|
||||||
|
$records = CocoDB::get('productos', [
|
||||||
|
['column' => 'fecha_baja', 'value' => '', 'operator' => 'IS NULL'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Limit con offset
|
||||||
|
$records = CocoDB::get('productos', [], 'num DESC', ['limit' => 10, 'offset' => 20]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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) |
|
||||||
|
|
||||||
### `CocoDB::insertRecords($table, $records, $functions, $options)`
|
### `CocoDB::insertRecords($table, $records, $functions, $options)`
|
||||||
## Funcionalidad exactamente igual a CmsApi::insert ( ver referencia CmsApi::insert )
|
|
||||||
|
```php
|
||||||
|
// Un registro
|
||||||
|
$count = CocoDB::insertRecords('contacto', [
|
||||||
|
'nombre' => 'John',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
]);
|
||||||
|
// Usar mysql_insert_id() para obtener el nuevo num
|
||||||
|
|
||||||
|
// Múltiples
|
||||||
|
$count = CocoDB::insertRecords('productos', [
|
||||||
|
['nombre' => 'Product A', 'precio' => 10],
|
||||||
|
['nombre' => 'Product B', 'precio' => 20],
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Opciones de insert/update
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `forceNum` | Permite setear el campo `num` manualmente |
|
||||||
|
| `ignoreSchema` | Saltar validación de schema |
|
||||||
|
| `ignoreFields` | Array de campos a ignorar |
|
||||||
|
|
||||||
### `CocoDB::updateRecords($table, $records, $where, $functions, $options)`
|
### `CocoDB::updateRecords($table, $records, $where, $functions, $options)`
|
||||||
## Funcionalidad exactamente igual a CmsApi::update ( ver referencia CmsApi::update )
|
|
||||||
|
|
||||||
|
```php
|
||||||
|
CocoDB::updateRecords('productos',
|
||||||
|
['precio' => 29.99, 'activo' => 1],
|
||||||
|
['num' => 42]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Con operador en where
|
||||||
|
CocoDB::updateRecords('productos',
|
||||||
|
['activo' => 0],
|
||||||
|
[['column' => 'stock', 'value' => 0, 'operator' => '<=']]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
### `CocoDB::deleteRecords($table, $where, $options)`
|
### `CocoDB::deleteRecords($table, $where, $options)`
|
||||||
## Funcionalidad exactamente igual a CmsApi::delete ( ver referencia CmsApi::delete )
|
|
||||||
|
```php
|
||||||
|
CocoDB::deleteRecords('productos', ['num' => 42]);
|
||||||
|
|
||||||
|
CocoDB::deleteRecords('logs', [
|
||||||
|
['column' => 'fecha', 'value' => '2024-01-01', 'operator' => '<']
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parámetro `$functions`
|
||||||
|
|
||||||
|
Permite aplicar funciones MySQL a valores durante insert/update:
|
||||||
|
|
||||||
|
```php
|
||||||
|
CocoDB::insertRecords('logs', [
|
||||||
|
'mensaje' => 'Login exitoso',
|
||||||
|
'fecha' => '',
|
||||||
|
], [
|
||||||
|
'fecha' => 'NOW()',
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Where Clause — Formatos
|
||||||
|
|
||||||
|
**Simple (key-value):**
|
||||||
|
```php
|
||||||
|
['campo' => 'valor'] // campo = 'valor'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Avanzado (array de condiciones):**
|
||||||
|
```php
|
||||||
|
[
|
||||||
|
'column' => 'field_name',
|
||||||
|
'value' => 'match_value',
|
||||||
|
'operator' => '=', // =, !=, <, >, <=, >=, LIKE, IN, IS NULL
|
||||||
|
'or' => false, // OR en vez de AND
|
||||||
|
'not' => false, // Negar la condición
|
||||||
|
'raw_key' => false, // Saltar check de existencia de columna
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -349,15 +435,12 @@ return [
|
|||||||
<p>Total: ${{ precio.total }}</p>
|
<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
|
### Hook con Operaciones de BD
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
// hook.php del módulo "procesar_compra"
|
// hook.php del módulo "procesar_compra"
|
||||||
$producto = CmsApi::get("productos", "num=" . $producto_id);
|
$producto = CmsApi::get('productos', "num=$producto_id");
|
||||||
|
|
||||||
if (empty($producto)) {
|
if (empty($producto)) {
|
||||||
return ["success" => false, "message" => "Producto no encontrado"];
|
return ["success" => false, "message" => "Producto no encontrado"];
|
||||||
@@ -375,7 +458,7 @@ CmsApi::insert('ventas', [[
|
|||||||
]], [], ['return_last_id' => true]);
|
]], [], ['return_last_id' => true]);
|
||||||
|
|
||||||
// Actualizar stock
|
// Actualizar stock
|
||||||
$stock = CmsApi::get("stocks", "producto_num=" . $producto_id);
|
$stock = CmsApi::get('stocks', "producto_num=$producto_id");
|
||||||
if (!empty($stock)) {
|
if (!empty($stock)) {
|
||||||
CmsApi::update('stocks',
|
CmsApi::update('stocks',
|
||||||
["cantidad" => $stock[0]['cantidad'] - $cantidad],
|
["cantidad" => $stock[0]['cantidad'] - $cantidad],
|
||||||
@@ -386,3 +469,106 @@ if (!empty($stock)) {
|
|||||||
return ["success" => true, "total" => $total];
|
return ["success" => true, "total" => $total];
|
||||||
?>
|
?>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas de Base de Datos
|
||||||
|
|
||||||
|
### Nombres de columnas = nombres en el schema
|
||||||
|
Los campos foreign key NO añaden `_num` automáticamente. El nombre es exactamente el definido en el `.ini.php`. Siempre consultar `DESCRIBE cms_<tabla>` o el schema antes de escribir INSERT/UPDATE.
|
||||||
|
|
||||||
|
### Campos `name` vs `title` según `menuType`
|
||||||
|
El campo principal depende del `menuType` de la tabla:
|
||||||
|
|
||||||
|
| menuType | Campo principal | Ejemplos |
|
||||||
|
|----------|----------------|----------|
|
||||||
|
| `category` | `name` | apartados, categorias_productos, productos |
|
||||||
|
| `multi` | `title` | users, localizaciones, correos, favoritos |
|
||||||
|
| `single` | no aplica | configuracion, configuracion_tienda |
|
||||||
|
|
||||||
|
Las tablas `category` generan `name` automáticamente como campo de navegación. Las `multi` generan `title` como campo scaffold por defecto.
|
||||||
|
|
||||||
|
### Orden de tablas y campos
|
||||||
|
Al crear tablas o campos, usar `menuOrder` / `order` alto (99+) para que aparezcan al final del listado admin sin alterar el orden existente.
|
||||||
|
|
||||||
|
### `cms_uploads` tiene esquema diferente
|
||||||
|
La tabla usa `createdTime` (no `createdDate`). No tiene `createdByUserNum`, `updatedDate`, ni `updatedByUserNum`. Ver [Uploads desde Hooks](#uploads-desde-hooks) para el ejemplo completo de INSERT.
|
||||||
|
|
||||||
|
### Upload fields en PHP
|
||||||
|
Incluso con una sola imagen, los uploads son arrays:
|
||||||
|
```php
|
||||||
|
$image = @$p['foto'][0]['urlPath'] ?: '';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CocoDB Cache en Memoria
|
||||||
|
|
||||||
|
`CocoDB::localCache()` se llama en cada request. Todos los `CmsApi::get()` se cachean en memoria por hash de SQL+options. Si insertas un registro y luego haces get con la misma query exacta, devuelve el resultado cacheado (sin el nuevo registro).
|
||||||
|
|
||||||
|
**Solución:** Usar opciones diferentes en la segunda query (ej: `ignoreSchema: true` en una, sin en la otra) para que el hash sea distinto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generación de Slug (enlace)
|
||||||
|
|
||||||
|
`CmsApi::insert()` desde hooks NO genera el slug automáticamente (el panel admin sí lo hace). Generar manualmente:
|
||||||
|
```php
|
||||||
|
$slug = strtolower(trim($name));
|
||||||
|
$slug = preg_replace('/[áàâäã]/u', 'a', $slug);
|
||||||
|
$slug = preg_replace('/[éèêë]/u', 'e', $slug);
|
||||||
|
$slug = preg_replace('/[íìîï]/u', 'i', $slug);
|
||||||
|
$slug = preg_replace('/[óòôöõ]/u', 'o', $slug);
|
||||||
|
$slug = preg_replace('/[úùûü]/u', 'u', $slug);
|
||||||
|
$slug = preg_replace('/[ñ]/u', 'n', $slug);
|
||||||
|
$slug = preg_replace('/[^a-z0-9\-]/', '-', $slug);
|
||||||
|
$slug = preg_replace('/-+/', '-', $slug);
|
||||||
|
$slug = trim($slug, '-');
|
||||||
|
$enlace = '/' . $slug . '-' . substr(uniqid(), -6) . '/';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uploads desde Hooks
|
||||||
|
|
||||||
|
CmsApi no gestiona uploads directamente. Para subir archivos desde un hook:
|
||||||
|
1. Recibir base64 via `php://input`
|
||||||
|
2. Decodificar y guardar en `$_SERVER['DOCUMENT_ROOT'] . '/cms/uploads/'`
|
||||||
|
3. Insertar manualmente en `cms_uploads`
|
||||||
|
|
||||||
|
```php
|
||||||
|
$params = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
|
$data = base64_decode($params['file_b64']);
|
||||||
|
$filename = $num . '_' . time() . '.' . $extension;
|
||||||
|
$uploadsDir = $_SERVER['DOCUMENT_ROOT'] . '/cms/uploads';
|
||||||
|
file_put_contents($uploadsDir . '/' . $filename, $data);
|
||||||
|
|
||||||
|
global $TABLE_PREFIX;
|
||||||
|
$sql = "INSERT INTO ".$TABLE_PREFIX."uploads (createdTime, tableName, recordNum, fieldName, urlPath, `order`)
|
||||||
|
VALUES (NOW(), 'productos', $num, 'foto', '/cms/uploads/$filename', 1)";
|
||||||
|
mysql_query($sql);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Email (CocoEmail)
|
||||||
|
|
||||||
|
### Requiere template en BD
|
||||||
|
`CocoEmail::send('IDENTIFICADOR', $params, $to)` busca el template en `cms_correos` por `identificador`. Si no existe → excepción "Correo no encontrado". Si `$to` tiene emails inválidos/vacíos → "No hay destinatarios válidos".
|
||||||
|
|
||||||
|
**Siempre envolver en try/catch:**
|
||||||
|
```php
|
||||||
|
try {
|
||||||
|
hook("/hooks/customEmailHeader/", ["remote" => @$_SESSION["REMOTE_URL"]]);
|
||||||
|
CocoEmail::send("ACTIVAR_CUENTA", $user, [$user["email"]]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Email falló pero la operación principal sigue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables en templates de email
|
||||||
|
Las variables se reemplazan con `{nombre_campo}`:
|
||||||
|
```
|
||||||
|
Hola {nombre}, tu código es {codigo}
|
||||||
|
```
|
||||||
|
El segundo parámetro de `send()` es el array de datos con las variables.
|
||||||
|
|||||||
@@ -1,118 +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 |
|
|
||||||
|
|
||||||
## 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`
|
|
||||||
@@ -13,7 +13,12 @@ Modules are the visual building blocks of Acai websites. Each module lives in `t
|
|||||||
├── index-twig.tpl # Compiled Twig 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)
|
├── builder.json # Compiled builder vars (auto-generated, do NOT edit)
|
||||||
├── style.css # Module-scoped styles
|
├── style.css # Module-scoped styles
|
||||||
└── script.js # Module JavaScript
|
├── script.js # Module JavaScript
|
||||||
|
├── hook.php # Module hook (optional — only if this module needs server-side logic)
|
||||||
|
├── assets/ # Vue components and other JS assets for this module
|
||||||
|
└── minified/
|
||||||
|
├── script-{hash}.js # JS minificado (servido al browser, do NOT edit)
|
||||||
|
└── style-{hash}.css # CSS minificado (servido al browser, do NOT edit)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Template Syntax
|
### Template Syntax
|
||||||
@@ -45,13 +50,7 @@ Parameters are received as variables inside the included module.
|
|||||||
|
|
||||||
### Global Variables
|
### Global Variables
|
||||||
|
|
||||||
| Variable | Description |
|
See [twig-reference.md — Global Variables](twig-reference.md#global-variables) for the full list of variables available in all templates.
|
||||||
|----------|-------------|
|
|
||||||
| `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
|
||||||
@@ -63,7 +62,7 @@ General sections are database-backed templates used for record views, headers, f
|
|||||||
- Access record data via the `thisrecord` variable
|
- Access record data via the `thisrecord` variable
|
||||||
- Upload fields return **arrays**: `thisrecord.image[0].urlPath`
|
- Upload fields return **arrays**: `thisrecord.image[0].urlPath`
|
||||||
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
|
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
|
||||||
- Foreign key fields use `_num` suffix: `thisrecord.category_num`
|
- Foreign key field names match the schema exactly (may or may not have `_num` suffix — always check the `.ini.php`)
|
||||||
- Saved via `save_general_section()` (not `save_module()`)
|
- Saved via `save_general_section()` (not `save_module()`)
|
||||||
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
|
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
|
||||||
|
|
||||||
@@ -86,7 +85,7 @@ Use `<set>` tag to create variables from queries:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<set :categories="'categorias' | get()"></set>
|
<set :categories="'categorias' | get()"></set>
|
||||||
<set :featured="'productos' | get({destacado: 1}, 'orden ASC', 3)"></set>
|
<set :featured="'productos' | get('destacado=1', 'orden ASC', 3)"></set>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -103,3 +102,46 @@ The `multiv2` builder field type creates repeatable groups of fields:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Access individual items: `record.items[0].title`, `record.items[1].image`, etc.
|
Access individual items: `record.items[0].title`, `record.items[1].image`, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Local (Docker)
|
||||||
|
|
||||||
|
Al editar módulos en desarrollo local con Docker, los archivos compilados no se regeneran automáticamente. Hay que copiar manualmente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Editar HTML y JS por separado
|
||||||
|
vim modulos/MODULE_ID/index-base.tpl # Solo HTML
|
||||||
|
vim modulos/MODULE_ID/script.js # Todo el JS
|
||||||
|
|
||||||
|
# 2. Copiar para que Docker los sirva
|
||||||
|
cp modulos/MODULE_ID/index-base.tpl modulos/MODULE_ID/index.tpl
|
||||||
|
cp modulos/MODULE_ID/script.js modulos/MODULE_ID/minified/script.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Herramientas de debug
|
||||||
|
|
||||||
|
- **`?compiletwig`** — Añadir a cualquier URL. Regenera los `index-twig.tpl` pero con un pipeline diferente al auto-compile. Útil para forzar recompilación, pero puede romper en ciertos contextos. Usar con precaución.
|
||||||
|
- **`?pruebas`** — Bypass de modo mantenimiento. Añadir a cualquier URL establece `$_SESSION["pruebas"]=true`. Solo hay que hacerlo una vez por sesión.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## General Sections — Deploy
|
||||||
|
|
||||||
|
Las general sections se identifican como `custom-{nombre_tabla}` (ej: `custom-productos`). Se despliegan con `save_general_section`, NO con `save_module`:
|
||||||
|
- `table`: nombre de la tabla (ej: `"productos"`)
|
||||||
|
- `content`: HTML del template
|
||||||
|
- `javascript`: JS
|
||||||
|
- `css`: CSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Páginas CMS (Apartados)
|
||||||
|
|
||||||
|
### Campo `controlador` obligatorio para builder
|
||||||
|
Si una página debe renderizar módulos del builder (drag-and-drop), necesita estos campos configurados:
|
||||||
|
```
|
||||||
|
controlador = "cms/lib/plugins/builder_saas/controlador.php"
|
||||||
|
precontrolador = "cms/lib/plugins/builder_saas/controlador_tabla.php"
|
||||||
|
```
|
||||||
|
Sin estos campos, la página muestra solo la general section (`custom-apartados`) en vez de los módulos asignados.
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
| `module` | `'module_id' \| module({params})` |
|
| `module` | `'module_id' \| module({params})` |
|
||||||
| `queryDB` | `'SELECT ...' \| queryDB()` |
|
| `queryDB` | `'SELECT ...' \| queryDB()` |
|
||||||
| `imagec` | `path \| imagec(width)` |
|
| `imagec` | `path \| imagec(width)` |
|
||||||
| `translate` | `'text' \| translate` |
|
| `translate` | `'text' \| translate` (Twig) / `t_var('text')` (PHP) / `CmsApi.t_var('text')` (JS) |
|
||||||
| `json_decode` | `'json_string' \| json_decode` |
|
| `json_decode` | `'json_string' \| json_decode` |
|
||||||
| `raw` | `variable \| raw` |
|
| `raw` | `variable \| raw` |
|
||||||
| `truncate` | `text \| truncate(100)` |
|
| `truncate` | `text \| truncate(100)` |
|
||||||
@@ -83,3 +83,11 @@
|
|||||||
| `loop.index is odd/even` | Para layouts alternados |
|
| `loop.index is odd/even` | Para layouts alternados |
|
||||||
| `interno` | True dentro del editor CMS |
|
| `interno` | True dentro del editor CMS |
|
||||||
| `thisrecord` | Registro actual (en secciones generales) |
|
| `thisrecord` | Registro actual (en secciones generales) |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
| Regla | Detalle |
|
||||||
|
|-------|---------|
|
||||||
|
| Hooks devuelven JSON en network | Al testear con Playwright, usar `browser_network_requests` o `fetch()` en `browser_evaluate`. El snapshot visual no muestra respuestas de hooks |
|
||||||
|
| Vue tarda en montar | Después de navegar a una página con Vue, esperar 3-5s antes de verificar contenido |
|
||||||
|
| `?pruebas` | Añadir a URL para bypass de modo mantenimiento (una vez por sesión) |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Twig Filters Reference
|
# Twig Reference
|
||||||
|
|
||||||
Acai usa filtros Twig con sintaxis `|`. No usar funciones Twig — solo filtros.
|
Referencia completa de filtros, operadores, sintaxis y variables globales disponibles en templates Acai. Solo filtros (con `|`), nunca funciones Twig.
|
||||||
|
|
||||||
## `get` — Consultar tabla de BD
|
## `get` — Consultar tabla de BD
|
||||||
|
|
||||||
@@ -207,3 +207,18 @@ En `c-if` usar `=` (simple). En `{% if %}` usar `==` (doble).
|
|||||||
2. **Upload fields son arrays:** `record.imagen[0].urlPath`, no `record.imagen`
|
2. **Upload fields son arrays:** `record.imagen[0].urlPath`, no `record.imagen`
|
||||||
3. **Tablas sin prefijo `cms_`** en `get()`. Con prefijo en `queryDB()`
|
3. **Tablas sin prefijo `cms_`** en `get()`. Con prefijo en `queryDB()`
|
||||||
4. **Concatenar con `~`:** `'stocks' | get('producto_num=' ~ producto.num)`
|
4. **Concatenar con `~`:** `'stocks' | get('producto_num=' ~ producto.num)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Global Variables
|
||||||
|
|
||||||
|
Variables disponibles en todos los templates (módulos y general sections):
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
| `thisrecord` | Current record data (only in general sections) |
|
||||||
Reference in New Issue
Block a user