Skip to content

Commit 2a876de

Browse files
[ENG-1368] Drag and drop links from Base and File explorer into canvas (#780)
* feature finished * Update apps/obsidian/src/components/canvas/utils/externalContentHandlers.ts Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 2e151ce commit 2a876de

4 files changed

Lines changed: 306 additions & 27 deletions

File tree

apps/obsidian/src/components/canvas/TldrawViewComponent.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
defaultBindingUtils,
1414
TLPointerEventInfo,
1515
DefaultSharePanel,
16+
type TLDefaultExternalContentHandlerOpts,
17+
type TLUiToast,
1618
} from "tldraw";
1719
import "tldraw/tldraw.css";
1820
import {
@@ -27,7 +29,7 @@ import {
2729
TLDATA_DELIMITER_END,
2830
TLDATA_DELIMITER_START,
2931
} from "~/constants";
30-
import { TFile } from "obsidian";
32+
import { Notice, TFile } from "obsidian";
3133
import { ObsidianTLAssetStore } from "~/components/canvas/stores/assetStore";
3234
import {
3335
createDiscourseNodeUtil,
@@ -52,6 +54,7 @@ import {
5254
openFileInNewLeaf,
5355
resolveDiscourseNodeFile,
5456
} from "./utils/openFileUtils";
57+
import { handleExternalUrlContent } from "./utils/externalContentHandlers";
5558
type TldrawPreviewProps = {
5659
store: TLStore;
5760
file: TFile;
@@ -264,6 +267,28 @@ export const TldrawPreviewComponent = ({
264267
editorRef.current = editor;
265268
setIsEditorMounted(true);
266269

270+
editor.registerExternalContentHandler("url", (externalContent) => {
271+
void handleExternalUrlContent({
272+
editor,
273+
url: externalContent.url,
274+
point: externalContent.point,
275+
plugin,
276+
canvasFile: file,
277+
defaultHandlerOpts: {
278+
toasts: {
279+
addToast: (t: Omit<TLUiToast, "id"> & { id?: string }) => {
280+
new Notice(t.description ?? t.title ?? "Error");
281+
return "";
282+
},
283+
removeToast: () => "",
284+
clearToasts: () => {},
285+
toasts: { get: () => [], update: () => {} },
286+
},
287+
msg: (key?: string) => key ?? "",
288+
} as unknown as TLDefaultExternalContentHandlerOpts,
289+
});
290+
});
291+
267292
editor.on("event", (event) => {
268293
// Handle pointer events
269294
if (event.type !== "pointer") return;

apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { useEffect, useMemo, useState } from "react";
22
import type { TFile } from "obsidian";
33
import type DiscourseGraphPlugin from "~/index";
4-
import { DiscourseNodeShape } from "~/components/canvas/shapes/DiscourseNodeShape";
4+
import {
5+
buildDiscourseNodeShapeRecord,
6+
type DiscourseNodeShape,
7+
} from "~/components/canvas/shapes/DiscourseNodeShape";
58
import {
69
ensureBlockRefForFile,
710
resolveLinkedFileFromSrc,
@@ -229,37 +232,25 @@ export const RelationsPanel = ({
229232

230233
if (existing) return existing;
231234

232-
// Create a new node shape near the selected node
233235
const newId = createShapeId();
234236
const src = `asset:obsidian.blockref.${blockRef}`;
235237
const x = nodeShape.x + nodeShape.props.w + 80;
236238
const y = nodeShape.y;
237-
238239
const nodeTypeId = getFrontmatterForFile(plugin.app, file)
239240
?.nodeTypeId as string;
240241

241-
const created: DiscourseNodeShape = {
242+
const created = buildDiscourseNodeShapeRecord(editor, {
242243
id: newId,
243-
typeName: "shape",
244-
type: "discourse-node",
245244
x,
246245
y,
247-
rotation: 0,
248-
index: editor.getHighestIndexForParent(editor.getCurrentPageId()),
249-
parentId: editor.getCurrentPageId(),
250-
isLocked: false,
251-
opacity: 1,
252-
meta: {},
253246
props: {
254-
w: 200,
255-
h: 100,
256247
src,
257248
title: file.basename,
258-
nodeTypeId: nodeTypeId,
249+
nodeTypeId,
259250
size: "m",
260251
fontFamily: "sans",
261252
},
262-
};
253+
});
263254

264255
editor.createShape(created);
265256
return created;

apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import {
22
BaseBoxShapeUtil,
3+
Editor,
34
HTMLContainer,
45
resizeBox,
56
T,
67
TLBaseShape,
78
TLResizeInfo,
9+
TLShapeId,
810
useEditor,
911
useValue,
1012
DefaultSizeStyle,
@@ -52,6 +54,54 @@ export type DiscourseNodeUtilOptions = {
5254
canvasFile: TFile;
5355
};
5456

57+
/** Default props for new discourse node shapes. Used by getDefaultProps and buildDiscourseNodeShapeRecord. */
58+
export const DEFAULT_DISCOURSE_NODE_PROPS: DiscourseNodeShape["props"] = {
59+
w: 200,
60+
h: 100,
61+
src: null,
62+
title: "",
63+
nodeTypeId: "",
64+
imageSrc: undefined,
65+
size: "s",
66+
fontFamily: "sans",
67+
};
68+
69+
export type BuildDiscourseNodeShapeRecordParams = {
70+
id: TLShapeId;
71+
x: number;
72+
y: number;
73+
props: Partial<DiscourseNodeShape["props"]> &
74+
Pick<DiscourseNodeShape["props"], "src" | "title" | "nodeTypeId">;
75+
};
76+
77+
/**
78+
* Build a full DiscourseNodeShape record for editor.createShape.
79+
* Merges given props with DEFAULT_DISCOURSE_NODE_PROPS.
80+
*/
81+
export const buildDiscourseNodeShapeRecord = (
82+
editor: Editor,
83+
{ id, x, y, props: propsPartial }: BuildDiscourseNodeShapeRecordParams,
84+
): DiscourseNodeShape => {
85+
const props: DiscourseNodeShape["props"] = {
86+
...DEFAULT_DISCOURSE_NODE_PROPS,
87+
...propsPartial,
88+
};
89+
return {
90+
id,
91+
typeName: "shape",
92+
type: "discourse-node",
93+
x,
94+
y,
95+
rotation: 0,
96+
index: editor.getHighestIndexForParent(editor.getCurrentPageId()),
97+
parentId: editor.getCurrentPageId(),
98+
isLocked: false,
99+
opacity: 1,
100+
meta: {},
101+
props,
102+
};
103+
};
104+
55105
export class DiscourseNodeUtil extends BaseBoxShapeUtil<DiscourseNodeShape> {
56106
static type = "discourse-node" as const;
57107
declare options: DiscourseNodeUtilOptions;
@@ -68,16 +118,7 @@ export class DiscourseNodeUtil extends BaseBoxShapeUtil<DiscourseNodeShape> {
68118
};
69119

70120
getDefaultProps(): DiscourseNodeShape["props"] {
71-
return {
72-
w: 200,
73-
h: 100,
74-
src: null,
75-
title: "",
76-
nodeTypeId: "",
77-
imageSrc: undefined,
78-
size: "s",
79-
fontFamily: "sans",
80-
};
121+
return { ...DEFAULT_DISCOURSE_NODE_PROPS };
81122
}
82123

83124
override isAspectRatioLocked = () => false;

0 commit comments

Comments
 (0)