Skip to content

Commit 004d0bb

Browse files
authored
ENG-889: Render SmartBlocks as interactive buttons in left sidebar (#940)
* ENG-889: Render SmartBlocks as interactive buttons in left sidebar Block references containing SmartBlock syntax (`:SmartBlock:` buttons or `<%` workflows) now render via Roam's renderBlock API instead of plain text. Clicking a SmartBlock button navigates to the block. - Add RoamRenderedBlock component using renderBlock with open?:false - Extract hasSmartBlockSyntax to module level in createDiscourseNode, add isSmartBlockUid export for sidebar/settings detection - Hide alias settings gear for SmartBlock children - Inject CSS once in mountLeftSidebar to hide Roam chrome - Add "DG: Favorites - Add to Global section" context menu with dual-write (restores global support removed in PR #902 self-review) * Address review: use react.block and move isSmartBlockUid - Replace useEffect+renderBlock with roamAlphaAPI.ui.components.react.block - Simplify isSmartBlockUid to only check :SmartBlock: text - Move isSmartBlockUid to getLeftSidebarSettings (shared by both consumers) - Restore hasSmartBlockSyntax as local fn in createDiscourseNode * Add ts-expect-error for untyped react.block API * Address review: use RenderRoamBlock util and extract isSmartBlockUid - Use RenderRoamBlock from ~/utils/roamReactComponents (correct API path: ui.react.Block, not ui.components.react.block). Fixes runtime break and removes the @ts-expect-error + eslint-disables. - Move isSmartBlockUid into its own file at ~/utils/isSmartBlockUid.ts. * Fix sidebar SmartBlocks: stop click navigation, refresh on text change - Remove openBlock click handler. SmartBlock buttons handle their own clicks; the wrapper was running the SmartBlock AND navigating to its source block. - Pull-watch :block/string and force RenderRoamBlock remount via key. Roam's react.Block doesn't re-render on text changes when used outside the main render tree, so editing a SmartBlock action left the sidebar showing the previous action. * Rename SmartBlock button ref helper
1 parent 3c66a72 commit 004d0bb

5 files changed

Lines changed: 135 additions & 11 deletions

File tree

apps/roam/src/components/LeftSidebarView.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU
4848
import { migrateLeftSidebarSettings } from "~/utils/migrateLeftSidebarSettings";
4949
import posthog from "posthog-js";
5050
import { commands, cleanCommandName } from "~/components/LeftSidebarCommands";
51+
import { isSmartBlockUid } from "~/utils/isSmartBlockUid";
52+
import { RenderRoamBlock } from "~/utils/roamReactComponents";
5153

5254
const parseReference = (text: string) => {
5355
const extracted = extractRef(text);
@@ -135,6 +137,26 @@ const toggleFoldedState = ({
135137
}
136138
};
137139

140+
const RoamRenderedBlock = ({ uid }: { uid: string }) => {
141+
const [version, setVersion] = useState(0);
142+
143+
useEffect(() => {
144+
const pattern = "[:block/string]";
145+
const entityId = `[:block/uid "${uid}"]`;
146+
const callback = () => setVersion((v) => v + 1);
147+
window.roamAlphaAPI.data.addPullWatch(pattern, entityId, callback);
148+
return () => {
149+
window.roamAlphaAPI.data.removePullWatch(pattern, entityId, callback);
150+
};
151+
}, [uid]);
152+
153+
return (
154+
<div className="dg-sidebar-rendered-block">
155+
<RenderRoamBlock key={version} uid={uid} open={false} />
156+
</div>
157+
);
158+
};
159+
138160
type ChildNode = { uid: string; text: string; alias?: { value: string } };
139161

140162
const ChildRow = ({
@@ -147,6 +169,17 @@ const ChildRow = ({
147169
onloadArgs: OnloadArgs;
148170
}) => {
149171
const ref = parseReference(child.text);
172+
173+
if (ref.type === "block" && isSmartBlockUid(ref.uid)) {
174+
return (
175+
<div className="pl-8 pr-2.5">
176+
<div className="section-child-item rounded-sm leading-normal text-gray-600">
177+
<RoamRenderedBlock uid={ref.uid} />
178+
</div>
179+
</div>
180+
);
181+
}
182+
150183
const alias = child.alias?.value;
151184
const display =
152185
ref.type === "command"
@@ -701,6 +734,22 @@ export const mountLeftSidebar = async (
701734
): Promise<void> => {
702735
if (!wrapper) return;
703736

737+
const styleId = "dg-sidebar-rendered-block-styles";
738+
if (!document.getElementById(styleId)) {
739+
const style = document.createElement("style");
740+
style.id = styleId;
741+
style.textContent = `
742+
.dg-sidebar-rendered-block .rm-bullet { display: none; }
743+
.dg-sidebar-rendered-block .rm-block-separator { display: none; }
744+
.dg-sidebar-rendered-block .controls { display: none; }
745+
.dg-sidebar-rendered-block .block-expand { display: none; }
746+
.dg-sidebar-rendered-block .block-border-left { display: none; }
747+
.dg-sidebar-rendered-block .block-ref-count-button { display: none; }
748+
.dg-sidebar-rendered-block .rm-block-main { min-height: unset; padding: 0; }
749+
`;
750+
document.head.appendChild(style);
751+
}
752+
704753
const id = "dg-left-sidebar-root";
705754
let root = wrapper.querySelector(`#${id}`) as HTMLDivElement;
706755
if (!root) {

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ import {
4646
commands,
4747
SidebarCommandPopover,
4848
} from "~/components/LeftSidebarCommands";
49+
import { isSmartBlockUid } from "~/utils/isSmartBlockUid";
50+
51+
const isSmartBlockButtonRef = (text: string): boolean => {
52+
if (!text.startsWith("((") || !text.endsWith("))")) return false;
53+
return isSmartBlockUid(extractRef(text));
54+
};
4955

5056
/* eslint-disable @typescript-eslint/naming-convention */
5157
export const sectionsToBlockProps = (
@@ -429,6 +435,9 @@ const SectionItem = memo(
429435
renderItem={(child, handle) => {
430436
const childAlias = child.alias?.value;
431437
const isSettingsOpen = childSettingsUid === child.uid;
438+
const childIsSmartBlockButtonRef = isSmartBlockButtonRef(
439+
child.text,
440+
);
432441
const childDisplayTitle =
433442
getPageTitleByPageUid(child.text) ||
434443
getTextByBlockUid(extractRef(child.text)) ||
@@ -444,7 +453,7 @@ const SectionItem = memo(
444453
className="mr-2 min-w-0 flex-1 truncate"
445454
title={childDisplayTitle}
446455
>
447-
{childAlias ? (
456+
{childAlias && !childIsSmartBlockButtonRef ? (
448457
<span>
449458
<span className="font-medium">
450459
{childAlias}
@@ -458,13 +467,15 @@ const SectionItem = memo(
458467
)}
459468
</div>
460469
<ButtonGroup minimal className="flex-shrink-0">
461-
<Button
462-
icon="settings"
463-
small
464-
onClick={() => setChildSettingsUid(child.uid)}
465-
title="Child settings"
466-
className="opacity-0 transition-opacity group-hover:opacity-100"
467-
/>
470+
{!childIsSmartBlockButtonRef && (
471+
<Button
472+
icon="settings"
473+
small
474+
onClick={() => setChildSettingsUid(child.uid)}
475+
title="Child settings"
476+
className="opacity-0 transition-opacity group-hover:opacity-100"
477+
/>
478+
)}
468479
<Button
469480
icon="trash"
470481
small

apps/roam/src/utils/createDiscourseNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ const createDiscourseNode = async ({
163163
await handleImageCreation(pageUid);
164164
};
165165

166-
const hasSmartBlockSyntax = (node: RoamBasicNode) => {
166+
const hasSmartBlockSyntax = (node: RoamBasicNode): boolean => {
167167
if (node.text.includes("<%")) return true;
168168
if (node.children) return node.children.some(hasSmartBlockSyntax);
169169
return false;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid";
2+
3+
export const isSmartBlockUid = (uid: string): boolean => {
4+
const text = getTextByBlockUid(uid);
5+
if (!text) return false;
6+
return text.includes(":SmartBlock:");
7+
};

apps/roam/src/utils/registerCommandPaletteCommands.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,17 @@ import { HIDE_METADATA_KEY } from "~/data/userSettings";
2323
import posthog from "posthog-js";
2424
import { extractRef } from "roamjs-components/util";
2525
import discourseConfigRef from "~/utils/discourseConfigRef";
26-
import { getLeftSidebarPersonalSectionConfig } from "~/utils/getLeftSidebarSettings";
26+
import {
27+
getLeftSidebarPersonalSectionConfig,
28+
getLeftSidebarGlobalSectionConfig,
29+
} from "~/utils/getLeftSidebarSettings";
2730
import { getUidAndBooleanSetting } from "~/utils/getExportSettings";
2831
import refreshConfigTree from "~/utils/refreshConfigTree";
2932
import { refreshAndNotify } from "~/components/LeftSidebarView";
30-
import { setPersonalSetting } from "~/components/settings/utils/accessors";
33+
import {
34+
setPersonalSetting,
35+
setGlobalSetting,
36+
} from "~/components/settings/utils/accessors";
3137
import { sectionsToBlockProps } from "~/components/settings/LeftSidebarPersonalSettings";
3238

3339
type BlockSelection = {
@@ -364,6 +370,22 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => {
364370
},
365371
});
366372
}
373+
374+
const globalSection = getLeftSidebarGlobalSectionConfig(
375+
leftSidebarNode?.children || [],
376+
);
377+
if (globalSection.childrenUid) {
378+
window.roamAlphaAPI.ui.blockContextMenu.addCommand({
379+
label: "DG: Favorites - Add to Global section",
380+
// eslint-disable-next-line @typescript-eslint/naming-convention
381+
callback: (props: { "block-uid": string }) => {
382+
void addBlockToGlobalSection({
383+
blockUid: props["block-uid"],
384+
globalChildrenUid: globalSection.childrenUid,
385+
});
386+
},
387+
});
388+
}
367389
}
368390
};
369391

@@ -427,3 +449,38 @@ const addBlockToPersonalSection = async ({
427449
});
428450
}
429451
};
452+
453+
const addBlockToGlobalSection = async ({
454+
blockUid,
455+
globalChildrenUid,
456+
}: {
457+
blockUid: string;
458+
globalChildrenUid: string;
459+
}) => {
460+
const blockRef = `((${blockUid}))`;
461+
462+
try {
463+
await createBlock({
464+
parentUid: globalChildrenUid,
465+
order: "last",
466+
node: { text: blockRef },
467+
});
468+
469+
refreshConfigTree();
470+
const updatedChildren = getLeftSidebarGlobalSectionConfig(
471+
discourseConfigRef.tree.find((n) => n.text === "Left Sidebar")
472+
?.children || [],
473+
).children;
474+
setGlobalSetting(
475+
["Left sidebar", "Children"],
476+
updatedChildren.map((c) => c.text),
477+
);
478+
refreshAndNotify();
479+
} catch {
480+
renderToast({
481+
content: "Failed to add block to global section",
482+
intent: "danger",
483+
id: "add-block-to-global-section-error",
484+
});
485+
}
486+
};

0 commit comments

Comments
 (0)