@@ -42,12 +42,18 @@ import {
4242} from "~/components/canvas/shapes/DiscourseRelationBinding" ;
4343import ToastListener from "./ToastListener" ;
4444import { RelationsOverlay } from "./overlays/RelationOverlay" ;
45+ import { DiscourseNodeShape } from "~/components/canvas/shapes/DiscourseNodeShape" ;
46+ import {
47+ extractBlockRefId ,
48+ resolveLinkedTFileByBlockRef ,
49+ } from "~/components/canvas/stores/assetStore" ;
50+ import { showToast } from "~/components/canvas/utils/toastUtils" ;
4551
46- interface TldrawPreviewProps {
52+ type TldrawPreviewProps = {
4753 store : TLStore ;
4854 file : TFile ;
4955 assetStore : ObsidianTLAssetStore ;
50- }
56+ } ;
5157
5258// No longer needed - using tldraw's event system instead
5359
@@ -60,9 +66,11 @@ export const TldrawPreviewComponent = ({
6066 const [ currentStore , setCurrentStore ] = useState < TLStore > ( store ) ;
6167 const [ isReady , setIsReady ] = useState ( false ) ;
6268 const isCreatingRelationRef = useRef ( false ) ;
63- const saveTimeoutRef = useRef < NodeJS . Timeout > ( ) ;
69+ const lastShiftClickRef = useRef < number > ( 0 ) ;
70+ const SHIFT_CLICK_DEBOUNCE_MS = 300 ; // Prevent double clicks within 300ms
71+ const saveTimeoutRef = useRef < NodeJS . Timeout > ( null ) ;
6472 const lastSavedDataRef = useRef < string > ( "" ) ;
65- const editorRef = useRef < Editor > ( ) ;
73+ const editorRef = useRef < Editor > ( null ) ;
6674 const plugin = usePlugin ( ) ;
6775
6876 const customShapeUtils = [
@@ -190,6 +198,84 @@ export const TldrawPreviewComponent = ({
190198 BaseRelationBindingUtil . checkAndReifyRelation ( editor ) ;
191199 isCreatingRelationRef . current = false ;
192200 }
201+
202+ if ( e . shiftKey ) {
203+ const now = Date . now ( ) ;
204+
205+ // Debounce to prevent double opening
206+ if ( now - lastShiftClickRef . current < SHIFT_CLICK_DEBOUNCE_MS ) {
207+ return ;
208+ }
209+ lastShiftClickRef . current = now ;
210+
211+ const shape = editor . getShapeAtPoint (
212+ editor . inputs . currentPagePoint ,
213+ ) as DiscourseNodeShape ;
214+
215+ if ( ! shape || shape . type !== "discourse-node" ) return ;
216+
217+ const selectedShapes = editor . getSelectedShapes ( ) ;
218+ const selectedDiscourseNodes = selectedShapes . filter (
219+ ( s ) => s . type === "discourse-node" ,
220+ ) ;
221+
222+ if ( selectedDiscourseNodes . length > 1 ) {
223+ return ;
224+ }
225+
226+ const blockRefId = extractBlockRefId ( shape . props . src ?? undefined ) ;
227+ if ( ! blockRefId ) {
228+ showToast ( {
229+ severity : "warning" ,
230+ title : "Cannot open node" ,
231+ description : "No valid block reference found" ,
232+ } ) ;
233+ return ;
234+ }
235+
236+ const canvasFileCache = plugin . app . metadataCache . getFileCache ( file ) ;
237+ if ( ! canvasFileCache ) {
238+ showToast ( {
239+ severity : "error" ,
240+ title : "Error" ,
241+ description : "Could not read canvas file" ,
242+ } ) ;
243+ return ;
244+ }
245+
246+ void resolveLinkedTFileByBlockRef ( {
247+ app : plugin . app ,
248+ canvasFile : file ,
249+ blockRefId,
250+ canvasFileCache,
251+ } )
252+ . then ( ( linkedFile ) => {
253+ if ( ! linkedFile ) {
254+ showToast ( {
255+ severity : "warning" ,
256+ title : "Cannot open node" ,
257+ description : "Linked file not found" ,
258+ } ) ;
259+ return ;
260+ }
261+
262+ void plugin . app . workspace . openLinkText (
263+ linkedFile . path ,
264+ file . path ,
265+ true ,
266+ ) ;
267+
268+ editor . selectNone ( ) ;
269+ } )
270+ . catch ( ( error ) => {
271+ console . error ( "Error opening linked file:" , error ) ;
272+ showToast ( {
273+ severity : "error" ,
274+ title : "Error" ,
275+ description : "Failed to open linked file" ,
276+ } ) ;
277+ } ) ;
278+ }
193279 }
194280 } ) ;
195281 } ;
0 commit comments