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
6 changes: 3 additions & 3 deletions apps/obsidian/src/components/RelationshipSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,12 @@ const AddRelationship = ({ activeFile }: RelationshipSectionProps) => {
const nodeTypeIdsToSearch = compatibleNodeTypes.map((type) => type.id);

const results =
await queryEngineRef.current?.searchCompatibleNodeByTitle(
await queryEngineRef.current.searchCompatibleNodeByTitle({
query,
nodeTypeIdsToSearch,
compatibleNodeTypeIds: nodeTypeIdsToSearch,
activeFile,
selectedRelationType,
);
});

if (results.length === 0 && query.length >= 2) {
setSearchError(
Expand Down
55 changes: 33 additions & 22 deletions apps/obsidian/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,37 +74,50 @@ const SearchBar = <T,>({
renderItem,
asyncSearch,
disabled = false,
className,
}: {
onSelect: (item: T | null) => void;
placeholder?: string;
getItemText: (item: T) => string;
renderItem?: (item: T, el: HTMLElement) => void;
asyncSearch: (query: string) => Promise<T[]>;
disabled?: boolean;
className?: string;
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const [selected, setSelected] = useState<T | null>(null);
const plugin = usePlugin();
const app = plugin.app;
const asyncSearchRef = useRef(asyncSearch);

useEffect(() => {
if (inputRef.current && app) {
const suggest = new GenericSuggest(
app,
inputRef.current,
(item) => {
setSelected(item);
onSelect(item);
},
{
getItemText,
renderItem,
asyncSearch,
asyncSearchRef.current = asyncSearch;
}, [asyncSearch]);

useEffect(() => {
if (!inputRef.current || !app) return;
const suggest = new GenericSuggest<T>(
app,
inputRef.current,
(item) => {
setSelected(item);
onSelect(item);
inputRef.current?.blur();
},
{
getItemText: (item: T) => getItemText(item),
renderItem: (item: T, el: HTMLElement) => {
if (renderItem) {
renderItem(item, el);
return;
}
el.setText(getItemText(item));
},
);
return () => suggest.close();
}
}, [onSelect, app, getItemText, renderItem, asyncSearch]);
asyncSearch: (query: string) => asyncSearchRef.current(query),
},
);
return () => suggest.close();
}, [app, getItemText, renderItem, onSelect, asyncSearch]);

const clearSelection = useCallback(() => {
if (inputRef.current) {
Expand All @@ -115,23 +128,21 @@ const SearchBar = <T,>({
}, [onSelect]);

return (
<div className="relative">
<div className="relative flex items-center">
<input
ref={inputRef}
type="text"
placeholder={placeholder || "Search..."}
className={`w-full p-2 ${
selected ? "pr-9" : ""
} border-modifier-border rounded border bg-${
className={`border-modifier-border flex-1 rounded border p-2 pr-8 bg-${
selected || disabled ? "secondary" : "primary"
} ${disabled ? "cursor-not-allowed opacity-70" : "cursor-text"}`}
} ${disabled ? "cursor-not-allowed opacity-70" : "cursor-text"} ${className}`}
readOnly={!!selected || disabled}
disabled={disabled}
/>
{selected && !disabled && (
<button
onClick={clearSelection}
className="text-muted absolute right-1 top-1/2 -translate-y-1/2 cursor-pointer rounded border-0 bg-transparent p-1"
className="text-muted hover:text-normal absolute right-2 flex h-4 w-4 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-xs"
aria-label="Clear selection"
>
Expand Down
54 changes: 30 additions & 24 deletions apps/obsidian/src/components/canvas/DiscourseNodePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { openCreateDiscourseNodeAt } from "./utils/nodeCreationFlow";
import { getNodeTypeById } from "~/utils/utils";
import { useEffect } from "react";
import { setDiscourseNodeToolContext } from "./DiscourseNodeTool";
import { ExistingNodeSearch } from "./ExistingNodeSearch";

export const DiscourseNodePanel = ({
plugin,
Expand All @@ -26,8 +27,8 @@ export const DiscourseNodePanel = ({
const rDraggingImage = React.useRef<HTMLDivElement>(null);
const didDragRef = React.useRef(false);
const [focusedNodeTypeId, setFocusedNodeTypeId] = React.useState<
string | null
>(null);
string | undefined
>(undefined);

type DragState =
| { name: "idle" }
Expand Down Expand Up @@ -188,7 +189,7 @@ export const DiscourseNodePanel = ({
useEffect(() => {
if (!focusedNodeTypeId) return;
const exists = !!getNodeTypeById(plugin, focusedNodeTypeId);
if (!exists) setFocusedNodeTypeId(null);
if (!exists) setFocusedNodeTypeId(undefined);
}, [focusedNodeTypeId, plugin]);

const focusedNodeType = focusedNodeTypeId
Expand All @@ -208,7 +209,7 @@ export const DiscourseNodePanel = ({
const handleItemClick = (id: string) => {
if (didDragRef.current) return;
if (focusedNodeTypeId) {
setFocusedNodeTypeId(null);
setFocusedNodeTypeId(undefined);
return;
}
setFocusedNodeTypeId(id);
Expand All @@ -217,26 +218,31 @@ export const DiscourseNodePanel = ({
};

return (
<div className="tlui-layout__top__right">
<div
className="tlui-style-panel tlui-style-panel__wrapper"
ref={rPanelContainer}
>
<div className="flex flex-col">
{displayNodeTypes.map((nodeType) => (
<NodeTypeButton
key={nodeType.id}
nodeType={nodeType}
handlers={handlers}
didDragRef={didDragRef}
onClickNoDrag={() => handleItemClick(nodeType.id)}
/>
))}
</div>
<div ref={rDraggingImage}>
{state.name === "dragging"
? (getNodeTypeById(plugin, state.nodeTypeId)?.name ?? "")
: null}
<div className="flex flex-row">
<ExistingNodeSearch
plugin={plugin}
canvasFile={canvasFile}
getEditor={() => editor}
nodeTypeId={focusedNodeTypeId}
/>
<div className="tlui-layout__top__right">
<div className="tlui-style-panel tlui-style-panel__wrapper" ref={rPanelContainer}>
<div className="flex flex-col">
{displayNodeTypes.map((nodeType) => (
<NodeTypeButton
key={nodeType.id}
nodeType={nodeType}
handlers={handlers}
didDragRef={didDragRef}
onClickNoDrag={() => handleItemClick(nodeType.id)}
/>
))}
</div>
<div ref={rDraggingImage}>
{state.name === "dragging"
? (getNodeTypeById(plugin, state.nodeTypeId)?.name ?? "")
: null}
</div>
</div>
</div>
</div>
Expand Down
89 changes: 89 additions & 0 deletions apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useCallback, useState } from "react";
import { TFile } from "obsidian";
import { createShapeId, Editor } from "tldraw";
import DiscourseGraphPlugin from "~/index";
import { QueryEngine } from "~/services/QueryEngine";
import SearchBar from "~/components/SearchBar";
import { addWikilinkBlockrefForFile } from "./stores/assetStore";
import { getFrontmatterForFile } from "./shapes/discourseNodeShapeUtils";

export const ExistingNodeSearch = ({
plugin,
canvasFile,
getEditor,
nodeTypeId,
}: {
plugin: DiscourseGraphPlugin;
canvasFile: TFile;
getEditor: () => Editor | null;
nodeTypeId?: string;
}) => {
const [engine] = useState(() => new QueryEngine(plugin.app));

const search = useCallback(
async (query: string) => {
return await engine.searchDiscourseNodesByTitle(query, nodeTypeId);
},
[engine, nodeTypeId],
);

const getItemText = useCallback((file: TFile) => file.basename, []);

const renderItem = useCallback((file: TFile, el: HTMLElement) => {
const wrapper = el.createEl("div", {
cls: "file-suggestion",
attr: { style: "display:flex; align-items:center; gap:8px;" },
});
wrapper.createEl("div", { text: "📄" });
wrapper.createEl("div", { text: file.basename });
}, []);

const handleSelect = useCallback(
(file: TFile | null) => {
const editor = getEditor();
if (!file || !editor) return;
void (async () => {
const pagePoint = editor.getViewportScreenCenter();
try {
const src = await addWikilinkBlockrefForFile({
app: plugin.app,
canvasFile,
linkedFile: file,
});
const id = createShapeId();
editor.createShape({
id,
type: "discourse-node",
x: pagePoint.x - Math.random() * 100,
y: pagePoint.y - Math.random() * 100,
props: {
w: 200,
h: 100,
src,
title: file.basename,
nodeTypeId: getFrontmatterForFile(plugin.app, file)?.nodeTypeId,
},
});
editor.markHistoryStoppingPoint("add existing discourse node");
editor.setSelectedShapes([id]);
} catch (error) {
console.error("Error in handleSelect:", error);
}
})();
},
[canvasFile, getEditor, plugin.app],
);

return (
<div className="pointer-events-auto rounded-md p-1">
<SearchBar<TFile>
onSelect={handleSelect}
placeholder="Node search"
getItemText={getItemText}
renderItem={renderItem}
asyncSearch={search}
className="!bg-[var(--color-panel)] !text-[var(--color-text)]"
/>
</div>
);
};
14 changes: 8 additions & 6 deletions apps/obsidian/src/components/canvas/TldrawView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from "react";
import DiscourseGraphPlugin from "~/index";
import { processInitialData, TLData } from "~/components/canvas/utils/tldraw";
import { ObsidianTLAssetStore } from "~/components/canvas/stores/assetStore";
import { PluginProvider } from "../PluginContext";

export class TldrawView extends TextFileView {
plugin: DiscourseGraphPlugin;
Expand Down Expand Up @@ -147,12 +148,13 @@ export class TldrawView extends TextFileView {

root.render(
<React.StrictMode>
<TldrawPreviewComponent
store={store}
plugin={this.plugin}
file={this.file}
assetStore={this.assetStore}
/>
<PluginProvider plugin={this.plugin}>
<TldrawPreviewComponent
store={store}
file={this.file}
assetStore={this.assetStore}
/>
</PluginProvider>
</React.StrictMode>,
);
return root;
Expand Down
5 changes: 2 additions & 3 deletions apps/obsidian/src/components/canvas/TldrawViewComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
TLData,
processInitialData,
} from "~/components/canvas/utils/tldraw";
import DiscourseGraphPlugin from "~/index";
import {
DEFAULT_SAVE_DELAY,
TLDATA_DELIMITER_END,
Expand All @@ -32,17 +31,16 @@ import { ObsidianTLAssetStore } from "~/components/canvas/stores/assetStore";
import { DiscourseNodeUtil } from "~/components/canvas/shapes/DiscourseNodeShape";
import { DiscourseNodeTool } from "./DiscourseNodeTool";
import { DiscourseNodePanel } from "./DiscourseNodePanel";
import { usePlugin } from "~/components/PluginContext";

interface TldrawPreviewProps {
store: TLStore;
plugin: DiscourseGraphPlugin;
file: TFile;
assetStore: ObsidianTLAssetStore;
}

export const TldrawPreviewComponent = ({
store,
plugin,
file,
assetStore,
}: TldrawPreviewProps) => {
Expand All @@ -52,6 +50,7 @@ export const TldrawPreviewComponent = ({
const saveTimeoutRef = useRef<NodeJS.Timeout>();
const lastSavedDataRef = useRef<string>("");
const editorRef = useRef<Editor>();
const plugin = usePlugin();

const customShapeUtils = [
...defaultShapeUtils,
Expand Down
Loading
Loading