Skip to content
Open
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
50 changes: 49 additions & 1 deletion apps/obsidian/src/components/canvas/CustomContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
import type { TFile } from "obsidian";
import { usePlugin } from "~/components/PluginContext";
import { convertToDiscourseNode } from "./utils/convertToDiscourseNode";
import {
convertArrowToDiscourseRelation,
getValidRelationTypesForArrow,
} from "./utils/convertArrowToDiscourseRelation";

type CustomContextMenuProps = {
canvasFile: TFile;
Expand All @@ -34,9 +38,54 @@ export const CustomContextMenu = ({
selectedShape &&
(selectedShape.type === "text" || selectedShape.type === "image");

const isReadonly = useValue(
"isReadonly",
() => editor.getInstanceState().isReadonly,
[editor],
);

const validRelationTypes = useValue(
"validRelationTypes",
() => {
if (!selectedShape || selectedShape.type !== "arrow") return [];
return getValidRelationTypesForArrow({
editor,
plugin,
arrowId: selectedShape.id,
});
},
[editor, plugin, selectedShape?.id, selectedShape?.type],
);

const shouldShowRelationMenu =
selectedShape?.type === "arrow" && validRelationTypes.length > 0;

return (
<DefaultContextMenu {...props}>
<DefaultContextMenuContent />
{shouldShowRelationMenu && (
<TldrawUiMenuGroup id="relation">
<TldrawUiMenuSubmenu id="relation-submenu" label="Relation">
{validRelationTypes.map((relationType) => (
<TldrawUiMenuItem
key={relationType.id}
id={`relation-${relationType.id}`}
label={relationType.label}
disabled={isReadonly}
onSelect={() => {
void convertArrowToDiscourseRelation({
editor,
plugin,
canvasFile,
arrowId: selectedShape.id,
relationTypeId: relationType.id,
});
}}
/>
))}
</TldrawUiMenuSubmenu>
</TldrawUiMenuGroup>
)}
{shouldShowConvertTo && (
<TldrawUiMenuGroup id="convert-to">
<TldrawUiMenuSubmenu id="convert-to-submenu" label="Convert To">
Expand All @@ -63,4 +112,3 @@ export const CustomContextMenu = ({
</DefaultContextMenu>
);
};

Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ export class BaseRelationBindingUtil extends BindingUtil<RelationBinding> {
}
}

static isRelationReified(shapeId: TLShapeId): boolean {
return BaseRelationBindingUtil.reifiedArrows.has(shapeId);
}

static markRelationReified(shapeId: TLShapeId): void {
BaseRelationBindingUtil.reifiedArrows.add(shapeId);
}

static unmarkRelationReified(shapeId: TLShapeId): void {
BaseRelationBindingUtil.reifiedArrows.delete(shapeId);
}

/**
* Check selected relation shapes for completed bindings
* Called from mouseup event handler
Expand All @@ -150,19 +162,20 @@ export class BaseRelationBindingUtil extends BindingUtil<RelationBinding> {
) as DiscourseRelationShape[];

relationShapes.forEach((arrow) => {
if (arrow.meta.relationInstanceId) return;

const bindings = getArrowBindings(editor, arrow);
if (
bindings.start &&
bindings.end &&
!BaseRelationBindingUtil.reifiedArrows.has(arrow.id)
!BaseRelationBindingUtil.isRelationReified(arrow.id)
) {
BaseRelationBindingUtil.reifiedArrows.add(arrow.id);
const util = editor.getShapeUtil(arrow);
if (util instanceof DiscourseRelationUtil) {
BaseRelationBindingUtil.markRelationReified(arrow.id);
util.reifyRelation(arrow, bindings).catch((error) => {
console.error("Failed to reify relation:", error);
// Remove from reified set on error so it can be retried
BaseRelationBindingUtil.reifiedArrows.delete(arrow.id);
BaseRelationBindingUtil.unmarkRelationReified(arrow.id);
});
}
}
Expand Down
121 changes: 45 additions & 76 deletions apps/obsidian/src/components/canvas/shapes/DiscourseRelationShape.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ import {
updateArrowTerminal,
} from "~/components/canvas/utils/relationUtils";
import { RelationBindings } from "./DiscourseRelationBinding";
import { DiscourseNodeShape, DiscourseNodeUtil } from "./DiscourseNodeShape";
import { addRelationToRelationsJson } from "~/components/canvas/utils/relationJsonUtils";
import { DiscourseNodeShape } from "./DiscourseNodeShape";
import { persistRelationBetweenNodeShapes } from "~/components/canvas/utils/relationJsonUtils";
import {
getDiscourseNodeAtPoint,
getDiscourseNodeTypeId,
getRelationDirection,
getRelationLabelForDirection,
isValidRelationConnection,
} from "~/components/canvas/utils/relationTypeUtils";
import { getNodeTypeById, getRelationTypeById } from "~/utils/typeUtils";
Expand Down Expand Up @@ -1084,20 +1084,13 @@ export class DiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape> {

if (!relationType) return;

const { direct, reverse } = getRelationDirection({
const newText = getRelationLabelForDirection({
discourseRelations: plugin.settings.discourseRelations,
relationTypeId,
relationType,
sourceNodeTypeId: startNodeTypeId,
targetNodeTypeId: endNodeTypeId,
});

let newText = relationType.label; // Default to main label

if (reverse && !direct) {
// This is purely a reverse connection, use complement
newText = relationType.complement;
}

// Update the shape text if it's different
if (shape.props.text !== newText) {
this.editor.updateShapes([
Expand All @@ -1122,77 +1115,53 @@ export class DiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape> {
return;
}

try {
const startNode = this.editor.getShape(bindings.start.toId);
const endNode = this.editor.getShape(bindings.end.toId);

if (
!startNode ||
!endNode ||
startNode.type !== "discourse-node" ||
endNode.type !== "discourse-node"
) {
return;
}

const startNodeUtil = this.editor.getShapeUtil(startNode);
const endNodeUtil = this.editor.getShapeUtil(endNode);
const startNode = this.editor.getShape(bindings.start.toId);
const endNode = this.editor.getShape(bindings.end.toId);

// Get the files associated with both nodes
const sourceFile = await (startNodeUtil as DiscourseNodeUtil).getFile(
startNode as DiscourseNodeShape,
{
app: this.options.app,
canvasFile: this.options.canvasFile,
},
);
const targetFile = await (endNodeUtil as DiscourseNodeUtil).getFile(
endNode as DiscourseNodeShape,
{
app: this.options.app,
canvasFile: this.options.canvasFile,
},
);
if (
!startNode ||
!endNode ||
startNode.type !== "discourse-node" ||
endNode.type !== "discourse-node"
) {
return;
}

if (!sourceFile || !targetFile) {
console.warn("Could not resolve files for relation nodes");
return;
}
const persistResult = await persistRelationBetweenNodeShapes({
plugin: this.options.plugin,
canvasFile: this.options.canvasFile,
editor: this.editor,
startNode: startNode as DiscourseNodeShape,
endNode: endNode as DiscourseNodeShape,
relationTypeId: shape.props.relationTypeId,
});

const { alreadyExisted, relationInstanceId } =
await addRelationToRelationsJson({
plugin: this.options.plugin,
sourceFile,
targetFile,
relationTypeId: shape.props.relationTypeId,
});
if (!persistResult.ok) {
return;
}

if (relationInstanceId) {
this.editor.updateShape({
id: shape.id,
type: shape.type,
meta: { ...shape.meta, relationInstanceId },
});
}
if (persistResult.relationInstanceId) {
this.editor.updateShape({
id: shape.id,
type: shape.type,
meta: {
...shape.meta,
relationInstanceId: persistResult.relationInstanceId,
},
});
}

const relationType = getRelationTypeById(
this.options.plugin,
shape.props.relationTypeId,
);
const relationType = getRelationTypeById(
this.options.plugin,
shape.props.relationTypeId,
);

if (relationType && !alreadyExisted) {
showToast({
severity: "success",
title: "Relation Created",
description: `Added ${relationType.label} relation between ${sourceFile.basename} and ${targetFile.basename}`,
});
}
} catch (error) {
console.error("Failed to reify relation:", error);
if (relationType && !persistResult.alreadyExisted) {
showToast({
severity: "error",
title: "Failed to Save Relation",
description: "Could not save relation to files",
severity: "success",
title: "Relation Created",
description: `Added ${relationType.label} relation between ${persistResult.sourceFile.basename} and ${persistResult.targetFile.basename}`,
targetCanvasId: this.options.canvasFile.path,
});
}
Comment thread
trangdoan982 marked this conversation as resolved.
}
Expand Down
Loading