Skip to content

Commit ecbbe46

Browse files
authored
ENG-994 - move canvas drawer into tldraw component (#568)
* Refactor CanvasDrawer component and integrate CanvasDrawerPanel for improved UI. Remove CanvasDrawerButton and update related imports. Simplify state management for grouped shapes in Tldraw component. * Refactor CanvasDrawerPanel to enhance state management and UI integration. Move grouped shapes logic from Tldraw to CanvasDrawerPanel, simplifying component structure and improving maintainability. * Refactor CanvasDrawerPanel to utilize useEditor hook and streamline state management. Remove unnecessary props and simplify integration in Tldraw component. * Enhance CanvasDrawer to utilize editor prop for improved camera movement functionality. Refactor moveCameraToShape logic and update CanvasDrawerPanel to pass editor instance, streamlining component interactions. * Refactor CanvasDrawer to remove Card components, replacing them with divs for improved layout consistency. Adjust styles and structure in CanvasDrawerPanel for better responsiveness and visual clarity. * .
1 parent e3e7fc5 commit ecbbe46

File tree

5 files changed

+134
-190
lines changed

5 files changed

+134
-190
lines changed

apps/roam/src/components/canvas/CanvasDrawer.tsx

Lines changed: 131 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
import React, {
2-
useCallback,
3-
useEffect,
4-
useMemo,
5-
useRef,
6-
useState,
7-
} from "react";
8-
import ResizableDrawer from "~/components/ResizableDrawer";
9-
import renderOverlay from "roamjs-components/util/renderOverlay";
1+
import React, { useCallback, useEffect, useMemo, useState } from "react";
102
import {
113
Button,
12-
Card,
134
Collapse,
5+
Icon,
146
Menu,
157
MenuItem,
168
NonIdealState,
@@ -22,13 +14,11 @@ import {
2214
Tag,
2315
Tooltip,
2416
} from "@blueprintjs/core";
17+
import { Editor, useEditor, TLShapeId } from "tldraw";
2518
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
26-
import getDiscourseNodes from "~/utils/getDiscourseNodes";
2719
import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid";
28-
import getBlockProps from "~/utils/getBlockProps";
29-
import { TLBaseShape } from "tldraw";
20+
import getDiscourseNodes from "~/utils/getDiscourseNodes";
3021
import { DiscourseNodeShape } from "./DiscourseNodeUtil";
31-
import { render as renderToast } from "roamjs-components/components/Toast";
3222
import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings";
3323

3424
export type GroupedShapes = Record<string, DiscourseNodeShape[]>;
@@ -42,36 +32,17 @@ type NodeGroup = {
4232
isDuplicate: boolean;
4333
};
4434

45-
// Module-level ref holder set by the provider
46-
// This allows openCanvasDrawer to be called from non-React contexts
47-
// (command palette, context menus, etc.)
48-
let drawerUnmountRef: React.MutableRefObject<(() => void) | null> | null = null;
49-
50-
export const CanvasDrawerProvider = ({
51-
children,
52-
}: {
53-
children: React.ReactNode;
54-
}) => {
55-
const unmountRef = useRef<(() => void) | null>(null);
56-
57-
useEffect(() => {
58-
drawerUnmountRef = unmountRef;
59-
60-
return () => {
61-
if (unmountRef.current) {
62-
unmountRef.current();
63-
unmountRef.current = null;
64-
}
65-
drawerUnmountRef = null;
66-
};
67-
}, []);
68-
69-
return <>{children}</>;
35+
type Props = {
36+
groupedShapes: GroupedShapes;
37+
pageUid: string;
38+
editor: Editor;
7039
};
7140

72-
type Props = { groupedShapes: GroupedShapes; pageUid: string };
73-
74-
const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
41+
export const CanvasDrawerContent = ({
42+
groupedShapes,
43+
pageUid,
44+
editor,
45+
}: Props) => {
7546
const [openSections, setOpenSections] = useState<Record<string, boolean>>({});
7647
const [activeShapeId, setActiveShapeId] = useState<string | null>(null);
7748
const [filterType, setFilterType] = useState("All");
@@ -172,13 +143,19 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
172143
}));
173144
}, []);
174145

