diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index 3b6f6e1b9..9378e2d40 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -33,6 +33,7 @@ import { } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys"; import type { PersonalSettings } from "~/components/settings/utils/zodSchema"; +import { openCreateNodeDialogFromSelection } from "~/utils/openCreateNodeDialogFromSelection"; type Props = { textarea?: HTMLTextAreaElement; @@ -125,6 +126,27 @@ const NodeMenu = ({ // https://github.com/RoamJS/query-builder/issues/286 if (document.activeElement === textarea) document.body.click(); + if (textarea && selectionStart !== selectionEnd) { + const { windowId } = getUids(textarea); + onClose(); + openCreateNodeDialogFromSelection({ + blockUid: targetBlockUid, + extensionAPI, + nodeType: nodeUid, + onInserted: (pageTitle) => { + posthog.capture("Discourse Node: Created via Node Menu", { + nodeType: nodeUid, + text: pageTitle, + }); + }, + selectedText: highlighted, + selectionEnd, + selectionStart, + windowId: windowId || "main-window", + }); + return; + } + const createNodeAndUpdateBlock = async () => { const pageName = await getNewDiscourseNodeText({ text: highlighted, diff --git a/apps/roam/src/utils/__tests__/openCreateNodeDialogFromSelection.test.ts b/apps/roam/src/utils/__tests__/openCreateNodeDialogFromSelection.test.ts new file mode 100644 index 000000000..e7577270d --- /dev/null +++ b/apps/roam/src/utils/__tests__/openCreateNodeDialogFromSelection.test.ts @@ -0,0 +1,75 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OnloadArgs } from "roamjs-components/types"; +import type { ModifyNodeDialogProps } from "~/components/ModifyNodeDialog"; + +type InsertPageRefAtRangeArgs = { + blockUid: string; + pageTitle: string; + selectionEnd: number; + selectionStart: number; + windowId: string; +}; + +const mocks = vi.hoisted(() => ({ + insertPageRefAtRange: + vi.fn<(args: InsertPageRefAtRangeArgs) => Promise>(), + renderModifyNodeDialog: vi.fn<(props: ModifyNodeDialogProps) => void>(), +})); + +vi.mock("~/components/ModifyNodeDialog", () => ({ + renderModifyNodeDialog: mocks.renderModifyNodeDialog, +})); + +vi.mock("~/utils/advancedSearchFooterUtils", () => ({ + insertPageRefAtRange: mocks.insertPageRefAtRange, +})); + +import { openCreateNodeDialogFromSelection } from "~/utils/openCreateNodeDialogFromSelection"; + +describe("openCreateNodeDialogFromSelection", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.insertPageRefAtRange.mockResolvedValue(undefined); + }); + + it("prefills the dialog and replaces the selected text after creation", async () => { + const extensionAPI = {} as OnloadArgs["extensionAPI"]; + const onInserted = vi.fn(); + + openCreateNodeDialogFromSelection({ + blockUid: "block-uid", + extensionAPI, + nodeType: "node-type-uid", + onInserted, + selectedText: "highlighted text", + selectionEnd: 20, + selectionStart: 4, + windowId: "main-window", + }); + + expect(mocks.renderModifyNodeDialog).toHaveBeenCalledWith( + expect.objectContaining({ + extensionAPI, + initialValue: { text: "highlighted text", uid: "" }, + mode: "create", + nodeType: "node-type-uid", + }), + ); + + const dialogProps = mocks.renderModifyNodeDialog.mock.calls[0][0]; + await dialogProps.onSuccess({ + action: "create", + text: "CLM - highlighted text", + uid: "new-node-uid", + }); + + expect(mocks.insertPageRefAtRange).toHaveBeenCalledWith({ + blockUid: "block-uid", + pageTitle: "CLM - highlighted text", + selectionEnd: 20, + selectionStart: 4, + windowId: "main-window", + }); + expect(onInserted).toHaveBeenCalledWith("CLM - highlighted text"); + }); +}); diff --git a/apps/roam/src/utils/openCreateNodeDialogFromSelection.ts b/apps/roam/src/utils/openCreateNodeDialogFromSelection.ts new file mode 100644 index 000000000..b02727479 --- /dev/null +++ b/apps/roam/src/utils/openCreateNodeDialogFromSelection.ts @@ -0,0 +1,43 @@ +import { renderModifyNodeDialog } from "~/components/ModifyNodeDialog"; +import { insertPageRefAtRange } from "~/utils/advancedSearchFooterUtils"; +import type { OnloadArgs } from "roamjs-components/types"; + +type OpenCreateNodeDialogFromSelectionArgs = { + blockUid: string; + extensionAPI: OnloadArgs["extensionAPI"]; + nodeType: string; + onInserted?: (pageTitle: string) => void; + selectedText: string; + selectionEnd: number; + selectionStart: number; + windowId: string; +}; + +export const openCreateNodeDialogFromSelection = ({ + blockUid, + extensionAPI, + nodeType, + onInserted, + selectedText, + selectionEnd, + selectionStart, + windowId, +}: OpenCreateNodeDialogFromSelectionArgs): void => { + renderModifyNodeDialog({ + mode: "create", + nodeType, + initialValue: { text: selectedText, uid: "" }, + extensionAPI, + onSuccess: async (result) => { + await insertPageRefAtRange({ + blockUid, + pageTitle: result.text, + selectionEnd, + selectionStart, + windowId, + }); + onInserted?.(result.text); + }, + onClose: () => {}, + }); +};