Skip to content

Commit 114ed85

Browse files
trangdoan982claude
andauthored
[ENG-1546] Relation creation via drag handles (Roam) (#923)
* ENG-1546: Implement relation creation via drag handles in tldraw (Roam) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * DRY * Fix DragHandleOverlay lint warnings Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Apply handle padding in viewport space Matches the Obsidian (eng-1547) pattern: padding is now applied after pageToViewport conversion so dots remain a fixed screen distance from the node edge regardless of zoom level. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Fix relation direction and label color in drag handle flow - Show complement label when creating relation in reverse direction - DRY checkConnectionType into canvasUtils (used by RelationUtil + overlays) - Remove labelColor override to match DiscourseRelationTool convention Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * change to tailwind as much as possible --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 900eba0 commit 114ed85

5 files changed

Lines changed: 652 additions & 17 deletions

File tree

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import {
107107
BaseDiscourseNodeUtil,
108108
DiscourseNodeShape,
109109
} from "~/components/canvas/DiscourseNodeUtil";
110+
import { checkConnectionType } from "~/components/canvas/canvasUtils";
110111
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
111112
import { AddReferencedNodeType } from "./DiscourseRelationTool";
112113
import { dispatchToastEvent } from "~/components/canvas/ToastListener";
@@ -1705,15 +1706,7 @@ export class BaseDiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape>
17051706
sourceNodeType: string,
17061707
targetNodeType: string,
17071708
): { isDirect: boolean; isReverse: boolean } {
1708-
const isDirect =
1709-
sourceNodeType === relation.source &&
1710-
targetNodeType === relation.destination;
1711-
1712-
const isReverse =
1713-
sourceNodeType === relation.destination &&
1714-
targetNodeType === relation.source;
1715-
1716-
return { isDirect, isReverse };
1709+
return checkConnectionType(relation, sourceNodeType, targetNodeType);
17171710
}
17181711

17191712
checkConnectionTypeAcrossLabel(

apps/roam/src/components/canvas/Tldraw.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import {
5555
} from "tldraw";
5656
import "tldraw/tldraw.css";
5757
import tldrawStyles from "./tldrawStyles";
58+
import { DragHandleOverlay } from "./overlays/DragHandleOverlay";
59+
import { isDiscourseNodeShape } from "./canvasUtils";
5860
import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes";
5961
import getDiscourseRelations, {
6062
DiscourseRelation,
@@ -533,12 +535,6 @@ const TldrawCanvasShared = ({
533535
);
534536
};
535537

536-
const isDiscourseNodeShape = (
537-
shape: TLShape,
538-
): shape is DiscourseNodeShape => {
539-
return allNodes.some((node) => node.type === shape.type);
540-
};
541-
542538
// Add state for tracking relation creation
543539
const relationCreationRef = useRef<{
544540
isCreating: boolean;
@@ -563,7 +559,7 @@ const TldrawCanvasShared = ({
563559
relationCreationRef.current.toolType = currentToolId;
564560

565561
// If we clicked on a discourse node, record it as the source
566-
if (shapeAtPoint && isDiscourseNodeShape(shapeAtPoint)) {
562+
if (shapeAtPoint && isDiscourseNodeShape(app, shapeAtPoint)) {
567563
relationCreationRef.current.sourceShapeId = shapeAtPoint.id;
568564
}
569565
}
@@ -588,7 +584,7 @@ const TldrawCanvasShared = ({
588584
relationCreationRef.current.relationShapeId = relationShape.id;
589585

590586
// Check if we have a target shape
591-
if (shapeAtPoint && isDiscourseNodeShape(shapeAtPoint)) {
587+
if (shapeAtPoint && isDiscourseNodeShape(app, shapeAtPoint)) {
592588
posthog.capture("Canvas: Relation Created", {
593589
relationType: relationShape.type,
594590
toolType: relationCreationRef.current.toolType || "",
@@ -693,6 +689,7 @@ const TldrawCanvasShared = ({
693689
const editorComponents: TLEditorComponents = {
694690
...defaultEditorComponents,
695691
OnTheCanvas: ToastListener,
692+
InFrontOfTheCanvas: DragHandleOverlay,
696693
};
697694
const customUiComponents: TLUiComponents = createUiComponents({
698695
allNodes,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Editor, TLShape } from "tldraw";
2+
import {
3+
BaseDiscourseNodeUtil,
4+
DiscourseNodeShape,
5+
} from "~/components/canvas/DiscourseNodeUtil";
6+
import { discourseContext } from "~/components/canvas/Tldraw";
7+
8+
export const isDiscourseNodeShape = (
9+
editor: Editor,
10+
shape: TLShape,
11+
): shape is DiscourseNodeShape => {
12+
try {
13+
return editor.getShapeUtil(shape) instanceof BaseDiscourseNodeUtil;
14+
} catch {
15+
return false;
16+
}
17+
};
18+
19+
export const getAllRelations = () =>
20+
Object.values(discourseContext.relations).flat();
21+
22+
export const checkConnectionType = (
23+
relation: { source: string; destination: string },
24+
sourceNodeType: string,
25+
targetNodeType: string,
26+
): { isDirect: boolean; isReverse: boolean } => ({
27+
isDirect:
28+
sourceNodeType === relation.source &&
29+
targetNodeType === relation.destination,
30+
isReverse:
31+
sourceNodeType === relation.destination &&
32+
targetNodeType === relation.source,
33+
});
34+
35+
export const hasValidRelationTypes = (
36+
sourceNodeType: string,
37+
targetNodeType: string,
38+
): boolean =>
39+
getAllRelations().some(
40+
(r) =>
41+
(r.source === sourceNodeType && r.destination === targetNodeType) ||
42+
(r.source === targetNodeType && r.destination === sourceNodeType),
43+
);

0 commit comments

Comments
 (0)