Skip to content

Commit a0decd3

Browse files
authored
ENG-948: Add blocks to left sidebar via block context menu (#902)
* ENG-948: Add blocks to left sidebar via block context menu Register block context menu commands so users can right-click any block and add it to a left sidebar section. On click, the block is added (dual-write to tree + block props), and the settings panel opens to the relevant tab with the target section auto-expanded. - Uses `window.roamAlphaAPI.ui.blockContextMenu.addCommand()` (first usage in codebase — `extensionAPI.ui` types don't include it) - One command per personal section (with childrenUid) + one for global - Gated behind leftSidebarEnabled flag - Extended Settings render to accept selectedTabId and expandedSectionUid - Exported sectionsToBlockProps (now has 2 consumers) Limitations: - Commands registered once on load; new sections need extension reload - Simple sections (no childrenUid) are skipped * self review * Rename context menu label from "Left sidebar" to "Favorites"
1 parent bbb37d8 commit a0decd3

3 files changed

Lines changed: 133 additions & 5 deletions

File tree

apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU
4141
import posthog from "posthog-js";
4242

4343
/* eslint-disable @typescript-eslint/naming-convention */
44-
const sectionsToBlockProps = (sections: LeftSidebarPersonalSectionConfig[]) =>
44+
export const sectionsToBlockProps = (
45+
sections: LeftSidebarPersonalSectionConfig[],
46+
) =>
4547
sections.map((s) => ({
4648
name: s.text,
4749
Children: (s.children || []).map((c) => ({
@@ -72,6 +74,7 @@ const SectionItem = memo(
7274
isFirst,
7375
isLast,
7476
onMoveSection,
77+
initiallyExpanded,
7578
}: {
7679
section: LeftSidebarPersonalSectionConfig;
7780
setSections: Dispatch<SetStateAction<LeftSidebarPersonalSectionConfig[]>>;
@@ -82,6 +85,7 @@ const SectionItem = memo(
8285
isFirst: boolean;
8386
isLast: boolean;
8487
onMoveSection: (index: number, direction: "up" | "down") => void;
88+
initiallyExpanded?: boolean;
8589
}) => {
8690
const ref = extractRef(section.text);
8791
const blockText = getTextByBlockUid(ref);
@@ -90,7 +94,7 @@ const SectionItem = memo(
9094
const [childInputKey, setChildInputKey] = useState(0);
9195

9296
const [expandedChildLists, setExpandedChildLists] = useState<Set<string>>(
93-
new Set(),
97+
new Set(initiallyExpanded ? [section.uid] : []),
9498
);
9599
const isExpanded = expandedChildLists.has(section.uid);
96100
const [childSettingsUid, setChildSettingsUid] = useState<string | null>(
@@ -561,8 +565,10 @@ SectionItem.displayName = "SectionItem";
561565

562566
const LeftSidebarPersonalSectionsContent = ({
563567
leftSidebar,
568+
expandedSectionUid,
564569
}: {
565570
leftSidebar: RoamBasicNode;
571+
expandedSectionUid?: string;
566572
}) => {
567573
const [sections, setSections] = useState<LeftSidebarPersonalSectionConfig[]>(
568574
[],
@@ -736,6 +742,7 @@ const LeftSidebarPersonalSectionsContent = ({
736742
isFirst={index === 0}
737743
isLast={index === sections.length - 1}
738744
onMoveSection={moveSection}
745+
initiallyExpanded={section.uid === expandedSectionUid}
739746
/>
740747
</div>
741748
))}
@@ -822,7 +829,11 @@ const LeftSidebarPersonalSectionsContent = ({
822829
);
823830
};
824831

825-
export const LeftSidebarPersonalSections = () => {
832+
export const LeftSidebarPersonalSections = ({
833+
expandedSectionUid,
834+
}: {
835+
expandedSectionUid?: string;
836+
}) => {
826837
const [leftSidebar, setLeftSidebar] = useState<RoamBasicNode | null>(null);
827838

828839
useEffect(() => {
@@ -850,5 +861,10 @@ export const LeftSidebarPersonalSections = () => {
850861
return null;
851862
}
852863

853-
return <LeftSidebarPersonalSectionsContent leftSidebar={leftSidebar} />;
864+
return (
865+
<LeftSidebarPersonalSectionsContent
866+
leftSidebar={leftSidebar}
867+
expandedSectionUid={expandedSectionUid}
868+
/>
869+
);
854870
};

apps/roam/src/components/settings/Settings.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ export const SettingsDialog = ({
6666
isOpen,
6767
onClose,
6868
selectedTabId,
69+
expandedSectionUid,
6970
}: {
7071
onloadArgs: OnloadArgs;
7172
isOpen?: boolean;
7273
onClose?: () => void;
7374
selectedTabId?: TabId;
75+
expandedSectionUid?: string;
7476
}) => {
7577
const extensionAPI = onloadArgs.extensionAPI;
7678
const settings = getFormattedConfigTree();
@@ -172,7 +174,11 @@ export const SettingsDialog = ({
172174
id="left-sidebar-personal-settings"
173175
title="Left sidebar"
174176
className="overflow-y-auto"
175-
panel={<LeftSidebarPersonalSections />}
177+
panel={
178+
<LeftSidebarPersonalSections
179+
expandedSectionUid={expandedSectionUid}
180+
/>
181+
}
176182
/>
177183
<SectionHeader className="text-lg font-semibold text-neutral-dark">
178184
Global Settings
@@ -275,11 +281,15 @@ export const SettingsDialog = ({
275281

276282
type Props = {
277283
onloadArgs: OnloadArgs;
284+
selectedTabId?: TabId;
285+
expandedSectionUid?: string;
278286
};
279287
export const render = (props: Props) =>
280288
renderOverlay({
281289
Overlay: SettingsDialog,
282290
props: {
283291
onloadArgs: props.onloadArgs,
292+
selectedTabId: props.selectedTabId,
293+
expandedSectionUid: props.expandedSectionUid,
284294
},
285295
});

apps/roam/src/utils/registerCommandPaletteCommands.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ import {
2121
} from "./pageRefObserverHandlers";
2222
import { HIDE_METADATA_KEY } from "~/data/userSettings";
2323
import posthog from "posthog-js";
24+
import { extractRef } from "roamjs-components/util";
25+
import discourseConfigRef from "~/utils/discourseConfigRef";
26+
import { getLeftSidebarPersonalSectionConfig } from "~/utils/getLeftSidebarSettings";
27+
import { getUidAndBooleanSetting } from "~/utils/getExportSettings";
28+
import refreshConfigTree from "~/utils/refreshConfigTree";
29+
import { refreshAndNotify } from "~/components/LeftSidebarView";
30+
import { setPersonalSetting } from "~/components/settings/utils/accessors";
31+
import { sectionsToBlockProps } from "~/components/settings/LeftSidebarPersonalSettings";
2432

2533
export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => {
2634
const { extensionAPI } = onloadArgs;
@@ -301,4 +309,98 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => {
301309
);
302310
void addCommand("DG: Query block - Create", createQueryBlock);
303311
void addCommand("DG: Query block - Refresh", refreshCurrentQueryBuilder);
312+
313+
const leftSidebarEnabled = getUidAndBooleanSetting({
314+
tree: discourseConfigRef.tree,
315+
text: "(BETA) Left Sidebar",
316+
});
317+
if (leftSidebarEnabled.value) {
318+
const leftSidebarNode = discourseConfigRef.tree.find(
319+
(node) => node.text === "Left Sidebar",
320+
);
321+
const personalSections = getLeftSidebarPersonalSectionConfig(
322+
leftSidebarNode?.children || [],
323+
).sections;
324+
325+
for (const section of personalSections) {
326+
if (!section.childrenUid) continue;
327+
328+
const sectionName = section.text.startsWith("((")
329+
? getTextByBlockUid(extractRef(section.text)) || section.text
330+
: section.text;
331+
332+
window.roamAlphaAPI.ui.blockContextMenu.addCommand({
333+
label: `DG: Favorites - Add to "${sectionName}" section`,
334+
// eslint-disable-next-line @typescript-eslint/naming-convention
335+
callback: (props: { "block-uid": string }) => {
336+
void addBlockToPersonalSection({
337+
blockUid: props["block-uid"],
338+
sectionUid: section.uid,
339+
onloadArgs,
340+
});
341+
},
342+
});
343+
}
344+
}
345+
};
346+
347+
const addBlockToPersonalSection = async ({
348+
blockUid,
349+
sectionUid,
350+
onloadArgs,
351+
}: {
352+
blockUid: string;
353+
sectionUid: string;
354+
onloadArgs: OnloadArgs;
355+
}) => {
356+
refreshConfigTree();
357+
const leftSidebarNode = discourseConfigRef.tree.find(
358+
(node) => node.text === "Left Sidebar",
359+
);
360+
const sections = getLeftSidebarPersonalSectionConfig(
361+
leftSidebarNode?.children || [],
362+
).sections;
363+
const section = sections.find((s) => s.uid === sectionUid);
364+
if (!section?.childrenUid) return;
365+
366+
const blockRef = `((${blockUid}))`;
367+
368+
try {
369+
const newChildBlockUid = await createBlock({
370+
parentUid: section.childrenUid,
371+
order: "last",
372+
node: { text: blockRef },
373+
});
374+
375+
const updatedSections = sections.map((s) =>
376+
s.uid === sectionUid
377+
? {
378+
...s,
379+
children: [
380+
...(s.children || []),
381+
{
382+
text: blockRef,
383+
uid: newChildBlockUid,
384+
children: [],
385+
alias: { value: "" },
386+
},
387+
],
388+
}
389+
: s,
390+
);
391+
392+
setPersonalSetting(["Left sidebar"], sectionsToBlockProps(updatedSections));
393+
refreshAndNotify();
394+
renderSettings({
395+
onloadArgs,
396+
selectedTabId: "left-sidebar-personal-settings",
397+
expandedSectionUid: sectionUid,
398+
});
399+
} catch {
400+
renderToast({
401+
content: "Failed to add block to section",
402+
intent: "danger",
403+
id: "add-block-to-section-error",
404+
});
405+
}
304406
};

0 commit comments

Comments
 (0)