Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas Drawer #238

Merged
merged 2 commits into from
Mar 20, 2024
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
162 changes: 162 additions & 0 deletions src/components/CanvasDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { useEffect, useMemo, useState } from "react";
import ResizableDrawer from "./ResizableDrawer";
import renderOverlay from "roamjs-components/util/renderOverlay";
import { DiscourseNodeShape } from "./TldrawCanvas";
import { Button, Collapse, Checkbox } from "@blueprintjs/core";
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
import MenuItemSelect from "roamjs-components/components/MenuItemSelect";
import getDiscourseNodes from "../utils/getDiscourseNodes";

export type GroupedShapes = Record<string, DiscourseNodeShape[]>;

type Props = {
groupedShapes: GroupedShapes;
pageUid: string;
};

const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
const [openSections, setOpenSections] = useState<Record<string, boolean>>({});
const [showDuplicates, setShowDuplicates] = useState(false);
const [filterType, setFilterType] = useState("All");
const [filteredShapes, setFilteredShapes] = useState<GroupedShapes>({});

const pageTitle = useMemo(() => getPageTitleByPageUid(pageUid), []);
const noResults = Object.keys(groupedShapes).length === 0;
const typeToTitleMap = useMemo(() => {
const nodes = getDiscourseNodes();
const map: { [key: string]: string } = {};
nodes.forEach((node) => {
map[node.type] = node.text;
});
return map;
}, []);
const shapeTypes = useMemo(() => {
const allTypes = new Set(["All"]);
Object.values(groupedShapes).forEach((shapes) =>
shapes.forEach((shape) =>
allTypes.add(typeToTitleMap[shape.type] || shape.type)
)
);
return Array.from(allTypes);
}, [groupedShapes, typeToTitleMap]);
const hasDuplicates = useMemo(() => {
return Object.values(groupedShapes).some((shapes) => shapes.length > 1);
}, [groupedShapes]);

useEffect(() => {
const filtered = Object.entries(groupedShapes).reduce<GroupedShapes>(
(acc, [uid, shapes]) => {
const filteredShapes = shapes.filter(
(shape) =>
filterType === "All" || typeToTitleMap[shape.type] === filterType
);
if (
filteredShapes.length > 0 &&
(!showDuplicates || filteredShapes.length > 1)
) {
acc[uid] = filteredShapes;
}
return acc;
},
{}
);
setFilteredShapes(filtered);
}, [groupedShapes, showDuplicates, filterType, typeToTitleMap]);

const toggleCollapse = (uid: string) => {
setOpenSections((prevState) => ({
...prevState,
[uid]: !prevState[uid],
}));
};
const moveCameraToShape = (shapeId: string) => {
document.dispatchEvent(
new CustomEvent("roamjs:query-builder:action", {
detail: {
action: "move-camera-to-shape",
shapeId,
},
})
);
};

return (
<div>
<div className="flex items-baseline justify-around my-4">
<MenuItemSelect
onItemSelect={(type) => setFilterType(type)}
activeItem={filterType}
items={shapeTypes}
/>
{hasDuplicates && (
<Checkbox
label="Duplicates"
checked={showDuplicates}
onChange={() => setShowDuplicates(!showDuplicates)}
/>
)}
</div>
{noResults ? (
<div>No nodes found for {pageTitle}</div>
) : (
Object.entries(filteredShapes).map(([uid, shapes]) => {
const title = shapes[0].props.title;
const isExpandable = shapes.length > 1;
return (
<div key={uid} className="mb-2">
<Button
onClick={() => {
if (isExpandable) toggleCollapse(uid);
else moveCameraToShape(shapes[0].id);
}}
icon={
isExpandable
? openSections[uid]
? "chevron-down"
: "chevron-right"
: "dot"
}
alignText="left"
fill
minimal
>
{title}
</Button>
<Collapse isOpen={openSections[uid]}>
<div className="pt-2 " style={{ background: "#eeeeee80" }}>
{shapes.map((shape) => (
<Button
key={shape.id}
icon={"dot"}
onClick={() => moveCameraToShape(shape.id)}
alignText="left"
fill
minimal
className="ml-4"
>
{shape.props.title}
</Button>
))}
</div>
</Collapse>
</div>
);
})
)}
</div>
);
};

const CanvasDrawer = ({
onClose,
...props
}: { onClose: () => void } & Props) => (
<ResizableDrawer onClose={onClose} title={"Canvas Drawer"}>
<CanvasDrawerContent {...props} />
</ResizableDrawer>
);

export const render = (props: Props) =>
renderOverlay({ Overlay: CanvasDrawer, props });

export default CanvasDrawer;
70 changes: 57 additions & 13 deletions src/components/TldrawCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1572,16 +1572,18 @@ const TldrawCanvas = ({ title }: Props) => {
window.roamAlphaAPI.data.removePullWatch(...pullWatchProps);
};
}, [initialState, store]);

