Send to Planner Add-in Published

Right before the holidays I got my latest solution published to AppSource: “Send to Planner.”

This add-in is based on the “Send to Trello” Add-in and has a very similar look and feel. It was an unexpected side bar in my solutions development. A Send to Trello user contacted me because they also used Planner and were unable to find an effective solution that just did the basic “send an email to Planner” for free. While this has premium features to do much more, like keeping responses to the same email with the original task, the basic functionality will likely meet most casual use.

This was fun to create but also vexing at the same time. I am not sure where I got the idea, that there was an ability to get a front-end access token of the user running the Office add-in (getAccessToken). But turns out this is called an “on behalf of” flow that requires a complex manifest setup and you must send the customer email information to your backend web service to make the call “on behalf of” the user to Graph API. There are several options, but I had to go with the OAUTH flow using displayDialogAsync(). The AppSource (GDRP/Privacy) requirements for a backend data flow was more work and upkeep than I care for (cost and time wise). It is a headache I did not want.

So, I used the MSAL flow to pop an authentication dialog and then call the Graph API from the front-end to write to Planner. I already have users complaining that “it pops up the authentication dialog too much.” This is even though it does not require the entire authentication flow, it just pops up and then goes away.

I try the ssoSilent from the task-pane side and when/if that fails, I pop the dialog and do it there. The issue is because I do not own the frame of the Office task pane my origination domain is not correct. MSAL rejects the Silent SSO attempt. Ergo, it must pop open the dialog every time to get ssoSilent and refresh the token that way. A tad annoying.

I am not sure if I am missing something, but it would be nice to be able to request a front-end token with the proper scopes to do this, or a way to call ssoSilent from a task pane. But for now – this is what it is. Another annoying fact is that the token I get from MSAL, although it is refreshable (with ssoSilent), it lasts only 1 hour before it must be refreshed. So, 99% of the time a user clicks “Send to Planner” they see the dialog flash.

Either way, the add-in is out there and consumable. Please check it out!

Adding an Attachment to a Planner Task

I have recently started working on a new add-in in Office and had reason to create an newly uploaded attachment to a Planner Task item.

OMG!

I pulled out all my hair on this one as I was unable to figure out from the documentation how to do this. Hack, hack, hack — 3days later… well, I hope to save the world this frustration.

Here are the steps:

  1. First, get the file from an upload. Determine the type and convert all the binary in the file to a BASE64 string.
  2. Next, you need to GET the planners GROUP by the planner ID: https://graph.microsoft.com/v1.0/planner/plans/${plannerId}
  3. Then you create the item in the group drive container, where name is the name of the file via a POST (with the based 64 in the body, content-type: text/plain): https://graph.microsoft.com/v1.0/groups/${plan.container.containerId}/drive/items/root:/${name}:/content
  4. This returns a driveItem object and from this you need the driveItem.webUrl, but this is the part that killed me. The documentation tells you to use an encoded URL. WRONG. This fails every single time. After a lot of looking over the documentation and seeing what they were submitting, you actually only replace the “.” and the “:” and white space / /. Here is a function I created in JS:
/**
* Encodes the URL ins the special planner format that is for oif but not quite
* following the encodeURIComponent() specification…
* @param {String} url
* @returns {String}
*/
#encodePlannerExternalReferenceUrl = (url) => {
// Encode specific characters: : . _
const encodedUrl = url.replace(/:/g, "%3A").replace(/\./g, "%2E").replace(/ /g, "%20");
return encodedUrl;
};
  1. Next, GET the task item by ID: https://graph.microsoft.com/v1.0/planner/tasks/${id}
  2. Then GET the task items details by ID: https://graph.microsoft.com/v1.0/planner/tasks/${id}/details
  3. For this next part, you use the “@odata.etag” prop the details and you will create a new entry with a PATCH: https://graph.microsoft.com/v1.0/planner/tasks/${taskId}/details.
  4. For this part you need to create a fetch body like this: { references: ${ref} }, where the ref is defined as:
const ref = {
/** @type {PlannerReference} */
[this.#encodePlannerExternalReferenceUrl(driveItem.webUrl)]: {
"@odata.type": "#microsoft.graph.plannerExternalReference",
alias: name,
type: "Other",
},
};
view raw ref.js hosted with ❤ by GitHub
  1. To make the above call you have to supply the @odata.etag. And this is the NEXT part that messed with my noodle. You do NOT use the whole value returned AND you have to place it in double quotes. Ugh! So here is more code I wrote to help with that:
const updatedTag = eTag.replace('W/"', '"').replace('\\"', "");
view raw etag.js hosted with ❤ by GitHub
  1. Then when you make the call you place the tag in the headers: If-Match. And do not forget to add the Content-type, and to see the returned result the Prefer as well:

The final submission looks like this:

And let me stress this. You must PATCH this, not “patch.” Another thing I found is that GET/get, PUT/put, DELETE/delete, and POST/post all work interchangeably. But if you “patch” you will get some bazaar error about CORS and that path is not supported. And if you hit the service with OPTIONS you will see all the supported methods are returned in CAPS. Oddly, all work with lowercase or upper case, except for PATCH.

