Skip to content

Commit 5f582b8

Browse files
authored
[ENG-509] Convert existing page to node (#242)
* convert page to node * sma styling * address PR comments
1 parent 14839cc commit 5f582b8

File tree

3 files changed

+110
-5
lines changed

3 files changed

+110
-5
lines changed

apps/obsidian/src/components/CreateNodeModal.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,21 @@ type CreateNodeFormProps = {
99
plugin: DiscourseGraphPlugin;
1010
onNodeCreate: (nodeType: DiscourseNode, title: string) => Promise<void>;
1111
onCancel: () => void;
12+
initialTitle?: string;
13+
initialNodeType?: DiscourseNode;
1214
};
1315

1416
export function CreateNodeForm({
1517
nodeTypes,
1618
plugin,
1719
onNodeCreate,
1820
onCancel,
21+
initialTitle = "",
22+
initialNodeType,
1923
}: CreateNodeFormProps) {
20-
const [title, setTitle] = useState("");
24+
const [title, setTitle] = useState(initialTitle);
2125
const [selectedNodeType, setSelectedNodeType] =
22-
useState<DiscourseNode | null>(null);
26+
useState<DiscourseNode | null>(initialNodeType || null);
2327
const [isSubmitting, setIsSubmitting] = useState(false);
2428
const titleInputRef = useRef<HTMLInputElement>(null);
2529

@@ -33,7 +37,7 @@ export function CreateNodeForm({
3337
const isFormValid = title.trim() && selectedNodeType;
3438

3539
const handleKeyDown = (e: React.KeyboardEvent) => {
36-
if (e.key === "Enter" && !e.shiftKey) {
40+
if (e.key === "Enter" && !e.shiftKey && isFormValid && !isSubmitting) {
3741
e.preventDefault();
3842
handleConfirm();
3943
} else if (e.key === "Escape") {
@@ -151,6 +155,8 @@ type CreateNodeModalProps = {
151155
nodeTypes: DiscourseNode[];
152156
plugin: DiscourseGraphPlugin;
153157
onNodeCreate: (nodeType: DiscourseNode, title: string) => Promise<void>;
158+
initialTitle?: string;
159+
initialNodeType?: DiscourseNode;
154160
};
155161

156162
export class CreateNodeModal extends Modal {
@@ -161,12 +167,16 @@ export class CreateNodeModal extends Modal {
161167
title: string,
162168
) => Promise<void>;
163169
private root: Root | null = null;
170+
private initialTitle?: string;
171+
private initialNodeType?: DiscourseNode;
164172

165173
constructor(app: App, props: CreateNodeModalProps) {
166174
super(app);
167175
this.nodeTypes = props.nodeTypes;
168176
this.plugin = props.plugin;
169177
this.onNodeCreate = props.onNodeCreate;
178+
this.initialTitle = props.initialTitle;
179+
this.initialNodeType = props.initialNodeType;
170180
}
171181

172182
onOpen() {
@@ -181,6 +191,8 @@ export class CreateNodeModal extends Modal {
181191
plugin={this.plugin}
182192
onNodeCreate={this.onNodeCreate}
183193
onCancel={() => this.close()}
194+
initialTitle={this.initialTitle}
195+
initialNodeType={this.initialNodeType}
184196
/>
185197
</StrictMode>,
186198
);

apps/obsidian/src/index.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { Plugin, Editor, Menu } from "obsidian";
1+
import { Plugin, Editor, Menu, TFile, Events } from "obsidian";
22
import { SettingsTab } from "~/components/Settings";
33
import { Settings } from "~/types";
44
import { registerCommands } from "~/utils/registerCommands";
55
import { DiscourseContextView } from "~/components/DiscourseContextView";
66
import { VIEW_TYPE_DISCOURSE_CONTEXT } from "~/types";
7-
import { createDiscourseNode } from "~/utils/createNode";
7+
import {
8+
convertPageToDiscourseNode,
9+
createDiscourseNode,
10+
} from "~/utils/createNode";
811
import { DEFAULT_SETTINGS } from "~/constants";
12+
import { CreateNodeModal } from "~/components/CreateNodeModal";
913

1014
export default class DiscourseGraphPlugin extends Plugin {
1115
settings: Settings = { ...DEFAULT_SETTINGS };
@@ -28,6 +32,52 @@ export default class DiscourseGraphPlugin extends Plugin {
2832
// Initialize frontmatter CSS
2933
this.updateFrontmatterStyles();
3034

35+
this.registerEvent(
36+
// @ts-ignore - file-menu event exists but is not in the type definitions
37+
this.app.workspace.on("file-menu", (menu: Menu, file: TFile) => {
38+
const fileCache = this.app.metadataCache.getFileCache(file);
39+
const fileNodeType = fileCache?.frontmatter?.nodeTypeId;
40+
41+
if (
42+
!fileNodeType ||
43+
!this.settings.nodeTypes.some(
44+
(nodeType) => nodeType.id === fileNodeType,
45+
)
46+
) {
47+
menu.addItem((menuItem) => {
48+
menuItem.setTitle("Convert into");
49+
menuItem.setIcon("file-type");
50+
51+
// @ts-ignore - setSubmenu is not officially in the API but works
52+
const submenu = menuItem.setSubmenu();
53+
54+
this.settings.nodeTypes.forEach((nodeType) => {
55+
submenu.addItem((item: any) => {
56+
item
57+
.setTitle(nodeType.name)
58+
.setIcon("file-type")
59+
.onClick(() => {
60+
new CreateNodeModal(this.app, {
61+
nodeTypes: this.settings.nodeTypes,
62+
plugin: this,
63+
initialTitle: file.basename,
64+
initialNodeType: nodeType,
65+
onNodeCreate: async (nodeType, title) => {
66+
await convertPageToDiscourseNode({
67+
plugin: this,
68+
file,
69+
nodeType,
70+
});
71+
},
72+
}).open();
73+
});
74+
});
75+
});
76+
});
77+
}
78+
}),
79+
);
80+
3181
this.registerEvent(
3282
this.app.workspace.on("editor-menu", (menu: Menu, editor: Editor) => {
3383
if (!editor.getSelection()) return;

apps/obsidian/src/utils/createNode.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,46 @@ export const createDiscourseNode = async ({
139139

140140
return newFile;
141141
};
142+
143+
export const convertPageToDiscourseNode = async ({
144+
plugin,
145+
file,
146+
nodeType,
147+
}: {
148+
plugin: DiscourseGraphPlugin;
149+
file: TFile;
150+
nodeType: DiscourseNode;
151+
}): Promise<void> => {
152+
try {
153+
const formattedNodeName = formatNodeName(file.basename, nodeType);
154+
if (!formattedNodeName) {
155+
new Notice("Failed to format node name", 3000);
156+
return;
157+
}
158+
159+
const isFilenameValid = checkInvalidChars(formattedNodeName);
160+
if (!isFilenameValid.isValid) {
161+
new Notice(`${isFilenameValid.error}`, 5000);
162+
return;
163+
}
164+
165+
await plugin.app.fileManager.processFrontMatter(file, (fm) => {
166+
fm.nodeTypeId = nodeType.id;
167+
});
168+
169+
const dirPath = file.parent?.path ?? "";
170+
const newPath = dirPath
171+
? `${dirPath}/${formattedNodeName}.md`
172+
: `${formattedNodeName}.md`;
173+
await plugin.app.fileManager.renameFile(file, newPath);
174+
175+
176+
new Notice("Converted page to discourse node", 10000);
177+
} catch (error) {
178+
console.error("Error converting to discourse node:", error);
179+
new Notice(
180+
`Error converting to discourse node: ${error instanceof Error ? error.message : String(error)}`,
181+
5000,
182+
);
183+
}
184+
};

0 commit comments

Comments
 (0)