// Handle actions (roamjs:query-builder:action)
useEffect(() => {
const actionListener = ((
e: CustomEvent<{
action: string;
uid: string;
val: string;
onRefresh: () => void;
}>
) => {
if (!/canvas/i.test(e.detail.action)) return;
const handleCreateShapeAction = ({
uid,
val,
onRefresh,
}: {
uid: string;
val: string;
onRefresh: () => void;
}) => {
const app = appRef.current;
if (!app) return;
const { x, y } = app.pageCenter;
Expand All @@ -1593,21 +1595,63 @@ const TldrawCanvas = ({ title }: Props) => {
y: lastTime.y + h * 0.05,
}
: { x: x - DEFAULT_WIDTH / 2, y: y - DEFAULT_HEIGHT / 2 };
const nodeType = findDiscourseNode(e.detail.uid, allNodes);
const nodeType = findDiscourseNode(uid, allNodes);
if (nodeType) {
app.createShapes([
{
type: nodeType.type,
id: createShapeId(),
props: {
uid: e.detail.uid,
title: e.detail.val,
uid,
title: val,
},
...position,
},
]);
lastInsertRef.current = position;
e.detail.onRefresh();
onRefresh();
}
};
const handleMoveCameraToShapeAction = ({
shapeId,
}: {
shapeId: TLShapeId;
}) => {
const app = appRef.current;
if (!app) return;
const shape = app.getShapeById(shapeId);
if (!shape) {
return renderToast({
id: "tldraw-warning",
intent: "warning",
content: `Shape not found.`,
});
}
const x = shape?.x || 0;
const y = shape?.y || 0;
app.centerOnPoint(x, y, { duration: 500, easing: (t) => t * t });
app.select(shapeId);
};
const actionListener = ((
e: CustomEvent<{
action: string;
uid?: string;
val?: string;
shapeId?: TLShapeId;
onRefresh?: () => void;
}>
) => {
if (e.detail.action === "move-camera-to-shape") {
if (!e.detail.shapeId) return;
handleMoveCameraToShapeAction({ shapeId: e.detail.shapeId });
}
if (/canvas/i.test(e.detail.action)) {
if (!e.detail.uid || !e.detail.val || !e.detail.onRefresh) return;
handleCreateShapeAction({
uid: e.detail.uid,
val: e.detail.val,
onRefresh: e.detail.onRefresh,
});
}
}) as EventListener;
document.addEventListener("roamjs:query-builder:action", actionListener);
Expand Down
38 changes: 37 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ import initializeDiscourseGraphsMode, {
} from "./discourseGraphsMode";
import getPageMetadata from "./utils/getPageMetadata";
import { render as queryRender } from "./components/QueryDrawer";
import { render as canvasDrawerRender } from "./components/CanvasDrawer";
import createPage from "roamjs-components/writes/createPage";
import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid";
import isLiveBlock from "roamjs-components/queries/isLiveBlock";
import { renderTldrawCanvas } from "./components/TldrawCanvas";
import {
DiscourseNodeShape,
renderTldrawCanvas,
} from "./components/TldrawCanvas";
import { QBGlobalRefs } from "./utils/types";
import localStorageSet from "roamjs-components/util/localStorageSet";
import localStorageGet from "roamjs-components/util/localStorageGet";
Expand All @@ -45,6 +49,9 @@ import getBlockProps, { json } from "./utils/getBlockProps";
import resolveQueryBuilderRef from "./utils/resolveQueryBuilderRef";
import getBlockUidFromTarget from "roamjs-components/dom/getBlockUidFromTarget";
import { render as renderToast } from "roamjs-components/components/Toast";
import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid";
import { TLBaseShape, TLStore } from "@tldraw/tldraw";
import { GroupedShapes } from "./components/CanvasDrawer";

const loadedElsewhere = document.currentScript
? document.currentScript.getAttribute("data-source") === "discourse-graph"
Expand Down Expand Up @@ -595,6 +602,35 @@ svg.rs-svg-container {
).map((b) => ({ uid: b[0][":block/uid"] || "" })),
};

extensionAPI.ui.commandPalette.addCommand({
label: "Open Canvas Drawer",
callback: () => {
const pageUid = getCurrentPageUid();
const props = getBlockProps(pageUid) as Record<string, unknown>;
const rjsqb = props["roamjs-query-builder"] as Record<string, unknown>;
const tldraw = (rjsqb?.tldraw as Record<string, unknown>) || {};
const shapes = Object.values(tldraw).filter((s) => {
const shape = s as TLBaseShape<string, { uid: string }>;
const uid = shape.props?.uid;
return !!uid;
}) as DiscourseNodeShape[];

const groupShapesByUid = (shapes: DiscourseNodeShape[]) => {
const groupedShapes = shapes.reduce((acc: GroupedShapes, shape) => {
const uid = shape.props.uid;
if (!acc[uid]) acc[uid] = [];
acc[uid].push(shape);
return acc;
}, {});

return groupedShapes;
};

const groupedShapes = groupShapesByUid(shapes);
canvasDrawerRender({ groupedShapes, pageUid });
},
});

extensionAPI.ui.commandPalette.addCommand({
label: "Open Query Drawer",
callback: () =>
Expand Down
Loading