Skip to content

Commit

Permalink
Create app sample registering custom actions (#167)
Browse files Browse the repository at this point in the history
* PF-1292 - Create app sample registering custom actions

* Adding custom actions app sample

* Formatting code

* Removing package-lock.json
  • Loading branch information
fredcido authored Aug 7, 2023
1 parent 70729c2 commit 1d8de70
Show file tree
Hide file tree
Showing 11 changed files with 5,008 additions and 4,599 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ npx create-miro-app@latest
| [wordle](examples/wordle) | This example shows you how to create a Wordle-like game using the Miro Web SDK. |
| [blob-maker](examples/blob-maker) | This example shows you how to create a drag and drop blobmaker using Miro's Web SDK. |
| [youtube-room](examples/youtube-room) | This example shows you how to sync a YouTube player across multiple users through Socket.IO. |
| [custom-actions](examples/custom-actions) | This example shows you how register [custom actions](https://developers.miro.com/docs/add-custom-actions-to-your-app) in the item context menu. |

<p>&nbsp;</p>

Expand Down
24 changes: 24 additions & 0 deletions examples/custom-actions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.next

# testing
/coverage

# misc
.DS_Store
*.pem
.idea

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
dist
54 changes: 54 additions & 0 deletions examples/custom-actions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## Custom Actions

**&nbsp;&nbsp;Note**:

- We recommend a Chromium-based web browser for local development with HTTP. \
Safari enforces HTTPS; therefore, it doesn't allow localhost through HTTP.
- For more information, visit our [developer documentation](https://developers.miro.com).

### How to start locally

- Run `npm i` to install dependencies.
- Run `npm start` to start developing. \
Your URL should be similar to this example:

```
http://localhost:3000
```

- Paste the URL under **App URL** in your
[app settings](https://developers.miro.com/docs/build-your-first-hello-world-app#step-3-configure-your-app-in-miro).
- Open a board; you should see your app in the app toolbar or in the **Apps**
panel.

### How to build the app

- Run `npm run build`. \
This generates a static output inside [`dist/`](./dist), which you can host on a static hosting
service.

### Folder structure

<!-- The following tree structure is just an example -->

```
.
├── src
│ ├── assets
│ │ └── style.css
│ ├── app.ts // The code for the app lives here
│ └── index.ts // 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 what you specify in the 'App URL' box in the Miro app settings
```

### About the app

This sample app provides you with boilerplate setup and configuration that you can further customize to build your own app.

<!-- describe shortly the purpose of the sample app -->

Built using [`create-miro-app`](https://www.npmjs.com/package/create-miro-app).

This app uses [Vite](https://vitejs.dev/). \
If you want to modify the `vite.config.js` configuration, see the [Vite documentation](https://vitejs.dev/guide/).
16 changes: 16 additions & 0 deletions examples/custom-actions/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://miro.com/app/static/sdk/v2/miro.js"></script>
<title>Custom Actions</title>
</head>
<body>
<div id="root">
<h1>Custom actions home</h1>
</div>

<script type="module" src="/src/app.ts"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions examples/custom-actions/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// https://vitejs.dev/guide/features.html#typescript-compiler-options
/// <reference types="vite/client" />
15 changes: 15 additions & 0 deletions examples/custom-actions/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://miro.com/app/static/sdk/v2/miro.js"></script>
<title>Custom Actions</title>
</head>
<body>
<div id="root">
<h1>Custom actions</h1>
</div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>
18 changes: 18 additions & 0 deletions examples/custom-actions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "custom-actions",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"start": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"mirotone": "5"
},
"devDependencies": {
"@mirohq/websdk-types": "latest",
"typescript": "4.9.5",
"vite": "3.0.3"
}
}
198 changes: 198 additions & 0 deletions examples/custom-actions/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import type { CustomAction, CustomEvent, Frame } from "@mirohq/websdk-types";

const actionHandler = (action: string) => async (props: CustomEvent) => {
const content = props.items.map((w) => w.id).join(" - ");
const message = `Clicked on ${action} => ${content}`;
await miro.board.notifications.showInfo(message);
};

export async function init() {
await miro.board.ui.on(
"custom:translate-content",
actionHandler("translate-content")
);
await miro.board.ui.on(
"custom:align-to-grid",
actionHandler("align-to-grid")
);

const translateAction: CustomAction = {
event: "translate-content",
ui: {
label: {
en: "Translate content",
de: "Traduzir conteúdo",
es: "Traducir contenido",
},
icon: "chat-two",
description: {
en: "Translate the content of your element",
de: "Übersetzen Sie den Inhalt Ihres Elements",
es: "Traduce el contenido de tu elemento",
},
position: 1,
},
predicate: {
$or: [
// Matching multiple types
{
type: "preview",
},
{
type: "frame",
},
{
type: "text",
},
{
type: "card",
},
{
type: "sticky_note",
},
// Matching nested properties with dot notation
{
type: "shape",
shape: "circle",
"style.color": "#1a1a1a",
},
// Images that have landscape aspect ratio
{
type: "image",
$where: "this.width > this.height",
},
// Embed widget with a given URL matching the Regex
{ type: "embed", url: { $regex: ".*vimeo.com.*" } },
// App owned app_card
{ type: "app_card", owned: true },
// Connectors with both start and end connected to them
{
type: "connector",
start: {
$exists: true,
},
end: {
$exists: true,
},
},
],
},
};

await miro.board.ui.on("custom:active-action", (props: CustomEvent) => {
props.items.map((w) => w.setMetadata("status", "active"));
});
await miro.board.ui.on("custom:inactive-action", (props: CustomEvent) => {
props.items.map((w) => w.setMetadata("status", "inactive"));
});

const activeAction: CustomAction = {
event: "active-action",
ui: {
label: "Activate",
icon: "chat-dashes-lines-two",
description: "Set card to active",
position: 1,
},
predicate: {
type: "card",
$or: [
{
"metadata.status": {
$exists: false,
},
},
{
"metadata.status": "inactive",
},
],
},
};

const inactiveAction: CustomAction = {
event: "inactive-action",
ui: {
label: "Inactivate",
icon: "chat-lines-cross",
description: "Set card to inactive",
position: 1,
},
predicate: {
type: "card",
"metadata.status": "active",
},
};

const votingHandler = async (props: CustomEvent) => {
const voting = await miro.board.experimental.getVotingResults();
const boardInfo = await miro.board.getInfo();

if (!voting.length) {
await miro.board.notifications.showInfo("No voting results available");
}

const votingWithResults = voting.filter((v) => v.results.length > 0);
const [parent] = props.items as Frame[];

votingWithResults.map(async (voting) => {
const title = await miro.board.createText({
x: parent.x,
y: parent.y,
width: 500,
content: voting.title,
style: {
fontSize: 80,
},
});

await parent.add(title);
title.x = 400;
title.y = 200;

await title.sync();

const itemsResult = voting.results.map(async (item, i) => {
try {
const boardItem = await miro.board.createStickyNote({
x: parent.x,
y: parent.y,
content: `Item: ${item.itemId} => Votes: ${item.count}`,
linkedTo: `/app/board/${boardInfo.id}/?moveToWidget=${item.itemId}`,
});

await parent.add(boardItem);
boardItem.x = 200 * (i + 1);
boardItem.y = 400;
await boardItem.sync();
} catch (error) {
console.error(error);
}
});

await Promise.allSettled(itemsResult);

await miro.board.viewport.zoomTo([parent]);
});
};

await miro.board.ui.on("custom:get-voting-results", votingHandler);

const votingResults = {
event: "get-voting-results",
ui: {
label: "Get voting results",
icon: "trophy",
description: "Create sticky notes with voting results",
},
predicate: {
type: "frame",
},
};

await miro.board.experimental.action.register(activeAction);
await miro.board.experimental.action.register(inactiveAction);
await miro.board.experimental.action.register(translateAction);
await miro.board.experimental.action.register(votingResults);
}

init();
25 changes: 25 additions & 0 deletions examples/custom-actions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["esnext", "dom"],
"jsx": "preserve",
"moduleResolution": "node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["./node_modules/@types", "./node_modules/@mirohq"],
"allowJs": true,
"incremental": true,
"isolatedModules": true
},
"include": ["src", "pages", "*.ts"],
"exclude": ["node_modules"]
}
29 changes: 29 additions & 0 deletions examples/custom-actions/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import path from "path";
import fs from "fs";
import dns from "dns";
import { defineConfig } from "vite";

// https://vitejs.dev/config/server-options.html#server-host
dns.setDefaultResultOrder("verbatim");

// make sure vite picks up all html files in root, needed for vite build
const allHtmlEntries = fs
.readdirSync(".")
.filter((file) => path.extname(file) === ".html")
.reduce((acc, file) => {
acc[path.basename(file, ".html")] = path.resolve(__dirname, file);

return acc;
}, {});

// https://vitejs.dev/config/
export default defineConfig({
build: {
rollupOptions: {
input: allHtmlEntries,
},
},
server: {
port: 3000,
},
});
Loading

2 comments on commit 1d8de70

@vercel
Copy link

@vercel vercel bot commented on 1d8de70 Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

app-examples-wordle – ./examples/wordle

app-examples-wordle.vercel.app
app-examples-wordle-git-main-anthonyroux.vercel.app
app-examples-wordle-anthonyroux.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 1d8de70 Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

webhooks-manager – ./examples/webhooks-manager

webhooks-manager-git-main-miro-web.vercel.app
webhooks-manager-miro-web.vercel.app
webhooks-manager-sepia.vercel.app

Please sign in to comment.