@@ -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 ( ( ) => {
@@ -948,8 +985,9 @@ const ClipboardPageSection = ({
948985 </ div >
949986 ) : visibleGroupedNodes . length === 0 ? (
950987 < 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.
988+ { searchQuery || selectedNodeType !== "All"
989+ ? "No nodes match the current filters."
990+ : 'All nodes from this page are already on canvas. Turn on "Show nodes on canvas" to view them.' }
953991 </ div >
954992 ) : (
955993 < div className = "space-y-1" >
@@ -1090,6 +1128,17 @@ export const ClipboardPanel = () => {
10901128 } = useClipboard ( ) ;
10911129 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
10921130 const [ isCollapsed , setIsCollapsed ] = useState ( false ) ;
1131+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
1132+ const [ isSearchExpanded , setIsSearchExpanded ] = useState ( false ) ;
1133+ const [ sortDirection , setSortDirection ] = useState < "asc" | "desc" > ( "asc" ) ;
1134+ const [ selectedNodeType , setSelectedNodeType ] = useState ( "All" ) ;
1135+
1136+ const availableNodeTypes = useMemo ( ( ) => {
1137+ const types = getDiscourseNodes ( ) . filter ( excludeDefaultNodes ) ;
1138+ return [ "All" , ...types . map ( ( t ) => t . text ) ] ;
1139+ } , [ ] ) ;
1140+
1141+ const hasActiveFilters = ! ! searchQuery || selectedNodeType !== "All" ;
10931142
10941143 if ( ! isOpen ) return null ;
10951144
@@ -1118,7 +1167,7 @@ export const ClipboardPanel = () => {
11181167 </ h2 >
11191168 < div className = "flex-shrink-0" >
11201169 < Button
1121- icon = { < Icon icon = " minus" /> }
1170+ icon = { < Icon icon = { isCollapsed ? "chevron-down" : " minus"} /> }
11221171 onClick = { ( ) => setIsCollapsed ( ! isCollapsed ) }
11231172 minimal
11241173 small
@@ -1137,35 +1186,126 @@ export const ClipboardPanel = () => {
11371186 </ div >
11381187 { ! isCollapsed && (
11391188 < >
1140- < div
1141- className = "flex items-center justify-end px-2 py-1"
1142- style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
1143- >
1144- < Popover
1145- position = { Position . BOTTOM_RIGHT }
1146- content = {
1147- < div
1148- className = "p-3"
1149- onPointerDown = { ( e ) => e . stopPropagation ( ) }
1150- style = { { pointerEvents : "all" } }
1151- >
1152- < Switch
1153- checked = { showNodesOnCanvas }
1154- alignIndicator = "right"
1155- className = "m-0 w-full"
1156- label = "Show nodes on canvas"
1157- onChange = { ( e ) =>
1158- setShowNodesOnCanvas (
1159- ( e . target as HTMLInputElement ) . checked ,
1160- )
1161- }
1189+ { isSearchExpanded ? (
1190+ < div
1191+ className = "px-2 py-1"
1192+ style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
1193+ >
1194+ < InputGroup
1195+ autoFocus
1196+ leftIcon = "search"
1197+ placeholder = "Find page"
1198+ value = { searchQuery }
1199+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
1200+ onBlur = { ( ) => {
1201+ if ( ! searchQuery ) setIsSearchExpanded ( false ) ;
1202+ } }
1203+ rightElement = {
1204+ < Button
1205+ minimal
1206+ small
1207+ icon = "cross"
1208+ onClick = { ( ) => {
1209+ setSearchQuery ( "" ) ;
1210+ setIsSearchExpanded ( false ) ;
1211+ } }
11621212 />
1163- </ div >
1164- }
1213+ }
1214+ />
1215+ </ div >
1216+ ) : (
1217+ < div
1218+ className = "flex items-center gap-1 px-2 py-1"
1219+ style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
11651220 >
1166- < Button minimal small icon = "menu" title = "Clipboard options" />
1167- </ Popover >
1168- </ div >
1221+ < Button
1222+ minimal
1223+ small
1224+ icon = "search"
1225+ onClick = { ( ) => setIsSearchExpanded ( true ) }
1226+ />
1227+ < Button
1228+ minimal
1229+ small
1230+ icon = {
1231+ sortDirection === "asc"
1232+ ? "sort-alphabetical"
1233+ : "sort-alphabetical-desc"
1234+ }
1235+ title = {
1236+ sortDirection === "asc"
1237+ ? "Sorted A→Z (click for Z→A)"
1238+ : "Sorted Z→A (click for A→Z)"
1239+ }
1240+ onClick = { ( ) =>
1241+ setSortDirection ( ( d ) => ( d === "asc" ? "desc" : "asc" ) )
1242+ }
1243+ />
1244+ < Popover
1245+ position = { Position . BOTTOM }
1246+ content = {
1247+ < Menu >
1248+ { availableNodeTypes . map ( ( type ) => (
1249+ < MenuItem
1250+ key = { type }
1251+ text = { type }
1252+ active = { selectedNodeType === type }
1253+ onClick = { ( ) => setSelectedNodeType ( type ) }
1254+ />
1255+ ) ) }
1256+ </ Menu >
1257+ }
1258+ >
1259+ < Button
1260+ minimal
1261+ small
1262+ rightIcon = "caret-down"
1263+ text = { selectedNodeType }
1264+ />
1265+ </ Popover >
1266+ { hasActiveFilters && (
1267+ < Button
1268+ minimal
1269+ small
1270+ icon = "filter-remove"
1271+ onClick = { ( ) => {
1272+ setSearchQuery ( "" ) ;
1273+ setSelectedNodeType ( "All" ) ;
1274+ } }
1275+ title = "Clear filters"
1276+ />
1277+ ) }
1278+ < Popover
1279+ position = { Position . BOTTOM_RIGHT }
1280+ content = {
1281+ < div
1282+ className = "p-3"
1283+ onPointerDown = { ( e ) => e . stopPropagation ( ) }
1284+ style = { { pointerEvents : "all" } }
1285+ >
1286+ < Switch
1287+ checked = { showNodesOnCanvas }
1288+ alignIndicator = "right"
1289+ className = "m-0 w-full"
1290+ label = "Show nodes on canvas"
1291+ onChange = { ( e ) =>
1292+ setShowNodesOnCanvas (
1293+ ( e . target as HTMLInputElement ) . checked ,
1294+ )
1295+ }
1296+ />
1297+ </ div >
1298+ }
1299+ >
1300+ < Button
1301+ minimal
1302+ small
1303+ icon = "settings"
1304+ title = "Clipboard options"
1305+ />
1306+ </ Popover >
1307+ </ div >
1308+ ) }
11691309 < div className = "max-h-96 overflow-y-auto px-4 pb-4" >
11701310 { pages . length === 0 ? (
11711311 < NonIdealState
@@ -1187,6 +1327,9 @@ export const ClipboardPanel = () => {
11871327 page = { page }
11881328 onRemove = { removePage }
11891329 showNodesOnCanvas = { showNodesOnCanvas }
1330+ searchQuery = { searchQuery }
1331+ sortDirection = { sortDirection }
1332+ selectedNodeType = { selectedNodeType }
11901333 />
11911334 ) ) }
11921335 </ div >
0 commit comments