11import { TFile , Notice } from "obsidian" ;
2- import { useState , useRef , useEffect , useCallback , useMemo } from "react" ;
2+ import React , {
3+ useState ,
4+ useRef ,
5+ useEffect ,
6+ useCallback ,
7+ useMemo ,
8+ } from "react" ;
39import { QueryEngine } from "~/services/QueryEngine" ;
410import SearchBar from "./SearchBar" ;
511import { DiscourseNode } from "~/types" ;
612import DropdownSelect from "./DropdownSelect" ;
713import { usePlugin } from "./PluginContext" ;
8- import { getNodeTypeById } from "~/utils/typeUtils" ;
14+ import { getNodeTypeById , getAndFormatImportSource } from "~/utils/typeUtils" ;
915import type { RelationInstance } from "~/types" ;
1016import {
1117 getNodeInstanceIdForFile ,
1218 getRelationsForFile ,
1319 resolveEndpointToFile ,
1420 addRelation ,
1521 removeRelationBySourceDestinationType ,
22+ updateRelation ,
1623} from "~/utils/relationsStore" ;
1724
1825type RelationTypeOption = {
@@ -370,16 +377,59 @@ type GroupedRelation = {
370377
371378type CurrentRelationshipsProps = RelationshipSectionProps & {
372379 relationsVersion : number ;
380+ onRelationsChange ?: ( ) => void ;
381+ } ;
382+
383+ const buildGroupedRelations = (
384+ relations : RelationInstance [ ] ,
385+ activeIds : Set < string > ,
386+ plugin : ReturnType < typeof usePlugin > ,
387+ ) : Map < string , GroupedRelation > => {
388+ const map = new Map < string , GroupedRelation > ( ) ;
389+ for ( const r of relations ) {
390+ const relationType = plugin . settings . relationTypes . find (
391+ ( rt ) => rt . id === r . type ,
392+ ) ;
393+ if ( ! relationType ) continue ;
394+
395+ const isSource = activeIds . has ( r . source ) ;
396+ const relationLabel = isSource
397+ ? relationType . label
398+ : relationType . complement ;
399+ const relationKey = `${ r . type } -${ isSource ? "source" : "target" } ` ;
400+
401+ if ( ! map . has ( relationKey ) ) {
402+ map . set ( relationKey , {
403+ relationTypeOptions : {
404+ id : relationType . id ,
405+ label : relationLabel ,
406+ isSource,
407+ } ,
408+ linkedEntries : [ ] ,
409+ } ) ;
410+ }
411+
412+ const group = map . get ( relationKey ) ! ;
413+ const otherId = isSource ? r . destination : r . source ;
414+ const linkedFile = resolveEndpointToFile ( plugin , otherId ) ;
415+ if (
416+ linkedFile &&
417+ ! group . linkedEntries . some ( ( e ) => e . relation . id === r . id )
418+ ) {
419+ group . linkedEntries . push ( { file : linkedFile , relation : r } ) ;
420+ }
421+ }
422+ return map ;
373423} ;
374424
375425const CurrentRelationships = ( {
376426 activeFile,
377427 relationsVersion,
428+ onRelationsChange,
378429} : CurrentRelationshipsProps ) => {
379430 const plugin = usePlugin ( ) ;
380- const [ groupedRelationships , setGroupedRelationships ] = useState <
381- GroupedRelation [ ]
382- > ( [ ] ) ;
431+ const [ acceptedGroups , setAcceptedGroups ] = useState < GroupedRelation [ ] > ( [ ] ) ;
432+ const [ tentativeGroups , setTentativeGroups ] = useState < GroupedRelation [ ] > ( [ ] ) ;
383433
384434 const loadCurrentRelationships = useCallback ( async ( ) => {
385435 const fileCache = plugin . app . metadataCache . getFileCache ( activeFile ) ;
@@ -395,43 +445,15 @@ const CurrentRelationships = ({
395445 if ( activeIds . size === 0 ) return ;
396446
397447 const relations = await getRelationsForFile ( plugin , activeFile ) ;
398- const tempRelationships = new Map < string , GroupedRelation > ( ) ;
399448
400- for ( const r of relations ) {
401- const relationType = plugin . settings . relationTypes . find (
402- ( rt ) => rt . id === r . type ,
403- ) ;
404- if ( ! relationType ) continue ;
405-
406- const isSource = activeIds . has ( r . source ) ;
407- const relationLabel = isSource
408- ? relationType . label
409- : relationType . complement ;
410- const relationKey = `${ r . type } -${ isSource ? "source" : "target" } ` ;
411-
412- if ( ! tempRelationships . has ( relationKey ) ) {
413- tempRelationships . set ( relationKey , {
414- relationTypeOptions : {
415- id : relationType . id ,
416- label : relationLabel ,
417- isSource,
418- } ,
419- linkedEntries : [ ] ,
420- } ) ;
421- }
449+ const accepted = relations . filter ( ( r ) => r . tentative !== false ) ;
450+ const tentative = relations . filter ( ( r ) => r . tentative === false ) ;
422451
423- const group = tempRelationships . get ( relationKey ) ! ;
424- const otherId = isSource ? r . destination : r . source ;
425- const linkedFile = resolveEndpointToFile ( plugin , otherId ) ;
426- if ( linkedFile ) {
427- const already = group . linkedEntries . some ( ( e ) => e . relation . id === r . id ) ;
428- if ( ! already ) {
429- group . linkedEntries . push ( { file : linkedFile , relation : r } ) ;
430- }
431- }
432- }
452+ const acceptedMap = buildGroupedRelations ( accepted , activeIds , plugin ) ;
453+ const tentativeMap = buildGroupedRelations ( tentative , activeIds , plugin ) ;
433454
434- setGroupedRelationships ( Array . from ( tempRelationships . values ( ) ) ) ;
455+ setAcceptedGroups ( Array . from ( acceptedMap . values ( ) ) ) ;
456+ setTentativeGroups ( Array . from ( tentativeMap . values ( ) ) ) ;
435457 } , [ activeFile , plugin ] ) ;
436458
437459 useEffect ( ( ) => {
@@ -452,84 +474,153 @@ const CurrentRelationships = ({
452474 entry . relation . destination ,
453475 relationTypeId ,
454476 ) ;
455-
456477 new Notice (
457478 `Successfully removed ${ relationType . label } with ${ entry . file . basename } ` ,
458479 ) ;
459-
460480 await loadCurrentRelationships ( ) ;
481+ onRelationsChange ?.( ) ;
461482 } catch ( error ) {
462483 console . error ( "Failed to delete relationship:" , error ) ;
463484 new Notice (
464485 `Failed to delete relationship: ${ error instanceof Error ? error . message : "Unknown error" } ` ,
465486 ) ;
466487 }
467488 } ,
468- [ plugin , loadCurrentRelationships ] ,
489+ [ plugin , loadCurrentRelationships , onRelationsChange ] ,
469490 ) ;
470491
471- if ( groupedRelationships . length === 0 ) return null ;
492+ const acceptRelation = useCallback (
493+ async ( relationId : string ) => {
494+ try {
495+ await updateRelation ( plugin , relationId , { tentative : true } ) ;
496+ await loadCurrentRelationships ( ) ;
497+ onRelationsChange ?.( ) ;
498+ } catch ( error ) {
499+ console . error ( "Failed to accept relationship:" , error ) ;
500+ new Notice (
501+ `Failed to accept relationship: ${ error instanceof Error ? error . message : "Unknown error" } ` ,
502+ ) ;
503+ }
504+ } ,
505+ [ plugin , loadCurrentRelationships , onRelationsChange ] ,
506+ ) ;
507+
508+ const renderEntries = (
509+ group : GroupedRelation ,
510+ renderAction : ( entry : LinkedEntry ) => React . ReactNode ,
511+ ) => (
512+ < li
513+ key = { `${ group . relationTypeOptions . id } -${ group . relationTypeOptions . isSource } ` }
514+ className = "border-modifier-border border-b px-3 py-2"
515+ >
516+ < div className = "mb-1 flex items-center" >
517+ < div className = "mr-2" >
518+ { group . relationTypeOptions . isSource ? "→" : "←" }
519+ </ div >
520+ < div className = "font-bold" > { group . relationTypeOptions . label } </ div >
521+ </ div >
522+ < ul className = "m-0 ml-6 list-none p-0" >
523+ { group . linkedEntries . map ( ( entry ) => (
524+ < li key = { entry . relation . id } className = "mt-1 flex items-center gap-2" >
525+ < a
526+ href = "#"
527+ className = "text-accent-text flex-1"
528+ onClick = { ( e ) => {
529+ e . preventDefault ( ) ;
530+ void plugin . app . workspace . openLinkText (
531+ entry . file . path ,
532+ activeFile . path ,
533+ ) ;
534+ } }
535+ >
536+ { entry . file . basename }
537+ </ a >
538+ { renderAction ( entry ) }
539+ </ li >
540+ ) ) }
541+ </ ul >
542+ </ li >
543+ ) ;
544+
545+ const hasAccepted = acceptedGroups . some ( ( g ) => g . linkedEntries . length > 0 ) ;
546+ const tentativeCount = tentativeGroups . reduce (
547+ ( sum , g ) => sum + g . linkedEntries . length ,
548+ 0 ,
549+ ) ;
550+ const hasTentative = tentativeCount > 0 ;
551+ const [ showTentative , setShowTentative ] = useState ( true ) ;
552+
553+ if ( ! hasAccepted && ! hasTentative ) return null ;
472554
473555 return (
474- < div className = "current-relationships mb-6" >
475- < h4 className = "mb-2 text-base font-medium" > Current Relationships</ h4 >
476- < ul className = "border-modifier-border m-0 list-none rounded border p-0" >
477- { groupedRelationships . map (
478- ( group ) =>
479- group . linkedEntries . length > 0 && (
480- < li
481- key = { `${ group . relationTypeOptions . id } -${ group . relationTypeOptions . isSource } ` }
482- className = "border-modifier-border border-b px-3 py-2"
483- >
484- < div className = "mb-1 flex items-center" >
485- < div className = "mr-2" >
486- { group . relationTypeOptions . isSource ? "→" : "←" }
487- </ div >
488- < div className = "font-bold" >
489- { group . relationTypeOptions . label }
490- </ div >
491- </ div >
492-
493- < ul className = "m-0 ml-6 list-none p-0" >
494- { group . linkedEntries . map ( ( entry ) => (
495- < li
496- key = { entry . relation . id }
497- className = "mt-1 flex items-center gap-2"
556+ < >
557+ { hasAccepted && (
558+ < div className = "current-relationships mb-6" >
559+ < h4 className = "mb-2 text-base font-medium" > Current Relationships</ h4 >
560+ < ul className = "border-modifier-border m-0 list-none rounded border p-0" >
561+ { acceptedGroups . map (
562+ ( group ) =>
563+ group . linkedEntries . length > 0 &&
564+ renderEntries ( group , ( entry ) => (
565+ < button
566+ className = "!text-muted hover:!text-error flex h-6 w-6 cursor-pointer items-center justify-center border-0 !bg-transparent text-sm"
567+ onClick = { ( e ) => {
568+ e . preventDefault ( ) ;
569+ void deleteRelationship (
570+ entry ,
571+ group . relationTypeOptions . id ,
572+ ) ;
573+ } }
574+ title = "Delete relationship"
575+ >
576+ ×
577+ </ button >
578+ ) ) ,
579+ ) }
580+ </ ul >
581+ </ div >
582+ ) }
583+ { hasTentative && (
584+ < div className = "tentative-relationships mb-6" >
585+ < div className = "mb-2 flex items-center justify-between" >
586+ < span className = "text-base font-medium" >
587+ { showTentative ? "Hide" : "Show" } ({ tentativeCount } ) tentative{ " " }
588+ { tentativeCount === 1 ? "relation" : "relations" }
589+ </ span >
590+ < div
591+ className = { `checkbox-container ${ showTentative ? "is-enabled" : "" } ` }
592+ onClick = { ( ) => setShowTentative ( ( v ) => ! v ) }
593+ >
594+ < input type = "checkbox" checked = { showTentative } readOnly />
595+ </ div >
596+ </ div >
597+ { showTentative && (
598+ < ul className = "border-modifier-border m-0 list-none rounded border p-0" >
599+ { tentativeGroups . map (
600+ ( group ) =>
601+ group . linkedEntries . length > 0 &&
602+ renderEntries ( group , ( entry ) => (
603+ < button
604+ className = "!text-muted hover:!text-accent flex h-6 w-6 cursor-pointer items-center justify-center border-0 !bg-transparent text-sm"
605+ onClick = { ( e ) => {
606+ e . preventDefault ( ) ;
607+ void acceptRelation ( entry . relation . id ) ;
608+ } }
609+ title = {
610+ entry . relation . importedFromRid
611+ ? `Accept relation from space ${ getAndFormatImportSource ( entry . relation . importedFromRid , plugin . settings . spaceNames ) } `
612+ : "Accept relationship"
613+ }
498614 >
499- < a
500- href = "#"
501- className = "text-accent-text flex-1"
502- onClick = { ( e ) => {
503- e . preventDefault ( ) ;
504- void plugin . app . workspace . openLinkText (
505- entry . file . path ,
506- activeFile . path ,
507- ) ;
508- } }
509- >
510- { entry . file . basename }
511- </ a >
512- < button
513- className = "!text-muted hover:!text-error flex h-6 w-6 cursor-pointer items-center justify-center border-0 !bg-transparent text-sm"
514- onClick = { ( e ) => {
515- e . preventDefault ( ) ;
516- void deleteRelationship (
517- entry ,
518- group . relationTypeOptions . id ,
519- ) ;
520- } }
521- title = "Delete relationship"
522- >
523- ×
524- </ button >
525- </ li >
526- ) ) }
527- </ ul >
528- </ li >
529- ) ,
530- ) }
531- </ ul >
532- </ div >
615+ ✓
616+ </ button >
617+ ) ) ,
618+ ) }
619+ </ ul >
620+ ) }
621+ </ div >
622+ ) }
623+ </ >
533624 ) ;
534625} ;
535626
@@ -546,6 +637,7 @@ export const RelationshipSection = ({
546637 < CurrentRelationships
547638 activeFile = { activeFile }
548639 relationsVersion = { relationsVersion }
640+ onRelationsChange = { onRelationsChange }
549641 />
550642 < AddRelationship
551643 activeFile = { activeFile }
0 commit comments