Skip to content

Commit 43e24d0

Browse files
trangdoan982claude
andcommitted
[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 <noreply@anthropic.com>
1 parent e4a4339 commit 43e24d0

2 files changed

Lines changed: 84 additions & 41 deletions

File tree

apps/roam/src/components/ModifyNodeDialog.tsx

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,16 @@ const ModifyNodeDialog = ({
107107
: allNodes.filter(excludeDefaultNodes);
108108
}, [includeDefaultNodes]);
109109

110-
const [selectedNodeType, setSelectedNodeType] = useState(() => {
110+
const [selectedNodeType, setSelectedNodeType] = useState<
111+
(typeof discourseNodes)[number] | null
112+
>(() => {
113+
if (!nodeType) return null;
111114
const node = discourseNodes.find((n) => n.type === nodeType);
112-
return node || discourseNodes[0];
115+
return node || null;
113116
});
114117

115118
const nodeFormat = useMemo(() => {
116-
return selectedNodeType.format || "";
119+
return selectedNodeType?.format || "";
117120
}, [selectedNodeType]);
118121

119122
const referencedNode = useMemo(() => {
@@ -158,6 +161,39 @@ const ModifyNodeDialog = ({
158161
if (contentRequestIdRef.current === req && alive) {
159162
setOptions((prev) => ({ ...prev, content: results }));
160163
}
164+
} else {
165+
// Query all discourse node types in parallel
166+
const allResults = await Promise.all(
167+
discourseNodes.map(async (node) => {
168+
const conditionUid = window.roamAlphaAPI.util.generateUID();
169+
const results = await fireQuery({
170+
returnNode: "node",
171+
selections: [],
172+
conditions: [
173+
{
174+
source: "node",
175+
relation: "is a",
176+
target: node.type,
177+
uid: conditionUid,
178+
type: "clause",
179+
},
180+
],
181+
});
182+
return results.map((r) => ({
183+
...r,
184+
_discourseNodeType: node.type,
185+
}));
186+
}),
187+
);
188+
const seen = new Set<string>();
189+
const deduped = allResults.flat().filter((r) => {
190+
if (seen.has(r.uid)) return false;
191+
seen.add(r.uid);
192+
return true;
193+
});
194+
if (contentRequestIdRef.current === req && alive) {
195+
setOptions((prev) => ({ ...prev, content: deduped }));
196+
}
161197
}
162198
} catch (error) {
163199
if (contentRequestIdRef.current === req && alive) {
@@ -224,9 +260,20 @@ const ModifyNodeDialog = ({
224260
};
225261
}, [selectedNodeType, referencedNode]);
226262

227-
const setValue = useCallback((r: Result) => {
228-
setContent(r);
229-
}, []);
263+
const setValue = useCallback(
264+
(r: Result) => {
265+
setContent(r);
266+
if (!selectedNodeType && r.uid) {
267+
const detectedType = (r as Record<string, unknown>)
268+
._discourseNodeType as string | undefined;
269+
if (detectedType) {
270+
const nt = discourseNodes.find((n) => n.type === detectedType);
271+
if (nt) setSelectedNodeType(nt);
272+
}
273+
}
274+
},
275+
[selectedNodeType, discourseNodes],
276+
);
230277

231278
const setReferencedNodeValueCallback = useCallback((r: Result) => {
232279
setReferencedNodeValue(r);
@@ -302,9 +349,13 @@ const ModifyNodeDialog = ({
302349

303350
const onSubmit = async () => {
304351
if (!content.text.trim()) return;
352+
if (!selectedNodeType && !isContentLocked) {
353+
setError("Please select a node type");
354+
return;
355+
}
305356
posthog.capture("Modify Node Dialog: Submit Triggered", {
306357
mode,
307-
nodeType: selectedNodeType.type,
358+
nodeType: selectedNodeType?.type,
308359
});
309360
try {
310361
if (mode === "create") {
@@ -324,7 +375,7 @@ const ModifyNodeDialog = ({
324375
await addImageToPage({
325376
pageUid,
326377
imageUrl,
327-
configPageUid: selectedNodeType.type,
378+
configPageUid: selectedNodeType!.type,
328379
extensionAPI,
329380
});
330381
}
@@ -371,7 +422,7 @@ const ModifyNodeDialog = ({
371422
} else {
372423
formattedTitle = await getNewDiscourseNodeText({
373424
text: content.text.trim(),
374-
nodeType: selectedNodeType.type,
425+
nodeType: selectedNodeType!.type,
375426
blockUid: sourceBlockUid,
376427
});
377428
}
@@ -382,7 +433,7 @@ const ModifyNodeDialog = ({
382433
// Create new discourse node
383434
const newPageUid = await createDiscourseNode({
384435
text: formattedTitle,
385-
configPageUid: selectedNodeType.type,
436+
configPageUid: selectedNodeType!.type,
386437
extensionAPI,
387438
imageUrl,
388439
});
@@ -503,6 +554,26 @@ const ModifyNodeDialog = ({
503554
style={{ pointerEvents: "all" }}
504555
>
505556
<div className={`${Classes.DIALOG_BODY} flex flex-col gap-4`}>
557+
{/* Content Input */}
558+
<div className="w-full">
559+
<Label>Content</Label>
560+
<FuzzySelectInput
561+
value={content}
562+
setValue={setValue}
563+
options={options.content}
564+
placeholder={
565+
loading
566+
? "..."
567+
: selectedNodeType
568+
? `Enter a ${selectedNodeType.text.toLowerCase()} ...`
569+
: "Search all nodes..."
570+
}
571+
mode={mode}
572+
isLocked={isContentLocked}
573+
autoFocus={!isContentLocked}
574+
/>
575+
</div>
576+
506577
{/* Node Type Selector */}
507578
<div className="flex w-full">
508579
<Label autoFocus={false}>
@@ -512,38 +583,20 @@ const ModifyNodeDialog = ({
512583
transformItem={(t) =>
513584
discourseNodes.find((n) => n.type === t)?.text || t
514585
}
515-
activeItem={selectedNodeType.type}
586+
activeItem={selectedNodeType?.type ?? null}
516587
onItemSelect={(t) => {
517588
const nt = discourseNodes.find((n) => n.type === t);
518589
if (nt) {
519590
setSelectedNodeType(nt);
520591
setReferencedNodeValue({ text: "", uid: "" });
521592
}
522593
}}
523-
disabled={mode === "edit"}
594+
disabled={mode === "edit" || isContentLocked}
524595
popoverProps={{ openOnTargetFocus: false }}
525596
/>
526597
</Label>
527598
</div>
528599

529-
{/* Content Input */}
530-
<div className="w-full">
531-
<Label>Content</Label>
532-
<FuzzySelectInput
533-
value={content}
534-
setValue={setValue}
535-
options={options.content}
536-
placeholder={
537-
loading
538-
? "..."
539-
: `Enter a ${selectedNodeType.text.toLowerCase()} ...`
540-
}
541-
mode={mode}
542-
isLocked={isContentLocked}
543-
autoFocus={!isContentLocked}
544-
/>
545-
</div>
546-
547600
{/* Referenced Node Input */}
548601
{referencedNode && !isContentLocked && mode === "create" && (
549602
<div className="w-full">

apps/roam/src/utils/registerCommandPaletteCommands.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -189,19 +189,9 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => {
189189

190190
const selectionStart = uid ? getSelectionStartForBlock(uid) : 0;
191191

192-
const defaultNodeType =
193-
getDiscourseNodes().filter(excludeDefaultNodes)[0]?.type;
194-
if (!defaultNodeType) {
195-
renderToast({
196-
id: "create-discourse-node-command-no-types",
197-
content: "No discourse node types found in settings.",
198-
});
199-
return;
200-
}
201-
202192
renderModifyNodeDialog({
203193
mode: "create",
204-
nodeType: defaultNodeType,
194+
nodeType: "",
205195
initialValue: { text: "", uid: "" },
206196
extensionAPI,
207197
onSuccess: async (result) => {

0 commit comments

Comments
 (0)