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
35 changes: 35 additions & 0 deletions apps/obsidian/src/components/AdminPanelSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const AdminPanelSettings = () => {
const [username, setUsername] = useState<string>(
plugin.settings.username || "",
);
const [nodeCardContextMenuEnabled, setNodeCardContextMenuEnabled] =
useState<boolean>(plugin.settings.nodeCardContextMenuEnabled ?? false);

const handleSyncModeToggle = useCallback(
async (newValue: boolean) => {
Expand Down Expand Up @@ -43,6 +45,15 @@ export const AdminPanelSettings = () => {
await updateUsername(plugin, newValue);
};

const handleNodeCardContextMenuToggle = useCallback(
async (newValue: boolean) => {
setNodeCardContextMenuEnabled(newValue);
plugin.settings.nodeCardContextMenuEnabled = newValue;
await plugin.saveSettings();
},
[plugin],
);

const handleLoginHandoff = async () => {
const client = await getLoggedInClient(plugin);
if (!client) {
Expand Down Expand Up @@ -72,6 +83,30 @@ export const AdminPanelSettings = () => {

return (
<div className="general-settings">
<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">(BETA) Node card context menu</div>
<div className="setting-item-description">
Show discourse context and styling tabs when a node card is selected
on a canvas
</div>
</div>
<div className="setting-item-control">
<div
className={`checkbox-container ${nodeCardContextMenuEnabled ? "is-enabled" : ""}`}
onClick={() =>
void handleNodeCardContextMenuToggle(!nodeCardContextMenuEnabled)
}
>
<input
type="checkbox"
checked={nodeCardContextMenuEnabled}
aria-label="Enable node card context menu"
readOnly
/>
</div>
</div>
</div>
<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">(BETA) Sync mode enable</div>
Expand Down
97 changes: 97 additions & 0 deletions apps/obsidian/src/components/canvas/NodeCardContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { createElement, useEffect, useState, type ComponentType } from "react";
import type { TFile } from "obsidian";
import {
DefaultStylePanel,
DefaultStylePanelContent,
useEditor,
useRelevantStyles,
useValue,
type TLUiStylePanelContentProps,
type TLUiStylePanelProps,
} from "tldraw";
import type DiscourseGraphPlugin from "~/index";
import type { DiscourseNodeShape } from "./shapes/DiscourseNodeShape";
import { RelationsPanelContent } from "./overlays/RelationPanel";

type NodeCardContextMenuProps = TLUiStylePanelProps & {
plugin: DiscourseGraphPlugin;
canvasFile: TFile;
};

const NODE_CARD_CONTEXT_MENU_TABS = [
{ id: "context", label: "Context" },
{ id: "styling", label: "Styling" },
] as const;

type NodeCardContextMenuTab =
(typeof NODE_CARD_CONTEXT_MENU_TABS)[number]["id"];

const DefaultStylePanelComponent =
DefaultStylePanel as unknown as ComponentType<TLUiStylePanelProps>;
const DefaultStylePanelContentComponent =
DefaultStylePanelContent as unknown as ComponentType<TLUiStylePanelContentProps>;

export const NodeCardContextMenu = ({
plugin,
canvasFile,
isMobile,
}: NodeCardContextMenuProps) => {
const editor = useEditor();
const styles = useRelevantStyles();
const isEnabled = plugin.settings.nodeCardContextMenuEnabled ?? false;
const selectedShape = useValue(
"selected shape for node card context menu",
() =>
editor.getCurrentToolId() === "select"
? editor.getOnlySelectedShape()
: null,
[editor],
);
const selectedNode =
isEnabled && selectedShape?.type === "discourse-node"
? (selectedShape as DiscourseNodeShape)
: null;
const [activeTab, setActiveTab] = useState<NodeCardContextMenuTab>("context");

useEffect(() => {
setActiveTab("context");
}, [selectedNode?.id]);

if (!selectedNode) {
return createElement(DefaultStylePanelComponent, { isMobile });
}

return createElement(
Comment thread
sid597 marked this conversation as resolved.
DefaultStylePanelComponent,
{ isMobile },
<div className="dg-node-card-menu">
<div className="border-modifier-border grid grid-cols-2 border-b">
{NODE_CARD_CONTEXT_MENU_TABS.map(({ id, label }) => (
<button
key={id}
type="button"
aria-pressed={activeTab === id}
className={`cursor-pointer px-3 py-2 text-xs font-semibold ${
activeTab === id ? "accent-border-bottom" : ""
}`}
onClick={() => setActiveTab(id)}
>
{label}
</button>
))}
</div>

{activeTab === "context" ? (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Image

I'd add styling to show the active state of each tab as well.

<div className="p-3">
<RelationsPanelContent
plugin={plugin}
canvasFile={canvasFile}
nodeShape={selectedNode}
/>
</div>
) : (
createElement(DefaultStylePanelContentComponent, { styles })
)}
Comment on lines +84 to +94

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Selecting a node card silently edits the canvas note and can append duplicate link blocks

The relations list is now shown automatically whenever a single node card is selected (RelationsPanel at apps/obsidian/src/components/canvas/NodeCardContextMenu.tsx:85-91), and merely showing it writes new link blocks into the canvas note, so users get unexpected file edits just by clicking a card.
Impact: Clicking a node card can modify the saved canvas note without any user action, and repeated re-renders while dragging can append the same link text over and over.

How rendering the relations list triggers vault writes

Each listed related file is rendered as a RelationFileItem, whose mount effect calls checkExistingRelation (apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx:76-90). checkExistingRelation calls ensureBlockRefForFile (apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx:269-273), which, when no existing block ref points at the target file, appends [[link]]\n^uuid to the canvas markdown via app.vault.process (apps/obsidian/src/components/canvas/stores/assetStore.ts:143-155).

Previously this only ran after the user explicitly opened the floating Relations panel via the overlay button (apps/obsidian/src/components/canvas/overlays/RelationOverlay.tsx:80-101). With the new style-panel menu the panel mounts on every single-node selection.

Additionally, checkExistingRelation is re-created on every RelationsPanel render and is in the effect's dependency array, and NodeCardContextMenu re-renders whenever the selected shape record changes (e.g. while dragging, via useValue at apps/obsidian/src/components/canvas/NodeCardContextMenu.tsx:42-46). Because Obsidian's metadataCache updates asynchronously, consecutive invocations can miss the just-written block and append another duplicate block for the same file.

Prompt for agents
Rendering RelationsPanel now happens automatically on node selection (NodeCardContextMenu Context tab), but the panel's per-item relation check has a side effect: checkExistingRelation in apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx calls ensureBlockRefForFile, which appends a new '[[link]]\n^uuid' block to the canvas markdown file (apps/obsidian/src/components/canvas/stores/assetStore.ts) when none exists. This means merely selecting a card mutates the note. It is also re-run on every re-render because checkExistingRelation is re-created each render and is an effect dependency, and metadataCache lag can cause duplicate appends.

Possible approaches: introduce a read-only lookup (e.g. resolve an existing block ref without creating one) for the existence check, and only call ensureBlockRefForFile when the user actually creates a relation; and/or memoize checkExistingRelation (useCallback) so the per-item effect does not re-run on every render.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

</div>,
);
};
8 changes: 8 additions & 0 deletions apps/obsidian/src/components/canvas/TldrawViewComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
import ToastListener from "./ToastListener";
import { RelationsOverlay } from "./overlays/RelationOverlay";
import { DragHandleOverlay } from "./overlays/DragHandleOverlay";
import { NodeCardContextMenu } from "./NodeCardContextMenu";
import { WHITE_LOGO_SVG } from "~/icons";
import { CustomContextMenu } from "./CustomContextMenu";
import {
Expand Down Expand Up @@ -431,6 +432,13 @@ export const TldrawPreviewComponent = ({
ContextMenu: (props) => (
<CustomContextMenu canvasFile={file} props={props} />
),
StylePanel: (props) => (
<NodeCardContextMenu
plugin={plugin}
canvasFile={file}
{...props}
/>
),
SharePanel: () => {
const tools = useTools();
const isDiscourseNodeToolSelected = useIsToolSelected(
Expand Down
Loading