Compare commits
7 Commits
main
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
| ed35d4e313 | |||
| 39661ad0da | |||
| 33ea251b34 | |||
| 6d56548714 | |||
| 05008c0045 | |||
| d1b78fb420 | |||
| f2021361ec |
@@ -99,18 +99,22 @@ Do NOT modify web-base files — they are shared across all projects.
|
|||||||
5. Table names without `cms_` prefix everywhere
|
5. Table names without `cms_` prefix everywhere
|
||||||
6. Primary key is `num`, never `id`
|
6. Primary key is `num`, never `id`
|
||||||
7. Upload fields are arrays — access with `[0].urlPath`
|
7. Upload fields are arrays — access with `[0].urlPath`
|
||||||
8. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
|
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. Twig concatenation uses `~` operator: `'value=' ~ variable`
|
9. Twig concatenation uses `~` operator: `'value=' ~ variable`
|
||||||
10. `enlace` (link) fields already include slashes
|
10. `enlace` (link) fields already include slashes
|
||||||
|
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. **`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. **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.
|
||||||
|
|
||||||
## 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.)
|
||||||
|
|
||||||
|
|||||||
@@ -83,9 +83,23 @@ if (section) {
|
|||||||
### 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Promise
|
||||||
|
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }).then(function(res) {
|
||||||
|
console.log(res);
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traducción en JavaScript
|
||||||
|
Todo texto visible al usuario debe usar `CmsApi.t_var()`:
|
||||||
|
```js
|
||||||
|
CmsApi.t_var('Inicia sesión para guardar favoritos');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cuándo usar Vue 3
|
### Cuándo usar Vue 3
|
||||||
@@ -123,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
|
||||||
@@ -216,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
|
||||||
|
|||||||
@@ -4,16 +4,17 @@
|
|||||||
|
|
||||||
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 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`.
|
||||||
|
|
||||||
### Estructura de un Hook
|
### Hooks de módulo vs hooks de archivo
|
||||||
|
|
||||||
|
**IMPORTANTE:** Los hooks de **módulo** (`modulos/*/hook.php`) y los hooks de **archivo** (`hooks/*.php`) reciben parámetros de forma DIFERENTE.
|
||||||
|
|
||||||
|
#### 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,
|
||||||
@@ -22,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:
|
||||||
@@ -32,31 +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
|
||||||
|
|
||||||
**Desde HTML (recomendado para módulos):**
|
La URL del endpoint depende del tipo de hook:
|
||||||
|
- **Hook de módulo:** `/modulos/MODULE_ID/hook.php` (se llama con el path del módulo)
|
||||||
|
- **Hook general:** `/hooks/nombre/`
|
||||||
|
|
||||||
|
#### 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));
|
||||||
```
|
```
|
||||||
|
|
||||||
**Desde otro Hook PHP:**
|
#### 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"];
|
||||||
?>
|
?>
|
||||||
```
|
```
|
||||||
@@ -161,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
|
||||||
@@ -413,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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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