Files
mercadodevida/work/artifacts/F-062/implementer.md
2026-08-19 17:02:14 +02:00

5.3 KiB

F-062 — Implementer evidence

Scope delivered

The CMS page editor in the admin (apps/admin/src/app/(dashboard)/cms/page.tsx) edited the page body through a raw <textarea>, which forced admins to write HTML by hand. The new editor is a real WYSIWYG powered by Lexical: bold / italic / underline, H2 / H3 / paragraph, ordered / unordered lists, inline links, undo / redo. The body continues to be stored as HTML on the existing cms_pages.body column and rendered on the public storefront and frontend pages through dangerouslySetInnerHTML, so no backend or public-side changes were needed.

Changes

Dependencies (apps/admin/package.json)

Added the Lexical stack at version 0.49.0:

lexical
@lexical/react
@lexical/rich-text
@lexical/list
@lexical/link
@lexical/history
@lexical/utils
@lexical/html
@lexical/markdown   # available for future export/import work

npm install added 35 transitive packages; the admin production chunk that ships the editor is now ~270 KB (minified).

New component — apps/admin/src/features/cms/components/LexicalEditor.tsx

A reusable client component built on top of @lexical/react:

  • LexicalComposer + RichTextPlugin + ContentEditable for the editing surface.
  • HistoryPlugin, ListPlugin, LinkPlugin for the obvious basics.
  • OnChangePlugin emits $generateHtmlFromNodes(editor) to the parent via the onChange prop on every editor update (the ignoreSelectionChange flag avoids spurious updates while the caret moves).
  • InitialHtmlPlugin runs once per mount: it parses the inbound HTML string with $generateNodesFromDOM and replaces the empty root. Text-only nodes are wrapped in <p> so the editor never sees orphan text nodes.
  • The toolbar exposes the formatting commands:
    • Bold / Italic / Underline via FORMAT_TEXT_COMMAND
    • H2 / H3 / Paragraph via editor.update() that swaps the selected block node (since ElementNode.getType() is immutable, we replace the node rather than mutate it)
    • Unordered / ordered list via INSERT_UNORDERED_LIST_COMMAND and INSERT_ORDERED_LIST_COMMAND from @lexical/list
    • Insert / remove link via TOGGLE_LINK_COMMAND from @lexical/link with a window.prompt URL dialog
  • Active formatting (bold / italic / underline) is reflected on the toolbar by reading the current DOM selection after every editor update.
  • Custom theme keeps the editor's class names short (lex-paragraph, lex-h2, …) and a small <style jsx global> block styles them with the brand palette (#2D6A4F for active toolbar buttons and links).

Integration — apps/admin/src/app/(dashboard)/cms/page.tsx

The <textarea> for the body field was replaced with <LexicalEditor value={form.body} onChange={...} />. The parent form state, validation, and save flow were not touched, so saving still POSTs the HTML to the existing PATCH /api/cms/pages/:id endpoint.

Acceptance traceability

Acceptance criterion How it is met
LexicalEditor renders in the CMS edit form with a working toolbar Component mounted in the form; toolbar dispatches the documented Lexical commands; bundle ships the editor (chunk includes MDVCmsEditor, lex-paragraph, FORMAT_TEXT_COMMAND).
Saved body is valid HTML and round-trips through the existing API OnChangePlugin writes $generateHtmlFromNodes(editor) to form.body. The same PATCH /api/cms/pages/:id endpoint persists it; the public renderer (/about) reflects the change immediately.
Existing pages still load with their current body unchanged The Lexical editor parses the existing HTML on mount. Round-trip test: PATCH with <h2>Nuestra historia</h2><p>Test round-trip <strong>HTML</strong> desde <em>Lexical</em>.</p><ul><li>Item 1</li><li>Item 2</li></ul>; /about served it back exactly. Body was then restored to the original content.
verify.sh is green Exit 0.

Manual verification

$ curl -X PATCH http://192.168.18.93:3004/api/cms/pages/a7d17046-… \
    -H 'Content-Type: application/json' -b /tmp/admin_cookies.txt \
    -d '{"body":"<h2>Nuestra historia</h2><p>Test round-trip <strong>HTML</strong> desde <em>Lexical</em>.</p><ul><li>Item 1</li><li>Item 2</li></ul>"}'
{ "id": "a7d17046-…", "body": "<h2>Nuestra historia</h2><p>Test round-trip …", … }

$ curl http://192.168.18.93:3003/about | grep -oE '<h2[^>]*>[^<]+</h2>|<ul><li>.*?</li></ul>'
<h2>Nuestra historia</h2>
<ul><li>Item 1</li><li>Item 2</li></ul>

Build verification

  • npx tsc --noEmit (admin) — exit 0
  • npx tsc --noEmit (frontend) — exit 0
  • npx tsc --noEmit (storefront) — exit 0
  • npm run typecheck (backend) — exit 0
  • npm run build (admin) — exit 0; CMS route is statically rendered
  • ./scripts/verify.sh — exit 0
  • Admin service restarted via monolith.sh prod restart admin → HTTP 200 on /cms
  • Lexical chunks present in .next/static/chunks/

Files touched

project/apps/admin/package.json                                       (added lexical deps)
project/apps/admin/package-lock.json                                  (npm install)
project/apps/admin/src/features/cms/components/LexicalEditor.tsx      (new)
project/apps/admin/src/app/(dashboard)/cms/page.tsx                  (textarea → LexicalEditor)