So, there you have it. 3 days of my life.

Procure the Board Button in Trello

I was recently posting an issue in the Developer Forum for Trello and found a post I could answer fairly quickly since I knew the answer already. Thought I would share it here on my blog as well.

The question was along the lines of:

How do I show my board button only to members of the board who have paid for it? Or how do I block guests from getting the board button?

Here is the code:

/// <reference path="trello.d.js" />
/** @type {TrelloPowerUp} */
const tpu = window.TrelloPowerUp;
tpu.initialize({
'board-buttons':
/**
* Returns the board button
* @param {TrelloObject} t
* @returns {TrelloBoardButtonOption[]}
*/
async (t) => {
/** @type {TrelloMemberObject} */
const member = await t.member("id");
/** @type {TrelloBoard} */
const board = await t.board("memberships");
/** @type {TrelloMembership} */
const membership = board.memberships.find(o=>o.idMember === member.id);
if(!membership || membership.memberType === "observer") {
t.alert({
message: "Sorry you are only a guest on this board!",
duration: 1,
});
return []; // no board button for you
}
/** @type {TrelloBoardButtonOption} */
const button = {
text: "hello",
icon: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAF2SURBVDhPtZMxT8JQEMfb0pI2xMbA0JWyNXQEJLHO7VrmfgAHTfgMLMTo0FknDU0TNze+gCbOSmSBwU2hgxMlAevd8wV95GG6+Euu73/v/e/aXlPhX8myrIBBUy4iXRmCIDicTqeeqqoHmKdp+lir1YaDweCeGHZx1u/vHTnOpWEYqSiKGWyRQI17juNc9cFDzNvEUay2ms1bkJtCXjTBE0WRCprFc70TTdO4Rb8DPa7rnoL+odfr6bZtP4HkFm0HeJ+xBrQg4WU+n7eSJLFR5wH8dfC3UJMGy+WyDJNGmQvwC4vFooyaNFAUZVUo/Pm5GdBbLBZXqEkD2Bjpuv6BOg/olSRpRNNv2u32NSzcoW0HeG9gJZAnQOx6/cKsVmc03YlZNWfgPacpi+/7rmma7yC5d8azDnhAb2AmNx6PJ77fGWqaqsmyvF8qleB19c9KpfJqWdZdo9E4juP4gdoJ3J8J6Xa7BgzXQr1er1/CMHwjBwyC8AW6vpgYpmCzMQAAAABJRU5ErkJggg==`, // for card front badges only
condition: "always",
callback: (tt) => {
tt.alert({
message: "You are all paid up!",
duration: 1,
})
}
};
// return the button
return [button];
}
});
view raw client.js hosted with ❤ by GitHub

Happy coding!

Creating a Trello Power-Up in Visual Studio Code

Are you a Trello enthusiast looking to start developing you own Power-Up? Trello Power-Ups are custom integrations add new functionality to your Trello boards, and creating one yourself is easier than you might think.

I created the following tutorial to walk you through the process of developing a Trello Power-Up in Visual Studio Code using npm (Node Package Manager). By the end, you’ll have a basic project that you can further enhance as needed. But this will take you some time. I took my time walking through this and it took me a good hour to get through all the steps below. The good news is once you have done this, you will have a quick, easy to use project that is totally reusable for multiple Power-Ups. With that said, let’s get started…

Prerequisites

Before we dive in, make sure you have the following prerequisites:

  1. Node.js and npm: If you don’t have Node.js and npm installed, download and install them from the official website here.
  2. Trello Account: You’ll need a Trello account to create and test your Power-Up.
  3. Visual Studio Code: If you don’t have VS Code, download and install it from https://code.visualstudio.com/.
  4. Once you have VS Code install, here are some suggested Extensions to make your life MUCH easier:
  • ESLint
  • Code Spell Checker
  • Prettier – Code formatter
  • Inline HTML

Something to read so you can get an idea of how powerful VS Code can be with extensions and other capabilities, here is more about JavaScript development in VS Code:
https://code.visualstudio.com/Docs/languages/javascript

Step 1: Create your Project Folder

In the least you ONLY need two files [index.js] and [index.html].
But it helps to build out your project with the proper folder structure right off the bat. So here are the folders I create:

  • [certs] – required for local development and covered later
  • [js] – for all your code files
  • +– [common] – where you will keep common files (usually static defined stuff)
  • +– [pages] – for all your js page files (more below)
  • +– [components] – we will not get to this here, but might cover it in a future post.
  • [types] – Go get this file: https://github.com/davecra/Trello-Power-Up-TypeDefs/blob/main/trello.d.ts, never Power-Up develop without it (*).

When I decided to make the Type Definitions file for Trello Power-Ups it CHANGED MY LIFE. Now, it is not perfect and I am still working on adding things to it, and find errors every now and then, but please notify me if you find anything wrong. But when I added this to my project it took the guesswork out of creating a Power-Up, in that “what does this return”, or “what are the properties of this or that.” And I did not have to have the Trello API reference page open in the browser all the time. My code got cleaner, more concise, and I was able to more quickly develop Power-Ups.

Step 2: Add files and Code

So, now we add the files.

If you do things “creatively” – correctly in my opinion – you really only need one HTML file for your WHOLE project even if you open multiple forms.

In the [views] folder, create the [index.html] file:

<!DOCTYPE html>
<html lang="en-us">
<head>
<meta http-equiv="Cache-control" content="no-cache"/>
<meta http-equiv="pragma" content="no-cache"/>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<!– if you want Trello sizeTo to work correctly, you need to keep this –>
<link crossorigin="anonymous" rel="stylesheet" href="https://p.trellocdn.com/power-up.min.css"&gt;
<title>Power-Up</title>
</head>
<body>
<div id="content"></div>
</body>
<script crossorigin="anonymous" src="https://p.trellocdn.com/power-up.min.js"></script&gt;
</html>
view raw index.html hosted with ❤ by GitHub

Now in the root of the [js/common] folder you will create a file [common.js]:

const PACKAGE = require('../../package.json');
/**
* CommonFunctions to be shared across all the project
*/
export default class Common {
/** @type {String} */
static APPNAME = PACKAGE.appName;
/** @type {String} */
static VERSION = PACKAGE.version;
/** @type {String} */
static detailsPage = "./details.html";
}
view raw common.js hosted with ❤ by GitHub

Now in the root of the [js] folder create the [client.js] file:

/* global TrelloPowerUp */
/// <reference path="../types/trello.d.js" />
import Common from './common/common';
/** @type {TrelloPowerUp} */
const tpu = window.TrelloPowerUp;
tpu.initialize({
'board-buttons': async (t) => await getBoardButton(t),
});
/**
* Returns the board button
* @param {TrelloObject} t
* @return {TrelloBoardButtonOption[]}
*/
const getBoardButton = async (t) => {
/** @type {String} */
const buttonName = await t.get("board", "private", "buttonName", "Hello World");
/** @type {TrelloBoardButtonOption} */
const boardButton = {
text: buttonName,
condition: "always",
icon: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAF2SURBVDhPtZMxT8JQEMfb0pI2xMbA0JWyNXQEJLHO7VrmfgAHTfgMLMTo0FknDU0TNze+gCbOSmSBwU2hgxMlAevd8wV95GG6+Euu73/v/e/aXlPhX8myrIBBUy4iXRmCIDicTqeeqqoHmKdp+lir1YaDweCeGHZx1u/vHTnOpWEYqSiKGWyRQI17juNc9cFDzNvEUay2ms1bkJtCXjTBE0WRCprFc70TTdO4Rb8DPa7rnoL+odfr6bZtP4HkFm0HeJ+xBrQg4WU+n7eSJLFR5wH8dfC3UJMGy+WyDJNGmQvwC4vFooyaNFAUZVUo/Pm5GdBbLBZXqEkD2Bjpuv6BOg/olSRpRNNv2u32NSzcoW0HeG9gJZAnQOx6/cKsVmc03YlZNWfgPacpi+/7rmma7yC5d8azDnhAb2AmNx6PJ77fGWqaqsmyvF8qleB19c9KpfJqWdZdo9E4juP4gdoJ3J8J6Xa7BgzXQr1er1/CMHwjBwyC8AW6vpgYpmCzMQAAAABJRU5ErkJggg==`,
callback: (tt) => { getBoardMenu(tt) },
};
// technically we send back an array with only one item
return [boardButton];
}
/**
* Gets the menu for the board button when the board button is clicked
* @param {TrelloObject} t
*/
const getBoardMenu = (t) => {
/** @type {TrelloPopupListOptions} */
const boardMenuPopup = {
title: "Hello World menu",
items: [],
};
boardMenuPopup.items.push({
text: "Settings",
callback: (tt) => { return showSettings(tt) },
});
// show it
t.popup(boardMenuPopup);
}
/**
* Shows the settings form
* @param {TrelloObject} t
*/
const showSettings = (t) => {
/** @type {TrelloPopupIFrameOptions} */
const settingsIframeOptions = {
title: "Settings",
url: Common.detailsPage,
args: { page: "settings" }
}
// show settings page
t.popup(settingsIframeOptions);
}
view raw client.js hosted with ❤ by GitHub

So, what this is doing is:

  1. Initializing the Trello Power-Up
  2. Hooking to the board buttons callback from Trello and adding a custom board button
  3. The button calls the Trello List Popup (essentially menu), when clicked.
  4. In that menu, I added ONE item, to show the settings in a pop-up iframe. Now, this is where things get interesting…

You might see how I might be doing something different from what Trello tutorials might have you doing. But if you look at the iframe reference, to Common.detailsPage, you will see that we added that as “details.html.” But WAIT, you did not ask me to create that page, so how is it going to work? Trust me, it will and in a moment, I will show you how it all works out. First clue is something called Web Pack. The second is pay close attention to the ARGS we are sending.

Next, create a new file called [details.js] and place it in the [js] folder with [details.js]. Here is the code for that page:

/* global TrelloPowerUp */
/// <reference path="../types/trello.d.js" />
import SettingsPage from "./pages/settingsPage";
/** @type {TrelloPowerUp} */
const tpu = window.TrelloPowerUp;
/** @type {TrelloObject} Trello iframe object */
const t = tpu.iframe();
t.render(() => {
/** @type { "settings" } */
const page = t.arg("page");
switch (page) {
case "settings":
const settings = new SettingsPage();
settings.render(t);
break;
}
});
view raw details.js hosted with ❤ by GitHub

What we are doing here is setting this up for “reuse.” Gosh, I love that word. “Reuse,” said it again. REUSE. Ok…

So, we read the PAGE args (remember how I told you to pay close attention). I ask Trello for the arg passed call “page” and from that get which page to load. I then create an instance of a page object.

Now this is where I love, love, love ES6 and classes. I created an instance of the SettingsPage class and called render on it, passing the ever-important reference to the Trello context object (t). But what the heck, there still is not a Settings Page and details.html still does not exist??? Wait for it…

Now, in the [pages] folder, you will create the [settingsPage.js] file, and you will add this code:

/// <reference path="../../types/trello.d.js" />
import Common from "../common/common";
export default class SettingsPage {
constructor() { }
/**
* Renders the settings page
* @param {TrelloObject} t
*/
render = async (t) => {
/** @type {String} */
const html = /*html*/`
<p>You are on the setting page for ${Common.APPNAME}, ${Common.VERSION}</p>
<p>Set the label for your button:</p>
<input type="text" id="boardButtonTextInput" />
<button id="saveButton" disabled>Save</button>&nbsp;<button id="closeButton">Close</button>
`;
// If you look at the index.html you will see there is a single div with this ID
// this is how we DYNAMICALLY build a page in memory (above) and plop it in place
document.getElementById("content").innerHTML = html;
// now hook up things…
/** @type {HTMLButtonElement} */
const saveButton = document.getElementById("saveButton");
/** @type {HTMLInputElement} */
const input = document.getElementById("boardButtonTextInput");
input.value = await t.get("board", "private", "buttonName", "Hello World");
input.addEventListener("keypress", () => saveButton.disabled = false);
saveButton.addEventListener("click", () => {
t.set("board", "private", "buttonName", input.value);
saveButton.disabled = true;
});
document.getElementById("closeButton").addEventListener("click", () => {
t.closePopup();
});
t.sizeTo("#content");
};
}
view raw settingsPage.js hosted with ❤ by GitHub

What we are doing here is PURE ES6 GLORIOUS MAGIC. I generate a string using inline HTML (via the beautiful backtick), and you will see some VS Code fun stuff (/*html*/), this makes your code look like React inline HTML via the Inline HTML extension. OMG, it is awesome. Ok, then remember in the [index.html] file the div with the “id” of “content.” Well, I then assign the HTML string to the content div and viola, the settings page comes alive in the iframe of the Trello Popup window – just like that. And then because it is dynamic, I hook to the controls I created, add event hooks and such. I then call t.sizeTo() to tell Trello to fit the contents of the iframe neatly into the Popup window.

Hey, but there still is not a [details.html] page… should I not go and create that? NO!!! It’s coming, I promise.

Now, that is it for the code that makes your Power-Up tick. The rest is getting the development environment configured for runtime testing and building your code via Web Pack.

Step 3: Set Up Node and Web Pack

So, writing the code is half the battle. You could just make a copy if index.html and called it details.html place all the above code on a web server and call it a day. And everything will run.

But building a Power-Up and being able to update and test your code live is crucial to being able to make a compelling Power-Up in short order. You could just make changes, publish to the web, test, rinse – repeat. But it will take you a long time, frustration and for just a little more work, you will get SO MUCH MORE. And finally, your code will not be optimized (minified) for better browser runtime compile. So, here is how you round out your development environment for a Trello Power-Up.

Next thing you do is create a [package.json] at the root of the project folder. This file will tell NODE which packages to go pull of the Internet so that you can do the paragraph above. Here is the code:

{
"name": "trello-hello-world",
"appName": "Hello World",
"version": "1.0",
"description": "A Power-Up to say hello.",
"scripts": {
"start": "webpack-dev-server –mode development",
"build": "node webpack –mode production"
},
"dependencies": {
"browserify": "^17.0.0",
"cors": "^2.8.5",
"express": "^4.15.2",
"marked": "^4.0.17",
"node": "^19.8.1",
"openssl": "^2.0.0"
},
"engines": {
"node": "6.10.x"
},
"license": "MIT",
"author": "<your name>",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^8.2.3",
"babel-preset-es2015": "^6.24.1",
"copy-webpack-plugin": "^10.2.4",
"html-loader": "^3.1.0",
"html-webpack-plugin": "^5.5.0",
"terser-webpack-plugin": "^5.3.6",
"webpack": "^5.69.1",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
}
}
view raw package.json hosted with ❤ by GitHub

The next thing you need is the Web Pack file. Ok, remember how I kept putting off the creating the [details.html] file. Wait for it…

The following file is going to look rather complex if you have not used Web PAck before. What this does it tells NODE how to “compile” your code and setup the development environment for local development testing.

So, let’s get started by adding the following file to the root of you project folder [webpack.config.js]:

const CopyWebpackPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const fs = require("fs");
module.exports = async (env, options) => {
const isProduction = options.mode === 'production';
const config = {
devtool: isProduction ? false : 'source-map',
mode: isProduction ? "production" : "development",
entry: {
details: "./js/details.js",
client: "./js/client.js",
},
output: {
devtoolModuleFilenameTemplate: "webpack:///[resource-path]?[loaders]",
clean: true,
},
resolve: {
extensions: [".ts", ".tsx", ".html", ".js"],
},
module: { },
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
template: "./views/index.html",
chunks: ["client"],
}),
new HtmlWebpackPlugin({
filename: "details.html",
template: "./views/index.html",
chunks: ["details"],
}),
],
devServer: {
hot: true,
headers: {
"Access-Control-Allow-Origin": "*",
},
https: env.WEBPACK_BUILD || options.https !== undefined ? options.https : {
key: fs.readFileSync('./certs/server.key'),
cert: fs.readFileSync('./certs/server.crt'),
},
port: process.env.npm_package_config_dev_server_port || 12345,
},
};
return config;
};

