Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion apps/obsidian/src/components/GeneralSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,89 @@
import React, { useState } from "react";
import { useState, useCallback } from "react";
import { usePlugin } from "./PluginContext";
import { Notice } from "obsidian";
import SuggestInput from "./SuggestInput";

export const FolderSuggestInput = ({
value,
onChange,
placeholder = "Enter folder path",
className = "",
disabled = false,
}: {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
disabled?: boolean;
}) => {
const plugin = usePlugin();

const getAllFolders = useCallback((): string[] => {
const folders = plugin.app.vault.getAllFolders();
return folders.map((folder) => folder.path).sort();
}, [plugin.app.vault]);

const getFilteredFolders = useCallback(
(query: string): string[] => {
const allFolders = getAllFolders();

if (!query.trim()) {
return allFolders.slice(0, 10);
}

return allFolders
.filter((path) => path.toLowerCase().includes(query.toLowerCase()))
.slice(0, 10);
},
[getAllFolders],
);

const renderFolder = useCallback((folder: string, el: HTMLElement) => {
el.createDiv({
text: folder || "(Root folder)",
cls: "folder-suggestion-item",
});
}, []);

const getDisplayText = useCallback((folder: string) => folder, []);

return (
<SuggestInput<string>
value={value}
onChange={onChange}
getSuggestions={getFilteredFolders}
getDisplayText={getDisplayText}
renderItem={renderFolder}
placeholder={placeholder}
className={className}
disabled={disabled}
/>
);
};

