diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index bb3ea0fb5..d068e863b 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]), @@ -209,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 === "export") setSelectedTabId("export"); - }, [initialPanel]); + if (initialPanel === "publish" && !syncEnabled) return; + if (initialPanel) setSelectedTabId(INITIAL_PANEL_TO_TAB_ID[initialPanel]); + }, [initialPanel, syncEnabled]); const [includeDiscourseContext, setIncludeDiscourseContext] = useState(false); const [gitHubAccessToken, setGitHubAccessToken] = useState( getSetting("oauth-github", null), @@ -220,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/components/PublishNodeTitleButton.tsx b/apps/roam/src/components/PublishNodeTitleButton.tsx new file mode 100644 index 000000000..68d546624 --- /dev/null +++ b/apps/roam/src/components/PublishNodeTitleButton.tsx @@ -0,0 +1,55 @@ +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, + , + { layout: "inline" }, + ); +}; diff --git a/apps/roam/src/utils/handleTitleAdditions.ts b/apps/roam/src/utils/handleTitleAdditions.ts index 9901fc312..c0eadd73d 100644 --- a/apps/roam/src/utils/handleTitleAdditions.ts +++ b/apps/roam/src/utils/handleTitleAdditions.ts @@ -3,10 +3,16 @@ import ReactDOM from "react-dom"; const ROAM_TITLE_CONTAINER_CLASS = "rm-title-display-container"; const ADDITIONS_CONTAINER_CLASS = "discourse-graph-title-additions"; +const ADDITIONS_CONTAINER_CLASSES = `${ADDITIONS_CONTAINER_CLASS} flex flex-wrap items-start gap-x-2 gap-y-2`; +const INLINE_ADDITION_CLASSES = "min-w-0 flex-none"; +const BLOCK_ADDITION_CLASSES = "min-w-0 basis-full grow shrink-0"; + +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 +31,7 @@ export const handleTitleAdditions = ( if (!parent) return; container = document.createElement("div"); - container.className = `${ADDITIONS_CONTAINER_CLASS} flex flex-col`; + container.className = ADDITIONS_CONTAINER_CLASSES; const oldMarginBottom = getComputedStyle(h1).marginBottom; const oldMarginBottomNum = Number.isFinite(parseFloat(oldMarginBottom)) @@ -45,6 +51,8 @@ export const handleTitleAdditions = ( if (React.isValidElement(element)) { const renderContainer = document.createElement("div"); + renderContainer.className = + layout === "inline" ? INLINE_ADDITION_CLASSES : BLOCK_ADDITION_CLASSES; 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 75e15cc15..16618003a 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -45,16 +45,18 @@ 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"; +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"; import findDiscourseNode from "./findDiscourseNode"; import { bulkReadSettings, + getFeatureFlag, + isSyncEnabled, type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { @@ -123,7 +125,17 @@ export const initObservers = ({ const isDiscourseNode = node && node.backedBy !== "default"; if (isDiscourseNode) { - renderDiscourseContext({ h1, uid }); + if (isSyncEnabled() && node.backedBy === "user") { + 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/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 96d5433f8..00c7ff076 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, @@ -29,6 +30,7 @@ import { getPersonalSetting, setPersonalSetting, setGlobalSetting, + isSyncEnabled, } from "~/components/settings/utils/accessors"; import { DISCOURSE_NODE_KEYS, @@ -244,6 +246,54 @@ 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({ + 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 !== "user") { + renderToast({ + id: "share-node-not-a-node", + content: "This page is not a publishable discourse node.", + }); + return; + } + + posthog.capture("Share Node: Current Node Command Triggered", { + pageUid, + nodeType: discourseNode.type, + }); + + openShareNodeDialog({ + uid: pageUid, + title: pageTitle, + nodeType: discourseNode.type, + }); + }; + const exportDiscourseGraph = async () => { posthog.capture("Export: Discourse Graph Command Triggered"); const discourseNodes = getDiscourseNodes().filter(excludeDefaultNodes); @@ -364,6 +414,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"); 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"); };