feat(F-062): completed feature
This commit is contained in:
117
work/artifacts/F-062/implementer.md
Normal file
117
work/artifacts/F-062/implementer.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 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)
|
||||
```
|
||||
Reference in New Issue
Block a user