feat(canvas): Excalidraw read-only board + CanvasElement mapping (slices 1-2)#1407
Conversation
First slice of the tldraw -> Excalidraw migration (canvas engine = Excalidraw, CanvasElement stays canonical). element-to-excalidraw.ts is the pure view-side projection from a backend CanvasElement to an Excalidraw skeleton element (the convertToExcalidrawElements input shape), mirroring element-to-konva: - note -> coloured rectangle + label - link -> rectangle labelled with title/url - image -> image skeleton carrying the file id - text -> text element - mermaid/flowchart -> rectangle labelled with the first source line (the diagram-render slice converts the real source later) - mindmap_edge -> arrow bound to the from/to element ids - user_shape/unknown -> generic rectangle The skeleton types are a local faithful subset of ExcalidrawElementSkeleton, so this pure mapping carries no @excalidraw/excalidraw runtime dependency; the board slice wires the real package. 11 unit tests; malformed geometry coerces to defaults rather than crashing the renderer.
… (slice 2) Adds @excalidraw/excalidraw (MIT) and ExcalidrawBoard.tsx: a read-only (viewMode + zenMode) Excalidraw view that renders the canonical CanvasElement scene through element-to-excalidraw + convertToExcalidrawElements. Built alongside the tldraw board, not yet wired into CanvasView; write-back interactions, diagram rendering, and the swap that retires tldraw are later slices. Theme prop maps to Excalidraw light/dark. Smoke-tested with Excalidraw mocked (jsdom cannot run its canvas): asserts the scene is mapped, soft-deleted elements dropped, read-only set, theme threaded. Also fixes a strict-index (noUncheckedIndexedAccess) type error in the slice-1 note-background fallback.
|
Warning Review limit reached
More reviews will be available in 30 minutes and 52 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
| case "mindmap_edge": | ||
| return { | ||
| ...base, | ||
| type: "arrow", | ||
| start: { id: str(p.from) }, | ||
| end: { id: str(p.to) }, | ||
| }; |
There was a problem hiding this comment.
💡 Edge Case: Arrow bindings may reference missing/deleted element ids
mindmap_edge maps to an arrow with start: { id: str(p.from) } / end: { id: str(p.to) }. elementsToSkeletons filters out soft-deleted elements, but the arrow can still bind to a from/to id that was filtered out (soft-deleted) or never present in the scene. When the board is wired in, convertToExcalidrawElements resolves these bindings against the supplied elements and can warn/drop or otherwise misbehave when the referenced element is absent. Additionally, when p.from/p.to are missing, str() yields an empty-string id ({ id: "" }), which is never a valid binding target. Consider only emitting start/end when the referenced id is non-empty and present among the mapped elements (or omitting the binding otherwise). Low priority since the board is not yet wired into CanvasView.
Only bind arrow endpoints when the referenced id is non-empty.:
case "mindmap_edge": {
const from = str(p.from);
const to = str(p.to);
return {
...base,
type: "arrow",
...(from ? { start: { id: from } } : {}),
...(to ? { end: { id: to } } : {}),
};
}
Was this helpful? React with 👍 / 👎
|
Note Your trial team has used its Gitar budget, so automatic reviews are paused. Upgrade now to unlock full capacity. Comment "Gitar review" to trigger a review manually. Code Review 👍 Approved with suggestions 0 resolved / 1 findingsImplements the initial Excalidraw mapping and read-only board component, ensuring a clean migration path for CanvasElements. Ensure mindmap_edge logic accounts for potentially missing or deleted element IDs to prevent dangling arrow references. 💡 Edge Case: Arrow bindings may reference missing/deleted element ids📄 desktop/src/apps/ProjectsApp/canvas/element-to-excalidraw.ts:133-139 📄 desktop/src/apps/ProjectsApp/canvas/element-to-excalidraw.ts:148-153
Only bind arrow endpoints when the referenced id is non-empty.🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change:
Important Your trial ends in 4 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more. Was this helpful? React with 👍 / 👎 | Gitar |
Excalidraw opens at scroll origin, so a scene whose elements sit away from (0,0) rendered off-screen. Capture the imperative API and call scrollToContent with fitToContent once the API is ready and whenever the scene changes, so the read-only board shows the mapped CanvasElements. Verified visually via a local preview screenshot (notes/text/link/mermaid render).
Only emit an arrow start/end binding when its id is present. An empty-string id (missing from/to) is never a valid Excalidraw binding target; an arrow with no bindings renders as a free-floating line instead of misbehaving when the referenced element is absent or soft-deleted.
First two slices of the tldraw to Excalidraw migration (engine = Excalidraw, CanvasElement stays canonical with Excalidraw as the view). Supersedes #1406 (its mapping is included here, with a strict-index type fix).
Slice 1:
element-to-excalidraw.ts(pure mapping)Projects a backend CanvasElement to an Excalidraw skeleton element (the
convertToExcalidrawElementsinput), mirroringelement-to-konva: note→coloured rectangle+label, link→labelled rectangle, image→image skeleton, text→text, mermaid/flowchart→rectangle labelled with the first source line (real diagram render is a later slice), mindmap_edge→arrow bound to from/to ids, unknown→generic rectangle. Skeleton types are a local faithful subset, so the mapping carries no Excalidraw runtime dependency.Slice 2:
ExcalidrawBoard.tsx(read-only board)Adds
@excalidraw/excalidraw(MIT). A read-only (viewMode + zenMode) board that renders the mapped scene; theme prop maps to Excalidraw light/dark. Built alongside the tldraw board, not yet wired into CanvasView (write-back, diagram render, and the swap are later slices), so it ships as harmless until the swap slice.Tests
14 new (11 mapping unit tests + 3 mocked board smoke tests); full ProjectsApp suite 60 green;
tsc -bclean.Verification note
The board is not user-visible yet (not wired in). I will render it with sample data and screenshot on the Pi for visual review before the swap slice makes it default.