From 7c67612b2bb2c32b8b36dd0dd3b04326cd13d612 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 19 Jun 2026 10:33:14 +0530 Subject: [PATCH 1/7] [ENG-1851] Add Roam single-node share command (capture-half) Add a 'DG: Share current node' command-palette action, gated on isSyncEnabled(). It deterministically captures the current page's discourse node (page uid = source-local id), validates it via findDiscourseNode, and opens the existing Share Data dialog with exactly one node. The Publish-tab handoff (initialPanel: "publish") is deferred to ENG-1890, which owns Export.tsx's initialPanel union and the tab; finishing it is a one-line change to the exportRender call here. Mirrors Obsidian's publish-discourse-node command and Roam's exportCurrentPage. --- .../utils/registerCommandPaletteCommands.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 96d5433f8..16daee7e0 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -29,6 +29,7 @@ import { getPersonalSetting, setPersonalSetting, setGlobalSetting, + isSyncEnabled, } from "~/components/settings/utils/accessors"; import { DISCOURSE_NODE_KEYS, @@ -244,6 +245,45 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }); }; + const shareCurrentNode = () => { + const pageUid = getCurrentPageUid(); + if (!pageUid) { + renderToast({ + id: "share-node-no-page", + content: "Navigate to a discourse node page to share it.", + }); + return; + } + + const pageTitle = getPageTitleByPageUid(pageUid); + if (!pageTitle) { + renderToast({ + id: "share-node-no-title", + content: "Could not determine the current page title.", + }); + return; + } + + const discourseNode = findDiscourseNode({ uid: pageUid, title: pageTitle }); + if (!discourseNode || discourseNode.backedBy === "default") { + renderToast({ + id: "share-node-not-a-node", + content: "This page is not a discourse node, so it can't be shared.", + }); + return; + } + + posthog.capture("Share Node: Current Node Command Triggered", { + pageUid, + nodeType: discourseNode.type, + }); + + exportRender({ + results: [{ uid: pageUid, text: pageTitle, type: discourseNode.type }], + isExportDiscourseGraph: true, + }); + }; + const exportDiscourseGraph = async () => { posthog.capture("Export: Discourse Graph Command Triggered"); const discourseNodes = getDiscourseNodes().filter(excludeDefaultNodes); @@ -364,6 +404,9 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { void addCommand("DG: Export - Current page", exportCurrentPage); void addCommand("DG: Export - Discourse graph", exportDiscourseGraph); void addCommand("DG: Open - Discourse settings", renderSettingsPopup); + if (isSyncEnabled()) { + void addCommand("DG: Share current node", shareCurrentNode); + } if (getFeatureFlag("Advanced node search enabled")) { void addCommand("DG: Open Node Search", () => { posthog.capture("Node Search: Open Command Triggered"); From 1780de9d94c5794e9526a87771cdab30ef2fbb63 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 22 Jun 2026 19:09:48 +0530 Subject: [PATCH 2/7] [ENG-1851] Open single-node share on Publish tab --- apps/roam/src/components/Export.tsx | 12 ++++++++++-- .../roam/src/utils/registerCommandPaletteCommands.ts | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index bb3ea0fb5..be012f5c7 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -128,7 +128,7 @@ export type ExportDialogProps = { title?: string; columns?: Column[]; isExportDiscourseGraph?: boolean; - initialPanel?: "sendTo" | "export"; + initialPanel?: "sendTo" | "export" | "publish"; }; type ExportDialogComponent = ( @@ -141,6 +141,14 @@ const EXPORT_DESTINATIONS = [ { id: "github", label: "Send to GitHub", active: true }, ]; const SEND_TO_DESTINATIONS = ["page", "graph"]; +const INITIAL_PANEL_TO_TAB_ID: Record< + NonNullable, + string +> = { + sendTo: "sendto", + export: "export", + publish: "publish", +}; const exportDestinationById = Object.fromEntries( EXPORT_DESTINATIONS.map((ed) => [ed.id, ed]), @@ -211,7 +219,7 @@ const ExportDialog: ExportDialogComponent = ({ const [livePages, setLivePages] = useState([]); const [selectedTabId, setSelectedTabId] = useState("sendto"); useEffect(() => { - if (initialPanel === "export") setSelectedTabId("export"); + if (initialPanel) setSelectedTabId(INITIAL_PANEL_TO_TAB_ID[initialPanel]); }, [initialPanel]); const [includeDiscourseContext, setIncludeDiscourseContext] = useState(false); const [gitHubAccessToken, setGitHubAccessToken] = useState( diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 16daee7e0..d210c2454 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -281,6 +281,7 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { exportRender({ results: [{ uid: pageUid, text: pageTitle, type: discourseNode.type }], isExportDiscourseGraph: true, + initialPanel: "publish", }); }; From f8e9a6670f84c3fee6c3c579d6cfab0f6ee561de Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 22 Jun 2026 19:34:54 +0530 Subject: [PATCH 3/7] [ENG-1851] Add page title publish button --- .../src/components/PublishNodeTitleButton.tsx | 54 +++++++++++++++++++ .../utils/initializeObserversAndListeners.ts | 7 +++ apps/roam/src/utils/openShareNodeDialog.ts | 17 ++++++ .../utils/registerCommandPaletteCommands.ts | 9 ++-- 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 apps/roam/src/components/PublishNodeTitleButton.tsx create mode 100644 apps/roam/src/utils/openShareNodeDialog.ts diff --git a/apps/roam/src/components/PublishNodeTitleButton.tsx b/apps/roam/src/components/PublishNodeTitleButton.tsx new file mode 100644 index 000000000..d0edf417a --- /dev/null +++ b/apps/roam/src/components/PublishNodeTitleButton.tsx @@ -0,0 +1,54 @@ +import { Button } from "@blueprintjs/core"; +import posthog from "posthog-js"; +import React from "react"; +import { handleTitleAdditions } from "~/utils/handleTitleAdditions"; +import { openShareNodeDialog } from "~/utils/openShareNodeDialog"; + +const PUBLISH_TITLE_BUTTON_ATTRIBUTE = "data-roamjs-publish-node-title-button"; + +const PublishNodeTitleButton = ({ + uid, + title, + nodeType, +}: { + uid: string; + title: string; + nodeType: string; +}): JSX.Element => ( +
+
+); + +export const renderPublishNodeTitleButton = ({ + h1, + uid, + title, + nodeType, +}: { + h1: HTMLHeadingElement; + uid: string; + title: string; + nodeType: string; +}): void => { + if (!uid) return; + if (h1.getAttribute(PUBLISH_TITLE_BUTTON_ATTRIBUTE) === uid) return; + + h1.setAttribute(PUBLISH_TITLE_BUTTON_ATTRIBUTE, uid); + handleTitleAdditions( + h1, + , + ); +}; diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 75e15cc15..f9a23d87c 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -49,6 +49,7 @@ import { getFeatureFlag } from "~/components/settings/utils/accessors"; import { getCleanTagText } from "~/components/settings/NodeConfig"; import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors"; import { renderPossibleDuplicates } from "~/components/VectorDuplicateMatches"; +import { renderPublishNodeTitleButton } from "~/components/PublishNodeTitleButton"; import { renderCanvasEmbed } from "~/components/canvas/CanvasEmbed"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; @@ -124,6 +125,12 @@ export const initObservers = ({ const isDiscourseNode = node && node.backedBy !== "default"; if (isDiscourseNode) { renderDiscourseContext({ h1, uid }); + renderPublishNodeTitleButton({ + h1, + uid, + title, + nodeType: node.type, + }); if (getFeatureFlag("Duplicate node alert enabled")) { renderPossibleDuplicates(h1, title, node); } diff --git a/apps/roam/src/utils/openShareNodeDialog.ts b/apps/roam/src/utils/openShareNodeDialog.ts new file mode 100644 index 000000000..bb78b7e3c --- /dev/null +++ b/apps/roam/src/utils/openShareNodeDialog.ts @@ -0,0 +1,17 @@ +import { render as exportRender } from "~/components/Export"; + +export const openShareNodeDialog = ({ + uid, + title, + nodeType, +}: { + uid: string; + title: string; + nodeType: string; +}): void => { + exportRender({ + results: [{ uid, text: title, type: nodeType }], + isExportDiscourseGraph: true, + initialPanel: "publish", + }); +}; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index d210c2454..32cb4959f 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -1,6 +1,7 @@ import { openQueryDrawer } from "~/components/QueryDrawer"; import { render as exportRender } from "~/components/Export"; import { render as renderToast } from "roamjs-components/components/Toast"; +import { openShareNodeDialog } from "~/utils/openShareNodeDialog"; import { createBlock, updateBlock } from "roamjs-components/writes"; import { getCurrentPageUid, @@ -278,10 +279,10 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { nodeType: discourseNode.type, }); - exportRender({ - results: [{ uid: pageUid, text: pageTitle, type: discourseNode.type }], - isExportDiscourseGraph: true, - initialPanel: "publish", + openShareNodeDialog({ + uid: pageUid, + title: pageTitle, + nodeType: discourseNode.type, }); }; From 574313c3cf8e426433fc7310958b519ff0b3da5f Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 22 Jun 2026 20:20:51 +0530 Subject: [PATCH 4/7] [ENG-1851] Align title publish actions --- .../src/components/PublishNodeTitleButton.tsx | 1 + apps/roam/src/styles/styles.css | 18 ++++++++++++++++++ apps/roam/src/utils/handleTitleAdditions.ts | 9 ++++++++- .../utils/initializeObserversAndListeners.ts | 4 +++- .../utils/renderLinkedReferenceAdditions.ts | 1 + 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/components/PublishNodeTitleButton.tsx b/apps/roam/src/components/PublishNodeTitleButton.tsx index d0edf417a..1885ecee9 100644 --- a/apps/roam/src/components/PublishNodeTitleButton.tsx +++ b/apps/roam/src/components/PublishNodeTitleButton.tsx @@ -50,5 +50,6 @@ export const renderPublishNodeTitleButton = ({ handleTitleAdditions( h1, , + { layout: "inline" }, ); }; diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index ef6ccf4f8..a4ceb4fb5 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -2,6 +2,24 @@ outline-width: 2px; } +.discourse-graph-title-additions { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + column-gap: 8px; + row-gap: 8px; +} + +.discourse-graph-title-addition-inline { + flex: 0 0 auto; + min-width: 0; +} + +.discourse-graph-title-addition-block { + flex: 1 0 100%; + min-width: 0; +} + .roamjs-item-dirty, .roamjs-item-dirty.bp3-menu-item .bp3-icon, .roamjs-item-dirty.bp3-button .bp3-icon { diff --git a/apps/roam/src/utils/handleTitleAdditions.ts b/apps/roam/src/utils/handleTitleAdditions.ts index 9901fc312..27e930e5f 100644 --- a/apps/roam/src/utils/handleTitleAdditions.ts +++ b/apps/roam/src/utils/handleTitleAdditions.ts @@ -3,10 +3,15 @@ import ReactDOM from "react-dom"; const ROAM_TITLE_CONTAINER_CLASS = "rm-title-display-container"; const ADDITIONS_CONTAINER_CLASS = "discourse-graph-title-additions"; +const INLINE_ADDITION_CLASS = "discourse-graph-title-addition-inline"; +const BLOCK_ADDITION_CLASS = "discourse-graph-title-addition-block"; + +type TitleAdditionLayout = "inline" | "block"; export const handleTitleAdditions = ( h1: HTMLHeadingElement, element: React.ReactNode, + { layout = "block" }: { layout?: TitleAdditionLayout } = {}, ): void => { const titleDisplayContainer = h1.closest(`.${ROAM_TITLE_CONTAINER_CLASS}`) || @@ -25,7 +30,7 @@ export const handleTitleAdditions = ( if (!parent) return; container = document.createElement("div"); - container.className = `${ADDITIONS_CONTAINER_CLASS} flex flex-col`; + container.className = ADDITIONS_CONTAINER_CLASS; const oldMarginBottom = getComputedStyle(h1).marginBottom; const oldMarginBottomNum = Number.isFinite(parseFloat(oldMarginBottom)) @@ -45,6 +50,8 @@ export const handleTitleAdditions = ( if (React.isValidElement(element)) { const renderContainer = document.createElement("div"); + renderContainer.className = + layout === "inline" ? INLINE_ADDITION_CLASS : BLOCK_ADDITION_CLASS; container.appendChild(renderContainer); // eslint-disable-next-line react/no-deprecated ReactDOM.render(element, renderContainer); diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index f9a23d87c..4ff94f958 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -124,13 +124,15 @@ export const initObservers = ({ const isDiscourseNode = node && node.backedBy !== "default"; if (isDiscourseNode) { - renderDiscourseContext({ h1, uid }); renderPublishNodeTitleButton({ h1, uid, title, nodeType: node.type, }); + if (settings.personalSettings[PERSONAL_KEYS.discourseContextOverlay]) { + renderDiscourseContext({ h1, uid }); + } if (getFeatureFlag("Duplicate node alert enabled")) { renderPossibleDuplicates(h1, title, node); } diff --git a/apps/roam/src/utils/renderLinkedReferenceAdditions.ts b/apps/roam/src/utils/renderLinkedReferenceAdditions.ts index 043dda1db..687e714c5 100644 --- a/apps/roam/src/utils/renderLinkedReferenceAdditions.ts +++ b/apps/roam/src/utils/renderLinkedReferenceAdditions.ts @@ -20,6 +20,7 @@ export const renderDiscourseContext = ({ uid, id: nanoid(), }), + { layout: "inline" }, ); h1.setAttribute("data-roamjs-top-discourse-context", "true"); }; From b2a28990a84db21b87bb513af135642c3aa26693 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 22 Jun 2026 21:34:52 +0530 Subject: [PATCH 5/7] [ENG-1851] Gate publish action availability --- apps/roam/src/components/Export.tsx | 5 +++-- .../utils/initializeObserversAndListeners.ts | 17 ++++++++++------- .../src/utils/registerCommandPaletteCommands.ts | 12 ++++++++++-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index be012f5c7..d068e863b 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -217,10 +217,12 @@ const ExportDialog: ExportDialogComponent = ({ useState<(typeof SEND_TO_DESTINATIONS)[number]>("page"); const isSendToGraph = activeSendToDestination === "graph"; const [livePages, setLivePages] = useState([]); + const syncEnabled = useMemo(() => isSyncEnabled(), []); const [selectedTabId, setSelectedTabId] = useState("sendto"); useEffect(() => { + if (initialPanel === "publish" && !syncEnabled) return; if (initialPanel) setSelectedTabId(INITIAL_PANEL_TO_TAB_ID[initialPanel]); - }, [initialPanel]); + }, [initialPanel, syncEnabled]); const [includeDiscourseContext, setIncludeDiscourseContext] = useState(false); const [gitHubAccessToken, setGitHubAccessToken] = useState( getSetting("oauth-github", null), @@ -228,7 +230,6 @@ const ExportDialog: ExportDialogComponent = ({ const [canSendToGitHub, setCanSendToGitHub] = useState(false); - const syncEnabled = useMemo(() => isSyncEnabled(), []); const [myGroups, setMyGroups] = useState([]); const [groupsLoading, setGroupsLoading] = useState(false); const [groupsLoaded, setGroupsLoaded] = useState(false); diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 4ff94f958..16618003a 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -45,7 +45,6 @@ import { import { renderNodeTagPopupButton } from "./renderNodeTagPopup"; import { renderImageToolsMenu } from "./renderImageToolsMenu"; import { mountLeftSidebar } from "~/components/LeftSidebarView"; -import { getFeatureFlag } from "~/components/settings/utils/accessors"; import { getCleanTagText } from "~/components/settings/NodeConfig"; import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors"; import { renderPossibleDuplicates } from "~/components/VectorDuplicateMatches"; @@ -56,6 +55,8 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU import findDiscourseNode from "./findDiscourseNode"; import { bulkReadSettings, + getFeatureFlag, + isSyncEnabled, type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { @@ -124,12 +125,14 @@ export const initObservers = ({ const isDiscourseNode = node && node.backedBy !== "default"; if (isDiscourseNode) { - renderPublishNodeTitleButton({ - h1, - uid, - title, - nodeType: node.type, - }); + if (isSyncEnabled() && node.backedBy === "user") { + renderPublishNodeTitleButton({ + h1, + uid, + title, + nodeType: node.type, + }); + } if (settings.personalSettings[PERSONAL_KEYS.discourseContextOverlay]) { renderDiscourseContext({ h1, uid }); } diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 32cb4959f..00c7ff076 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -247,6 +247,14 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }; const shareCurrentNode = () => { + if (!isSyncEnabled()) { + renderToast({ + id: "share-node-sync-disabled", + content: "Sync must be enabled to publish discourse nodes.", + }); + return; + } + const pageUid = getCurrentPageUid(); if (!pageUid) { renderToast({ @@ -266,10 +274,10 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { } const discourseNode = findDiscourseNode({ uid: pageUid, title: pageTitle }); - if (!discourseNode || discourseNode.backedBy === "default") { + if (!discourseNode || discourseNode.backedBy !== "user") { renderToast({ id: "share-node-not-a-node", - content: "This page is not a discourse node, so it can't be shared.", + content: "This page is not a publishable discourse node.", }); return; } From 8b08a6b85f9d619195e63852ea069fef6ed3c03b Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 22 Jun 2026 21:38:58 +0530 Subject: [PATCH 6/7] [ENG-1851] Inline title action layout styles --- .../src/components/PublishNodeTitleButton.tsx | 2 +- apps/roam/src/styles/styles.css | 18 ------------------ apps/roam/src/utils/handleTitleAdditions.ts | 9 +++++---- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/apps/roam/src/components/PublishNodeTitleButton.tsx b/apps/roam/src/components/PublishNodeTitleButton.tsx index 1885ecee9..68d546624 100644 --- a/apps/roam/src/components/PublishNodeTitleButton.tsx +++ b/apps/roam/src/components/PublishNodeTitleButton.tsx @@ -15,7 +15,7 @@ const PublishNodeTitleButton = ({ title: string; nodeType: string; }): JSX.Element => ( -
+
+