4.3 KiB
F-064 — Implementer evidence
Scope delivered
The product description in the admin (ProductEditor) was edited through
a plain <textarea>, which forced admins to write raw HTML by hand, and
the public renderers treated product.description as a plain string.
Both frontend pages (/products/[slug]) and the storefront product page
(/productos/[slug]) now render the description as HTML, and the admin
uses the same Lexical editor that the CMS already uses (F-062).
Changes
Admin
project/apps/admin/src/features/products/components/ProductEditor.tsx
- Imported the Lexical editor from
@/features/cms/components/LexicalEditor. - Replaced the description
<textarea>with<LexicalEditor value={desc} onChange={setDesc} />. desccontinues to live in the existing form state, gets included in the save payload, and is sent toPATCH /api/products/:idexactly as before. The only difference is that what gets sent is now HTML produced by the WYSIWYG instead of raw text from the textarea.
Public renderers
project/frontend/src/app/products/[slug]/page.tsx
- <p className="text-gray-600 leading-relaxed">{product.description}</p>
+ <div
+ className="text-gray-600 leading-relaxed prose prose-sm max-w-none"
+ dangerouslySetInnerHTML={{ __html: product.description }}
+ />
project/storefront/src/app/productos/[slug]/page.tsx
- <p classNameName="text-lg leading-8 text-stone-700">
- {product.description ?? 'Producto del catálogo público de mercadodevida.'}
- </p>
+ <p classNameName="text-lg leading-8 text-stone-700">
+ {product.description ? (
+ <span dangerouslySetInnerHTML={{ __html: product.description }} />
+ ) : (
+ 'Producto del catálogo público de mercadodevida.'
+ )}
+ </p>
The fallback to a literal Spanish sentence is preserved on the storefront
when the description is empty; the frontend already had its own copy in
ContentPage.
Acceptance traceability
| Acceptance criterion | How it is met |
|---|---|
| Admin product editor description is the Lexical WYSIWYG with toolbar | ProductEditor.tsx imports LexicalEditor from @/features/cms/components and mounts it for the description field. |
| Saving the product persists the HTML body produced by the editor | The save payload already includes description; the editor writes HTML to desc on every change. |
| Public product pages render the description HTML safely | Both public renderers use dangerouslySetInnerHTML for the description. Plain-text descriptions render as a single text node, identical to the previous behaviour. |
| Existing plain text descriptions still display | product.description is still a string field on the backend; passing plain text through dangerouslySetInnerHTML is a no-op. |
verify.sh is green |
Exit 0. |
Manual verification
$ curl -X PATCH http://192.168.18.93:3004/api/products/13a65dc0-… \
-H 'Content-Type: application/json' -b /tmp/admin_cookies.txt \
-d '{"description":"<p>A great product with <strong>bold</strong> and <em>italic</em> text.</p><ul><li>Item 1</li><li>Item 2</li></ul>"}'
{ "description": "<p>A great product with <strong>bold</strong> …", … }
$ curl http://192.168.18.93:3003/products/proteina-guisante-ecologica
<div class="text-gray-600 leading-relaxed prose prose-sm max-w-none">
<p>A great product with <strong>bold</strong> and <em>italic</em> text.</p>
<ul><li>Item 1</li><li>Item 2</li></ul>
</div>
$ curl http://192.168.18.93:3005/productos/proteina-guisante-ecologica
<p class="text-lg leading-8 text-stone-700">
<span><p>A great product with …
The description was restored to the original plain text after the test.
Build verification
npx tsc --noEmit(admin / frontend / storefront) — exit 0npm run typecheck(backend) — exit 0- Admin build (
monolith.sh prod restart admin) →MDVCmsEditorchunk present in the new build - Admin
/products/<id>responds 200 ./scripts/verify.sh— exit 0
Files touched
project/apps/admin/src/features/products/components/ProductEditor.tsx (textarea → LexicalEditor)
project/frontend/src/app/products/[slug]/page.tsx (description → dangerouslySetInnerHTML)
project/storefront/src/app/productos/[slug]/page.tsx (description → dangerouslySetInnerHTML)