diff --git a/apps/obsidian/src/components/DiscourseContextView.tsx b/apps/obsidian/src/components/DiscourseContextView.tsx index b99c147fe..79e95dabe 100644 --- a/apps/obsidian/src/components/DiscourseContextView.tsx +++ b/apps/obsidian/src/components/DiscourseContextView.tsx @@ -4,6 +4,7 @@ import { WorkspaceLeaf, Notice, FrontMatterCache, + setIcon, } from "obsidian"; import { createRoot, Root } from "react-dom/client"; import DiscourseGraphPlugin from "~/index"; @@ -14,6 +15,7 @@ import { PluginProvider, usePlugin } from "~/components/PluginContext"; import { getNodeTypeById } from "~/utils/typeUtils"; import { refreshImportedFile } from "~/utils/importNodes"; import { publishNode } from "~/utils/publishNode"; +import { createBaseForNodeType } from "~/utils/baseForNodeType"; import { useState, useEffect } from "react"; type DiscourseContextProps = { @@ -154,6 +156,18 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => { /> )} {nodeType.name || "Unnamed Node Type"} + {isImported && ( + + + )} ); }; diff --git a/apps/obsidian/src/utils/baseForNodeType.ts b/apps/obsidian/src/utils/baseForNodeType.ts new file mode 100644 index 000000000..d344e023d --- /dev/null +++ b/apps/obsidian/src/utils/baseForNodeType.ts @@ -0,0 +1,47 @@ +import { Notice } from "obsidian"; +import type DiscourseGraphPlugin from "~/index"; +import type { DiscourseNode } from "~/types"; + +const generateBaseYaml = (nodeType: DiscourseNode): string => { + return [ + "views:", + " - type: table", + ` name: "${nodeType.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')} Nodes"`, + " order:", + " - file.name", + " filters:", + " and:", + ` - nodeTypeId == "${nodeType.id}"`, + "", + ].join("\n"); +}; + +const getAvailableFilename = ( + plugin: DiscourseGraphPlugin, + baseName: string, +): string => { + if (!plugin.app.vault.getAbstractFileByPath(`${baseName}.base`)) { + return `${baseName}.base`; + } + let i = 1; + while (plugin.app.vault.getAbstractFileByPath(`${baseName} ${i}.base`)) { + i++; + } + return `${baseName} ${i}.base`; +}; + +export const createBaseForNodeType = async ( + plugin: DiscourseGraphPlugin, + nodeType: DiscourseNode, +): Promise => { + try { + const filename = getAvailableFilename(plugin, `${nodeType.name} Nodes`); + const content = generateBaseYaml(nodeType); + await plugin.app.vault.create(filename, content); + await plugin.app.workspace.openLinkText(filename, ""); + new Notice(`Created Base view for ${nodeType.name}`); + } catch (e) { + new Notice(e instanceof Error ? e.message : "Failed to create Base view"); + console.error("Failed to create Base view:", e); + } +}; diff --git a/apps/obsidian/src/utils/registerCommands.ts b/apps/obsidian/src/utils/registerCommands.ts index 7763dee95..25d8f0619 100644 --- a/apps/obsidian/src/utils/registerCommands.ts +++ b/apps/obsidian/src/utils/registerCommands.ts @@ -13,6 +13,7 @@ import { publishNode } from "./publishNode"; import { addRelationIfRequested } from "~/components/canvas/utils/relationJsonUtils"; import type { DiscourseNode } from "~/types"; import { TldrawView } from "~/components/canvas/TldrawView"; +import { createBaseForNodeType } from "./baseForNodeType"; type ModifyNodeSubmitParams = { nodeType: DiscourseNode; @@ -65,7 +66,14 @@ export const registerCommands = (plugin: DiscourseGraphPlugin) => { const hasSelection = !!editor.getSelection(); if (hasSelection) { - new NodeTypeModal(editor, plugin.settings.nodeTypes, plugin).open(); + new NodeTypeModal(plugin, (nodeType) => { + void createDiscourseNode({ + plugin, + editor, + nodeType, + text: editor.getSelection().trim() || "", + }); + }).open(); } else { const currentFile = plugin.app.workspace.getActiveViewOfType(MarkdownView)?.file || @@ -249,6 +257,16 @@ export const registerCommands = (plugin: DiscourseGraphPlugin) => { return true; }, }); + plugin.addCommand({ + id: "create-base-for-node-type", + name: "Create Base view for node type", + callback: () => { + new NodeTypeModal(plugin, (nodeType) => { + void createBaseForNodeType(plugin, nodeType); + }).open(); + }, + }); + plugin.addCommand({ id: "publish-discourse-node", name: "Publish current node to lab space", diff --git a/apps/website/app/(docs)/docs/obsidian/navigation.ts b/apps/website/app/(docs)/docs/obsidian/navigation.ts index da0935df8..dbb6008b3 100644 --- a/apps/website/app/(docs)/docs/obsidian/navigation.ts +++ b/apps/website/app/(docs)/docs/obsidian/navigation.ts @@ -63,6 +63,10 @@ export const navigation: NavigationList = [ title: "Node tags", href: `${ROOT}/node-tags`, }, + { + title: "Querying your discourse graph", + href: `${ROOT}/querying-discourse-graph`, + }, ], }, diff --git a/apps/website/app/(docs)/docs/obsidian/pages/querying-discourse-graph.md b/apps/website/app/(docs)/docs/obsidian/pages/querying-discourse-graph.md new file mode 100644 index 000000000..a2cf61855 --- /dev/null +++ b/apps/website/app/(docs)/docs/obsidian/pages/querying-discourse-graph.md @@ -0,0 +1,51 @@ +--- +title: "Querying your discourse graph" +date: "2026-04-02" +author: "" +published: true +--- + +As your discourse graph grows, you'll want to view and filter nodes by type — for example, seeing all your Claims or all your Questions in one place. Discourse Graphs integrates with Obsidian's [Bases](https://obsidian.md/blog/bases/) feature to create filtered table views for any node type. + +## What is a Base view? + +A Base view is a `.base` file that Obsidian renders as a filterable, sortable table. Discourse Graphs can generate these files pre-configured to show only nodes of a specific type, using the `nodeTypeId` frontmatter property as a filter. + +## Creating a Base view + +There are three ways to create a Base view for a node type: + +### From the command palette + +1. Open the command palette (`Cmd/Ctrl + P`) +2. Search for "Create Base view for node type" +3. Select the node type you want to query + +![base-from-command.png](/docs/obsidian/base-from-command.png) + +### From node type settings + +1. Open Discourse Graphs settings +2. Click on a node type to edit it +3. Click the **Create Base view** button at the bottom of the edit form + + + +![base-from-setting.png](/docs/obsidian/base-from-setting.png) + +### From the discourse context panel + +When viewing a discourse node, you can create a Base view for its node type directly from the context panel: + +1. Open the [Discourse context panel](./discourse-context) for any discourse node +2. Click the table icon next to the node type name + +![base-from-context.png](/docs/obsidian/base-from-context.png) + +## How it works + +Each time you create a Base view, a new `.base` file is created at the root of your vault with the name `{Node Type} Nodes.base` (e.g., `Claim Nodes.base`). If a file with that name already exists, a numbered suffix is added (e.g., `Claim Nodes 1.base`). + +The generated file contains a table view filtered to show only nodes matching the selected node type. You can then further customize the view in Obsidian — add columns, change sorting, or add additional filters. + +> **Note:** A new Base file is always created rather than opening an existing one. This ensures you always get a fresh view with the correct filter, even if you've modified a previous Base view. diff --git a/apps/website/public/docs/obsidian/base-from-command.png b/apps/website/public/docs/obsidian/base-from-command.png new file mode 100644 index 000000000..2e642cb0c Binary files /dev/null and b/apps/website/public/docs/obsidian/base-from-command.png differ diff --git a/apps/website/public/docs/obsidian/base-from-context.png b/apps/website/public/docs/obsidian/base-from-context.png new file mode 100644 index 000000000..df1e2821b Binary files /dev/null and b/apps/website/public/docs/obsidian/base-from-context.png differ diff --git a/apps/website/public/docs/obsidian/base-from-setting.png b/apps/website/public/docs/obsidian/base-from-setting.png new file mode 100644 index 000000000..56226a89e Binary files /dev/null and b/apps/website/public/docs/obsidian/base-from-setting.png differ