175-
const moveCameraToShape = useCallback((shapeId: string) => {
176-
document.dispatchEvent(
177-
new CustomEvent("roamjs:query-builder:action", {
178-
detail: { action: "move-camera-to-shape", shapeId },
179-
}),
180-
);
181-
}, []);
146+
const moveCameraToShape = useCallback(
147+
(shapeId: string) => {
148+
const shape = editor.getShape(shapeId as TLShapeId);
149+
if (!shape) {
150+
return;
151+
}
152+
const x = shape.x || 0;
153+
const y = shape.y || 0;
154+
editor.centerOnPoint({ x, y }, { animation: { duration: 200 } });
155+
editor.select(shapeId as TLShapeId);
156+
},
157+
[editor],
158+
);
182159

183160
const handleShapeSelection = useCallback(
184161
(shape: DiscourseNodeShape) => {
@@ -328,8 +305,8 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
328305
);
329306

330307
return (
331-
<div className="space-y-4">
332-
<Card elevation={1} className="space-y-3">
308+
<div className="flex h-full flex-col gap-3">
309+
<div className="flex-shrink-0 space-y-3">
333310
<Tabs
334311
id="canvas-drawer-tabs"
335312
selectedTabId={activeTabId}
@@ -377,7 +354,7 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
377354
</Tag>
378355
)}
379356
</div>
380-
</Card>
357+
</div>
381358

382359
{!visibleGroups.length ? (
383360
<NonIdealState
@@ -401,83 +378,117 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
401378
}
402379
/>
403380
) : (
404-
<Card elevation={1} className="divide-y divide-gray-300">
381+
<div className="min-h-0 flex-1 divide-y divide-gray-300 overflow-y-auto overflow-x-hidden">
405382
{visibleGroups.map((group) => renderListView(group))}
406-
</Card>
383+
</div>
407384
)}
408385
</div>
409386
);
410387
};
411388

