From 27923b75a19d38cc74a14fc2e35318a114d68e1f Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Wed, 18 Mar 2026 14:43:54 -0400 Subject: [PATCH 1/4] add filter search --- apps/roam/src/components/canvas/Clipboard.tsx | 213 +++++++++++++++--- 1 file changed, 178 insertions(+), 35 deletions(-) diff --git a/apps/roam/src/components/canvas/Clipboard.tsx b/apps/roam/src/components/canvas/Clipboard.tsx index d5254313f..4fc8588ff 100644 --- a/apps/roam/src/components/canvas/Clipboard.tsx +++ b/apps/roam/src/components/canvas/Clipboard.tsx @@ -14,7 +14,10 @@ import { Collapse, Dialog, Icon, + InputGroup, Intent, + Menu, + MenuItem, NonIdealState, Popover, Position, @@ -53,6 +56,9 @@ import { openBlockInSidebar, createBlock } from "roamjs-components/writes"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import findDiscourseNode from "~/utils/findDiscourseNode"; +import getDiscourseNodes, { + excludeDefaultNodes, +} from "~/utils/getDiscourseNodes"; import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg"; import { useExtensionAPI } from "roamjs-components/components/ExtensionApiContext"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; @@ -389,6 +395,7 @@ const AddPageModal = ({ isOpen, onClose, onConfirm }: AddPageModalProps) => { type NodeGroup = { uid: string; text: string; + type: string; shapes: DiscourseNodeShape[]; isDuplicate: boolean; }; @@ -422,10 +429,16 @@ const ClipboardPageSection = ({ page, onRemove, showNodesOnCanvas, + searchQuery, + sortDirection, + selectedNodeType, }: { page: ClipboardPage; onRemove: (uid: string) => void; showNodesOnCanvas: boolean; + searchQuery: string; + sortDirection: "asc" | "desc"; + selectedNodeType: string; }) => { const [isOpen, setIsOpen] = useState(true); const [discourseNodes, setDiscourseNodes] = useState< @@ -534,24 +547,48 @@ const ClipboardPageSection = ({ const groupedNodes = useMemo(() => { const groups: NodeGroup[] = discourseNodes.map((node) => { const shapes = shapesByUid.get(node.uid) ?? []; + const discourseNode = findDiscourseNode({ uid: node.uid }); return { uid: node.uid, text: node.text, + type: discourseNode ? discourseNode.text : "Unknown", shapes, isDuplicate: shapes.length > 1, }; }); - return groups.sort((a, b) => a.text.localeCompare(b.text)); + return groups; // eslint-disable-next-line react-hooks/exhaustive-deps }, [discourseNodes, shapesByUid]); const visibleGroupedNodes = useMemo( () => - groupedNodes.filter((group) => - showNodesOnCanvas ? true : group.shapes.length === 0, - ), - [groupedNodes, showNodesOnCanvas], + groupedNodes + .filter((group) => + showNodesOnCanvas ? true : group.shapes.length === 0, + ) + .filter((group) => + searchQuery + ? group.text.toLowerCase().includes(searchQuery.toLowerCase()) + : true, + ) + .filter((group) => + selectedNodeType && selectedNodeType !== "All" + ? group.type === selectedNodeType + : true, + ) + .sort((a, b) => + sortDirection === "asc" + ? a.text.localeCompare(b.text) + : b.text.localeCompare(a.text), + ), + [ + groupedNodes, + showNodesOnCanvas, + searchQuery, + selectedNodeType, + sortDirection, + ], ); useEffect(() => { @@ -949,8 +986,9 @@ const ClipboardPageSection = ({ ) : visibleGroupedNodes.length === 0 ? (
- All nodes from this page are already on canvas. Turn on "Show - nodes on canvas" to view them. + {searchQuery || selectedNodeType !== "All" + ? "No nodes match the current filters." + : 'All nodes from this page are already on canvas. Turn on "Show nodes on canvas" to view them.'}
) : (
@@ -1091,6 +1129,17 @@ export const ClipboardPanel = () => { } = useClipboard(); const [isModalOpen, setIsModalOpen] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [isSearchExpanded, setIsSearchExpanded] = useState(false); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const [selectedNodeType, setSelectedNodeType] = useState("All"); + + const availableNodeTypes = useMemo(() => { + const types = getDiscourseNodes().filter(excludeDefaultNodes); + return ["All", ...types.map((t) => t.text)]; + }, []); + + const hasActiveFilters = !!searchQuery || selectedNodeType !== "All"; if (!isOpen) return null; @@ -1119,7 +1168,7 @@ export const ClipboardPanel = () => {
{!isCollapsed && ( <> -
- e.stopPropagation()} - style={{ pointerEvents: "all" }} - > - - setShowNodesOnCanvas( - (e.target as HTMLInputElement).checked, - ) - } + {isSearchExpanded ? ( +
+ setSearchQuery(e.target.value)} + onBlur={() => { + if (!searchQuery) setIsSearchExpanded(false); + }} + rightElement={ +
- } + } + /> +
+ ) : ( +
-
+
+ } + > +