Skip to content
17 changes: 13 additions & 4 deletions apps/roam/src/components/Export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export type ExportDialogProps = {
title?: string;
columns?: Column[];
isExportDiscourseGraph?: boolean;
initialPanel?: "sendTo" | "export";
initialPanel?: "sendTo" | "export" | "publish";
};

type ExportDialogComponent = (
Expand All @@ -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<ExportDialogProps["initialPanel"]>,
string
> = {
sendTo: "sendto",
export: "export",
publish: "publish",
};

const exportDestinationById = Object.fromEntries(
EXPORT_DESTINATIONS.map((ed) => [ed.id, ed]),
Expand Down Expand Up @@ -230,18 +238,19 @@ const ExportDialog: ExportDialogComponent = ({
useState<(typeof SEND_TO_DESTINATIONS)[number]>("page");
const isSendToGraph = activeSendToDestination === "graph";
const [livePages, setLivePages] = useState<Result[]>([]);
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<string | null>(
getSetting<string | null>("oauth-github", null),
);

const [canSendToGitHub, setCanSendToGitHub] = useState(false);

const syncEnabled = useMemo(() => isSyncEnabled(), []);
const [myGroups, setMyGroups] = useState<MyGroup[]>([]);
const [groupsLoading, setGroupsLoading] = useState(false);
const [groupsLoaded, setGroupsLoaded] = useState(false);
Expand Down
53 changes: 53 additions & 0 deletions apps/roam/src/components/PublishNodeTitleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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 => (
<Button
text="Publish"
icon="upload"
minimal
outlined
onClick={() => {
posthog.capture("Share Node: Page Title Button Triggered", {
pageUid: uid,
nodeType,
});
openShareNodeDialog({ uid, title, nodeType });
}}
/>
);

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,
<PublishNodeTitleButton uid={uid} title={title} nodeType={nodeType} />,
{ layout: "inline" },
);
};
10 changes: 9 additions & 1 deletion apps/roam/src/utils/handleTitleAdditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`) ||
Expand All @@ -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))
Expand All @@ -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);
Expand Down
24 changes: 20 additions & 4 deletions apps/roam/src/utils/initializeObserversAndListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ 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";
Expand All @@ -62,6 +62,7 @@ import {
settingKeys,
} from "~/components/settings/utils/settingsEmitter";
import {
FEATURE_FLAG_KEYS,
PERSONAL_KEYS,
GLOBAL_KEYS,
} from "~/components/settings/utils/settingKeys";
Expand Down Expand Up @@ -123,8 +124,23 @@ export const initObservers = ({

const isDiscourseNode = node && node.backedBy !== "default";
if (isDiscourseNode) {
renderDiscourseContext({ h1, uid });
if (getFeatureFlag("Duplicate node alert enabled")) {
const syncEnabled =
settings.featureFlags[FEATURE_FLAG_KEYS.duplicateNodeAlertEnabled] ||
settings.featureFlags[FEATURE_FLAG_KEYS.suggestiveModeOverlayEnabled];
if (syncEnabled && node.backedBy === "user") {
renderPublishNodeTitleButton({
h1,
uid,
title,
nodeType: node.type,
});
}
if (settings.personalSettings[PERSONAL_KEYS.discourseContextOverlay]) {
renderDiscourseContext({ h1, uid });
}
if (
settings.featureFlags[FEATURE_FLAG_KEYS.duplicateNodeAlertEnabled]
) {
renderPossibleDuplicates(h1, title, node);
}
const linkedReferencesDiv = document.querySelector(
Expand Down Expand Up @@ -211,7 +227,7 @@ export const initObservers = ({
});
}) as EventListener;

if (getFeatureFlag("Suggestive mode overlay enabled")) {
if (settings.featureFlags[FEATURE_FLAG_KEYS.suggestiveModeOverlayEnabled]) {
addPageRefObserver(getSuggestiveOverlayHandler(onloadArgs));
}

Expand Down
17 changes: 17 additions & 0 deletions apps/roam/src/utils/openShareNodeDialog.ts
Original file line number Diff line number Diff line change
@@ -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",
});
};
53 changes: 53 additions & 0 deletions apps/roam/src/utils/registerCommandPaletteCommands.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -29,6 +30,7 @@ import {
getPersonalSetting,
setPersonalSetting,
setGlobalSetting,
isSyncEnabled,
} from "~/components/settings/utils/accessors";
import {
DISCOURSE_NODE_KEYS,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions apps/roam/src/utils/renderLinkedReferenceAdditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const renderDiscourseContext = ({
uid,
id: nanoid(),
}),
{ layout: "inline" },
);
h1.setAttribute("data-roamjs-top-discourse-context", "true");
};
Expand Down