Skip to content

Commit 6f6e957

Browse files
Roam: [Eng-621] show node tag options in inline node creation menu (#298)
* real time validation * use on blur validation, add tag to config page, settings, extract with other data * conditionally show node tag * use live validation, don't save conflicting state to roam * press down key to trigger * coderabbit review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * update block text * node tage to node conversion * Revert "node tage to node conversion" This reverts commit 3cdac68. * address review * --amend * fix arrow movement while shift, default menu selection or not * merge conflicts prev, address review --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 417b852 commit 6f6e957

3 files changed

Lines changed: 105 additions & 36 deletions

File tree

apps/roam/src/components/DiscourseNodeMenu.tsx

Lines changed: 99 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,29 @@ type Props = {
3232
textarea: HTMLTextAreaElement;
3333
extensionAPI: OnloadArgs["extensionAPI"];
3434
trigger?: JSX.Element;
35+
isShift?: boolean;
3536
};
3637

3738
const NodeMenu = ({
3839
onClose,
3940
textarea,
4041
extensionAPI,
4142
trigger,
43+
isShift,
4244
}: { onClose: () => void } & Props) => {
43-
const discourseNodes = useMemo(
45+
const isInitialTextSelected =
46+
textarea.selectionStart !== textarea.selectionEnd;
47+
48+
const [showNodeTypes, setShowNodeTypes] = useState(
49+
isInitialTextSelected || (isShift ?? false),
50+
);
51+
const userDiscourseNodes = useMemo(
4452
() => getDiscourseNodes().filter((n) => n.backedBy === "user"),
4553
[],
4654
);
55+
const discourseNodes = userDiscourseNodes.filter(
56+
(n) => showNodeTypes || n.tag,
57+
);
4758
const indexBySC = useMemo(
4859
() => Object.fromEntries(discourseNodes.map((mi, i) => [mi.shortcut, i])),
4960
[discourseNodes],
@@ -55,52 +66,80 @@ const NodeMenu = ({
5566
const [isOpen, setIsOpen] = useState(!trigger);
5667

5768
const onSelect = useCallback(
58-
(index) => {
69+
(index: number) => {
5970
const menuItem =
6071
menuRef.current?.children[index].querySelector(".bp3-menu-item");
6172
if (!menuItem) return;
62-
const nodeUid = menuItem.getAttribute("data-node") || "";
63-
const highlighted = textarea.value.substring(
64-
textarea.selectionStart,
65-
textarea.selectionEnd,
66-
);
67-
setTimeout(async () => {
68-
const pageName = await getNewDiscourseNodeText({
69-
text: highlighted,
70-
nodeType: nodeUid,
71-
blockUid,
72-
});
7373

74-
if (!pageName) {
75-
return;
76-
}
77-
78-
const currentBlockText = getTextByBlockUid(blockUid);
79-
const newText = `${currentBlockText.substring(
80-
0,
74+
if (showNodeTypes) {
75+
const nodeUid = menuItem.getAttribute("data-node") || "";
76+
const highlighted = textarea.value.substring(
8177
textarea.selectionStart,
82-
)}[[${pageName}]]${currentBlockText.substring(textarea.selectionEnd)}`;
78+
textarea.selectionEnd,
79+
);
80+
setTimeout(async () => {
81+
const pageName = await getNewDiscourseNodeText({
82+
text: highlighted,
83+
nodeType: nodeUid,
84+
blockUid,
85+
});
8386

84-
updateBlock({ text: newText, uid: blockUid });
85-
posthog.capture("Discourse Node: Created via Node Menu", {
86-
nodeType: nodeUid,
87-
text: pageName,
87+
if (!pageName) {
88+
return;
89+
}
90+
91+
const currentBlockText = getTextByBlockUid(blockUid);
92+
const newText = `${currentBlockText.substring(
93+
0,
94+
textarea.selectionStart,
95+
)}[[${pageName}]]${currentBlockText.substring(textarea.selectionEnd)}`;
96+
97+
updateBlock({ text: newText, uid: blockUid });
98+
posthog.capture("Discourse Node: Created via Node Menu", {
99+
nodeType: nodeUid,
100+
text: pageName,
101+
});
102+
103+
createDiscourseNode({
104+
text: pageName,
105+
configPageUid: nodeUid,
106+
extensionAPI,
107+
});
88108
});
109+
} else {
110+
const tag = menuItem.getAttribute("data-tag") || "";
111+
if (!tag) return;
112+
113+
setTimeout(() => {
114+
const currentText = textarea.value;
115+
const cursorPos = textarea.selectionStart;
116+
const textToInsert = `#${tag} `;
117+
118+
const newText = `${currentText.substring(
119+
0,
120+
cursorPos,
121+
)}${textToInsert}${currentText.substring(cursorPos)}`;
89122

90-
createDiscourseNode({
91-
text: pageName,
92-
configPageUid: nodeUid,
93-
extensionAPI,
123+
updateBlock({ text: newText, uid: blockUid });
124+
posthog.capture("Discourse Tag: Created via Node Menu", {
125+
tag,
126+
});
94127
});
95-
});
128+
}
96129
onClose();
97130
},
98-
[menuRef, blockUid, onClose, textarea, extensionAPI],
131+
[menuRef, blockUid, onClose, textarea, extensionAPI, showNodeTypes],
99132
);
100133

101134
const keydownListener = useCallback(
102135
(e: KeyboardEvent) => {
103-
if (!isOpen || e.metaKey || e.ctrlKey || e.shiftKey) return;
136+
if (!isOpen || e.metaKey || e.ctrlKey) return;
137+
if (e.key === "Shift") {
138+
if (!isInitialTextSelected) {
139+
setShowNodeTypes(true);
140+
}
141+
return;
142+
}
104143

105144
if (e.key === "ArrowDown") {
106145
const index = Number(
@@ -134,26 +173,46 @@ const NodeMenu = ({
134173
e.stopPropagation();
135174
e.preventDefault();
136175
},
137-
[onSelect, onClose, indexBySC, isOpen],
176+
[onSelect, onClose, indexBySC, isOpen, isInitialTextSelected],
177+
);
178+
179+
const keyupListener = useCallback(
180+
(e: KeyboardEvent) => {
181+
if (e.key === "Shift" && !isInitialTextSelected) {
182+
setShowNodeTypes(false);
183+
}
184+
},
185+
[isInitialTextSelected],
138186
);
187+
139188
useEffect(() => {
140189
const eventTarget = trigger ? document : textarea;
141190
const keydownHandler = (e: Event) => {
142191
keydownListener(e as KeyboardEvent);
143192
};
193+
144194
eventTarget.addEventListener("keydown", keydownHandler);
195+
eventTarget.addEventListener("keyup", keyupListener as EventListener);
145196

146197
if (!trigger) {
147198
textarea.addEventListener("input", onClose);
148199
}
149200

150201
return () => {
151202
eventTarget.removeEventListener("keydown", keydownHandler);
203+
eventTarget.removeEventListener("keyup", keyupListener as EventListener);
152204
if (!trigger) {
153205
textarea.removeEventListener("input", onClose);
154206
}
155207
};
156-
}, [keydownListener, onClose, textarea, trigger]);
208+
}, [
209+
keydownListener,
210+
keyupListener,
211+
onClose,
212+
textarea,
213+
trigger,
214+
isInitialTextSelected,
215+
]);
157216

158217
const handlePopoverInteraction = useCallback(
159218
(nextOpenState: boolean) => {
@@ -190,10 +249,14 @@ const NodeMenu = ({
190249
<MenuItem
191250
key={item.text}
192251
data-node={item.type}
193-
text={item.text}
252+
data-tag={item.tag}
253+
text={
254+
showNodeTypes ? item.text : item.tag ? `#${item.tag}` : ""
255+
}
194256
active={i === activeIndex}
195257
onMouseEnter={() => setActiveIndex(i)}
196258
onClick={() => onSelect(i)}
259+
disabled={!showNodeTypes && !item.tag}
197260
className="flex items-center"
198261
icon={
199262
<div
@@ -275,6 +338,7 @@ export const TextSelectionNodeMenu = ({
275338
extensionAPI={extensionAPI}
276339
trigger={trigger}
277340
onClose={onClose}
341+
isShift
278342
/>
279343
);
280344
};

apps/roam/src/utils/getDiscourseNodes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const DEFAULT_NODES: DiscourseNode[] = [
3333
text: "Page",
3434
type: "page-node",
3535
shortcut: "p",
36+
tag: "",
3637
format: "{content}",
3738
specification: [
3839
{
@@ -50,6 +51,7 @@ const DEFAULT_NODES: DiscourseNode[] = [
5051
text: "Block",
5152
type: "blck-node",
5253
shortcut: "b",
54+
tag: "",
5355
format: "{content}",
5456
specification: [
5557
{
@@ -112,6 +114,7 @@ const getDiscourseNodes = (relations = getDiscourseRelations()) => {
112114
text: r.label,
113115
type: r.id,
114116
shortcut: r.label.slice(0, 1),
117+
tag: "",
115118
specification: r.triples.map(([source, relation, target]) => ({
116119
type: "clause",
117120
source: /anchor/i.test(source) ? r.label : source,

apps/roam/src/utils/initializeObserversAndListeners.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,12 @@ export const initObservers = async ({
167167
target.tagName === "TEXTAREA" &&
168168
target.classList.contains("rm-block-input")
169169
) {
170+
const textarea = target as HTMLTextAreaElement;
170171
removeTextSelectionPopup();
171172
renderDiscourseNodeMenu({
172-
textarea: target as HTMLTextAreaElement,
173+
textarea,
173174
extensionAPI: onloadArgs.extensionAPI,
175+
isShift: evt.shiftKey,
174176
});
175177
evt.preventDefault();
176178
evt.stopPropagation();

0 commit comments

Comments
 (0)