From 40872adafefa6a1f8e02c62af6ee94c8afe04a13 Mon Sep 17 00:00:00 2001 From: DrHaid <40427630+DrHaid@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:59:48 +0100 Subject: [PATCH] Implement chopping --- package-lock.json | 5 ++++ src/index.ts | 21 ++++++++++----- src/todo-tree-helper.ts | 58 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 src/todo-tree-helper.ts diff --git a/package-lock.json b/package-lock.json index 8b0401e..2e13a2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -515,6 +516,7 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -582,6 +584,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1370,6 +1373,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1617,6 +1621,7 @@ "integrity": "sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.14.47", "postcss": "^8.4.14", diff --git a/src/index.ts b/src/index.ts index a7a1f8e..025b7e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { StickyNote, Image } from "@mirohq/websdk-types"; +import { getStickiesToChopInTree } from "./todo-tree-helper"; -export async function init() { +export const init = async () => { miro.board.ui.on('icon:click', async () => { await miro.board.ui.openPanel({ url: 'app.html' }); }); @@ -42,19 +43,18 @@ const playAxeAnimation = async ({axe, stickyNote, onChopped}: AxeAnimationArgs) for (let i = 0; i < 3; i++) { axe.rotation = 90; await axe.sync(); - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise(resolve => setTimeout(resolve, 300)); axe.rotation = 0; await axe.sync(); - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise(resolve => setTimeout(resolve, 300)); } onChopped(); } -async function postInfoNotification(message: string) { +const postInfoNotification = async (message: string) => await miro.board.notifications.showInfo(message); -} const handleChoppingAction = async ({items}: {items:StickyNote[]}) => { const axe = await findExistingAxe(); @@ -73,15 +73,22 @@ const handleChoppingAction = async ({items}: {items:StickyNote[]}) => { playAxeAnimation({ axe, stickyNote: target, - onChopped: async () => await miro.board.remove(target) + onChopped: () => removeTree(target) }); }; +const removeTree = async (root: StickyNote) => { + const stickiesToRemove = await getStickiesToChopInTree(root, ["light_green", "red"]); + + for (const item of stickiesToRemove) { + await miro.board.remove(item); + } +}; + export const findExistingAxe = async () => { const items = await miro.board.get({ type: 'image' }); const axe = items.find(item => item.title === 'todo-tree-axe'); return axe; }; - init(); diff --git a/src/todo-tree-helper.ts b/src/todo-tree-helper.ts new file mode 100644 index 0000000..857ad27 --- /dev/null +++ b/src/todo-tree-helper.ts @@ -0,0 +1,58 @@ +import { Connector, Item, StickyNote } from "@mirohq/websdk-types"; + +const getTypedMiroItem = async (id: string, type: T['type']): Promise => { + const item = await miro.board.getById(id); + return item?.type === type ? item as T : undefined; +}; + +interface ConnectorDirection { + parentId?: string; + childId?: string; +} + +const getConnectorDirection = (connector: Connector): ConnectorDirection | undefined => { + const hasEndArrow = connector.style.endStrokeCap !== 'none'; + const hasStartArrow = connector.style.startStrokeCap !== 'none'; + + if (hasEndArrow && !hasStartArrow) { + return { parentId: connector.start?.item, childId: connector.end?.item }; + } + if (hasStartArrow && !hasEndArrow) { + return { parentId: connector.end?.item, childId: connector.start?.item }; + } + return; +}; + +export const getStickiesToChopInTree = async (root: StickyNote, removableColors: string[]): Promise => { + if (!removableColors.includes(root.style.fillColor)) return []; + + const toRemove: StickyNote[] = []; + const visitedIds = new Set(); + + const traverseTree = async (node: StickyNote) => { + if (visitedIds.has(node.id)){ + // prevent infinite loops of circular arrows + return; + } + visitedIds.add(node.id); + + for (const connectorId of node.connectorIds || []) { + const connector = await getTypedMiroItem(connectorId, 'connector'); + if (!connector) continue; + + const direction = getConnectorDirection(connector); + if (!direction || !direction.childId || direction.parentId !== node.id) continue; + + const childSticky = await getTypedMiroItem(direction.childId, 'sticky_note'); + if (childSticky) { + if (removableColors.includes(childSticky.style.fillColor)) { + toRemove.push(childSticky); + } + await traverseTree(childSticky); + } + } + }; + + await traverseTree(root); + return toRemove; +}; \ No newline at end of file