From dc87ee0c6a8d06496e045124780e0fcd40ed3a81 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Wed, 22 Jul 2026 19:08:55 -0400 Subject: [PATCH 1/2] eng-2049-refactor-publish-nodes --- apps/roam/src/components/Export.tsx | 10 ++-- apps/roam/src/utils/publishNodesToGroups.ts | 27 ++++++++-- .../src/utils/roamToCrossAppConverters.ts | 52 +++++++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index d7ae86e84..7fd8c14b1 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -87,8 +87,8 @@ import { AddReferencedNodeType } from "./canvas/DiscourseRelationShape/Discourse import posthog from "posthog-js"; import { getMyGroups, type MyGroup } from "@repo/database/lib/groups"; import { - publishNodesToGroups, - type PublishNode, + publishNodeUidsWithTypeToGroups, + type nodeUidWithType, } from "~/utils/publishNodesToGroups"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; import { isSyncEnabled } from "~/components/settings/utils/accessors"; @@ -248,7 +248,7 @@ const ExportDialog: ExportDialogComponent = ({ ? { uid: r.uid, type: node.type } : null; }) - .filter((n): n is PublishNode => n !== null) + .filter((n): n is nodeUidWithType => n !== null) : [], [results, syncEnabled], ); @@ -843,11 +843,11 @@ const ExportDialog: ExportDialogComponent = ({ skippedUnsyncedUids, okGroupIds, failedGroupIds, - } = await publishNodesToGroups({ + } = await publishNodeUidsWithTypeToGroups({ client, spaceId: context.spaceId, groupIds: selectedGroupIds, - nodes: publishableNodes, + publishNodes: publishableNodes, }); posthog.capture("Export Dialog: Publish", { groupCount: okGroupIds.length, diff --git a/apps/roam/src/utils/publishNodesToGroups.ts b/apps/roam/src/utils/publishNodesToGroups.ts index 4cba7b06a..f41e15925 100644 --- a/apps/roam/src/utils/publishNodesToGroups.ts +++ b/apps/roam/src/utils/publishNodesToGroups.ts @@ -1,7 +1,9 @@ +import { CrossAppNode } from "@repo/database/crossAppContracts"; import type { DGSupabaseClient } from "@repo/database/lib/client"; import { getAvailableGroupIds } from "@repo/database/lib/groups"; +import { nodeUidsWithTypeToCrossApp } from "./roamToCrossAppConverters"; -export type PublishNode = { +export type nodeUidWithType = { uid: string; type: string; }; @@ -37,7 +39,7 @@ export const publishNodesToGroups = async ({ client: DGSupabaseClient; spaceId: number; groupIds: string[]; - nodes: PublishNode[]; + nodes: CrossAppNode[]; }): Promise => { const result: PublishNodesResult = { publishedNodeUids: [], @@ -57,7 +59,7 @@ export const publishNodesToGroups = async ({ ); if (targetGroupIds.length === 0) return result; - const uids = [...new Set(nodes.map((node) => node.uid))]; + const uids = [...new Set(nodes.map((node) => node.localId))]; const syncedRes = await client .from("my_concepts") @@ -77,7 +79,9 @@ export const publishNodesToGroups = async ({ // Required dependency: the node-type schema concept, when it is synced too. const types = [ ...new Set( - nodes.filter((node) => syncedUids.has(node.uid)).map((node) => node.type), + nodes + .filter((node) => syncedUids.has(node.localId)) + .map((node) => node.nodeType), ), ]; const schemaRes = await client @@ -125,3 +129,18 @@ export const publishNodesToGroups = async ({ result.publishedNodeUids = result.okGroupIds.length > 0 ? syncedNodeUids : []; return result; }; + +export const publishNodeUidsWithTypeToGroups = async ({ + client, + spaceId, + groupIds, + publishNodes, +}: { + client: DGSupabaseClient; + spaceId: number; + groupIds: string[]; + publishNodes: nodeUidWithType[]; +}): Promise => { + const nodes = await nodeUidsWithTypeToCrossApp(publishNodes); + return await publishNodesToGroups({ client, spaceId, groupIds, nodes }); +}; diff --git a/apps/roam/src/utils/roamToCrossAppConverters.ts b/apps/roam/src/utils/roamToCrossAppConverters.ts index 011069c66..da96b22e8 100644 --- a/apps/roam/src/utils/roamToCrossAppConverters.ts +++ b/apps/roam/src/utils/roamToCrossAppConverters.ts @@ -2,6 +2,8 @@ import type { CrossAppNode } from "@repo/database/crossAppContracts"; import type { RoamFullContentNode } from "./convertRoamNodeToFullContent"; import type { DiscourseNode } from "./getDiscourseNodes"; import type { TreeNode, ViewType } from "roamjs-components/types"; +import type { nodeUidWithType } from "~/utils/publishNodesToGroups"; +import type { Json } from "@repo/database/dbTypes"; import { toMarkdown } from "./pageToMarkdown"; import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid"; import getPageViewType from "roamjs-components/queries/getPageViewType"; @@ -64,3 +66,53 @@ export const fullContentNodeToCrossApp = ( }, }; }; + +export const nodeUidsWithTypeToCrossApp = async ( + nodes: nodeUidWithType[], +): Promise => { + const typesByUid = Object.fromEntries(nodes.map((n) => [n.uid, n.type])); + const nodeRows = (await window.roamAlphaAPI.data.async.pull_many( + `[:block/uid :create/user :create/time :edit/time :page/edit-time :node/title]`, + nodes.map((n) => [":block/uid", n.uid]), + )) as Record[]; + const userEids = [ + ...new Set( + nodeRows.map( + (r) => (r[":create/user"] as Record)[":db/id"], + ), + ), + ]; + const userRows = await window.roamAlphaAPI.data.async.pull_many( + `[:db/id :user/uid]`, + // @ts-expect-error array of dbIds is valid + userEids, + ); + const userUidByEid = Object.fromEntries( + userRows.map((r) => [r[":db/id"] as number, r[":user/uid"] as string]), + ); + const results = nodeRows.map((row) => { + const uid = row[":block/uid"] as string; + const userUid = + userUidByEid[(row[":create/user"] as Record)[":db/id"]]; + + return { + localId: uid, + nodeType: typesByUid[uid], + authorId: userUid, + createdAt: new Date((row[":create/time"] as number) || Date.now()), + modifiedAt: new Date( + Math.max( + row[":edit/time"] as number, + row[":page/edit-time"] as number, + ) || Date.now(), + ), + content: { + direct: { + localId: uid, + value: row[":node/title"] as string, + }, + }, + }; + }); + return results; +}; From fa1310cc97c51014db348285a0853500fce42620 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Jul 2026 17:43:30 -0400 Subject: [PATCH 2/2] Respond to comments --- apps/roam/src/components/Export.tsx | 6 +++--- apps/roam/src/utils/publishNodesToGroups.ts | 8 ++++---- apps/roam/src/utils/roamToCrossAppConverters.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index 7fd8c14b1..6d1b5f071 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -88,7 +88,7 @@ import posthog from "posthog-js"; import { getMyGroups, type MyGroup } from "@repo/database/lib/groups"; import { publishNodeUidsWithTypeToGroups, - type nodeUidWithType, + type NodeUidWithType, } from "~/utils/publishNodesToGroups"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; import { isSyncEnabled } from "~/components/settings/utils/accessors"; @@ -248,7 +248,7 @@ const ExportDialog: ExportDialogComponent = ({ ? { uid: r.uid, type: node.type } : null; }) - .filter((n): n is nodeUidWithType => n !== null) + .filter((n): n is NodeUidWithType => n !== null) : [], [results, syncEnabled], ); @@ -847,7 +847,7 @@ const ExportDialog: ExportDialogComponent = ({ client, spaceId: context.spaceId, groupIds: selectedGroupIds, - publishNodes: publishableNodes, + nodeUids: publishableNodes, }); posthog.capture("Export Dialog: Publish", { groupCount: okGroupIds.length, diff --git a/apps/roam/src/utils/publishNodesToGroups.ts b/apps/roam/src/utils/publishNodesToGroups.ts index f41e15925..b873fc2be 100644 --- a/apps/roam/src/utils/publishNodesToGroups.ts +++ b/apps/roam/src/utils/publishNodesToGroups.ts @@ -3,7 +3,7 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; import { getAvailableGroupIds } from "@repo/database/lib/groups"; import { nodeUidsWithTypeToCrossApp } from "./roamToCrossAppConverters"; -export type nodeUidWithType = { +export type NodeUidWithType = { uid: string; type: string; }; @@ -134,13 +134,13 @@ export const publishNodeUidsWithTypeToGroups = async ({ client, spaceId, groupIds, - publishNodes, + nodeUids, }: { client: DGSupabaseClient; spaceId: number; groupIds: string[]; - publishNodes: nodeUidWithType[]; + nodeUids: NodeUidWithType[]; }): Promise => { - const nodes = await nodeUidsWithTypeToCrossApp(publishNodes); + const nodes = await nodeUidsWithTypeToCrossApp(nodeUids); return await publishNodesToGroups({ client, spaceId, groupIds, nodes }); }; diff --git a/apps/roam/src/utils/roamToCrossAppConverters.ts b/apps/roam/src/utils/roamToCrossAppConverters.ts index da96b22e8..bc7b9b77d 100644 --- a/apps/roam/src/utils/roamToCrossAppConverters.ts +++ b/apps/roam/src/utils/roamToCrossAppConverters.ts @@ -2,7 +2,7 @@ import type { CrossAppNode } from "@repo/database/crossAppContracts"; import type { RoamFullContentNode } from "./convertRoamNodeToFullContent"; import type { DiscourseNode } from "./getDiscourseNodes"; import type { TreeNode, ViewType } from "roamjs-components/types"; -import type { nodeUidWithType } from "~/utils/publishNodesToGroups"; +import type { NodeUidWithType } from "~/utils/publishNodesToGroups"; import type { Json } from "@repo/database/dbTypes"; import { toMarkdown } from "./pageToMarkdown"; import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid"; @@ -68,7 +68,7 @@ export const fullContentNodeToCrossApp = ( }; export const nodeUidsWithTypeToCrossApp = async ( - nodes: nodeUidWithType[], + nodes: NodeUidWithType[], ): Promise => { const typesByUid = Object.fromEntries(nodes.map((n) => [n.uid, n.type])); const nodeRows = (await window.roamAlphaAPI.data.async.pull_many(