412-
const CanvasDrawer = ({
413-
onClose,
414-
unmountRef,
415-
...props
416-
}: {
417-
onClose: () => void;
418-
unmountRef: React.MutableRefObject<(() => void) | null>;
419-
} & Props) => {
420-
const handleClose = () => {
421-
unmountRef.current = null;
422-
onClose();
423-
};
389+
export const CanvasDrawerPanel = () => {
390+
const editor = useEditor();
391+
const toggleDrawer = useCallback(() => {
392+
setIsOpen((prev) => !prev);
393+
}, []);
394+
const [isOpen, setIsOpen] = useState(false);
395+
const pageUid = getCurrentPageUid();
396+
const [groupedShapes, setGroupedShapes] = useState<GroupedShapes>({});
424397

425-
return (
426-
<ResizableDrawer onClose={handleClose} title={"Canvas Drawer"}>
427-
<CanvasDrawerContent {...props} />
428-
</ResizableDrawer>
429-
);
430-
};
398+
useEffect(() => {
399+
const updateGroupedShapes = () => {
400+
const allRecords = editor.store.allRecords();
401+
const shapes = allRecords.filter((record) => {
402+
if (record.typeName !== "shape") return false;
403+
const shape = record as DiscourseNodeShape;
404+
return !!shape.props?.uid;
405+
}) as DiscourseNodeShape[];
406+
407+
const grouped = shapes.reduce((acc: GroupedShapes, shape) => {
408+
const uid = shape.props.uid;
409+
if (!acc[uid]) acc[uid] = [];
410+
acc[uid].push(shape);
411+
return acc;
412+
}, {});
413+
414+
setGroupedShapes(grouped);
415+
};
416+
417+
updateGroupedShapes();
431418

432-
export const openCanvasDrawer = (): void => {
433-
if (!drawerUnmountRef) {
434-
renderToast({
435-
id: "canvas-drawer-not-found",
436-
content:
437-
"Unable to open Canvas Drawer. Please load canvas in main window first.",
438-
intent: "warning",
419+
const unsubscribe = editor.store.listen(() => {
420+
updateGroupedShapes();
439421
});
440-
console.error(
441-
"CanvasDrawer: Cannot open drawer - CanvasDrawerProvider not found",
442-
);
443-
return;
444-
}
445422

446-
if (drawerUnmountRef.current) {
447-
drawerUnmountRef.current();
448-
drawerUnmountRef.current = null;
449-
return;
450-
}
423+
return () => {
424+
unsubscribe();
425+
};
426+
}, [editor.store]);
451427

452-
const pageUid = getCurrentPageUid();
453-
const props = getBlockProps(pageUid) as Record<string, unknown>;
454-
const rjsqb = props["roamjs-query-builder"] as Record<string, unknown>;
455-
const tldraw = (rjsqb?.tldraw as Record<string, unknown>) || {};
456-
const store = (tldraw?.["store"] as Record<string, unknown>) || {};
457-
const shapes = Object.values(store).filter((s) => {
458-
const shape = s as TLBaseShape<string, { uid: string }>;
459-
const uid = shape.props?.uid;
460-
return !!uid;
461-
}) as DiscourseNodeShape[];
462-
463-
const groupShapesByUid = (shapes: DiscourseNodeShape[]) => {
464-
const groupedShapes = shapes.reduce((acc: GroupedShapes, shape) => {
465-
const uid = shape.props.uid;
466-
if (!acc[uid]) acc[uid] = [];
467-
acc[uid].push(shape);
468-
return acc;
469-
}, {});
470-
471-
return groupedShapes;
472-
};
473-
474-
const groupedShapes = groupShapesByUid(shapes);
475-
drawerUnmountRef.current =
476-
renderOverlay({
477-
// eslint-disable-next-line @typescript-eslint/naming-convention
478-
Overlay: CanvasDrawer,
479-
props: { groupedShapes, pageUid, unmountRef: drawerUnmountRef },
480-
}) || null;
428+
return (
429+
<>
430+
<div
431+
className={`pointer-events-auto absolute top-11 m-2 rounded-lg ${isOpen ? "hidden" : ""}`}
432+
style={{
433+
zIndex: 250,
434+
// copying tldraw var(--shadow-2)
435+
boxShadow:
436+
"0px 0px 2px hsl(0, 0%, 0%, 16%), 0px 2px 3px hsl(0, 0%, 0%, 24%), 0px 2px 6px hsl(0, 0%, 0%, 0.1), inset 0px 0px 0px 1px hsl(0, 0%, 100%)",
437+
backgroundColor: "white",
438+
}}
439+
>
440+
<Button
441+
icon={<Icon icon="add-column-left" />}
442+
onClick={toggleDrawer}
443+
minimal
444+
title="Toggle Canvas Drawer"
445+
/>
446+
</div>
447+
{isOpen && (
448+
<div
449+
className="pointer-events-auto absolute bottom-10 left-2 flex w-80 flex-col rounded-lg bg-white"
450+
style={{
451+
top: "3.25rem",
452+
height: "calc(100% - 50px)",
453+
454+
zIndex: 250,
455+
boxShadow:
456+
"0px 0px 2px hsl(0, 0%, 0%, 16%), 0px 2px 3px hsl(0, 0%, 0%, 24%), 0px 2px 6px hsl(0, 0%, 0%, 0.1), inset 0px 0px 0px 1px hsl(0, 0%, 100%)",
457+
}}
458+
>
459+
<div className="flex max-h-10 flex-shrink-0 items-center rounded-lg bg-white px-1">
460+
<div className="flex-shrink-0">
461+
<Button
462+
icon={<Icon icon="add-column-left" />}
463+
onClick={() => setIsOpen(false)}
464+
minimal
465+
/>
466+
</div>
467+
<h2 className="m-0 flex-1 border-b border-gray-300 pb-1 text-center text-sm font-semibold leading-tight">
468+
Canvas Drawer
469+
</h2>
470+
<div className="flex-shrink-0">
471+
<Button
472+
icon={<Icon icon="cross" />}
473+
onClick={() => setIsOpen(false)}
474+
minimal
475+
small
476+
className="h-6 min-h-0 p-1"
477+
/>
478+
</div>
479+
</div>
480+
<div
481+
className="flex min-h-0 flex-1 flex-col overflow-hidden p-4"
482+
style={{ borderTop: "1px solid hsl(0, 0%, 91%)" }}
483+
>
484+
<CanvasDrawerContent
485+
groupedShapes={groupedShapes}
486+
pageUid={pageUid}
487+
editor={editor}
488+
/>
489+
</div>
490+
</div>
491+
)}
492+
</>
493+
);
481494
};
482-
483-
export default CanvasDrawer;

apps/roam/src/components/canvas/CanvasDrawerButton.tsx

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)