feat(F-128): completed feature
This commit is contained in:
86
work/artifacts/F-128/architect.md
Normal file
86
work/artifacts/F-128/architect.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# F-128 — CMS plantillas: contenido por defecto editable
|
||||
|
||||
## Diagnóstico
|
||||
|
||||
En `/admin/cms`, el operador reporta que al pulsar "Editar" sobre una plantilla (Home, Productos, Categorías, Marcas), el formulario se abre con el campo **body vacío**, sin contenido para modificar.
|
||||
|
||||
Reproducción:
|
||||
1. `GET /api/cms/pages` → 6 páginas (about, contact, cookies, privacy, shipping, terms). **Ninguna** coincide con los slugs del array `TEMPLATES`.
|
||||
2. `TEMPLATES` está hardcoded con `home / products / categories / brands`.
|
||||
3. `openTemplate(tpl)` (cms/page.tsx:69):
|
||||
```ts
|
||||
const existing = items.find(p => p.slug === tpl.slug);
|
||||
if (existing) { openEdit(existing); return; }
|
||||
setEditing(null);
|
||||
setForm({ slug: tpl.slug, title: tpl.defaultTitle, body: '' }); // ← body vacío
|
||||
```
|
||||
4. Cuando NO existe CMS page para ese slug (caso actual para los 4 templates), `form.body` queda `''` y el botón Guardar está disabled (`!form.body`).
|
||||
|
||||
Resultado: el operador abre una plantilla → no puede editar nada porque no hay contenido previo. Tendría que crear el contenido desde cero (pero el botón Guardar está disabled por el body vacío, así que tampoco puede).
|
||||
|
||||
## Diseño
|
||||
|
||||
Añadir `defaultBody` a cada template (HTML por defecto, listo para Lexical) y pre-rellenar `form.body` cuando se abre una plantilla sin página existente.
|
||||
|
||||
### Cambios
|
||||
|
||||
`apps/admin/src/app/(dashboard)/cms/page.tsx`:
|
||||
|
||||
```diff
|
||||
const TEMPLATES = [
|
||||
- { slug: 'home', label: 'Home', route: '/', icon: '🏠',
|
||||
- defaultTitle: 'Bienvenido a Mercado de Vida',
|
||||
- hint: 'Sección de bienvenida destacada en la página de inicio.' },
|
||||
+ { slug: 'home', label: 'Home', route: '/', icon: '🏠',
|
||||
+ defaultTitle: 'Bienvenido a Mercado de Vida',
|
||||
+ hint: 'Sección de bienvenida destacada en la página de inicio.',
|
||||
+ defaultBody: '<h2>Bienvenido a Mercado de Vida</h2><p>Productos naturales, ecológicos y de comercio justo. Envío a toda la península en 24-48h.</p>' },
|
||||
{ slug: 'products', label: 'Productos', route: '/products', icon: '🛒',
|
||||
- defaultTitle: 'Nuestros productos',
|
||||
- hint: 'Texto introductorio del listado de productos.' },
|
||||
+ defaultTitle: 'Nuestros productos',
|
||||
+ hint: 'Texto introductorio del listado de productos.',
|
||||
+ defaultBody: '<p>Descubre nuestra selección de productos naturales y ecológicos. Filtra por categoría, marca o atributo (vegano, sin gluten, bio...).</p>' },
|
||||
{ slug: 'categories', label: 'Categorías', route: '/categories', icon: '🗂️',
|
||||
- defaultTitle: 'Nuestras categorías',
|
||||
- hint: 'Texto introductorio del listado de categorías.' },
|
||||
+ defaultTitle: 'Nuestras categorías',
|
||||
+ hint: 'Texto introductorio del listado de categorías.',
|
||||
+ defaultBody: '<p>Explora todas nuestras categorías: alimentación, cosmética, limpieza del hogar y más.</p>' },
|
||||
{ slug: 'brands', label: 'Marcas', route: '/brands', icon: '🏷️',
|
||||
- defaultTitle: 'Nuestras marcas',
|
||||
- hint: 'Texto introductorio del listado de marcas.' },
|
||||
+ defaultTitle: 'Nuestras marcas',
|
||||
+ hint: 'Texto introductorio del listado de marcas.',
|
||||
+ defaultBody: '<p>Trabajamos con marcas comprometidas con la producción ecológica y el comercio justo.</p>' },
|
||||
];
|
||||
```
|
||||
|
||||
`openTemplate`:
|
||||
```diff
|
||||
const openTemplate = (tpl: typeof TEMPLATES[number]) => {
|
||||
const existing = items.find(p => p.slug === tpl.slug);
|
||||
if (existing) { openEdit(existing); return; }
|
||||
setEditing(null);
|
||||
- setForm({ slug: tpl.slug, title: tpl.defaultTitle, body: '' });
|
||||
+ setForm({ slug: tpl.slug, title: tpl.defaultTitle, body: tpl.defaultBody });
|
||||
setShowForm(true);
|
||||
};
|
||||
```
|
||||
|
||||
### Por qué `defaultBody` y no crear la página automáticamente
|
||||
|
||||
- El operador todavía debe poder editar el contenido antes de guardar. Si auto-creamos la página con el contenido por defecto, cualquier apertura accidental del template ensucia la DB.
|
||||
- Con el body pre-rellenado, el botón Guardar se habilita (porque `form.body` ya no es vacío) y el operador puede ajustar el contenido y guardarlo cuando quiera.
|
||||
|
||||
### No tocar el backend
|
||||
|
||||
El backend ya devuelve el `body` correctamente. No requiere cambios ni migración.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Editar `apps/admin/src/app/(dashboard)/cms/page.tsx` — añadir `defaultBody` a cada template + actualizar `openTemplate`.
|
||||
2. `cd apps/admin && npx tsc --noEmit`.
|
||||
3. `cd apps/admin && npm run build`.
|
||||
4. Verificar manualmente (build OK suficiente — la lógica es trivial).
|
||||
5. Cerrar gates.
|
||||
39
work/artifacts/F-128/implementer.md
Normal file
39
work/artifacts/F-128/implementer.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# F-128 — CMS plantillas: contenido por defecto editable
|
||||
|
||||
## Cambios
|
||||
|
||||
### `apps/admin/src/app/(dashboard)/cms/page.tsx`
|
||||
- Añadido `defaultBody` (HTML) a cada uno de los 4 templates en el array `TEMPLATES`:
|
||||
- **home**: bienvenida + descripción de envío
|
||||
- **products**: intro sobre filtros y atributos
|
||||
- **categories**: intro sobre las categorías principales
|
||||
- **brands**: intro sobre las marcas
|
||||
- `openTemplate(tpl)` modificado: cuando no existe CMS page para el slug, ahora pre-rellena `form.body = tpl.defaultBody` (antes era `''`).
|
||||
|
||||
```diff
|
||||
const openTemplate = (tpl: typeof TEMPLATES[number]) => {
|
||||
const existing = items.find(p => p.slug === tpl.slug);
|
||||
if (existing) { openEdit(existing); return; }
|
||||
setEditing(null);
|
||||
- setForm({ slug: tpl.slug, title: tpl.defaultTitle, body: '' });
|
||||
+ setForm({ slug: tpl.slug, title: tpl.defaultTitle, body: tpl.defaultBody });
|
||||
setShowForm(true);
|
||||
};
|
||||
```
|
||||
|
||||
## Por qué `defaultBody` y no auto-crear la página
|
||||
|
||||
- El operador debe poder revisar/ajustar el contenido antes de guardarlo en DB.
|
||||
- Si auto-creáramos la página al abrir el template, cada apertura accidental ensucia la DB con contenido placeholder.
|
||||
- Con el body pre-rellenado, el botón Guardar se habilita (porque `!form.body` ya no es true) y el operador puede ajustar y guardar cuando quiera.
|
||||
|
||||
## Verificación
|
||||
|
||||
- `cd apps/admin && npx tsc --noEmit` → exit 0.
|
||||
- `cd apps/admin && NEXT_PUBLIC_API_URL=http://192.168.18.93:3000 npm run build` → exit 0.
|
||||
|
||||
## Notas
|
||||
|
||||
- Sin cambios en backend: el endpoint `/api/cms/pages` ya devuelve `body` correctamente para las páginas existentes (privacy, terms, etc.).
|
||||
- Las 6 páginas existentes (about, contact, cookies, privacy, shipping, terms) no se ven afectadas: siguen apareciendo en la lista inferior con su contenido real.
|
||||
- El operador debe reiniciar el admin (`./scripts/monolith.sh prod restart`) para desplegar el nuevo CMS.
|
||||
17
work/artifacts/F-128/leader-close.json
Normal file
17
work/artifacts/F-128/leader-close.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"verdict": "APPROVED",
|
||||
"agent": "leader",
|
||||
"feature_id": "F-128",
|
||||
"summary": "F-128 listo para commit. Build del admin regenerado.",
|
||||
"checks": [
|
||||
"reviewer.json APPROVED",
|
||||
"security.json APPROVED",
|
||||
"qa.json APPROVED",
|
||||
"implementer.md completo",
|
||||
"verify.sh verde",
|
||||
"1 archivo modificado: apps/admin/src/app/(dashboard)/cms/page.tsx"
|
||||
],
|
||||
"commit_message": "feat(F-128): completed feature",
|
||||
"next_step": "operador: ./scripts/monolith.sh prod restart",
|
||||
"closed_at": "2026-08-21T16:00:00Z"
|
||||
}
|
||||
21
work/artifacts/F-128/qa.json
Normal file
21
work/artifacts/F-128/qa.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"verdict": "APPROVED",
|
||||
"reviewer": "qa",
|
||||
"feature_id": "F-128",
|
||||
"summary": "Verificación: build OK, contenido por defecto presente en 4 templates.",
|
||||
"checks": [
|
||||
"TEMPLATES.home.defaultBody contiene H2 'Bienvenido a Mercado de Vida'",
|
||||
"TEMPLATES.products.defaultBody contiene texto introductorio",
|
||||
"TEMPLATES.categories.defaultBody contiene texto introductorio",
|
||||
"TEMPLATES.brands.defaultBody contiene texto introductorio",
|
||||
"openTemplate con existing page → openEdit (sin cambios)",
|
||||
"openTemplate sin existing page → form.body = tpl.defaultBody (NUEVO)",
|
||||
"tsc --noEmit exit 0",
|
||||
"npm run build exit 0"
|
||||
],
|
||||
"evidence_files": [
|
||||
"apps/admin/src/app/(dashboard)/cms/page.tsx"
|
||||
],
|
||||
"notes": "Tras restart, abrir Home/Productos/Categorías/Marcas sin página creada mostrará contenido por defecto listo para editar y guardar.",
|
||||
"reviewed_at": "2026-08-21T16:00:00Z"
|
||||
}
|
||||
17
work/artifacts/F-128/reviewer.json
Normal file
17
work/artifacts/F-128/reviewer.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"verdict": "APPROVED",
|
||||
"reviewer": "reviewer",
|
||||
"feature_id": "F-128",
|
||||
"summary": "Fix UI mínimo: pre-rellenar body con contenido por defecto al abrir templates sin página existente.",
|
||||
"checks": [
|
||||
"Cada uno de los 4 TEMPLATES tiene defaultBody HTML no vacío",
|
||||
"openTemplate pre-rellena form.body con defaultBody cuando no hay página existente",
|
||||
"Cuando SÍ hay página existente, openEdit usa p.body (sin cambios)",
|
||||
"Sin cambios en backend",
|
||||
"tsc --noEmit exit 0",
|
||||
"npm run build exit 0",
|
||||
"Botón Guardar ya no queda disabled por body vacío al abrir una plantilla nueva"
|
||||
],
|
||||
"notes": "El cambio evita el ciclo: abrir plantilla → editor vacío → botón disabled → no se puede guardar.",
|
||||
"reviewed_at": "2026-08-21T16:00:00Z"
|
||||
}
|
||||
14
work/artifacts/F-128/security.json
Normal file
14
work/artifacts/F-128/security.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"verdict": "APPROVED",
|
||||
"reviewer": "security",
|
||||
"feature_id": "F-128",
|
||||
"summary": "Cambio puramente UI: contenido estático por defecto. Sin impacto de seguridad.",
|
||||
"checks": [
|
||||
"Sin cambios en backend ni en endpoints",
|
||||
"Sin cambios en autenticación / autorización",
|
||||
"El HTML por defecto es estático, sin inputs del usuario — no hay vector XSS",
|
||||
"El contenido sigue siendo sanitizado por el LexicalEditor al guardar (no se introduce HTML arbitrario)"
|
||||
],
|
||||
"notes": "Riesgo de seguridad nulo.",
|
||||
"reviewed_at": "2026-08-21T16:00:00Z"
|
||||
}
|
||||
Reference in New Issue
Block a user