Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f963748
[ENG-495] Tldraw obsidian setup (#285)
trangdoan982 Jul 22, 2025
2675eff
[ENG-598] Data persistence for tldraw (#303)
trangdoan982 Aug 4, 2025
c7f90a8
[ENG-624] TLDraw Obsidian asset store (#326)
trangdoan982 Aug 15, 2025
22da289
correct styles
trangdoan982 Aug 20, 2025
d087768
[ENG-599] Discourse node shape (#341)
trangdoan982 Aug 27, 2025
21f2a8b
[ENG-604] Create node flow (#387)
trangdoan982 Sep 1, 2025
41d114a
[ENG-658] Add existing node flow (#389)
trangdoan982 Sep 1, 2025
3fbd11a
[ENG-601] Create settings for canvas and attachment default folder (#…
trangdoan982 Sep 1, 2025
a291c54
ENG-600: Discourse Relation shape definition (#408)
trangdoan982 Sep 17, 2025
f919b83
ENG-605: Add new relation flow (#411)
trangdoan982 Sep 25, 2025
58605d9
[ENG-603] Add existing relations (#412)
trangdoan982 Sep 25, 2025
cf0587e
[ENG-844] Add color setting for relation types (#429)
trangdoan982 Sep 25, 2025
308d4bc
fix icons
trangdoan982 Sep 29, 2025
d6290bf
ENG-812 Update of database cli tools (#401)
maparent Sep 6, 2025
2120d8b
[ENG-495] Tldraw obsidian setup (#285)
trangdoan982 Jul 22, 2025
e86aacf
[ENG-598] Data persistence for tldraw (#303)
trangdoan982 Aug 4, 2025
f9b40eb
switch to pnpm
trangdoan982 Oct 3, 2025
3746e53
delete wrong rebase file
trangdoan982 Oct 9, 2025
c78913f
fix pnpm lock
trangdoan982 Oct 9, 2025
d4341e6
fix type checks
trangdoan982 Oct 9, 2025
dffbc58
address all the PR comments
trangdoan982 Oct 10, 2025
3c6b48f
delete redundant files
trangdoan982 Oct 10, 2025
19cb634
fix types
trangdoan982 Oct 10, 2025
60ba994
shift click to open file on the right split (#485)
trangdoan982 Oct 13, 2025
e876042
address PR comments
trangdoan982 Oct 14, 2025
9183987
final lint cleanup
trangdoan982 Oct 16, 2025
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
4 changes: 3 additions & 1 deletion apps/obsidian/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"nanoid": "^4.0.2",
"react": "catalog:obsidian",
"react-dom": "catalog:obsidian",
"tailwindcss-animate": "^1.0.7"
"date-fns": "^4.1.0",
"tailwindcss-animate": "^1.0.7",
"tldraw": "3.14.2"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type DiscourseGraphPlugin from "../index";
import { BulkImportCandidate, BulkImportPattern } from "~/types";
import { QueryEngine } from "~/services/QueryEngine";
import { TFile } from "obsidian";
import { getNodeTypeById } from "~/utils/typeUtils";

type BulkImportModalProps = {
plugin: DiscourseGraphPlugin;
Expand Down Expand Up @@ -216,9 +217,7 @@ const BulkImportContent = ({ plugin, onClose }: BulkImportModalProps) => {
<div className="mb-6 h-80 overflow-y-auto rounded border p-4">
<div className="flex flex-col gap-4">
{patterns.map((pattern, index) => {
const nodeType = plugin.settings.nodeTypes.find(
(n) => n.id === pattern.nodeTypeId,
);
const nodeType = getNodeTypeById(plugin, pattern.nodeTypeId);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convenient fix

return (
<div key={pattern.nodeTypeId} className="rounded border p-3">
<div
Expand Down
5 changes: 2 additions & 3 deletions apps/obsidian/src/components/DiscourseContextView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getDiscourseNodeFormatExpression } from "~/utils/getDiscourseNodeFormat
import { RelationshipSection } from "~/components/RelationshipSection";
import { VIEW_TYPE_DISCOURSE_CONTEXT } from "~/types";
import { PluginProvider, usePlugin } from "~/components/PluginContext";
import { getNodeTypeById } from "~/utils/typeUtils";

type DiscourseContextProps = {
activeFile: TFile | null;
Expand Down Expand Up @@ -39,9 +40,7 @@ const DiscourseContext = ({ activeFile }: DiscourseContextProps) => {
return <div>Not a discourse node (no nodeTypeId)</div>;
}

const nodeType = plugin.settings.nodeTypes.find(
(type) => type.id === frontmatter.nodeTypeId,
);
const nodeType = getNodeTypeById(plugin, frontmatter.nodeTypeId as string);

if (!nodeType) {
return <div>Unknown node type: {frontmatter.nodeTypeId}</div>;
Expand Down
58 changes: 57 additions & 1 deletion apps/obsidian/src/components/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ const GeneralSettings = () => {
const [nodesFolderPath, setNodesFolderPath] = useState(
plugin.settings.nodesFolderPath,
);
const [canvasFolderPath, setCanvasFolderPath] = useState<string>(
plugin.settings.canvasFolderPath,
);
const [canvasAttachmentsFolderPath, setCanvasAttachmentsFolderPath] =
useState<string>(plugin.settings.canvasAttachmentsFolderPath);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

const handleToggleChange = (newValue: boolean) => {
Expand All @@ -81,9 +86,24 @@ const GeneralSettings = () => {
setHasUnsavedChanges(true);
}, []);

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

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

const handleSave = async () => {
plugin.settings.showIdsInFrontmatter = showIdsInFrontmatter;
plugin.settings.nodesFolderPath = nodesFolderPath;
plugin.settings.canvasFolderPath = canvasFolderPath;
plugin.settings.canvasAttachmentsFolderPath = canvasAttachmentsFolderPath;
await plugin.saveSettings();
new Notice("General settings saved");
setHasUnsavedChanges(false);
Expand Down Expand Up @@ -126,9 +146,45 @@ const GeneralSettings = () => {
</div>
</div>

<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">Canvas folder path</div>
<div className="setting-item-description">
Folder where new Discourse Graph canvases will be created. Default:
&quot;Discourse Canvas&quot;.
</div>
</div>
<div className="setting-item-control">
<FolderSuggestInput
value={canvasFolderPath}
onChange={handleCanvasFolderPathChange}
placeholder="Example: Discourse Canvas"
/>
</div>
</div>

<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">
Canvas attachments folder path
</div>
<div className="setting-item-description">
Folder where attachments for canvases are stored. Default:
&quot;attachments&quot;.
</div>
</div>
<div className="setting-item-control">
<FolderSuggestInput
value={canvasAttachmentsFolderPath}
onChange={handleCanvasAttachmentsFolderPathChange}
placeholder="Example: attachments"
/>
</div>
</div>

<div className="setting-item">
<button
onClick={handleSave}
onClick={() => void handleSave()}
className={hasUnsavedChanges ? "mod-cta" : ""}
disabled={!hasUnsavedChanges}
>
Expand Down
14 changes: 5 additions & 9 deletions apps/obsidian/src/components/RelationshipSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SearchBar from "./SearchBar";
import { DiscourseNode } from "~/types";
import DropdownSelect from "./DropdownSelect";
import { usePlugin } from "./PluginContext";
import { getNodeTypeById } from "~/utils/typeUtils";

type RelationTypeOption = {
id: string;
Expand Down Expand Up @@ -60,12 +61,7 @@ const AddRelationship = ({ activeFile }: RelationshipSectionProps) => {
);

const compatibleNodeTypes = compatibleNodeTypeIds
.map((id) => {
const nodeType = plugin.settings.nodeTypes.find(
(type) => type.id === id,
);
return nodeType;
})
.map((id) => getNodeTypeById(plugin, id))
.filter(Boolean) as DiscourseNode[];

setCompatibleNodeTypes(compatibleNodeTypes);
Expand Down Expand Up @@ -152,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
21 changes: 7 additions & 14 deletions apps/obsidian/src/components/RelationshipSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { useState } from "react";
import {
DiscourseRelation,
DiscourseNode,
DiscourseRelationType,
} from "~/types";
import { DiscourseRelation, DiscourseRelationType } from "~/types";
import { Notice } from "obsidian";
import { usePlugin } from "./PluginContext";
import { ConfirmationModal } from "./ConfirmationModal";
import { getNodeTypeById } from "~/utils/typeUtils";

const RelationshipSettings = () => {
const plugin = usePlugin();
Expand All @@ -15,10 +12,6 @@ const RelationshipSettings = () => {
>(() => plugin.settings.discourseRelations ?? []);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

const findNodeById = (id: string): DiscourseNode | undefined => {
return plugin.settings.nodeTypes.find((node) => node.id === id);
};

const findRelationTypeById = (
id: string,
): DiscourseRelationType | undefined => {
Expand Down Expand Up @@ -72,8 +65,8 @@ const RelationshipSettings = () => {
relation.destinationId &&
relation.relationshipTypeId
) {
const sourceNode = findNodeById(relation.sourceId);
const targetNode = findNodeById(relation.destinationId);
const sourceNode = getNodeTypeById(plugin, relation.sourceId);
const targetNode = getNodeTypeById(plugin, relation.destinationId);
const relationType = findRelationTypeById(relation.relationshipTypeId);

if (sourceNode && targetNode && relationType) {
Expand Down Expand Up @@ -202,7 +195,7 @@ const RelationshipSettings = () => {
<div className="text-normal mt-2 p-2">
<div className="flex items-center justify-between">
<div className="flex-1">
{findNodeById(relation.sourceId)?.name ||
{getNodeTypeById(plugin, relation.sourceId)?.name ||
"Unknown Node"}
</div>

Expand All @@ -224,8 +217,8 @@ const RelationshipSettings = () => {
</div>

<div className="flex-1 text-right">
{findNodeById(relation.destinationId)?.name ||
"Unknown Node"}
{getNodeTypeById(plugin, relation.destinationId)
?.name || "Unknown Node"}
</div>
</div>
</div>
Expand Down
24 changes: 20 additions & 4 deletions apps/obsidian/src/components/RelationshipTypeSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@ const RelationshipTypeSettings = () => {
);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

const handleRelationTypeChange = async (
const handleRelationTypeChange = (
index: number,
field: keyof DiscourseRelationType,
value: string,
): Promise<void> => {
): void => {
const updatedRelationTypes = [...relationTypes];
if (!updatedRelationTypes[index]) {
const newId = generateUid("rel");
updatedRelationTypes[index] = { id: newId, label: "", complement: "" };
updatedRelationTypes[index] = {
id: newId,
label: "",
complement: "",
color: "#000000",
};
}

updatedRelationTypes[index][field] = value;
Expand All @@ -37,6 +42,7 @@ const RelationshipTypeSettings = () => {
id: newId,
label: "",
complement: "",
color: "#000000",
},
];
setRelationTypes(updatedRelationTypes);
Expand All @@ -47,6 +53,7 @@ const RelationshipTypeSettings = () => {
const relationType = relationTypes[index] || {
label: "Unnamed",
complement: "",
color: "#000000",
};
const modal = new ConfirmationModal(plugin.app, {
title: "Delete Relation Type",
Expand Down Expand Up @@ -77,7 +84,7 @@ const RelationshipTypeSettings = () => {

const handleSave = async (): Promise<void> => {
for (const relType of relationTypes) {
if (!relType.id || !relType.label || !relType.complement) {
if (!relType.id || !relType.label || !relType.complement || !relType.color) {
new Notice("All fields are required for relation types.");
return;
}
Expand Down Expand Up @@ -125,6 +132,15 @@ const RelationshipTypeSettings = () => {
}
className="flex-1"
/>
<input
type="color"
value={relationType.color}
onChange={(e) =>
handleRelationTypeChange(index, "color", e.target.value)
}
className="w-12 h-8 rounded border"
title="Relation color"
/>
<button
onClick={() => confirmDeleteRelationType(index)}
className="mod-warning p-2"
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
Loading