From 56194aed08246af33bcb2709572a5d4a188129ed Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Tue, 21 Apr 2026 10:01:10 -0400 Subject: [PATCH 01/19] eng-1650 WIP --- .../src/components/GeneralSettings.tsx | 30 +++++++++++++++++++ apps/obsidian/src/types.ts | 1 + apps/obsidian/src/utils/supabaseContext.ts | 5 ++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/obsidian/src/components/GeneralSettings.tsx b/apps/obsidian/src/components/GeneralSettings.tsx index d89453575..ae1fcb005 100644 --- a/apps/obsidian/src/components/GeneralSettings.tsx +++ b/apps/obsidian/src/components/GeneralSettings.tsx @@ -152,6 +152,9 @@ const GeneralSettings = () => { const [canvasFolderPath, setCanvasFolderPath] = useState( plugin.settings.canvasFolderPath, ); + const [username, setUsername] = useState( + plugin.settings.username || "", + ); const [canvasAttachmentsFolderPath, setCanvasAttachmentsFolderPath] = useState(plugin.settings.canvasAttachmentsFolderPath); const [nodeTagHotkey, setNodeTagHotkey] = useState( @@ -203,6 +206,12 @@ const GeneralSettings = () => { [plugin], ); + const handleSetUsername = (newValue: string) => { + setUsername(newValue); + plugin.settings.username = newValue; + void plugin.saveSettings(); + }; + return (
@@ -307,6 +316,27 @@ const GeneralSettings = () => {
+
+
+
Username
+
+ A username that will be associated with your vault if you share + data. +
+
+
+ handleSetUsername(e.target.value)} + /> +
+
+ ); diff --git a/apps/obsidian/src/types.ts b/apps/obsidian/src/types.ts index 18c3e1731..bc3da7962 100644 --- a/apps/obsidian/src/types.ts +++ b/apps/obsidian/src/types.ts @@ -69,6 +69,7 @@ export type Settings = { syncModeEnabled?: boolean; /** Maps spaceUri (e.g. "obsidian:abc123") to human-readable name (e.g. "My Vault") */ spaceNames?: Record; + username?: string; }; export type BulkImportCandidate = { diff --git a/apps/obsidian/src/utils/supabaseContext.ts b/apps/obsidian/src/utils/supabaseContext.ts index b164e8a66..e90162e03 100644 --- a/apps/obsidian/src/utils/supabaseContext.ts +++ b/apps/obsidian/src/utils/supabaseContext.ts @@ -77,6 +77,7 @@ export const getSupabaseContext = async ( if (contextCache === null) { try { const vaultName = plugin.app.vault.getName() || "obsidian-vault"; + const username = plugin.settings.username || vaultName; const spacePassword = await getOrCreateSpacePassword(plugin); const accountLocalId = await getOrCreateAccountLocalId(plugin, vaultName); @@ -100,8 +101,8 @@ export const getSupabaseContext = async ( const userId = await fetchOrCreatePlatformAccount({ platform: "Obsidian", accountLocalId, - name: vaultName, - email: accountLocalId, + name: username, + email: undefined, spaceId, password: spacePassword, }); From 21be3b3965bc16fad70520e66043ebb646dc4298 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Tue, 21 Apr 2026 10:17:53 -0400 Subject: [PATCH 02/19] allow viewing PlatformAccounts of shared spaces --- packages/database/src/dbTypes.ts | 5 ++++- ...lect_platform_account_of_partial_space.sql | 22 +++++++++++++++++++ .../database/supabase/schemas/account.sql | 6 ++--- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql diff --git a/packages/database/src/dbTypes.ts b/packages/database/src/dbTypes.ts index b968544ba..734d86cf3 100644 --- a/packages/database/src/dbTypes.ts +++ b/packages/database/src/dbTypes.ts @@ -1291,7 +1291,10 @@ export type Database = { } } account_in_shared_space: { - Args: { p_account_id: number } + Args: { + access_level?: Database["public"]["Enums"]["SpaceAccessPermissions"] + p_account_id: number + } Returns: boolean } author_of_concept: { diff --git a/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql b/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql new file mode 100644 index 000000000..46849b3ff --- /dev/null +++ b/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql @@ -0,0 +1,22 @@ +CREATE OR REPLACE FUNCTION public.account_in_shared_space(p_account_id BIGINT, access_level public."SpaceAccessPermissions" = 'reader') RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public."LocalAccess" AS la + JOIN public."SpaceAccess" AS sa USING (space_id) + JOIN public.my_user_accounts() ON (sa.account_uid = my_user_accounts) + WHERE la.account_id = p_account_id + AND sa.permissions >= access_level + ); +$$; + +DROP POLICY IF EXISTS platform_account_select_policy ON public."PlatformAccount"; +CREATE POLICY platform_account_select_policy ON public."PlatformAccount" FOR SELECT USING (dg_account = (SELECT auth.uid() LIMIT 1) OR public.account_in_shared_space(id, 'partial')); + +DROP POLICY IF EXISTS agent_identifier_select_policy ON public."AgentIdentifier"; + +DROP function public.account_in_shared_space(p_account_id BIGINT); + +CREATE POLICY agent_identifier_select_policy ON public."AgentIdentifier" FOR SELECT USING (public.account_in_shared_space(account_id)); diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 527f877f7..01adb4d5a 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -286,7 +286,7 @@ $$; COMMENT ON FUNCTION public.in_space IS 'security utility: does current user have access to this space?'; -CREATE OR REPLACE FUNCTION public.account_in_shared_space(p_account_id BIGINT) RETURNS boolean +CREATE OR REPLACE FUNCTION public.account_in_shared_space(p_account_id BIGINT, access_level public."SpaceAccessPermissions" = 'reader') RETURNS boolean STABLE SECURITY DEFINER SET search_path = '' LANGUAGE sql AS $$ @@ -296,7 +296,7 @@ LANGUAGE sql AS $$ JOIN public."SpaceAccess" AS sa USING (space_id) JOIN public.my_user_accounts() ON (sa.account_uid = my_user_accounts) WHERE la.account_id = p_account_id - AND sa.permissions >= 'reader' + AND sa.permissions >= access_level ); $$; @@ -459,7 +459,7 @@ WHERE id IN ( DROP POLICY IF EXISTS platform_account_policy ON public."PlatformAccount"; DROP POLICY IF EXISTS platform_account_select_policy ON public."PlatformAccount"; -CREATE POLICY platform_account_select_policy ON public."PlatformAccount" FOR SELECT USING (dg_account = (SELECT auth.uid() LIMIT 1) OR public.account_in_shared_space(id)); +CREATE POLICY platform_account_select_policy ON public."PlatformAccount" FOR SELECT USING (dg_account = (SELECT auth.uid() LIMIT 1) OR public.account_in_shared_space(id, 'partial')); DROP POLICY IF EXISTS platform_account_delete_policy ON public."PlatformAccount"; CREATE POLICY platform_account_delete_policy ON public."PlatformAccount" FOR DELETE USING (dg_account = (SELECT auth.uid() LIMIT 1) OR (dg_account IS null AND public.unowned_account_in_shared_space(id))); From 140c24c80513390c0108899ed0ece36b4eb5fb9c Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Wed, 22 Apr 2026 09:33:24 -0400 Subject: [PATCH 03/19] Also modify my_accounts --- ...lect_platform_account_of_partial_space.sql | 19 +++++++++++++++++++ .../database/supabase/schemas/account.sql | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql b/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql index 46849b3ff..0436bc3b0 100644 --- a/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql +++ b/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql @@ -20,3 +20,22 @@ DROP POLICY IF EXISTS agent_identifier_select_policy ON public."AgentIdentifier" DROP function public.account_in_shared_space(p_account_id BIGINT); CREATE POLICY agent_identifier_select_policy ON public."AgentIdentifier" FOR SELECT USING (public.account_in_shared_space(account_id)); + +CREATE OR REPLACE VIEW public.my_accounts AS +SELECT + id, + name, + platform, + account_local_id, + write_permission, + active, + agent_type, + metadata, + dg_account +FROM public."PlatformAccount" +WHERE id IN ( + SELECT "LocalAccess".account_id FROM public."LocalAccess" + JOIN public."SpaceAccess" USING (space_id) + JOIN public.my_user_accounts() ON (account_uid = my_user_accounts) + WHERE permissions >= 'partial' +); diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 01adb4d5a..42bcd8c5c 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -453,7 +453,7 @@ WHERE id IN ( SELECT "LocalAccess".account_id FROM public."LocalAccess" JOIN public."SpaceAccess" USING (space_id) JOIN public.my_user_accounts() ON (account_uid = my_user_accounts) - WHERE permissions >= 'reader' + WHERE permissions >= 'partial' ); DROP POLICY IF EXISTS platform_account_policy ON public."PlatformAccount"; From 5c3db774b4da084a8f17bd7eeacdcea8f4c3ef77 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Wed, 22 Apr 2026 09:33:48 -0400 Subject: [PATCH 04/19] add authorName to frontmatter --- .../src/components/ImportNodesModal.tsx | 1 + apps/obsidian/src/index.ts | 7 ++-- apps/obsidian/src/types.ts | 1 + apps/obsidian/src/utils/importNodes.ts | 39 ++++++++++--------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/apps/obsidian/src/components/ImportNodesModal.tsx b/apps/obsidian/src/components/ImportNodesModal.tsx index 0584e1524..6b6baba45 100644 --- a/apps/obsidian/src/components/ImportNodesModal.tsx +++ b/apps/obsidian/src/components/ImportNodesModal.tsx @@ -120,6 +120,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { createdAt: node.createdAt, modifiedAt: node.modifiedAt, filePath: node.filePath, + authorName: node.authorName, }); } diff --git a/apps/obsidian/src/index.ts b/apps/obsidian/src/index.ts index bb638fbae..b1b71216f 100644 --- a/apps/obsidian/src/index.ts +++ b/apps/obsidian/src/index.ts @@ -330,12 +330,13 @@ export default class DiscourseGraphPlugin extends Plugin { if (!this.settings.showIdsInFrontmatter) { keysToHide.push( ...[ - "nodeTypeId", + "authorName", + "importedAssets", "importedFromRid", + "lastModified", "nodeInstanceId", + "nodeTypeId", "publishedToGroups", - "lastModified", - "importedAssets", ], ); keysToHide.push(...this.settings.relationTypes.map((rt) => rt.id)); diff --git a/apps/obsidian/src/types.ts b/apps/obsidian/src/types.ts index bc3da7962..29899a668 100644 --- a/apps/obsidian/src/types.ts +++ b/apps/obsidian/src/types.ts @@ -97,6 +97,7 @@ export type ImportableNode = { createdAt?: number; modifiedAt?: number; filePath?: string; + authorName?: string; }; export type GroupWithNodes = { diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 2a8841b71..142a309da 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -36,6 +36,16 @@ export const getAvailableGroupIds = async ( return (data || []).map((g) => g.group_id); }; +type publishedNode = { + source_local_id: string; + space_id: number; + text: string; + createdAt: number; + modifiedAt: number; + filePath: string | undefined; + authorName: string | undefined; +}; + export const getPublishedNodesForGroups = async ({ client, groupIds, @@ -44,16 +54,7 @@ export const getPublishedNodesForGroups = async ({ client: DGSupabaseClient; groupIds: string[]; currentSpaceId: number; -}): Promise< - Array<{ - source_local_id: string; - space_id: number; - text: string; - createdAt: number; - modifiedAt: number; - filePath: string | undefined; - }> -> => { +}): Promise> => { if (groupIds.length === 0) { return []; } @@ -63,7 +64,7 @@ export const getPublishedNodesForGroups = async ({ const { data, error } = await client .from("my_contents") .select( - "source_local_id, space_id, text, created, last_modified, variant, metadata", + "source_local_id, space_id, text, created, last_modified, variant, metadata, author:my_accounts!author_id(name)", ) .neq("space_id", currentSpaceId); @@ -83,6 +84,7 @@ export const getPublishedNodesForGroups = async ({ created: string | null; last_modified: string | null; variant: string | null; + author: { name: string } | null; metadata: Json; }; @@ -95,14 +97,7 @@ export const getPublishedNodesForGroups = async ({ groups.get(k)!.push(row); } - const nodes: Array<{ - source_local_id: string; - space_id: number; - text: string; - createdAt: number; - modifiedAt: number; - filePath: string | undefined; - }> = []; + const nodes: Array = []; for (const rows of groups.values()) { const withDate = rows.filter( @@ -133,6 +128,7 @@ export const getPublishedNodesForGroups = async ({ createdAt, modifiedAt, filePath, + authorName: latest.author ? latest.author.name : undefined, }); } @@ -975,6 +971,7 @@ type ParsedFrontmatter = { nodeTypeId?: string; nodeInstanceId?: string; publishedToGroups?: string[]; + authorName?: string; [key: string]: unknown; }; @@ -1105,6 +1102,7 @@ const processFileContent = async ({ filePath, importedCreatedAt, importedModifiedAt, + authorName, }: { plugin: DiscourseGraphPlugin; client: DGSupabaseClient; @@ -1115,6 +1113,7 @@ const processFileContent = async ({ filePath: string; importedCreatedAt?: number; importedModifiedAt?: number; + authorName?: string; }): Promise< { file: TFile; error?: never } | { file?: never; error: string } > => { @@ -1173,6 +1172,7 @@ const processFileContent = async ({ "note", ); record.lastModified = importedModifiedAt; + if (authorName) record.authorName = authorName; }, stat, ); @@ -1322,6 +1322,7 @@ export const importSelectedNodes = async ({ filePath: finalFilePath, importedCreatedAt: createdAt, importedModifiedAt: modifiedAt, + authorName: node.authorName, }); if (result.error) { From 8a5c9c92f1d6e834307cec2b0a57228dff9fdeb5 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Wed, 22 Apr 2026 09:46:10 -0400 Subject: [PATCH 05/19] Add authorName to DiscourseContext --- .../src/components/DiscourseContextView.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/obsidian/src/components/DiscourseContextView.tsx b/apps/obsidian/src/components/DiscourseContextView.tsx index e52eaad82..bfa647c66 100644 --- a/apps/obsidian/src/components/DiscourseContextView.tsx +++ b/apps/obsidian/src/components/DiscourseContextView.tsx @@ -161,6 +161,13 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => { !isImported && !!frontmatter.nodeTypeId; + const formattedVaultName = isImported + ? getAndFormatImportSource( + frontmatter.importedFromRid as string, + plugin.settings.spaceNames, + ) + : ""; + return ( <>
@@ -232,7 +239,7 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => { View only
)} @@ -244,6 +251,13 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => { )} + {isImported && + frontmatter.authorName && + `Vault ${frontmatter.authorName}` !== formattedVaultName && ( +
+
Author: {frontmatter.authorName}
+
+ )} {isImported && sourceDates && (
Created in source: {sourceDates.createdAt}
From 2096ab08ff0d911e6ebead073602dcf341837cca Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Wed, 22 Apr 2026 10:16:29 -0400 Subject: [PATCH 06/19] add authors to import modal. --- .../src/components/ImportNodesModal.tsx | 21 +++++++++++++++++-- apps/obsidian/src/types.ts | 1 + 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/obsidian/src/components/ImportNodesModal.tsx b/apps/obsidian/src/components/ImportNodesModal.tsx index 6b6baba45..a4f59748c 100644 --- a/apps/obsidian/src/components/ImportNodesModal.tsx +++ b/apps/obsidian/src/components/ImportNodesModal.tsx @@ -106,15 +106,18 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { groupName: spaceNames.get(node.space_id) ?? `Space ${node.space_id}`, nodes: [], + authors: new Set(), }); } const group = grouped.get(groupId)!; + const spaceName = + spaceNames.get(node.space_id) ?? `Space ${node.space_id}`; group.nodes.push({ nodeInstanceId: node.source_local_id, title: node.text, spaceId: node.space_id, - spaceName: spaceNames.get(node.space_id) ?? `Space ${node.space_id}`, + spaceName, groupId, selected: false, createdAt: node.createdAt, @@ -122,6 +125,8 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { filePath: node.filePath, authorName: node.authorName, }); + if (node.authorName && node.authorName !== spaceName) + group.authors.add(node.authorName); } setGroupsWithNodes(Array.from(grouped.values())); @@ -259,6 +264,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { number, { spaceName: string; + authors: Set; nodes: Array<{ node: ImportableNode; groupId: string; @@ -272,6 +278,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { if (!nodesBySpace.has(node.spaceId)) { nodesBySpace.set(node.spaceId, { spaceName: node.spaceName, + authors: group.authors, nodes: [], }); } @@ -323,7 +330,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => {
{Array.from(nodesBySpace.entries()).map( - ([spaceId, { spaceName, nodes }]) => { + ([spaceId, { spaceName, nodes, authors }]) => { return (
@@ -331,6 +338,9 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { {spaceName} + {authors.size === 1 && ( +  ({[...authors][0]}) + )} ({nodes.length} node{nodes.length !== 1 ? "s" : ""}) @@ -350,6 +360,13 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => {
{node.title} + {node.authorName && + authors.size > 1 && + node.authorName !== spaceName && ( + +  ({node.authorName}) + + )}
diff --git a/apps/obsidian/src/types.ts b/apps/obsidian/src/types.ts index 29899a668..a04078d66 100644 --- a/apps/obsidian/src/types.ts +++ b/apps/obsidian/src/types.ts @@ -104,6 +104,7 @@ export type GroupWithNodes = { groupId: string; groupName?: string; nodes: ImportableNode[]; + authors: Set; }; export const VIEW_TYPE_DISCOURSE_CONTEXT = "discourse-context-view"; From 2689844cc34f5f2d9e42812c01765e1f8d7342ce Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Wed, 22 Apr 2026 11:20:36 -0400 Subject: [PATCH 07/19] Devin: type name, known vault name, update username on change --- .../src/components/DiscourseContextView.tsx | 2 +- .../obsidian/src/components/GeneralSettings.tsx | 8 +++++--- apps/obsidian/src/utils/importNodes.ts | 6 +++--- apps/obsidian/src/utils/supabaseContext.ts | 17 +++++++++++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/obsidian/src/components/DiscourseContextView.tsx b/apps/obsidian/src/components/DiscourseContextView.tsx index bfa647c66..0ba837264 100644 --- a/apps/obsidian/src/components/DiscourseContextView.tsx +++ b/apps/obsidian/src/components/DiscourseContextView.tsx @@ -253,7 +253,7 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => { {isImported && frontmatter.authorName && - `Vault ${frontmatter.authorName}` !== formattedVaultName && ( + frontmatter.authorName !== formattedVaultName && (
Author: {frontmatter.authorName}
diff --git a/apps/obsidian/src/components/GeneralSettings.tsx b/apps/obsidian/src/components/GeneralSettings.tsx index ae1fcb005..132afe671 100644 --- a/apps/obsidian/src/components/GeneralSettings.tsx +++ b/apps/obsidian/src/components/GeneralSettings.tsx @@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { usePlugin } from "./PluginContext"; import { setIcon } from "obsidian"; import SuggestInput from "./SuggestInput"; +import { updateUsername } from "~/utils/supabaseContext"; import { SLACK_LOGO, WHITE_LOGO_SVG } from "~/icons"; const DOCS_URL = "https://discoursegraphs.com/docs/obsidian"; @@ -206,10 +207,11 @@ const GeneralSettings = () => { [plugin], ); - const handleSetUsername = (newValue: string) => { + const handleSetUsername = async (newValue: string) => { setUsername(newValue); plugin.settings.username = newValue; - void plugin.saveSettings(); + await plugin.saveSettings(); + await updateUsername(plugin, newValue); }; return ( @@ -332,7 +334,7 @@ const GeneralSettings = () => { handleSetUsername(e.target.value)} + onChange={(e) => void handleSetUsername(e.target.value)} />
diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 142a309da..063ea4cdb 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -36,7 +36,7 @@ export const getAvailableGroupIds = async ( return (data || []).map((g) => g.group_id); }; -type publishedNode = { +type PublishedNode = { source_local_id: string; space_id: number; text: string; @@ -54,7 +54,7 @@ export const getPublishedNodesForGroups = async ({ client: DGSupabaseClient; groupIds: string[]; currentSpaceId: number; -}): Promise> => { +}): Promise> => { if (groupIds.length === 0) { return []; } @@ -97,7 +97,7 @@ export const getPublishedNodesForGroups = async ({ groups.get(k)!.push(row); } - const nodes: Array = []; + const nodes: Array = []; for (const rows of groups.values()) { const withDate = rows.filter( diff --git a/apps/obsidian/src/utils/supabaseContext.ts b/apps/obsidian/src/utils/supabaseContext.ts index e90162e03..d52ce351b 100644 --- a/apps/obsidian/src/utils/supabaseContext.ts +++ b/apps/obsidian/src/utils/supabaseContext.ts @@ -122,6 +122,23 @@ export const getSupabaseContext = async ( return contextCache; }; +export const updateUsername = async ( + plugin: DiscourseGraphPlugin, + username: string, +): Promise => { + const context = await getSupabaseContext(plugin); + if (!context) return; + const client = await getLoggedInClient(plugin); + if (!client) return; + const result = await client + .from("PlatformAccount") + .update({ name: username }) + .eq("id", context.userId); + if (result.error) { + console.error(result.error); + } +}; + let loggedInClient: DGSupabaseClient | null = null; export const getLoggedInClient = async ( From 943295333cefa971dd2dc6818e7822909b4716f7 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 10:09:21 -0400 Subject: [PATCH 08/19] Put the name map in settings. Use authorId rather than authorName --- .../src/components/DiscourseContextView.tsx | 18 +++++---- .../src/components/ImportNodesModal.tsx | 32 ++++++++-------- apps/obsidian/src/index.ts | 2 +- apps/obsidian/src/types.ts | 9 ++++- apps/obsidian/src/utils/importNodes.ts | 38 ++++++++++++++----- apps/obsidian/src/utils/typeUtils.ts | 8 ++++ 6 files changed, 71 insertions(+), 36 deletions(-) diff --git a/apps/obsidian/src/components/DiscourseContextView.tsx b/apps/obsidian/src/components/DiscourseContextView.tsx index 0ba837264..96a1592ec 100644 --- a/apps/obsidian/src/components/DiscourseContextView.tsx +++ b/apps/obsidian/src/components/DiscourseContextView.tsx @@ -13,7 +13,11 @@ import { getDiscourseNodeFormatExpression } from "~/utils/getDiscourseNodeFormat import { RelationshipSection } from "~/components/RelationshipSection"; import { VIEW_TYPE_DISCOURSE_CONTEXT } from "~/types"; import { PluginProvider, usePlugin } from "~/components/PluginContext"; -import { getNodeTypeById, getAndFormatImportSource } from "~/utils/typeUtils"; +import { + getNodeTypeById, + getAndFormatImportSource, + getUserNameById, +} from "~/utils/typeUtils"; import { refreshImportedFile } from "~/utils/importNodes"; import { publishNode } from "~/utils/publishNode"; import { createBaseForNodeType } from "~/utils/baseForNodeType"; @@ -251,13 +255,11 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => {
)} - {isImported && - frontmatter.authorName && - frontmatter.authorName !== formattedVaultName && ( -
-
Author: {frontmatter.authorName}
-
- )} + {isImported && frontmatter.authorId && ( +
+
Author: {getUserNameById(plugin, frontmatter.authorId)}
+
+ )} {isImported && sourceDates && (
Created in source: {sourceDates.createdAt}
diff --git a/apps/obsidian/src/components/ImportNodesModal.tsx b/apps/obsidian/src/components/ImportNodesModal.tsx index a4f59748c..b7196e248 100644 --- a/apps/obsidian/src/components/ImportNodesModal.tsx +++ b/apps/obsidian/src/components/ImportNodesModal.tsx @@ -3,6 +3,7 @@ import { createRoot, Root } from "react-dom/client"; import { StrictMode, useState, useEffect, useCallback } from "react"; import type DiscourseGraphPlugin from "../index"; import type { ImportableNode, GroupWithNodes } from "~/types"; +import { getUserNameById } from "~/utils/typeUtils"; import { getAvailableGroupIds, getPublishedNodesForGroups, @@ -106,7 +107,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { groupName: spaceNames.get(node.space_id) ?? `Space ${node.space_id}`, nodes: [], - authors: new Set(), + authorIds: new Set(), }); } @@ -123,10 +124,9 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { createdAt: node.createdAt, modifiedAt: node.modifiedAt, filePath: node.filePath, - authorName: node.authorName, + authorId: node.authorId, }); - if (node.authorName && node.authorName !== spaceName) - group.authors.add(node.authorName); + if (node.authorId) group.authorIds.add(node.authorId); } setGroupsWithNodes(Array.from(grouped.values())); @@ -264,7 +264,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { number, { spaceName: string; - authors: Set; + authorIds: Set; nodes: Array<{ node: ImportableNode; groupId: string; @@ -278,7 +278,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { if (!nodesBySpace.has(node.spaceId)) { nodesBySpace.set(node.spaceId, { spaceName: node.spaceName, - authors: group.authors, + authorIds: group.authorIds, nodes: [], }); } @@ -330,7 +330,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => {
{Array.from(nodesBySpace.entries()).map( - ([spaceId, { spaceName, nodes, authors }]) => { + ([spaceId, { spaceName, nodes, authorIds }]) => { return (
@@ -338,8 +338,10 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { {spaceName} - {authors.size === 1 && ( -  ({[...authors][0]}) + {authorIds.size === 1 && ( + +  ({getUserNameById(plugin, [...authorIds][0]!)}) + )} ({nodes.length} node{nodes.length !== 1 ? "s" : ""}) @@ -360,13 +362,11 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => {
{node.title} - {node.authorName && - authors.size > 1 && - node.authorName !== spaceName && ( - -  ({node.authorName}) - - )} + {node.authorId && authorIds.size > 1 && ( + +  ({getUserNameById(plugin, node.authorId)}) + + )}
diff --git a/apps/obsidian/src/index.ts b/apps/obsidian/src/index.ts index b1b71216f..d1ba1c2ed 100644 --- a/apps/obsidian/src/index.ts +++ b/apps/obsidian/src/index.ts @@ -330,7 +330,7 @@ export default class DiscourseGraphPlugin extends Plugin { if (!this.settings.showIdsInFrontmatter) { keysToHide.push( ...[ - "authorName", + "authorId", "importedAssets", "importedFromRid", "lastModified", diff --git a/apps/obsidian/src/types.ts b/apps/obsidian/src/types.ts index a04078d66..1bc382415 100644 --- a/apps/obsidian/src/types.ts +++ b/apps/obsidian/src/types.ts @@ -15,6 +15,7 @@ export type DiscourseNode = { created: number; modified: number; importedFromRid?: string; + authorId?: number; }; export type ImportStatus = "provisional" | "accepted"; @@ -28,6 +29,7 @@ export type DiscourseRelationType = { modified: number; importedFromRid?: string; status?: ImportStatus; + authorId?: number; }; export type DiscourseRelation = { @@ -39,6 +41,7 @@ export type DiscourseRelation = { modified: number; importedFromRid?: string; status?: ImportStatus; + authorId?: number; }; export type RelationInstance = { @@ -53,6 +56,7 @@ export type RelationInstance = { importedFromRid?: string; /** Tracks acceptance of imported relations. false = imported, not yet accepted. true or undefined = accepted/local. */ tentative?: boolean; + authorId?: number; }; export type Settings = { @@ -70,6 +74,7 @@ export type Settings = { /** Maps spaceUri (e.g. "obsidian:abc123") to human-readable name (e.g. "My Vault") */ spaceNames?: Record; username?: string; + userNames?: Record; }; export type BulkImportCandidate = { @@ -97,14 +102,14 @@ export type ImportableNode = { createdAt?: number; modifiedAt?: number; filePath?: string; - authorName?: string; + authorId?: number; }; export type GroupWithNodes = { groupId: string; groupName?: string; nodes: ImportableNode[]; - authors: Set; + authorIds: Set; }; export const VIEW_TYPE_DISCOURSE_CONTEXT = "discourse-context-view"; diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 063ea4cdb..fb631b389 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -43,7 +43,7 @@ type PublishedNode = { createdAt: number; modifiedAt: number; filePath: string | undefined; - authorName: string | undefined; + authorId: number | undefined; }; export const getPublishedNodesForGroups = async ({ @@ -64,7 +64,7 @@ export const getPublishedNodesForGroups = async ({ const { data, error } = await client .from("my_contents") .select( - "source_local_id, space_id, text, created, last_modified, variant, metadata, author:my_accounts!author_id(name)", + "source_local_id, space_id, text, created, last_modified, variant, metadata, author_id", ) .neq("space_id", currentSpaceId); @@ -84,7 +84,7 @@ export const getPublishedNodesForGroups = async ({ created: string | null; last_modified: string | null; variant: string | null; - author: { name: string } | null; + author_id: number | null; metadata: Json; }; @@ -128,7 +128,7 @@ export const getPublishedNodesForGroups = async ({ createdAt, modifiedAt, filePath, - authorName: latest.author ? latest.author.name : undefined, + authorId: latest.author_id || undefined, }); } @@ -229,6 +229,25 @@ export const getSpaceUris = async ( return spaceMap; }; +const fetchUserNames = async ( + plugin: DiscourseGraphPlugin, + client: DGSupabaseClient, +) => { + const result = await client + .from("PlatformAccount") + .select("id, name") + .eq("agent_type", "person"); + if (result.error || !result.data) { + console.error(result.error); + return; + } + const nameById: Record = Object.fromEntries( + result.data.map(({ id, name }) => [id, name]), + ); + plugin.settings.userNames = nameById; + await plugin.saveSettings(); +}; + export const fetchNodeContent = async ({ client, spaceId, @@ -971,7 +990,7 @@ type ParsedFrontmatter = { nodeTypeId?: string; nodeInstanceId?: string; publishedToGroups?: string[]; - authorName?: string; + authorId?: number; [key: string]: unknown; }; @@ -1102,7 +1121,7 @@ const processFileContent = async ({ filePath, importedCreatedAt, importedModifiedAt, - authorName, + authorId, }: { plugin: DiscourseGraphPlugin; client: DGSupabaseClient; @@ -1113,7 +1132,7 @@ const processFileContent = async ({ filePath: string; importedCreatedAt?: number; importedModifiedAt?: number; - authorName?: string; + authorId?: number; }): Promise< { file: TFile; error?: never } | { file?: never; error: string } > => { @@ -1172,7 +1191,7 @@ const processFileContent = async ({ "note", ); record.lastModified = importedModifiedAt; - if (authorName) record.authorName = authorName; + if (authorId) record.authorId = authorId; }, stat, ); @@ -1222,6 +1241,7 @@ export const importSelectedNodes = async ({ nodesBySpace.get(node.spaceId)!.push(node); } + await fetchUserNames(plugin, client); const spaceUris = await getSpaceUris(client, [...nodesBySpace.keys()]); // Process each space @@ -1322,7 +1342,7 @@ export const importSelectedNodes = async ({ filePath: finalFilePath, importedCreatedAt: createdAt, importedModifiedAt: modifiedAt, - authorName: node.authorName, + authorId: node.authorId, }); if (result.error) { diff --git a/apps/obsidian/src/utils/typeUtils.ts b/apps/obsidian/src/utils/typeUtils.ts index 28fc9e53c..eb373579c 100644 --- a/apps/obsidian/src/utils/typeUtils.ts +++ b/apps/obsidian/src/utils/typeUtils.ts @@ -1,6 +1,7 @@ import type DiscourseGraphPlugin from "~/index"; import { DiscourseNode, DiscourseRelationType, ImportStatus } from "~/types"; import { ridToSpaceUriAndLocalId } from "./rid"; +import { DGSupabaseClient } from "@repo/database/lib/client"; export const getNodeTypeById = ( plugin: DiscourseGraphPlugin, @@ -88,3 +89,10 @@ export const getAndFormatImportSource = ( const importInfo = getImportInfo(importedFromRid); return formatImportSource(importInfo.spaceUri || "", spaceNames); }; + +export const getUserNameById = ( + plugin: DiscourseGraphPlugin, + id: number, +): string => { + return (plugin.settings.userNames || {})[id] || `user ${id}`; +}; From 9bd02458bed2de45e6484e2305a788570912d686 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 11:23:36 -0400 Subject: [PATCH 09/19] Add AuthorId to schemas. Replace author by authorId in relations. Absence means local author. --- apps/obsidian/src/types.ts | 1 - apps/obsidian/src/utils/conceptConversion.ts | 57 +++++++------------ apps/obsidian/src/utils/importNodes.ts | 3 +- apps/obsidian/src/utils/importRelations.ts | 12 +++- apps/obsidian/src/utils/relationsStore.ts | 8 +-- .../src/utils/syncDgNodesToSupabase.ts | 21 +------ 6 files changed, 37 insertions(+), 65 deletions(-) diff --git a/apps/obsidian/src/types.ts b/apps/obsidian/src/types.ts index 1bc382415..2045635b2 100644 --- a/apps/obsidian/src/types.ts +++ b/apps/obsidian/src/types.ts @@ -50,7 +50,6 @@ export type RelationInstance = { source: string; destination: string; created: number; - author: string; lastModified?: number; publishedToGroupId?: string[]; importedFromRid?: string; diff --git a/apps/obsidian/src/utils/conceptConversion.ts b/apps/obsidian/src/utils/conceptConversion.ts index f5eb66a34..5843484e1 100644 --- a/apps/obsidian/src/utils/conceptConversion.ts +++ b/apps/obsidian/src/utils/conceptConversion.ts @@ -16,30 +16,25 @@ import type { Json } from "@repo/database/dbTypes"; */ const getNodeExtraData = ( file: TFile, - accountLocalId: string, + author_id: number, ): { /* eslint-disable @typescript-eslint/naming-convention */ - author_local_id: string; + author_id: number; created: string; last_modified: string; } => { return { - author_local_id: accountLocalId, + author_id, created: new Date(file.stat.ctime).toISOString(), last_modified: new Date(file.stat.mtime).toISOString(), }; /* eslint-enable @typescript-eslint/naming-convention */ }; -export const discourseNodeSchemaToLocalConcept = ({ - context, - node, - accountLocalId, -}: { - context: SupabaseContext; - node: DiscourseNode; - accountLocalId: string; -}): LocalConceptDataInput => { +export const discourseNodeSchemaToLocalConcept = ( + context: SupabaseContext, + node: DiscourseNode, +): LocalConceptDataInput => { const { description, template, @@ -62,7 +57,7 @@ export const discourseNodeSchemaToLocalConcept = ({ name, source_local_id: id, is_schema: true, - author_local_id: accountLocalId, + author_id: context.userId, created: new Date(created).toISOString(), last_modified: new Date(modified).toISOString(), description: description, @@ -73,15 +68,10 @@ export const discourseNodeSchemaToLocalConcept = ({ const STANDARD_ROLES = ["source", "destination"]; -export const discourseRelationTypeToLocalConcept = ({ - context, - relationType, - accountLocalId, -}: { - context: SupabaseContext; - relationType: DiscourseRelationType; - accountLocalId: string; -}): LocalConceptDataInput => { +export const discourseRelationTypeToLocalConcept = ( + context: SupabaseContext, + relationType: DiscourseRelationType, +): LocalConceptDataInput => { const { id, label, @@ -109,7 +99,7 @@ export const discourseRelationTypeToLocalConcept = ({ name: label, source_local_id: id, is_schema: true, - author_local_id: accountLocalId, + author_id: context.userId, created: new Date(created).toISOString(), last_modified: new Date(modified).toISOString(), literal_content, @@ -120,13 +110,11 @@ export const discourseRelationTypeToLocalConcept = ({ export const discourseRelationTripleSchemaToLocalConcept = ({ context, relation, - accountLocalId, nodeTypesById, relationTypesById, }: { context: SupabaseContext; relation: DiscourseRelation; - accountLocalId: string; nodeTypesById: Record; relationTypesById: Record; }): LocalConceptDataInput | null => { @@ -158,7 +146,7 @@ export const discourseRelationTripleSchemaToLocalConcept = ({ name: `${sourceName} -${label}-> ${destinationName}`, source_local_id: id, is_schema: true, - author_local_id: accountLocalId, + author_id: context.userId, created: new Date(created).toISOString(), last_modified: new Date(modified).toISOString(), literal_content, @@ -174,16 +162,11 @@ export const discourseRelationTripleSchemaToLocalConcept = ({ /** * Convert discourse node instance (file) to LocalConceptDataInput */ -export const discourseNodeInstanceToLocalConcept = ({ - context, - nodeData, - accountLocalId, -}: { - context: SupabaseContext; - nodeData: ObsidianDiscourseNodeData; - accountLocalId: string; -}): LocalConceptDataInput => { - const extraData = getNodeExtraData(nodeData.file, accountLocalId); +export const discourseNodeInstanceToLocalConcept = ( + context: SupabaseContext, + nodeData: ObsidianDiscourseNodeData, +): LocalConceptDataInput => { + const extraData = getNodeExtraData(nodeData.file, context.userId); const { nodeInstanceId, nodeTypeId, importedFromRid, ...otherData } = nodeData.frontmatter; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -240,7 +223,7 @@ export const relationInstanceToLocalConcept = ({ space_id: context.spaceId, name: `[[${sourceNode.file.basename}]] -${relationType.label}-> [[${destinationNode.file.basename}]]`, source_local_id: relationInstanceData.id, - author_local_id: relationInstanceData.author, + author_id: relationInstanceData.authorId, schema_represented_by_local_id: type, is_schema: false, created: new Date(created).toISOString(), diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index fb631b389..793b31152 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -1053,7 +1053,7 @@ export const mapNodeTypeIdToLocal = async ({ // Find the schema in the source space with this nodeTypeId (my_concepts applies RLS) const { data: schemaData } = await client .from("my_concepts") - .select("name, literal_content") + .select("name, literal_content, author_id") .eq("space_id", sourceSpaceId) .eq("is_schema", true) .eq("source_local_id", sourceNodeTypeId) @@ -1104,6 +1104,7 @@ export const mapNodeTypeIdToLocal = async ({ keyImage: parsed.keyImage, created: now, modified: now, + authorId: schemaData.author_id || undefined, importedFromRid, }; plugin.settings.nodeTypes = [...plugin.settings.nodeTypes, newNodeType]; diff --git a/apps/obsidian/src/utils/importRelations.ts b/apps/obsidian/src/utils/importRelations.ts index 0884defd0..743465449 100644 --- a/apps/obsidian/src/utils/importRelations.ts +++ b/apps/obsidian/src/utils/importRelations.ts @@ -28,6 +28,7 @@ export type RemoteRelationInstance = { created: string | null; last_modified: string | null; concepts_of_relation: ConceptInRelation[]; + author_id: number | null; }; /** @@ -49,7 +50,7 @@ const mapRelationTypeToLocal = async ({ }): Promise => { const { data: schemaData } = await client .from("my_concepts") - .select("name, literal_content") + .select("name, literal_content, author_id") .eq("space_id", sourceSpaceId) .eq("is_schema", true) .eq("source_local_id", sourceRelationTypeId) @@ -99,6 +100,7 @@ const mapRelationTypeToLocal = async ({ modified: now, importedFromRid, status: "provisional", + authorId: schemaData.author_id || undefined, }; plugin.settings.relationTypes = [ ...(plugin.settings.relationTypes ?? []), @@ -121,6 +123,7 @@ const findOrCreateTriple = async ({ importedCreatedAt, importedModifiedAt, importedFromRid, + authorId, }: { plugin: DiscourseGraphPlugin; sourceNodeTypeId: string; @@ -129,6 +132,7 @@ const findOrCreateTriple = async ({ importedCreatedAt?: number; importedModifiedAt?: number; importedFromRid?: string; + authorId?: number; }): Promise => { const existing = plugin.settings.discourseRelations?.find( (dr) => @@ -156,6 +160,7 @@ const findOrCreateTriple = async ({ modified, ...(importedFromRid && { importedFromRid }), status: "provisional", + authorId, }; plugin.settings.discourseRelations = [ ...(plugin.settings.discourseRelations ?? []), @@ -179,7 +184,7 @@ export const fetchRelationInstancesFromSpace = async ({ const { data: instances, error } = await client .from("my_concepts") .select( - "id, source_local_id, schema_id, reference_content, refs, created, last_modified, concepts_of_relation!inner(id, space_id, source_local_id)", + "id, source_local_id, schema_id, reference_content, refs, created, last_modified, author_id, concepts_of_relation!inner(id, space_id, source_local_id)", ) .eq("space_id", spaceId) .eq("is_schema", false) @@ -345,6 +350,7 @@ export const importRelationsForImportedNodes = async ({ ).getTime() : undefined; + const authorId = rel.author_id || undefined; if (mappedSourceNodeTypeId && mappedDestNodeTypeId) { await findOrCreateTriple({ plugin, @@ -354,6 +360,7 @@ export const importRelationsForImportedNodes = async ({ importedCreatedAt, importedModifiedAt, importedFromRid: relationImportedFromRid, + authorId, }); } @@ -371,6 +378,7 @@ export const importRelationsForImportedNodes = async ({ destination: destEndpointId, importedFromRid: relationImportedFromRid, tentative: false, + authorId, }); imported++; diff --git a/apps/obsidian/src/utils/relationsStore.ts b/apps/obsidian/src/utils/relationsStore.ts index f89e0ba8a..b7c8f92cc 100644 --- a/apps/obsidian/src/utils/relationsStore.ts +++ b/apps/obsidian/src/utils/relationsStore.ts @@ -130,7 +130,7 @@ export type AddRelationParams = { type: string; source: string; destination: string; - author?: string; + authorId?: number; importedFromRid?: string; publishedToGroupId?: string[]; /** On first import, set to false. true or undefined = accepted/local. */ @@ -147,15 +147,13 @@ export const addRelationNoCheck = async ( ): Promise => { const now = Date.now(); const id = uuidv7(); - const author = - params.author ?? plugin.settings.accountLocalId ?? getVaultId(plugin.app); const instance: RelationInstance = { id, type: params.type, source: params.source, destination: params.destination, created: now, - author, + authorId: params.authorId, importedFromRid: params.importedFromRid, publishedToGroupId: params.publishedToGroupId, ...(params.tentative !== undefined && { @@ -700,14 +698,12 @@ export const migrateFrontmatterRelationsToRelationsJson = async ( const id = uuidv7(); const now = Date.now(); - const author = plugin.settings.accountLocalId ?? getVaultId(plugin.app); data.relations[id] = { id, type: relationType.id, source: sourceNodeInstanceId, destination: destNodeInstanceId, created: now, - author, }; added++; pendingCleanups.push({ diff --git a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts index a4d5c410e..c7a37a720 100644 --- a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts +++ b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts @@ -474,13 +474,7 @@ const convertDgToSupabaseConcepts = async ({ const nodesTypesToLocalConcepts = nodeTypes .filter((nodeType) => nodeType.modified > lastNodeSchemaSync) - .map((nodeType) => - discourseNodeSchemaToLocalConcept({ - context, - node: nodeType, - accountLocalId, - }), - ); + .map((nodeType) => discourseNodeSchemaToLocalConcept(context, nodeType)); const relationTypesById = Object.fromEntries( relationTypes.map((relationType) => [relationType.id, relationType]), @@ -489,11 +483,7 @@ const convertDgToSupabaseConcepts = async ({ const relationTypesToLocalConcepts = relationTypes .filter((relationType) => relationType.modified > lastRelationSchemaSync) .map((relationType) => - discourseRelationTypeToLocalConcept({ - context, - relationType, - accountLocalId, - }), + discourseRelationTypeToLocalConcept(context, relationType), ); const discourseRelationTriplesToLocalConcepts = discourseRelations @@ -513,7 +503,6 @@ const convertDgToSupabaseConcepts = async ({ discourseRelationTripleSchemaToLocalConcept({ context, relation, - accountLocalId, nodeTypesById, relationTypesById, }), @@ -521,11 +510,7 @@ const convertDgToSupabaseConcepts = async ({ .filter((n) => !!n); const nodeInstanceToLocalConcepts = nodesSince.map((node) => { - return discourseNodeInstanceToLocalConcept({ - context, - nodeData: node, - accountLocalId, - }); + return discourseNodeInstanceToLocalConcept(context, node); }); const relationInstancesData = await loadRelations(plugin); From 71705ba6347d17b210e3b60c9a59f7426ffb9df7 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 11:33:35 -0400 Subject: [PATCH 10/19] Add author name to relation hover --- .../src/components/RelationshipSection.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/obsidian/src/components/RelationshipSection.tsx b/apps/obsidian/src/components/RelationshipSection.tsx index cb8a7582e..c3339c2c5 100644 --- a/apps/obsidian/src/components/RelationshipSection.tsx +++ b/apps/obsidian/src/components/RelationshipSection.tsx @@ -15,6 +15,7 @@ import { getNodeTypeById, getAndFormatImportSource, isAcceptedSchema, + getUserNameById, } from "~/utils/typeUtils"; import type { RelationInstance } from "~/types"; import { @@ -527,7 +528,19 @@ const CurrentRelationships = ({
{isImported && importInfo.spaceUri && ( + {nodeType.authorId && + `by ${getUserNameById(plugin, nodeType.authorId)} `} from{" "} {formatImportSource( importInfo.spaceUri || "", diff --git a/apps/obsidian/src/components/RelationshipSettings.tsx b/apps/obsidian/src/components/RelationshipSettings.tsx index 443e4dddd..6e52c720b 100644 --- a/apps/obsidian/src/components/RelationshipSettings.tsx +++ b/apps/obsidian/src/components/RelationshipSettings.tsx @@ -9,6 +9,7 @@ import { formatImportSource, isAcceptedSchema, isProvisionalSchema, + getUserNameById, } from "~/utils/typeUtils"; import generateUid from "~/utils/generateUid"; @@ -285,6 +286,8 @@ const RelationshipSettings = () => { )} {importInfo.spaceUri && ( + {relation.authorId && + `by ${getUserNameById(plugin, relation.authorId)} `} from{" "} {formatImportSource( importInfo.spaceUri, diff --git a/apps/obsidian/src/components/RelationshipTypeSettings.tsx b/apps/obsidian/src/components/RelationshipTypeSettings.tsx index 223afca88..30ac6db20 100644 --- a/apps/obsidian/src/components/RelationshipTypeSettings.tsx +++ b/apps/obsidian/src/components/RelationshipTypeSettings.tsx @@ -16,6 +16,7 @@ import { getImportInfo, formatImportSource, isProvisionalSchema, + getUserNameById, } from "~/utils/typeUtils"; type ColorPickerProps = { @@ -346,6 +347,8 @@ const RelationshipTypeSettings = () => { )} {importInfo.spaceUri && ( + {relationType.authorId && + `by ${getUserNameById(plugin, relationType.authorId)} `} from{" "} {formatImportSource( importInfo.spaceUri, From 1a2e4e3ff1b0f484b1508a465da27248850ddc56 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 11:46:00 -0400 Subject: [PATCH 12/19] linting --- .../src/components/DiscourseContextView.tsx | 14 +++++++++----- apps/obsidian/src/utils/conceptConversion.ts | 2 +- apps/obsidian/src/utils/typeUtils.ts | 1 - 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/obsidian/src/components/DiscourseContextView.tsx b/apps/obsidian/src/components/DiscourseContextView.tsx index 96a1592ec..18ae137e1 100644 --- a/apps/obsidian/src/components/DiscourseContextView.tsx +++ b/apps/obsidian/src/components/DiscourseContextView.tsx @@ -255,11 +255,15 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => {
)} - {isImported && frontmatter.authorId && ( -
-
Author: {getUserNameById(plugin, frontmatter.authorId)}
-
- )} + {isImported && + frontmatter.authorId && + typeof frontmatter.authorId === "number" && ( +
+
+ Author: {getUserNameById(plugin, frontmatter.authorId)} +
+
+ )} {isImported && sourceDates && (
Created in source: {sourceDates.createdAt}
diff --git a/apps/obsidian/src/utils/conceptConversion.ts b/apps/obsidian/src/utils/conceptConversion.ts index 5843484e1..9ed248753 100644 --- a/apps/obsidian/src/utils/conceptConversion.ts +++ b/apps/obsidian/src/utils/conceptConversion.ts @@ -16,9 +16,9 @@ import type { Json } from "@repo/database/dbTypes"; */ const getNodeExtraData = ( file: TFile, + /* eslint-disable @typescript-eslint/naming-convention */ author_id: number, ): { - /* eslint-disable @typescript-eslint/naming-convention */ author_id: number; created: string; last_modified: string; diff --git a/apps/obsidian/src/utils/typeUtils.ts b/apps/obsidian/src/utils/typeUtils.ts index eb373579c..09ebee4b7 100644 --- a/apps/obsidian/src/utils/typeUtils.ts +++ b/apps/obsidian/src/utils/typeUtils.ts @@ -1,7 +1,6 @@ import type DiscourseGraphPlugin from "~/index"; import { DiscourseNode, DiscourseRelationType, ImportStatus } from "~/types"; import { ridToSpaceUriAndLocalId } from "./rid"; -import { DGSupabaseClient } from "@repo/database/lib/client"; export const getNodeTypeById = ( plugin: DiscourseGraphPlugin, From ae67d978d88315a70c4762754650b369eb38aafe Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 11:58:01 -0400 Subject: [PATCH 13/19] Devin comments --- apps/obsidian/src/components/GeneralSettings.tsx | 2 +- apps/obsidian/src/utils/conceptConversion.ts | 2 +- ...40_select_platform_account_of_partial_space.sql | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/obsidian/src/components/GeneralSettings.tsx b/apps/obsidian/src/components/GeneralSettings.tsx index 132afe671..7c182e4b6 100644 --- a/apps/obsidian/src/components/GeneralSettings.tsx +++ b/apps/obsidian/src/components/GeneralSettings.tsx @@ -334,7 +334,7 @@ const GeneralSettings = () => { void handleSetUsername(e.target.value)} + onBlur={(e) => void handleSetUsername(e.target.value)} />
diff --git a/apps/obsidian/src/utils/conceptConversion.ts b/apps/obsidian/src/utils/conceptConversion.ts index 9ed248753..b768715de 100644 --- a/apps/obsidian/src/utils/conceptConversion.ts +++ b/apps/obsidian/src/utils/conceptConversion.ts @@ -223,7 +223,7 @@ export const relationInstanceToLocalConcept = ({ space_id: context.spaceId, name: `[[${sourceNode.file.basename}]] -${relationType.label}-> [[${destinationNode.file.basename}]]`, source_local_id: relationInstanceData.id, - author_id: relationInstanceData.authorId, + author_id: relationInstanceData.authorId ?? context.userId, schema_represented_by_local_id: type, is_schema: false, created: new Date(created).toISOString(), diff --git a/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql b/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql index 0436bc3b0..4bc62fc83 100644 --- a/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql +++ b/packages/database/supabase/migrations/20260421141140_select_platform_account_of_partial_space.sql @@ -39,3 +39,17 @@ WHERE id IN ( JOIN public.my_user_accounts() ON (account_uid = my_user_accounts) WHERE permissions >= 'partial' ); + +CREATE OR REPLACE FUNCTION public.generic_entity_access(target_id BIGINT, target_type public."EntityType") RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql AS $$ + SELECT CASE + WHEN target_type = 'Space' THEN public.in_space(target_id) + WHEN target_type = 'Content' THEN public.content_in_space(target_id) + WHEN target_type = 'Concept' THEN public.concept_in_space(target_id) + WHEN target_type = 'Document' THEN public.document_in_space(target_id) + WHEN target_type = 'PlatformAccount' THEN public.account_in_shared_space(target_id) + ELSE false + END; +$$; From f1e4a06890be78d0f9f224ae0ec8ce2c7fcd1483 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 12:22:03 -0400 Subject: [PATCH 14/19] Devin comments, bis --- apps/obsidian/src/components/GeneralSettings.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/obsidian/src/components/GeneralSettings.tsx b/apps/obsidian/src/components/GeneralSettings.tsx index 7c182e4b6..867dab731 100644 --- a/apps/obsidian/src/components/GeneralSettings.tsx +++ b/apps/obsidian/src/components/GeneralSettings.tsx @@ -334,6 +334,7 @@ const GeneralSettings = () => { setUsername(e.target.value)} onBlur={(e) => void handleSetUsername(e.target.value)} /> From d713f714ecc795b3eb6f99f724564c6d5f9c10ed Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 12:52:07 -0400 Subject: [PATCH 15/19] Devin nitpick --- apps/obsidian/src/utils/importNodes.ts | 4 ++-- apps/obsidian/src/utils/importRelations.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 793b31152..dade6d35a 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -128,7 +128,7 @@ export const getPublishedNodesForGroups = async ({ createdAt, modifiedAt, filePath, - authorId: latest.author_id || undefined, + authorId: latest.author_id ?? undefined, }); } @@ -1104,7 +1104,7 @@ export const mapNodeTypeIdToLocal = async ({ keyImage: parsed.keyImage, created: now, modified: now, - authorId: schemaData.author_id || undefined, + authorId: schemaData.author_id ?? undefined, importedFromRid, }; plugin.settings.nodeTypes = [...plugin.settings.nodeTypes, newNodeType]; diff --git a/apps/obsidian/src/utils/importRelations.ts b/apps/obsidian/src/utils/importRelations.ts index 743465449..93ae97143 100644 --- a/apps/obsidian/src/utils/importRelations.ts +++ b/apps/obsidian/src/utils/importRelations.ts @@ -100,7 +100,7 @@ const mapRelationTypeToLocal = async ({ modified: now, importedFromRid, status: "provisional", - authorId: schemaData.author_id || undefined, + authorId: schemaData.author_id ?? undefined, }; plugin.settings.relationTypes = [ ...(plugin.settings.relationTypes ?? []), @@ -350,7 +350,7 @@ export const importRelationsForImportedNodes = async ({ ).getTime() : undefined; - const authorId = rel.author_id || undefined; + const authorId = rel.author_id ?? undefined; if (mappedSourceNodeTypeId && mappedDestNodeTypeId) { await findOrCreateTriple({ plugin, From aef00d2f7537b04626088d09ec076c1a5ca5c913 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Tue, 28 Apr 2026 22:57:08 -0400 Subject: [PATCH 16/19] Move username to admin panel --- .../src/components/AdminPanelSettings.tsx | 32 ++++++++++++++++++ .../src/components/GeneralSettings.tsx | 33 ------------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/apps/obsidian/src/components/AdminPanelSettings.tsx b/apps/obsidian/src/components/AdminPanelSettings.tsx index ac6be9af2..23e1ebab1 100644 --- a/apps/obsidian/src/components/AdminPanelSettings.tsx +++ b/apps/obsidian/src/components/AdminPanelSettings.tsx @@ -1,6 +1,7 @@ import { useState, useCallback } from "react"; import { usePlugin } from "./PluginContext"; import { Notice } from "obsidian"; +import { updateUsername } from "~/utils/supabaseContext"; import { initializeSupabaseSync } from "~/utils/syncDgNodesToSupabase"; export const AdminPanelSettings = () => { @@ -8,6 +9,9 @@ export const AdminPanelSettings = () => { const [syncModeEnabled, setSyncModeEnabled] = useState( plugin.settings.syncModeEnabled ?? false, ); + const [username, setUsername] = useState( + plugin.settings.username || "", + ); const handleSyncModeToggle = useCallback( async (newValue: boolean) => { @@ -30,6 +34,13 @@ export const AdminPanelSettings = () => { [plugin], ); + const handleSetUsername = async (newValue: string) => { + setUsername(newValue); + plugin.settings.username = newValue; + await plugin.saveSettings(); + await updateUsername(plugin, newValue); + }; + return (
@@ -48,6 +59,27 @@ export const AdminPanelSettings = () => {
+
+
+
Username
+
+ A username that will be associated with your vault if you share + data. +
+
+
+ setUsername(e.target.value)} + onBlur={(e) => void handleSetUsername(e.target.value)} + /> +
+
); }; diff --git a/apps/obsidian/src/components/GeneralSettings.tsx b/apps/obsidian/src/components/GeneralSettings.tsx index 867dab731..d89453575 100644 --- a/apps/obsidian/src/components/GeneralSettings.tsx +++ b/apps/obsidian/src/components/GeneralSettings.tsx @@ -2,7 +2,6 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { usePlugin } from "./PluginContext"; import { setIcon } from "obsidian"; import SuggestInput from "./SuggestInput"; -import { updateUsername } from "~/utils/supabaseContext"; import { SLACK_LOGO, WHITE_LOGO_SVG } from "~/icons"; const DOCS_URL = "https://discoursegraphs.com/docs/obsidian"; @@ -153,9 +152,6 @@ const GeneralSettings = () => { const [canvasFolderPath, setCanvasFolderPath] = useState( plugin.settings.canvasFolderPath, ); - const [username, setUsername] = useState( - plugin.settings.username || "", - ); const [canvasAttachmentsFolderPath, setCanvasAttachmentsFolderPath] = useState(plugin.settings.canvasAttachmentsFolderPath); const [nodeTagHotkey, setNodeTagHotkey] = useState( @@ -207,13 +203,6 @@ const GeneralSettings = () => { [plugin], ); - const handleSetUsername = async (newValue: string) => { - setUsername(newValue); - plugin.settings.username = newValue; - await plugin.saveSettings(); - await updateUsername(plugin, newValue); - }; - return (
@@ -318,28 +307,6 @@ const GeneralSettings = () => {
-
-
-
Username
-
- A username that will be associated with your vault if you share - data. -
-
-
- setUsername(e.target.value)} - onBlur={(e) => void handleSetUsername(e.target.value)} - /> -
-
- ); From 1fdc86cb3f7d25619f194b67329211e98ec2e967 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Tue, 28 Apr 2026 23:00:35 -0400 Subject: [PATCH 17/19] Use my_accounts for efficiency --- apps/obsidian/src/utils/importNodes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index dade6d35a..f8ec3ea39 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -234,7 +234,7 @@ const fetchUserNames = async ( client: DGSupabaseClient, ) => { const result = await client - .from("PlatformAccount") + .from("my_accounts") .select("id, name") .eq("agent_type", "person"); if (result.error || !result.data) { From af2c2c531487f4cafb7f6064c101efe67cbce093 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Tue, 28 Apr 2026 23:03:54 -0400 Subject: [PATCH 18/19] Move call to fetchUserNames --- apps/obsidian/src/components/ImportNodesModal.tsx | 3 +++ apps/obsidian/src/utils/importNodes.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/obsidian/src/components/ImportNodesModal.tsx b/apps/obsidian/src/components/ImportNodesModal.tsx index b7196e248..2db1366c7 100644 --- a/apps/obsidian/src/components/ImportNodesModal.tsx +++ b/apps/obsidian/src/components/ImportNodesModal.tsx @@ -5,6 +5,7 @@ import type DiscourseGraphPlugin from "../index"; import type { ImportableNode, GroupWithNodes } from "~/types"; import { getUserNameById } from "~/utils/typeUtils"; import { + fetchUserNames, getAvailableGroupIds, getPublishedNodesForGroups, getLocalNodeInstanceIds, @@ -62,6 +63,8 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { return; } + await fetchUserNames(plugin, client); + const publishedNodes = await getPublishedNodesForGroups({ client, groupIds, diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index f8ec3ea39..e44d64d54 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -229,7 +229,7 @@ export const getSpaceUris = async ( return spaceMap; }; -const fetchUserNames = async ( +export const fetchUserNames = async ( plugin: DiscourseGraphPlugin, client: DGSupabaseClient, ) => { @@ -1242,7 +1242,6 @@ export const importSelectedNodes = async ({ nodesBySpace.get(node.spaceId)!.push(node); } - await fetchUserNames(plugin, client); const spaceUris = await getSpaceUris(client, [...nodesBySpace.keys()]); // Process each space From af50919a18b21849312ca43d0bd7629d6c293ab6 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Tue, 28 Apr 2026 23:14:34 -0400 Subject: [PATCH 19/19] lint error --- apps/obsidian/src/utils/importNodes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index e44d64d54..c91814a43 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -241,8 +241,8 @@ export const fetchUserNames = async ( console.error(result.error); return; } - const nameById: Record = Object.fromEntries( - result.data.map(({ id, name }) => [id, name]), + const nameById = Object.fromEntries( + result.data.map(({ id, name }) => [id, name]) as [number, string][], ); plugin.settings.userNames = nameById; await plugin.saveSettings();