I am not going into huge depth explaining this file as that is a whole post on its own. Bottom line what this is doing is configuring web pack. I will explain it like this:

  • defines the core entry points as chunks [detial.js] and [client.js].
  • in the plugins section, you will see the “reuse” coolness I was referring to above. On the fly we use the same file [index.html] for creating TWO files: index and details. And there you have it.
  • the devServer is the last bit that makes this whole journey worthwhile. This creates a NODE server on your box, running on port 12345.

Your own Dev Server running locally on your box is the key but it will not work without this next bit. Trello REQUIRES all connections to be protected by HTTPS, so you MUST have SSL certificates.

Generating certs is a mild amount of pain, but with a LONG-TERM gain. So, you WILL need to do this part. And it is not hard. Here is how you generate a cert (and key) for yourself:

  1. Go here: https://slproweb.com/products/Win32OpenSSL.html
  2. Download the light version that is for your OS (for me it was Windows x64 – Light)
  3. Install it.
  4. Once installed you can run this command in VS Code (in Windows):
& "C:\Program Files\OpenSSL-Win64\bin\openssl" req -newkey rsa:2048 -x509 -nodes -keyout server.key -new -out server.crt -sha256 -days 3650
  1. It will ask you for a passphrase (something like “test” is ok), and to confirm the passphrase, then country, state, city, company, unit, and such. Nothing is critical here, put what you want for these fields.
  2. It will create two files in the root of your project [server.crt] and [server.key].
  3. Move these files to the [certs] folder.

