Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 7 additions & 116 deletions apps/roam/src/components/canvas/overlays/DragHandleOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { TLShapeId, createShapeId, useEditor, useValue } from "tldraw";
import { TLShapeId, useEditor, useValue } from "tldraw";
import {
DiscourseNodeShape,
getDiscourseNodeTypeId,
} from "~/components/canvas/DiscourseNodeUtil";
import {
BaseDiscourseRelationUtil,
DiscourseRelationShape,
getRelationColor,
} from "~/components/canvas/DiscourseRelationShape/DiscourseRelationUtil";
import { createOrUpdateArrowBinding } from "~/components/canvas/DiscourseRelationShape/helpers";
import {
checkConnectionType,
getAllRelations,
hasValidRelationTypes,
isDiscourseNodeShape,
} from "~/components/canvas/canvasUtils";
import { dispatchToastEvent } from "~/components/canvas/ToastListener";
import { RelationTypeDropdown } from "./RelationTypeDropdown";
import { createDefaultRelationBetweenNodes } from "./relationCreation";

const HANDLE_RADIUS = 5;
const HANDLE_HIT_AREA = 12;
Expand Down Expand Up @@ -263,115 +256,13 @@ export const DragHandleOverlay = () => {
(relationId: string) => {
if (!pending) return;

const selectedRelation = getAllRelations().find(
(r) => r.id === relationId,
);
if (!selectedRelation) {
setPending(null);
sourceNodeRef.current = null;
return;
}

const color = getRelationColor(selectedRelation.label);

// Determine direction: if we dragged from the relation's destination type,
// the arrow is in reverse and should display the complement label.
const sourceNode = editor.getShape(pending.sourceId);
const targetNode = editor.getShape(pending.targetId);
if (
!sourceNode ||
!targetNode ||
!isDiscourseNodeShape(editor, sourceNode) ||
!isDiscourseNodeShape(editor, targetNode)
) {
setPending(null);
sourceNodeRef.current = null;
return;
}
const sourceNodeTypeId = getDiscourseNodeTypeId({ shape: sourceNode });
const targetNodeTypeId = getDiscourseNodeTypeId({ shape: targetNode });
const { isReverse } = checkConnectionType(
selectedRelation,
sourceNodeTypeId,
targetNodeTypeId,
);
const label =
isReverse && selectedRelation.complement
? selectedRelation.complement
: selectedRelation.label;

// Get source bounds for arrow positioning
const sourceBounds = editor.getShapePageBounds(pending.sourceId);
if (!sourceBounds) {
setPending(null);
sourceNodeRef.current = null;
return;
}

// Create the real relation shape with the correct type
const arrowId = createShapeId();
editor.createShape<DiscourseRelationShape>({
id: arrowId,
type: relationId,
x: sourceBounds.midX,
y: sourceBounds.midY,
props: {
color,
text: label,
dash: "draw",
size: "m",
fill: "none",
bend: 0,
start: { x: 0, y: 0 },
end: { x: 0, y: 0 },
arrowheadStart: "none",
arrowheadEnd: "arrow",
labelPosition: 0.5,
font: "draw",
scale: 1,
},
void createDefaultRelationBetweenNodes({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaces the old inline drag-handle relation creation block with the same default-create path in a named helper. The drag flow still creates a fresh relation arrow with default geometry and center bindings; conversion does not use this helper because it has to preserve an existing arrow's geometry.

editor,
relationId,
sourceId: pending.sourceId,
targetId: pending.targetId,
});

const newArrow = editor.getShape<DiscourseRelationShape>(arrowId);
if (!newArrow) {
setPending(null);
sourceNodeRef.current = null;
return;
}

// Bind start and end
createOrUpdateArrowBinding(editor, newArrow, pending.sourceId, {
terminal: "start",
normalizedAnchor: { x: 0.5, y: 0.5 },
isPrecise: false,
isExact: false,
});
createOrUpdateArrowBinding(editor, newArrow, pending.targetId, {
terminal: "end",
normalizedAnchor: { x: 0.5, y: 0.5 },
isPrecise: false,
isExact: false,
});

// Persist via handleCreateRelationsInRoam
const util = editor.getShapeUtil(newArrow);
if (
util instanceof BaseDiscourseRelationUtil &&
"handleCreateRelationsInRoam" in util
) {
type UtilWithRoamPersistence = BaseDiscourseRelationUtil & {
handleCreateRelationsInRoam: (args: {
arrow: DiscourseRelationShape;
targetId: TLShapeId;
}) => Promise<void>;
};
void (util as UtilWithRoamPersistence).handleCreateRelationsInRoam({
arrow: editor.getShape<DiscourseRelationShape>(arrowId) ?? newArrow,
targetId: pending.targetId,
});
}

editor.select(arrowId);
setPending(null);
sourceNodeRef.current = null;
},
Expand Down
59 changes: 6 additions & 53 deletions apps/roam/src/components/canvas/overlays/RelationTypeDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { TLShapeId, useEditor, DefaultColorThemePalette } from "tldraw";
import { getRelationColor } from "~/components/canvas/DiscourseRelationShape/DiscourseRelationUtil";
import {
checkConnectionType,
getAllRelations,
isDiscourseNodeShape,
} from "~/components/canvas/canvasUtils";
import { isRelationComplete } from "~/utils/isRelationComplete";
import { getDiscourseNodeTypeId } from "~/components/canvas/DiscourseNodeUtil";
import { TLShapeId, useEditor } from "tldraw";
import { getValidRelationTypesBetween } from "./relationCreation";

type RelationTypeDropdownProps = {
sourceId: TLShapeId;
Expand All @@ -27,50 +20,10 @@ export const RelationTypeDropdown = ({
const editor = useEditor();
const dropdownRef = useRef<HTMLDivElement>(null);

// Get valid relation types based on source/target node types
const validRelationTypes = useMemo(() => {
const startNode = editor.getShape(sourceId);
const endNode = editor.getShape(targetId);
if (!startNode || !endNode) return [];

// Verify both are discourse nodes
if (
!isDiscourseNodeShape(editor, startNode) ||
!isDiscourseNodeShape(editor, endNode)
)
return [];

const startNodeType = getDiscourseNodeTypeId({ shape: startNode });
const endNodeType = getDiscourseNodeTypeId({ shape: endNode });

const colorPalette = DefaultColorThemePalette.lightMode;
const validTypes: { id: string; label: string; color: string }[] = [];
const allRelations = getAllRelations();
const seenLabels = new Set<string>();

for (const relation of allRelations) {
if (!isRelationComplete(relation)) continue;
const { isDirect: isForward, isReverse } = checkConnectionType(
relation,
startNodeType,
endNodeType,
);

if (!isForward && !isReverse) continue;

const label =
isReverse && relation.complement ? relation.complement : relation.label;

if (!seenLabels.has(label)) {
seenLabels.add(label);
const tldrawColor = getRelationColor(relation.label);
const hexColor = colorPalette[tldrawColor]?.solid ?? "#333";
validTypes.push({ id: relation.id, label, color: hexColor });
}
}

return validTypes;
}, [editor, sourceId, targetId]);
const validRelationTypes = useMemo(
() => getValidRelationTypesBetween(editor, sourceId, targetId),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This keeps the dropdown's relation filtering behavior the same, but reuses the same valid-relation lookup that the new arrow context menu needs.

[editor, sourceId, targetId],
);

// Handle click outside
useEffect(() => {
Expand Down
Loading