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
8 changes: 4 additions & 4 deletions apps/roam/src/components/CreateRelationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import getDiscourseRelations, {
type DiscourseRelation,
} from "~/utils/getDiscourseRelations";
import { createReifiedRelation } from "~/utils/createReifiedBlock";
import { findDiscourseNodeByTitleAndUid } from "~/utils/findDiscourseNode";
import findDiscourseNode from "~/utils/findDiscourseNode";
import { getDiscourseNodeFormatInnerExpression } from "~/utils/getDiscourseNodeFormatExpression";
import type { DiscourseNode } from "~/utils/getDiscourseNodes";
import type { Result } from "~/utils/types";
Expand Down Expand Up @@ -97,7 +97,7 @@ const CreateRelationDialog = ({

const identifyRelationMatch = (target: Result): RelWithDirection | null => {
if (target.text.length === 0) return null;
const selectedTargetType = findDiscourseNodeByTitleAndUid({
const selectedTargetType = findDiscourseNode({
uid: target.uid,
title: target.text,
nodes: discourseNodes,
Expand Down Expand Up @@ -280,7 +280,7 @@ const prepareRelData = (
nodeTitle = nodeTitle || getPageTitleByPageUid(targetNodeUid).trim();
const discourseNodeSchemas = getDiscourseNodes();
const relations = getDiscourseRelations();
const nodeSchema = findDiscourseNodeByTitleAndUid({
const nodeSchema = findDiscourseNode({
uid: targetNodeUid,
title: nodeTitle,
nodes: discourseNodeSchemas,
Expand Down Expand Up @@ -319,7 +319,7 @@ const extendProps = ({
}: CreateRelationDialogProps): ExtendedCreateRelationDialogProps | null => {
const nodeTitle = getPageTitleByPageUid(sourceNodeUid).trim();
const relData = prepareRelData(sourceNodeUid, nodeTitle);
const selectedSourceType = findDiscourseNodeByTitleAndUid({
const selectedSourceType = findDiscourseNode({
uid: sourceNodeUid,
title: nodeTitle,
});
Expand Down
2 changes: 1 addition & 1 deletion apps/roam/src/components/DiscourseContextOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const DiscourseContextOverlay = ({
() =>
getOverlayInfo(tag ?? (uid ? (getPageTitleByPageUid(uid) ?? "") : ""))
.then(({ refs, results }) => {
const discourseNode = findDiscourseNode(tagUid);
const discourseNode = findDiscourseNode({ uid: tagUid });
if (discourseNode) {
const attribute = getSettingValueFromTree({
tree: getBasicTreeByParentUid(discourseNode.type),
Expand Down
2 changes: 1 addition & 1 deletion apps/roam/src/components/Export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ const ExportDialog: ExportDialogComponent = ({
let nextShapeX = COMMON_BOUNDS_XOFFSET;
let shapeY = commonBounds.top;
for (const [i, r] of results.entries()) {
const discourseNode = findDiscourseNode(r.uid);
const discourseNode = findDiscourseNode({ uid: r.uid });
const nodeType = discourseNode ? discourseNode.type : "page-node";
const extensionAPI = getExtensionAPI();
const { h, w, imageUrl } = await calcCanvasNodeSizeAndImg({
Expand Down
5 changes: 4 additions & 1 deletion apps/roam/src/components/SuggestionsBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ const SuggestionsBody = ({
>([]);

const tagUid = useMemo(() => getPageUidByPageTitle(tag), [tag]);
const discourseNode = useMemo(() => findDiscourseNode(tagUid), [tagUid]);
const discourseNode = useMemo(
() => findDiscourseNode({ uid: tagUid }),
[tagUid],
);
const allRelations = useMemo(() => getDiscourseRelations(), []);
const allNodes = useMemo(() => getDiscourseNodes(), []);

Expand Down
4 changes: 2 additions & 2 deletions apps/roam/src/components/canvas/Clipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ const ClipboardPageSection = ({
async (node: { uid: string; text: string }, pagePoint: Vec) => {
if (!extensionAPI) return;

const nodeType = findDiscourseNode(node.uid);
const nodeType = findDiscourseNode({ uid: node.uid });
if (!nodeType) {
internalError({
error: new Error("Canvas Clipboard: Node type not found"),
Expand Down Expand Up @@ -726,7 +726,7 @@ const ClipboardPageSection = ({
const nodeText = target.dataset.clipboardNodeText;
if (!nodeUid || !nodeText) return;

const nodeType = findDiscourseNode(nodeUid);
const nodeType = findDiscourseNode({ uid: nodeUid });
if (!nodeType) return;
const { backgroundColor, textColor } = getDiscourseNodeColors({
nodeType: nodeType.type,
Expand Down
5 changes: 4 additions & 1 deletion apps/roam/src/components/canvas/Tldraw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,10 @@ const TldrawCanvas = ({ title }: { title: string }) => {
const position = lastTime
? { x: lastTime.x + w * 0.025, y: lastTime.y + h * 0.05 }
: { x: x - DEFAULT_WIDTH / 2, y: y - DEFAULT_HEIGHT / 2 };
const nodeType = findDiscourseNode(e.detail.uid, allNodes);
const nodeType = findDiscourseNode({
uid: e.detail.uid,
nodes: allNodes,
});
if (nodeType) {
app.createShapes([
{
Expand Down
2 changes: 1 addition & 1 deletion apps/roam/src/utils/deriveDiscourseNodeAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const deriveNodeAttribute = async ({
}): Promise<string | number> => {
const relations = getDiscourseRelations();
const nodes = getDiscourseNodes(relations);
const discourseNode = findDiscourseNode(uid, nodes);
const discourseNode = findDiscourseNode({ uid, nodes });
if (!discourseNode) return 0;
const nodeType = discourseNode.type;
const attributeNode = getSubTree({
Expand Down
43 changes: 15 additions & 28 deletions apps/roam/src/utils/findDiscourseNode.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,30 @@
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
import getDiscourseNodes, { type DiscourseNode } from "./getDiscourseNodes";
import matchDiscourseNode from "./matchDiscourseNode";

const discourseNodeTypeCache: Record<string, DiscourseNode | false> = {};

const findDiscourseNode = (
uid = "",
nodes = getDiscourseNodes(),
): DiscourseNode | false => {
if (typeof discourseNodeTypeCache[uid] !== "undefined") {
return discourseNodeTypeCache[uid];
}

const matchingNode = nodes.find((node) =>
matchDiscourseNode({ ...node, uid }),
);

discourseNodeTypeCache[uid] = matchingNode || false;
return discourseNodeTypeCache[uid];
};
export default findDiscourseNode;

export const findDiscourseNodeByTitleAndUid = ({
const findDiscourseNode = ({
uid,
title,
nodes,
nodes = getDiscourseNodes(),
}: {
uid: string;
title: string;
uid?: string;
title?: string;
nodes?: DiscourseNode[];
}): DiscourseNode | false => {
nodes = nodes || getDiscourseNodes();
if (uid === undefined) return false;
Comment on lines +7 to +16
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Early return breaks title-only lookups.

The guard at line 16 returns false immediately when uid is undefined, preventing title-only lookups. This contradicts the PR objective to "extend findDiscourseNode to accept the title as an argument."

If a caller provides only title (without uid), the function will return false without attempting to match by title. Consider revising the guard to allow title-only matching.

🔎 Proposed fix
 const findDiscourseNode = ({
   uid,
   title,
   nodes = getDiscourseNodes(),
 }: {
   uid?: string;
   title?: string;
   nodes?: DiscourseNode[];
 }): DiscourseNode | false => {
-  if (uid === undefined) return false;
+  if (uid === undefined && title === undefined) return false;
+  
+  // For title-only lookups, skip cache and match directly
+  if (uid === undefined && title !== undefined) {
+    return nodes.find((node) => matchDiscourseNode({ ...node, title })) || false;
+  }
+  
   if (typeof discourseNodeTypeCache[uid] !== "undefined") {
     return discourseNodeTypeCache[uid];
   }
 
   const matchingNode =
     nodes.find((node) =>
       title === undefined
         ? matchDiscourseNode({ ...node, uid: uid })
         : matchDiscourseNode({ ...node, title }),
     ) || false;
   discourseNodeTypeCache[uid] = matchingNode;
   return matchingNode;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const findDiscourseNode = ({
uid,
title,
nodes,
nodes = getDiscourseNodes(),
}: {
uid: string;
title: string;
uid?: string;
title?: string;
nodes?: DiscourseNode[];
}): DiscourseNode | false => {
nodes = nodes || getDiscourseNodes();
if (uid === undefined) return false;
const findDiscourseNode = ({
uid,
title,
nodes = getDiscourseNodes(),
}: {
uid?: string;
title?: string;
nodes?: DiscourseNode[];
}): DiscourseNode | false => {
if (uid === undefined && title === undefined) return false;
// For title-only lookups, skip cache and match directly
if (uid === undefined && title !== undefined) {
return nodes.find((node) => matchDiscourseNode({ ...node, title })) || false;
}
if (typeof discourseNodeTypeCache[uid] !== "undefined") {
return discourseNodeTypeCache[uid];
}
const matchingNode =
nodes.find((node) =>
title === undefined
? matchDiscourseNode({ ...node, uid: uid })
: matchDiscourseNode({ ...node, title }),
) || false;
discourseNodeTypeCache[uid] = matchingNode;
return matchingNode;
};
🤖 Prompt for AI Agents
In apps/roam/src/utils/findDiscourseNode.ts around lines 7 to 16, the early
return `if (uid === undefined) return false;` prevents title-only lookups;
remove or change this guard so the function continues when uid is undefined and
instead attempts to match by title (and only skip uid-based matching when uid is
absent). Update logic to: if uid is present try uid match first; if not found
and title is provided try title match; return false only if neither lookup
succeeds.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought of doing that, and decided it may be interpreted as scope creep. (But I'm all for it of course.)
Btw if we decide to do this, we also have to get the uid from getPageUidByPageTitle so we can populate the cache.
Leaving the decision to @mdroidian .

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

if (typeof discourseNodeTypeCache[uid] !== "undefined") {
return discourseNodeTypeCache[uid];
}

const matchingNode = nodes.find((node) =>
matchDiscourseNode({ ...node, title }),
);

discourseNodeTypeCache[uid] = matchingNode || false;
return discourseNodeTypeCache[uid];
const matchingNode =
nodes.find((node) =>
title === undefined
? matchDiscourseNode({ ...node, uid: uid })
: matchDiscourseNode({ ...node, title }),
) || false;
discourseNodeTypeCache[uid] = matchingNode;
return matchingNode;
};
export default findDiscourseNode;
2 changes: 1 addition & 1 deletion apps/roam/src/utils/formatUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const getReferencedNodeInFormat = ({
}) => {
let format = providedFormat;
if (!format) {
const discourseNode = findDiscourseNode(uid);
const discourseNode = findDiscourseNode({ uid });
if (discourseNode) format = discourseNode.format;
}
if (!format) return null;
Expand Down
2 changes: 1 addition & 1 deletion apps/roam/src/utils/getDiscourseContextResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ const getDiscourseContextResults = async ({
}) => {
const args = { ignoreCache };

const discourseNode = findDiscourseNode(targetUid);
const discourseNode = findDiscourseNode({ uid: targetUid });
if (!discourseNode) return [];
const nodeType = discourseNode?.type;
const nodeTextByType = Object.fromEntries(
Expand Down
6 changes: 4 additions & 2 deletions apps/roam/src/utils/hyde.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,9 @@ export const performHydeSearch = async ({
})
)
.map((c) => {
const node = findDiscourseNode(c.Content?.source_local_id || "");
const node = findDiscourseNode({
uid: c.Content?.source_local_id || "",
});
return {
uid: c.Content?.source_local_id || "",
text: c.Content?.text || "",
Expand Down Expand Up @@ -492,7 +494,7 @@ export const performHydeSearch = async ({
);
candidateNodesForHyde = uniqueReferenced
.map((n) => {
const node = findDiscourseNode(n.uid);
const node = findDiscourseNode({ uid: n.uid });
if (
!node ||
node.backedBy === "default" ||
Expand Down
2 changes: 1 addition & 1 deletion apps/roam/src/utils/isDiscourseNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import findDiscourseNode from "./findDiscourseNode";

const isDiscourseNode = (uid: string) => {
const nodes = getDiscourseNodes();
const node = findDiscourseNode(uid, nodes);
const node = findDiscourseNode({ uid, nodes });
if (!node) return false;
return node.backedBy !== "default";
};
Expand Down