And this cert is good for 10 years, so you can add it to all your future projects.

You are now ready to start development. Open a terminal (press CTRL+`) and in the root directory of your project, run the following command to enable/init/update the project:

npm update

This pulls in all the bits to make your development environment come alive.

Step 4: Setup Trello Admin Panel

To test your Power-Up, you can use the Trello development sandbox. Ensure you’ve created a Trello board and head to https://trello.com/power-ups/admin to enable the developer mode (see Trello documentation on this part).

Then, add your Power-Up to start testing. Here are the steps for our demo:

  1. Click New
  2. In the first field type “Hello World”
  3. Next, select your workspace.
  4. In the iframe URL, you type in your (soon to be launched local sandbox web server): https://localhost:12345/index.html
  5. Fill in the emails and your name fields, then click Create.
  6. You go to Capabilities and tick on “Board Buttons”

You are ready for the next step.

Step 5: Start your local web server and Test

In the terminal window, type this command:

npm start

You will see the web server start and now it is time to approve the certificate:

  1. Open your browser and go to: https://localhost:12345/index.html
  2. You will see a REJECTION, something like: Your connection isn’t private!
  3. You must click Advanced, then click [Continue to localhost (unsafe)]

You can get around this by adding the certificate to the Trusted Root Certificate Authorities on your computer. But that is different for Mac and Windows users, so I will let you look that one up on your own.

By the way, I never do that. I just ALWAYS run this step before I run locally. Just remember if you go into Trello and it appears to hang and your Power-Up is not loaded… did you forget this step?

Now we go to Trello to add your Power-Up. Go to Trello, and go to your board:

  1. Click Power-Ups, Add Power-Up.
  2. Go to the Custom option.
  3. Select Hello World, then click Add.

You will now see the [Hello World] board button appears. You are now running a local developer environment from VS Code with a live running debug in Trello. Go make a code change, refresh the browser and viola – your updated code changes are there and ready to test.

Now a word about “DEBUG.” I have not found a way to really hook up VS Code to Edge/Chrome yet and live debug. I am sure there is a way and if someone knows, please add a comment — I will give it a whirl and add a future post. What I do instead is open F12. When I get an error, it usually tells me the exact line it failed on, and I can go there in VS Code. I also use a lot of console.log() statements to follow code flow and get results. And when I feel particularly stuck, I though in a good debugger; line or two in the code so the F12 tools stop, and I can review things there. In the end this has worked rather well, but I really would like to be able to set a breakpoint in VS Code too… for now, I am happy enough.

Conclusion

Creating a Trello Power-Up in Visual Studio Code using npm is the only way to go as far as I am concerned.

Happy coding, and remember, the possibilities are endless when it comes to creating Trello Power-Ups to match your specific workflow. And over time, I will probably extend on this Power-Up tutorial as I find new things to write about.

Trello Type Definitions Updated

I have developed 6 Power-Ups now for Trello. When I first started developing for Trello it was HARD to keep track of and use the Power-Up API. I would ask myself questions like:

“t.alert(), is the JSON it takes {message:’…’} or {text:’…’} and was the delay set with ‘duration’ or ‘time’…?”

I kept having to jump back and forth from the online documentation and my code. It took forever. My code was full of errors. I usually did not know I got something wrong until I saw red in the F12 Developer Tools in Edge. It felt so… 1995 all over again.

I might be dating myself a bit here, but when I started development, I learned BASIC on a TI-99/4a. In those days, outside plain old “compile errors” (which, it took a while to compile too), you usually learned about errors when you got a syntax error at runtime. In my first job, job as a developer, I wrote COBOL and you learned about an error in your code when the job failed its run at 2am in the morning and was pulled off the mainframe.

In today’s modern programming landscape, you get intellisense helping you identify the properties of objects and what comes next. You have ChatGPT helping you write out the super complex sorting algorithms. Algorithms, by the way, that you went to college to learn. You know algorithms, those ugly mind warping beasts where professors cackled like Smith in the Matrix as they assigned them. Algorithms that caused you to break and sharpen thousands of #2 pencils. Algorithms that wasted brain-cells. Oh, so many brain cells. And after it all you still only made a C in the class. I digress… So… Yep… we have come a LONG way.

So, given how far we have come it boggles my mind that some object models are not brought forward to work with modern development platforms.

So, as per my previous post on the subject, I hunkered down and built a Trello Type Definitions file.

As a Trello developer, the one thing I think helps to keep me sane is that I am able to use the HECK out of the Type Definitions file, which makes my life much easier.

But my Type Definitions file is not perfect, and I am constantly “fixing it” and adding to it.

Yesterday was one such day. I have taken the last 4 months of Trello development (lessons learned, etc.) and published a rather significant update.

I just published v1.0.1.20231021. If you are using it and find it helpful, please let me know.

Autolaunch and Outlook on Windows

I have been updating my 5entences add-in install for use in the AppSource/App Center for Office, plus adding an auto-summarization capability via OpenAI. 😉 COMING SOON!!!!

Originally, this add-in was using the Dialog API inside the “original” OnSend event. This was a bit nifty as it presented the user with a blocking popup that would allow them to manage everything in one place. It told them, they had more than 5 sentences, and would suggest they go back and fix it, or go ahead and send it. But that type of event is NOT supported in the App Store.

Enter Smart Events, and with it specifically the OnMessageSend event. The adventure began…

I have an Office 365 account and installed Office 365 from my portal.office.com page, made sure I was updated, and everything was looking good, except that:

  • When I have my personal Outlook.com email address attached to Outlook, I cannot debug add-ins in Outlook full client on Windows.
  • Even when I did remove my Outlook.com email address, my events were still not firing.
  • Everything worked great in Office on the Web.

So, I proceeded with updating the code using Office on the web to debug, giving up on Outlook full client in the interim. Once I got everything working well in Outlook on the Web, I went back to Outlook on Windows and began to lose my hair.

I quickly discovered that my ES6 code in the command.js, were not working. As you dig into the documentation you find that the WebViewURL and the override for JSRuntime, sharing the same file become an issue. It turns out that even with the WebView2 control installed on my box, and even though I have the latest version of Office 365 full client installed, my JSRuntime code reverts to Trident+ (IE11). I refactored my code to:

  • stop using const, instead var everything.
  • stop using => arrow operators, reverting to full function()
  • stop using async/await, and built pyramids of doom
  • stopped using Promises() even

The reason I reverted, is because I pulled out all transpiling and polyfills because they SLOW down my add-in code, make it too large and it impacts my already overwhelmed server. I also like to remain as pure JavaScript ES6 as I can. I am a bit puritanical, I guess. 😆

But even with all that it did not work. So, line by line I went and found the first problem:

/**
* Ensures the Office.js library is loaded.
*/
Office.onReady((info) => {
/**
* Maps the event handler name specified in the manifest's LaunchEvent element to its JavaScript counterpart.
* This ensures support in Outlook on Windows.
*/
if (Office.context.platform === Office.PlatformType.PC || Office.context.platform == null) {
Office.actions.associate("onMessageSendHandler", onMessageSendHandler);
}
});
view raw onready.js hosted with ❤ by GitHub

That code seemed to work, but the add-in would just hang telling me “…it is taking too long… Try Again.” I resolved it by doing this:

/**
* Checks to see if we are running in Windows Outlook
* @returns {Boolean}
*/
function isPC() {
try {
if (Office.context.platform === Office.PlatformType.PC || Office.context.platform === null) {
return true;
} else {
return false;
}
} catch {
return false;
}
}
if (isPC() === true) {
Office.actions.associate("onMessageSendHandler", onMessageSendHandler);
} else {
Office.onReady(function () {});
// Everything below is for OTHER (non-PC) clients per older constructs. Not certain
// if any of this is needed except for COMMANDS (rather than EVENTS), so eventually
// if commands are added, we have this in place…
var g = getGlobal();
// The add-in command functions need to be available in global scope
g.onMessageSendHandler = onMessageSendHandler; // on send event
}
/**
* OnSend event triggered
* @param {Office.AddinCommands.Event} event
*/
function onMessageSendHandler(event) {
Office.onReady(function () {});
Office.context.mailbox.item.body.getAsync(Office.CoercionType.Text, { asyncContext: event }, function (asyncResult) {
var body = asyncResult.value;
var event = asyncResult.asyncContext;
// … more code here …
});
}
view raw commands.js hosted with ❤ by GitHub

Essentially, I had to put the associate at the root of the file. Without the PC check, this would bomb on Office online and the Mac, so I gated that with the function you see: IsPC(). And Office online needs the Office.onReady() in the root to work effectively, so you see that there. But putting Office.ready() in the root broken Outlook on Windows, but also ignoring it/not using it, gave me the same problem. I discovered that if I put it in the event activation itself, as you see, I was finally able to get into my event handler and execute and Office.context (line of code). However, deeper in my code, it was STILL failing.

Debugging my code further, I had a LOT of Regex to determine what was going on in the body of the email. Introduce Trident+ and IE11 (es5), and you realize that “look heads” and a lot of really cool stuff I have grown accustomed to do not work. So, I struggled and struggled, until I just sent to my friend ChatGPT and asked it to produce me IE11 compatible Regex for each of my Regex statements. And then viola, my add-in was fully functional on Windows and Office online.

I am still a hacker by nature – what works, works. And I have converted from Office Add-ins written in C# with Visual Studio Tools to Office to this OfficeJS paradigm. I spend a lot of time reading the documentation and I SWEAR it tells me that with my version of Office 365 and having the WebView2 control installed, I should not be reverting to IE11 (Trident+) for Smart Events:

Microsoft® Outlook® for Microsoft 365 MSO (Version 2308 Build 16.0.16731.20052) 64-bit

But maybe it is buried in there somewhere that on Windows/PC, it always uses IE11 for Smart Events. Either way, to make sure you support 99% of the market out there, I guess you need to write you code like this or use polyfills and transpile. Bottom line is I got it to work. If anyone else is having similar issues, hopefully this helps. If anyone has an alternative or can point to something I am doing wrong (other than using polyfills and transpile my code), I am open to suggestions.

Excel Send to Trello

Well, that was fast. My latest add-in is published. Excel Send to Trello.

As per my previous post, the thing I found most interesting was how Excel full client seems to fail if you configure your server .htaccess file to prevent caching. Well, I found out that my Outlook Send to Trello add-in actually had the same problem too. The Outlook client just happened to refresh this morning and my icon disappeared there too. In Office online it seems to work, but in the full client you cannot seem to force the client to NOT cache. I see my files all pulled down locally in the Wef folder and my concern is that when I update the add-in it will not go get the latest every time… I have actually seem and beat my head over this problem a few times. But the solution there for anyone working on a BETA site for example making changes and all of a sudden the full-client stops refreshing your updates, this is a good article to keep handy.

But when Wef strikes in production, I have found customers are not so excited to blow away this folder and find all their add-ins, preferences, stored cached credentials and other goodies for each and every add-in are gone. Ergo why I added the no-cahce to the .htacess. Oh well. 🙁

Also, just to share something else as I am delving more and more into publishing add-ins for real. As an Office Developer, in the traditional sense (aka boomer 😛, VBA/VSTO/COM), there are aspects of living in an HTML web world that I still learning (although this is an old one it comes up now and again because I forget something).

You have to worry about various attack vectors and sanitizing HTML strings. There are LOTS of libraries and solutions out there and Mozilla even has documented a possible standard supported in several browsers, but not all. It is a tricky thing because some sanitizers do too much or not enough, and then you also rely on a dependency which has now burned me more often than just owning things that might be a hundred lines of code for my own common library of goodies.

So, I have created my own based on various library implementations, and found the best option is to escape most of the stuff you find “injected” rather than remove it.

/**
* Sanitizes the string for possible malicious values
* @param {String} string
* @returns {String}
*/
static sanitizeString = (string) => {
try {
string = string.replace(/(javascript:|onerror)/gi, "");
string = string.replace(/undefined/gi, "");
string = string.replace(/<script/gi, "&lt;script");
string = string.replace(/<iframe/gi, "&lt;iframe");
string = string.replace(/<object/gi, "&lt;object");
string = string.replace(/<embed/gi, "&lt;embed");
string = string.replace(/<applet/gi, "&lt;applet");
string = string.replace(/<form/gi, "&lt;form");
string = string.replace(/<meta/gi, "&lt;meta");
string = string.replace(/<link/gi, "&lt;link");
string = string.replace(/<a\s/gi, "&lt;a ");
string = string.replace(/<img\s/gi, "&lt;img ");
string = string.replace(/="/gi, "&#x3D;&#39;");
string = string.replace(/='/gi, "&#x3D;&#39;");
string = string.replace(/=`/gi, "&#x3D;&#x60;");
string = string.replace(/\/>/gi, "&#x2F;&gt;");
return string;
} catch (e) {
return `[[SANITIZED STRING MALFORMED: ${e}]]`;
}
};

