From 564ee2d636d10986c3b1862852fd7a81d27ebb0c Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 23 Aug 2026 13:40:59 +0530 Subject: [PATCH 1/2] ENG-2156 Decorate imported node titles in Roam from core_title --- .../components/settings/utils/accessors.ts | 3 + .../utils/__tests__/importSharedNodes.test.ts | 47 ++++ .../__tests__/materializeSharedNode.test.ts | 130 +++++++++ .../__tests__/refreshImportedNode.test.ts | 38 +++ .../__tests__/resolveSharedNodeTypes.test.ts | 256 ++++++++++++++++++ apps/roam/src/utils/importSharedNodes.ts | 8 +- apps/roam/src/utils/materializeSharedNode.ts | 12 +- apps/roam/src/utils/refreshImportedNode.ts | 6 + apps/roam/src/utils/resolveSharedNodeTypes.ts | 101 +++++++ .../src/lib/__tests__/sharedNodes.test.ts | 21 +- packages/database/src/lib/sharedNodes.ts | 8 +- 11 files changed, 624 insertions(+), 6 deletions(-) create mode 100644 apps/roam/src/utils/__tests__/resolveSharedNodeTypes.test.ts create mode 100644 apps/roam/src/utils/resolveSharedNodeTypes.ts diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 20250daff..014f9fff2 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -1090,13 +1090,16 @@ export const createDiscourseNodeType = async ({ text, shortcut, format, + uid, }: { text: string; shortcut: string; format: string; + uid?: string; }): Promise => { const pageUid = await createPage({ title: `${DISCOURSE_NODE_PAGE_PREFIX}${text}`, + uid, tree: [ { text: "Shortcut", children: [{ text: shortcut }] }, { text: "Tag", children: [{ text: "" }] }, diff --git a/apps/roam/src/utils/__tests__/importSharedNodes.test.ts b/apps/roam/src/utils/__tests__/importSharedNodes.test.ts index d89d70400..23c0fed30 100644 --- a/apps/roam/src/utils/__tests__/importSharedNodes.test.ts +++ b/apps/roam/src/utils/__tests__/importSharedNodes.test.ts @@ -1,11 +1,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { DGSupabaseClient } from "@repo/database/lib/client"; import type { SharedNode } from "@repo/database/lib/sharedNodes"; +import type { DiscourseNode } from "~/utils/getDiscourseNodes"; import { importSharedNodes, isFailedSharedNodeImport, } from "~/utils/importSharedNodes"; import { materializeSharedNode } from "~/utils/materializeSharedNode"; +import { resolveSharedNodeTypes } from "~/utils/resolveSharedNodeTypes"; vi.mock("~/utils/materializeSharedNode", async () => { const actual = await vi.importActual< @@ -14,13 +16,29 @@ vi.mock("~/utils/materializeSharedNode", async () => { return { ...actual, materializeSharedNode: vi.fn() }; }); +vi.mock("~/utils/resolveSharedNodeTypes", () => ({ + resolveSharedNodeTypes: vi.fn(), +})); + const mockedMaterializeSharedNode = vi.mocked(materializeSharedNode); +const mockedResolveSharedNodeTypes = vi.mocked(resolveSharedNodeTypes); + +const NODE_TYPE: DiscourseNode = { + text: "Evidence", + type: "evd-type-uid", + shortcut: "E", + format: "[[EVD]] - {content}", + specification: [], + backedBy: "user", + canvasSettings: {}, +}; const client = {} as DGSupabaseClient; const makeSharedNode = (sourceLocalId: string): SharedNode => ({ rid: `orn:obsidian.note:vault-a/${sourceLocalId}`, sourceLocalId, + schemaId: 200, spaceId: 20, spaceName: "Research vault", spaceUri: "obsidian:vault-a", @@ -45,6 +63,7 @@ const successResult = ( beforeEach(() => { vi.clearAllMocks(); + mockedResolveSharedNodeTypes.mockResolvedValue(new Map()); }); describe("importSharedNodes", () => { @@ -89,6 +108,34 @@ describe("importSharedNodes", () => { }); }); + it("resolves node types once and gives each node the one for its schema", async () => { + const sharedNodes = ["node-1", "node-2"].map(makeSharedNode); + mockedResolveSharedNodeTypes.mockResolvedValue( + new Map([[sharedNodes[0].rid, NODE_TYPE]]), + ); + mockedMaterializeSharedNode + .mockResolvedValueOnce(successResult(sharedNodes[0], "created")) + .mockResolvedValueOnce(successResult(sharedNodes[1], "created")); + + await importSharedNodes({ client, sharedNodes, onProgress: vi.fn() }); + + expect(mockedResolveSharedNodeTypes).toHaveBeenCalledTimes(1); + expect(mockedResolveSharedNodeTypes).toHaveBeenCalledWith({ + client, + sharedNodes, + }); + expect(mockedMaterializeSharedNode).toHaveBeenNthCalledWith(1, { + client, + sharedNode: sharedNodes[0], + nodeType: NODE_TYPE, + }); + expect(mockedMaterializeSharedNode).toHaveBeenNthCalledWith(2, { + client, + sharedNode: sharedNodes[1], + nodeType: undefined, + }); + }); + it("keeps importing the remaining nodes when a materialization throws", async () => { const sharedNodes = ["node-1", "node-2"].map(makeSharedNode); mockedMaterializeSharedNode diff --git a/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts b/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts index 425404fc8..724f3cf1b 100644 --- a/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts +++ b/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts @@ -49,9 +49,14 @@ const pageCreate = vi.fn(); const pageDelete = vi.fn(); const updatePage = vi.fn(); +const CORE_TITLE = "REM sleep and recall"; +const DECORATED_TITLE = "[[EVD]] - REM sleep and recall"; +const NODE_TYPE = { format: "[[EVD]] - {content}" }; + const sharedNode: SharedNode = { rid: "orn:obsidian.note:vault-a/node-1", sourceLocalId: "node-1", + schemaId: 200, spaceId: 20, spaceName: "Research vault", spaceUri: "obsidian:vault-a", @@ -63,6 +68,11 @@ const sharedNode: SharedNode = { directMetadata: null, }; +const decoratedSharedNode: SharedNode = { + ...sharedNode, + coreTitle: CORE_TITLE, +}; + const roamSharedNode: SharedNode = { ...sharedNode, rid: "https://roamresearch.com/#/app/source-graph/node-2", @@ -337,6 +347,126 @@ describe("materializeSharedNode", () => { }); }); + it("decorates the page title with the local node type format", async () => { + const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); + + const result = await materializeSharedNode({ + client, + sharedNode: decoratedSharedNode, + nodeType: NODE_TYPE, + }); + + expect(result.success).toBe(true); + expect(pageFromMarkdown).toHaveBeenCalledWith({ + page: { title: DECORATED_TITLE, uid: GENERATED_PAGE_UID }, + "markdown-string": MATERIALIZED_MARKDOWN, + }); + }); + + it("keeps the incoming title when the source published no core title", async () => { + const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); + + const result = await materializeSharedNode({ + client, + sharedNode, + nodeType: NODE_TYPE, + }); + + expect(result.success).toBe(true); + expect(pageFromMarkdown).toHaveBeenCalledWith({ + page: { title: sharedNode.title, uid: GENERATED_PAGE_UID }, + "markdown-string": MATERIALIZED_MARKDOWN, + }); + }); + + it("keeps the incoming title when the local node type has no format", async () => { + const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); + + const result = await materializeSharedNode({ + client, + sharedNode: decoratedSharedNode, + nodeType: { format: "" }, + }); + + expect(result.success).toBe(true); + expect(pageFromMarkdown).toHaveBeenCalledWith({ + page: { title: sharedNode.title, uid: GENERATED_PAGE_UID }, + "markdown-string": MATERIALIZED_MARKDOWN, + }); + }); + + it("decorates a format whose source placeholder has no value yet", async () => { + const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); + + const result = await materializeSharedNode({ + client, + sharedNode: decoratedSharedNode, + nodeType: { format: "[[EVD]] - {content} - {Source}" }, + }); + + expect(result.success).toBe(true); + expect(pageFromMarkdown).toHaveBeenCalledWith({ + page: { title: `${DECORATED_TITLE} - `, uid: GENERATED_PAGE_UID }, + "markdown-string": MATERIALIZED_MARKDOWN, + }); + }); + + it("strips the Roam heading by the source title while decorating the page title", async () => { + const { client } = clientWithFullContent({ + text: `# ${roamSharedNode.title}\n\n- REM sleep improves recall`, + contentType: "text/roam+markdown", + }); + + const result = await materializeSharedNode({ + client, + sharedNode: { ...roamSharedNode, coreTitle: CORE_TITLE }, + nodeType: NODE_TYPE, + }); + + expect(result.success).toBe(true); + expect(pageFromMarkdown).toHaveBeenCalledWith({ + page: { title: DECORATED_TITLE, uid: GENERATED_PAGE_UID }, + "markdown-string": "- REM sleep improves recall", + }); + }); + + it("renames the imported page when decoration changes its title", async () => { + const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); + mockedFindImportedNodeUidBySourceRid.mockResolvedValue(EXISTING_PAGE_UID); + mockedGetPageTitleByPageUid.mockReturnValue(sharedNode.title); + + const result = await materializeSharedNode({ + client, + sharedNode: decoratedSharedNode, + nodeType: NODE_TYPE, + }); + + expect(result.success).toBe(true); + expect(updatePage).toHaveBeenCalledWith({ + page: { uid: EXISTING_PAGE_UID, title: DECORATED_TITLE }, + }); + }); + + it("leaves an already decorated title untouched when refreshing", async () => { + const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); + mockedFindImportedNodeUidBySourceRid.mockResolvedValue(EXISTING_PAGE_UID); + mockedGetPageTitleByPageUid.mockReturnValue(DECORATED_TITLE); + mockedReadImportedSourceIdentity.mockReturnValue({ + sourceModifiedAt: sharedNode.lastModified, + sourceNodeRid: sharedNode.rid, + }); + + const result = await materializeSharedNode({ + client, + sharedNode: decoratedSharedNode, + nodeType: NODE_TYPE, + force: true, + }); + + expect(result).toMatchObject({ success: true, action: "updated" }); + expect(updatePage).not.toHaveBeenCalled(); + }); + it("refuses to clobber a page that was not imported from this source", async () => { const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); mockedGetPageUidByPageTitle.mockReturnValue("unrelated-page-uid"); diff --git a/apps/roam/src/utils/__tests__/refreshImportedNode.test.ts b/apps/roam/src/utils/__tests__/refreshImportedNode.test.ts index a308aa3ee..a0badc322 100644 --- a/apps/roam/src/utils/__tests__/refreshImportedNode.test.ts +++ b/apps/roam/src/utils/__tests__/refreshImportedNode.test.ts @@ -5,10 +5,12 @@ import { getSharedNodeByRid, type SharedNode, } from "@repo/database/lib/sharedNodes"; +import type { DiscourseNode } from "~/utils/getDiscourseNodes"; import { readImportedSourceIdentity } from "~/utils/importedSourceIdentity"; import internalError from "~/utils/internalError"; import { materializeSharedNode } from "~/utils/materializeSharedNode"; import { refreshImportedNode } from "~/utils/refreshImportedNode"; +import { resolveSharedNodeTypes } from "~/utils/resolveSharedNodeTypes"; import { getLoggedInClient } from "~/utils/supabaseContext"; vi.mock("roamjs-components/queries/getPageTitleByPageUid", () => ({ @@ -27,6 +29,9 @@ vi.mock("~/utils/materializeSharedNode", async (importOriginal) => ({ ...(await importOriginal()), materializeSharedNode: vi.fn(), })); +vi.mock("~/utils/resolveSharedNodeTypes", () => ({ + resolveSharedNodeTypes: vi.fn(), +})); vi.mock("~/utils/supabaseContext", () => ({ getLoggedInClient: vi.fn(), })); @@ -37,6 +42,17 @@ const mockedReadImportedSourceIdentity = vi.mocked(readImportedSourceIdentity); const mockedInternalError = vi.mocked(internalError); const mockedMaterializeSharedNode = vi.mocked(materializeSharedNode); const mockedGetLoggedInClient = vi.mocked(getLoggedInClient); +const mockedResolveSharedNodeTypes = vi.mocked(resolveSharedNodeTypes); + +const NODE_TYPE: DiscourseNode = { + text: "Evidence", + type: "evd-type-uid", + shortcut: "E", + format: "[[EVD]] - {content}", + specification: [], + backedBy: "user", + canvasSettings: {}, +}; const PAGE_UID = "imported-page-uid"; const LOCAL_TITLE = "EVD - old local title"; @@ -47,6 +63,7 @@ const client = {} as DGSupabaseClient; const sharedNode: SharedNode = { rid: "orn:obsidian.note:vault-a/node-1", sourceLocalId: "node-1", + schemaId: 200, spaceId: 20, spaceName: "Research vault", spaceUri: "obsidian:vault-a", @@ -69,6 +86,7 @@ beforeEach(() => { }); mockedGetLoggedInClient.mockResolvedValue(client); mockedGetSharedNodeByRid.mockResolvedValue(sharedNode); + mockedResolveSharedNodeTypes.mockResolvedValue(new Map()); mockedMaterializeSharedNode.mockResolvedValue({ success: true, action: "updated", @@ -88,14 +106,34 @@ describe("refreshImportedNode", () => { client, rid: sharedNode.rid, }); + expect(mockedResolveSharedNodeTypes).toHaveBeenCalledWith({ + client, + sharedNodes: [sharedNode], + }); expect(mockedMaterializeSharedNode).toHaveBeenCalledWith({ client, sharedNode, + nodeType: undefined, force: true, }); expect(mockedInternalError).not.toHaveBeenCalled(); }); + it("passes the resolved node type to the materializer", async () => { + mockedResolveSharedNodeTypes.mockResolvedValue( + new Map([[sharedNode.rid, NODE_TYPE]]), + ); + + await refreshImportedNode({ pageUid: PAGE_UID }); + + expect(mockedMaterializeSharedNode).toHaveBeenCalledWith({ + client, + sharedNode, + nodeType: NODE_TYPE, + force: true, + }); + }); + it("fails when the page has no stored source identity", async () => { mockedReadImportedSourceIdentity.mockReturnValue(undefined); diff --git a/apps/roam/src/utils/__tests__/resolveSharedNodeTypes.test.ts b/apps/roam/src/utils/__tests__/resolveSharedNodeTypes.test.ts new file mode 100644 index 000000000..74f4a148c --- /dev/null +++ b/apps/roam/src/utils/__tests__/resolveSharedNodeTypes.test.ts @@ -0,0 +1,256 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { DGSupabaseClient } from "@repo/database/lib/client"; +import type { SharedNode } from "@repo/database/lib/sharedNodes"; +import { createDiscourseNodeType } from "~/components/settings/utils/accessors"; +import getDiscourseNodes, { + type DiscourseNode, +} from "~/utils/getDiscourseNodes"; +import internalError from "~/utils/internalError"; +import { resolveSharedNodeTypes } from "~/utils/resolveSharedNodeTypes"; + +vi.mock("posthog-js", () => ({ default: { capture: vi.fn() } })); +vi.mock("~/components/settings/utils/accessors", () => ({ + createDiscourseNodeType: vi.fn(), +})); +vi.mock("~/utils/getDiscourseNodes", () => ({ + default: vi.fn(), + excludeDefaultNodes: (node: DiscourseNode) => node.backedBy !== "default", +})); +vi.mock("~/utils/internalError", () => ({ default: vi.fn() })); + +const mockedCreateDiscourseNodeType = vi.mocked(createDiscourseNodeType); +const mockedGetDiscourseNodes = vi.mocked(getDiscourseNodes); +const mockedInternalError = vi.mocked(internalError); + +const SCHEMA_ID = 200; +const REMOTE_TYPE_UID = "node_hs6r0kqxvbmc3l9ywtd2fp"; +const FORMAT = "[[EVD]] - {content}"; + +const evidenceType: DiscourseNode = { + text: "Evidence", + type: "local-evd-uid", + shortcut: "E", + format: FORMAT, + specification: [], + backedBy: "user", + canvasSettings: {}, +}; + +const pageType: DiscourseNode = { + text: "Page", + type: "page-node", + shortcut: "p", + format: "{content}", + specification: [], + backedBy: "default", + canvasSettings: {}, +}; + +const sharedNode: SharedNode = { + rid: "orn:obsidian.note:vault-a/node-1", + sourceLocalId: "node-1", + schemaId: SCHEMA_ID, + spaceId: 20, + spaceName: "Research vault", + spaceUri: "obsidian:vault-a", + platform: "Obsidian", + title: "EVD - REM sleep and recall", + coreTitle: "REM sleep and recall", + created: "2026-06-14T12:30:00.000Z", + lastModified: "2026-06-14T15:00:00.000Z", + authorId: 7, + directMetadata: null, +}; + +type SchemaRow = { + id: number; + name: string | null; + source_local_id: string | null; + format: string | null; + source_data_format: string | null; +}; + +const schemaRow = (overrides: Partial = {}): SchemaRow => ({ + id: SCHEMA_ID, + name: "Evidence", + source_local_id: REMOTE_TYPE_UID, + format: FORMAT, + source_data_format: null, + ...overrides, +}); + +const makeClient = ({ + rows = [schemaRow()], + error = null, +}: { + rows?: SchemaRow[]; + error?: { message: string } | null; +} = {}) => { + const result = { data: error ? null : rows, error }; + const builder = { + select: vi.fn(), + eq: vi.fn(), + in: vi.fn(), + then: ( + resolve: (value: typeof result) => unknown, + reject?: (reason: unknown) => unknown, + ) => Promise.resolve(result).then(resolve, reject), + }; + builder.select.mockReturnValue(builder); + builder.eq.mockReturnValue(builder); + builder.in.mockReturnValue(builder); + const from = vi.fn().mockReturnValue(builder); + return { client: { from } as unknown as DGSupabaseClient, builder, from }; +}; + +beforeEach(() => { + vi.clearAllMocks(); + mockedGetDiscourseNodes.mockReturnValue([]); +}); + +describe("resolveSharedNodeTypes", () => { + it("matches the local node type carrying the published type id", async () => { + const { client, builder } = makeClient(); + const importedType = { ...evidenceType, type: REMOTE_TYPE_UID }; + mockedGetDiscourseNodes.mockReturnValue([ + { ...evidenceType, text: "Other name" }, + importedType, + ]); + + await expect( + resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), + ).resolves.toEqual(new Map([[sharedNode.rid, importedType]])); + expect(builder.eq).toHaveBeenCalledWith("is_schema", true); + expect(builder.eq).toHaveBeenCalledWith("is_relation", false); + expect(builder.in).toHaveBeenCalledWith("id", [SCHEMA_ID]); + expect(mockedCreateDiscourseNodeType).not.toHaveBeenCalled(); + }); + + it("matches the local node type by name when no id matches", async () => { + const { client } = makeClient(); + mockedGetDiscourseNodes.mockReturnValue([evidenceType]); + + await expect( + resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), + ).resolves.toEqual(new Map([[sharedNode.rid, evidenceType]])); + expect(mockedCreateDiscourseNodeType).not.toHaveBeenCalled(); + }); + + it("creates a local node type reusing the remote id and format", async () => { + const { client } = makeClient(); + const createdType = { ...evidenceType, type: REMOTE_TYPE_UID }; + mockedCreateDiscourseNodeType.mockResolvedValue(createdType); + + await expect( + resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), + ).resolves.toEqual(new Map([[sharedNode.rid, createdType]])); + expect(mockedCreateDiscourseNodeType).toHaveBeenCalledWith({ + text: "Evidence", + shortcut: "", + format: FORMAT, + uid: REMOTE_TYPE_UID, + }); + }); + + it("reads the format Obsidian nests under source_data", async () => { + const { client } = makeClient({ + rows: [ + schemaRow({ + format: null, + source_data_format: "[[EVD]] - {content} - {Source}", + }), + ], + }); + mockedCreateDiscourseNodeType.mockResolvedValue(evidenceType); + + await resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }); + + expect(mockedCreateDiscourseNodeType).toHaveBeenCalledWith( + expect.objectContaining({ format: "[[EVD]] - {content} - {Source}" }), + ); + }); + + it("creates a formatless node type from a schema published before formats", async () => { + const { client } = makeClient({ rows: [schemaRow({ format: null })] }); + mockedCreateDiscourseNodeType.mockResolvedValue(evidenceType); + + await resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }); + + expect(mockedCreateDiscourseNodeType).toHaveBeenCalledWith( + expect.objectContaining({ format: "" }), + ); + }); + + it("never matches the default Page and Block types", async () => { + const { client } = makeClient({ + rows: [schemaRow({ name: "Page", source_local_id: "page-node" })], + }); + mockedGetDiscourseNodes.mockReturnValue([pageType]); + mockedCreateDiscourseNodeType.mockResolvedValue(evidenceType); + + await resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }); + + expect(mockedCreateDiscourseNodeType).toHaveBeenCalledWith( + expect.objectContaining({ text: "Page", uid: "page-node" }), + ); + }); + + it("leaves out a node whose schema row is not visible", async () => { + const { client } = makeClient({ rows: [] }); + + await expect( + resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), + ).resolves.toEqual(new Map()); + expect(mockedCreateDiscourseNodeType).not.toHaveBeenCalled(); + }); + + it("fetches every node type schema in one query", async () => { + const { client, builder, from } = makeClient({ + rows: [schemaRow(), schemaRow({ id: 300, name: "Claim" })], + }); + mockedGetDiscourseNodes.mockReturnValue([evidenceType]); + mockedCreateDiscourseNodeType.mockResolvedValue(evidenceType); + + await resolveSharedNodeTypes({ + client, + sharedNodes: [ + sharedNode, + { ...sharedNode, rid: "orn:obsidian.note:vault-a/node-2" }, + { + ...sharedNode, + rid: "orn:obsidian.note:vault-a/node-3", + schemaId: 300, + }, + ], + }); + + expect(from).toHaveBeenCalledTimes(1); + expect(builder.in).toHaveBeenCalledWith("id", [SCHEMA_ID, 300]); + }); + + it("reports a failed type creation and leaves the node undecorated", async () => { + const { client } = makeClient(); + mockedCreateDiscourseNodeType.mockRejectedValue( + new Error("page create failed"), + ); + + await expect( + resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), + ).resolves.toEqual(new Map()); + expect(mockedInternalError).toHaveBeenCalledWith( + expect.objectContaining({ sendEmail: false }), + ); + }); + + it("reports a failed schema query and leaves every node undecorated", async () => { + const { client } = makeClient({ error: { message: "permission denied" } }); + + await expect( + resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), + ).resolves.toEqual(new Map()); + expect(mockedInternalError).toHaveBeenCalledWith( + expect.objectContaining({ sendEmail: false }), + ); + expect(mockedCreateDiscourseNodeType).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/roam/src/utils/importSharedNodes.ts b/apps/roam/src/utils/importSharedNodes.ts index 9557182d0..d86c145eb 100644 --- a/apps/roam/src/utils/importSharedNodes.ts +++ b/apps/roam/src/utils/importSharedNodes.ts @@ -4,6 +4,7 @@ import { getErrorMessage, materializeSharedNode, } from "./materializeSharedNode"; +import { resolveSharedNodeTypes } from "./resolveSharedNodeTypes"; export type FailedSharedNodeImport = { sharedNode: SharedNode; @@ -28,10 +29,15 @@ export const importSharedNodes = async ({ sharedNodes: SharedNode[]; onProgress: (current: number, total: number) => void; }): Promise => { + const nodeTypesByRid = await resolveSharedNodeTypes({ client, sharedNodes }); const items: SharedNodeImportItem[] = []; for (const sharedNode of sharedNodes) { try { - const result = await materializeSharedNode({ client, sharedNode }); + const result = await materializeSharedNode({ + client, + sharedNode, + nodeType: nodeTypesByRid.get(sharedNode.rid), + }); items.push( result.success ? { diff --git a/apps/roam/src/utils/materializeSharedNode.ts b/apps/roam/src/utils/materializeSharedNode.ts index 599aed4a3..9dc0c700c 100644 --- a/apps/roam/src/utils/materializeSharedNode.ts +++ b/apps/roam/src/utils/materializeSharedNode.ts @@ -5,12 +5,14 @@ import { trimBlankLines, } from "@repo/content-model"; import type { DGSupabaseClient } from "@repo/database/lib/client"; +import { decorateTitle } from "@repo/database/lib/decorateTitle"; import { isRid } from "@repo/database/lib/rid"; import type { SharedNode } from "@repo/database/lib/sharedNodes"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid"; import deleteBlock from "roamjs-components/writes/deleteBlock"; +import type { DiscourseNode } from "./getDiscourseNodes"; import { findImportedNodeUidBySourceRid, readImportedSourceIdentity, @@ -292,10 +294,12 @@ const updateImportedPage = async ({ export const materializeSharedNode = async ({ client, sharedNode, + nodeType, force = false, }: { client: DGSupabaseClient; sharedNode: SharedNode; + nodeType?: Pick; force?: boolean; }): Promise => { const rawIdentity: SourceIdentity = { @@ -315,6 +319,10 @@ export const materializeSharedNode = async ({ sourceModifiedAt: validated.sourceModifiedAt, sourceNodeRid: sharedNode.rid, }; + const pageTitle = + sharedNode.coreTitle && nodeType?.format + ? decorateTitle(nodeType.format, sharedNode.coreTitle) + : validated.title; let importedPageUid: string | null; let storedIdentity: ImportedSourceIdentity | undefined; @@ -363,11 +371,11 @@ export const materializeSharedNode = async ({ identity, markdown: content.markdown, pageUid: importedPageUid, - title: validated.title, + title: pageTitle, }) : createImportedPage({ identity, markdown: content.markdown, - title: validated.title, + title: pageTitle, }); }; diff --git a/apps/roam/src/utils/refreshImportedNode.ts b/apps/roam/src/utils/refreshImportedNode.ts index 62e32381d..dc8b28d27 100644 --- a/apps/roam/src/utils/refreshImportedNode.ts +++ b/apps/roam/src/utils/refreshImportedNode.ts @@ -6,6 +6,7 @@ import { getErrorMessage, materializeSharedNode, } from "./materializeSharedNode"; +import { resolveSharedNodeTypes } from "./resolveSharedNodeTypes"; import { getLoggedInClient } from "./supabaseContext"; const REFRESH_ERROR_TYPE = "Imported node refresh failed"; @@ -47,9 +48,14 @@ export const refreshImportedNode = async ({ message: `The source of "${title}" is no longer shared with your groups, so it cannot be refreshed.`, }; + const nodeTypesByRid = await resolveSharedNodeTypes({ + client, + sharedNodes: [sharedNode], + }); const result = await materializeSharedNode({ client, sharedNode, + nodeType: nodeTypesByRid.get(sharedNode.rid), force: true, }); if (!result.success) { diff --git a/apps/roam/src/utils/resolveSharedNodeTypes.ts b/apps/roam/src/utils/resolveSharedNodeTypes.ts new file mode 100644 index 000000000..aabfbed9c --- /dev/null +++ b/apps/roam/src/utils/resolveSharedNodeTypes.ts @@ -0,0 +1,101 @@ +import posthog from "posthog-js"; +import type { DGSupabaseClient } from "@repo/database/lib/client"; +import type { Tables } from "@repo/database/dbTypes"; +import type { SharedNode } from "@repo/database/lib/sharedNodes"; +import { createDiscourseNodeType } from "~/components/settings/utils/accessors"; +import getDiscourseNodes, { + excludeDefaultNodes, + type DiscourseNode, +} from "./getDiscourseNodes"; +import internalError from "./internalError"; + +const SCHEMA_COLUMNS = + "format:literal_content->>format, id, name, source_data_format:literal_content->source_data->>format, source_local_id"; + +const RESOLVE_ERROR_TYPE = "Imported node type resolution failed"; +const RESOLVE_ERROR_OPERATION = "resolve-shared-node-types"; + +type SharedNodeSchema = Pick< + Tables<"my_concepts">, + "id" | "name" | "source_local_id" +> & { + format: string | null; + source_data_format: string | null; +}; + +const findOrCreateNodeType = async ( + schema: SharedNodeSchema, +): Promise => { + const localNodeTypes = getDiscourseNodes().filter(excludeDefaultNodes); + const matchedById = localNodeTypes.find( + (nodeType) => nodeType.type === schema.source_local_id, + ); + if (matchedById) return matchedById; + const matchedByName = localNodeTypes.find( + (nodeType) => nodeType.text === schema.name, + ); + if (matchedByName) return matchedByName; + if (!schema.name || !schema.source_local_id) return undefined; + + posthog.capture("Discourse Node: Type Created From Import", { + label: schema.name, + }); + return createDiscourseNodeType({ + text: schema.name, + shortcut: "", + format: schema.format ?? schema.source_data_format ?? "", + uid: schema.source_local_id, + }); +}; + +export const resolveSharedNodeTypes = async ({ + client, + sharedNodes, +}: { + client: DGSupabaseClient; + sharedNodes: SharedNode[]; +}): Promise> => { + const schemaIds = [...new Set(sharedNodes.map(({ schemaId }) => schemaId))]; + const { data, error } = await client + .from("my_concepts") + .select(SCHEMA_COLUMNS) + .eq("is_schema", true) + .eq("is_relation", false) + .in("id", schemaIds); + if (error) { + internalError({ + error, + type: RESOLVE_ERROR_TYPE, + context: { operation: RESOLVE_ERROR_OPERATION, schemaIds }, + sendEmail: false, + }); + return new Map(); + } + + const nodeTypeBySchemaId = new Map(); + for (const schema of data) { + if (schema.id === null) continue; + try { + const nodeType = await findOrCreateNodeType(schema); + if (nodeType) nodeTypeBySchemaId.set(schema.id, nodeType); + } catch (error) { + internalError({ + error, + type: RESOLVE_ERROR_TYPE, + context: { + operation: RESOLVE_ERROR_OPERATION, + schemaId: schema.id, + schemaName: schema.name, + }, + sendEmail: false, + }); + } + } + + return new Map( + sharedNodes.flatMap((sharedNode): [string, DiscourseNode][] => { + const nodeType = nodeTypeBySchemaId.get(sharedNode.schemaId); + return nodeType ? [[sharedNode.rid, nodeType]] : []; + }), + ); +}; diff --git a/packages/database/src/lib/__tests__/sharedNodes.test.ts b/packages/database/src/lib/__tests__/sharedNodes.test.ts index 37b422299..75820c350 100644 --- a/packages/database/src/lib/__tests__/sharedNodes.test.ts +++ b/packages/database/src/lib/__tests__/sharedNodes.test.ts @@ -14,6 +14,7 @@ const spaces: BuildArgs["spaces"] = [ ]; const nodes: BuildArgs["nodes"] = [ { + core_title: "REM sleep and recall", is_schema: false, last_modified: "2026-06-14T12:00:00", schema_id: 200, @@ -66,11 +67,13 @@ describe("buildSharedNodes", () => { { rid, sourceLocalId: "node-1", + schemaId: 200, spaceId: 20, spaceName: "Research vault", spaceUri: "obsidian:vault-a", platform: "Obsidian", title: "EVD - REM sleep and recall", + coreTitle: "REM sleep and recall", created: "2026-06-14T11:00:00.000Z", lastModified: "2026-06-14T15:00:00.000Z", authorId: 42, @@ -89,7 +92,12 @@ describe("buildSharedNodes", () => { }, ]; const roamNodes: BuildArgs["nodes"] = [ - { ...nodes[0]!, space_id: 30, source_local_id: "roam-uid-1" }, + { + ...nodes[0]!, + core_title: "Sleep improves memory consolidation", + space_id: 30, + source_local_id: "roam-uid-1", + }, ]; const roamDirect: BuildArgs["directContents"] = [ { @@ -118,11 +126,13 @@ describe("buildSharedNodes", () => { { rid: "https://roamresearch.com/#/app/research-graph/roam-uid-1", sourceLocalId: "roam-uid-1", + schemaId: 200, spaceId: 30, spaceName: "Research graph", spaceUri: "https://roamresearch.com/#/app/research-graph", platform: "Roam", title: "CLM - Sleep improves memory consolidation", + coreTitle: "Sleep improves memory consolidation", created: "2026-06-14T11:00:00.000Z", lastModified: "2026-06-14T15:00:00.000Z", authorId: 42, @@ -131,6 +141,13 @@ describe("buildSharedNodes", () => { ]); }); + it("leaves the core title unset when the source published none", () => { + expect( + build({ nodesOverride: [{ ...nodes[0]!, core_title: null }] })[0] + ?.coreTitle, + ).toBeUndefined(); + }); + it("discovers a node without full content", () => { expect(build({ fullOverride: [] })[0]?.lastModified).toBe( "2026-06-14T13:00:00.000Z", @@ -263,11 +280,13 @@ describe("getSharedNodeByRid", () => { await expect(getSharedNodeByRid({ client, rid })).resolves.toEqual({ rid, sourceLocalId: "node-1", + schemaId: 200, spaceId: 20, spaceName: "Research vault", spaceUri: "obsidian:vault-a", platform: "Obsidian", title: "EVD - REM sleep and recall", + coreTitle: "REM sleep and recall", created: "2026-06-14T11:00:00.000Z", lastModified: "2026-06-14T15:00:00.000Z", authorId: 42, diff --git a/packages/database/src/lib/sharedNodes.ts b/packages/database/src/lib/sharedNodes.ts index 36dd023e7..0e7a160a4 100644 --- a/packages/database/src/lib/sharedNodes.ts +++ b/packages/database/src/lib/sharedNodes.ts @@ -5,7 +5,7 @@ import type { Enums, Json, Tables } from "../dbTypes"; type SharedConcept = Pick< Tables<"my_concepts">, "is_schema" | "last_modified" | "schema_id" | "source_local_id" | "space_id" ->; +> & { core_title: string | null }; type SharedContent = Pick< Tables<"my_contents">, | "author_id" @@ -36,11 +36,13 @@ type ValidSharedSpace = { export type SharedNode = { rid: string; sourceLocalId: string; + schemaId: number; spaceId: number; spaceName: string; spaceUri: string; platform: Platform; title: string; + coreTitle?: string; created: string | null; lastModified: string; authorId?: number; @@ -55,7 +57,7 @@ export type SharedNodeRows = { }; const CONCEPT_COLUMNS = - "is_schema, last_modified, schema_id, source_local_id, space_id"; + "core_title:literal_content->>core_title, is_schema, last_modified, schema_id, source_local_id, space_id"; const DIRECT_CONTENT_COLUMNS = "author_id, created, last_modified, metadata, source_local_id, space_id, text, variant"; const FULL_CONTENT_SUMMARY_COLUMNS = "last_modified, source_local_id, space_id"; @@ -188,11 +190,13 @@ export const buildSharedNodes = ({ { rid, sourceLocalId: node.source_local_id, + schemaId: node.schema_id, spaceId: node.space_id, spaceName: space.name, spaceUri: space.url, platform: space.platform, title: direct.text, + coreTitle: node.core_title ?? undefined, created, lastModified, authorId: direct.author_id ?? undefined, From 4463f2a9d300ffb1eb0bb8d6c13c558b06b8a996 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 23 Aug 2026 16:25:12 +0530 Subject: [PATCH 2/2] Refresh the legacy node type cache after creating a type from an import createDiscourseNodeType only invalidates the new-store cache; with the store flag off getDiscourseNodes reads discourseConfigRef.nodes, so a created type stayed invisible and every later import re-entered the create branch. Mirror the settings panel and call refreshConfigTree after a successful create, and only count the type as created once the create resolved. Built-in types now take part in name matching so a remote schema named Page resolves to Roam's Page instead of creating a user type that shadows it. The resolver returns the map keyed by schema id, which both callers already hold. Format precedence follows the Obsidian reader (source_data first, ||), and the Roam format-expression helper reuses the shared placeholder pattern. --- .../utils/__tests__/importSharedNodes.test.ts | 7 ++- .../__tests__/materializeSharedNode.test.ts | 4 +- .../__tests__/refreshImportedNode.test.ts | 2 +- .../__tests__/resolveSharedNodeTypes.test.ts | 43 +++++++++++-------- .../utils/getDiscourseNodeFormatExpression.ts | 4 +- apps/roam/src/utils/importSharedNodes.ts | 7 ++- apps/roam/src/utils/materializeSharedNode.ts | 4 +- apps/roam/src/utils/refreshImportedNode.ts | 4 +- apps/roam/src/utils/resolveSharedNodeTypes.ts | 31 ++++++------- 9 files changed, 58 insertions(+), 48 deletions(-) diff --git a/apps/roam/src/utils/__tests__/importSharedNodes.test.ts b/apps/roam/src/utils/__tests__/importSharedNodes.test.ts index 23c0fed30..0712131d8 100644 --- a/apps/roam/src/utils/__tests__/importSharedNodes.test.ts +++ b/apps/roam/src/utils/__tests__/importSharedNodes.test.ts @@ -109,9 +109,12 @@ describe("importSharedNodes", () => { }); it("resolves node types once and gives each node the one for its schema", async () => { - const sharedNodes = ["node-1", "node-2"].map(makeSharedNode); + const sharedNodes = [ + makeSharedNode("node-1"), + { ...makeSharedNode("node-2"), schemaId: 300 }, + ]; mockedResolveSharedNodeTypes.mockResolvedValue( - new Map([[sharedNodes[0].rid, NODE_TYPE]]), + new Map([[sharedNodes[0].schemaId, NODE_TYPE]]), ); mockedMaterializeSharedNode .mockResolvedValueOnce(successResult(sharedNodes[0], "created")) diff --git a/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts b/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts index 724f3cf1b..5cf18f4c0 100644 --- a/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts +++ b/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts @@ -395,7 +395,7 @@ describe("materializeSharedNode", () => { }); }); - it("decorates a format whose source placeholder has no value yet", async () => { + it("keeps the incoming title when the format has a placeholder core_title cannot fill", async () => { const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); const result = await materializeSharedNode({ @@ -406,7 +406,7 @@ describe("materializeSharedNode", () => { expect(result.success).toBe(true); expect(pageFromMarkdown).toHaveBeenCalledWith({ - page: { title: `${DECORATED_TITLE} - `, uid: GENERATED_PAGE_UID }, + page: { title: decoratedSharedNode.title, uid: GENERATED_PAGE_UID }, "markdown-string": MATERIALIZED_MARKDOWN, }); }); diff --git a/apps/roam/src/utils/__tests__/refreshImportedNode.test.ts b/apps/roam/src/utils/__tests__/refreshImportedNode.test.ts index a0badc322..4728b7d39 100644 --- a/apps/roam/src/utils/__tests__/refreshImportedNode.test.ts +++ b/apps/roam/src/utils/__tests__/refreshImportedNode.test.ts @@ -121,7 +121,7 @@ describe("refreshImportedNode", () => { it("passes the resolved node type to the materializer", async () => { mockedResolveSharedNodeTypes.mockResolvedValue( - new Map([[sharedNode.rid, NODE_TYPE]]), + new Map([[sharedNode.schemaId, NODE_TYPE]]), ); await refreshImportedNode({ pageUid: PAGE_UID }); diff --git a/apps/roam/src/utils/__tests__/resolveSharedNodeTypes.test.ts b/apps/roam/src/utils/__tests__/resolveSharedNodeTypes.test.ts index 74f4a148c..cf2f2ef01 100644 --- a/apps/roam/src/utils/__tests__/resolveSharedNodeTypes.test.ts +++ b/apps/roam/src/utils/__tests__/resolveSharedNodeTypes.test.ts @@ -6,21 +6,22 @@ import getDiscourseNodes, { type DiscourseNode, } from "~/utils/getDiscourseNodes"; import internalError from "~/utils/internalError"; +import refreshConfigTree from "~/utils/refreshConfigTree"; import { resolveSharedNodeTypes } from "~/utils/resolveSharedNodeTypes"; -vi.mock("posthog-js", () => ({ default: { capture: vi.fn() } })); +const mockedCapture = vi.hoisted(() => vi.fn()); +vi.mock("posthog-js", () => ({ default: { capture: mockedCapture } })); vi.mock("~/components/settings/utils/accessors", () => ({ createDiscourseNodeType: vi.fn(), })); -vi.mock("~/utils/getDiscourseNodes", () => ({ - default: vi.fn(), - excludeDefaultNodes: (node: DiscourseNode) => node.backedBy !== "default", -})); +vi.mock("~/utils/getDiscourseNodes", () => ({ default: vi.fn() })); vi.mock("~/utils/internalError", () => ({ default: vi.fn() })); +vi.mock("~/utils/refreshConfigTree", () => ({ default: vi.fn() })); const mockedCreateDiscourseNodeType = vi.mocked(createDiscourseNodeType); const mockedGetDiscourseNodes = vi.mocked(getDiscourseNodes); const mockedInternalError = vi.mocked(internalError); +const mockedRefreshConfigTree = vi.mocked(refreshConfigTree); const SCHEMA_ID = 200; const REMOTE_TYPE_UID = "node_hs6r0kqxvbmc3l9ywtd2fp"; @@ -119,11 +120,12 @@ describe("resolveSharedNodeTypes", () => { await expect( resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), - ).resolves.toEqual(new Map([[sharedNode.rid, importedType]])); + ).resolves.toEqual(new Map([[SCHEMA_ID, importedType]])); expect(builder.eq).toHaveBeenCalledWith("is_schema", true); expect(builder.eq).toHaveBeenCalledWith("is_relation", false); expect(builder.in).toHaveBeenCalledWith("id", [SCHEMA_ID]); expect(mockedCreateDiscourseNodeType).not.toHaveBeenCalled(); + expect(mockedRefreshConfigTree).not.toHaveBeenCalled(); }); it("matches the local node type by name when no id matches", async () => { @@ -132,7 +134,7 @@ describe("resolveSharedNodeTypes", () => { await expect( resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), - ).resolves.toEqual(new Map([[sharedNode.rid, evidenceType]])); + ).resolves.toEqual(new Map([[SCHEMA_ID, evidenceType]])); expect(mockedCreateDiscourseNodeType).not.toHaveBeenCalled(); }); @@ -143,20 +145,25 @@ describe("resolveSharedNodeTypes", () => { await expect( resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), - ).resolves.toEqual(new Map([[sharedNode.rid, createdType]])); + ).resolves.toEqual(new Map([[SCHEMA_ID, createdType]])); expect(mockedCreateDiscourseNodeType).toHaveBeenCalledWith({ text: "Evidence", shortcut: "", format: FORMAT, uid: REMOTE_TYPE_UID, }); + expect(mockedRefreshConfigTree).toHaveBeenCalledTimes(1); + expect(mockedCapture).toHaveBeenCalledWith( + "Discourse Node: Type Created From Import", + { label: "Evidence" }, + ); }); - it("reads the format Obsidian nests under source_data", async () => { + it("prefers the format Obsidian nests under source_data", async () => { const { client } = makeClient({ rows: [ schemaRow({ - format: null, + format: "EVD - {content}", source_data_format: "[[EVD]] - {content} - {Source}", }), ], @@ -181,18 +188,16 @@ describe("resolveSharedNodeTypes", () => { ); }); - it("never matches the default Page and Block types", async () => { + it("resolves a schema named like a built-in type to the built-in instead of shadowing it", async () => { const { client } = makeClient({ - rows: [schemaRow({ name: "Page", source_local_id: "page-node" })], + rows: [schemaRow({ name: "Page", source_local_id: "remote-page-type" })], }); mockedGetDiscourseNodes.mockReturnValue([pageType]); - mockedCreateDiscourseNodeType.mockResolvedValue(evidenceType); - - await resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }); - expect(mockedCreateDiscourseNodeType).toHaveBeenCalledWith( - expect.objectContaining({ text: "Page", uid: "page-node" }), - ); + await expect( + resolveSharedNodeTypes({ client, sharedNodes: [sharedNode] }), + ).resolves.toEqual(new Map([[SCHEMA_ID, pageType]])); + expect(mockedCreateDiscourseNodeType).not.toHaveBeenCalled(); }); it("leaves out a node whose schema row is not visible", async () => { @@ -240,6 +245,8 @@ describe("resolveSharedNodeTypes", () => { expect(mockedInternalError).toHaveBeenCalledWith( expect.objectContaining({ sendEmail: false }), ); + expect(mockedRefreshConfigTree).not.toHaveBeenCalled(); + expect(mockedCapture).not.toHaveBeenCalled(); }); it("reports a failed schema query and leaves every node undecorated", async () => { diff --git a/apps/roam/src/utils/getDiscourseNodeFormatExpression.ts b/apps/roam/src/utils/getDiscourseNodeFormatExpression.ts index 30ed3884b..d0a329eaf 100644 --- a/apps/roam/src/utils/getDiscourseNodeFormatExpression.ts +++ b/apps/roam/src/utils/getDiscourseNodeFormatExpression.ts @@ -1,7 +1,9 @@ +import { FORMAT_PLACEHOLDER } from "@repo/database/lib/decorateTitle"; + export const getDiscourseNodeFormatInnerExpression = (format: string): string => `${format .replace(/(\[|\]|\?|\.|\+)/g, "\\$1") - .replace(/{[a-zA-Z]+}/g, "(.*?)")}`; + .replace(FORMAT_PLACEHOLDER, "(.*?)")}`; const getDiscourseNodeFormatExpression = (format: string): RegExp => format diff --git a/apps/roam/src/utils/importSharedNodes.ts b/apps/roam/src/utils/importSharedNodes.ts index d86c145eb..6a4f23abc 100644 --- a/apps/roam/src/utils/importSharedNodes.ts +++ b/apps/roam/src/utils/importSharedNodes.ts @@ -29,14 +29,17 @@ export const importSharedNodes = async ({ sharedNodes: SharedNode[]; onProgress: (current: number, total: number) => void; }): Promise => { - const nodeTypesByRid = await resolveSharedNodeTypes({ client, sharedNodes }); + const nodeTypesBySchemaId = await resolveSharedNodeTypes({ + client, + sharedNodes, + }); const items: SharedNodeImportItem[] = []; for (const sharedNode of sharedNodes) { try { const result = await materializeSharedNode({ client, sharedNode, - nodeType: nodeTypesByRid.get(sharedNode.rid), + nodeType: nodeTypesBySchemaId.get(sharedNode.schemaId), }); items.push( result.success diff --git a/apps/roam/src/utils/materializeSharedNode.ts b/apps/roam/src/utils/materializeSharedNode.ts index 9dc0c700c..e98504f25 100644 --- a/apps/roam/src/utils/materializeSharedNode.ts +++ b/apps/roam/src/utils/materializeSharedNode.ts @@ -320,9 +320,9 @@ export const materializeSharedNode = async ({ sourceNodeRid: sharedNode.rid, }; const pageTitle = - sharedNode.coreTitle && nodeType?.format + (sharedNode.coreTitle && nodeType ? decorateTitle(nodeType.format, sharedNode.coreTitle) - : validated.title; + : null) ?? validated.title; let importedPageUid: string | null; let storedIdentity: ImportedSourceIdentity | undefined; diff --git a/apps/roam/src/utils/refreshImportedNode.ts b/apps/roam/src/utils/refreshImportedNode.ts index dc8b28d27..66f52eba5 100644 --- a/apps/roam/src/utils/refreshImportedNode.ts +++ b/apps/roam/src/utils/refreshImportedNode.ts @@ -48,14 +48,14 @@ export const refreshImportedNode = async ({ message: `The source of "${title}" is no longer shared with your groups, so it cannot be refreshed.`, }; - const nodeTypesByRid = await resolveSharedNodeTypes({ + const nodeTypesBySchemaId = await resolveSharedNodeTypes({ client, sharedNodes: [sharedNode], }); const result = await materializeSharedNode({ client, sharedNode, - nodeType: nodeTypesByRid.get(sharedNode.rid), + nodeType: nodeTypesBySchemaId.get(sharedNode.schemaId), force: true, }); if (!result.success) { diff --git a/apps/roam/src/utils/resolveSharedNodeTypes.ts b/apps/roam/src/utils/resolveSharedNodeTypes.ts index aabfbed9c..bdc2736fc 100644 --- a/apps/roam/src/utils/resolveSharedNodeTypes.ts +++ b/apps/roam/src/utils/resolveSharedNodeTypes.ts @@ -3,11 +3,9 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; import type { Tables } from "@repo/database/dbTypes"; import type { SharedNode } from "@repo/database/lib/sharedNodes"; import { createDiscourseNodeType } from "~/components/settings/utils/accessors"; -import getDiscourseNodes, { - excludeDefaultNodes, - type DiscourseNode, -} from "./getDiscourseNodes"; +import getDiscourseNodes, { type DiscourseNode } from "./getDiscourseNodes"; import internalError from "./internalError"; +import refreshConfigTree from "./refreshConfigTree"; const SCHEMA_COLUMNS = "format:literal_content->>format, id, name, source_data_format:literal_content->source_data->>format, source_local_id"; @@ -26,7 +24,7 @@ type SharedNodeSchema = Pick< const findOrCreateNodeType = async ( schema: SharedNodeSchema, ): Promise => { - const localNodeTypes = getDiscourseNodes().filter(excludeDefaultNodes); + const localNodeTypes = getDiscourseNodes(); const matchedById = localNodeTypes.find( (nodeType) => nodeType.type === schema.source_local_id, ); @@ -37,15 +35,17 @@ const findOrCreateNodeType = async ( if (matchedByName) return matchedByName; if (!schema.name || !schema.source_local_id) return undefined; - posthog.capture("Discourse Node: Type Created From Import", { - label: schema.name, - }); - return createDiscourseNodeType({ + const nodeType = await createDiscourseNodeType({ text: schema.name, shortcut: "", - format: schema.format ?? schema.source_data_format ?? "", + format: schema.source_data_format || schema.format || "", uid: schema.source_local_id, }); + refreshConfigTree(); + posthog.capture("Discourse Node: Type Created From Import", { + label: schema.name, + }); + return nodeType; }; export const resolveSharedNodeTypes = async ({ @@ -54,7 +54,7 @@ export const resolveSharedNodeTypes = async ({ }: { client: DGSupabaseClient; sharedNodes: SharedNode[]; -}): Promise> => { +}): Promise> => { const schemaIds = [...new Set(sharedNodes.map(({ schemaId }) => schemaId))]; const { data, error } = await client .from("my_concepts") @@ -69,7 +69,7 @@ export const resolveSharedNodeTypes = async ({ context: { operation: RESOLVE_ERROR_OPERATION, schemaIds }, sendEmail: false, }); - return new Map(); + return new Map(); } const nodeTypeBySchemaId = new Map(); @@ -92,10 +92,5 @@ export const resolveSharedNodeTypes = async ({ } } - return new Map( - sharedNodes.flatMap((sharedNode): [string, DiscourseNode][] => { - const nodeType = nodeTypeBySchemaId.get(sharedNode.schemaId); - return nodeType ? [[sharedNode.rid, nodeType]] : []; - }), - ); + return nodeTypeBySchemaId; };