Skip to content

Commit 7e96f02

Browse files
Tldraw obsidian (#406)
* [ENG-495] Tldraw obsidian setup (#285) * cleaned * sm * address PR comments * [ENG-598] Data persistence for tldraw (#303) * data persistence to the file * error handling * address PR comments * address some PR comments * address other PR comments * address PR comments * [ENG-624] TLDraw Obsidian asset store (#326) * current state * works now * clean up * address PR comments * address PR reviews * cleanup * fix styling issues * address PR comments * correct styles * [ENG-599] Discourse node shape (#341) * current state * works now * clean up * address PR comments * address PR reviews * fix styling issues * latest progress * update new shape * shape defined * address PR comments * sm address PR review * current progress * reorg * address other PR comments * clean * simplify flow * address PR comments * [ENG-604] Create node flow (#387) * eng-604: create node flow * pwd * [ENG-658] Add existing node flow (#389) * eng-658-add-existing-nodes-flow * address PR comments * small changes * [ENG-601] Create settings for canvas and attachment default folder (#338) * add new settings * small add * ENG-600: Discourse Relation shape definition (#408) * ENG-605: Add new relation flow (#411) * [ENG-603] Add existing relations (#412) https://www.loom.com/share/3641f2a642714b0d849262344e8c6ee5?sid=0614c657-e541-4bfd-92df-9b1aa60945b6 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Added a Relations overlay on the canvas that shows a “Relations” button when a discourse node is selected. - Introduced a Relations panel to view and manage relations for the selected node, including adding or removing links, with clear loading/error states. - Overlay appears above the canvas without disrupting existing tools. - Chores - Consolidated relation-type lookup into shared utilities and updated imports. No user-facing changes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> * [ENG-844] Add color setting for relation types (#429) * add color setting * address PR reviews * address PR commens * fix icons * ENG-812 Update of database cli tools (#401) * eng-812 : Update database cli tools: supabase, vercel, cucumber. * newer cucumber constrains node * [ENG-495] Tldraw obsidian setup (#285) * cleaned * sm * address PR comments * [ENG-598] Data persistence for tldraw (#303) * data persistence to the file * error handling * address PR comments * address some PR comments * address other PR comments * address PR comments * switch to pnpm * delete wrong rebase file * fix pnpm lock * fix type checks * address all the PR comments * delete redundant files * fix types * shift click to open file on the right split (#485) * address PR comments * final lint cleanup --------- Co-authored-by: Marc-Antoine Parent <maparent@acm.org>
1 parent 5ef439f commit 7e96f02

42 files changed

Lines changed: 13644 additions & 130 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/obsidian/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"nanoid": "^4.0.2",
4040
"react": "catalog:obsidian",
4141
"react-dom": "catalog:obsidian",
42-
"tailwindcss-animate": "^1.0.7"
42+
"date-fns": "^4.1.0",
43+
"tailwindcss-animate": "^1.0.7",
44+
"tldraw": "3.14.2"
4345
}
4446
}

apps/obsidian/src/components/BulkIdentifyDiscourseNodesModal.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type DiscourseGraphPlugin from "../index";
55
import { BulkImportCandidate, BulkImportPattern } from "~/types";
66
import { QueryEngine } from "~/services/QueryEngine";
77
import { TFile } from "obsidian";
8+
import { getNodeTypeById } from "~/utils/typeUtils";
89

