Skip to content

Commit 9d14ba5

Browse files
trangdoan982claudedevin-ai-integration[bot]
authored
ENG-1608: Add command to create Base view for a node type (#935)
* ENG-1608: Add command to create Base view for a node type Adds a "Create Base view for node type" command that generates a .base YAML file filtered by nodeTypeId, with entry points from the command palette, node type settings, and discourse context panel. Also refactors NodeTypeModal to accept a generic callback instead of being hardcoded to createDiscourseNode, enabling reuse across both the existing node creation flow and the new Base view creation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Apply suggestions from code review Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> * add documentations * PR comment --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent e0710ec commit 9d14ba5

10 files changed

Lines changed: 168 additions & 16 deletions

File tree

apps/obsidian/src/components/DiscourseContextView.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
WorkspaceLeaf,
55
Notice,
66
FrontMatterCache,
7+
setIcon,
78
} from "obsidian";
89
import { createRoot, Root } from "react-dom/client";
910
import DiscourseGraphPlugin from "~/index";
@@ -14,6 +15,7 @@ import { PluginProvider, usePlugin } from "~/components/PluginContext";
1415
import { getNodeTypeById } from "~/utils/typeUtils";
1516
import { refreshImportedFile } from "~/utils/importNodes";
1617
import { publishNode } from "~/utils/publishNode";
18+
import { createBaseForNodeType } from "~/utils/baseForNodeType";
1719
import { useState, useEffect } from "react";
1820

