@@ -14,7 +14,10 @@ import {
1414 Collapse ,
1515 Dialog ,
1616 Icon ,
17+ InputGroup ,
1718 Intent ,
19+ Menu ,
20+ MenuItem ,
1821 NonIdealState ,
1922 Popover ,
2023 Position ,
@@ -43,7 +46,6 @@ import AutocompleteInput from "roamjs-components/components/AutocompleteInput";
4346import { Result } from "roamjs-components/types/query-builder" ;
4447import fuzzy from "fuzzy" ;
4548import getAllReferencesOnPage from "~/utils/getAllReferencesOnPage" ;
46- import isDiscourseNode from "~/utils/isDiscourseNode" ;
4749import {
4850 DiscourseNodeShape ,
4951 DEFAULT_STYLE_PROPS ,
@@ -53,9 +55,11 @@ import { openBlockInSidebar, createBlock } from "roamjs-components/writes";
5355import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid" ;
5456import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle" ;
5557import findDiscourseNode from "~/utils/findDiscourseNode" ;
58+ import getDiscourseNodes from "~/utils/getDiscourseNodes" ;
5659import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg" ;
5760import { useExtensionAPI } from "roamjs-components/components/ExtensionApiContext" ;
5861import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors" ;
62+ import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings" ;
5963import { MAX_WIDTH } from "./Tldraw" ;
6064import getBlockProps from "~/utils/getBlockProps" ;
6165import setBlockProps from "~/utils/setBlockProps" ;
@@ -389,6 +393,7 @@ const AddPageModal = ({ isOpen, onClose, onConfirm }: AddPageModalProps) => {
389393type NodeGroup = {
390394 uid : string ;
391395 text : string ;
396+ type : string ;
392397 shapes : DiscourseNodeShape [ ] ;
393398 isDuplicate : boolean ;
394399} ;
@@ -422,14 +427,20 @@ const ClipboardPageSection = ({
422427 page,
423428 onRemove,
424429 showNodesOnCanvas,
430+ searchQuery,
431+ selectedNodeType,
432+ onNodeTypesChange,
425433} : {
426434 page : ClipboardPage ;
427435 onRemove : ( uid : string ) => void ;
428436 showNodesOnCanvas : boolean ;
437+ searchQuery : string ;
438+ selectedNodeType : string ;
439+ onNodeTypesChange : ( pageUid : string , types : string [ ] ) => void ;
429440} ) => {
430441 const [ isOpen , setIsOpen ] = useState ( true ) ;
431442 const [ discourseNodes , setDiscourseNodes ] = useState <
432- Array < { uid : string ; text : string } >
443+ Array < { uid : string ; text : string ; type : string } >
433444 > ( [ ] ) ;
434445 const [ isLoading , setIsLoading ] = useState ( false ) ;
435446 const [ openSections , setOpenSections ] = useState < Record < string , boolean > > ( { } ) ;
@@ -453,9 +464,17 @@ const ClipboardPageSection = ({
453464 setIsLoading ( true ) ;
454465 try {
455466 const referencedPages = await getAllReferencesOnPage ( page . text ) ;
456- const nodes = referencedPages . filter ( ( refPage ) =>
457- isDiscourseNode ( refPage . uid ) ,
458- ) ;
467+ const nodes = referencedPages . flatMap ( ( refPage ) => {
468+ const discourseNode = findDiscourseNode ( { uid : refPage . uid } ) ;
469+ if ( ! discourseNode || discourseNode . backedBy === "default" ) return [ ] ;
470+ return [
471+ {
472+ uid : refPage . uid ,
473+ text : refPage . text ,
474+ type : discourseNode . text ,
475+ } ,
476+ ] ;
477+ } ) ;
459478 setDiscourseNodes ( nodes ) ;
460479 } catch ( error ) {
461480 internalError ( {
@@ -537,23 +556,48 @@ const ClipboardPageSection = ({
537556 return {
538557 uid : node . uid ,
539558 text : node . text ,
559+ type : node . type ,
540560 shapes,
541561 isDuplicate : shapes . length > 1 ,
542562 } ;
543563 } ) ;
544564
545- return groups . sort ( ( a , b ) => a . text . localeCompare ( b . text ) ) ;
565+ return groups ;
546566 // eslint-disable-next-line react-hooks/exhaustive-deps
547567 } , [ discourseNodes , shapesByUid ] ) ;
548568
549569 const visibleGroupedNodes = useMemo (
550570 ( ) =>
551- groupedNodes . filter ( ( group ) =>
552- showNodesOnCanvas ? true : group . shapes . length === 0 ,
553- ) ,
554- [ groupedNodes , showNodesOnCanvas ] ,
571+ groupedNodes
572+ . filter ( ( group ) =>
573+ showNodesOnCanvas ? true : group . shapes . length === 0 ,
574+ )
575+ . filter ( ( group ) =>
576+ searchQuery
577+ ? group . text . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) )
578+ : true ,
579+ )
580+ . filter ( ( group ) =>
581+ selectedNodeType && selectedNodeType !== "All"
582+ ? group . type === selectedNodeType
583+ : true ,
584+ )
585+ . sort ( ( a , b ) => a . text . localeCompare ( b . text ) ) ,
586+ [ groupedNodes , showNodesOnCanvas , searchQuery , selectedNodeType ] ,
555587 ) ;
556588
589+ // Publish this page's distinct node types up to ClipboardPanel so it can
590+ // build a unified filter dropdown across all open pages. When
591+ // `showNodesOnCanvas` is off, only consider nodes not yet on the canvas so
592+ // the filter options match what the user can actually act on.
593+ useEffect ( ( ) => {
594+ const candidateNodes = showNodesOnCanvas
595+ ? groupedNodes
596+ : groupedNodes . filter ( ( n ) => n . shapes . length === 0 ) ;
597+ const types = [ ...new Set ( candidateNodes . map ( ( n ) => n . type ) ) ] ;
598+ onNodeTypesChange ( page . uid , types ) ;
599+ } , [ groupedNodes , page . uid , onNodeTypesChange , showNodesOnCanvas ] ) ;
600+
557601 useEffect ( ( ) => {
558602 setOpenSections ( ( prev ) => {
559603 const next : Record < string , boolean > = { } ;
@@ -948,8 +992,13 @@ const ClipboardPageSection = ({
948992 </ div >
949993 ) : visibleGroupedNodes . length === 0 ? (
950994 < div className = "rounded border border-dashed border-gray-200 p-2" >
951- All nodes from this page are already on canvas. Turn on "Show
952- nodes on canvas" to view them.
995+ { searchQuery || selectedNodeType !== "All"
996+ ? showNodesOnCanvas
997+ ? "No nodes match the current filters."
998+ : 'No nodes match the current filters, or matching nodes are already on canvas. Turn on "Show nodes on canvas" to view them.'
999+ : showNodesOnCanvas
1000+ ? "All nodes from this page are already on canvas."
1001+ : 'All nodes from this page are already on canvas. Turn on "Show nodes on canvas" to view them.' }
9531002 </ div >
9541003 ) : (
9551004 < div className = "space-y-1" >
@@ -1090,6 +1139,51 @@ export const ClipboardPanel = () => {
10901139 } = useClipboard ( ) ;
10911140 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
10921141 const [ isCollapsed , setIsCollapsed ] = useState ( false ) ;
1142+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
1143+ const [ selectedNodeType , setSelectedNodeType ] = useState ( "All" ) ;
1144+ const [ nodeTypesByPage , setNodeTypesByPage ] = useState <
1145+ Record < string , string [ ] >
1146+ > ( { } ) ;
1147+
1148+ const handleNodeTypesChange = useCallback (
1149+ ( pageUid : string , types : string [ ] ) => {
1150+ setNodeTypesByPage ( ( prev ) => ( { ...prev , [ pageUid ] : types } ) ) ;
1151+ } ,
1152+ [ ] ,
1153+ ) ;
1154+
1155+ const availableNodeTypes = useMemo ( ( ) => {
1156+ const pageUids = new Set ( pages . map ( ( p ) => p . uid ) ) ;
1157+ const allTypes = new Set (
1158+ Object . entries ( nodeTypesByPage )
1159+ . filter ( ( [ uid ] ) => pageUids . has ( uid ) )
1160+ . flatMap ( ( [ , types ] ) => types ) ,
1161+ ) ;
1162+ return [ "All" , ...Array . from ( allTypes ) . sort ( ) ] ;
1163+ } , [ nodeTypesByPage , pages ] ) ;
1164+
1165+ const nodeTypeColorMap = useMemo ( ( ) => {
1166+ return Object . fromEntries (
1167+ getDiscourseNodes ( ) . map ( ( n ) => [
1168+ n . text ,
1169+ formatHexColor ( n . canvasSettings ?. color ) || "#000000" ,
1170+ ] ) ,
1171+ ) ;
1172+ } , [ ] ) ;
1173+
1174+ // Reset the filter to "All" when the currently selected type disappears
1175+ // from `availableNodeTypes` (e.g. the page containing it was removed),
1176+ // otherwise the dropdown would keep a stale value that filters everything out.
1177+ useEffect ( ( ) => {
1178+ if (
1179+ selectedNodeType !== "All" &&
1180+ ! availableNodeTypes . includes ( selectedNodeType )
1181+ ) {
1182+ setSelectedNodeType ( "All" ) ;
1183+ }
1184+ } , [ availableNodeTypes , selectedNodeType ] ) ;
1185+
1186+ const hasActiveFilters = ! ! searchQuery || selectedNodeType !== "All" ;
10931187
10941188 if ( ! isOpen ) return null ;
10951189
@@ -1138,9 +1232,78 @@ export const ClipboardPanel = () => {
11381232 { ! isCollapsed && (
11391233 < >
11401234 < div
1141- className = "flex items-center justify-end px-2 py-1"
1235+ className = "flex items-center gap-1 px-2 py-1"
11421236 style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
11431237 >
1238+ < InputGroup
1239+ small
1240+ leftIcon = "search"
1241+ placeholder = "Find page"
1242+ value = { searchQuery }
1243+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
1244+ className = "flex-1"
1245+ rightElement = {
1246+ searchQuery ? (
1247+ < Button
1248+ minimal
1249+ small
1250+ icon = "cross"
1251+ onClick = { ( ) => setSearchQuery ( "" ) }
1252+ />
1253+ ) : undefined
1254+ }
1255+ />
1256+ < Popover
1257+ position = { Position . BOTTOM }
1258+ content = {
1259+ < div
1260+ onPointerDown = { ( e ) => e . stopPropagation ( ) }
1261+ style = { { pointerEvents : "all" } }
1262+ >
1263+ < Menu >
1264+ { availableNodeTypes . map ( ( type ) => (
1265+ < MenuItem
1266+ key = { type }
1267+ active = { selectedNodeType === type }
1268+ onClick = { ( ) => setSelectedNodeType ( type ) }
1269+ text = {
1270+ < span className = "flex items-center gap-2" >
1271+ { type !== "All" && (
1272+ < span
1273+ className = "inline-block h-3 w-3 shrink-0 rounded-full"
1274+ style = { {
1275+ backgroundColor :
1276+ nodeTypeColorMap [ type ] || "#000000" ,
1277+ } }
1278+ />
1279+ ) }
1280+ { type }
1281+ </ span >
1282+ }
1283+ />
1284+ ) ) }
1285+ </ Menu >
1286+ </ div >
1287+ }
1288+ >
1289+ < Button
1290+ minimal
1291+ small
1292+ rightIcon = "caret-down"
1293+ text = { selectedNodeType }
1294+ />
1295+ </ Popover >
1296+ < Button
1297+ minimal
1298+ small
1299+ icon = "filter-remove"
1300+ disabled = { ! hasActiveFilters }
1301+ onClick = { ( ) => {
1302+ setSearchQuery ( "" ) ;
1303+ setSelectedNodeType ( "All" ) ;
1304+ } }
1305+ title = "Clear filters"
1306+ />
11441307 < Popover
11451308 position = { Position . BOTTOM_RIGHT }
11461309 content = {
@@ -1163,7 +1326,7 @@ export const ClipboardPanel = () => {
11631326 </ div >
11641327 }
11651328 >
1166- < Button minimal small icon = "menu " title = "Clipboard options" />
1329+ < Button minimal small icon = "settings " title = "Clipboard options" />
11671330 </ Popover >
11681331 </ div >
11691332 < div className = "max-h-96 overflow-y-auto px-4 pb-4" >
@@ -1187,6 +1350,9 @@ export const ClipboardPanel = () => {
11871350 page = { page }
11881351 onRemove = { removePage }
11891352 showNodesOnCanvas = { showNodesOnCanvas }
1353+ searchQuery = { searchQuery }
1354+ selectedNodeType = { selectedNodeType }
1355+ onNodeTypesChange = { handleNodeTypesChange }
11901356 />
11911357 ) ) }
11921358 </ div >
0 commit comments