Skip to content

Commit e5876cc

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 51a394b commit e5876cc

2 files changed

Lines changed: 86 additions & 41 deletions

File tree

apps/roam/src/components/ModifyNodeDialog.tsx

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

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

117120
const nodeFormat = useMemo(() => {
118-
return selectedNodeType.format || "";
121+
return selectedNodeType?.format || "";
119122
}, [selectedNodeType]);
120123

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

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

233280
const setReferencedNodeValueCallback = useCallback((r: Result) => {
234281
setReferencedNodeValue(r);
@@ -304,9 +351,13 @@ const ModifyNodeDialog = ({
304351

305352
const onSubmit = async () => {
306353
if (!content.text.trim()) return;
354+
if (!selectedNodeType && !isContentLocked) {
355+
setError("Please select a node type");
356+
return;
357+
}
307358
posthog.capture("Modify Node Dialog: Submit Triggered", {
308359
mode,
309-
nodeType: selectedNodeType.type,
360+
nodeType: selectedNodeType?.type,
310361
});
311362
try {
312363
if (mode === "create") {
@@ -326,7 +377,7 @@ const ModifyNodeDialog = ({
326377
await addImageToPage({
327378
pageUid,
328379
imageUrl,
329-
configPageUid: selectedNodeType.type,
380+
configPageUid: selectedNodeType!.type,
330381
extensionAPI,
331382
});
332383
}
@@ -373,7 +424,7 @@ const ModifyNodeDialog = ({
373424
} else {
374425
formattedTitle = await getNewDiscourseNodeText({
375426
text: content.text.trim(),
376-
nodeType: selectedNodeType.type,
427+
nodeType: selectedNodeType!.type,
377428
blockUid: sourceBlockUid,
378429
});
379430
}
@@ -384,7 +435,7 @@ const ModifyNodeDialog = ({
384435
// Create new discourse node
385436
const newPageUid = await createDiscourseNode({
386437
text: formattedTitle,
387-
configPageUid: selectedNodeType.type,
438+
configPageUid: selectedNodeType!.type,
388439
extensionAPI,
389440
imageUrl,
390441
});
@@ -505,6 +556,26 @@ const ModifyNodeDialog = ({
505556
style={{ pointerEvents: "all" }}
506557
>
507558
<div className={`${Classes.DIALOG_BODY} flex flex-col gap-4`}>
559+
{/* Content Input */}
560+
<div className="w-full">
561+
<Label>Content</Label>
562+
<FuzzySelectInput
563+
value={content}
564+
setValue={setValue}
565+
options={options.content}
566+
placeholder={
567+
loading
568+
? "..."
569+
: selectedNodeType
570+
? `Enter a ${selectedNodeType.text.toLowerCase()} ...`
571+
: "Search all nodes..."
572+
}
573+
mode={mode}
574+
isLocked={isContentLocked}
575+
autoFocus={!isContentLocked}
576+
/>
577+
</div>
578+
508579
{/* Node Type Selector */}
509580
<div className="flex w-full">
510581
<Label autoFocus={false}>
@@ -514,15 +585,17 @@ const ModifyNodeDialog = ({
514585
transformItem={(t) =>
515586
discourseNodes.find((n) => n.type === t)?.text || t
516587
}
517-
activeItem={selectedNodeType.type}
588+
activeItem={selectedNodeType?.type ?? null}
518589
onItemSelect={(t) => {
519590
const nt = discourseNodes.find((n) => n.type === t);
520591
if (nt) {
521592
setSelectedNodeType(nt);
522593
setReferencedNodeValue({ text: "", uid: "" });
523594
}
524595
}}
525-
disabled={mode === "edit" || disableNodeTypeChange}
596+
disabled={
597+
mode === "edit" || disableNodeTypeChange || isContentLocked
598+
}
526599
popoverProps={{ openOnTargetFocus: false }}
527600
className={
528601
mode === "edit" || disableNodeTypeChange
@@ -533,24 +606,6 @@ const ModifyNodeDialog = ({
533606
</Label>
534607
</div>
535608

536-
{/* Content Input */}
537-
<div className="w-full">
538-
<Label>Content</Label>
539-
<FuzzySelectInput
540-
value={content}
541-
setValue={setValue}
542-
options={options.content}
543-
placeholder={
544-
loading
545-
? "..."
546-
: `Enter a ${selectedNodeType.text.toLowerCase()} ...`
547-
}
548-
mode={mode}
549-
isLocked={isContentLocked}
550-
autoFocus={!isContentLocked}
551-
/>
552-
</div>
553-
554609
{/* Referenced Node Input */}
555610
{referencedNode && !isContentLocked && mode === "create" && (
556611
<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)