1921
type DiscourseContextProps = {
@@ -154,6 +156,18 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => {
154156
/>
155157
)}
156158
{nodeType.name || "Unnamed Node Type"}
159+
<button
160+
onClick={() => {
161+
void createBaseForNodeType(plugin, nodeType);
162+
}}
163+
className="clickable-icon ml-1"
164+
title={`Create Base view for ${nodeType.name}`}
165+
aria-label={`Create Base view for ${nodeType.name}`}
166+
>
167+
<div
168+
ref={(el) => (el && setIcon(el, "layout-list")) || undefined}
169+
/>
170+
</button>
157171
{isImported && (
158172
<button
159173
onClick={() => {
Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
1-
import { App, Editor, SuggestModal, TFile, Notice } from "obsidian";
2-
import { DiscourseNode } from "~/types";
3-
import { createDiscourseNode } from "~/utils/createNode";
1+
import { SuggestModal } from "obsidian";
2+
import type { DiscourseNode } from "~/types";
43
import type DiscourseGraphPlugin from "~/index";
54

65
export class NodeTypeModal extends SuggestModal<DiscourseNode> {
6+
private nodeTypes: DiscourseNode[];
7+
private onSelect: (nodeType: DiscourseNode) => void;
8+
79
constructor(
8-
private editor: Editor,
9-
private nodeTypes: DiscourseNode[],
10-
private plugin: DiscourseGraphPlugin,
10+
plugin: DiscourseGraphPlugin,
11+
onSelect: (nodeType: DiscourseNode) => void,
1112
) {
1213
super(plugin.app);
14+
this.nodeTypes = plugin.settings.nodeTypes;
15+
this.onSelect = onSelect;
1316
}
1417

1518
getItemText(item: DiscourseNode): string {
1619
return item.name;
1720
}
1821

19-
getSuggestions() {
22+
getSuggestions(): DiscourseNode[] {
2023
const query = this.inputEl.value.toLowerCase();
2124
return this.nodeTypes.filter((node) =>
2225
this.getItemText(node).toLowerCase().includes(query),
2326
);
2427
}
2528

26-
renderSuggestion(nodeType: DiscourseNode, el: HTMLElement) {
29+
renderSuggestion(nodeType: DiscourseNode, el: HTMLElement): void {
2730
const container = el.createDiv({ cls: "flex items-center gap-2" });
2831
if (nodeType.color) {
2932
container.createDiv({
@@ -34,12 +37,7 @@ export class NodeTypeModal extends SuggestModal<DiscourseNode> {
3437
container.createDiv({ text: nodeType.name });
3538
}
3639

37-
async onChooseSuggestion(nodeType: DiscourseNode) {
38-
await createDiscourseNode({
39-
plugin: this.plugin,
40-
editor: this.editor,
41-
nodeType,
42-
text: this.editor.getSelection().trim() || "",
43-
});
40+
onChooseSuggestion(nodeType: DiscourseNode): void {
41+
this.onSelect(nodeType);
4442
}
4543
}

apps/obsidian/src/components/NodeTypeSettings.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getAndFormatImportSource,
1313
} from "~/utils/typeUtils";
1414
import { FolderSuggestInput } from "./GeneralSettings";
15+
import { createBaseForNodeType } from "~/utils/baseForNodeType";
1516

1617
const generateTagPlaceholder = (format: string, nodeName?: string): string => {
1718
if (!format) return "Enter tag (e.g., clm-candidate)";
@@ -709,6 +710,25 @@ const NodeTypeSettings = () => {
709710
/>
710711
</div>
711712
</div>
713+
{selectedNodeIndex !== null && selectedNodeIndex < nodeTypes.length && (
714+
<div className="setting-item">
715+
<div className="setting-item-info">
716+
<div className="setting-item-name">Base view</div>
717+
<div className="setting-item-description">
718+
Create a new Base view filtered to this node type
719+
</div>
720+
</div>
721+
<div className="setting-item-control">
722+
<button
723+
onClick={() =>
724+
void createBaseForNodeType(plugin, editingNodeType)
725+
}
726+
>
727+
Create Base view
728+
</button>
729+
</div>
730+
</div>
731+
)}
712732
</div>
713733
);
714734
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Notice } from "obsidian";
2+
import type DiscourseGraphPlugin from "~/index";
3+
import type { DiscourseNode } from "~/types";
4+
5+
const generateBaseYaml = (nodeType: DiscourseNode): string => {
6+
return [
7+
"views:",
8+
" - type: table",
9+
` name: "${nodeType.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')} Nodes"`,
10+
" order:",
11+
" - file.name",
12+
" filters:",
13+
" and:",
14+
` - nodeTypeId == "${nodeType.id}"`,
15+
"",
16+
].join("\n");
17+
};
18+
19+
const getAvailableFilename = (
20+
plugin: DiscourseGraphPlugin,
21+
baseName: string,
22+
): string => {
23+
if (!plugin.app.vault.getAbstractFileByPath(`${baseName}.base`)) {
24+
return `${baseName}.base`;
25+
}
26+
let i = 1;
27+
while (plugin.app.vault.getAbstractFileByPath(`${baseName} ${i}.base`)) {
28+
i++;
29+
}
30+
return `${baseName} ${i}.base`;
31+
};
32+
33+
export const createBaseForNodeType = async (
34+
plugin: DiscourseGraphPlugin,
35+
nodeType: DiscourseNode,
36+
): Promise<void> => {
37+
try {
38+
const filename = getAvailableFilename(plugin, `${nodeType.name} Nodes`);
39+
const content = generateBaseYaml(nodeType);
40+
await plugin.app.vault.create(filename, content);
41+
await plugin.app.workspace.openLinkText(filename, "");
42+
new Notice(`Created Base view for ${nodeType.name}`);
43+
} catch (e) {
44+
new Notice(e instanceof Error ? e.message : "Failed to create Base view");
45+
console.error("Failed to create Base view:", e);
46+
}
47+
};

apps/obsidian/src/utils/registerCommands.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { publishNode } from "./publishNode";
1313
import { addRelationIfRequested } from "~/components/canvas/utils/relationJsonUtils";
1414
import type { DiscourseNode } from "~/types";
1515
import { TldrawView } from "~/components/canvas/TldrawView";
16+
import { createBaseForNodeType } from "./baseForNodeType";
1617

1718
type ModifyNodeSubmitParams = {
1819
nodeType: DiscourseNode;
@@ -65,7 +66,14 @@ export const registerCommands = (plugin: DiscourseGraphPlugin) => {
6566
const hasSelection = !!editor.getSelection();
6667

6768
if (hasSelection) {
68-
new NodeTypeModal(editor, plugin.settings.nodeTypes, plugin).open();
69+
new NodeTypeModal(plugin, (nodeType) => {
70+
void createDiscourseNode({
71+
plugin,
72+
editor,
73+
nodeType,
74+
text: editor.getSelection().trim() || "",
75+
});
76+
}).open();
6977
} else {
7078
const currentFile =
7179
plugin.app.workspace.getActiveViewOfType(MarkdownView)?.file ||
@@ -249,6 +257,16 @@ export const registerCommands = (plugin: DiscourseGraphPlugin) => {
249257
return true;
250258
},
251259
});
260+
plugin.addCommand({
261+
id: "create-base-for-node-type",
262+
name: "Create Base view for node type",
263+
callback: () => {
264+
new NodeTypeModal(plugin, (nodeType) => {
265+
void createBaseForNodeType(plugin, nodeType);
266+
}).open();
267+
},
268+
});
269+
252270
plugin.addCommand({
253271
id: "publish-discourse-node",
254272
name: "Publish current node to lab space",

apps/website/app/(docs)/docs/obsidian/navigation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export const navigation: NavigationList = [
6363
title: "Node tags",
6464
href: `${ROOT}/node-tags`,
6565
},
66+
{
67+
title: "Querying your discourse graph",
68+
href: `${ROOT}/querying-discourse-graph`,
69+
},
6670
],
6771
},
6872

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
title: "Querying your discourse graph"
3+
date: "2026-04-02"
4+
author: ""
5+
published: true
6+
---
7+
8+
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.
9+
10+
## What is a Base view?
11+
12+
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.
13+
14+
## Creating a Base view
15+
16+
There are three ways to create a Base view for a node type:
17+
18+
### From the command palette
19+
20+
1. Open the command palette (`Cmd/Ctrl + P`)
21+
2. Search for "Create Base view for node type"
22+
3. Select the node type you want to query
23+
24+
![base-from-command.png](/docs/obsidian/base-from-command.png)
25+
26+
### From node type settings
27+
28+
1. Open Discourse Graphs settings
29+
2. Click on a node type to edit it
30+
3. Click the **Create Base view** button at the bottom of the edit form
31+
32+
<!-- TODO: Add screenshot of the "Create Base view" button in node type settings -->
33+
34+
![base-from-setting.png](/docs/obsidian/base-from-setting.png)
35+
36+
### From the discourse context panel
37+
38+
When viewing a discourse node, you can create a Base view for its node type directly from the context panel:
39+
40+
1. Open the [Discourse context panel](./discourse-context) for any discourse node
41+
2. Click the table icon next to the node type name
42+
43+
![base-from-context.png](/docs/obsidian/base-from-context.png)
44+
45+
## How it works
46+
47+
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`).
48+
49+
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.
50+
51+
> **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.
255 KB
Loading
110 KB
Loading
587 KB
Loading

0 commit comments

Comments
 (0)