diff --git a/interface/src/components/Graph/CommentBox.tsx b/interface/src/components/Graph/CommentBox.tsx index b12c4e57..85570bfb 100644 --- a/interface/src/components/Graph/CommentBox.tsx +++ b/interface/src/components/Graph/CommentBox.tsx @@ -7,6 +7,11 @@ import { createEventListenerMap } from "@solid-primitives/event-listener"; import clsx from "clsx"; import { Show, createMemo, createRoot, createSignal, onMount } from "solid-js"; +import { + commentBoxToClipboardItem, + writeClipboardItemToClipboard, +} from "@macrograph/clipboard"; +import { toast } from "solid-sonner"; import type { SelectionItem } from "../../actions"; import { useInterfaceContext } from "../../context"; import { useGraphContext } from "./Context"; @@ -160,6 +165,18 @@ export function CommentBox(props: Props) { setEditing(true)}> Rename + { + writeClipboardItemToClipboard( + commentBoxToClipboardItem(box(), (node) => + interfaceCtx.nodeSizes.get(node), + ), + ); + toast("Comment Box copied to clipboard"); + }} + > + Copy + { interfaceCtx.execute("deleteGraphItems", { diff --git a/interface/src/components/Graph/Node.tsx b/interface/src/components/Graph/Node.tsx index 55194b69..f89e9ffe 100644 --- a/interface/src/components/Graph/Node.tsx +++ b/interface/src/components/Graph/Node.tsx @@ -18,6 +18,11 @@ import clsx from "clsx"; import * as Solid from "solid-js"; import { createContext, useContext } from "solid-js"; +import { + nodeToClipboardItem, + writeClipboardItemToClipboard, +} from "@macrograph/clipboard"; +import { toast } from "solid-sonner"; import { config } from "../../ConfigDialog"; import { useInterfaceContext } from "../../context"; import { isCtrlEvent } from "../../util"; @@ -312,6 +317,16 @@ export const Node = (props: Props) => { )} */} + { + writeClipboardItemToClipboard( + nodeToClipboardItem(node()), + ); + toast("Node copied to clipboard"); + }} + > + Copy + { interfaceCtx.execute("deleteGraphItems", { diff --git a/interface/src/components/SchemaMenu/index.tsx b/interface/src/components/SchemaMenu/index.tsx index 4016deb3..464f1106 100644 --- a/interface/src/components/SchemaMenu/index.tsx +++ b/interface/src/components/SchemaMenu/index.tsx @@ -42,6 +42,7 @@ interface Props { extra?: { name?: string; defaultProperties?: Record }, ): void | Promise; onCreateCommentBox(): void; + onPasteClipboard(): void; position: XY; suggestion?: { pin: Pin }; onClose?(): void; @@ -220,6 +221,12 @@ export function SchemaMenu(props: Props) { > Add Comment Box + {/* + Paste from Clipboard + */} {(p) => { diff --git a/interface/src/index.tsx b/interface/src/index.tsx index 1b7cac5b..2ba14a2e 100644 --- a/interface/src/index.tsx +++ b/interface/src/index.tsx @@ -444,6 +444,35 @@ function ProjectInterface() { ctx.setState({ status: "idle" }); }); }} + onPasteClipboard={async () => { + const item = deserializeClipboardItem( + await readFromClipboard(), + ); + + if (item.type === "selection") { + const graph = currentGraph(); + if (!graph) return; + + const { model, state } = graph; + + const mousePosition = toGraphSpace( + { + x: data().position.x - (rootBounds.left ?? 0), + y: data().position.y - (rootBounds.top ?? 0) + 40, + }, + ctx.graphBounds, + state, + ); + + ctx.execute("pasteGraphSelection", { + graphId: model.id, + mousePosition, + selection: item, + }); + + ctx.setState({ status: "idle" }); + } + }} onSchemaClicked={(schema, targetSuggestion, extra) => { const graphId = data().graph.id; ctx.batch(() => { @@ -675,34 +704,7 @@ function createKeydownShortcuts( case "KeyV": { if (!isCtrlEvent(e)) return; - let item = deserializeClipboardItem(await readFromClipboard()); - - switch (item.type) { - case "node": { - item = { - type: "selection", - origin: item.node.position, - nodes: [item.node], - commentBoxes: [], - connections: [], - }; - - break; - } - case "commentBox": { - item = { - type: "selection", - origin: item.commentBox.position, - nodes: item.nodes, - commentBoxes: [item.commentBox], - connections: item.connections, - }; - - break; - } - default: - break; - } + const item = deserializeClipboardItem(await readFromClipboard()); switch (item.type) { case "selection": { diff --git a/packages/clipboard/src/index.ts b/packages/clipboard/src/index.ts index f15fcdaa..f560c20e 100644 --- a/packages/clipboard/src/index.ts +++ b/packages/clipboard/src/index.ts @@ -64,7 +64,36 @@ export function serializeClipboardItem( } export function deserializeClipboardItem(input: string) { - return v.parse(ClipboardItem, JSON.parse(atob(input))); + let item = v.parse(ClipboardItem, JSON.parse(atob(input))); + + switch (item.type) { + case "node": { + item = { + type: "selection", + origin: item.node.position, + nodes: [item.node], + commentBoxes: [], + connections: [], + }; + + break; + } + case "commentBox": { + item = { + type: "selection", + origin: item.commentBox.position, + nodes: item.nodes, + commentBoxes: [item.commentBox], + connections: item.connections, + }; + + break; + } + default: + break; + } + + return item; } export async function readFromClipboard() { diff --git a/packages/packages/src/http.ts b/packages/packages/src/http.ts index bc6efaab..1d554116 100644 --- a/packages/packages/src/http.ts +++ b/packages/packages/src/http.ts @@ -1,7 +1,7 @@ -import { JSONEnum, type JSONValue, jsToJSON, jsonToJS } from "@macrograph/json"; +import { JSONEnum, jsToJSON, jsonToJS } from "@macrograph/json"; import { Maybe, None } from "@macrograph/option"; -import { type Core, Package, type PropertyDef } from "@macrograph/runtime"; -import { type MapValue, t } from "@macrograph/typesystem"; +import { type Core, Package } from "@macrograph/runtime"; +import { t } from "@macrograph/typesystem"; import { ReactiveMap } from "@solid-primitives/map"; import { writeBinaryFile } from "@tauri-apps/api/fs";