From 43e24d0512fe98a800baec0696e8b6199d7ac9c1 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Sun, 22 Mar 2026 16:24:48 -0400 Subject: [PATCH] [ENG-1545] Switch positions of node type and node title fields Move Content input above Node Type selector so users can immediately start typing. Support empty Node Type (queries all node types), auto-detect type on selection, and lock type when a node is selected. Co-Authored-By: Claude Opus 4.6 --- apps/roam/src/components/ModifyNodeDialog.tsx | 113 +++++++++++++----- .../utils/registerCommandPaletteCommands.ts | 12 +- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/apps/roam/src/components/ModifyNodeDialog.tsx b/apps/roam/src/components/ModifyNodeDialog.tsx index 3137e338f..a765cd418 100644 --- a/apps/roam/src/components/ModifyNodeDialog.tsx +++ b/apps/roam/src/components/ModifyNodeDialog.tsx @@ -107,13 +107,16 @@ const ModifyNodeDialog = ({ : allNodes.filter(excludeDefaultNodes); }, [includeDefaultNodes]); - const [selectedNodeType, setSelectedNodeType] = useState(() => { + const [selectedNodeType, setSelectedNodeType] = useState< + (typeof discourseNodes)[number] | null + >(() => { + if (!nodeType) return null; const node = discourseNodes.find((n) => n.type === nodeType); - return node || discourseNodes[0]; + return node || null; }); const nodeFormat = useMemo(() => { - return selectedNodeType.format || ""; + return selectedNodeType?.format || ""; }, [selectedNodeType]); const referencedNode = useMemo(() => { @@ -158,6 +161,39 @@ const ModifyNodeDialog = ({ if (contentRequestIdRef.current === req && alive) { setOptions((prev) => ({ ...prev, content: results })); } + } else { + // Query all discourse node types in parallel + const allResults = await Promise.all( + discourseNodes.map(async (node) => { + const conditionUid = window.roamAlphaAPI.util.generateUID(); + const results = await fireQuery({ + returnNode: "node", + selections: [], + conditions: [ + { + source: "node", + relation: "is a", + target: node.type, + uid: conditionUid, + type: "clause", + }, + ], + }); + return results.map((r) => ({ + ...r, + _discourseNodeType: node.type, + })); + }), + ); + const seen = new Set(); + const deduped = allResults.flat().filter((r) => { + if (seen.has(r.uid)) return false; + seen.add(r.uid); + return true; + }); + if (contentRequestIdRef.current === req && alive) { + setOptions((prev) => ({ ...prev, content: deduped })); + } } } catch (error) { if (contentRequestIdRef.current === req && alive) { @@ -224,9 +260,20 @@ const ModifyNodeDialog = ({ }; }, [selectedNodeType, referencedNode]); - const setValue = useCallback((r: Result) => { - setContent(r); - }, []); + const setValue = useCallback( + (r: Result) => { + setContent(r); + if (!selectedNodeType && r.uid) { + const detectedType = (r as Record) + ._discourseNodeType as string | undefined; + if (detectedType) { + const nt = discourseNodes.find((n) => n.type === detectedType); + if (nt) setSelectedNodeType(nt); + } + } + }, + [selectedNodeType, discourseNodes], + ); const setReferencedNodeValueCallback = useCallback((r: Result) => { setReferencedNodeValue(r); @@ -302,9 +349,13 @@ const ModifyNodeDialog = ({ const onSubmit = async () => { if (!content.text.trim()) return; + if (!selectedNodeType && !isContentLocked) { + setError("Please select a node type"); + return; + } posthog.capture("Modify Node Dialog: Submit Triggered", { mode, - nodeType: selectedNodeType.type, + nodeType: selectedNodeType?.type, }); try { if (mode === "create") { @@ -324,7 +375,7 @@ const ModifyNodeDialog = ({ await addImageToPage({ pageUid, imageUrl, - configPageUid: selectedNodeType.type, + configPageUid: selectedNodeType!.type, extensionAPI, }); } @@ -371,7 +422,7 @@ const ModifyNodeDialog = ({ } else { formattedTitle = await getNewDiscourseNodeText({ text: content.text.trim(), - nodeType: selectedNodeType.type, + nodeType: selectedNodeType!.type, blockUid: sourceBlockUid, }); } @@ -382,7 +433,7 @@ const ModifyNodeDialog = ({ // Create new discourse node const newPageUid = await createDiscourseNode({ text: formattedTitle, - configPageUid: selectedNodeType.type, + configPageUid: selectedNodeType!.type, extensionAPI, imageUrl, }); @@ -503,6 +554,26 @@ const ModifyNodeDialog = ({ style={{ pointerEvents: "all" }} >
+ {/* Content Input */} +
+ + +
+ {/* Node Type Selector */}
- {/* Content Input */} -
- - -
- {/* Referenced Node Input */} {referencedNode && !isContentLocked && mode === "create" && (
diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 6e5dbf61e..2eb143229 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -189,19 +189,9 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { const selectionStart = uid ? getSelectionStartForBlock(uid) : 0; - const defaultNodeType = - getDiscourseNodes().filter(excludeDefaultNodes)[0]?.type; - if (!defaultNodeType) { - renderToast({ - id: "create-discourse-node-command-no-types", - content: "No discourse node types found in settings.", - }); - return; - } - renderModifyNodeDialog({ mode: "create", - nodeType: defaultNodeType, + nodeType: "", initialValue: { text: "", uid: "" }, extensionAPI, onSuccess: async (result) => {