Skip to content

Commit 6d7905d

Browse files
committed
allow reverse label creation
1 parent 7b4cb58 commit 6d7905d

File tree

2 files changed

+145
-13
lines changed

2 files changed

+145
-13
lines changed

apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -363,14 +363,17 @@ export const createAllRelationShapeTools = (
363363
);
364364

365365
const relation = discourseContext.relations[name].find(
366-
(r) => r.source === target?.type,
366+
(r) => r.source === target?.type || r.destination === target?.type,
367367
);
368368
if (relation) {
369369
this.shapeType = relation.id;
370370
} else {
371-
const acceptableTypes = discourseContext.relations[name].map(
372-
(r) => discourseContext.nodes[r.source].text,
373-
);
371+
const acceptableTypes = discourseContext.relations[name]
372+
.flatMap((r) => [
373+
discourseContext.nodes[r.source]?.text,
374+
discourseContext.nodes[r.destination]?.text,
375+
])
376+
.filter(Boolean);
374377
const uniqueTypes = [...new Set(acceptableTypes)];
375378
this.cancelAndWarn(
376379
`Starting node must be one of ${uniqueTypes.join(", ")}`,

apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx

Lines changed: 138 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -537,14 +537,30 @@ export const createAllRelationShapeUtils = (
537537
const relations = Object.values(discourseContext.relations).flat();
538538
const relation = relations.find((r) => r.id === arrow.type);
539539
if (!relation) return;
540-
const possibleTargets = discourseContext.relations[relation.label]
541-
.filter((r) => r.source === relation.source)
542-
.map((r) => r.destination);
543540

544-
if (!possibleTargets.includes(target.type)) {
545-
const uniqueTargets = [...new Set(possibleTargets)];
546-
const uniqueTargetTexts = uniqueTargets.map(
547-
(t) => discourseContext.nodes[t].text,
541+
const sourceNodeType = source.type;
542+
const targetNodeType = target.type;
543+
544+
const { isDirect, isReverse } = this.checkConnectionType(
545+
relation,
546+
sourceNodeType,
547+
targetNodeType,
548+
);
549+
550+
if (!isDirect && !isReverse) {
551+
const possibleTargets = discourseContext.relations[relation.label]
552+
.filter((r) => r.source === relation.source)
553+
.map((r) => r.destination);
554+
const possibleReverseTargets = discourseContext.relations[
555+
relation.label
556+
]
557+
.filter((r) => r.destination === relation.source)
558+
.map((r) => r.source);
559+
const allPossibleTargets = [
560+
...new Set([...possibleTargets, ...possibleReverseTargets]),
561+
];
562+
const uniqueTargetTexts = allPossibleTargets.map(
563+
(t) => discourseContext.nodes[t]?.text || t,
548564
);
549565
return deleteAndWarn(
550566
`Target node must be of type ${uniqueTargetTexts.join(", ")}`,
@@ -553,6 +569,7 @@ export const createAllRelationShapeUtils = (
553569
if (arrow.type !== target.type) {
554570
editor.updateShapes([{ id: arrow.id, type: target.type }]);
555571
}
572+
arrow = editor.getShape(arrow.id) as DiscourseRelationShape;
556573
if (getSetting("use-reified-relations")) {
557574
const sourceAsDNS = asDiscourseNodeShape(source, editor);
558575
const targetAsDNS = asDiscourseNodeShape(target, editor);
@@ -572,8 +589,9 @@ export const createAllRelationShapeUtils = (
572589
}).catch(() => undefined);
573590
}
574591
} else {
575-
const { triples, label: relationLabel } = relation;
576-
const isOriginal = arrow.props.text === relationLabel;
592+
const { triples } = relation;
593+
const isOriginal = isDirect && !isReverse;
594+
577595
const newTriples = triples
578596
.map((t) => {
579597
if (/is a/i.test(t[1])) {
@@ -756,6 +774,52 @@ export const createAllRelationShapeUtils = (
756774
return update;
757775
}
758776

777+
// Validate target node type compatibility before creating binding
778+
if (
779+
target.type !== "arrow" &&
780+
otherBinding &&
781+
target.id !== otherBinding.toId &&
782+
(!currentBinding || target.id !== currentBinding.toId)
783+
) {
784+
const sourceNodeId = otherBinding.toId;
785+
const sourceNode = this.editor.getShape(sourceNodeId);
786+
const targetNodeType = target.type;
787+
const sourceNodeType = sourceNode?.type;
788+
789+
if (sourceNodeType && targetNodeType && shape.type) {
790+
const isValidConnection = this.isValidNodeConnection(
791+
sourceNodeType,
792+
targetNodeType,
793+
shape.type,
794+
);
795+
796+
if (!isValidConnection) {
797+
const sourceNodeTypeText =
798+
discourseContext.nodes[sourceNodeType]?.text || sourceNodeType;
799+
const targetNodeTypeText =
800+
discourseContext.nodes[targetNodeType]?.text || targetNodeType;
801+
const relations = Object.values(
802+
discourseContext.relations,
803+
).flat();
804+
const relation = relations.find((r) => r.id === shape.type);
805+
const relationLabel = relation?.label || shape.type;
806+
807+
const errorMessage = `Cannot connect "${sourceNodeTypeText}" to "${targetNodeTypeText}" with "${relationLabel}" relation`;
808+
dispatchToastEvent({
809+
id: `tldraw-invalid-connection-${shape.id}`,
810+
title: "Invalid Connection",
811+
description: errorMessage,
812+
severity: "error",
813+
});
814+
815+
removeArrowBinding(this.editor, shape, handleId);
816+
update.props![handleId] = { x: handle.x, y: handle.y };
817+
this.editor.deleteShapes([shape.id]);
818+
return update;
819+
}
820+
}
821+
}
822+
759823
// we've got a target! the handle is being dragged over a shape, bind to it
760824

761825
const targetGeometry = this.editor.getShapeGeometry(target);
@@ -832,6 +896,37 @@ export const createAllRelationShapeUtils = (
832896
this.editor.setHintingShapes([target.id]);
833897

834898
const newBindings = getArrowBindings(this.editor, shape);
899+
900+
// Check if both ends are bound and determine the correct text based on direction
901+
if (newBindings.start && newBindings.end) {
902+
const relations = Object.values(discourseContext.relations).flat();
903+
const relation = relations.find((r) => r.id === shape.type);
904+
905+
if (relation) {
906+
const startNode = this.editor.getShape(newBindings.start.toId);
907+
const endNode = this.editor.getShape(newBindings.end.toId);
908+
909+
if (startNode && endNode) {
910+
const startNodeType = startNode.type;
911+
const endNodeType = endNode.type;
912+
913+
const { isDirect, isReverse } = this.checkConnectionType(
914+
relation,
915+
startNodeType,
916+
endNodeType,
917+
);
918+
919+
const newText =
920+
isReverse && !isDirect ? relation.complement : relation.label;
921+
922+
if (shape.props.text !== newText) {
923+
update.props = update.props || {};
924+
update.props.text = newText;
925+
}
926+
}
927+
}
928+
}
929+
835930
if (
836931
newBindings.start &&
837932
newBindings.end &&
@@ -1454,6 +1549,40 @@ export class BaseDiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape>
14541549
];
14551550
}
14561551

1552+
checkConnectionType(
1553+
relation: { source: string; destination: string },
1554+
sourceNodeType: string,
1555+
targetNodeType: string,
1556+
): { isDirect: boolean; isReverse: boolean } {
1557+
const isDirect =
1558+
sourceNodeType === relation.source &&
1559+
targetNodeType === relation.destination;
1560+
1561+
const isReverse =
1562+
sourceNodeType === relation.destination &&
1563+
targetNodeType === relation.source;
1564+
1565+
return { isDirect, isReverse };
1566+
}
1567+
1568+
isValidNodeConnection(
1569+
sourceNodeType: string,
1570+
targetNodeType: string,
1571+
relationId: string,
1572+
): boolean {
1573+
const relations = Object.values(discourseContext.relations).flat();
1574+
const relation = relations.find((r) => r.id === relationId);
1575+
if (!relation) return false;
1576+
1577+
const { isDirect, isReverse } = this.checkConnectionType(
1578+
relation,
1579+
sourceNodeType,
1580+
targetNodeType,
1581+
);
1582+
1583+
return isDirect || isReverse;
1584+
}
1585+
14571586
component(shape: DiscourseRelationShape) {
14581587
// eslint-disable-next-line react-hooks/rules-of-hooks
14591588
// const theme = useDefaultColorTheme();

0 commit comments

Comments
 (0)