@@ -16,8 +16,11 @@ import {
1616 Icon ,
1717 Intent ,
1818 NonIdealState ,
19+ Popover ,
20+ Position ,
1921 Spinner ,
2022 SpinnerSize ,
23+ Switch ,
2124 Tag ,
2225} from "@blueprintjs/core" ;
2326import getCurrentUserUid from "roamjs-components/queries/getCurrentUserUid" ;
@@ -67,17 +70,20 @@ export type ClipboardPage = {
6770type ClipboardContextValue = {
6871 isOpen : boolean ;
6972 pages : ClipboardPage [ ] ;
73+ showNodesOnCanvas : boolean ;
7074 openClipboard : ( ) => void ;
7175 closeClipboard : ( ) => void ;
7276 toggleClipboard : ( ) => void ;
7377 addPage : ( page : ClipboardPage ) => void ;
7478 removePage : ( uid : string ) => void ;
79+ setShowNodesOnCanvas : ( show : boolean ) => void ;
7580} ;
7681
7782// eslint-disable-next-line @typescript-eslint/naming-convention
7883const ClipboardContext = createContext < ClipboardContextValue | null > ( null ) ;
7984
8085const CLIPBOARD_PROP_KEY = "pages" ;
86+ const CLIPBOARD_SHOW_NODES_ON_CANVAS_PROP_KEY = "showNodesOnCanvas" ;
8187
8288const getOrCreateClipboardBlock = async (
8389 canvasPageTitle : string ,
@@ -130,6 +136,7 @@ export const ClipboardProvider = ({
130136} ) => {
131137 const [ isOpen , setIsOpen ] = useState ( false ) ;
132138 const [ pages , setPages ] = useState < ClipboardPage [ ] > ( [ ] ) ;
139+ const [ showNodesOnCanvas , setShowNodesOnCanvas ] = useState ( true ) ;
133140 const [ clipboardBlockUid , setClipboardBlockUid ] = useState < string | null > (
134141 null ,
135142 ) ;
@@ -166,6 +173,12 @@ export const ClipboardProvider = ({
166173 ) {
167174 setPages ( storedPages as ClipboardPage [ ] ) ;
168175 }
176+
177+ const storedShowNodesOnCanvas =
178+ props [ CLIPBOARD_SHOW_NODES_ON_CANVAS_PROP_KEY ] ;
179+ if ( typeof storedShowNodesOnCanvas === "boolean" ) {
180+ setShowNodesOnCanvas ( storedShowNodesOnCanvas ) ;
181+ }
169182 } catch ( error ) {
170183 internalError ( {
171184 error,
@@ -186,15 +199,20 @@ export const ClipboardProvider = ({
186199 try {
187200 setBlockProps ( clipboardBlockUid , {
188201 [ CLIPBOARD_PROP_KEY ] : pages ,
202+ [ CLIPBOARD_SHOW_NODES_ON_CANVAS_PROP_KEY ] : showNodesOnCanvas ,
189203 } ) ;
190204 } catch ( error ) {
191205 internalError ( {
192206 error,
193207 type : "Canvas Clipboard: Failed to persist state" ,
194- context : { clipboardBlockUid, pageCount : pages . length } ,
208+ context : {
209+ clipboardBlockUid,
210+ pageCount : pages . length ,
211+ showNodesOnCanvas,
212+ } ,
195213 } ) ;
196214 }
197- } , [ pages , clipboardBlockUid , isInitialized ] ) ;
215+ } , [ pages , clipboardBlockUid , isInitialized , showNodesOnCanvas ] ) ;
198216
199217 const openClipboard = useCallback ( ( ) => setIsOpen ( true ) , [ ] ) ;
200218 const closeClipboard = useCallback ( ( ) => setIsOpen ( false ) , [ ] ) ;
@@ -216,19 +234,23 @@ export const ClipboardProvider = ({
216234 ( ) => ( {
217235 isOpen,
218236 pages,
237+ showNodesOnCanvas,
219238 openClipboard,
220239 closeClipboard,
221240 toggleClipboard,
222241 addPage,
223242 removePage,
243+ setShowNodesOnCanvas,
224244 } ) ,
225245 [
226246 isOpen ,
227247 pages ,
248+ showNodesOnCanvas ,
228249 addPage ,
229250 closeClipboard ,
230251 openClipboard ,
231252 removePage ,
253+ setShowNodesOnCanvas ,
232254 toggleClipboard ,
233255 ] ,
234256 ) ;
@@ -395,9 +417,11 @@ type DragState =
395417const ClipboardPageSection = ( {
396418 page,
397419 onRemove,
420+ showNodesOnCanvas,
398421} : {
399422 page : ClipboardPage ;
400423 onRemove : ( uid : string ) => void ;
424+ showNodesOnCanvas : boolean ;
401425} ) => {
402426 const [ isOpen , setIsOpen ] = useState ( true ) ;
403427 const [ discourseNodes , setDiscourseNodes ] = useState <
@@ -484,22 +508,28 @@ const ClipboardPageSection = ({
484508 } ;
485509 } , [ editor . store ] ) ;
486510
487- const findShapesByUid = useCallback (
488- ( uid : string ) : DiscourseNodeShape [ ] => {
489- const allRecords = editor . store . allRecords ( ) ;
490- const shapes = allRecords . filter ( ( record ) => {
491- if ( record . typeName !== "shape" ) return false ;
492- const shape = record as DiscourseNodeShape ;
493- return shape . props ?. uid === uid ;
494- } ) as DiscourseNodeShape [ ] ;
495- return shapes ;
496- } ,
497- [ editor . store ] ,
498- ) ;
511+ const shapesByUid = useMemo ( ( ) => {
512+ void storeVersion ;
513+ const groupedShapes = new Map < string , DiscourseNodeShape [ ] > ( ) ;
514+ const allRecords = editor . store . allRecords ( ) ;
515+ allRecords . forEach ( ( record ) => {
516+ if ( record . typeName !== "shape" ) return ;
517+ const shape = record as DiscourseNodeShape ;
518+ const uid = shape . props ?. uid ;
519+ if ( ! uid ) return ;
520+ const currentShapes = groupedShapes . get ( uid ) ;
521+ if ( currentShapes ) {
522+ currentShapes . push ( shape ) ;
523+ } else {
524+ groupedShapes . set ( uid , [ shape ] ) ;
525+ }
526+ } ) ;
527+ return groupedShapes ;
528+ } , [ editor . store , storeVersion ] ) ;
499529
500530 const groupedNodes = useMemo ( ( ) => {
501531 const groups : NodeGroup [ ] = discourseNodes . map ( ( node ) => {
502- const shapes = findShapesByUid ( node . uid ) ;
532+ const shapes = shapesByUid . get ( node . uid ) ?? [ ] ;
503533 return {
504534 uid : node . uid ,
505535 text : node . text ,
@@ -510,7 +540,15 @@ const ClipboardPageSection = ({
510540
511541 return groups . sort ( ( a , b ) => a . text . localeCompare ( b . text ) ) ;
512542 // eslint-disable-next-line react-hooks/exhaustive-deps
513- } , [ discourseNodes , findShapesByUid , storeVersion ] ) ;
543+ } , [ discourseNodes , shapesByUid ] ) ;
544+
545+ const visibleGroupedNodes = useMemo (
546+ ( ) =>
547+ groupedNodes . filter ( ( group ) =>
548+ showNodesOnCanvas ? true : group . shapes . length === 0 ,
549+ ) ,
550+ [ groupedNodes , showNodesOnCanvas ] ,
551+ ) ;
514552
515553 useEffect ( ( ) => {
516554 setOpenSections ( ( prev ) => {
@@ -586,6 +624,14 @@ const ClipboardPageSection = ({
586624 const handleDropNode = useCallback (
587625 async ( node : { uid : string ; text : string } , pagePoint : Vec ) => {
588626 if ( ! extensionAPI ) return ;
627+ if ( ! showNodesOnCanvas ) {
628+ const nodeExistsOnCanvas = editor . store . allRecords ( ) . some ( ( record ) => {
629+ if ( record . typeName !== "shape" ) return false ;
630+ const shape = record as DiscourseNodeShape ;
631+ return shape . props ?. uid === node . uid ;
632+ } ) ;
633+ if ( nodeExistsOnCanvas ) return ;
634+ }
589635
590636 const nodeType = findDiscourseNode ( { uid : node . uid } ) ;
591637 if ( ! nodeType ) {
@@ -624,7 +670,7 @@ const ClipboardPageSection = ({
624670 editor . createShape < DiscourseNodeShape > ( shape ) ;
625671 editor . setCurrentTool ( "select" ) ;
626672 } ,
627- [ editor , extensionAPI ] ,
673+ [ editor , extensionAPI , showNodesOnCanvas ] ,
628674 ) ;
629675
630676 // Drag and drop handlers
@@ -897,9 +943,14 @@ const ClipboardPageSection = ({
897943 < div className = "rounded border border-dashed border-gray-200 p-2" >
898944 No discourse nodes found on this page.
899945 </ div >
946+ ) : visibleGroupedNodes . length === 0 ? (
947+ < div className = "rounded border border-dashed border-gray-200 p-2" >
948+ All nodes from this page are already on canvas. Turn on "Show
949+ nodes on canvas" to view them.
950+ </ div >
900951 ) : (
901952 < div className = "space-y-1" >
902- { groupedNodes . map ( ( group ) => {
953+ { visibleGroupedNodes . map ( ( group ) => {
903954 const nodeExistsInCanvas = group . shapes . length > 0 ;
904955 const isGroupOpen = openSections [ group . uid ] ?? false ;
905956
@@ -1025,7 +1076,15 @@ const ClipboardPageSection = ({
10251076} ;
10261077
10271078export const ClipboardPanel = ( ) => {
1028- const { isOpen, pages, closeClipboard, addPage, removePage } = useClipboard ( ) ;
1079+ const {
1080+ isOpen,
1081+ pages,
1082+ closeClipboard,
1083+ addPage,
1084+ removePage,
1085+ showNodesOnCanvas,
1086+ setShowNodesOnCanvas,
1087+ } = useClipboard ( ) ;
10291088 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
10301089 const [ isCollapsed , setIsCollapsed ] = useState ( false ) ;
10311090
@@ -1076,9 +1135,35 @@ export const ClipboardPanel = () => {
10761135 { ! isCollapsed && (
10771136 < >
10781137 < div
1079- className = "max-h-96 overflow-y-auto p-4 "
1138+ className = "flex items-center justify-end px-2 py-1 "
10801139 style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
10811140 >
1141+ < Popover
1142+ position = { Position . BOTTOM_RIGHT }
1143+ content = {
1144+ < div
1145+ className = "p-3"
1146+ onPointerDown = { ( e ) => e . stopPropagation ( ) }
1147+ style = { { pointerEvents : "all" } }
1148+ >
1149+ < Switch
1150+ checked = { showNodesOnCanvas }
1151+ alignIndicator = "right"
1152+ className = "m-0 w-full"
1153+ label = "Show nodes on canvas"
1154+ onChange = { ( e ) =>
1155+ setShowNodesOnCanvas (
1156+ ( e . target as HTMLInputElement ) . checked ,
1157+ )
1158+ }
1159+ />
1160+ </ div >
1161+ }
1162+ >
1163+ < Button minimal small icon = "menu" title = "Clipboard options" />
1164+ </ Popover >
1165+ </ div >
1166+ < div className = "max-h-96 overflow-y-auto px-4 pb-4" >
10821167 { pages . length === 0 ? (
10831168 < NonIdealState
10841169 action = {
@@ -1098,6 +1183,7 @@ export const ClipboardPanel = () => {
10981183 key = { page . uid }
10991184 page = { page }
11001185 onRemove = { removePage }
1186+ showNodesOnCanvas = { showNodesOnCanvas }
11011187 />
11021188 ) ) }
11031189 </ div >
0 commit comments