const GeneralSettings = () => {
const plugin = usePlugin();
const [showIdsInFrontmatter, setShowIdsInFrontmatter] = useState(
plugin.settings.showIdsInFrontmatter,
);
const [nodesFolderPath, setNodesFolderPath] = useState(
plugin.settings.nodesFolderPath,
);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

const handleToggleChange = (newValue: boolean) => {
setShowIdsInFrontmatter(newValue);
setHasUnsavedChanges(true);
};

const handleFolderPathChange = useCallback((newValue: string) => {
setNodesFolderPath(newValue);
setHasUnsavedChanges(true);
}, []);

const handleSave = async () => {
plugin.settings.showIdsInFrontmatter = showIdsInFrontmatter;
plugin.settings.nodesFolderPath = nodesFolderPath;
await plugin.saveSettings();
new Notice("General settings saved");
setHasUnsavedChanges(false);
Expand Down Expand Up @@ -43,6 +111,23 @@ const GeneralSettings = () => {
</div>
</div>

<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">Discourse Nodes folder path</div>
<div className="setting-item-description">
Specify the folder where new Discourse Nodes should be created.
Leave empty to create nodes in the root folder.
</div>
</div>
<div className="setting-item-control">
<FolderSuggestInput
value={nodesFolderPath}
onChange={handleFolderPathChange}
placeholder="Discourse Nodes"
/>
</div>
</div>

<div className="setting-item">
<button
onClick={handleSave}
Expand Down
7 changes: 4 additions & 3 deletions apps/obsidian/src/components/NodeTypeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { App, Editor, SuggestModal, TFile, Notice } from "obsidian";
import { DiscourseNode } from "~/types";
import { processTextToDiscourseNode } from "~/utils/createNodeFromSelectedText";
import type DiscourseGraphPlugin from "~/index";

export class NodeTypeModal extends SuggestModal<DiscourseNode> {
constructor(
app: App,
private editor: Editor,
private nodeTypes: DiscourseNode[],
private plugin: DiscourseGraphPlugin,
) {
super(app);
super(plugin.app);
}

getItemText(item: DiscourseNode): string {
Expand All @@ -28,7 +29,7 @@ export class NodeTypeModal extends SuggestModal<DiscourseNode> {

async onChooseSuggestion(nodeType: DiscourseNode) {
await processTextToDiscourseNode({
app: this.app,
plugin: this.plugin,
editor: this.editor,
nodeType,
});
Expand Down
1 change: 1 addition & 0 deletions apps/obsidian/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const Settings = () => {
{activeTab === "nodeTypes" && <NodeTypeSettings />}
{activeTab === "relationTypes" && <RelationshipTypeSettings />}
{activeTab === "relations" && <RelationshipSettings />}
{activeTab === "frontmatter" && <GeneralSettings />}
</div>
);
};
Expand Down
138 changes: 138 additions & 0 deletions apps/obsidian/src/components/SuggestInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { useRef, useEffect, useState, useCallback } from "react";
import { AbstractInputSuggest, App } from "obsidian";
import { usePlugin } from "./PluginContext";

class GenericSuggestInput<T> extends AbstractInputSuggest<T> {
private getSuggestionsFn: (query: string) => T[];
private onSelectCallback: (item: T) => void;
private getDisplayTextFn: (item: T) => string;
private renderItemFn?: (item: T, el: HTMLElement) => void;

constructor(
app: App,
private textInputEl: HTMLInputElement,
config: {
getSuggestions: (query: string) => T[];
onSelect: (item: T) => void;
getDisplayText: (item: T) => string;
renderItem?: (item: T, el: HTMLElement) => void;
},
) {
super(app, textInputEl);
this.getSuggestionsFn = config.getSuggestions;
this.onSelectCallback = config.onSelect;
this.getDisplayTextFn = config.getDisplayText;
this.renderItemFn = config.renderItem;
}

getSuggestions(inputStr: string): T[] {
return this.getSuggestionsFn(inputStr);
}

renderSuggestion(item: T, el: HTMLElement): void {
if (this.renderItemFn) {
this.renderItemFn(item, el);
} else {
el.createDiv({
text: this.getDisplayTextFn(item),
cls: "suggestion-item",
});
}
}

selectSuggestion(item: T, evt: MouseEvent | KeyboardEvent): void {
this.textInputEl.value = this.getDisplayTextFn(item);
this.onSelectCallback(item);
this.close();
}
}

type SuggestInputProps<T> = {
value: string;
onChange: (value: string) => void;
getSuggestions: (query: string) => T[];
getDisplayText: (item: T) => string;
onSelect?: (item: T) => void;
renderItem?: (item: T, el: HTMLElement) => void;
placeholder?: string;
className?: string;
disabled?: boolean;
};

const SuggestInput = <T,>({
value,
onChange,
getSuggestions,
getDisplayText,
onSelect,
renderItem,
placeholder = "Enter value",
className = "",
disabled = false,
}: SuggestInputProps<T>) => {
const plugin = usePlugin();
const inputRef = useRef<HTMLInputElement>(null);
const [suggest, setSuggest] = useState<GenericSuggestInput<T> | null>(null);

const handleSelect = useCallback(
(item: T) => {
const displayText = getDisplayText(item);
onChange(displayText);
onSelect?.(item);
},
[getDisplayText, onChange, onSelect],
);

useEffect(() => {
if (inputRef.current && !suggest && !disabled) {
const genericSuggest = new GenericSuggestInput<T>(
plugin.app,
inputRef.current,
{
getSuggestions,
onSelect: handleSelect,
getDisplayText,
renderItem,
},
);
setSuggest(genericSuggest);

return () => {
genericSuggest.close();
setSuggest(null);
};
}
}, [
plugin.app,
getSuggestions,
getDisplayText,
renderItem,
disabled,
handleSelect,
]);

useEffect(() => {
if (inputRef.current && inputRef.current.value !== value) {
inputRef.current.value = value;
}
}, [value]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};

return (
<input
ref={inputRef}
type="text"
value={value}
onChange={handleChange}
placeholder={placeholder}
className={className}
disabled={disabled}
autoComplete="off"
/>
);
};

export default SuggestInput;
1 change: 1 addition & 0 deletions apps/obsidian/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ export const DEFAULT_SETTINGS: Settings = {
},
],
showIdsInFrontmatter: true,
nodesFolderPath: "Discourse Nodes",
};
2 changes: 1 addition & 1 deletion apps/obsidian/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default class DiscourseGraphPlugin extends Plugin {
.setIcon("file-type")
.onClick(async () => {
await processTextToDiscourseNode({
app: this.app,
plugin: this,
editor,
nodeType,
});
Expand Down
1 change: 1 addition & 0 deletions apps/obsidian/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type Settings = {
discourseRelations: DiscourseRelation[];
relationTypes: DiscourseRelationType[];
showIdsInFrontmatter: boolean;
nodesFolderPath: string;
};

export const VIEW_TYPE_DISCOURSE_CONTEXT = "discourse-context-view";
Loading