Skip to content

Commit 0673532

Browse files
authored
Implement Discourse Graph Tool and UI Enhancements (#365)
- Introduced DiscourseGraphPanel component to manage node and relation tools. - Added onEnter cursor behavior for DiscourseGraphTool and relation tools. - Updated Tldraw component to include the new DiscourseGraphTool. - Enhanced UI with new styles for the Discourse Graph tool button. - Refactored UI components to integrate the DiscourseGraphPanel for better tool management.
1 parent 7cb2e08 commit 0673532

File tree

6 files changed

+215
-12
lines changed

6 files changed

+215
-12
lines changed

apps/roam/src/components/canvas/DiscourseNodeUtil.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ export const createNodeShapeTools = (
103103
static initial = "idle";
104104
shapeType = n.type;
105105

106+
override onEnter = () => {
107+
this.editor.setCursor({
108+
type: "cross",
109+
rotation: 45,
110+
});
111+
};
112+
106113
override onPointerDown = () => {
107114
const { currentPagePoint } = this.editor.inputs;
108115
const shapeId = createShapeId();

apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export const createAllReferencedNodeTools = (
3333
this.Pointing,
3434
];
3535

36+
override onEnter = () => {
37+
this.editor.setCursor({
38+
type: "cross",
39+
});
40+
};
41+
3642
static Pointing = class extends StateNode {
3743
static override id = "pointing";
3844
shape?: DiscourseRelationShape;
@@ -268,7 +274,7 @@ export const createAllReferencedNodeTools = (
268274
};
269275

270276
override onEnter = () => {
271-
this.editor.setCursor({ type: "cross", rotation: 0 });
277+
this.editor.setCursor({ type: "cross" });
272278
};
273279

274280
override onCancel = () => {
@@ -314,6 +320,12 @@ export const createAllRelationShapeTools = (
314320
this.Pointing,
315321
];
316322

323+
override onEnter = () => {
324+
this.editor.setCursor({
325+
type: "cross",
326+
});
327+
};
328+
317329
static Pointing = class extends StateNode {
318330
static override id = "pointing";
319331
shape?: DiscourseRelationShape;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import React from "react";
2+
import { DiscourseNode } from "~/utils/getDiscourseNodes";
3+
import { DiscourseRelation } from "~/utils/getDiscourseRelations";
4+
import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings";
5+
import { useEditor, useValue } from "tldraw";
6+
import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil";
7+
8+
export type DiscourseGraphPanelProps = {
9+
nodes: DiscourseNode[];
10+
relations: string[];
11+
};
12+
13+
const DiscourseGraphPanel = ({
14+
nodes,
15+
relations,
16+
}: DiscourseGraphPanelProps) => {
17+
const editor = useEditor();
18+
19+
const currentToolId = useValue(
20+
"currentToolId",
21+
() => {
22+
return editor.getCurrentToolId();
23+
},
24+
[editor],
25+
);
26+
27+
const uniqueRelations = [...new Set(relations)];
28+
29+
const currentNodeTool = nodes.find((node) => node.type === currentToolId);
30+
const currentRelationTool = uniqueRelations.find(
31+
(relation) => relation === currentToolId,
32+
);
33+
34+
// If it's a node tool, show only that node
35+
if (currentNodeTool) {
36+
return (
37+
<div className="tlui-layout__top__right">
38+
<div className="tlui-style-panel tlui-style-panel__wrapper">
39+
<div
40+
className="tlui-style-panel__row tlui-button tlui-button__icon flex h-5 cursor-pointer items-center gap-2 px-3"
41+
style={{
42+
justifyContent: "flex-start",
43+
}}
44+
onClick={() => editor.setCurrentTool("discourse-tool")}
45+
>
46+
<span
47+
className="tlui-icon tlui-button__icon mr-2"
48+
style={{
49+
mask: `url("https://cdn.tldraw.com/2.3.0/icons/icon/color.svg") center 100% / 100% no-repeat`,
50+
backgroundColor:
51+
formatHexColor(currentNodeTool.canvasSettings.color) ||
52+
"black",
53+
}}
54+
/>
55+
<span>{currentNodeTool.text}</span>
56+
</div>
57+
</div>
58+
</div>
59+
);
60+
}
61+
62+
// If it's a relation tool, show only that relation
63+
if (currentRelationTool) {
64+
const color = getRelationColor(
65+
currentRelationTool,
66+
uniqueRelations.indexOf(currentRelationTool),
67+
);
68+
return (
69+
<div className="tlui-layout__top__right">
70+
<div className="tlui-style-panel tlui-style-panel__wrapper">
71+
<div
72+
className="tlui-style-panel__row tlui-button tlui-button__icon flex h-5 cursor-pointer items-center gap-2 px-3"
73+
style={{
74+
justifyContent: "flex-start",
75+
}}
76+
onClick={() => editor.setCurrentTool("discourse-tool")}
77+
>
78+
<div
79+
className="tlui-icon tlui-button__icon mr-2"
80+
style={{
81+
color,
82+
mask: `url("https://cdn.tldraw.com/2.3.0/icons/icon/tool-arrow.svg") center 100% / 100% no-repeat`,
83+
}}
84+
></div>
85+
<span>{currentRelationTool}</span>
86+
</div>
87+
</div>
88+
</div>
89+
);
90+
}
91+
92+
return currentToolId === "discourse-tool" ? (
93+
<div className="tlui-layout__top__right">
94+
<div className="tlui-style-panel tlui-style-panel__wrapper">
95+
{/* Nodes Section */}
96+
<>
97+
{nodes.map((n) => (
98+
<div
99+
key={n.type}
100+
className="tlui-style-panel__row tlui-button tlui-button__icon flex h-5 cursor-pointer items-center gap-2 px-3"
101+
style={{
102+
justifyContent: "flex-start",
103+
}}
104+
onClick={() => editor.setCurrentTool(n.type)}
105+
>
106+
<span
107+
className="tlui-icon tlui-button__icon mr-2"
108+
style={{
109+
mask: `url("https://cdn.tldraw.com/2.3.0/icons/icon/color.svg") center 100% / 100% no-repeat`,
110+
backgroundColor:
111+
formatHexColor(n.canvasSettings.color) || "black",
112+
}}
113+
/>
114+
<span>{n.text}</span>
115+
</div>
116+
))}
117+
</>
118+
119+
{/* Relations Section */}
120+
{uniqueRelations.length > 0 && (
121+
<>
122+
{uniqueRelations.map((relation, index) => {
123+
const color = getRelationColor(relation, index);
124+
return (
125+
<div
126+
key={relation}
127+
className="tlui-style-panel__row tlui-button tlui-button__icon flex h-5 cursor-pointer items-center gap-2 px-3"
128+
style={{
129+
justifyContent: "flex-start",
130+
}}
131+
onClick={() => editor.setCurrentTool(relation)}
132+
>
133+
<div
134+
className="tlui-icon tlui-button__icon mr-2"
135+
style={{
136+
color,
137+
mask: `url("https://cdn.tldraw.com/2.3.0/icons/icon/tool-arrow.svg") center 100% / 100% no-repeat`,
138+
}}
139+
></div>
140+
<span>{relation}</span>
141+
</div>
142+
);
143+
})}
144+
</>
145+
)}
146+
</div>
147+
</div>
148+
) : null;
149+
};
150+
151+
export default DiscourseGraphPanel;

apps/roam/src/components/canvas/Tldraw.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
12
import React, { useState, useRef, useMemo, useEffect } from "react";
23
import ExtensionApiContextProvider, {
34
useExtensionAPI,
@@ -39,6 +40,7 @@ import {
3940
registerDefaultSideEffects,
4041
defaultEditorAssetUrls,
4142
usePreloadAssets,
43+
StateNode,
4244
} from "tldraw";
4345
import "tldraw/tldraw.css";
4446
import tldrawStyles from "./tldrawStyles";
@@ -317,12 +319,17 @@ const TldrawCanvas = ({ title }: { title: string }) => {
317319
];
318320

319321
// TOOLS
322+
const discourseGraphTool = class DiscourseGraphTool extends StateNode {
323+
static override id = "discourse-tool";
324+
static override initial = "idle";
325+
};
320326
const discourseNodeTools = createNodeShapeTools(allNodes);
321327
const discourseRelationTools = createAllRelationShapeTools(allRelationNames);
322328
const referencedNodeTools = createAllReferencedNodeTools(
323329
allAddReferencedNodeByAction,
324330
);
325331
const customTools = [
332+
discourseGraphTool,
326333
...discourseNodeTools,
327334
...discourseRelationTools,
328335
...referencedNodeTools,

apps/roam/src/components/canvas/tldrawStyles.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,15 @@ export default `
6565
white-space: nowrap;
6666
font-family: "Inter", sans-serif;
6767
} */
68+
69+
/* Discourse Graph ToolButton */
70+
button[data-value="discourse-tool"] div::before {
71+
content: "";
72+
display: inline-block;
73+
width: 18px;
74+
height: 18px;
75+
background-image: url("data:image/svg+xml,%3Csvg width='256' height='264' viewBox='0 0 256 264' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M156.705 252.012C140.72 267.995 114.803 267.995 98.8183 252.012L11.9887 165.182C-3.99622 149.197 -3.99622 123.28 11.9886 107.296L55.4035 63.8807C63.3959 55.8881 76.3541 55.8881 84.3467 63.8807C92.3391 71.8731 92.3391 84.8313 84.3467 92.8239L69.8751 107.296C53.8901 123.28 53.8901 149.197 69.8751 165.182L113.29 208.596C121.282 216.589 134.241 216.589 142.233 208.596C150.225 200.604 150.225 187.646 142.233 179.653L127.761 165.182C111.777 149.197 111.777 123.28 127.761 107.296C143.746 91.3105 143.746 65.3939 127.761 49.4091L113.29 34.9375C105.297 26.9452 105.297 13.9868 113.29 5.99432C121.282 -1.99811 134.241 -1.99811 142.233 5.99434L243.533 107.296C259.519 123.28 259.519 149.197 243.533 165.182L156.705 252.012ZM200.119 121.767C192.127 113.775 179.168 113.775 171.176 121.767C163.184 129.76 163.184 142.718 171.176 150.71C179.168 158.703 192.127 158.703 200.119 150.71C208.112 142.718 208.112 129.76 200.119 121.767Z' fill='%23000000'/%3E%3C/svg%3E");
76+
background-size: contain;
77+
background-repeat: no-repeat;
78+
}
6879
`;

apps/roam/src/components/canvas/uiOverrides.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { openCanvasDrawer } from "./CanvasDrawer";
4444
import { AddReferencedNodeType } from "./DiscourseRelationShape/DiscourseRelationTool";
4545
import { dispatchToastEvent } from "./ToastListener";
4646
import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil";
47+
import DiscourseGraphPanel from "./DiscourseToolPanel";
4748

4849
const convertToDiscourseNode = async ({
4950
text,
@@ -224,8 +225,13 @@ export const createUiComponents = ({
224225
const tools = useTools();
225226
return (
226227
<DefaultToolbar {...props}>
228+
<TldrawUiMenuItem
229+
key="discourse-tool"
230+
{...tools["discourse-tool"]}
231+
isSelected={useIsToolSelected(tools["discourse-tool"])}
232+
/>
227233
<DefaultToolbarContent />
228-
{allNodes.map((n) => (
234+
{/* {allNodes.map((n) => (
229235
<TldrawUiMenuItem
230236
key={n.type}
231237
{...tools[n.type]}
@@ -248,7 +254,7 @@ export const createUiComponents = ({
248254
// eslint-disable-next-line react-hooks/rules-of-hooks
249255
isSelected={useIsToolSelected(tools[action])}
250256
/>
251-
))}
257+
))} */}
252258
</DefaultToolbar>
253259
);
254260
},
@@ -260,13 +266,6 @@ export const createUiComponents = ({
260266
{allNodes.map((n) => (
261267
<TldrawUiMenuItem key={n.type} {...tools[n.type]} />
262268
))}
263-
{/* {allRelationNames.map((name) => (
264-
<TldrawUiMenuItem {...tools[name]} />
265-
))}
266-
{allAddRefNodeActions.map((action) => (
267-
<TldrawUiMenuItem {...tools[action]} />
268-
))}
269-
*/}
270269
<TldrawUiMenuItem {...actions["toggle-full-screen"]} />
271270
<TldrawUiMenuItem {...actions["convert-to"]} />
272271
<DefaultKeyboardShortcutsDialogContent />
@@ -300,6 +299,13 @@ export const createUiComponents = ({
300299
</DefaultMainMenu>
301300
);
302301
},
302+
SharePanel: () => {
303+
const allRelations = [
304+
...allRelationNames,
305+
...allAddReferencedNodeActions,
306+
];
307+
return <DiscourseGraphPanel nodes={allNodes} relations={allRelations} />;
308+
},
303309
};
304310
};
305311
export const createUiOverrides = ({
@@ -320,6 +326,16 @@ export const createUiOverrides = ({
320326
setConvertToDialogOpen: (open: boolean) => void;
321327
}): TLUiOverrides => ({
322328
tools: (editor, tools) => {
329+
tools["discourse-tool"] = {
330+
id: "discourse-tool",
331+
icon: "none",
332+
label: "tool.discourse-tool" as TLUiTranslationKey,
333+
kbd: "",
334+
readonlyOk: true,
335+
onSelect: () => {
336+
editor.setCurrentTool("discourse-tool");
337+
},
338+
};
323339
allNodes.forEach((node, index) => {
324340
const nodeId = node.type;
325341
tools[nodeId] = {
@@ -418,15 +434,13 @@ export const createUiOverrides = ({
418434
addToast({ title: "Copied as PNG" });
419435
},
420436
};
421-
422437
// Disable print keyboard binding to prevent conflict with command palette
423438
if (originalPrintAction) {
424439
actions["print"] = {
425440
...originalPrintAction,
426441
kbd: "", // Remove keyboard shortcut to prevent conflict
427442
};
428443
}
429-
430444
return actions;
431445
},
432446
translations: {
@@ -442,6 +456,7 @@ export const createUiOverrides = ({
442456
// allAddRefNodeActions.map((name) => [`shape.referenced.${name}`, name])
443457
// ),
444458
"action.toggle-full-screen": "Toggle Full Screen",
459+
"tool.discourse-tool": "Discourse Graph",
445460
// "action.convert-to": "Convert to",
446461
// ...Object.fromEntries(
447462
// allNodes.map((node) => [

0 commit comments

Comments
 (0)