117 lines
5.3 KiB
Markdown
117 lines
5.3 KiB
Markdown
# 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](https://github.com/facebook/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)
|
|
``` |