From 2067f6a147d45b1cf2c87c535bd217e032ccacfa Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Sat, 5 Oct 2024 13:57:19 +0200 Subject: [PATCH 01/23] Added sticky-pair-clusterer This is main frame work for creating the app. It is tested and working --- examples/sticky-pair-clusterer/README.md | 91 +++++++++++++++ .../sticky-pair-clusterer/app-manifest.yaml | 7 ++ examples/sticky-pair-clusterer/app.html | 14 +++ examples/sticky-pair-clusterer/index.html | 9 ++ examples/sticky-pair-clusterer/jsconfig.json | 7 ++ examples/sticky-pair-clusterer/package.json | 15 +++ examples/sticky-pair-clusterer/src/app.js | 107 ++++++++++++++++++ examples/sticky-pair-clusterer/src/index.js | 9 ++ examples/sticky-pair-clusterer/src/styles.css | 34 ++++++ examples/sticky-pair-clusterer/vite.config.js | 21 ++++ 10 files changed, 314 insertions(+) create mode 100644 examples/sticky-pair-clusterer/README.md create mode 100644 examples/sticky-pair-clusterer/app-manifest.yaml create mode 100644 examples/sticky-pair-clusterer/app.html create mode 100644 examples/sticky-pair-clusterer/index.html create mode 100644 examples/sticky-pair-clusterer/jsconfig.json create mode 100644 examples/sticky-pair-clusterer/package.json create mode 100644 examples/sticky-pair-clusterer/src/app.js create mode 100644 examples/sticky-pair-clusterer/src/index.js create mode 100644 examples/sticky-pair-clusterer/src/styles.css create mode 100644 examples/sticky-pair-clusterer/vite.config.js diff --git a/examples/sticky-pair-clusterer/README.md b/examples/sticky-pair-clusterer/README.md new file mode 100644 index 000000000..85a76818b --- /dev/null +++ b/examples/sticky-pair-clusterer/README.md @@ -0,0 +1,91 @@ +# Miro Template Builder App + +This app shows how you can create a template with shape and text items on the board. + +# 👨🏻‍💻 App Demo + +https://github.com/miroapp/app-examples/assets/10428517/24aacae3-5183-4142-bdae-9cbfeff06a69 + +# 📒 Table of Contents + +- [Included Features](#features) +- [Tools and Technologies](#tools) +- [Prerequisites](#prerequisites) +- [Associated Developer Tutorial](#tutorial) +- [Run the app locally](#run) +- [Folder Structure](#folder) +- [Contributing](#contributing) +- [License](#license) + +# ⚙️ Included Features + +- [Miro Web SDK](https://developers.miro.com/docs/web-sdk-reference) + - [miro.board.createText()](https://developers.miro.com/docs/board_board#createtext) + - [miro.board.createShape()](https://developers.miro.com/docs/board_board#createshape) + - [miro.board.ui.openPanel()](https://developers.miro.com/docs/ui_boardui#openpanel) + +# 🛠️ Tools and Technologies + +- [Vite](https://vitejs.dev/) + +# ✅ Prerequisites + +- You have a [Miro account](https://miro.com/signup/). +- You're [signed in to Miro](https://miro.com/login/). +- Your Miro account has a [Developer team](https://developers.miro.com/docs/create-a-developer-team). +- Your development environment includes [Node.js 14.13](https://nodejs.org/en/download) or a later version. +- All examples use `npm` as a package manager and `npx` as a package runner. + +# 🏃🏽‍♂️ Run the app locally + +1. Run `npm install` to install dependencies. +2. Run `npm start` to start developing. \ + Your URL should be similar to this example: + ``` + http://localhost:3000 + ``` +3. Open the [app manifest editor](https://developers.miro.com/docs/manually-create-an-app#step-2-configure-your-app-in-miro) by clicking **Edit in Manifest**. \ + In the app manifest editor, configure the app as follows: + +```yaml +# See https://developers.miro.com/docs/app-manifest on how to use this +appName: Template Builder +sdkVersion: SDK_V2 +sdkUri: http://localhost:3000 +scopes: + - boards:read + - boards:write +``` + +4. Go back to your app home page, and under the `Permissions` section, you will see a blue button that says `Install app and get OAuth token`. Click that button. Then click on `Add` as shown in the video below. In the video we install a different app, but the process is the same regardless of the app. + +> ⚠️ We recommend to install your app on a [developer team](https://developers.miro.com/docs/create-a-developer-team) while you are developing or testing apps.⚠️ + +https://github.com/miroapp/app-examples/assets/10428517/1e6862de-8617-46ef-b265-97ff1cbfe8bf + +5. Go to your developer team, and open your boards. +6. Click on the plus icon from the bottom section of your left sidebar. If you hover over it, it will say `More apps`. +7. Search for your app `Template Builder` or whatever you chose to name it. Click on your app to use it, as shown in the video below. In the video we search for a different app, but the process is the same regardless of the app. + +https://github.com/horeaporutiu/app-examples-template/assets/10428517/b23d9c4c-e785-43f9-a72e-fa5d82c7b019 + +# 🗂️ Folder structure + +``` +. +├── src +│ ├── assets +│ │ └── style.css +│ ├── app.js // The code for the app lives here. +│ └── index.js // The code for the app entry point lives here. +├── app.html // The app itself. It's loaded on the board inside the 'appContainer'. +└── index.html // The app entry point. This is the value you assign to 'sdkUri' in the app manifest file. +``` + +# 🫱🏻‍🫲🏽 Contributing + +If you want to contribute to this example, or any other Miro Open Source project, please review [Miro's contributing guide](https://github.com/miroapp/app-examples/blob/main/CONTRIBUTING.md). + +# 🪪 License + +[MIT License](https://github.com/miroapp/app-examples/blob/main/LICENSE). diff --git a/examples/sticky-pair-clusterer/app-manifest.yaml b/examples/sticky-pair-clusterer/app-manifest.yaml new file mode 100644 index 000000000..e5baa531b --- /dev/null +++ b/examples/sticky-pair-clusterer/app-manifest.yaml @@ -0,0 +1,7 @@ +# See https://developers.miro.com/docs/app-manifest on how to use this +#appName: Template Builder +appName: Sticky notes to shapes +sdkUri: "http://localhost:3000" +scopes: + - boards:read + - boards:write diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html new file mode 100644 index 000000000..2ae6ebef1 --- /dev/null +++ b/examples/sticky-pair-clusterer/app.html @@ -0,0 +1,14 @@ + + + + + + + + +
+ +
+ + + diff --git a/examples/sticky-pair-clusterer/index.html b/examples/sticky-pair-clusterer/index.html new file mode 100644 index 000000000..54bad6699 --- /dev/null +++ b/examples/sticky-pair-clusterer/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/sticky-pair-clusterer/jsconfig.json b/examples/sticky-pair-clusterer/jsconfig.json new file mode 100644 index 000000000..d23878618 --- /dev/null +++ b/examples/sticky-pair-clusterer/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "typeRoots": ["./node_modules/@types", "./node_modules/@mirohq"] + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/examples/sticky-pair-clusterer/package.json b/examples/sticky-pair-clusterer/package.json new file mode 100644 index 000000000..141d17033 --- /dev/null +++ b/examples/sticky-pair-clusterer/package.json @@ -0,0 +1,15 @@ +{ + "name": "sticky-pair-clusterer", + "description": "Create a template programmatically with the Miro Web SDK. This app uses Vite.", + "keywords": ["Miro SDK", "Vite"], + "version": "0.0.0", + "scripts": { + "start": "vite", + "build": "vite build", + "serve": "vite preview" + }, + "devDependencies": { + "@mirohq/websdk-types": "latest", + "vite": "2.9.17" + } +} diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js new file mode 100644 index 000000000..f11905bb9 --- /dev/null +++ b/examples/sticky-pair-clusterer/src/app.js @@ -0,0 +1,107 @@ +const { board } = window.miro; + +document.getElementById('createPair').addEventListener('click', async () => { + const pairId = `pair_${Date.now()}`; // Unique pair identifier based on timestamp + + + const frames = await board.get({ type: 'frame' }); + if (frames.length === 0) { + alert('No frames found on the board. Please create a frame first.'); + return; + } + const targetFrame = frames[0]; // Using the first frame found + + const centerX = targetFrame.x; + const centerY = targetFrame.y; + + const stickyNote1 = await board.createStickyNote({ + content: `Note 1 of ${pairId}`, + x: centerX - 100, + y: centerY, + style: { + fillColor: "red", // Red color using valid enum value + }, + }); + + const stickyNote2 = await board.createStickyNote({ + content: `Note 2 of ${pairId}`, + x: centerX + 100, + y: centerY, + style: { + fillColor: "red", // Red color using valid enum value + }, + }); + + // Update first sticky note to reference the second sticky note + await board.get([{ id: stickyNote1.id, metadata: { myApp: { pairId: pairId, pairedNoteId: stickyNote2.id } } }]); +}); + +async function doubleStickySizeOnSelect() { + const originalSizes = new Map(); // Store original sizes for each sticky note + + // Function to double the size of the sticky note + async function doubleSize(stickyNote) { + const { width, height } = stickyNote; + // Save the original size if not already saved + if (!originalSizes.has(stickyNote.id)) { + originalSizes.set(stickyNote.id, { width, height }); + } + // Double the size + await stickyNote.update({ + width: width * 2, + height: height * 2, + }); + } + + // Function to reset the sticky note size to its original size + async function resetSize(stickyNote) { + const originalSize = originalSizes.get(stickyNote.id); + if (originalSize) { + await stickyNote.update({ + width: originalSize.width, + height: originalSize.height, + }); + } + } + + // Event listener for selection change + miro.board.ui.on('selection:update', async (event) => { + const selectedWidgets = event.items; + const allWidgets = await miro.board.widgets.get(); + + // Loop through all sticky notes + for (const widget of allWidgets) { + if (widget.type === 'sticky_note') { + if (selectedWidgets.some((selected) => selected.id === widget.id)) { + // If the sticky note is selected, double its size + await doubleSize(widget); + } else { + // If the sticky note is unselected, reset its size + await resetSize(widget); + } + } + } + }); +} +/** When a user clicks and selects multiple board items on a board: + * 1. The 'selection:update' method logs the selection to the developer console + * 2. A filter identifies sticky note items in the selection + * 3. The color of the sticky notes is changed to 'cyan' + */ + +// Listen to the 'selection:update' event +miro.board.ui.on('selection:update', async (event) => { + console.log('Subscribed to selection update event', event); + console.log(event.items); + const selectedItems = event.items; + + // Filter sticky notes from the selected items + const stickyNotes = selectedItems.filter((item) => item.type === 'sticky_note'); + + // Change the fill color of the sticky notes + for (const stickyNote of stickyNotes) { + stickyNote.style.fillColor = 'yellow'; + await stickyNote.sync(); + } +}); +// Initialize the app diff --git a/examples/sticky-pair-clusterer/src/index.js b/examples/sticky-pair-clusterer/src/index.js new file mode 100644 index 000000000..e68777e43 --- /dev/null +++ b/examples/sticky-pair-clusterer/src/index.js @@ -0,0 +1,9 @@ +const { board } = window.miro; + +async function init() { + board.ui.on("icon:click", async () => { + await board.ui.openPanel({ url: "app.html" }); + }); +} + +init(); diff --git a/examples/sticky-pair-clusterer/src/styles.css b/examples/sticky-pair-clusterer/src/styles.css new file mode 100644 index 000000000..44f6ad5eb --- /dev/null +++ b/examples/sticky-pair-clusterer/src/styles.css @@ -0,0 +1,34 @@ +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +.scrollable-container { + height: 100%; + overflow-y: auto; + padding-top: 30px; + padding-left: 23px; + padding-bottom: 20px; + box-sizing: border-box; +} + +.scrollable-content { + height: 2000px; + background-color: #2a79ff; +} + +.create-pair-button { + display: block; + background: none; + border: 1px solid #4262ff; + color: #4262ff; + height: 30px; + box-sizing: border-box; + border-radius: 4px; + text-align: center; + font-size: 14px; + cursor: pointer; + margin-bottom: 20px; +} diff --git a/examples/sticky-pair-clusterer/vite.config.js b/examples/sticky-pair-clusterer/vite.config.js new file mode 100644 index 000000000..1a0e09fef --- /dev/null +++ b/examples/sticky-pair-clusterer/vite.config.js @@ -0,0 +1,21 @@ +const path = require("path"); +const fs = require("fs"); +const { defineConfig } = require("vite"); + +// make sure vite picks up all html files in root +const allHtmlEntries = fs + .readdirSync(".") + .filter((file) => path.extname(file) === ".html") + .reduce((acc, file) => { + acc[path.basename(file, ".html")] = path.resolve(__dirname, file); + + return acc; + }, {}); + +module.exports = defineConfig({ + build: { + rollupOptions: { + input: allHtmlEntries, + }, + }, +}); From 2ed0d3099762ff38a932123fc99c59f58a7eb863 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Mon, 7 Oct 2024 11:41:03 +0200 Subject: [PATCH 02/23] Added text box for number of trait pairs to add --- examples/sticky-pair-clusterer/app.html | 8 +++++++ examples/sticky-pair-clusterer/src/styles.css | 24 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index 2ae6ebef1..b859cd8e3 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -7,6 +7,14 @@
+
diff --git a/examples/sticky-pair-clusterer/src/styles.css b/examples/sticky-pair-clusterer/src/styles.css index 44f6ad5eb..089d30ef8 100644 --- a/examples/sticky-pair-clusterer/src/styles.css +++ b/examples/sticky-pair-clusterer/src/styles.css @@ -18,9 +18,21 @@ body { height: 2000px; background-color: #2a79ff; } +.traits-count-input { + width: 200px; + padding: 5px 10px; + margin-right: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; + height: 30px; + vertical-align: middle; +} .create-pair-button { - display: block; + display: inline-block; + width: 200px; background: none; border: 1px solid #4262ff; color: #4262ff; @@ -30,5 +42,15 @@ body { text-align: center; font-size: 14px; cursor: pointer; + vertical-align: middle; +} + +.create-pair-button:hover { + background-color: #45a049; +} + +.input-button-container { + display: flex; + align-items: center; margin-bottom: 20px; } From cd170bd06aae65e75f8fc01c185d2cf3ad99feb0 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Mon, 7 Oct 2024 12:51:13 +0200 Subject: [PATCH 03/23] removed event listner for changing selection size, added sticky pair support added function to add trait pairs based on frame size. Requires that frame with name "Benefit Template" exists on board --- examples/sticky-pair-clusterer/app.html | 2 +- examples/sticky-pair-clusterer/src/app.js | 312 ++++++++++++++++------ 2 files changed, 224 insertions(+), 90 deletions(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index b859cd8e3..13b4acdeb 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -12,7 +12,7 @@ id="traitsCount" class="traits-count-input" min="1" - value="1" + value="20" placeholder="Number of traits" /> diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index f11905bb9..4bdb2366e 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -1,107 +1,241 @@ const { board } = window.miro; -document.getElementById('createPair').addEventListener('click', async () => { +document.getElementById("createPair").addEventListener("click", async () => { const pairId = `pair_${Date.now()}`; // Unique pair identifier based on timestamp - - const frames = await board.get({ type: 'frame' }); + const frames = await board.get({ type: "frame" }); if (frames.length === 0) { - alert('No frames found on the board. Please create a frame first.'); + alert("No frames found on the board. Please create a frame first."); return; } - const targetFrame = frames[0]; // Using the first frame found - const centerX = targetFrame.x; - const centerY = targetFrame.y; + // Read the number of squares from the input field + const noOfSquares = parseInt( + document.getElementById("traitsCount").value, + 10, + ); - const stickyNote1 = await board.createStickyNote({ - content: `Note 1 of ${pairId}`, - x: centerX - 100, - y: centerY, - style: { - fillColor: "red", // Red color using valid enum value - }, - }); - - const stickyNote2 = await board.createStickyNote({ - content: `Note 2 of ${pairId}`, - x: centerX + 100, - y: centerY, - style: { - fillColor: "red", // Red color using valid enum value - }, - }); + // Validate the input + if (isNaN(noOfSquares) || noOfSquares < 1 || noOfSquares > 1000) { + alert("Please enter a valid number of squares between 1 and 1000."); + return; + } - // Update first sticky note to reference the second sticky note - await board.get([{ id: stickyNote1.id, metadata: { myApp: { pairId: pairId, pairedNoteId: stickyNote2.id } } }]); -}); + const frame = await findFrameByName("Benefit Template"); + const targetFrame = frame; + const frameDimensions = await getFrameDimensions(frame); + const squarePositions = calculateBestSquaresInRectangle( + frameDimensions.width, + frameDimensions.height, + noOfSquares, + ); + console.log("Square Positions:", squarePositions); + + // Calculate the upper-left corner of the frame + const frameLeft = targetFrame.x - targetFrame.width / 2; + const frameTop = targetFrame.y - targetFrame.height / 2; + + // Create sticky notes + for (let i = 0; i < noOfSquares; i++) { + const row = Math.floor(i / squarePositions.gridInfo.columns); + const col = i % squarePositions.gridInfo.columns; + const position = squarePositions.placement.getSquarePosition(row, col); -async function doubleStickySizeOnSelect() { - const originalSizes = new Map(); // Store original sizes for each sticky note - - // Function to double the size of the sticky note - async function doubleSize(stickyNote) { - const { width, height } = stickyNote; - // Save the original size if not already saved - if (!originalSizes.has(stickyNote.id)) { - originalSizes.set(stickyNote.id, { width, height }); - } - // Double the size - await stickyNote.update({ - width: width * 2, - height: height * 2, + await board.createStickyNote({ + content: `Note ${i + 1} of ${pairId}`, + x: + frameLeft + + position.x + + squarePositions.gridInfo.effectiveSquareSize / 2, + y: + frameTop + + position.y + + squarePositions.gridInfo.effectiveSquareSize / 2, + width: squarePositions.gridInfo.effectiveSquareSize, + style: { + fillColor: "light_yellow", + }, }); } - // Function to reset the sticky note size to its original size - async function resetSize(stickyNote) { - const originalSize = originalSizes.get(stickyNote.id); - if (originalSize) { - await stickyNote.update({ - width: originalSize.width, - height: originalSize.height, - }); - } + console.log(`Created ${noOfSquares} sticky notes for pair ${pairId}`); +}); + +// const originalSizes = new Map(); // Store original sizes for each sticky note +// const resizedStickyNotes = new Set(); // Store the IDs of resized sticky notes + +// // Function to double the size of a sticky note +// async function doubleSize(stickyNote) { +// const { width, height } = stickyNote; +// +// // Save the original size if not already saved +// if (!originalSizes.has(stickyNote.id)) { +// originalSizes.set(stickyNote.id, { width, height }); +// } +// stickyNote.width = width * 2; +// stickyNote.sync(); +// +// resizedStickyNotes.add(stickyNote.id); // Track resized sticky note +// } + +// // Function to reset the sticky note size to its original size +// async function resetSize(stickyNote) { +// const originalSize = originalSizes.get(stickyNote.id); +// if (originalSize) { +// +// stickyNote.width = originalSize.width; +// stickyNote.sync(); +// //stickyNote.height= originalSize.height; +// //stickyNote.sync(); +// +// // Remove from the resized sticky notes set and the original size map +// resizedStickyNotes.delete(stickyNote.id); +// originalSizes.delete(stickyNote.id); +// } +// } + +// Event listener for selection changes +// miro.board.ui.on('selection:update', async (event) => { +// const selectedWidgets = event.items; +// +// for (const widget of selectedWidgets) { +// if (widget.type === 'sticky_note') { +// // Get the metadata of the selected sticky note +// const metadata = await widget.getMetadata('myApp'); +// +// // Check if the sticky note has a pairId +// if (metadata && metadata.pairId && metadata.pairedNoteId) { +// const pairedNoteId = metadata.pairedNoteId; +// +// // Double the size of the selected sticky note +// await doubleSize(widget); +// +// // Find the paired sticky note and double its size +// const [pairedNote] = await miro.board.widgets.get({ id: pairedNoteId }); +// if (pairedNote) { +// await doubleSize(pairedNote); +// } +// } else { +// // If the sticky note doesn't have a pair, just resize the selected one +// await doubleSize(widget); +// } +// } +// } +// +// // Handle unselected sticky notes by restoring their original size +// const allStickyNotes = await miro.board.widgets.get({ type: 'sticky_note' }); +// const unselectedStickyNotes = allStickyNotes.filter( +// (stickyNote) => !selectedWidgets.some((selected) => selected.id === stickyNote.id) +// ); +// +// for (const stickyNote of unselectedStickyNotes) { +// if (resizedStickyNotes.has(stickyNote.id)) { +// await resetSize(stickyNote); +// } +// } +// }); + +// Function to find a frame by name +async function findFrameByName(frameName) { + const frames = await board.get({ type: "frame" }); + return frames.find((frame) => frame.title === frameName); +} + +// New function to get frame dimensions +async function getFrameDimensions(frame) { + if (!frame) { + console.error("Frame not found"); + return null; } - // Event listener for selection change - miro.board.ui.on('selection:update', async (event) => { - const selectedWidgets = event.items; - const allWidgets = await miro.board.widgets.get(); - - // Loop through all sticky notes - for (const widget of allWidgets) { - if (widget.type === 'sticky_note') { - if (selectedWidgets.some((selected) => selected.id === widget.id)) { - // If the sticky note is selected, double its size - await doubleSize(widget); - } else { - // If the sticky note is unselected, reset its size - await resetSize(widget); - } - } - } - }); + return { + width: frame.width, + height: frame.height, + x: frame.x, + y: frame.y, + }; } -/** When a user clicks and selects multiple board items on a board: - * 1. The 'selection:update' method logs the selection to the developer console - * 2. A filter identifies sticky note items in the selection - * 3. The color of the sticky notes is changed to 'cyan' - */ - -// Listen to the 'selection:update' event -miro.board.ui.on('selection:update', async (event) => { - console.log('Subscribed to selection update event', event); - console.log(event.items); - const selectedItems = event.items; - - // Filter sticky notes from the selected items - const stickyNotes = selectedItems.filter((item) => item.type === 'sticky_note'); - - // Change the fill color of the sticky notes - for (const stickyNote of stickyNotes) { - stickyNote.style.fillColor = 'yellow'; - await stickyNote.sync(); + +// Example usage: +// const frame = await findFrameByName('My Frame'); +// const dimensions = await getFrameDimensions(frame); +// console.log('Frame dimensions:', dimensions); +function calculateBestSquaresInRectangle( + rectangleWidth, + rectangleHeight, + numSquares, + margin = 0.1, +) { + // Compute ratio of the rectangle + var ratio = rectangleWidth / rectangleHeight; + + // Initial estimates for number of columns and rows + var ncols_float = Math.sqrt(numSquares * ratio); + var nrows_float = numSquares / ncols_float; + + // Find the best option for filling the whole height + var nrows1 = Math.ceil(nrows_float); + var ncols1 = Math.ceil(numSquares / nrows1); + while (nrows1 * ratio < ncols1) { + nrows1++; + ncols1 = Math.ceil(numSquares / nrows1); } -}); -// Initialize the app + var cell_size1 = rectangleHeight / nrows1; + + // Find the best option for filling the whole width + var ncols2 = Math.ceil(ncols_float); + var nrows2 = Math.ceil(numSquares / ncols2); + while (ncols2 < nrows2 * ratio) { + ncols2++; + nrows2 = Math.ceil(numSquares / ncols2); + } + var cell_size2 = rectangleWidth / ncols2; + + // Determine the best configuration + var nrows, ncols, cell_size; + if (cell_size1 < cell_size2) { + nrows = nrows2; + ncols = ncols2; + cell_size = cell_size2; + } else { + nrows = nrows1; + ncols = ncols1; + cell_size = cell_size1; + } + + // Calculate effective cell size including margin + const effectiveCellSize = cell_size * (1 - margin); + + // Calculate usable area within the rectangle + const usableWidth = ncols * effectiveCellSize; + const usableHeight = nrows * effectiveCellSize; + + // Calculate offset to center the grid in the rectangle + const offsetX = (rectangleWidth - usableWidth) / 2; + const offsetY = (rectangleHeight - usableHeight) / 2; + + return { + gridInfo: { + columns: ncols, + rows: nrows, + squareSize: cell_size, + margin: margin, + effectiveSquareSize: effectiveCellSize, + }, + rectangleInfo: { + width: rectangleWidth, + height: rectangleHeight, + usableWidth: usableWidth, + usableHeight: usableHeight, + }, + placement: { + offsetX: offsetX, + offsetY: offsetY, + getSquarePosition: (row, col) => ({ + x: offsetX + col * effectiveCellSize, + y: offsetY + row * effectiveCellSize, + }), + }, + totalSquares: nrows * ncols, + }; +} From c7bb5ada9dc10e08ca6b80cb7ab684bb4f0668ba Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Thu, 17 Oct 2024 12:31:53 +0200 Subject: [PATCH 04/23] create matrix button creates frames and places stickynotes on them. Working status --- examples/sticky-pair-clusterer/app.html | 37 +- examples/sticky-pair-clusterer/src/app.js | 653 ++++++++++++++++-- examples/sticky-pair-clusterer/src/styles.css | 19 + 3 files changed, 632 insertions(+), 77 deletions(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index 13b4acdeb..aa8b87e51 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -7,15 +7,36 @@
- +
+ + +
+
+ + +
+ + +
diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 4bdb2366e..cfd13f8e0 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -1,65 +1,548 @@ const { board } = window.miro; +////////////////////////////////////////////////////////////////////////////////////////////////// +class MatrixCell { + constructor(value, stickyNoteId) { + this.value = value; // Integer value between 1-8 + this.stickyNoteId = stickyNoteId; // Reference to a sticky note + } +} -document.getElementById("createPair").addEventListener("click", async () => { - const pairId = `pair_${Date.now()}`; // Unique pair identifier based on timestamp +class Matrix { + constructor(rows, columns) { + this.rows = rows; + this.columns = columns; + this.matrix = Array.from({ length: rows }, () => Array(columns).fill(null)); + this.columnToFrameMap = new Map(); // Maps column index to frame ID + this.rowToStickyNotesMap = new Map(); // Maps row index to an array of sticky note IDs + } - const frames = await board.get({ type: "frame" }); - if (frames.length === 0) { - alert("No frames found on the board. Please create a frame first."); - return; + setCell(row, col, value, stickyNoteId) { + const cell = new MatrixCell(value, stickyNoteId); + this.matrix[row][col] = cell; + + // Add sticky note ID to the row map + if (!this.rowToStickyNotesMap.has(row)) { + this.rowToStickyNotesMap.set(row, []); + } + this.rowToStickyNotesMap.get(row).push(stickyNoteId); + } + + linkColumnToFrame(col, frameId) { + this.columnToFrameMap.set(col, frameId); + } + + getStickyNotesForRow(row) { + return this.rowToStickyNotesMap.get(row) || []; + } + + getFrameForColumn(col) { + return this.columnToFrameMap.get(col); + } + // CRUD operations for Matrix + + getCell(row, col) { + return this.matrix[row][col]; + } + + updateCell(row, col, value, stickyNoteId) { + if (row >= 0 && row < this.rows && col >= 0 && col < this.columns) { + this.setCell(row, col, value, stickyNoteId); + return true; + } + return false; + } + + deleteCell(row, col) { + if (row >= 0 && row < this.rows && col >= 0 && col < this.columns) { + this.matrix[row][col] = null; + + // Remove sticky note ID from the row map + const stickyNotes = this.rowToStickyNotesMap.get(row); + if (stickyNotes) { + const index = stickyNotes.findIndex( + (id) => id === this.matrix[row][col]?.stickyNoteId, + ); + if (index !== -1) { + stickyNotes.splice(index, 1); + } + } + + return true; + } + return false; + } + + addRow() { + this.matrix.push(Array(this.columns).fill(null)); + this.rows++; + } + + removeRow(rowIndex) { + if (rowIndex >= 0 && rowIndex < this.rows) { + this.matrix.splice(rowIndex, 1); + this.rows--; + this.rowToStickyNotesMap.delete(rowIndex); + return true; + } + return false; + } + setRowName(rowIndex, name) { + if (rowIndex >= 0 && rowIndex < this.rows) { + if (!this.rowNames) { + this.rowNames = new Map(); + } + this.rowNames.set(rowIndex, name); + return true; + } + return false; + } + + getRowName(rowIndex) { + if (this.rowNames && rowIndex >= 0 && rowIndex < this.rows) { + return this.rowNames.get(rowIndex); + } + return null; + } + + setColumnName(colIndex, name) { + if (colIndex >= 0 && colIndex < this.columns) { + if (!this.columnNames) { + this.columnNames = new Map(); + } + this.columnNames.set(colIndex, name); + return true; + } + return false; + } + + getColumnName(colIndex) { + if (this.columnNames && colIndex >= 0 && colIndex < this.columns) { + return this.columnNames.get(colIndex); + } + return null; + } + + removeColumn(colIndex) { + if (colIndex >= 0 && colIndex < this.columns) { + for (let i = 0; i < this.rows; i++) { + this.matrix[i].splice(colIndex, 1); + } + this.columns--; + this.columnToFrameMap.delete(colIndex); + return true; + } + return false; + } + updateRowName(rowIndex, newName) { + if (rowIndex >= 0 && rowIndex < this.rows) { + if (!this.rowNames) { + this.rowNames = new Map(); + } + this.rowNames.set(rowIndex, newName); + return true; + } + return false; + } + + updateColumnName(colIndex, newName) { + if (colIndex >= 0 && colIndex < this.columns) { + if (!this.columnNames) { + this.columnNames = new Map(); + } + this.columnNames.set(colIndex, newName); + return true; + } + return false; + } + + deleteRowName(rowIndex) { + if (this.rowNames && rowIndex >= 0 && rowIndex < this.rows) { + return this.rowNames.delete(rowIndex); + } + return false; } - // Read the number of squares from the input field - const noOfSquares = parseInt( - document.getElementById("traitsCount").value, + deleteColumnName(colIndex) { + if (this.columnNames && colIndex >= 0 && colIndex < this.columns) { + return this.columnNames.delete(colIndex); + } + return false; + } + + getAllRowNames() { + return this.rowNames ? Object.fromEntries(this.rowNames) : {}; + } + + getAllColumnNames() { + return this.columnNames ? Object.fromEntries(this.columnNames) : {}; + } + + async addColumn() { + const newColumnIndex = this.columns; + this.columns++; + + // Create a new frame for the column + const frame = await board.createFrame({ + title: `Column ${newColumnIndex}`, + width: 1920, // 1080p width + height: 1080, // 1080p height + }); + + // Link the new column to the frame + this.linkColumnToFrame(newColumnIndex, frame.id); + + // Add a new cell to each row and create a sticky note for it + for (let i = 0; i < this.rows; i++) { + if (!this.matrix[i]) { + this.matrix[i] = []; + } + const sticky = await board.createStickyNote({ + content: "", + x: frame.x, + y: frame.y + i * 100, // Adjust vertical position for each sticky note + width: 200, + }); + this.matrix[i][newColumnIndex] = { value: 0, stickyNoteId: sticky.id }; + } + + // Update column names if necessary + if (this.columnNames) { + this.updateColumnName(newColumnIndex, `Column ${newColumnIndex}`); + } + + return newColumnIndex; + } + + // Additional methods to manage the matrix, rows, and columns can be added here +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Example usage: +// const matrix = new Matrix(3, 3); +// +// // Set cell values and link sticky notes +// matrix.setCell(0, 0, 5, 'stickyNote1'); +// matrix.setCell(0, 1, 3, 'stickyNote2'); +// matrix.setCell(1, 0, 2, 'stickyNote3'); +// matrix.setCell(2, 2, 4, 'stickyNote4'); +// +// // Link columns to frames +// matrix.linkColumnToFrame(0, 'frame1'); +// matrix.linkColumnToFrame(1, 'frame2'); +// matrix.linkColumnToFrame(2, 'frame3'); +// +// // Get sticky notes for a row +// console.log(matrix.getStickyNotesForRow(0)); // ['stickyNote1', 'stickyNote2'] +// +// // Get frame for a column +// console.log(matrix.getFrameForColumn(0)); // 'frame1' +// +// // Add a new row +// matrix.addRow(); +// console.log(matrix.rows); // 4 +// +// // Add a new column +// matrix.addColumn(); +// console.log(matrix.columns); // 4 +// +// // Update row and column names +// matrix.updateRowName(0, 'First Row'); +// matrix.updateColumnName(1, 'Second Column'); +// +// // Get all row and column names +// console.log(matrix.getAllRowNames()); +// console.log(matrix.getAllColumnNames()); +// +// matrix.linkColumnToFrame(0, 'frame1'); +// +// console.log(matrix.getStickyNotesForRow(0)); // ['stickyNote1', 'stickyNote2'] +// console.log(matrix.getFrameForColumn(0)); // 'frame1' +////////////////////////////////////////////////////////////////////////////////////////////////// + +class Trait { + constructor(stickyNotesIds, content = "", tags = []) { + this.stickyNotesIds = stickyNotesIds; + this.content = content; + this.tags = tags; + } + + addTag(tag) { + if (!this.tags.includes(tag)) { + this.tags.push(tag); + } + } + + removeTag(tag) { + const index = this.tags.indexOf(tag); + if (index !== -1) { + this.tags.splice(index, 1); + } + } + + setContent(content) { + this.content = content; + } +} + +async function createBenefitTraitMatrix(benefits, traits) { + const matrix = []; + + // Create rows for each benefit + for (let i = 0; i < benefits; i++) { + // Create a new frame with 1080 16:9 aspect ratio + const newFrame = await board.createFrame({ + title: `Benefit ${i + 1}`, + x: i * 1100, // Offset each frame horizontally + y: 0, + width: 1080, + height: 608, + }); + console.log(`Created frame ${newFrame.id}`); + const row = []; + + // Create cells for each trait + for (let j = 0; j < traits; j++) { + // create a new sticky note and place it in the new frame + const newStickyNote = await board.createStickyNote({ + x: 0, + y: 0, + width: 100, + height: 100, + content: `Trait ${j + 1}`, + frameId: newFrame.id, + }); + // create a new trait from the new sticky note and place it in the row + const newTrait = new Trait([newStickyNote.id], `Trait ${j + 1}`); + row.push(newTrait); + } + + matrix.push(row); + } + + return matrix; +} + +// Example usage: +// const benefitTraitMatrix = createBenefitTraitMatrix(benefits, traits); +async function createMatrix() { + console.log("Create Matrix button clicked"); + + // Get the number of rows and columns from the input fields + const rowsCount = parseInt(document.getElementById("rowsCount").value, 10); + const columnsCount = parseInt( + document.getElementById("columnsCount").value, 10, ); // Validate the input - if (isNaN(noOfSquares) || noOfSquares < 1 || noOfSquares > 1000) { - alert("Please enter a valid number of squares between 1 and 1000."); + if ( + isNaN(rowsCount) || + isNaN(columnsCount) || + rowsCount < 1 || + columnsCount < 1 + ) { + alert("Please enter valid numbers for rows and columns."); return; } - const frame = await findFrameByName("Benefit Template"); - const targetFrame = frame; - const frameDimensions = await getFrameDimensions(frame); - const squarePositions = calculateBestSquaresInRectangle( - frameDimensions.width, - frameDimensions.height, - noOfSquares, - ); - console.log("Square Positions:", squarePositions); - - // Calculate the upper-left corner of the frame - const frameLeft = targetFrame.x - targetFrame.width / 2; - const frameTop = targetFrame.y - targetFrame.height / 2; - - // Create sticky notes - for (let i = 0; i < noOfSquares; i++) { - const row = Math.floor(i / squarePositions.gridInfo.columns); - const col = i % squarePositions.gridInfo.columns; - const position = squarePositions.placement.getSquarePosition(row, col); - - await board.createStickyNote({ - content: `Note ${i + 1} of ${pairId}`, - x: - frameLeft + - position.x + - squarePositions.gridInfo.effectiveSquareSize / 2, - y: - frameTop + - position.y + - squarePositions.gridInfo.effectiveSquareSize / 2, - width: squarePositions.gridInfo.effectiveSquareSize, + // Create the matrix + const matrix = new Matrix(rowsCount, columnsCount); + + // Create frames for each column + for (let j = 0; j < columnsCount; j++) { + const frame = await board.createFrame({ + title: `Column ${j + 1}`, + width: 1920, + height: 1080, + x: j * 2000, // Offset each frame horizontally + y: 0, style: { - fillColor: "light_yellow", + fillColor: "#ffffff", // Set background color to white }, }); + matrix.linkColumnToFrame(j, frame.id); } + console.log("Matrix created: 366", matrix); + + // Create sticky notes for each cell in each column + for (let j = 0; j < columnsCount; j++) { + const frameId = matrix.getFrameForColumn(j); + const frame = await board.getById(frameId); + //safa here + const squarePositions = calculateBestSquaresInRectangle( + frame.width, + frame.height, + rowsCount, + ); + + console.log("Square Positions:", squarePositions); + // + // // Calculate the upper-left corner of the frame + const frameLeft = frame.x - frame.width / 2; + const frameTop = frame.y - frame.height / 2; + // + // + // // Create sticky notes + // const createdNotes = []; + // for (let i = 0; i < noOfSquares; i++) { + // const row = Math.floor(i / squarePositions.gridInfo.columns); + // const col = i % squarePositions.gridInfo.columns; + // const position = squarePositions.placement.getSquarePosition(row, col); + // + // const stickyNote = await board.createStickyNote({ + // x : frameLeft + position.x + squarePositions.gridInfo.effectiveSquareSize / 2, + // y: frameTop + position.y + squarePositions.gridInfo.effectiveSquareSize / 2, + // width: squarePositions.gridInfo.effectiveSquareSize, + // style: { + // fillColor: "light_yellow", + // } + // }); + + // safa here + + // Calculate optimal sticky note size + // const stickyNoteSize = calculateStickyNoteSize(frame.width, frame.height, rowsCount, columnsCount); + // const offsetSize = stickyNoteSize * 0.15; + + for (let i = 0; i < rowsCount; i++) { + const row = Math.floor(i / squarePositions.gridInfo.columns); + const col = i % squarePositions.gridInfo.columns; + const position = squarePositions.placement.getSquarePosition(row, col); + + const stickyNote = await board.createStickyNote({ + content: `Cell ${i + 1},${j + 1}`, + x: + frameLeft + + position.x + + squarePositions.gridInfo.effectiveSquareSize / 2, + y: + frameTop + + position.y + + squarePositions.gridInfo.effectiveSquareSize / 2, + width: squarePositions.gridInfo.effectiveSquareSize, + style: { + fillColor: "light_yellow", + }, + }); + // const stickyNote = await miro.board.createStickyNote({ + // content: `Cell ${i + 1},${j + 1}`, + // x: frame.x + (j * (stickyNoteSize + offsetSize)) + stickyNoteSize / 2, + // y: frame.y + (i * (stickyNoteSize + offsetSize)) + stickyNoteSize / 2, + // width: stickyNoteSize, + // }); + await frame.add(stickyNote); + + matrix.setCell( + i, + j, + new Trait([stickyNote.id], `Cell ${i + 1},${j + 1}`), + ); + } + } + + console.log("Matrix created:", matrix); + return matrix; +} - console.log(`Created ${noOfSquares} sticky notes for pair ${pairId}`); +document.getElementById("createMatrix").addEventListener("click", createMatrix); + +document.getElementById("debugButton").addEventListener("click", async () => { + console.log("Debug button clicked"); + //call the createBenefitTraitMatrix function with 10 benefits and 10 traits + const benefitTraitMatrix = await createBenefitTraitMatrix(10, 10); + console.log(benefitTraitMatrix); }); +// +// document.getElementById("createPair").addEventListener("click", async () => { +// +// const pairId = `pair_${Date.now()}`; // Unique pair identifier based on timestamp +// +// const frames = await board.get({ type: "frame" }); +// if (frames.length === 0) { +// alert("No frames found on the board. Please create a frame first."); +// return; +// } +// +// // Read the number of squares from the input field +// let noOfSquares = parseInt( +// document.getElementById("traitsCount").value, +// 10, +// ); +// +// // Validate the input +// if (isNaN(noOfSquares) || noOfSquares < 1 || noOfSquares > 200) { +// alert("Please enter a valid number of squares between 1 and 200."); +// return; +// } +// +// noOfSquares = noOfSquares * 2; +// const frame = await findFrameByName("Benefit Template"); +// const targetFrame = frame; +// const frameDimensions = await getFrameDimensions(frame); +// const squarePositions = calculateBestSquaresInRectangle( +// frameDimensions.width, +// frameDimensions.height, +// noOfSquares, +// ); +// console.log("Square Positions:", squarePositions); +// +// // Calculate the upper-left corner of the frame +// const frameLeft = targetFrame.x - targetFrame.width / 2; +// const frameTop = targetFrame.y - targetFrame.height / 2; +// +// +// // Create sticky notes +// const createdNotes = []; +// for (let i = 0; i < noOfSquares; i++) { +// const row = Math.floor(i / squarePositions.gridInfo.columns); +// const col = i % squarePositions.gridInfo.columns; +// const position = squarePositions.placement.getSquarePosition(row, col); +// +// const stickyNote = await board.createStickyNote({ +// x : frameLeft + position.x + squarePositions.gridInfo.effectiveSquareSize / 2, +// y: frameTop + position.y + squarePositions.gridInfo.effectiveSquareSize / 2, +// width: squarePositions.gridInfo.effectiveSquareSize, +// style: { +// fillColor: "light_yellow", +// } +// }); +// +// createdNotes.push(stickyNote); +// } +// +// // Link sticky notes in pairs and update content +// for (let i = 0; i < createdNotes.length; i += 2) { +// const sticky1 = createdNotes[i]; +// const sticky2 = createdNotes[i + 1]; +// +// await sticky1.setMetadata('myApp', { itsPair: sticky2.id }); +// await sticky2.setMetadata('myApp', { itsPair: sticky1.id }); +// +// +// sticky1.content = `ID: ${sticky1.id}\nPair ID: ${sticky2.id}\nPair: ${Math.floor(i/2) + 1} of ${noOfSquares/2}`; +// await sticky1.sync(); +// +// sticky2.content = `ID: ${sticky2.id}\nPair ID: ${sticky1.id}\nPair: ${Math.floor(i/2) + 1} of ${noOfSquares/2}`; +// await sticky2.sync(); +// } +// +// console.log(`Created ${noOfSquares} sticky notes for pair ${pairId}`); +// }); + +// Add this listener after your existing code +board.ui.on("selection:update", async (event) => { + const selectedItems = event.items; + + for (const item of selectedItems) { + if (item.type === "sticky_note") { + const metadata = await item.getMetadata("myApp"); + console.log(`Metadata for sticky note ${item.id}:`, metadata); + console.log(`Tags for sticky note ${item.id}:`, item.tagIds); + } + } +}); + +// ... rest of your code ... // const originalSizes = new Map(); // Store original sizes for each sticky note // const resizedStickyNotes = new Set(); // Store the IDs of resized sticky notes @@ -135,31 +618,6 @@ document.getElementById("createPair").addEventListener("click", async () => { // } // }); -// Function to find a frame by name -async function findFrameByName(frameName) { - const frames = await board.get({ type: "frame" }); - return frames.find((frame) => frame.title === frameName); -} - -// New function to get frame dimensions -async function getFrameDimensions(frame) { - if (!frame) { - console.error("Frame not found"); - return null; - } - - return { - width: frame.width, - height: frame.height, - x: frame.x, - y: frame.y, - }; -} - -// Example usage: -// const frame = await findFrameByName('My Frame'); -// const dimensions = await getFrameDimensions(frame); -// console.log('Frame dimensions:', dimensions); function calculateBestSquaresInRectangle( rectangleWidth, rectangleHeight, @@ -239,3 +697,60 @@ function calculateBestSquaresInRectangle( totalSquares: nrows * ncols, }; } + +// function calculateOptimalLayout(frameWidth, frameHeight, totalSquares) { +// let bestSize = 0; +// let bestRows = 0; +// let bestColumns = 0; + +// // Try different column counts (must be even) +// for (let columns = 2; columns <= Math.sqrt(totalSquares) * 2; columns += 2) { +// const rows = Math.ceil(totalSquares / columns); +// const sizeByWidth = frameWidth / (columns * 1.15); +// const sizeByHeight = frameHeight / (rows * 1.15); +// const size = Math.min(sizeByWidth, sizeByHeight); + +// if (size > bestSize) { +// bestSize = size; +// bestRows = rows; +// bestColumns = columns; +// } +// } + +// return { +// stickyNoteSize: bestSize, +// rows: bestRows, +// columns: bestColumns +// }; +// } + +// Usage in your main function +// async function createPairMatrix(rowsCount, columnsCount) { +// const frame = await board.createFrame({ +// title: 'Pair Matrix', +// width: 1920, +// height: 1080 +// }); + +// const totalSquares = rowsCount * columnsCount; +// const layout = calculateOptimalLayout(frame.width, frame.height, totalSquares); + +// const matrix = new Matrix(rowsCount, columnsCount); +// const offsetSize = layout.stickyNoteSize * 0.15; + +// for (let i = 0; i < rowsCount; i++) { +// for (let j = 0; j < columnsCount; j++) { +// const stickyNote = await board.createStickyNote({ +// content: `Cell ${i + 1},${j + 1}`, +// x: frame.x + (j % layout.columns) * (layout.stickyNoteSize + offsetSize) + layout.stickyNoteSize / 2, +// y: frame.y + Math.floor(j / layout.columns) * (layout.stickyNoteSize + offsetSize) + layout.stickyNoteSize / 2, +// width: layout.stickyNoteSize, +// }); +// await frame.add(stickyNote); + +// matrix.setCell(i, j, 0, stickyNote.id); +// } +// } + +// // Rest of your function... +// } diff --git a/examples/sticky-pair-clusterer/src/styles.css b/examples/sticky-pair-clusterer/src/styles.css index 089d30ef8..e8d458ae7 100644 --- a/examples/sticky-pair-clusterer/src/styles.css +++ b/examples/sticky-pair-clusterer/src/styles.css @@ -45,6 +45,25 @@ body { vertical-align: middle; } +.update-benefits-button { + display: inline-block; + width: 200px; + background: none; + border: 1px solid #4262ff; + color: #4262ff; + height: 30px; + box-sizing: border-box; + border-radius: 4px; + text-align: center; + font-size: 14px; + cursor: pointer; + vertical-align: middle; +} + +.update-benefits-button:hover { + background-color: #45a049; +} + .create-pair-button:hover { background-color: #45a049; } From 2e1fd419dd125da5ad803000f3ce025992b7af2b Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Thu, 17 Oct 2024 18:48:52 +0200 Subject: [PATCH 05/23] sticky note and its corresponding sticky in other frames is doubled in size and returned to original size upon deselection --- examples/sticky-pair-clusterer/src/app.js | 382 +++++++++++++++------- 1 file changed, 258 insertions(+), 124 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index cfd13f8e0..2bc0694ca 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -1,4 +1,88 @@ +/** + * Feature List: + * ------------- + * 1. Matrix Creation and Management + * - Create matrix with specified rows and columns + * - Set and update cell values + * - Link columns to frames + * - Map rows to sticky notes + * + * 2. UI Interactions + * - Detect selected sticky note and double its size + * - Detect deselected sticky note and change its size to normal + * - Detect content changes in sticky notes and update correspoding cell values and sticky notes that their content should be the same + * - Detect selected sticky note and double the size of its corresponding sticky notes in all other frames + * - Detect deselected sticky note and change the updated sticky notes to their normal size + * + * 3. Benefit-Trait Matrix Generation + * - Create benefit-trait matrix with specified dimensions + * - Calculate and populate matrix values + * + * 4. Visualization + * - Create visual representation of matrix on Miro board + * - Position squares within frames + * + * 5. Debug and Testing + * - Debug button to test matrix creation + * - Console logging for various operations + * + * 6. Event Handling + * - Listen for selection updates + * - Listen for content updates + * + * TODO: Implement clustering algorithm for sticky note pairs + * TODO: Add functionality to save and load matrix state + * TODO: Implement undo/redo functionality for matrix operations + */ + const { board } = window.miro; +// Global definitions +let g_matrix = null; + +// Function to save the matrix to the board +async function saveMatrixToBoard(matrix) { + const matrixData = JSON.stringify({ + rows: matrix.rows, + columns: matrix.columns, + data: matrix.matrix, + columnToFrameMap: Object.fromEntries(matrix.columnToFrameMap), + rowToStickyNotesMap: Object.fromEntries(matrix.rowToStickyNotesMap), + rowNames: matrix.rowNames ? Object.fromEntries(matrix.rowNames) : undefined, + columnNames: matrix.columnNames + ? Object.fromEntries(matrix.columnNames) + : undefined, + }); + await board.setAppData("benefitTraitMatrix", matrixData); + console.log("Matrix saved to board:", matrixData); +} + +// Function to load the matrix from the board +async function loadMatrixFromBoard() { + const storedMatrixData = await board.getAppData("benefitTraitMatrix"); + if (storedMatrixData) { + const storedMatrix = JSON.parse(storedMatrixData); + const matrix = new Matrix(storedMatrix.rows, storedMatrix.columns); + matrix.matrix = storedMatrix.data; + matrix.columnToFrameMap = new Map( + Object.entries(storedMatrix.columnToFrameMap), + ); + matrix.rowToStickyNotesMap = new Map( + Object.entries(storedMatrix.rowToStickyNotesMap), + ); + if (storedMatrix.rowNames) { + matrix.rowNames = new Map(Object.entries(storedMatrix.rowNames)); + } + if (storedMatrix.columnNames) { + matrix.columnNames = new Map(Object.entries(storedMatrix.columnNames)); + } + return matrix; + } + return null; +} + +// Call this function when the board is loaded +loadMatrixFromBoard(); + ////////////////////////////////////////////////////////////////////////////////////////////////// class MatrixCell { constructor(value, stickyNoteId) { @@ -214,6 +298,23 @@ class Matrix { } // Additional methods to manage the matrix, rows, and columns can be added here + findCellByStickyNoteId(stickyNoteId) { + console.log("Finding cell by sticky note id: __line 261__", stickyNoteId); + console.log( + "Finding cell by sticky note id: __line 262__", + this.matrix.matrix, + ); + + for (let row = 0; row < this.rows; row++) { + for (let col = 0; col < this.columns; col++) { + const cell = this.matrix[row][col]; + if (cell && cell.value.stickyNotesIds.includes(stickyNoteId)) { + return cell; + } + } + } + return null; + } } ////////////////////////////////////////////////////////////////////////////////////////////////// @@ -345,8 +446,7 @@ async function createMatrix() { return; } - // Create the matrix - const matrix = new Matrix(rowsCount, columnsCount); + g_matrix = new Matrix(rowsCount, columnsCount); // Create frames for each column for (let j = 0; j < columnsCount; j++) { @@ -360,13 +460,13 @@ async function createMatrix() { fillColor: "#ffffff", // Set background color to white }, }); - matrix.linkColumnToFrame(j, frame.id); + g_matrix.linkColumnToFrame(j, frame.id); } - console.log("Matrix created: 366", matrix); + console.log("Matrix created: 366", g_matrix); // Create sticky notes for each cell in each column for (let j = 0; j < columnsCount; j++) { - const frameId = matrix.getFrameForColumn(j); + const frameId = g_matrix.getFrameForColumn(j); const frame = await board.getById(frameId); //safa here const squarePositions = calculateBestSquaresInRectangle( @@ -380,29 +480,6 @@ async function createMatrix() { // // Calculate the upper-left corner of the frame const frameLeft = frame.x - frame.width / 2; const frameTop = frame.y - frame.height / 2; - // - // - // // Create sticky notes - // const createdNotes = []; - // for (let i = 0; i < noOfSquares; i++) { - // const row = Math.floor(i / squarePositions.gridInfo.columns); - // const col = i % squarePositions.gridInfo.columns; - // const position = squarePositions.placement.getSquarePosition(row, col); - // - // const stickyNote = await board.createStickyNote({ - // x : frameLeft + position.x + squarePositions.gridInfo.effectiveSquareSize / 2, - // y: frameTop + position.y + squarePositions.gridInfo.effectiveSquareSize / 2, - // width: squarePositions.gridInfo.effectiveSquareSize, - // style: { - // fillColor: "light_yellow", - // } - // }); - - // safa here - - // Calculate optimal sticky note size - // const stickyNoteSize = calculateStickyNoteSize(frame.width, frame.height, rowsCount, columnsCount); - // const offsetSize = stickyNoteSize * 0.15; for (let i = 0; i < rowsCount; i++) { const row = Math.floor(i / squarePositions.gridInfo.columns); @@ -424,15 +501,9 @@ async function createMatrix() { fillColor: "light_yellow", }, }); - // const stickyNote = await miro.board.createStickyNote({ - // content: `Cell ${i + 1},${j + 1}`, - // x: frame.x + (j * (stickyNoteSize + offsetSize)) + stickyNoteSize / 2, - // y: frame.y + (i * (stickyNoteSize + offsetSize)) + stickyNoteSize / 2, - // width: stickyNoteSize, - // }); await frame.add(stickyNote); - matrix.setCell( + g_matrix.setCell( i, j, new Trait([stickyNote.id], `Cell ${i + 1},${j + 1}`), @@ -440,10 +511,143 @@ async function createMatrix() { } } - console.log("Matrix created:", matrix); - return matrix; + console.log("Matrix created: 421", g_matrix); + // Save the matrix to the board after creation + await saveMatrixToBoard(g_matrix); + + console.log("Matrix created and saved:", g_matrix); } +// Function to check if content of selected sticky note has changed +// async function checkStickyNoteContentChange(item) { +// if (item.type === "sticky_note") { +// const currentContent = item.content; +// +// if (!item.originalContent) { +// // If it's newly selected, store the original content and print it +// item.originalContent = currentContent; +// console.log(`Sticky note ${item.id} selected. Content:`, currentContent); +// } else { +// // If it's already selected, check for changes +// if (currentContent !== item.originalContent) { +// console.log(`Content changed for sticky note ${item.id}`); +// console.log(`Original: ${item.originalContent}`); +// console.log(`New: ${currentContent}`); +// +// // Update the original content +// item.originalContent = currentContent; +// } +// } +// } +// } + +// Update the selection:update event listener +let previouslySelectedItems = []; +let currentlySelectedItems = []; +board.ui.on("selection:update", async (event) => { + if (!g_matrix) { + await loadMatrix(); // todo: either this or loadmatrixfromboard + } + + // Store the previously selected items for comparison + // Get the currently selected items + currentlySelectedItems = event.items; + + // Check if only one item is selected and it's a sticky note + if ( + currentlySelectedItems.length === 1 && + currentlySelectedItems[0].type === "sticky_note" + ) { + const selectedStickyNote = currentlySelectedItems[0]; + + // Check if the sticky note's id exists in the matrix + console.log("Selected Sticky Note:", selectedStickyNote); + console.log("Matrix:", g_matrix); + if (g_matrix == null) { + g_matrix = await loadMatrixFromBoard(); // todo: either this or loadmatrixfromboard + console.log("Matrix was null and is now loaded from the board"); + } + if (g_matrix.findCellByStickyNoteId(selectedStickyNote.id)) { + // Double the size of the sticky note + selectedStickyNote.width = selectedStickyNote.width * 2; + previouslySelectedItems.push(selectedStickyNote); + // Find the row and column of the selected sticky note + let selectedRow, selectedCol; + for (let row = 0; row < g_matrix.rows; row++) { + for (let col = 0; col < g_matrix.columns; col++) { + const cell = g_matrix.getCell(row, col); + if ( + cell && + cell.value.stickyNotesIds.includes(selectedStickyNote.id) + ) { + selectedRow = row; + selectedCol = col; + break; + } + } + if (selectedRow !== undefined) break; + } + + // If we found the selected sticky note in the matrix + if (selectedRow !== undefined && selectedCol !== undefined) { + // Double the size of corresponding sticky notes in other columns + for (let col = 0; col < g_matrix.columns; col++) { + if (col !== selectedCol) { + const cell = g_matrix.getCell(selectedRow, col); + if (cell && cell.value.stickyNotesIds.length > 0) { + const correspondingStickyNote = await board.getById( + cell.value.stickyNotesIds[0], + ); + if (correspondingStickyNote) { + correspondingStickyNote.width *= 2; + // Add the corresponding sticky note to the previously selected items + previouslySelectedItems.push(correspondingStickyNote); + await correspondingStickyNote.sync(); + } + } + } + } + } + await selectedStickyNote.sync(); + } + } else { + // User has either deselected everything or selected something else + // Restore the size of all previously selected sticky notes + for (const prevSelectedItem of previouslySelectedItems) { + prevSelectedItem.width /= 2; + await prevSelectedItem.sync(); + } + + // Clear the previouslySelectedItems array + previouslySelectedItems = []; + + // If a new sticky note is selected (that's not in the matrix), update previouslySelectedItems + if ( + currentlySelectedItems.length === 1 && + currentlySelectedItems[0].type === "sticky_note" + ) { + const newSelectedStickyNote = currentlySelectedItems[0]; + if ( + g_matrix && + !g_matrix.findCellByStickyNoteId(newSelectedStickyNote.id) + ) { + previouslySelectedItems.push(newSelectedStickyNote); + } + } + } + + // if the selected item is a sticky note, and its id exists in the matrix, then double its size + // if previously sticky note is not selected anymore then restore its original size +}); + +// Example usage: +// const selectedStickyNote = await detectSelectedStickyNote(); +// if (selectedStickyNote) { +// // Do something with the selected sticky note +// } + +//safa here + document.getElementById("createMatrix").addEventListener("click", createMatrix); document.getElementById("debugButton").addEventListener("click", async () => { @@ -530,93 +734,6 @@ document.getElementById("debugButton").addEventListener("click", async () => { // }); // Add this listener after your existing code -board.ui.on("selection:update", async (event) => { - const selectedItems = event.items; - - for (const item of selectedItems) { - if (item.type === "sticky_note") { - const metadata = await item.getMetadata("myApp"); - console.log(`Metadata for sticky note ${item.id}:`, metadata); - console.log(`Tags for sticky note ${item.id}:`, item.tagIds); - } - } -}); - -// ... rest of your code ... - -// const originalSizes = new Map(); // Store original sizes for each sticky note -// const resizedStickyNotes = new Set(); // Store the IDs of resized sticky notes - -// // Function to double the size of a sticky note -// async function doubleSize(stickyNote) { -// const { width, height } = stickyNote; -// -// // Save the original size if not already saved -// if (!originalSizes.has(stickyNote.id)) { -// originalSizes.set(stickyNote.id, { width, height }); -// } -// stickyNote.width = width * 2; -// stickyNote.sync(); -// -// resizedStickyNotes.add(stickyNote.id); // Track resized sticky note -// } - -// // Function to reset the sticky note size to its original size -// async function resetSize(stickyNote) { -// const originalSize = originalSizes.get(stickyNote.id); -// if (originalSize) { -// -// stickyNote.width = originalSize.width; -// stickyNote.sync(); -// //stickyNote.height= originalSize.height; -// //stickyNote.sync(); -// -// // Remove from the resized sticky notes set and the original size map -// resizedStickyNotes.delete(stickyNote.id); -// originalSizes.delete(stickyNote.id); -// } -// } - -// Event listener for selection changes -// miro.board.ui.on('selection:update', async (event) => { -// const selectedWidgets = event.items; -// -// for (const widget of selectedWidgets) { -// if (widget.type === 'sticky_note') { -// // Get the metadata of the selected sticky note -// const metadata = await widget.getMetadata('myApp'); -// -// // Check if the sticky note has a pairId -// if (metadata && metadata.pairId && metadata.pairedNoteId) { -// const pairedNoteId = metadata.pairedNoteId; -// -// // Double the size of the selected sticky note -// await doubleSize(widget); -// -// // Find the paired sticky note and double its size -// const [pairedNote] = await miro.board.widgets.get({ id: pairedNoteId }); -// if (pairedNote) { -// await doubleSize(pairedNote); -// } -// } else { -// // If the sticky note doesn't have a pair, just resize the selected one -// await doubleSize(widget); -// } -// } -// } -// -// // Handle unselected sticky notes by restoring their original size -// const allStickyNotes = await miro.board.widgets.get({ type: 'sticky_note' }); -// const unselectedStickyNotes = allStickyNotes.filter( -// (stickyNote) => !selectedWidgets.some((selected) => selected.id === stickyNote.id) -// ); -// -// for (const stickyNote of unselectedStickyNotes) { -// if (resizedStickyNotes.has(stickyNote.id)) { -// await resetSize(stickyNote); -// } -// } -// }); function calculateBestSquaresInRectangle( rectangleWidth, @@ -754,3 +871,20 @@ function calculateBestSquaresInRectangle( // // Rest of your function... // } + +// Add a function to load the matrix when needed +async function loadMatrix() { + g_matrix = await loadMatrixFromBoard(); + if (g_matrix) { + console.log("Matrix loaded from board:", g_matrix); + } else { + console.log("No matrix found on the board"); + } +} + +// // Add a function to clear the matrix data if needed +// async function clearMatrixData() { +// await board.setAppData('benefitTraitMatrix', null); +// g_matrix = null; +// console.log("Matrix data cleared from board"); +// } From 55fea6a5e250de60bb8c231bc8cf7e668fbb179d Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Thu, 17 Oct 2024 23:00:50 +0200 Subject: [PATCH 06/23] code cleanup and refactor --- examples/sticky-pair-clusterer/src/app.js | 386 ++-------------------- 1 file changed, 31 insertions(+), 355 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 2bc0694ca..183a91a76 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -85,23 +85,22 @@ loadMatrixFromBoard(); ////////////////////////////////////////////////////////////////////////////////////////////////// class MatrixCell { - constructor(value, stickyNoteId) { - this.value = value; // Integer value between 1-8 + constructor(stickyNoteId) { this.stickyNoteId = stickyNoteId; // Reference to a sticky note } } class Matrix { constructor(rows, columns) { - this.rows = rows; - this.columns = columns; + this.rows = rows; // traits + this.columns = columns; // benefits this.matrix = Array.from({ length: rows }, () => Array(columns).fill(null)); this.columnToFrameMap = new Map(); // Maps column index to frame ID this.rowToStickyNotesMap = new Map(); // Maps row index to an array of sticky note IDs } - setCell(row, col, value, stickyNoteId) { - const cell = new MatrixCell(value, stickyNoteId); + setCell(row, col, stickyNoteId) { + const cell = new MatrixCell(stickyNoteId); this.matrix[row][col] = cell; // Add sticky note ID to the row map @@ -128,29 +127,9 @@ class Matrix { return this.matrix[row][col]; } - updateCell(row, col, value, stickyNoteId) { + updateCell(row, col, stickyNoteId) { if (row >= 0 && row < this.rows && col >= 0 && col < this.columns) { - this.setCell(row, col, value, stickyNoteId); - return true; - } - return false; - } - - deleteCell(row, col) { - if (row >= 0 && row < this.rows && col >= 0 && col < this.columns) { - this.matrix[row][col] = null; - - // Remove sticky note ID from the row map - const stickyNotes = this.rowToStickyNotesMap.get(row); - if (stickyNotes) { - const index = stickyNotes.findIndex( - (id) => id === this.matrix[row][col]?.stickyNoteId, - ); - if (index !== -1) { - stickyNotes.splice(index, 1); - } - } - + this.setCell(row, col, stickyNoteId); return true; } return false; @@ -299,17 +278,11 @@ class Matrix { // Additional methods to manage the matrix, rows, and columns can be added here findCellByStickyNoteId(stickyNoteId) { - console.log("Finding cell by sticky note id: __line 261__", stickyNoteId); - console.log( - "Finding cell by sticky note id: __line 262__", - this.matrix.matrix, - ); - for (let row = 0; row < this.rows; row++) { for (let col = 0; col < this.columns; col++) { const cell = this.matrix[row][col]; - if (cell && cell.value.stickyNotesIds.includes(stickyNoteId)) { - return cell; + if (cell && cell.stickyNoteId === stickyNoteId) { + return { cell, row, col }; } } } @@ -360,71 +333,7 @@ class Matrix { // console.log(matrix.getFrameForColumn(0)); // 'frame1' ////////////////////////////////////////////////////////////////////////////////////////////////// -class Trait { - constructor(stickyNotesIds, content = "", tags = []) { - this.stickyNotesIds = stickyNotesIds; - this.content = content; - this.tags = tags; - } - - addTag(tag) { - if (!this.tags.includes(tag)) { - this.tags.push(tag); - } - } - - removeTag(tag) { - const index = this.tags.indexOf(tag); - if (index !== -1) { - this.tags.splice(index, 1); - } - } - - setContent(content) { - this.content = content; - } -} - -async function createBenefitTraitMatrix(benefits, traits) { - const matrix = []; - - // Create rows for each benefit - for (let i = 0; i < benefits; i++) { - // Create a new frame with 1080 16:9 aspect ratio - const newFrame = await board.createFrame({ - title: `Benefit ${i + 1}`, - x: i * 1100, // Offset each frame horizontally - y: 0, - width: 1080, - height: 608, - }); - console.log(`Created frame ${newFrame.id}`); - const row = []; - - // Create cells for each trait - for (let j = 0; j < traits; j++) { - // create a new sticky note and place it in the new frame - const newStickyNote = await board.createStickyNote({ - x: 0, - y: 0, - width: 100, - height: 100, - content: `Trait ${j + 1}`, - frameId: newFrame.id, - }); - // create a new trait from the new sticky note and place it in the row - const newTrait = new Trait([newStickyNote.id], `Trait ${j + 1}`); - row.push(newTrait); - } - - matrix.push(row); - } - - return matrix; -} - // Example usage: -// const benefitTraitMatrix = createBenefitTraitMatrix(benefits, traits); async function createMatrix() { console.log("Create Matrix button clicked"); @@ -462,22 +371,17 @@ async function createMatrix() { }); g_matrix.linkColumnToFrame(j, frame.id); } - console.log("Matrix created: 366", g_matrix); // Create sticky notes for each cell in each column for (let j = 0; j < columnsCount; j++) { const frameId = g_matrix.getFrameForColumn(j); const frame = await board.getById(frameId); - //safa here const squarePositions = calculateBestSquaresInRectangle( frame.width, frame.height, rowsCount, ); - console.log("Square Positions:", squarePositions); - // - // // Calculate the upper-left corner of the frame const frameLeft = frame.x - frame.width / 2; const frameTop = frame.y - frame.height / 2; @@ -503,16 +407,9 @@ async function createMatrix() { }); await frame.add(stickyNote); - g_matrix.setCell( - i, - j, - new Trait([stickyNote.id], `Cell ${i + 1},${j + 1}`), - ); + g_matrix.setCell(i, j, stickyNote.id); } } - - console.log("Matrix created: 421", g_matrix); - // Save the matrix to the board after creation await saveMatrixToBoard(g_matrix); console.log("Matrix created and saved:", g_matrix); @@ -543,198 +440,51 @@ async function createMatrix() { // Update the selection:update event listener let previouslySelectedItems = []; -let currentlySelectedItems = []; board.ui.on("selection:update", async (event) => { if (!g_matrix) { - await loadMatrix(); // todo: either this or loadmatrixfromboard + await loadMatrixFromBoard(); // todo: either this or loadmatrixfromboard } // Store the previously selected items for comparison // Get the currently selected items - currentlySelectedItems = event.items; // Check if only one item is selected and it's a sticky note - if ( - currentlySelectedItems.length === 1 && - currentlySelectedItems[0].type === "sticky_note" - ) { - const selectedStickyNote = currentlySelectedItems[0]; - - // Check if the sticky note's id exists in the matrix - console.log("Selected Sticky Note:", selectedStickyNote); - console.log("Matrix:", g_matrix); - if (g_matrix == null) { - g_matrix = await loadMatrixFromBoard(); // todo: either this or loadmatrixfromboard - console.log("Matrix was null and is now loaded from the board"); + if (event.items.length === 1 && event.items[0].type === "sticky_note") { + const result = g_matrix.findCellByStickyNoteId(event.items[0].id); + if (null == result) { + return; } - if (g_matrix.findCellByStickyNoteId(selectedStickyNote.id)) { - // Double the size of the sticky note - selectedStickyNote.width = selectedStickyNote.width * 2; - previouslySelectedItems.push(selectedStickyNote); - // Find the row and column of the selected sticky note - let selectedRow, selectedCol; - for (let row = 0; row < g_matrix.rows; row++) { - for (let col = 0; col < g_matrix.columns; col++) { - const cell = g_matrix.getCell(row, col); - if ( - cell && - cell.value.stickyNotesIds.includes(selectedStickyNote.id) - ) { - selectedRow = row; - selectedCol = col; - break; - } - } - if (selectedRow !== undefined) break; - } - - // If we found the selected sticky note in the matrix - if (selectedRow !== undefined && selectedCol !== undefined) { - // Double the size of corresponding sticky notes in other columns - for (let col = 0; col < g_matrix.columns; col++) { - if (col !== selectedCol) { - const cell = g_matrix.getCell(selectedRow, col); - if (cell && cell.value.stickyNotesIds.length > 0) { - const correspondingStickyNote = await board.getById( - cell.value.stickyNotesIds[0], - ); - if (correspondingStickyNote) { - correspondingStickyNote.width *= 2; - // Add the corresponding sticky note to the previously selected items - previouslySelectedItems.push(correspondingStickyNote); - await correspondingStickyNote.sync(); - } - } - } + // Double the size of all sticky notes in the same row + for (let col = 0; col < g_matrix.columns; col++) { + const cell = g_matrix.matrix[result.row][col]; + if (cell && cell.stickyNoteId) { + let stickyNote = await board.getById(cell.stickyNoteId); + // Add the sticky note to the previously selected items + if (stickyNote) { + previouslySelectedItems.push(stickyNote.id); + stickyNote.width *= 2; + await stickyNote.sync(); } } - await selectedStickyNote.sync(); } } else { // User has either deselected everything or selected something else // Restore the size of all previously selected sticky notes - for (const prevSelectedItem of previouslySelectedItems) { - prevSelectedItem.width /= 2; - await prevSelectedItem.sync(); + for (const prevSelectedItemId of previouslySelectedItems) { + const prevSelectedItem = await board.getById(prevSelectedItemId); + if (prevSelectedItem) { + prevSelectedItem.width /= 2; + await prevSelectedItem.sync(); + } } // Clear the previouslySelectedItems array previouslySelectedItems = []; - - // If a new sticky note is selected (that's not in the matrix), update previouslySelectedItems - if ( - currentlySelectedItems.length === 1 && - currentlySelectedItems[0].type === "sticky_note" - ) { - const newSelectedStickyNote = currentlySelectedItems[0]; - if ( - g_matrix && - !g_matrix.findCellByStickyNoteId(newSelectedStickyNote.id) - ) { - previouslySelectedItems.push(newSelectedStickyNote); - } - } } - - // if the selected item is a sticky note, and its id exists in the matrix, then double its size - // if previously sticky note is not selected anymore then restore its original size }); -// Example usage: -// const selectedStickyNote = await detectSelectedStickyNote(); -// if (selectedStickyNote) { -// // Do something with the selected sticky note -// } - -//safa here - document.getElementById("createMatrix").addEventListener("click", createMatrix); -document.getElementById("debugButton").addEventListener("click", async () => { - console.log("Debug button clicked"); - //call the createBenefitTraitMatrix function with 10 benefits and 10 traits - const benefitTraitMatrix = await createBenefitTraitMatrix(10, 10); - console.log(benefitTraitMatrix); -}); -// -// document.getElementById("createPair").addEventListener("click", async () => { -// -// const pairId = `pair_${Date.now()}`; // Unique pair identifier based on timestamp -// -// const frames = await board.get({ type: "frame" }); -// if (frames.length === 0) { -// alert("No frames found on the board. Please create a frame first."); -// return; -// } -// -// // Read the number of squares from the input field -// let noOfSquares = parseInt( -// document.getElementById("traitsCount").value, -// 10, -// ); -// -// // Validate the input -// if (isNaN(noOfSquares) || noOfSquares < 1 || noOfSquares > 200) { -// alert("Please enter a valid number of squares between 1 and 200."); -// return; -// } -// -// noOfSquares = noOfSquares * 2; -// const frame = await findFrameByName("Benefit Template"); -// const targetFrame = frame; -// const frameDimensions = await getFrameDimensions(frame); -// const squarePositions = calculateBestSquaresInRectangle( -// frameDimensions.width, -// frameDimensions.height, -// noOfSquares, -// ); -// console.log("Square Positions:", squarePositions); -// -// // Calculate the upper-left corner of the frame -// const frameLeft = targetFrame.x - targetFrame.width / 2; -// const frameTop = targetFrame.y - targetFrame.height / 2; -// -// -// // Create sticky notes -// const createdNotes = []; -// for (let i = 0; i < noOfSquares; i++) { -// const row = Math.floor(i / squarePositions.gridInfo.columns); -// const col = i % squarePositions.gridInfo.columns; -// const position = squarePositions.placement.getSquarePosition(row, col); -// -// const stickyNote = await board.createStickyNote({ -// x : frameLeft + position.x + squarePositions.gridInfo.effectiveSquareSize / 2, -// y: frameTop + position.y + squarePositions.gridInfo.effectiveSquareSize / 2, -// width: squarePositions.gridInfo.effectiveSquareSize, -// style: { -// fillColor: "light_yellow", -// } -// }); -// -// createdNotes.push(stickyNote); -// } -// -// // Link sticky notes in pairs and update content -// for (let i = 0; i < createdNotes.length; i += 2) { -// const sticky1 = createdNotes[i]; -// const sticky2 = createdNotes[i + 1]; -// -// await sticky1.setMetadata('myApp', { itsPair: sticky2.id }); -// await sticky2.setMetadata('myApp', { itsPair: sticky1.id }); -// -// -// sticky1.content = `ID: ${sticky1.id}\nPair ID: ${sticky2.id}\nPair: ${Math.floor(i/2) + 1} of ${noOfSquares/2}`; -// await sticky1.sync(); -// -// sticky2.content = `ID: ${sticky2.id}\nPair ID: ${sticky1.id}\nPair: ${Math.floor(i/2) + 1} of ${noOfSquares/2}`; -// await sticky2.sync(); -// } -// -// console.log(`Created ${noOfSquares} sticky notes for pair ${pairId}`); -// }); - -// Add this listener after your existing code - function calculateBestSquaresInRectangle( rectangleWidth, rectangleHeight, @@ -814,77 +564,3 @@ function calculateBestSquaresInRectangle( totalSquares: nrows * ncols, }; } - -// function calculateOptimalLayout(frameWidth, frameHeight, totalSquares) { -// let bestSize = 0; -// let bestRows = 0; -// let bestColumns = 0; - -// // Try different column counts (must be even) -// for (let columns = 2; columns <= Math.sqrt(totalSquares) * 2; columns += 2) { -// const rows = Math.ceil(totalSquares / columns); -// const sizeByWidth = frameWidth / (columns * 1.15); -// const sizeByHeight = frameHeight / (rows * 1.15); -// const size = Math.min(sizeByWidth, sizeByHeight); - -// if (size > bestSize) { -// bestSize = size; -// bestRows = rows; -// bestColumns = columns; -// } -// } - -// return { -// stickyNoteSize: bestSize, -// rows: bestRows, -// columns: bestColumns -// }; -// } - -// Usage in your main function -// async function createPairMatrix(rowsCount, columnsCount) { -// const frame = await board.createFrame({ -// title: 'Pair Matrix', -// width: 1920, -// height: 1080 -// }); - -// const totalSquares = rowsCount * columnsCount; -// const layout = calculateOptimalLayout(frame.width, frame.height, totalSquares); - -// const matrix = new Matrix(rowsCount, columnsCount); -// const offsetSize = layout.stickyNoteSize * 0.15; - -// for (let i = 0; i < rowsCount; i++) { -// for (let j = 0; j < columnsCount; j++) { -// const stickyNote = await board.createStickyNote({ -// content: `Cell ${i + 1},${j + 1}`, -// x: frame.x + (j % layout.columns) * (layout.stickyNoteSize + offsetSize) + layout.stickyNoteSize / 2, -// y: frame.y + Math.floor(j / layout.columns) * (layout.stickyNoteSize + offsetSize) + layout.stickyNoteSize / 2, -// width: layout.stickyNoteSize, -// }); -// await frame.add(stickyNote); - -// matrix.setCell(i, j, 0, stickyNote.id); -// } -// } - -// // Rest of your function... -// } - -// Add a function to load the matrix when needed -async function loadMatrix() { - g_matrix = await loadMatrixFromBoard(); - if (g_matrix) { - console.log("Matrix loaded from board:", g_matrix); - } else { - console.log("No matrix found on the board"); - } -} - -// // Add a function to clear the matrix data if needed -// async function clearMatrixData() { -// await board.setAppData('benefitTraitMatrix', null); -// g_matrix = null; -// console.log("Matrix data cleared from board"); -// } From 693c5610e2175dbe873c2db73a9495ab7547612d Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Thu, 17 Oct 2024 23:13:24 +0200 Subject: [PATCH 07/23] store metadata on board so that when the board is closed and reopened info is restored --- examples/sticky-pair-clusterer/src/app.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 183a91a76..27ce2dcee 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -53,6 +53,8 @@ async function saveMatrixToBoard(matrix) { : undefined, }); await board.setAppData("benefitTraitMatrix", matrixData); + let matrixStoredData = await board.getAppData("benefitTraitMatrix"); + console.log("Matrix read back from board:", matrixStoredData); console.log("Matrix saved to board:", matrixData); } @@ -75,14 +77,12 @@ async function loadMatrixFromBoard() { if (storedMatrix.columnNames) { matrix.columnNames = new Map(Object.entries(storedMatrix.columnNames)); } + g_matrix = matrix; return matrix; } return null; } -// Call this function when the board is loaded -loadMatrixFromBoard(); - ////////////////////////////////////////////////////////////////////////////////////////////////// class MatrixCell { constructor(stickyNoteId) { @@ -441,8 +441,13 @@ async function createMatrix() { // Update the selection:update event listener let previouslySelectedItems = []; board.ui.on("selection:update", async (event) => { - if (!g_matrix) { + if (g_matrix === null) { await loadMatrixFromBoard(); // todo: either this or loadmatrixfromboard + if (g_matrix === null) { + console.log("Matrix not found"); + return; + } + console.log("Matrix loaded from board:", g_matrix); } // Store the previously selected items for comparison From 953b604459adfa8ec22f6f7e565294b3fe89e548 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Thu, 17 Oct 2024 23:26:23 +0200 Subject: [PATCH 08/23] Copy the content of one sticky note when it changes to its corresponding sticky notes in other frames --- examples/sticky-pair-clusterer/src/app.js | 32 ++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 27ce2dcee..d02d40175 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -440,6 +440,7 @@ async function createMatrix() { // Update the selection:update event listener let previouslySelectedItems = []; +let previouslySelectedContent = { id: null, content: null }; board.ui.on("selection:update", async (event) => { if (g_matrix === null) { await loadMatrixFromBoard(); // todo: either this or loadmatrixfromboard @@ -472,6 +473,10 @@ board.ui.on("selection:update", async (event) => { } } } + previouslySelectedContent = { + id: event.items[0].id, + content: event.items[0].content, + }; } else { // User has either deselected everything or selected something else // Restore the size of all previously selected sticky notes @@ -482,7 +487,32 @@ board.ui.on("selection:update", async (event) => { await prevSelectedItem.sync(); } } - + if ( + previouslySelectedContent.content !== + (await board.getById(previouslySelectedContent.id)).content + ) { + console.log( + "Content changed for sticky note:", + previouslySelectedContent.id, + ); + const result = g_matrix.findCellByStickyNoteId( + previouslySelectedContent.id, + ); + if (result) { + const newContent = (await board.getById(previouslySelectedContent.id)) + .content; + for (let col = 0; col < g_matrix.columns; col++) { + const cell = g_matrix.matrix[result.row][col]; + if (cell && cell.stickyNoteId) { + let stickyNote = await board.getById(cell.stickyNoteId); + if (stickyNote) { + stickyNote.content = newContent; + await stickyNote.sync(); + } + } + } + } + } // Clear the previouslySelectedItems array previouslySelectedItems = []; } From 63f037aed7c2d3ace274cfbcd38a92afd35ae960 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Fri, 18 Oct 2024 12:34:30 +0200 Subject: [PATCH 09/23] added button to assign random tags for debug purpose --- examples/sticky-pair-clusterer/app.html | 8 +- examples/sticky-pair-clusterer/src/app.js | 165 +++++++++++++++++++++- 2 files changed, 168 insertions(+), 5 deletions(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index aa8b87e51..05b0a476e 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -33,7 +33,13 @@ - + + diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index d02d40175..67546176e 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -38,7 +38,7 @@ const { board } = window.miro; // Global definitions let g_matrix = null; - +// let g_tags = null; // Function to save the matrix to the board async function saveMatrixToBoard(matrix) { const matrixData = JSON.stringify({ @@ -488,8 +488,9 @@ board.ui.on("selection:update", async (event) => { } } if ( + previouslySelectedContent !== null && previouslySelectedContent.content !== - (await board.getById(previouslySelectedContent.id)).content + (await board.getById(previouslySelectedContent.id)).content ) { console.log( "Content changed for sticky note:", @@ -518,8 +519,6 @@ board.ui.on("selection:update", async (event) => { } }); -document.getElementById("createMatrix").addEventListener("click", createMatrix); - function calculateBestSquaresInRectangle( rectangleWidth, rectangleHeight, @@ -599,3 +598,161 @@ function calculateBestSquaresInRectangle( totalSquares: nrows * ncols, }; } + +async function assignRandomTagsToSelection() { + console.log("Assign Random Tags To Selection button clicked"); + + async function assignRandomTags() { + const validTags = await createRandomTags(); + console.log("Valid tags:", validTags); + // Get currently selected items + const selectedItems = await board.getSelection(); + const stickyNotes = selectedItems.filter( + (item) => item.type === "sticky_note", + ); + + if (stickyNotes.length === 0) { + console.log("No sticky notes selected"); + return; + } + + // Randomly assign or not assign tags to selected sticky notes + const chanceToNotAssign = 1 / (validTags.length + 1); + for (const stickyNote of stickyNotes) { + if (Math.random() >= chanceToNotAssign) { + const randomTag = + validTags[Math.floor(Math.random() * validTags.length)]; + stickyNote.tagIds = [randomTag.id]; + stickyNote.sync(); + console.log( + `Assigned tag "${randomTag.title}" to sticky note ${stickyNote.id}`, + ); + } else { + console.log(`No tag assigned to sticky note ${stickyNote.id}`); + } + } + } + + await assignRandomTags(); +} + +// async function createTags() { +// console.log("Create Tags button clicked"); + +// try { +// const tags = await getOrCreateTags(); +// console.log("Tags created or retrieved:", tags); + +// // Display a confirmation message to the user +// await board.notifications.showInfo('Tags have been created or updated successfully.'); +// } catch (error) { +// console.error("Error creating tags:", error); + +// // Display an error message to the user +// await board.notifications.showError('An error occurred while creating tags. Please try again.'); +// } +// } +async function createRandomTags() { + console.log("Creating random tags"); + + let tagDefinitions = [ + { title: "Very Important", color: "red" }, + { title: "Highly Important", color: "yellow" }, + { title: "Moderately Important", color: "light_green" }, + { title: "Somewhat Important", color: "cyan" }, + { title: "Low Importance", color: "blue" }, + ]; + + try { + const createdTags = []; + // Function to generate a random postfix + const generateRandomPostfix = () => { + return Math.floor(Math.random() * 1000) + .toString() + .padStart(3, "0"); + }; + + // Modify tagDefinitions to include random postfix + tagDefinitions = tagDefinitions.map((tagDef) => ({ + ...tagDef, + title: `${tagDef.title} #${generateRandomPostfix()}`, + })); + + for (const tagDef of tagDefinitions) { + const newTag = await board.createTag({ + title: tagDef.title, + color: tagDef.color, + }); + + createdTags.push(newTag); + console.log(`Created tag: ${tagDef.title} with color ${tagDef.color}`); + } + + console.log("Tags created:", createdTags); + await board.notifications.showInfo("Tags have been created successfully."); + return createdTags; + } catch (error) { + console.error("Error creating tags:", error); + await board.notifications.showError( + "An error occurred while creating tags. Please try again.", + ); + throw error; + } +} + +// Add event listener for a new button to create random tags + +document.getElementById("createMatrix").addEventListener("click", createMatrix); +//document.getElementById("createTags").addEventListener("click", createTags); +document + .getElementById("assignRandomTagsToSelection") + .addEventListener("click", assignRandomTagsToSelection); + +// async function getOrCreateTags() { +// // Check if g_tags is available in appdata +// console.log("getOrCreateTags, g_tags:", g_tags); +// if (g_tags === null) { +// try { +// const storedTags = await board.getAppData('g_tags'); +// g_tags = JSON.parse(storedTags); +// if (g_tags !== null && g_tags.length > 0) { +// console.log('Tags retrieved from appdata:', g_tags); +// return g_tags; +// } else { +// console.log('Stored tags array is empty'); +// } +// } catch (error) { +// console.error('Error parsing stored tags:', error); +// } +// } + +// const tagDefinitions = [ +// { title: "Very Important", color: "red" }, +// { title: "Highly Important", color: "yellow" }, +// { title: "Moderately Important", color: "light_green" }, +// { title: "Somewhat Important", color: "cyan" }, +// { title: "Low Importance", color: "blue" } +// ]; +// const validTags = []; +// for (const tagDef of tagDefinitions) { +// try { +// const boardTag = await board.createTag({ +// title: tagDef.title, +// color: tagDef.color, +// }); +// validTags.push(boardTag); +// } catch (error) { +// if (error.message.includes("The title must be unique")) { +// console.log(`Tag "${tagDef.title}" already exists. Skipping creation.`); +// } else { +// throw error; +// } +// } +// } +// g_tags = validTags; +// console.log('g_tags: line no. 645', g_tags); +// await board.setAppData('g_tags', JSON.stringify(g_tags)); +// const readBack = await board.getAppData('g_tags') +// console.log('Tags stored in appdata:', readBack); +// return validTags; +// } From 7c2be055a39386d7fd66c923464d3f8da4d1f121 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Fri, 18 Oct 2024 18:13:46 +0200 Subject: [PATCH 10/23] Update matrix so that it contains tags and create predefined tags for debug and automation --- examples/sticky-pair-clusterer/src/app.js | 260 +++++++++------------- 1 file changed, 107 insertions(+), 153 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 67546176e..3d42f4de1 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -38,8 +38,18 @@ const { board } = window.miro; // Global definitions let g_matrix = null; +let g_tagDefinitions = [ + { title: "Very Important", color: "red", id: null }, + { title: "Highly Important", color: "yellow", id: null }, + { title: "Moderately Important", color: "light_green", id: null }, + { title: "Low Importance", color: "cyan", id: null }, + { title: "Not Important", color: "blue", id: null }, +]; // let g_tags = null; // Function to save the matrix to the board +// app setup on load functions should be called in this order +loadMatrixFromBoard(); +getSetPredefinedTagsFromBoard(); async function saveMatrixToBoard(matrix) { const matrixData = JSON.stringify({ rows: matrix.rows, @@ -87,6 +97,8 @@ async function loadMatrixFromBoard() { class MatrixCell { constructor(stickyNoteId) { this.stickyNoteId = stickyNoteId; // Reference to a sticky note + this.tagIds = []; + this.tagTitles = []; } } @@ -485,8 +497,18 @@ board.ui.on("selection:update", async (event) => { if (prevSelectedItem) { prevSelectedItem.width /= 2; await prevSelectedItem.sync(); + // potentially the tags have changed, so we need to update the tags for the cell + const result = g_matrix.findCellByStickyNoteId(prevSelectedItemId); + if (result) { + updateCellTags(result.row, result.col, prevSelectedItem.tagIds); + console.log( + `Updated tagIds for cell at row ${result.row}, col ${result.col}:`, + prevSelectedItem.tagIds, + ); + } } } + console.log(g_matrix); if ( previouslySelectedContent !== null && previouslySelectedContent.content !== @@ -519,6 +541,91 @@ board.ui.on("selection:update", async (event) => { } }); +async function assignRandomTagsToSelection() { + console.log("Assign Random Tags To Selection button clicked"); + g_tagDefinitions = await getSetPredefinedTagsFromBoard(); + + async function assignRandomTags() { + // Get currently selected items + const selectedItems = await board.getSelection(); + const stickyNotes = selectedItems.filter( + (item) => item.type === "sticky_note", + ); + + // Randomly assign or not assign tags to selected sticky notes + const chanceToNotAssign = 1 / (g_tagDefinitions.length + 1); + for (const stickyNote of stickyNotes) { + if (Math.random() >= chanceToNotAssign) { + const randomTag = + g_tagDefinitions[Math.floor(Math.random() * g_tagDefinitions.length)]; + stickyNote.tagIds = [randomTag.id]; + stickyNote.sync(); + const result = g_matrix.findCellByStickyNoteId(stickyNote.id); + if (result) { + updateCellTags(result.row, result.col, stickyNote.tagIds); + console.log( + `Updated tagIds for cell at row ${result.row}, col ${result.col}:`, + stickyNote.tagIds, + ); + } + } + } + } + + // Call the function to update matrix tags + await assignRandomTags(); + console.log("line no. 601, assigned random tags"); + console.log("line no. 604, g_matrix:", g_matrix); +} + +async function getSetPredefinedTagsFromBoard() { + // Define tagDefinitions if not already defined + + // Fetch all tags from the board + const boardTags = await board.get({ type: ["tag"] }); + + // Create a map for quick lookup + const boardTagMap = new Map(boardTags.map((tag) => [tag.title, tag])); + + // Update or create tags + for (const tagDef of g_tagDefinitions) { + const existingTag = boardTagMap.get(tagDef.title); + if (existingTag) { + tagDef.id = existingTag.id; + } else { + try { + const newTag = await board.createTag({ + title: tagDef.title, + color: tagDef.color, + }); + tagDef.id = newTag.id; + } catch (error) { + console.error(`Error creating tag "${tagDef.title}":`, error); + } + } + } + return g_tagDefinitions; +} +function getTagTitleById(tagId) { + console.log("g_tagDefinitions:", g_tagDefinitions); + const tag = g_tagDefinitions.find((t) => t.id === tagId); + return tag ? tag.title : ""; +} + +function updateCellTags(row, col, newTagIds) { + if (g_matrix && g_matrix.matrix[row][col]) { + g_matrix.matrix[row][col].tagIds = newTagIds; + for (const tagId of newTagIds) { + const tagTitle = getTagTitleById(tagId); + g_matrix.matrix[row][col].tagTitles.push(tagTitle); + } + } else { + console.error( + `Cell at row ${row}, col ${col} is not a MatrixCell instance`, + ); + } +} + function calculateBestSquaresInRectangle( rectangleWidth, rectangleHeight, @@ -599,160 +706,7 @@ function calculateBestSquaresInRectangle( }; } -async function assignRandomTagsToSelection() { - console.log("Assign Random Tags To Selection button clicked"); - - async function assignRandomTags() { - const validTags = await createRandomTags(); - console.log("Valid tags:", validTags); - // Get currently selected items - const selectedItems = await board.getSelection(); - const stickyNotes = selectedItems.filter( - (item) => item.type === "sticky_note", - ); - - if (stickyNotes.length === 0) { - console.log("No sticky notes selected"); - return; - } - - // Randomly assign or not assign tags to selected sticky notes - const chanceToNotAssign = 1 / (validTags.length + 1); - for (const stickyNote of stickyNotes) { - if (Math.random() >= chanceToNotAssign) { - const randomTag = - validTags[Math.floor(Math.random() * validTags.length)]; - stickyNote.tagIds = [randomTag.id]; - stickyNote.sync(); - console.log( - `Assigned tag "${randomTag.title}" to sticky note ${stickyNote.id}`, - ); - } else { - console.log(`No tag assigned to sticky note ${stickyNote.id}`); - } - } - } - - await assignRandomTags(); -} - -// async function createTags() { -// console.log("Create Tags button clicked"); - -// try { -// const tags = await getOrCreateTags(); -// console.log("Tags created or retrieved:", tags); - -// // Display a confirmation message to the user -// await board.notifications.showInfo('Tags have been created or updated successfully.'); -// } catch (error) { -// console.error("Error creating tags:", error); - -// // Display an error message to the user -// await board.notifications.showError('An error occurred while creating tags. Please try again.'); -// } -// } -async function createRandomTags() { - console.log("Creating random tags"); - - let tagDefinitions = [ - { title: "Very Important", color: "red" }, - { title: "Highly Important", color: "yellow" }, - { title: "Moderately Important", color: "light_green" }, - { title: "Somewhat Important", color: "cyan" }, - { title: "Low Importance", color: "blue" }, - ]; - - try { - const createdTags = []; - // Function to generate a random postfix - const generateRandomPostfix = () => { - return Math.floor(Math.random() * 1000) - .toString() - .padStart(3, "0"); - }; - - // Modify tagDefinitions to include random postfix - tagDefinitions = tagDefinitions.map((tagDef) => ({ - ...tagDef, - title: `${tagDef.title} #${generateRandomPostfix()}`, - })); - - for (const tagDef of tagDefinitions) { - const newTag = await board.createTag({ - title: tagDef.title, - color: tagDef.color, - }); - - createdTags.push(newTag); - console.log(`Created tag: ${tagDef.title} with color ${tagDef.color}`); - } - - console.log("Tags created:", createdTags); - await board.notifications.showInfo("Tags have been created successfully."); - return createdTags; - } catch (error) { - console.error("Error creating tags:", error); - await board.notifications.showError( - "An error occurred while creating tags. Please try again.", - ); - throw error; - } -} - -// Add event listener for a new button to create random tags - document.getElementById("createMatrix").addEventListener("click", createMatrix); -//document.getElementById("createTags").addEventListener("click", createTags); document .getElementById("assignRandomTagsToSelection") .addEventListener("click", assignRandomTagsToSelection); - -// async function getOrCreateTags() { -// // Check if g_tags is available in appdata -// console.log("getOrCreateTags, g_tags:", g_tags); -// if (g_tags === null) { -// try { -// const storedTags = await board.getAppData('g_tags'); -// g_tags = JSON.parse(storedTags); -// if (g_tags !== null && g_tags.length > 0) { -// console.log('Tags retrieved from appdata:', g_tags); -// return g_tags; -// } else { -// console.log('Stored tags array is empty'); -// } -// } catch (error) { -// console.error('Error parsing stored tags:', error); -// } -// } - -// const tagDefinitions = [ -// { title: "Very Important", color: "red" }, -// { title: "Highly Important", color: "yellow" }, -// { title: "Moderately Important", color: "light_green" }, -// { title: "Somewhat Important", color: "cyan" }, -// { title: "Low Importance", color: "blue" } -// ]; -// const validTags = []; -// for (const tagDef of tagDefinitions) { -// try { -// const boardTag = await board.createTag({ -// title: tagDef.title, -// color: tagDef.color, -// }); -// validTags.push(boardTag); -// } catch (error) { -// if (error.message.includes("The title must be unique")) { -// console.log(`Tag "${tagDef.title}" already exists. Skipping creation.`); -// } else { -// throw error; -// } -// } -// } -// g_tags = validTags; -// console.log('g_tags: line no. 645', g_tags); -// await board.setAppData('g_tags', JSON.stringify(g_tags)); -// const readBack = await board.getAppData('g_tags') -// console.log('Tags stored in appdata:', readBack); -// return validTags; -// } From 9b2caef38b7f5859ebce7c21652e9cc4aa48a328 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Sun, 20 Oct 2024 17:21:18 +0200 Subject: [PATCH 11/23] optimized matrix creation --- examples/sticky-pair-clusterer/app.html | 2 +- examples/sticky-pair-clusterer/src/app.js | 159 +++++++++++++++++----- 2 files changed, 128 insertions(+), 33 deletions(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index 05b0a476e..4af5e6a73 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -14,7 +14,7 @@ id="rowsCount" class="rows-count-input" min="1" - value="10" + value="20" placeholder="Number of rows" /> diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 3d42f4de1..88c523e40 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -36,6 +36,48 @@ */ const { board } = window.miro; + +// // Throttle function with queue +// function throttleWithQueue(func, limit) { +// let lastRan; +// const queue = []; +// let isRunning = false; + +// const processQueue = async () => { +// if (queue.length === 0 || isRunning) return; +// isRunning = true; +// const { context, args, resolve } = queue.shift(); +// lastRan = Date.now(); +// const result = await func.apply(context, args); +// resolve(result); +// isRunning = false; +// if (queue.length > 0) { +// setTimeout(processQueue, Math.max(0, limit - (Date.now() - lastRan))); +// } +// }; + +// return function(...args) { +// const context = this; +// return new Promise((resolve) => { +// queue.push({ context, args, resolve }); +// if (!isRunning) { +// processQueue(); +// } +// }); +// }; +// } + +// // Wrap the board object +// Object.keys(board).forEach(key => { +// if (typeof board[key] === 'function') { +// const originalMethod = board[key]; +// board[key] = throttleWithQueue(async function(...args) { +// console.log(`Calling board.${key} with arguments:`, args); +// return await originalMethod.apply(this, args); +// }, 1); // 1 millisecond throttle for all methods +// } +// }); + // Global definitions let g_matrix = null; let g_tagDefinitions = [ @@ -397,34 +439,85 @@ async function createMatrix() { const frameLeft = frame.x - frame.width / 2; const frameTop = frame.y - frame.height / 2; + let stickyNotePromises = []; for (let i = 0; i < rowsCount; i++) { const row = Math.floor(i / squarePositions.gridInfo.columns); const col = i % squarePositions.gridInfo.columns; const position = squarePositions.placement.getSquarePosition(row, col); - - const stickyNote = await board.createStickyNote({ - content: `Cell ${i + 1},${j + 1}`, - x: - frameLeft + - position.x + - squarePositions.gridInfo.effectiveSquareSize / 2, - y: - frameTop + - position.y + - squarePositions.gridInfo.effectiveSquareSize / 2, - width: squarePositions.gridInfo.effectiveSquareSize, - style: { - fillColor: "light_yellow", - }, - }); - await frame.add(stickyNote); - - g_matrix.setCell(i, j, stickyNote.id); + const stickyNotePromise = ((currentI, currentJ) => { + return board + .createStickyNote({ + content: `Cell ${currentI + 1},${currentJ + 1}`, + x: + frameLeft + + position.x + + squarePositions.gridInfo.effectiveSquareSize / 2, + y: + frameTop + + position.y + + squarePositions.gridInfo.effectiveSquareSize / 2, + width: squarePositions.gridInfo.effectiveSquareSize, + style: { + fillColor: "light_yellow", + }, + }) + .then((stickyNote) => { + // instead of adding to frame, we just slightly resize the frame and that's enough + g_matrix.setCell(currentI, currentJ, stickyNote.id); + return stickyNote; + }); + })(i, j); + + stickyNotePromises.push(stickyNotePromise); + } + // Wait for all sticky notes to be created + await Promise.all(stickyNotePromises); + } + // Notify user that sticky notes are being added to frames + await board.notifications.showInfo("Attaching sticky notes to frames..."); + // Add all sticky notes to their respective frames + for (let j = 0; j < g_matrix.columns; j++) { + try { + const frameId = g_matrix.getFrameForColumn(j); + if (frameId) { + const frame = await board.getById(frameId); + if (frame) { + for (let i = 0; i < g_matrix.rows; i++) { + try { + const cell = g_matrix.getCell(i, j); + if (cell && cell.stickyNoteId) { + const stickyNote = await board.getById(cell.stickyNoteId); + if (stickyNote) { + await frame.add(stickyNote); + } else { + console.error( + `Sticky note with ID ${cell.stickyNoteId} not found for cell (${i}, ${j})`, + ); + } + } + } catch (error) { + console.error(`Error processing cell (${i}, ${j}):`, error); + } + } + } else { + console.error(`Frame with ID ${frameId} not found for column ${j}`); + } + } else { + console.error(`No frame ID found for column ${j}`); + } + } catch (error) { + console.error(`Error processing column ${j}:`, error); } } - await saveMatrixToBoard(g_matrix); + await board.notifications.showInfo( + "Sticky notes attached to frames successfully!", + ); + + saveMatrixToBoard(g_matrix); console.log("Matrix created and saved:", g_matrix); + // Display message on board, matrix creation completed + await board.notifications.showInfo("Matrix creation completed successfully!"); } // Function to check if content of selected sticky note has changed @@ -460,7 +553,6 @@ board.ui.on("selection:update", async (event) => { console.log("Matrix not found"); return; } - console.log("Matrix loaded from board:", g_matrix); } // Store the previously selected items for comparison @@ -489,8 +581,8 @@ board.ui.on("selection:update", async (event) => { id: event.items[0].id, content: event.items[0].content, }; - } else { - // User has either deselected everything or selected something else + } else if (event.items.length === 0 && previouslySelectedItems.length > 0) { + // User has deselected everything // Restore the size of all previously selected sticky notes for (const prevSelectedItemId of previouslySelectedItems) { const prevSelectedItem = await board.getById(prevSelectedItemId); @@ -501,23 +593,14 @@ board.ui.on("selection:update", async (event) => { const result = g_matrix.findCellByStickyNoteId(prevSelectedItemId); if (result) { updateCellTags(result.row, result.col, prevSelectedItem.tagIds); - console.log( - `Updated tagIds for cell at row ${result.row}, col ${result.col}:`, - prevSelectedItem.tagIds, - ); } } } - console.log(g_matrix); if ( previouslySelectedContent !== null && previouslySelectedContent.content !== (await board.getById(previouslySelectedContent.id)).content ) { - console.log( - "Content changed for sticky note:", - previouslySelectedContent.id, - ); const result = g_matrix.findCellByStickyNoteId( previouslySelectedContent.id, ); @@ -538,6 +621,18 @@ board.ui.on("selection:update", async (event) => { } // Clear the previouslySelectedItems array previouslySelectedItems = []; + } else { + // User has selected something else (multiple items or non-sticky note) + // Restore the size of all previously selected sticky notes + for (const prevSelectedItemId of previouslySelectedItems) { + const prevSelectedItem = await board.getById(prevSelectedItemId); + if (prevSelectedItem) { + prevSelectedItem.width /= 2; + await prevSelectedItem.sync(); + } + } + // Clear the previouslySelectedItems array + previouslySelectedItems = []; } }); From fc7444764a6c5f16bdd1a9f05217f0851221a38c Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Mon, 21 Oct 2024 20:30:33 +0200 Subject: [PATCH 12/23] Added handling of api rate limit --- examples/sticky-pair-clusterer/src/app.js | 144 +++++++++++++++------- 1 file changed, 101 insertions(+), 43 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 88c523e40..1c53b15e0 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -413,16 +413,32 @@ async function createMatrix() { // Create frames for each column for (let j = 0; j < columnsCount; j++) { - const frame = await board.createFrame({ - title: `Column ${j + 1}`, - width: 1920, - height: 1080, - x: j * 2000, // Offset each frame horizontally - y: 0, - style: { - fillColor: "#ffffff", // Set background color to white - }, - }); + let frame; + let attempts = 0; + const MAX_ATTEMPTS = 3; + + while (attempts < MAX_ATTEMPTS) { + try { + frame = await board.createFrame({ + title: `Column ${j + 1}`, + width: 1920, + height: 1080, + x: j * 2000, // Offset each frame horizontally + y: 0, + style: { + fillColor: "#ffffff", // Set background color to white + }, + }); + break; // Exit the loop if successful + } catch (error) { + if (error.message.includes("API rate limit")) { + console.error("API rate limit exceeded. Retrying in 10 seconds..."); + await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait for 10 seconds + } else { + throw error; // Rethrow if it's a different error + } + } + } g_matrix.linkColumnToFrame(j, frame.id); } @@ -445,27 +461,53 @@ async function createMatrix() { const col = i % squarePositions.gridInfo.columns; const position = squarePositions.placement.getSquarePosition(row, col); const stickyNotePromise = ((currentI, currentJ) => { - return board - .createStickyNote({ - content: `Cell ${currentI + 1},${currentJ + 1}`, - x: - frameLeft + - position.x + - squarePositions.gridInfo.effectiveSquareSize / 2, - y: - frameTop + - position.y + - squarePositions.gridInfo.effectiveSquareSize / 2, - width: squarePositions.gridInfo.effectiveSquareSize, - style: { - fillColor: "light_yellow", - }, - }) - .then((stickyNote) => { - // instead of adding to frame, we just slightly resize the frame and that's enough - g_matrix.setCell(currentI, currentJ, stickyNote.id); - return stickyNote; - }); + return (async () => { + let sticky; + let attempts = 0; + const MAX_ATTEMPTS = 3; + + while (attempts < MAX_ATTEMPTS) { + try { + sticky = await board.createStickyNote({ + content: `Cell ${currentI + 1},${currentJ + 1}`, + x: + frameLeft + + position.x + + squarePositions.gridInfo.effectiveSquareSize / 2, + y: + frameTop + + position.y + + squarePositions.gridInfo.effectiveSquareSize / 2, + width: squarePositions.gridInfo.effectiveSquareSize, + style: { + fillColor: "light_yellow", + }, + }); + g_matrix.setCell(currentI, currentJ, sticky.id); + return sticky; + } catch (error) { + if (error.message.includes("API rate limit")) { + console.error( + `API rate limit exceeded. Retrying in ${ + 10 * (attempts + 1) + } seconds... (Attempt ${attempts + 1})`, + ); + console.error(`Full error message: ${error.message}`); + await new Promise((resolve) => + setTimeout(resolve, 10000 * (attempts + 1)), + ); + attempts++; // Increment attempts counter + } else { + console.error(`Error processing:`, error); + break; // Exit the retry loop on other errors + } + } + } + + if (attempts >= MAX_ATTEMPTS) { + console.error("Max retry attempts reached. Operation failed."); + } + })(); })(i, j); stickyNotePromises.push(stickyNotePromise); @@ -477,12 +519,14 @@ async function createMatrix() { await board.notifications.showInfo("Attaching sticky notes to frames..."); // Add all sticky notes to their respective frames for (let j = 0; j < g_matrix.columns; j++) { - try { - const frameId = g_matrix.getFrameForColumn(j); - if (frameId) { - const frame = await board.getById(frameId); - if (frame) { - for (let i = 0; i < g_matrix.rows; i++) { + const frameId = g_matrix.getFrameForColumn(j); + if (frameId) { + const frame = await board.getById(frameId); + if (frame) { + for (let i = 0; i < g_matrix.rows; i++) { + let attempts = 0; // Initialize attempts counter + while (attempts < 10) { + // Retry up to 10 times try { const cell = g_matrix.getCell(i, j); if (cell && cell.stickyNoteId) { @@ -495,20 +539,34 @@ async function createMatrix() { ); } } + break; // Exit the retry loop if successful } catch (error) { - console.error(`Error processing cell (${i}, ${j}):`, error); + if (error.message.includes("API rate limit")) { + console.error( + `API rate limit exceeded. Retrying in ${ + 10 * (attempts + 1) + } seconds... (Attempt ${attempts + 1})`, + ); + console.error(`Full error message: ${error.message}`); + attempts++; // Increment attempts counter + await new Promise( + (resolve) => setTimeout(resolve, 10000 * attempts), // Wait for 10 seconds multiplied by attempts + ); + } else { + console.error(`Error processing cell (${i}, ${j}):`, error); + break; // Exit the retry loop on other errors + } } } - } else { - console.error(`Frame with ID ${frameId} not found for column ${j}`); } } else { - console.error(`No frame ID found for column ${j}`); + console.error(`Frame with ID ${frameId} not found for column ${j}`); } - } catch (error) { - console.error(`Error processing column ${j}:`, error); + } else { + console.error(`No frame ID found for column ${j}`); } } + console.log("Sticky notes attached to frames successfully!"); await board.notifications.showInfo( "Sticky notes attached to frames successfully!", ); From 0924f8f6fd6f0fce9b6d41468db570b356df8514 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Mon, 21 Oct 2024 20:44:09 +0200 Subject: [PATCH 13/23] Added light blue color to frames --- examples/sticky-pair-clusterer/src/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 1c53b15e0..461605aa2 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -559,6 +559,10 @@ async function createMatrix() { } } } + frame.style = { + fillColor: "#ADD8E6", // Light blue color + }; + await frame.sync(); } else { console.error(`Frame with ID ${frameId} not found for column ${j}`); } From 96e67681c8718cf5459b8c8f3b6062fe63c55c99 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Tue, 22 Oct 2024 15:14:23 +0200 Subject: [PATCH 14/23] Added better handling of storage collection --- examples/sticky-pair-clusterer/app.html | 3 ++ examples/sticky-pair-clusterer/src/app.js | 45 ++++++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index 4af5e6a73..01f2b2a47 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -43,6 +43,9 @@ + diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 461605aa2..5e3508403 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -93,6 +93,11 @@ let g_tagDefinitions = [ loadMatrixFromBoard(); getSetPredefinedTagsFromBoard(); async function saveMatrixToBoard(matrix) { + const collection = board.storage.collection("benefitTraitMatrix"); + + // Remove the old data + await collection.remove("matrixData"); + const matrixData = JSON.stringify({ rows: matrix.rows, columns: matrix.columns, @@ -104,15 +109,31 @@ async function saveMatrixToBoard(matrix) { ? Object.fromEntries(matrix.columnNames) : undefined, }); - await board.setAppData("benefitTraitMatrix", matrixData); - let matrixStoredData = await board.getAppData("benefitTraitMatrix"); - console.log("Matrix read back from board:", matrixStoredData); - console.log("Matrix saved to board:", matrixData); + + // Calculate and print the size of matrixData + const matrixDataSize = new Blob([matrixData]).size; + console.log(`Size of matrixData: ${matrixDataSize} bytes`); + + if (matrixDataSize > 30 * 1024) { + console.warn( + `Warning: matrixData size (${matrixDataSize} bytes) exceeds the 30KB limit!`, + ); + } + + // Set the new data + await collection.set("matrixData", matrixData); + + // Read back the stored data + // let matrixStoredData = await collection.get('matrixData'); + // console.log("Matrix read back from board:", matrixStoredData); + // console.log("Matrix saved to board:", matrixData); } // Function to load the matrix from the board async function loadMatrixFromBoard() { - const storedMatrixData = await board.getAppData("benefitTraitMatrix"); + const collection = board.storage.collection("benefitTraitMatrix"); + const storedMatrixData = await collection.get("matrixData"); + if (storedMatrixData) { const storedMatrix = JSON.parse(storedMatrixData); const matrix = new Matrix(storedMatrix.rows, storedMatrix.columns); @@ -867,3 +888,17 @@ document.getElementById("createMatrix").addEventListener("click", createMatrix); document .getElementById("assignRandomTagsToSelection") .addEventListener("click", assignRandomTagsToSelection); +document.getElementById("printAppData").addEventListener("click", async () => { + console.log("Print App Data button clicked"); + + // Load the matrix from the board + const matrix = await loadMatrixFromBoard(); + if (!matrix) { + console.error("Matrix not found in board data."); + return; + } + console.log("Matrix:", matrix); + + // Notify user that app data has been printed + await board.notifications.showInfo("App data printed to console."); +}); From 7c9f4b3abbb8aa948230830a181db236aea539db Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Thu, 24 Oct 2024 20:07:05 +0200 Subject: [PATCH 15/23] Removed dead code --- examples/sticky-pair-clusterer/src/app.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 5e3508403..940259002 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -80,6 +80,7 @@ const { board } = window.miro; // Global definitions let g_matrix = null; +let stickyNoteSize = null; let g_tagDefinitions = [ { title: "Very Important", color: "red", id: null }, { title: "Highly Important", color: "yellow", id: null }, @@ -431,6 +432,15 @@ async function createMatrix() { } g_matrix = new Matrix(rowsCount, columnsCount); + const frameWidth = 1920; + const frameHeight = 1080; + + const squarePositions = calculateBestSquaresInRectangle( + frameWidth, + frameHeight, + rowsCount, + ); + stickyNoteSize = squarePositions.gridInfo.squareSize; // Create frames for each column for (let j = 0; j < columnsCount; j++) { @@ -442,9 +452,9 @@ async function createMatrix() { try { frame = await board.createFrame({ title: `Column ${j + 1}`, - width: 1920, - height: 1080, - x: j * 2000, // Offset each frame horizontally + width: frameWidth, + height: frameHeight, + x: j * frameWidth, // Offset each frame horizontally y: 0, style: { fillColor: "#ffffff", // Set background color to white @@ -655,8 +665,8 @@ board.ui.on("selection:update", async (event) => { // Add the sticky note to the previously selected items if (stickyNote) { previouslySelectedItems.push(stickyNote.id); - stickyNote.width *= 2; - await stickyNote.sync(); + stickyNote.width = stickyNoteSize * 2; + stickyNote.sync(); } } } @@ -710,7 +720,7 @@ board.ui.on("selection:update", async (event) => { for (const prevSelectedItemId of previouslySelectedItems) { const prevSelectedItem = await board.getById(prevSelectedItemId); if (prevSelectedItem) { - prevSelectedItem.width /= 2; + prevSelectedItem.width = stickyNoteSize; await prevSelectedItem.sync(); } } From ab6471cbfd8cd18a12921a620a46a984eb25f811 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Thu, 24 Oct 2024 20:08:35 +0200 Subject: [PATCH 16/23] Removed dead code --- examples/sticky-pair-clusterer/app.html | 44 ++++---- examples/sticky-pair-clusterer/src/app.js | 117 +++++----------------- 2 files changed, 50 insertions(+), 111 deletions(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index 01f2b2a47..2dbb78ae8 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -29,24 +29,32 @@ placeholder="Number of columns" /> - - - - - - +
+ + + +
+
+ + + +
+ +
+
- diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 940259002..b7ea8e38f 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -37,47 +37,6 @@ const { board } = window.miro; -// // Throttle function with queue -// function throttleWithQueue(func, limit) { -// let lastRan; -// const queue = []; -// let isRunning = false; - -// const processQueue = async () => { -// if (queue.length === 0 || isRunning) return; -// isRunning = true; -// const { context, args, resolve } = queue.shift(); -// lastRan = Date.now(); -// const result = await func.apply(context, args); -// resolve(result); -// isRunning = false; -// if (queue.length > 0) { -// setTimeout(processQueue, Math.max(0, limit - (Date.now() - lastRan))); -// } -// }; - -// return function(...args) { -// const context = this; -// return new Promise((resolve) => { -// queue.push({ context, args, resolve }); -// if (!isRunning) { -// processQueue(); -// } -// }); -// }; -// } - -// // Wrap the board object -// Object.keys(board).forEach(key => { -// if (typeof board[key] === 'function') { -// const originalMethod = board[key]; -// board[key] = throttleWithQueue(async function(...args) { -// console.log(`Calling board.${key} with arguments:`, args); -// return await originalMethod.apply(this, args); -// }, 1); // 1 millisecond throttle for all methods -// } -// }); - // Global definitions let g_matrix = null; let stickyNoteSize = null; @@ -123,11 +82,6 @@ async function saveMatrixToBoard(matrix) { // Set the new data await collection.set("matrixData", matrixData); - - // Read back the stored data - // let matrixStoredData = await collection.get('matrixData'); - // console.log("Matrix read back from board:", matrixStoredData); - // console.log("Matrix saved to board:", matrixData); } // Function to load the matrix from the board @@ -157,7 +111,6 @@ async function loadMatrixFromBoard() { return null; } -////////////////////////////////////////////////////////////////////////////////////////////////// class MatrixCell { constructor(stickyNoteId) { this.stickyNoteId = stickyNoteId; // Reference to a sticky note @@ -366,50 +319,6 @@ class Matrix { } } -////////////////////////////////////////////////////////////////////////////////////////////////// -// Example usage: -// const matrix = new Matrix(3, 3); -// -// // Set cell values and link sticky notes -// matrix.setCell(0, 0, 5, 'stickyNote1'); -// matrix.setCell(0, 1, 3, 'stickyNote2'); -// matrix.setCell(1, 0, 2, 'stickyNote3'); -// matrix.setCell(2, 2, 4, 'stickyNote4'); -// -// // Link columns to frames -// matrix.linkColumnToFrame(0, 'frame1'); -// matrix.linkColumnToFrame(1, 'frame2'); -// matrix.linkColumnToFrame(2, 'frame3'); -// -// // Get sticky notes for a row -// console.log(matrix.getStickyNotesForRow(0)); // ['stickyNote1', 'stickyNote2'] -// -// // Get frame for a column -// console.log(matrix.getFrameForColumn(0)); // 'frame1' -// -// // Add a new row -// matrix.addRow(); -// console.log(matrix.rows); // 4 -// -// // Add a new column -// matrix.addColumn(); -// console.log(matrix.columns); // 4 -// -// // Update row and column names -// matrix.updateRowName(0, 'First Row'); -// matrix.updateColumnName(1, 'Second Column'); -// -// // Get all row and column names -// console.log(matrix.getAllRowNames()); -// console.log(matrix.getAllColumnNames()); -// -// matrix.linkColumnToFrame(0, 'frame1'); -// -// console.log(matrix.getStickyNotesForRow(0)); // ['stickyNote1', 'stickyNote2'] -// console.log(matrix.getFrameForColumn(0)); // 'frame1' -////////////////////////////////////////////////////////////////////////////////////////////////// - -// Example usage: async function createMatrix() { console.log("Create Matrix button clicked"); @@ -440,7 +349,7 @@ async function createMatrix() { frameHeight, rowsCount, ); - stickyNoteSize = squarePositions.gridInfo.squareSize; + stickyNoteSize = squarePositions.gridInfo.effectiveSquareSize; // Create frames for each column for (let j = 0; j < columnsCount; j++) { @@ -666,6 +575,8 @@ board.ui.on("selection:update", async (event) => { if (stickyNote) { previouslySelectedItems.push(stickyNote.id); stickyNote.width = stickyNoteSize * 2; + // Bring the sticky note to the front + stickyNote.bringToFront(); stickyNote.sync(); } } @@ -680,7 +591,7 @@ board.ui.on("selection:update", async (event) => { for (const prevSelectedItemId of previouslySelectedItems) { const prevSelectedItem = await board.getById(prevSelectedItemId); if (prevSelectedItem) { - prevSelectedItem.width /= 2; + prevSelectedItem.width = stickyNoteSize; await prevSelectedItem.sync(); // potentially the tags have changed, so we need to update the tags for the cell const result = g_matrix.findCellByStickyNoteId(prevSelectedItemId); @@ -894,11 +805,31 @@ function calculateBestSquaresInRectangle( }; } +async function removeAllTags() { + console.log("Remove All Tags button clicked"); + const allTags = await board.get({ type: ["tag"] }); + for (const tag of allTags) { + await board.remove(tag); + console.log(`Removed tag: ${tag.title}`); + } + await board.notifications.showInfo(`Removed all tags from board`); +} + +// Add event listener for the "Remove All Tags" button +document + .getElementById("removeAllTags") + .addEventListener("click", removeAllTags); + document.getElementById("createMatrix").addEventListener("click", createMatrix); document .getElementById("assignRandomTagsToSelection") .addEventListener("click", assignRandomTagsToSelection); document.getElementById("printAppData").addEventListener("click", async () => { + const frames = await board.get({ type: ["frame"] }); + for (const frame of frames) { + console.log("Frame title:", frame.title); + } + console.log("Print App Data button clicked"); // Load the matrix from the board From 15fa913d602d2c6e5086fd3df4b86fa7b50cfb70 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Fri, 25 Oct 2024 13:30:30 +0200 Subject: [PATCH 17/23] fixed bug in resize of sticky nots upon de/selectoin --- examples/sticky-pair-clusterer/src/app.js | 34 +++++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index b7ea8e38f..7773d7218 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -39,7 +39,7 @@ const { board } = window.miro; // Global definitions let g_matrix = null; -let stickyNoteSize = null; +let g_stickyNoteSize = null; let g_tagDefinitions = [ { title: "Very Important", color: "red", id: null }, { title: "Highly Important", color: "yellow", id: null }, @@ -47,11 +47,22 @@ let g_tagDefinitions = [ { title: "Low Importance", color: "cyan", id: null }, { title: "Not Important", color: "blue", id: null }, ]; -// let g_tags = null; -// Function to save the matrix to the board + // app setup on load functions should be called in this order -loadMatrixFromBoard(); -getSetPredefinedTagsFromBoard(); +async function init() { + console.log( + "##################################################################################3", + ); + g_stickyNoteSize = await board.storage + .collection("benefitTraitMatrix") + .get("effectiveSquareSize"); + console.log("line no. 42, g_stickyNoteSize:", g_stickyNoteSize); + loadMatrixFromBoard(); + getSetPredefinedTagsFromBoard(); +} + +init(); + async function saveMatrixToBoard(matrix) { const collection = board.storage.collection("benefitTraitMatrix"); @@ -349,7 +360,12 @@ async function createMatrix() { frameHeight, rowsCount, ); - stickyNoteSize = squarePositions.gridInfo.effectiveSquareSize; + + // Store effectiveSquareSize as a number + g_stickyNoteSize = squarePositions.gridInfo.effectiveSquareSize; + board.storage + .collection("benefitTraitMatrix") + .set("effectiveSquareSize", g_stickyNoteSize); // Create frames for each column for (let j = 0; j < columnsCount; j++) { @@ -574,7 +590,7 @@ board.ui.on("selection:update", async (event) => { // Add the sticky note to the previously selected items if (stickyNote) { previouslySelectedItems.push(stickyNote.id); - stickyNote.width = stickyNoteSize * 2; + stickyNote.width = g_stickyNoteSize * 2; // Bring the sticky note to the front stickyNote.bringToFront(); stickyNote.sync(); @@ -591,7 +607,7 @@ board.ui.on("selection:update", async (event) => { for (const prevSelectedItemId of previouslySelectedItems) { const prevSelectedItem = await board.getById(prevSelectedItemId); if (prevSelectedItem) { - prevSelectedItem.width = stickyNoteSize; + prevSelectedItem.width = g_stickyNoteSize; await prevSelectedItem.sync(); // potentially the tags have changed, so we need to update the tags for the cell const result = g_matrix.findCellByStickyNoteId(prevSelectedItemId); @@ -631,7 +647,7 @@ board.ui.on("selection:update", async (event) => { for (const prevSelectedItemId of previouslySelectedItems) { const prevSelectedItem = await board.getById(prevSelectedItemId); if (prevSelectedItem) { - prevSelectedItem.width = stickyNoteSize; + prevSelectedItem.width = g_stickyNoteSize; await prevSelectedItem.sync(); } } From 0b4df91d374cca0c3a816b4926e46f577d90f553 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Sat, 26 Oct 2024 18:14:25 +0200 Subject: [PATCH 18/23] refactor of g_matrix, optimized --- examples/sticky-pair-clusterer/src/app.js | 85 ++++++++++++++--------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 7773d7218..063ca94ac 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -51,7 +51,7 @@ let g_tagDefinitions = [ // app setup on load functions should be called in this order async function init() { console.log( - "##################################################################################3", + "###########################################################################", ); g_stickyNoteSize = await board.storage .collection("benefitTraitMatrix") @@ -75,10 +75,8 @@ async function saveMatrixToBoard(matrix) { data: matrix.matrix, columnToFrameMap: Object.fromEntries(matrix.columnToFrameMap), rowToStickyNotesMap: Object.fromEntries(matrix.rowToStickyNotesMap), - rowNames: matrix.rowNames ? Object.fromEntries(matrix.rowNames) : undefined, - columnNames: matrix.columnNames - ? Object.fromEntries(matrix.columnNames) - : undefined, + rowNames: matrix.rowNames, + columnNames: matrix.columnNames, }); // Calculate and print the size of matrixData @@ -103,7 +101,7 @@ async function loadMatrixFromBoard() { if (storedMatrixData) { const storedMatrix = JSON.parse(storedMatrixData); const matrix = new Matrix(storedMatrix.rows, storedMatrix.columns); - matrix.matrix = storedMatrix.data; + matrix.matrix = [...storedMatrix.data]; matrix.columnToFrameMap = new Map( Object.entries(storedMatrix.columnToFrameMap), ); @@ -111,10 +109,10 @@ async function loadMatrixFromBoard() { Object.entries(storedMatrix.rowToStickyNotesMap), ); if (storedMatrix.rowNames) { - matrix.rowNames = new Map(Object.entries(storedMatrix.rowNames)); + matrix.rowNames = [...storedMatrix.rowNames]; } if (storedMatrix.columnNames) { - matrix.columnNames = new Map(Object.entries(storedMatrix.columnNames)); + matrix.columnNames = [...storedMatrix.columnNames]; } g_matrix = matrix; return matrix; @@ -137,6 +135,8 @@ class Matrix { this.matrix = Array.from({ length: rows }, () => Array(columns).fill(null)); this.columnToFrameMap = new Map(); // Maps column index to frame ID this.rowToStickyNotesMap = new Map(); // Maps row index to an array of sticky note IDs + this.rowNames = Array(rows).fill(""); // Array to store row names + this.columnNames = Array(columns).fill(""); // Array to store column names } setCell(row, col, stickyNoteId) { @@ -191,36 +191,30 @@ class Matrix { } setRowName(rowIndex, name) { if (rowIndex >= 0 && rowIndex < this.rows) { - if (!this.rowNames) { - this.rowNames = new Map(); - } - this.rowNames.set(rowIndex, name); + this.rowNames[rowIndex] = name; return true; } return false; } getRowName(rowIndex) { - if (this.rowNames && rowIndex >= 0 && rowIndex < this.rows) { - return this.rowNames.get(rowIndex); + if (rowIndex >= 0 && rowIndex < this.rows) { + return this.rowNames[rowIndex]; } return null; } setColumnName(colIndex, name) { if (colIndex >= 0 && colIndex < this.columns) { - if (!this.columnNames) { - this.columnNames = new Map(); - } - this.columnNames.set(colIndex, name); + this.columnNames[colIndex] = name; return true; } return false; } getColumnName(colIndex) { - if (this.columnNames && colIndex >= 0 && colIndex < this.columns) { - return this.columnNames.get(colIndex); + if (colIndex >= 0 && colIndex < this.columns) { + return this.columnNames[colIndex]; } return null; } @@ -238,10 +232,7 @@ class Matrix { } updateRowName(rowIndex, newName) { if (rowIndex >= 0 && rowIndex < this.rows) { - if (!this.rowNames) { - this.rowNames = new Map(); - } - this.rowNames.set(rowIndex, newName); + this.rowNames[rowIndex] = newName; return true; } return false; @@ -249,25 +240,24 @@ class Matrix { updateColumnName(colIndex, newName) { if (colIndex >= 0 && colIndex < this.columns) { - if (!this.columnNames) { - this.columnNames = new Map(); - } - this.columnNames.set(colIndex, newName); + this.columnNames[colIndex] = newName; return true; } return false; } deleteRowName(rowIndex) { - if (this.rowNames && rowIndex >= 0 && rowIndex < this.rows) { - return this.rowNames.delete(rowIndex); + if (rowIndex >= 0 && rowIndex < this.rows) { + this.rowNames[rowIndex] = ""; + return true; } return false; } deleteColumnName(colIndex) { - if (this.columnNames && colIndex >= 0 && colIndex < this.columns) { - return this.columnNames.delete(colIndex); + if (colIndex >= 0 && colIndex < this.columns) { + this.columnNames[colIndex] = ""; + return true; } return false; } @@ -616,6 +606,8 @@ board.ui.on("selection:update", async (event) => { } } } + // Check if the content of the previously selected sticky note has changed + // safa if ( previouslySelectedContent !== null && previouslySelectedContent.content !== @@ -627,13 +619,15 @@ board.ui.on("selection:update", async (event) => { if (result) { const newContent = (await board.getById(previouslySelectedContent.id)) .content; + g_matrix.rowNames[result.row] = newContent; + console.log("g_matrix.rowNames:", g_matrix.rowNames); for (let col = 0; col < g_matrix.columns; col++) { const cell = g_matrix.matrix[result.row][col]; if (cell && cell.stickyNoteId) { let stickyNote = await board.getById(cell.stickyNoteId); if (stickyNote) { stickyNote.content = newContent; - await stickyNote.sync(); + stickyNote.sync(); } } } @@ -654,6 +648,7 @@ board.ui.on("selection:update", async (event) => { // Clear the previouslySelectedItems array previouslySelectedItems = []; } + saveMatrixToBoard(g_matrix); }); async function assignRandomTagsToSelection() { @@ -854,8 +849,30 @@ document.getElementById("printAppData").addEventListener("click", async () => { console.error("Matrix not found in board data."); return; } - console.log("Matrix:", matrix); + console.log("g_matrix:", g_matrix); + // console.log("Matrix:", matrix); // Notify user that app data has been printed await board.notifications.showInfo("App data printed to console."); }); + +document + .getElementById("copyToClipboard") + .addEventListener("click", async () => { + // Notify user that app data has been copied to clipboard + const timestamp = new Date().toISOString(); + await navigator.clipboard.writeText(timestamp); + await board.notifications.showInfo("App data copied to clipboard."); + + const stickyNoteContent = JSON.stringify(g_matrix, null, 2); + + const stickyNote = await board.createStickyNote({ + content: stickyNoteContent, + x: 100, + y: 100, + width: 600, + }); + await board.viewport.zoomToObject(stickyNote); + + console.log("Created new sticky note with matrix content:", stickyNote); + }); From cc7deec0a6bb9405b55f7b91b337d9e0d4820259 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Sat, 26 Oct 2024 18:25:43 +0200 Subject: [PATCH 19/23] removed tagtitles from g_matrix --- examples/sticky-pair-clusterer/src/app.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 063ca94ac..0b3222a2b 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -50,9 +50,6 @@ let g_tagDefinitions = [ // app setup on load functions should be called in this order async function init() { - console.log( - "###########################################################################", - ); g_stickyNoteSize = await board.storage .collection("benefitTraitMatrix") .get("effectiveSquareSize"); @@ -124,7 +121,6 @@ class MatrixCell { constructor(stickyNoteId) { this.stickyNoteId = stickyNoteId; // Reference to a sticky note this.tagIds = []; - this.tagTitles = []; } } @@ -716,19 +712,15 @@ async function getSetPredefinedTagsFromBoard() { } return g_tagDefinitions; } -function getTagTitleById(tagId) { - console.log("g_tagDefinitions:", g_tagDefinitions); - const tag = g_tagDefinitions.find((t) => t.id === tagId); - return tag ? tag.title : ""; -} +// function getTagTitleById(tagId) { +// console.log("g_tagDefinitions:", g_tagDefinitions); +// const tag = g_tagDefinitions.find((t) => t.id === tagId); +// return tag ? tag.title : ""; +// } function updateCellTags(row, col, newTagIds) { if (g_matrix && g_matrix.matrix[row][col]) { g_matrix.matrix[row][col].tagIds = newTagIds; - for (const tagId of newTagIds) { - const tagTitle = getTagTitleById(tagId); - g_matrix.matrix[row][col].tagTitles.push(tagTitle); - } } else { console.error( `Cell at row ${row}, col ${col} is not a MatrixCell instance`, From 3f3b673989ac12f33f8e3b2fb2aa5ec6be0ca5ab Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Sat, 26 Oct 2024 19:36:13 +0200 Subject: [PATCH 20/23] Added exprot table construction of traits, benefits, and their corresponding tag values --- examples/sticky-pair-clusterer/src/app.js | 84 +++++++++++++++++++---- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 0b3222a2b..86fad0a77 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -41,11 +41,11 @@ const { board } = window.miro; let g_matrix = null; let g_stickyNoteSize = null; let g_tagDefinitions = [ - { title: "Very Important", color: "red", id: null }, - { title: "Highly Important", color: "yellow", id: null }, - { title: "Moderately Important", color: "light_green", id: null }, - { title: "Low Importance", color: "cyan", id: null }, - { title: "Not Important", color: "blue", id: null }, + { title: "Very Important", color: "red", id: null, value: 9 }, + { title: "Highly Important", color: "yellow", id: null, value: 3 }, + { title: "Moderately Important", color: "light_green", id: null, value: 0 }, + { title: "Low Importance", color: "cyan", id: null, value: -1 }, + { title: "Not Important", color: "blue", id: null, value: -3 }, ]; // app setup on load functions should be called in this order @@ -362,7 +362,7 @@ async function createMatrix() { while (attempts < MAX_ATTEMPTS) { try { frame = await board.createFrame({ - title: `Column ${j + 1}`, + title: `Benefit ${j + 1}`, width: frameWidth, height: frameHeight, x: j * frameWidth, // Offset each frame horizontally @@ -382,6 +382,7 @@ async function createMatrix() { } } g_matrix.linkColumnToFrame(j, frame.id); + g_matrix.columnNames[j] = frame.title; } // Create sticky notes for each cell in each column @@ -411,7 +412,8 @@ async function createMatrix() { while (attempts < MAX_ATTEMPTS) { try { sticky = await board.createStickyNote({ - content: `Cell ${currentI + 1},${currentJ + 1}`, + content: `Trait ${currentI + 1}`, + // content: `Cell ${currentI + 1},${currentJ + 1}`, x: frameLeft + position.x + @@ -426,6 +428,7 @@ async function createMatrix() { }, }); g_matrix.setCell(currentI, currentJ, sticky.id); + g_matrix.setRowName(currentI, sticky.content); //TODO: move this to outside of the first while loop return sticky; } catch (error) { if (error.message.includes("API rate limit")) { @@ -712,11 +715,11 @@ async function getSetPredefinedTagsFromBoard() { } return g_tagDefinitions; } -// function getTagTitleById(tagId) { -// console.log("g_tagDefinitions:", g_tagDefinitions); -// const tag = g_tagDefinitions.find((t) => t.id === tagId); -// return tag ? tag.title : ""; -// } + +function getTagValueByTagId(tagId) { + const tag = g_tagDefinitions.find((t) => t.id === tagId); + return tag ? tag.value : null; // Return null if the tag is not found +} function updateCellTags(row, col, newTagIds) { if (g_matrix && g_matrix.matrix[row][col]) { @@ -868,3 +871,60 @@ document console.log("Created new sticky note with matrix content:", stickyNote); }); + +document.getElementById("debugButton").addEventListener("click", () => { + console.log("Debug button clicked"); + generate2DMatrixFromTags(); +}); + +function generate2DMatrixFromTags( + rowNames = g_matrix.rowNames, + columnNames = g_matrix.columnNames, +) { + const rows = rowNames.length; + const columns = columnNames.length; + const matrix = Array.from({ length: rows + 1 }, () => + Array(columns + 1).fill(0), + ); + + // Set the first row with column names + for (let col = 1; col <= columns; col++) { + matrix[0][col] = columnNames[col - 1]; + } + + // Set the first column with row names + for (let row = 1; row <= rows; row++) { + matrix[row][0] = rowNames[row - 1]; + } + console.log("g_tagDefinitions:", g_tagDefinitions); + for (let row = 0; row < rows; row++) { + for (let col = 0; col < columns; col++) { + const cell = g_matrix.matrix[row][col]; + if (cell) { + console.log(`Processing cell at row ${row}, col ${col}`); + console.log(`Cell tagIds: ${cell.tagIds}`); + + if (cell.tagIds.length > 0) { + const tagValues = cell.tagIds.map((tagId) => { + const tagValue = getTagValueByTagId(tagId); + console.log(`Tag ID: ${tagId}, Tag Value: ${tagValue}`); + return tagValue; + }); + + matrix[row + 1][col + 1] = tagValues.join(", "); + console.log( + `Assigned to matrix[${row + 1}][${col + 1}]: ${ + matrix[row + 1][col + 1] + }`, + ); + } else { + console.log(`No tags found for cell at row ${row}, col ${col}`); + } + } else { + console.log(`No cell found at row ${row}, col ${col}`); + } + } + } + console.log("Generated 2D matrix:", matrix); + return matrix; +} From d6b8a76d3defce39f6f3300b62f1595985818704 Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Sat, 26 Oct 2024 21:23:25 +0200 Subject: [PATCH 21/23] Added formating of data for direct copy to excel --- examples/sticky-pair-clusterer/src/app.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/sticky-pair-clusterer/src/app.js b/examples/sticky-pair-clusterer/src/app.js index 86fad0a77..3ab74772f 100644 --- a/examples/sticky-pair-clusterer/src/app.js +++ b/examples/sticky-pair-clusterer/src/app.js @@ -874,10 +874,10 @@ document document.getElementById("debugButton").addEventListener("click", () => { console.log("Debug button clicked"); - generate2DMatrixFromTags(); + generateTableFromTags(); }); -function generate2DMatrixFromTags( +function generateTableFromTags( rowNames = g_matrix.rowNames, columnNames = g_matrix.columnNames, ) { @@ -925,6 +925,14 @@ function generate2DMatrixFromTags( } } } - console.log("Generated 2D matrix:", matrix); - return matrix; + + // Convert the matrix to a tab-separated string + const excelFormattedString = matrix.map((row) => row.join("\t")).join("\n"); + console.log("Excel formatted matrix:\n", excelFormattedString); + + // Write the excelFormattedString to clipboard + navigator.clipboard.writeText(excelFormattedString); + board.notifications.showInfo("Matrix data copied to clipboard."); + console.log("Matrix data copied to clipboard"); + return excelFormattedString; } From 292c936396e9a55466bed164d46385c6e7f10d7f Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Sat, 26 Oct 2024 22:03:53 +0200 Subject: [PATCH 22/23] fixed app.html --- examples/sticky-pair-clusterer/app.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index 2dbb78ae8..f190b4bf4 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -40,8 +40,6 @@ > Assign Random Tags To Selection - -
@@ -51,6 +49,10 @@ + +
From e840495a5c0b8e40b289cd4ce4553672e314bc0d Mon Sep 17 00:00:00 2001 From: Sam Safaei Date: Mon, 28 Oct 2024 14:40:26 +0100 Subject: [PATCH 23/23] Fixed styling for app --- examples/sticky-pair-clusterer/app.html | 8 +-- examples/sticky-pair-clusterer/src/app.js | 6 ++ examples/sticky-pair-clusterer/src/styles.css | 65 ++++++++++--------- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/examples/sticky-pair-clusterer/app.html b/examples/sticky-pair-clusterer/app.html index f190b4bf4..8b14b984a 100644 --- a/examples/sticky-pair-clusterer/app.html +++ b/examples/sticky-pair-clusterer/app.html @@ -7,8 +7,8 @@
-
- +
+
-
- +
+