@@ -596,14 +596,30 @@ export const createAllRelationShapeUtils = (
596596 const relations = Object . values ( discourseContext . relations ) . flat ( ) ;
597597 const relation = relations . find ( ( r ) => r . id === arrow . type ) ;
598598 if ( ! relation ) return ;
599- const possibleTargets = discourseContext . relations [ relation . label ]
600- . filter ( ( r ) => r . source === relation . source )
601- . map ( ( r ) => r . destination ) ;
602599
603- if ( ! possibleTargets . includes ( target . type ) ) {
604- const uniqueTargets = [ ...new Set ( possibleTargets ) ] ;
605- const uniqueTargetTexts = uniqueTargets . map (
606- ( t ) => discourseContext . nodes [ t ] . text ,
600+ const sourceNodeType = source . type ;
601+ const targetNodeType = target . type ;
602+
603+ const { isDirect, isReverse } = this . checkConnectionType (
604+ relation ,
605+ sourceNodeType ,
606+ targetNodeType ,
607+ ) ;
608+
609+ if ( ! isDirect && ! isReverse ) {
610+ const possibleTargets = discourseContext . relations [ relation . label ]
611+ . filter ( ( r ) => r . source === relation . source )
612+ . map ( ( r ) => r . destination ) ;
613+ const possibleReverseTargets = discourseContext . relations [
614+ relation . label
615+ ]
616+ . filter ( ( r ) => r . destination === relation . source )
617+ . map ( ( r ) => r . source ) ;
618+ const allPossibleTargets = [
619+ ...new Set ( [ ...possibleTargets , ...possibleReverseTargets ] ) ,
620+ ] ;
621+ const uniqueTargetTexts = allPossibleTargets . map (
622+ ( t ) => discourseContext . nodes [ t ] ?. text || t ,
607623 ) ;
608624 return deleteAndWarn (
609625 `Target node must be of type ${ uniqueTargetTexts . join ( ", " ) } ` ,
@@ -612,6 +628,7 @@ export const createAllRelationShapeUtils = (
612628 if ( arrow . type !== target . type ) {
613629 editor . updateShapes ( [ { id : arrow . id , type : target . type } ] ) ;
614630 }
631+ arrow = editor . getShape ( arrow . id ) as DiscourseRelationShape ;
615632 if ( getSetting < boolean > ( USE_REIFIED_RELATIONS , false ) ) {
616633 const sourceAsDNS = asDiscourseNodeShape ( source , editor ) ;
617634 const targetAsDNS = asDiscourseNodeShape ( target , editor ) ;
@@ -629,8 +646,9 @@ export const createAllRelationShapeUtils = (
629646 } ) ;
630647 }
631648 } else {
632- const { triples, label : relationLabel } = relation ;
633- const isOriginal = arrow . props . text === relationLabel ;
649+ const { triples } = relation ;
650+ const isOriginal = isDirect && ! isReverse ;
651+
634652 const newTriples = triples
635653 . map ( ( t ) => {
636654 if ( / i s a / i. test ( t [ 1 ] ) ) {
@@ -813,6 +831,52 @@ export const createAllRelationShapeUtils = (
813831 return update ;
814832 }
815833
834+ // Validate target node type compatibility before creating binding
835+ if (
836+ target . type !== "arrow" &&
837+ otherBinding &&
838+ target . id !== otherBinding . toId &&
839+ ( ! currentBinding || target . id !== currentBinding . toId )
840+ ) {
841+ const sourceNodeId = otherBinding . toId ;
842+ const sourceNode = this . editor . getShape ( sourceNodeId ) ;
843+ const targetNodeType = target . type ;
844+ const sourceNodeType = sourceNode ?. type ;
845+
846+ if ( sourceNodeType && targetNodeType && shape . type ) {
847+ const isValidConnection = this . isValidNodeConnection (
848+ sourceNodeType ,
849+ targetNodeType ,
850+ shape . type ,
851+ ) ;
852+
853+ if ( ! isValidConnection ) {
854+ const sourceNodeTypeText =
855+ discourseContext . nodes [ sourceNodeType ] ?. text || sourceNodeType ;
856+ const targetNodeTypeText =
857+ discourseContext . nodes [ targetNodeType ] ?. text || targetNodeType ;
858+ const relations = Object . values (
859+ discourseContext . relations ,
860+ ) . flat ( ) ;
861+ const relation = relations . find ( ( r ) => r . id === shape . type ) ;
862+ const relationLabel = relation ?. label || shape . type ;
863+
864+ const errorMessage = `Cannot connect "${ sourceNodeTypeText } " to "${ targetNodeTypeText } " with "${ relationLabel } " relation` ;
865+ dispatchToastEvent ( {
866+ id : `tldraw-invalid-connection-${ shape . id } ` ,
867+ title : "Invalid Connection" ,
868+ description : errorMessage ,
869+ severity : "error" ,
870+ } ) ;
871+
872+ removeArrowBinding ( this . editor , shape , handleId ) ;
873+ update . props ! [ handleId ] = { x : handle . x , y : handle . y } ;
874+ this . editor . deleteShapes ( [ shape . id ] ) ;
875+ return update ;
876+ }
877+ }
878+ }
879+
816880 // we've got a target! the handle is being dragged over a shape, bind to it
817881
818882 const targetGeometry = this . editor . getShapeGeometry ( target ) ;
@@ -889,6 +953,37 @@ export const createAllRelationShapeUtils = (
889953 this . editor . setHintingShapes ( [ target . id ] ) ;
890954
891955 const newBindings = getArrowBindings ( this . editor , shape ) ;
956+
957+ // Check if both ends are bound and determine the correct text based on direction
958+ if ( newBindings . start && newBindings . end ) {
959+ const relations = Object . values ( discourseContext . relations ) . flat ( ) ;
960+ const relation = relations . find ( ( r ) => r . id === shape . type ) ;
961+
962+ if ( relation ) {
963+ const startNode = this . editor . getShape ( newBindings . start . toId ) ;
964+ const endNode = this . editor . getShape ( newBindings . end . toId ) ;
965+
966+ if ( startNode && endNode ) {
967+ const startNodeType = startNode . type ;
968+ const endNodeType = endNode . type ;
969+
970+ const { isDirect, isReverse } = this . checkConnectionType (
971+ relation ,
972+ startNodeType ,
973+ endNodeType ,
974+ ) ;
975+
976+ const newText =
977+ isReverse && ! isDirect ? relation . complement : relation . label ;
978+
979+ if ( shape . props . text !== newText ) {
980+ update . props = update . props || { } ;
981+ update . props . text = newText ;
982+ }
983+ }
984+ }
985+ }
986+
892987 if (
893988 newBindings . start &&
894989 newBindings . end &&
@@ -1569,6 +1664,40 @@ export class BaseDiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape>
15691664 ] ;
15701665 }
15711666
1667+ checkConnectionType (
1668+ relation : { source : string ; destination : string } ,
1669+ sourceNodeType : string ,
1670+ targetNodeType : string ,
1671+ ) : { isDirect : boolean ; isReverse : boolean } {
1672+ const isDirect =
1673+ sourceNodeType === relation . source &&
1674+ targetNodeType === relation . destination ;
1675+
1676+ const isReverse =
1677+ sourceNodeType === relation . destination &&
1678+ targetNodeType === relation . source ;
1679+
1680+ return { isDirect, isReverse } ;
1681+ }
1682+
1683+ isValidNodeConnection (
1684+ sourceNodeType : string ,
1685+ targetNodeType : string ,
1686+ relationId : string ,
1687+ ) : boolean {
1688+ const relations = Object . values ( discourseContext . relations ) . flat ( ) ;
1689+ const relation = relations . find ( ( r ) => r . id === relationId ) ;
1690+ if ( ! relation ) return false ;
1691+
1692+ const { isDirect, isReverse } = this . checkConnectionType (
1693+ relation ,
1694+ sourceNodeType ,
1695+ targetNodeType ,
1696+ ) ;
1697+
1698+ return isDirect || isReverse ;
1699+ }
1700+
15721701 component ( shape : DiscourseRelationShape ) {
15731702 // eslint-disable-next-line react-hooks/rules-of-hooks
15741703 // const theme = useDefaultColorTheme();
0 commit comments