@@ -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 ,
@@ -53,6 +56,9 @@ import { openBlockInSidebar, createBlock } from "roamjs-components/writes";
5356import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid" ;
5457import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle" ;
5558import findDiscourseNode from "~/utils/findDiscourseNode" ;
59+ import getDiscourseNodes , {
60+ excludeDefaultNodes ,
61+ } from "~/utils/getDiscourseNodes" ;
5662import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg" ;
5763import { useExtensionAPI } from "roamjs-components/components/ExtensionApiContext" ;
5864import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors" ;
@@ -389,6 +395,7 @@ const AddPageModal = ({ isOpen, onClose, onConfirm }: AddPageModalProps) => {
389395type NodeGroup = {
390396 uid : string ;
391397 text : string ;
398+ type : string ;
392399 shapes : DiscourseNodeShape [ ] ;
393400 isDuplicate : boolean ;
394401} ;
@@ -422,10 +429,16 @@ const ClipboardPageSection = ({
422429 page,
423430 onRemove,
424431 showNodesOnCanvas,
432+ searchQuery,
433+ sortDirection,
434+ selectedNodeType,
425435} : {
426436 page : ClipboardPage ;
427437 onRemove : ( uid : string ) => void ;
428438 showNodesOnCanvas : boolean ;
439+ searchQuery : string ;
440+ sortDirection : "asc" | "desc" ;
441+ selectedNodeType : string ;
429442} ) => {
430443 const [ isOpen , setIsOpen ] = useState ( true ) ;
431444 const [ discourseNodes , setDiscourseNodes ] = useState <
@@ -534,24 +547,48 @@ const ClipboardPageSection = ({
534547 const groupedNodes = useMemo ( ( ) => {
535548 const groups : NodeGroup [ ] = discourseNodes . map ( ( node ) => {
536549 const shapes = shapesByUid . get ( node . uid ) ?? [ ] ;
550+ const discourseNode = findDiscourseNode ( { uid : node . uid } ) ;
537551 return {
538552 uid : node . uid ,
539553 text : node . text ,
554+ type : discourseNode ? discourseNode . text : "Unknown" ,
540555 shapes,
541556 isDuplicate : shapes . length > 1 ,
542557 } ;
543558 } ) ;
544559
545- return groups . sort ( ( a , b ) => a . text . localeCompare ( b . text ) ) ;
560+ return groups ;
546561 // eslint-disable-next-line react-hooks/exhaustive-deps
547562 } , [ discourseNodes , shapesByUid ] ) ;
548563
549564 const visibleGroupedNodes = useMemo (
550565 ( ) =>
551- groupedNodes . filter ( ( group ) =>
552- showNodesOnCanvas ? true : group . shapes . length === 0 ,
553- ) ,
554- [ groupedNodes , showNodesOnCanvas ] ,
566+ groupedNodes
567+ . filter ( ( group ) =>
568+ showNodesOnCanvas ? true : group . shapes . length === 0 ,
569+ )
570+ . filter ( ( group ) =>
571+ searchQuery
572+ ? group . text . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) )
573+ : true ,
574+ )
575+ . filter ( ( group ) =>
576+ selectedNodeType && selectedNodeType !== "All"
577+ ? group . type === selectedNodeType
578+ : true ,
579+ )
580+ . sort ( ( a , b ) =>
581+ sortDirection === "asc"
582+ ? a . text . localeCompare ( b . text )
583+ : b . text . localeCompare ( a . text ) ,
584+ ) ,
585+ [
586+ groupedNodes ,
587+ showNodesOnCanvas ,
588+ searchQuery ,
589+ selectedNodeType ,
590+ sortDirection ,
591+ ] ,
555592 ) ;
556593
557594 useEffect ( ( ) => {
@@ -949,8 +986,9 @@ const ClipboardPageSection = ({
949986 </ div >
950987 ) : visibleGroupedNodes . length === 0 ? (
951988 < div className = "rounded border border-dashed border-gray-200 p-2" >
952- All nodes from this page are already on canvas. Turn on "Show
953- nodes on canvas" to view them.
989+ { searchQuery || selectedNodeType !== "All"
990+ ? "No nodes match the current filters."
991+ : 'All nodes from this page are already on canvas. Turn on "Show nodes on canvas" to view them.' }
954992 </ div >
955993 ) : (
956994 < div className = "space-y-1" >
@@ -1091,6 +1129,17 @@ export const ClipboardPanel = () => {
10911129 } = useClipboard ( ) ;
10921130 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
10931131 const [ isCollapsed , setIsCollapsed ] = useState ( false ) ;
1132+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
1133+ const [ isSearchExpanded , setIsSearchExpanded ] = useState ( false ) ;
1134+ const [ sortDirection , setSortDirection ] = useState < "asc" | "desc" > ( "asc" ) ;
1135+ const [ selectedNodeType , setSelectedNodeType ] = useState ( "All" ) ;
1136+
1137+ const availableNodeTypes = useMemo ( ( ) => {
1138+ const types = getDiscourseNodes ( ) . filter ( excludeDefaultNodes ) ;
1139+ return [ "All" , ...types . map ( ( t ) => t . text ) ] ;
1140+ } , [ ] ) ;
1141+
1142+ const hasActiveFilters = ! ! searchQuery || selectedNodeType !== "All" ;
10941143
10951144 if ( ! isOpen ) return null ;
10961145
@@ -1119,7 +1168,7 @@ export const ClipboardPanel = () => {
11191168 </ h2 >
11201169 < div className = "flex-shrink-0" >
11211170 < Button
1122- icon = { < Icon icon = " minus" /> }
1171+ icon = { < Icon icon = { isCollapsed ? "chevron-down" : " minus"} /> }
11231172 onClick = { ( ) => setIsCollapsed ( ! isCollapsed ) }
11241173 minimal
11251174 small
@@ -1138,35 +1187,126 @@ export const ClipboardPanel = () => {
11381187 </ div >
11391188 { ! isCollapsed && (
11401189 < >
1141- < div
1142- className = "flex items-center justify-end px-2 py-1"
1143- style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
1144- >
1145- < Popover
1146- position = { Position . BOTTOM_RIGHT }
1147- content = {
1148- < div
1149- className = "p-3"
1150- onPointerDown = { ( e ) => e . stopPropagation ( ) }
1151- style = { { pointerEvents : "all" } }
1152- >
1153- < Switch
1154- checked = { showNodesOnCanvas }
1155- alignIndicator = "right"
1156- className = "m-0 w-full"
1157- label = "Show nodes on canvas"
1158- onChange = { ( e ) =>
1159- setShowNodesOnCanvas (
1160- ( e . target as HTMLInputElement ) . checked ,
1161- )
1162- }
1190+ { isSearchExpanded ? (
1191+ < div
1192+ className = "px-2 py-1"
1193+ style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
1194+ >
1195+ < InputGroup
1196+ autoFocus
1197+ leftIcon = "search"
1198+ placeholder = "Find page"
1199+ value = { searchQuery }
1200+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
1201+ onBlur = { ( ) => {
1202+ if ( ! searchQuery ) setIsSearchExpanded ( false ) ;
1203+ } }
1204+ rightElement = {
1205+ < Button
1206+ minimal
1207+ small
1208+ icon = "cross"
1209+ onClick = { ( ) => {
1210+ setSearchQuery ( "" ) ;
1211+ setIsSearchExpanded ( false ) ;
1212+ } }
11631213 />
1164- </ div >
1165- }
1214+ }
1215+ />
1216+ </ div >
1217+ ) : (
1218+ < div
1219+ className = "flex items-center gap-1 px-2 py-1"
1220+ style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
11661221 >
1167- < Button minimal small icon = "menu" title = "Clipboard options" />
1168- </ Popover >
1169- </ div >
1222+ < Button
1223+ minimal
1224+ small
1225+ icon = "search"
1226+ onClick = { ( ) => setIsSearchExpanded ( true ) }
1227+ />
1228+ < Button
1229+ minimal
1230+ small
1231+ icon = {
1232+ sortDirection === "asc"
1233+ ? "sort-alphabetical"
1234+ : "sort-alphabetical-desc"
1235+ }
1236+ title = {
1237+ sortDirection === "asc"
1238+ ? "Sorted A→Z (click for Z→A)"
1239+ : "Sorted Z→A (click for A→Z)"
1240+ }
1241+ onClick = { ( ) =>
1242+ setSortDirection ( ( d ) => ( d === "asc" ? "desc" : "asc" ) )
1243+ }
1244+ />
1245+ < Popover
1246+ position = { Position . BOTTOM }
1247+ content = {
1248+ < Menu >
1249+ { availableNodeTypes . map ( ( type ) => (
1250+ < MenuItem
1251+ key = { type }
1252+ text = { type }
1253+ active = { selectedNodeType === type }
1254+ onClick = { ( ) => setSelectedNodeType ( type ) }
1255+ />
1256+ ) ) }
1257+ </ Menu >
1258+ }
1259+ >
1260+ < Button
1261+ minimal
1262+ small
1263+ rightIcon = "caret-down"
1264+ text = { selectedNodeType }
1265+ />
1266+ </ Popover >
1267+ { hasActiveFilters && (
1268+ < Button
1269+ minimal
1270+ small
1271+ icon = "filter-remove"
1272+ onClick = { ( ) => {
1273+ setSearchQuery ( "" ) ;
1274+ setSelectedNodeType ( "All" ) ;
1275+ } }
1276+ title = "Clear filters"
1277+ />
1278+ ) }
1279+ < Popover
1280+ position = { Position . BOTTOM_RIGHT }
1281+ content = {
1282+ < div
1283+ className = "p-3"
1284+ onPointerDown = { ( e ) => e . stopPropagation ( ) }
1285+ style = { { pointerEvents : "all" } }
1286+ >
1287+ < Switch
1288+ checked = { showNodesOnCanvas }
1289+ alignIndicator = "right"
1290+ className = "m-0 w-full"
1291+ label = "Show nodes on canvas"
1292+ onChange = { ( e ) =>
1293+ setShowNodesOnCanvas (
1294+ ( e . target as HTMLInputElement ) . checked ,
1295+ )
1296+ }
1297+ />
1298+ </ div >
1299+ }
1300+ >
1301+ < Button
1302+ minimal
1303+ small
1304+ icon = "settings"
1305+ title = "Clipboard options"
1306+ />
1307+ </ Popover >
1308+ </ div >
1309+ ) }
11701310 < div className = "max-h-96 overflow-y-auto px-4 pb-4" >
11711311 { pages . length === 0 ? (
11721312 < NonIdealState
@@ -1188,6 +1328,9 @@ export const ClipboardPanel = () => {
11881328 page = { page }
11891329 onRemove = { removePage }
11901330 showNodesOnCanvas = { showNodesOnCanvas }
1331+ searchQuery = { searchQuery }
1332+ sortDirection = { sortDirection }
1333+ selectedNodeType = { selectedNodeType }
11911334 />
11921335 ) ) }
11931336 </ div >
0 commit comments