From c5852f3d673987109a62499bb712711b17732d56 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 24 Nov 2025 20:35:43 -0600 Subject: [PATCH 1/6] Refactor CanvasDrawer component and integrate CanvasDrawerPanel for improved UI. Remove CanvasDrawerButton and update related imports. Simplify state management for grouped shapes in Tldraw component. --- .../src/components/canvas/CanvasDrawer.tsx | 125 ++---------------- .../components/canvas/CanvasDrawerButton.tsx | 27 ---- apps/roam/src/components/canvas/Tldraw.tsx | 112 +++++++++++++++- .../src/components/canvas/uiOverrides.tsx | 11 -- .../utils/registerCommandPaletteCommands.ts | 2 - 5 files changed, 114 insertions(+), 163 deletions(-) delete mode 100644 apps/roam/src/components/canvas/CanvasDrawerButton.tsx diff --git a/apps/roam/src/components/canvas/CanvasDrawer.tsx b/apps/roam/src/components/canvas/CanvasDrawer.tsx index f1c4b6d35..67a302671 100644 --- a/apps/roam/src/components/canvas/CanvasDrawer.tsx +++ b/apps/roam/src/components/canvas/CanvasDrawer.tsx @@ -1,12 +1,4 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import ResizableDrawer from "~/components/ResizableDrawer"; -import renderOverlay from "roamjs-components/util/renderOverlay"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Button, Card, @@ -24,11 +16,7 @@ import { } from "@blueprintjs/core"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; -import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid"; -import getBlockProps from "~/utils/getBlockProps"; -import { TLBaseShape } from "tldraw"; import { DiscourseNodeShape } from "./DiscourseNodeUtil"; -import { render as renderToast } from "roamjs-components/components/Toast"; import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; export type GroupedShapes = Record; @@ -42,36 +30,9 @@ type NodeGroup = { isDuplicate: boolean; }; -// Module-level ref holder set by the provider -// This allows openCanvasDrawer to be called from non-React contexts -// (command palette, context menus, etc.) -let drawerUnmountRef: React.MutableRefObject<(() => void) | null> | null = null; - -export const CanvasDrawerProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const unmountRef = useRef<(() => void) | null>(null); - - useEffect(() => { - drawerUnmountRef = unmountRef; - - return () => { - if (unmountRef.current) { - unmountRef.current(); - unmountRef.current = null; - } - drawerUnmountRef = null; - }; - }, []); - - return <>{children}; -}; - type Props = { groupedShapes: GroupedShapes; pageUid: string }; -const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => { +export const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => { const [openSections, setOpenSections] = useState>({}); const [activeShapeId, setActiveShapeId] = useState(null); const [filterType, setFilterType] = useState("All"); @@ -328,8 +289,8 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => { ); return ( -
- +
+ { } /> ) : ( - + {visibleGroups.map((group) => renderListView(group))} )}
); }; - -const CanvasDrawer = ({ - onClose, - unmountRef, - ...props -}: { - onClose: () => void; - unmountRef: React.MutableRefObject<(() => void) | null>; -} & Props) => { - const handleClose = () => { - unmountRef.current = null; - onClose(); - }; - - return ( - - - - ); -}; - -export const openCanvasDrawer = (): void => { - if (!drawerUnmountRef) { - renderToast({ - id: "canvas-drawer-not-found", - content: - "Unable to open Canvas Drawer. Please load canvas in main window first.", - intent: "warning", - }); - console.error( - "CanvasDrawer: Cannot open drawer - CanvasDrawerProvider not found", - ); - return; - } - - if (drawerUnmountRef.current) { - drawerUnmountRef.current(); - drawerUnmountRef.current = null; - return; - } - - const pageUid = getCurrentPageUid(); - const props = getBlockProps(pageUid) as Record; - const rjsqb = props["roamjs-query-builder"] as Record; - const tldraw = (rjsqb?.tldraw as Record) || {}; - const store = (tldraw?.["store"] as Record) || {}; - const shapes = Object.values(store).filter((s) => { - const shape = s as TLBaseShape; - const uid = shape.props?.uid; - return !!uid; - }) as DiscourseNodeShape[]; - - const groupShapesByUid = (shapes: DiscourseNodeShape[]) => { - const groupedShapes = shapes.reduce((acc: GroupedShapes, shape) => { - const uid = shape.props.uid; - if (!acc[uid]) acc[uid] = []; - acc[uid].push(shape); - return acc; - }, {}); - - return groupedShapes; - }; - - const groupedShapes = groupShapesByUid(shapes); - drawerUnmountRef.current = - renderOverlay({ - // eslint-disable-next-line @typescript-eslint/naming-convention - Overlay: CanvasDrawer, - props: { groupedShapes, pageUid, unmountRef: drawerUnmountRef }, - }) || null; -}; - -export default CanvasDrawer; diff --git a/apps/roam/src/components/canvas/CanvasDrawerButton.tsx b/apps/roam/src/components/canvas/CanvasDrawerButton.tsx deleted file mode 100644 index d24516387..000000000 --- a/apps/roam/src/components/canvas/CanvasDrawerButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import { Button, Icon } from "@blueprintjs/core"; -import { openCanvasDrawer } from "./CanvasDrawer"; - -const CanvasDrawerButton = () => { - return ( -
-
- ); -}; - -export default CanvasDrawerButton; diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index 4dc7d66ac..f0cf1cd3f 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -84,9 +84,10 @@ import { import ConvertToDialog from "./ConvertToDialog"; import { createMigrations } from "./DiscourseRelationShape/discourseRelationMigrations"; import ToastListener, { dispatchToastEvent } from "./ToastListener"; -import CanvasDrawerButton from "./CanvasDrawerButton"; -import { CanvasDrawerProvider } from "./CanvasDrawer"; +import { CanvasDrawerContent, GroupedShapes } from "./CanvasDrawer"; import sendErrorEmail from "~/utils/sendErrorEmail"; +import { Button, Icon } from "@blueprintjs/core"; +import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid"; import { AUTO_CANVAS_RELATIONS_KEY } from "~/data/userSettings"; import { getSetting } from "~/utils/extensionSettings"; import { isPluginTimerReady, waitForPluginTimer } from "~/utils/pluginTimer"; @@ -133,6 +134,7 @@ const TldrawCanvas = ({ title }: { title: string }) => { ); const [isConvertToDialogOpen, setConvertToDialogOpen] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); const updateViewportScreenBounds = (el: HTMLDivElement) => { // Use tldraw's built-in viewport bounds update with centering @@ -707,9 +709,12 @@ const TldrawCanvas = ({ title }: { title: string }) => { allRelationIds={allRelationIds} allAddReferencedNodeActions={allAddReferencedNodeActions} /> + setIsDrawerOpen(!isDrawerOpen)} + /> - )}
@@ -956,6 +961,103 @@ const InsideEditorAndUiContext = ({ return ; }; +const CanvasDrawerPanel = ({ + isOpen, + onToggle, +}: { + isOpen: boolean; + onToggle: () => void; +}) => { + const editor = useEditor(); + const pageUid = getCurrentPageUid(); + const [groupedShapes, setGroupedShapes] = useState({}); + + useEffect(() => { + const updateGroupedShapes = () => { + const allRecords = editor.store.allRecords(); + const shapes = allRecords.filter((record) => { + if (record.typeName !== "shape") return false; + const shape = record as DiscourseNodeShape; + return !!shape.props?.uid; + }) as DiscourseNodeShape[]; + + const grouped = shapes.reduce((acc: GroupedShapes, shape) => { + const uid = shape.props.uid; + if (!acc[uid]) acc[uid] = []; + acc[uid].push(shape); + return acc; + }, {}); + + setGroupedShapes(grouped); + }; + + updateGroupedShapes(); + + const unsubscribe = editor.store.listen(() => { + updateGroupedShapes(); + }); + + return () => { + unsubscribe(); + }; + }, [editor.store]); + + return ( + <> +
+
+ {isOpen && ( +
+
+

+ Canvas Drawer +

+
+
+
+
+ +
+
+ )} + + ); +}; + const renderTldrawCanvasHelper = ({ title, onloadArgs, @@ -993,9 +1095,7 @@ const renderTldrawCanvasHelper = ({ const unmount = renderWithUnmount( - - - + , canvasWrapperEl, ); diff --git a/apps/roam/src/components/canvas/uiOverrides.tsx b/apps/roam/src/components/canvas/uiOverrides.tsx index 2b841b327..dd57c2533 100644 --- a/apps/roam/src/components/canvas/uiOverrides.tsx +++ b/apps/roam/src/components/canvas/uiOverrides.tsx @@ -41,7 +41,6 @@ import { DiscourseContextType } from "./Tldraw"; import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; import { COLOR_ARRAY } from "./DiscourseNodeUtil"; import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg"; -import { openCanvasDrawer } from "./CanvasDrawer"; import { AddReferencedNodeType } from "./DiscourseRelationShape/DiscourseRelationTool"; import { dispatchToastEvent } from "./ToastListener"; import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil"; @@ -185,16 +184,6 @@ export const CustomContextMenu = ({ return ( - {!selectedShape && ( - - - - )} {(isTextSelected || isImageSelected) && ( diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index ae61a4ae1..a963e0c3a 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -1,4 +1,3 @@ -import { openCanvasDrawer } from "~/components/canvas/CanvasDrawer"; import { openQueryDrawer } from "~/components/QueryDrawer"; import { render as exportRender } from "~/components/Export"; import { render as renderToast } from "roamjs-components/components/Toast"; @@ -145,7 +144,6 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { // Roam organizes commands by alphabetically addCommand("DG: Export - Current Page", exportCurrentPage); addCommand("DG: Export - Discourse Graph", exportDiscourseGraph); - addCommand("DG: Open - Canvas Drawer", openCanvasDrawer); addCommand("DG: Open - Discourse Settings", renderSettingsPopup); addCommand("DG: Open - Query Drawer", openQueryDrawerWithArgs); addCommand("DG: Query Block - Create", createQueryBlock); From 362cbb99ff276319dd4924d0c101c125e26b0b7a Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 24 Nov 2025 20:51:24 -0600 Subject: [PATCH 2/6] Refactor CanvasDrawerPanel to enhance state management and UI integration. Move grouped shapes logic from Tldraw to CanvasDrawerPanel, simplifying component structure and improving maintainability. --- .../src/components/canvas/CanvasDrawer.tsx | 103 ++++++++++++++++++ apps/roam/src/components/canvas/Tldraw.tsx | 94 +--------------- 2 files changed, 107 insertions(+), 90 deletions(-) diff --git a/apps/roam/src/components/canvas/CanvasDrawer.tsx b/apps/roam/src/components/canvas/CanvasDrawer.tsx index 67a302671..aec6c9f9e 100644 --- a/apps/roam/src/components/canvas/CanvasDrawer.tsx +++ b/apps/roam/src/components/canvas/CanvasDrawer.tsx @@ -3,6 +3,7 @@ import { Button, Card, Collapse, + Icon, Menu, MenuItem, NonIdealState, @@ -14,7 +15,9 @@ import { Tag, Tooltip, } from "@blueprintjs/core"; +import { Editor } from "tldraw"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; +import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; import { DiscourseNodeShape } from "./DiscourseNodeUtil"; import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; @@ -372,3 +375,103 @@ export const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => { ); }; + +type CanvasDrawerPanelProps = { + editor: Editor; + isOpen: boolean; + onToggle: () => void; +}; + +export const CanvasDrawerPanel = ({ + editor, + isOpen, + onToggle, +}: CanvasDrawerPanelProps) => { + const pageUid = getCurrentPageUid(); + const [groupedShapes, setGroupedShapes] = useState({}); + + useEffect(() => { + const updateGroupedShapes = () => { + const allRecords = editor.store.allRecords(); + const shapes = allRecords.filter((record) => { + if (record.typeName !== "shape") return false; + const shape = record as DiscourseNodeShape; + return !!shape.props?.uid; + }) as DiscourseNodeShape[]; + + const grouped = shapes.reduce((acc: GroupedShapes, shape) => { + const uid = shape.props.uid; + if (!acc[uid]) acc[uid] = []; + acc[uid].push(shape); + return acc; + }, {}); + + setGroupedShapes(grouped); + }; + + updateGroupedShapes(); + + const unsubscribe = editor.store.listen(() => { + updateGroupedShapes(); + }); + + return () => { + unsubscribe(); + }; + }, [editor.store]); + + return ( + <> +
+
+ {isOpen && ( +
+
+

+ Canvas Drawer +

+
+
+
+
+ +
+
+ )} + + ); +}; diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index f0cf1cd3f..f5324d2d6 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -84,10 +84,8 @@ import { import ConvertToDialog from "./ConvertToDialog"; import { createMigrations } from "./DiscourseRelationShape/discourseRelationMigrations"; import ToastListener, { dispatchToastEvent } from "./ToastListener"; -import { CanvasDrawerContent, GroupedShapes } from "./CanvasDrawer"; +import { CanvasDrawerPanel } from "./CanvasDrawer"; import sendErrorEmail from "~/utils/sendErrorEmail"; -import { Button, Icon } from "@blueprintjs/core"; -import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid"; import { AUTO_CANVAS_RELATIONS_KEY } from "~/data/userSettings"; import { getSetting } from "~/utils/extensionSettings"; import { isPluginTimerReady, waitForPluginTimer } from "~/utils/pluginTimer"; @@ -709,7 +707,7 @@ const TldrawCanvas = ({ title }: { title: string }) => { allRelationIds={allRelationIds} allAddReferencedNodeActions={allAddReferencedNodeActions} /> - setIsDrawerOpen(!isDrawerOpen)} /> @@ -961,7 +959,7 @@ const InsideEditorAndUiContext = ({ return ; }; -const CanvasDrawerPanel = ({ +const CanvasDrawerPanelWrapper = ({ isOpen, onToggle, }: { @@ -969,92 +967,8 @@ const CanvasDrawerPanel = ({ onToggle: () => void; }) => { const editor = useEditor(); - const pageUid = getCurrentPageUid(); - const [groupedShapes, setGroupedShapes] = useState({}); - - useEffect(() => { - const updateGroupedShapes = () => { - const allRecords = editor.store.allRecords(); - const shapes = allRecords.filter((record) => { - if (record.typeName !== "shape") return false; - const shape = record as DiscourseNodeShape; - return !!shape.props?.uid; - }) as DiscourseNodeShape[]; - - const grouped = shapes.reduce((acc: GroupedShapes, shape) => { - const uid = shape.props.uid; - if (!acc[uid]) acc[uid] = []; - acc[uid].push(shape); - return acc; - }, {}); - - setGroupedShapes(grouped); - }; - - updateGroupedShapes(); - - const unsubscribe = editor.store.listen(() => { - updateGroupedShapes(); - }); - - return () => { - unsubscribe(); - }; - }, [editor.store]); - return ( - <> -
-
- {isOpen && ( -
-
-

- Canvas Drawer -

-
-
-
-
- -
-
- )} - + ); }; From d6973c60c3fb00f05040d91bf2de40077cda5aa6 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 24 Nov 2025 21:24:12 -0600 Subject: [PATCH 3/6] Refactor CanvasDrawerPanel to utilize useEditor hook and streamline state management. Remove unnecessary props and simplify integration in Tldraw component. --- .../src/components/canvas/CanvasDrawer.tsx | 23 ++++++++----------- apps/roam/src/components/canvas/Tldraw.tsx | 19 +++------------ 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/apps/roam/src/components/canvas/CanvasDrawer.tsx b/apps/roam/src/components/canvas/CanvasDrawer.tsx index aec6c9f9e..0cbb23b05 100644 --- a/apps/roam/src/components/canvas/CanvasDrawer.tsx +++ b/apps/roam/src/components/canvas/CanvasDrawer.tsx @@ -15,7 +15,7 @@ import { Tag, Tooltip, } from "@blueprintjs/core"; -import { Editor } from "tldraw"; +import { Editor, useEditor } from "tldraw"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; @@ -376,17 +376,12 @@ export const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => { ); }; -type CanvasDrawerPanelProps = { - editor: Editor; - isOpen: boolean; - onToggle: () => void; -}; - -export const CanvasDrawerPanel = ({ - editor, - isOpen, - onToggle, -}: CanvasDrawerPanelProps) => { +export const CanvasDrawerPanel = () => { + const editor = useEditor(); + const toggleDrawer = useCallback(() => { + setIsOpen((prev) => !prev); + }, []); + const [isOpen, setIsOpen] = useState(false); const pageUid = getCurrentPageUid(); const [groupedShapes, setGroupedShapes] = useState({}); @@ -434,7 +429,7 @@ export const CanvasDrawerPanel = ({ >