The important thing is that in the web world, anytime you take data from one service to another, or take input in a field, or grab input from some element on a page and insert it back into another element in your code, there is a hack waiting to happen if you do not sanitize.

New Excel Add-in & Side Loading

I recently published a new Excel Add-in called “Excel Send to Trello.” It is a pretty nifty add-in that will take an Excel sheet full of names, descriptions, and dates and build a bunch of Trello Cards on a specific list in Trello for you. It is just a start (v1.0) and has many features to come based on popularity.

But when I submitted the add-in to AppSource they came back to me with an issue that I was not seeing. Specifically, that the icon on the Ribbon was showing the default add-ins image and not my icon. I did not see this on my “beta” test site that I was using but had sent them a link to my manifest with was right to my production published version. The issue it turns out after a lot of testing was the .htaccess on my production site and the ONLY difference was this line:

<IfModule mod_headers.c>
    Header set Cache-Control "no-store, no-cache, must-revalidate"
</IfModule>

The problem it turns out is that Excel (full client) cannot read your icon without caching it. This works FINE in Excel Online. Oh well. So, I cannot prevent my add-in JS files from being cached which it turns out is a huge problem when I push an update it can take days/weeks for the Office application to expire its own cache. Unless someone know a way around this…

Anway, the way I discovered this was via sideloading the manifest pointing to my production site. And it also turns out I had forgotten how EASY Outlook makes sideloading. In Excel (full client) it is not that easy, because you do not have the option to point to your own manifest, except in Excel Online. Then I recall some work a colleague Marty Andren and I worked on many, many years ago, called the Web Add-in Side loader. And low and behold, it still works like a charm.

