Skip to content

Commit 401dfe8

Browse files
committed
allow reverse label creation
1 parent 4dadfc5 commit 401dfe8

2 files changed

Lines changed: 145 additions & 13 deletions

File tree

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
@@ -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 (/is 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

Comments
 (0)