910
type BulkImportModalProps = {
1011
plugin: DiscourseGraphPlugin;
@@ -216,9 +217,7 @@ const BulkImportContent = ({ plugin, onClose }: BulkImportModalProps) => {
216217
<div className="mb-6 h-80 overflow-y-auto rounded border p-4">
217218
<div className="flex flex-col gap-4">
218219
{patterns.map((pattern, index) => {
219-
const nodeType = plugin.settings.nodeTypes.find(
220-
(n) => n.id === pattern.nodeTypeId,
221-
);
220+
const nodeType = getNodeTypeById(plugin, pattern.nodeTypeId);
222221
return (
223222
<div key={pattern.nodeTypeId} className="rounded border p-3">
224223
<div

apps/obsidian/src/components/DiscourseContextView.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getDiscourseNodeFormatExpression } from "~/utils/getDiscourseNodeFormat
55
import { RelationshipSection } from "~/components/RelationshipSection";
66
import { VIEW_TYPE_DISCOURSE_CONTEXT } from "~/types";
77
import { PluginProvider, usePlugin } from "~/components/PluginContext";
8+
import { getNodeTypeById } from "~/utils/typeUtils";
89

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

42-
const nodeType = plugin.settings.nodeTypes.find(
43-
(type) => type.id === frontmatter.nodeTypeId,
44-
);
43+
const nodeType = getNodeTypeById(plugin, frontmatter.nodeTypeId as string);
4544

4645
if (!nodeType) {
4746
return <div>Unknown node type: {frontmatter.nodeTypeId}</div>;

apps/obsidian/src/components/GeneralSettings.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ const GeneralSettings = () => {
6969
const [nodesFolderPath, setNodesFolderPath] = useState(
7070
plugin.settings.nodesFolderPath,
7171
);
72+
const [canvasFolderPath, setCanvasFolderPath] = useState<string>(
73+
plugin.settings.canvasFolderPath,
74+
);
75+
const [canvasAttachmentsFolderPath, setCanvasAttachmentsFolderPath] =
76+
useState<string>(plugin.settings.canvasAttachmentsFolderPath);
7277
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
7378

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

89+
const handleCanvasFolderPathChange = useCallback((newValue: string) => {
90+
setCanvasFolderPath(newValue);
91+
setHasUnsavedChanges(true);
92+
}, []);
93+
94+
const handleCanvasAttachmentsFolderPathChange = useCallback(
95+
(newValue: string) => {
96+
setCanvasAttachmentsFolderPath(newValue);
97+
setHasUnsavedChanges(true);
98+
},
99+
[],
100+
);
101+
84102
const handleSave = async () => {
85103
plugin.settings.showIdsInFrontmatter = showIdsInFrontmatter;
86104
plugin.settings.nodesFolderPath = nodesFolderPath;
105+
plugin.settings.canvasFolderPath = canvasFolderPath;
106+
plugin.settings.canvasAttachmentsFolderPath = canvasAttachmentsFolderPath;
87107
await plugin.saveSettings();
88108
new Notice("General settings saved");
89109
setHasUnsavedChanges(false);
@@ -126,9 +146,45 @@ const GeneralSettings = () => {
126146
</div>
127147
</div>
128148

149+
<div className="setting-item">
150+
<div className="setting-item-info">
151+
<div className="setting-item-name">Canvas folder path</div>
152+
<div className="setting-item-description">
153+
Folder where new Discourse Graph canvases will be created. Default:
154+
&quot;Discourse Canvas&quot;.
155+
</div>
156+
</div>
157+
<div className="setting-item-control">
158+
<FolderSuggestInput
159+
value={canvasFolderPath}
160+
onChange={handleCanvasFolderPathChange}
161+
placeholder="Example: Discourse Canvas"
162+
/>
163+
</div>
164+
</div>
165+
166+
<div className="setting-item">
167+
<div className="setting-item-info">
168+
<div className="setting-item-name">
169+
Canvas attachments folder path
170+
</div>
171+
<div className="setting-item-description">
172+
Folder where attachments for canvases are stored. Default:
173+
&quot;attachments&quot;.
174+
</div>
175+
</div>
176+
<div className="setting-item-control">
177+
<FolderSuggestInput
178+
value={canvasAttachmentsFolderPath}
179+
onChange={handleCanvasAttachmentsFolderPathChange}
180+
placeholder="Example: attachments"
181+
/>
182+
</div>
183+
</div>
184+
129185
<div className="setting-item">
130186
<button
131-
onClick={handleSave}
187+
onClick={() => void handleSave()}
132188
className={hasUnsavedChanges ? "mod-cta" : ""}
133189
disabled={!hasUnsavedChanges}
134190
>

apps/obsidian/src/components/RelationshipSection.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import SearchBar from "./SearchBar";
55
import { DiscourseNode } from "~/types";
66
import DropdownSelect from "./DropdownSelect";
77
import { usePlugin } from "./PluginContext";
8+
import { getNodeTypeById } from "~/utils/typeUtils";
89

910
type RelationTypeOption = {
1011
id: string;
@@ -60,12 +61,7 @@ const AddRelationship = ({ activeFile }: RelationshipSectionProps) => {
6061
);
6162

6263
const compatibleNodeTypes = compatibleNodeTypeIds
63-
.map((id) => {
64-
const nodeType = plugin.settings.nodeTypes.find(
65-
(type) => type.id === id,
66-
);
67-
return nodeType;
68-
})
64+
.map((id) => getNodeTypeById(plugin, id))
6965
.filter(Boolean) as DiscourseNode[];
7066

7167
setCompatibleNodeTypes(compatibleNodeTypes);
@@ -152,12 +148,12 @@ const AddRelationship = ({ activeFile }: RelationshipSectionProps) => {
152148
const nodeTypeIdsToSearch = compatibleNodeTypes.map((type) => type.id);
153149

154150
const results =
155-
await queryEngineRef.current?.searchCompatibleNodeByTitle(
151+
await queryEngineRef.current.searchCompatibleNodeByTitle({
156152
query,
157-
nodeTypeIdsToSearch,
153+
compatibleNodeTypeIds: nodeTypeIdsToSearch,
158154
activeFile,
159155
selectedRelationType,
160-
);
156+
});
161157

162158
if (results.length === 0 && query.length >= 2) {
163159
setSearchError(

apps/obsidian/src/components/RelationshipSettings.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { useState } from "react";
2-
import {
3-
DiscourseRelation,
4-
DiscourseNode,
5-
DiscourseRelationType,
6-
} from "~/types";
2+
import { DiscourseRelation, DiscourseRelationType } from "~/types";
73
import { Notice } from "obsidian";
84
import { usePlugin } from "./PluginContext";
95
import { ConfirmationModal } from "./ConfirmationModal";
6+
import { getNodeTypeById } from "~/utils/typeUtils";
107

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

18-
const findNodeById = (id: string): DiscourseNode | undefined => {
19-
return plugin.settings.nodeTypes.find((node) => node.id === id);
20-
};
21-
2215
const findRelationTypeById = (
2316
id: string,
2417
): DiscourseRelationType | undefined => {
@@ -72,8 +65,8 @@ const RelationshipSettings = () => {
7265
relation.destinationId &&
7366
relation.relationshipTypeId
7467
) {
75-
const sourceNode = findNodeById(relation.sourceId);
76-
const targetNode = findNodeById(relation.destinationId);
68+
const sourceNode = getNodeTypeById(plugin, relation.sourceId);
69+
const targetNode = getNodeTypeById(plugin, relation.destinationId);
7770
const relationType = findRelationTypeById(relation.relationshipTypeId);
7871

7972
if (sourceNode && targetNode && relationType) {
@@ -202,7 +195,7 @@ const RelationshipSettings = () => {
202195
<div className="text-normal mt-2 p-2">
203196
<div className="flex items-center justify-between">
204197
<div className="flex-1">
205-
{findNodeById(relation.sourceId)?.name ||
198+
{getNodeTypeById(plugin, relation.sourceId)?.name ||
206199
"Unknown Node"}
207200
</div>
208201

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

226219
<div className="flex-1 text-right">
227-
{findNodeById(relation.destinationId)?.name ||
228-
"Unknown Node"}
220+
{getNodeTypeById(plugin, relation.destinationId)
221+
?.name || "Unknown Node"}
229222
</div>
230223
</div>
231224
</div>

apps/obsidian/src/components/RelationshipTypeSettings.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@ const RelationshipTypeSettings = () => {
1212
);
1313
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
1414

15-
const handleRelationTypeChange = async (
15+
const handleRelationTypeChange = (
1616
index: number,
1717
field: keyof DiscourseRelationType,
1818
value: string,
19-
): Promise<void> => {
19+
): void => {
2020
const updatedRelationTypes = [...relationTypes];
2121
if (!updatedRelationTypes[index]) {
2222
const newId = generateUid("rel");
23-
updatedRelationTypes[index] = { id: newId, label: "", complement: "" };
23+
updatedRelationTypes[index] = {
24+
id: newId,
25+
label: "",
26+
complement: "",
27+
color: "#000000",
28+
};
2429
}
2530

2631
updatedRelationTypes[index][field] = value;
@@ -37,6 +42,7 @@ const RelationshipTypeSettings = () => {
3742
id: newId,
3843
label: "",
3944
complement: "",
45+
color: "#000000",
4046
},
4147
];
4248
setRelationTypes(updatedRelationTypes);
@@ -47,6 +53,7 @@ const RelationshipTypeSettings = () => {
4753
const relationType = relationTypes[index] || {
4854
label: "Unnamed",
4955
complement: "",
56+
color: "#000000",
5057
};
5158
const modal = new ConfirmationModal(plugin.app, {
5259
title: "Delete Relation Type",
@@ -77,7 +84,7 @@ const RelationshipTypeSettings = () => {
7784

7885
const handleSave = async (): Promise<void> => {
7986
for (const relType of relationTypes) {
80-
if (!relType.id || !relType.label || !relType.complement) {
87+
if (!relType.id || !relType.label || !relType.complement || !relType.color) {
8188
new Notice("All fields are required for relation types.");
8289
return;
8390
}
@@ -125,6 +132,15 @@ const RelationshipTypeSettings = () => {
125132
}
126133
className="flex-1"
127134
/>
135+
<input
136+
type="color"
137+
value={relationType.color}
138+
onChange={(e) =>
139+
handleRelationTypeChange(index, "color", e.target.value)
140+
}
141+
className="w-12 h-8 rounded border"
142+
title="Relation color"
143+
/>
128144
<button
129145
onClick={() => confirmDeleteRelationType(index)}
130146
className="mod-warning p-2"

apps/obsidian/src/components/SearchBar.tsx

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -74,37 +74,50 @@ const SearchBar = <T,>({
7474
renderItem,
7575
asyncSearch,
7676
disabled = false,
77+
className,
7778
}: {
7879
onSelect: (item: T | null) => void;
7980
placeholder?: string;
8081
getItemText: (item: T) => string;
8182
renderItem?: (item: T, el: HTMLElement) => void;
8283
asyncSearch: (query: string) => Promise<T[]>;
8384
disabled?: boolean;
85+
className?: string;
8486
}) => {
8587
const inputRef = useRef<HTMLInputElement>(null);
8688
const [selected, setSelected] = useState<T | null>(null);
8789
const plugin = usePlugin();
8890
const app = plugin.app;
91+
const asyncSearchRef = useRef(asyncSearch);
8992

9093
useEffect(() => {
91-
if (inputRef.current && app) {
92-
const suggest = new GenericSuggest(
93-
app,
94-
inputRef.current,
95-
(item) => {
96-
setSelected(item);
97-
onSelect(item);
98-
},
99-
{
100-
getItemText,
101-
renderItem,
102-
asyncSearch,
94+
asyncSearchRef.current = asyncSearch;
95+
}, [asyncSearch]);
96+
97+
useEffect(() => {
98+
if (!inputRef.current || !app) return;
99+
const suggest = new GenericSuggest<T>(
100+
app,
101+
inputRef.current,
102+
(item) => {
103+
setSelected(item);
104+
onSelect(item);
105+
inputRef.current?.blur();
106+
},
107+
{
108+
getItemText: (item: T) => getItemText(item),
109+
renderItem: (item: T, el: HTMLElement) => {
110+
if (renderItem) {
111+
renderItem(item, el);
112+
return;
113+
}
114+
el.setText(getItemText(item));
103115
},
104-
);
105-
return () => suggest.close();
106-
}
107-
}, [onSelect, app, getItemText, renderItem, asyncSearch]);
116+
asyncSearch: (query: string) => asyncSearchRef.current(query),
117+
},
118+
);
119+
return () => suggest.close();
120+
}, [app, getItemText, renderItem, onSelect, asyncSearch]);
108121

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

117130
return (
118-
<div className="relative">
131+
<div className="relative flex items-center">
119132
<input
120133
ref={inputRef}
121134
type="text"
122135
placeholder={placeholder || "Search..."}
123-
className={`w-full p-2 ${
124-
selected ? "pr-9" : ""
125-
} border-modifier-border rounded border bg-${
136+
className={`border-modifier-border flex-1 rounded border p-2 pr-8 bg-${
126137
selected || disabled ? "secondary" : "primary"
127-
} ${disabled ? "cursor-not-allowed opacity-70" : "cursor-text"}`}
138+
} ${disabled ? "cursor-not-allowed opacity-70" : "cursor-text"} ${className}`}
128139
readOnly={!!selected || disabled}
129140
disabled={disabled}
130141
/>
131142
{selected && !disabled && (
132143
<button
133144
onClick={clearSelection}
134-
className="text-muted absolute right-1 top-1/2 -translate-y-1/2 cursor-pointer rounded border-0 bg-transparent p-1"
145+
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"
135146
aria-label="Clear selection"
136147
>
137148

0 commit comments

Comments
 (0)