diff --git a/apps/roam/src/components/canvas/Clipboard.tsx b/apps/roam/src/components/canvas/Clipboard.tsx index d5254313f..8263044c9 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, @@ -389,6 +392,7 @@ const AddPageModal = ({ isOpen, onClose, onConfirm }: AddPageModalProps) => { type NodeGroup = { uid: string; text: string; + type: string; shapes: DiscourseNodeShape[]; isDuplicate: boolean; }; @@ -422,10 +426,18 @@ const ClipboardPageSection = ({ page, onRemove, showNodesOnCanvas, + searchQuery, + sortDirection, + selectedNodeType, + onNodeTypesChange, }: { page: ClipboardPage; onRemove: (uid: string) => void; showNodesOnCanvas: boolean; + searchQuery: string; + sortDirection: "asc" | "desc"; + selectedNodeType: string; + onNodeTypesChange: (pageUid: string, types: string[]) => void; }) => { const [isOpen, setIsOpen] = useState(true); const [discourseNodes, setDiscourseNodes] = useState< @@ -534,26 +546,58 @@ 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(() => { + const candidateNodes = showNodesOnCanvas + ? groupedNodes + : groupedNodes.filter((n) => n.shapes.length === 0); + const types = [...new Set(candidateNodes.map((n) => n.type))]; + onNodeTypesChange(page.uid, types); + }, [groupedNodes, page.uid, onNodeTypesChange, showNodesOnCanvas]); + useEffect(() => { setOpenSections((prev) => { const next: Record = {}; @@ -949,8 +993,13 @@ 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" + ? showNodesOnCanvas + ? "No nodes match the current filters." + : 'No nodes match the current filters, or matching nodes are already on canvas. Turn on "Show nodes on canvas" to view them.' + : showNodesOnCanvas + ? "All nodes from this page are already on canvas." + : 'All nodes from this page are already on canvas. Turn on "Show nodes on canvas" to view them.'}
) : (
@@ -1091,6 +1140,41 @@ 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 [nodeTypesByPage, setNodeTypesByPage] = useState< + Record + >({}); + + const handleNodeTypesChange = useCallback( + (pageUid: string, types: string[]) => { + setNodeTypesByPage((prev) => ({ ...prev, [pageUid]: types })); + }, + [], + ); + + const availableNodeTypes = useMemo(() => { + const pageUids = new Set(pages.map((p) => p.uid)); + const allTypes = new Set( + Object.entries(nodeTypesByPage) + .filter(([uid]) => pageUids.has(uid)) + .flatMap(([, types]) => types), + ); + return ["All", ...Array.from(allTypes).sort()]; + }, [nodeTypesByPage, pages]); + + useEffect(() => { + if ( + selectedNodeType !== "All" && + !availableNodeTypes.includes(selectedNodeType) + ) { + setSelectedNodeType("All"); + } + }, [availableNodeTypes, selectedNodeType]); + + const hasActiveFilters = !!searchQuery || selectedNodeType !== "All"; if (!isOpen) return null; @@ -1119,7 +1203,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={ +
- } + } + /> +
+ ) : ( +
-
+
+ } + > +