I was able to sideload in Excel using the command line tool, then I saw the problem. That is where I began the file-by-file comparison “Beta” to “Production” and found the difference mentioned above.

Anyway, it was an adventure, with a blast from the past! And keep an eye out, my new Excel Add-in will hopefully be published any day now.

Inline (a)Wait for JavaScript Constructors

I have been working on some new code and found a need to make a series of calls to server-side PHP, where it would process data in a MySQL database, munch on some data and then return a result. I wanted to perform this operation inside a class constructor, so I could essentially do something like this:

const myObj = new MyClassToCallTheServer();
let someValue = myObj.doSomegthingWithTheServerData();
view raw inline1.js hosted with ❤ by GitHub

The problem is that a constructor in JavaScript cannot be declared async and you cannot place an await on your server call. So, something like this fails:

export default class MyClassToCallTheServer {
#internvalValue = null;
constrcutor() {
// this.#internalValue = await this.#callTheServer(); <– the await will fail to compile
this.#internalValue = this.#callTheServer();
}
doSomethingWithTheServerData = () => {
// the #internalValue is still null becasue we could not await in the constructor
return this.#internalValue;
}
#callTheServer = async() => {
// imagine some code here with a fetch command…
return serverData;
}
}
view raw inline2.js hosted with ❤ by GitHub

