From c538fc09d773f061c161250c879f9a81c37066b7 Mon Sep 17 00:00:00 2001 From: MTRNord Date: Wed, 15 Jan 2025 16:00:40 +0100 Subject: [PATCH] Handle errors, offset paths correctly and allow urls to be exported Change-Id: I516a4bc41796fa6eef96c364185e37db9a36a7da --- src/app/importActions.ts | 79 +++++++++++++++++++++++++++--- src/app/page.tsx | 32 +++++++++--- src/app/selectionFormComponent.tsx | 11 ++++- 3 files changed, 106 insertions(+), 16 deletions(-) diff --git a/src/app/importActions.ts b/src/app/importActions.ts index 9d14df7..695e45a 100644 --- a/src/app/importActions.ts +++ b/src/app/importActions.ts @@ -10,9 +10,11 @@ import { createCanvas } from 'canvas'; export type FormState = { neoboards: { id: string; + name?: string; neoboard: Whiteboard; }[]; message: string; + url?: string; } const mapColorNameToHex = (color: string | undefined): string => { @@ -26,6 +28,26 @@ const mapColorNameToHex = (color: string | undefined): string => { } } +/** + * Gets the ID from the URL + * + * A miro looks like this: https://miro.com/app/board// + * + * @param url The URL to parse + * @returns The ID from the URL + * @throws If the URL is not valid + */ +function parseUrl(url: string): string { + const url_parsed = new URL(url); + const path = url_parsed.pathname.split('/'); + // Note that we might have something behind the slash of the board ID or no slash at all at the end + if (path.length < 3) { + throw new Error('Invalid URL'); + } + + return path[3]; +} + export async function importBoard(prevState: FormState, formData: FormData): Promise { const boardIdsRaw = formData.getAll('board'); if (!boardIdsRaw) { @@ -33,6 +55,14 @@ export async function importBoard(prevState: FormState, formData: FormData): Pro } const boardIds: string[] = Array.isArray(boardIdsRaw) ? boardIdsRaw as string[] : [boardIdsRaw] as string[]; + if (formData.has("url")) { + const url = formData.get("url") as string; + if (url && url.length > 0) { + const boardId = parseUrl(url); + boardIds.push(boardId); + } + } + // Get board from Miro API const { miro, userId } = initMiroAPI(); @@ -49,6 +79,7 @@ export async function importBoard(prevState: FormState, formData: FormData): Pro const resultingBoards: { id: string; + name: string; neoboard: Whiteboard; }[] = []; for (const boardId of boardIds) { @@ -150,10 +181,45 @@ export async function importBoard(prevState: FormState, formData: FormData): Pro const startItem = frameData.children.find(c => c.id === connector.startItem?.id); const endItem = frameData.children.find(c => c.id === connector.endItem?.id); if (startItem && endItem) { - const startItemX = startItem.position?.x ?? 0; - const startItemY = startItem.position?.y ?? 0; - const endItemX = endItem.position?.x ?? 0; - const endItemY = endItem.position?.y ?? 0; + let startItemX = startItem.position?.x ?? 0; + let startItemY = startItem.position?.y ?? 0; + let endItemX = endItem.position?.x ?? 0; + let endItemY = endItem.position?.y ?? 0; + + // Adjust based on the connector item's position which contains a relative offset where x=0% and y=0% equals the top left corner of the item + const startRelativeOffset = connector.startItem?.position; + const endRelativeOffset = connector.endItem?.position; + + // Note that the startItemX and startItemY are the center of the item and not the top left corner + // Same goes for endItemX and endItemY + + if (startRelativeOffset && startRelativeOffset.x && startRelativeOffset.y) { + // We need to remove the % sign from the string and parse it as a number + const offsetXNumber = parseFloat(startRelativeOffset.x.replace('%', '')); + const offsetYNumber = parseFloat(startRelativeOffset.y.replace('%', '')); + + // Note that startItemX and startItemY are the center of the item and not the top left corner so offset x of 50% is no change to the x position and not half of the width + + // 1. Move the x corrdinate to the top left corner by subtracting half of the width + // 2. Add the offset in percentage + + startItemX = (startItemX - (startItem.geometry?.width ?? 1) / 2) + (offsetXNumber / 100) * (startItem.geometry?.width ?? 1); + startItemY = (startItemY - (startItem.geometry?.height ?? 1) / 2) + (offsetYNumber / 100) * (startItem.geometry?.height ?? 1); + } + + if (endRelativeOffset && endRelativeOffset.x && endRelativeOffset.y) { + // We need to remove the % sign from the string and parse it as a number + const offsetXNumber = parseFloat(endRelativeOffset.x.replace('%', '')); + const offsetYNumber = parseFloat(endRelativeOffset.y.replace('%', '')); + + // Note that endItemX and endItemY are the center of the item and not the top left corner so offset x of 50% is no change to the x position and not half of the width + + // 1. Move the x corrdinate to the top left corner by subtracting half of the width + // 2. Add the offset in percentage + + endItemX = (endItemX - (endItem.geometry?.width ?? 1) / 2) + (offsetXNumber / 100) * (endItem.geometry?.width ?? 1); + endItemY = (endItemY - (endItem.geometry?.height ?? 1) / 2) + (offsetYNumber / 100) * (endItem.geometry?.height ?? 1); + } const points = [ { @@ -189,7 +255,7 @@ export async function importBoard(prevState: FormState, formData: FormData): Pro // Transform the board into neoboard coordinates const fittedNeoboard = fitItemsBestIntoFrame(neoboard); - resultingBoards.push({ id: boardId, neoboard: fittedNeoboard }); + resultingBoards.push({ id: boardId, neoboard: fittedNeoboard, name: board.name }); } return { @@ -528,7 +594,8 @@ function convertStickyNote(item: StickyNoteItem): Shape { y: item.position?.y ?? 0, }, width: item.geometry?.width ?? 1, - height: item.geometry?.height ?? 1, + // We remove the shadow size from the height as miro adds the shadow to the height + height: (item.geometry?.height ?? 1) - 43, fillColor: fillColor ?? "#ffffff", strokeColor: 'transparent', strokeWidth: 0, diff --git a/src/app/page.tsx b/src/app/page.tsx index 2167194..1f9cd56 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -18,16 +18,32 @@ const getBoards = async () => { }; } - const api = miro.as(userIdAwaited); + try { + const api = miro.as(userIdAwaited); - const boards: Board[] = []; - for await (const board of api.getAllBoards()) { - boards.push(board); - } + const boards: Board[] = []; + for await (const board of api.getAllBoards()) { + boards.push(board); + } - return { - boards, - }; + return { + boards, + }; + } catch (error) { + console.error(error); + // @ts-expect-error - error is usually a response object + if (error.hasOwnProperty('statusCode')) { + // @ts-expect-error - error is usually a response object + if (error.statusCode === 401) { + return { + authUrl: miro.getAuthUrl(), + }; + } + } + return { + error: 'Failed to get boards', + }; + } }; export default async function Page() { diff --git a/src/app/selectionFormComponent.tsx b/src/app/selectionFormComponent.tsx index 2607b4d..c9c63c0 100644 --- a/src/app/selectionFormComponent.tsx +++ b/src/app/selectionFormComponent.tsx @@ -19,6 +19,7 @@ export default function SelectionFormComponent({ boards }: SelectionFormProps) { return { neoboards: [], message: '', + url: undefined, } as FormState; } @@ -40,8 +41,8 @@ export default function SelectionFormComponent({ boards }: SelectionFormProps) { {state.message &&

{state.message}

}

Download the Neoboards below

{neoboardBlobs.map((blob, index) => ( - state.neoboards[index].id === x.id)?.name}.nwb`} className="button button-primary"> - Download Neoboard “{uniqueBoards.find(x => state.neoboards[index].id === x.id)?.name}” + + Download Neoboard “{state.neoboards[index].name}” ))} {/* Start over button which resets state */} @@ -70,6 +71,12 @@ export default function SelectionFormComponent({ boards }: SelectionFormProps) { ))} + {/* Allow entering an url for manual selection */} +
+ + +
+ -- 2.45.2