To get around this, I learned a new trick, so I thought I would share:

export default class MyClassToCallTheServer {
#internvalValue = null;
#loading = false;
constrcutor() {
this.#internalValue = this.#callTheServer();
}
doSomethingWithTheServerData = async () => {
while(this.#loading) await new Promise(resolve => setTimeout(resolve, 10));
// the #internalValue is not null because we waited
return this.#internalValue;
}
#callTheServer = async() => {
this.#loading = true;
// imagine some code here with a fetch command…
this.#loading = false;
return serverData;
}
}
view raw inline3.js hosted with ❤ by GitHub

Small modification to the original code above and done:

const myObj = new MyClassToCallTheServer();
let someValue = await myObj.doSomegthingWithTheServerData();
view raw inline4.js hosted with ❤ by GitHub

I threw post this together fairly quickly, but hopefully it makes sense. ☺️

Outlook Signature Add-in

I recently got a question about this add-in created as a sample by the Microsoft Office Developer Team. It is provided as a sample and is a challenging bit of code to follow. It all started because I was trying to provide some advice to someone who is new to the OfficeJS world (but not Office development in general). They wanted to build a cross-platform add-in for a cause. After tinkering with it for a few hours, I managed to get it to work. But then I looked it over and it was quite a bit of tinkering on my part.

In most cases I create Add-ins based on React, but because I have done a lot of Trello development of late, I have gotten into more of a pure ES6 vibe. The code provided in the add-in as I stated was hard to follow because I think I am losing my jQuery and ES5 JavaScript skills.

Anyway, as I said, I spent a few hours today and worked on getting it into ES6 format by moving most of the code into classes, removing redundant code, removing redundant HTML pages, consolidating CSS and adding a lot of additional JSDoc comments throughout the code.

Hopefully, this version is a bit easier to follow for those that develop in a more modern ES6 style.

NOTE: This looks and behaves exactly the same, but I have not thoroughly tested it. So, it might be a bit rough here and there. If you encounter issues please let me know. 😁