Repository: trello/power-up-template
Branch: gh-pages
Commit: fa6bb1554d56
Files: 17
Total size: 30.1 KB
Directory structure:
gitextract_uluu2zf9/
├── .gitignore
├── LICENSE
├── README.md
├── auth-success.html
├── authorize.html
├── board-bar.html
├── css/
│ ├── board-bar.css
│ └── modal.css
├── index.html
├── js/
│ ├── board-bar.js
│ ├── client.js
│ ├── modal.js
│ ├── section.js
│ └── settings.js
├── modal.html
├── section.html
└── settings.html
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2017 Trello
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Trello Power-Up Template
Hey there 👋
This is a full featured sample Trello Power-Up. What does that mean for you? We hope it's a great starting point for you to fork, and make your own great Power-Up that makes Trello work even better for you.
Want to try this sample out right away without waiting? Go ahead and grab the connector URL (https://trello.github.io/power-up-template/index.html). If you've forked it, you should grab your fork's connector URL. And then head here:
👉 [https://trello.com/power-ups/admin](https://trello.com/power-ups/admin)
Select the Trello team you want to add the Power-Up to. Note: You need to be an admin of the Trello team to add custom Power-Ups to it.
Now click the `Create new Power-Up` button. If this is your first time creating a Power-Up, you'll need to agree to a "Joint Developer Agreement" first. After you have done that, you need to give your cool new Power-Up a name, and paste the connector URL in (the one you created earlier). You can read about what all of the fields are used for at [Managing Power-Ups](https://developers.trello.com/v1.0/docs/managing-power-ups).
Click `Save` and it's time to celebrate 🎉 🎊
Now when you look at the Power-Ups for any board in that team, your awesome new Power-Up will be available under the `Custom` section. All changes you commit and push to the repo will be reflected in the Power-Up!
---
Want more information about Power-Ups? 🤔
👉 [https://developers.trello.com/power-ups/intro](https://developers.trello.com/power-ups/intro)
We even have office hours you can sign up for if you want to talk to a real live person about your Power-Up. Just grab a slot that works for you on this [calendar](https://calendar.google.com/calendar/selfsched?sstoken=UU5DczNLUkNIbk5ifGRlZmF1bHR8YzJmZWM4YWM0NTgxMTE1NmRmMzgxNzMwODRjYzEwZGU). (Remember to add a bit about what you'd like help with when signing up for a slot).
---
Looking for a more _realistic_ example Power-Up? You may find the Trello Card Snooze Power-Up useful. 😴
👉 [Trello Card Snooze Glitch Project](https://glitch.com/edit/#!/trellocardsnooze)
================================================
FILE: auth-success.html
================================================
You're set!
This window should close automatically. If it doesn't, go ahead and close it.
You should only call t.boardBar() as a response to a user action, such as clicking a button.
In addition to setting the height when calling t.boardBar() it can be helpful to resize the height from within the boardBar using t.sizeTo(). Remember the boardBar is restricted to 50% of the board height as a maximum.
Try to get your boardBar to load and render as quickly as possible.
This is the HTML connector for the Power-Up. It will be loaded into a hidden iframe when your Power-Up is enabled.
If you're looking for the manifest file, you should head over here.
Not sure what is going on? Head on over to the README.
================================================
FILE: js/board-bar.js
================================================
/* global TrelloPowerUp */
var t = TrelloPowerUp.iframe();
// want to know when you are being closed?
window.addEventListener('unload', function(e) {
// Our board bar is being closed, clean up if we need to
});
t.render(function(){
// this function we be called once on initial load
// and then called each time something changes that
// you might want to react to, such as new data being
// stored with t.set()
});
================================================
FILE: js/client.js
================================================
/* global TrelloPowerUp */
// we can access Bluebird Promises as follows
var Promise = TrelloPowerUp.Promise;
/*
Trello Data Access
The following methods show all allowed fields, you only need to include those you want.
They all return promises that resolve to an object with the requested fields.
Get information about the current board
t.board('id', 'name', 'url', 'shortLink', 'members')
Get information about the current list (only available when a specific list is in context)
So for example available inside 'attachment-sections' or 'card-badges' but not 'show-settings' or 'board-buttons'
t.list('id', 'name', 'cards')
Get information about all open lists on the current board
t.lists('id', 'name', 'cards')
Get information about the current card (only available when a specific card is in context)
So for example available inside 'attachment-sections' or 'card-badges' but not 'show-settings' or 'board-buttons'
t.card('id', 'name', 'desc', 'due', 'closed', 'cover', 'attachments', 'members', 'labels', 'url', 'shortLink', 'idList')
Get information about all open cards on the current board
t.cards('id', 'name', 'desc', 'due', 'closed', 'cover', 'attachments', 'members', 'labels', 'url', 'shortLink', 'idList')
Get information about the current active Trello member
t.member('id', 'fullName', 'username')
For access to the rest of Trello's data, you'll need to use the RESTful API. This will require you to ask the
user to authorize your Power-Up to access Trello on their behalf. We've included an example of how to
do this in the `🔑 Authorization Capabilities 🗝` section at the bottom.
*/
/*
Storing/Retrieving Your Own Data
Your Power-Up is afforded 4096 chars of space per scope/visibility
The following methods return Promises.
Storing data follows the format: t.set('scope', 'visibility', 'key', 'value')
With the scopes, you can only store data at the 'card' scope when a card is in scope
So for example in the context of 'card-badges' or 'attachment-sections', but not 'board-badges' or 'show-settings'
Also keep in mind storing at the 'organization' scope will only work if the active user is a member of the team
Information that is private to the current user, such as tokens should be stored using 'private' at the 'member' scope
t.set('organization', 'private', 'key', 'value');
t.set('board', 'private', 'key', 'value');
t.set('card', 'private', 'key', 'value');
t.set('member', 'private', 'key', 'value');
Information that should be available to all users of the Power-Up should be stored as 'shared'
t.set('organization', 'shared', 'key', 'value');
t.set('board', 'shared', 'key', 'value');
t.set('card', 'shared', 'key', 'value');
t.set('member', 'shared', 'key', 'value');
If you want to set multiple keys at once you can do that like so
t.set('board', 'shared', { key: value, extra: extraValue });
Reading back your data is as simple as
t.get('organization', 'shared', 'key');
Or want all in scope data at once?
t.getAll();
*/
var GLITCH_ICON = './images/glitch.svg';
var WHITE_ICON = './images/icon-white.svg';
var GRAY_ICON = './images/icon-gray.svg';
var randomBadgeColor = function() {
return ['green', 'yellow', 'red', 'none'][Math.floor(Math.random() * 4)];
};
var getBadges = function(t){
return t.card('name')
.get('name')
.then(function(cardName){
console.log('We just loaded the card name for fun: ' + cardName);
return [{
// dynamic badges can have their function rerun after a set number
// of seconds defined by refresh. Minimum of 10 seconds.
dynamic: function(){
// we could also return a Promise that resolves to this as well if we needed to do something async first
return {
title: 'Detail Badge', // for detail badges only
text: 'Dynamic ' + (Math.random() * 100).toFixed(0).toString(),
icon: GRAY_ICON, // for card front badges only
color: randomBadgeColor(),
refresh: 10 // in seconds
};
}
}, {
// its best to use static badges unless you need your badges to refresh
// you can mix and match between static and dynamic
title: 'Detail Badge', // for detail badges only
text: 'Static',
icon: GRAY_ICON, // for card front badges only
color: null
}, {
// card detail badges (those that appear on the back of cards)
// also support callback functions so that you can open for example
// open a popup on click
title: 'Popup Detail Badge', // for detail badges only
text: 'Popup',
icon: GRAY_ICON, // for card front badges only
callback: function(context) { // function to run on click
return context.popup({
title: 'Card Detail Badge Popup',
url: './settings.html',
height: 184 // we can always resize later, but if we know the size in advance, its good to tell Trello
});
}
}, {
// or for simpler use cases you can also provide a url
// when the user clicks on the card detail badge they will
// go to a new tab at that url
title: 'URL Detail Badge', // for detail badges only
text: 'URL',
icon: GRAY_ICON, // for card front badges only
url: 'https://trello.com/home',
target: 'Trello Landing Page' // optional target for above url
}];
});
};
var boardButtonCallback = function(t){
return t.popup({
title: 'Popup List Example',
items: [
{
text: 'Open Modal',
callback: function(t){
return t.modal({
url: './modal.html', // The URL to load for the iframe
args: { text: 'Hello' }, // Optional args to access later with t.arg('text') on './modal.html'
accentColor: '#F2D600', // Optional color for the modal header
height: 500, // Initial height for iframe; not used if fullscreen is true
fullscreen: true, // Whether the modal should stretch to take up the whole screen
callback: () => console.log('Goodbye.'), // optional function called if user closes modal (via `X` or escape)
title: 'Hello, Modal!', // Optional title for modal header
// You can add up to 3 action buttons on the modal header - max 1 on the right side.
actions: [{
icon: GRAY_ICON,
url: 'https://google.com', // Opens the URL passed to it.
alt: 'Leftmost',
position: 'left',
}, {
icon: GRAY_ICON,
callback: (tr) => tr.popup({ // Callback to be called when user clicks the action button.
title: 'Settings',
url: 'settings.html',
height: 164,
}),
alt: 'Second from left',
position: 'left',
}, {
icon: GRAY_ICON,
callback: () => console.log('🏎'),
alt: 'Right side',
position: 'right',
}],
})
}
},
{
text: 'Open Board Bar',
callback: function(t){
return t.boardBar({
url: './board-bar.html',
height: 200
})
.then(function(){
return t.closePopup();
});
}
}
]
});
};
var cardButtonCallback = function(t){
// Trello Power-Up Popups are actually pretty powerful
// Searching is a pretty common use case, so why reinvent the wheel
var items = ['acad', 'arch', 'badl', 'crla', 'grca', 'yell', 'yose'].map(function(parkCode){
var urlForCode = 'http://www.nps.gov/' + parkCode + '/';
var nameForCode = '🏞 ' + parkCode.toUpperCase();
return {
text: nameForCode,
url: urlForCode,
callback: function(t){
// In this case we want to attach that park to the card as an attachment
// but first let's ensure that the user can write on this model
if (t.memberCanWriteToModel('card')){
return t.attach({ url: urlForCode, name: nameForCode })
.then(function(){
// once that has completed we should tidy up and close the popup
return t.closePopup();
});
} else {
console.log("Oh no! You don't have permission to add attachments to this card.")
return t.closePopup(); // We're just going to close the popup for now.
};
}
};
});
// we could provide a standard iframe popup, but in this case we
// will let Trello do the heavy lifting
return t.popup({
title: 'Popup Search Example',
items: items, // Trello will search client-side based on the text property of the items
search: {
count: 5, // How many items to display at a time
placeholder: 'Search National Parks',
empty: 'No parks found'
}
});
// in the above case we let Trello do the searching client side
// but what if we don't have all the information up front?
// no worries, instead of giving Trello an array of `items` you can give it a function instead
/*
return t.popup({
title: 'Popup Async Search',
items: function(t, options) {
// use options.search which is the search text entered so far
// and return a Promise that resolves to an array of items
// similar to the items you provided in the client side version above
},
search: {
placeholder: 'Start typing your search',
empty: 'Huh, nothing there',
searching: 'Scouring the internet...'
}
});
*/
};
// We need to call initialize to get all of our capability handles set up and registered with Trello
TrelloPowerUp.initialize({
// NOTE about asynchronous responses
// If you need to make an asynchronous request or action before you can reply to Trello
// you can return a Promise (bluebird promises are included at TrelloPowerUp.Promise)
// The Promise should resolve to the object type that is expected to be returned
'attachment-sections': function(t, options){
// options.entries is a list of the attachments for this card
// you can look through them and 'claim' any that you want to
// include in your section.
// we will just claim urls for Yellowstone
var claimed = options.entries.filter(function(attachment){
return attachment.url.indexOf('http://www.nps.gov/yell/') === 0;
});
// you can have more than one attachment section on a card
// you can group items together into one section, have a section
// per attachment, or anything in between.
if(claimed && claimed.length > 0){
// if the title for your section requires a network call or other
// potentially length operation you can provide a function for the title
// that returns the section title. If you do so, provide a unique id for
// your section
return [{
id: 'Yellowstone', // optional if you aren't using a function for the title
claimed: claimed,
icon: GLITCH_ICON,
title: 'Example Attachment Section: Yellowstone',
content: {
type: 'iframe',
url: t.signUrl('./section.html', { arg: 'you can pass your section args here' }),
height: 230
}
}];
} else {
return [];
}
},
'attachment-thumbnail': function(t, options){
// options.url has the url of the attachment for us
// return an object (or a Promise that resolves to it) with some or all of these properties:
// url, title, image, modified (Date), created (Date), createdBy, modifiedBy
// You should use this if you have useful information about an attached URL but it
// doesn't warrant pulling it out into a section via the attachment-sections capability
// for example if you just want to show a preview image and give it a better name
// then attachment-thumbnail is the best option
return {
url: options.url,
title: '👉 ' + options.url + ' 👈',
image: {
url: GLITCH_ICON,
logo: true // false if you are using a thumbnail of the content
},
};
// if we don't actually have any valuable information about the url
// we can let Trello know like so:
// throw t.NotHandled();
},
'board-buttons': function(t, options){
return [{
// we can either provide a button that has a callback function
// that callback function should probably open a popup, overlay, or boardBar
icon: WHITE_ICON,
text: 'Popup',
callback: boardButtonCallback
}, {
// or we can also have a button that is just a simple url
// clicking it will open a new tab at the provided url
icon: WHITE_ICON,
text: 'URL',
url: 'https://trello.com/inspiration',
target: 'Inspiring Boards' // optional target for above url
}];
},
'card-badges': function(t, options){
return getBadges(t);
},
'card-buttons': function(t, options) {
return [{
// usually you will provide a callback function to be run on button click
// we recommend that you use a popup on click generally
icon: GRAY_ICON, // don't use a colored icon here
text: 'Open Popup',
callback: cardButtonCallback
}, {
// but of course, you could also just kick off to a url if that's your thing
icon: GRAY_ICON,
text: 'Just a URL',
url: 'https://developers.trello.com',
target: 'Trello Developer Site' // optional target for above url
}];
},
'card-detail-badges': function(t, options) {
return getBadges(t);
},
'card-from-url': function(t, options) {
// options.url has the url in question
// if we know cool things about that url we can give Trello a name and desc
// to use when creating a card. Trello will also automatically add that url
// as an attachment to the created card
// As always you can return a Promise that resolves to the card details
return new Promise(function(resolve) {
resolve({
name: '💻 ' + options.url + ' 🤔',
desc: 'This Power-Up knows cool things about the attached url'
});
});
// if we don't actually have any valuable information about the url
// we can let Trello know like so:
// throw t.NotHandled();
},
'format-url': function(t, options) {
// options.url has the url that we are being asked to format
// in our response we can include an icon as well as the replacement text
return {
icon: GRAY_ICON, // don't use a colored icon here
text: '👉 ' + options.url + ' 👈'
};
// if we don't actually have any valuable information about the url
// we can let Trello know like so:
// throw t.NotHandled();
},
'show-settings': function(t, options){
// when a user clicks the gear icon by your Power-Up in the Power-Ups menu
// what should Trello show. We highly recommend the popup in this case as
// it is the least disruptive, and fits in well with the rest of Trello's UX
return t.popup({
title: 'Settings',
url: './settings.html',
height: 184 // we can always resize later, but if we know the size in advance, its good to tell Trello
});
},
/*
🔑 Authorization Capabiltiies 🗝
The following two capabilities should be used together to determine:
1. whether a user is appropriately authorized
2. what to do when a user isn't completely authorized
*/
'authorization-status': function(t, options){
// Return a promise that resolves to an object with a boolean property 'authorized' of true or false
// The boolean value determines whether your Power-Up considers the user to be authorized or not.
// When the value is false, Trello will show the user an "Authorize Account" options when
// they click on the Power-Up's gear icon in the settings. The 'show-authorization' capability
// below determines what should happen when the user clicks "Authorize Account"
// For instance, if your Power-Up requires a token to be set for the member you could do the following:
return t.get('member', 'private', 'token')
.then(function(token){
if(token){
return { authorized: true };
}
return { authorized: false };
});
// You can also return the object synchronously if you know the answer synchronously.
},
'show-authorization': function(t, options){
// Returns what to do when a user clicks the 'Authorize Account' link from the Power-Up gear icon
// which shows when 'authorization-status' returns { authorized: false }.
// If we want to ask the user to authorize our Power-Up to make full use of the Trello API
// you'll need to add your API from trello.com/app-key below:
let trelloAPIKey = '';
// This key will be used to generate a token that you can pass along with the API key to Trello's
// RESTful API. Using the key/token pair, you can make requests on behalf of the authorized user.
// In this case we'll open a popup to kick off the authorization flow.
if (trelloAPIKey) {
return t.popup({
title: 'My Auth Popup',
args: { apiKey: trelloAPIKey }, // Pass in API key to the iframe
url: './authorize.html', // Check out public/authorize.html to see how to ask a user to auth
height: 140,
});
} else {
console.log("🙈 Looks like you need to add your API key to the project!");
}
}
});
console.log('Loaded by: ' + document.referrer);
================================================
FILE: js/modal.js
================================================
/* global TrelloPowerUp */
var t = TrelloPowerUp.iframe();
// you can access arguments passed to your iframe like so
var num = t.arg('rand');
t.render(function(){
// this function we be called once on initial load
// and then called each time something changes that
// you might want to react to, such as new data being
// stored with t.set()
});
// Important! If you are using the overlay, you should implement
// the following two methods to ensure that closing the overlay
// is simple and consistent for the Trello user
// close overlay if user clicks outside our content
document.addEventListener('click', function(e) {
if(e.target.tagName == 'BODY') {
t.closeOverlay().done();
}
});
// close overlay if user presses escape key
document.addEventListener('keyup', function(e) {
if(e.keyCode == 27) {
t.closeOverlay().done();
}
});
================================================
FILE: js/section.js
================================================
/* global TrelloPowerUp */
var t = TrelloPowerUp.iframe();
// you can access arguments passed to your iframe like so
var arg = t.arg('arg');
t.render(function(){
// make sure your rendering logic lives here, since we will
// recall this method as the user adds and removes attachments
// from your section
t.card('attachments')
.get('attachments')
.filter(function(attachment){
return attachment.url.indexOf('http://www.nps.gov/yell/') == 0;
})
.then(function(yellowstoneAttachments){
var urls = yellowstoneAttachments.map(function(a){ return a.url; });
document.getElementById('urls').textContent = urls.join(', ');
})
.then(function(){
return t.sizeTo('#content');
});
});
================================================
FILE: js/settings.js
================================================
/* global TrelloPowerUp */
var Promise = TrelloPowerUp.Promise;
var t = TrelloPowerUp.iframe();
var fruitSelector = document.getElementById('fruit');
var vegetableSelector = document.getElementById('vegetable');
t.render(function(){
return Promise.all([
t.get('board', 'shared', 'fruit'),
t.get('board', 'private', 'vegetable')
])
.spread(function(savedFruit, savedVegetable){
if(savedFruit && /[a-z]+/.test(savedFruit)){
fruitSelector.value = savedFruit;
}
if(savedVegetable && /[a-z]+/.test(savedVegetable)){
vegetableSelector.value = savedVegetable;
}
})
.then(function(){
t.sizeTo('#content')
.done();
})
});
document.getElementById('save').addEventListener('click', function(){
return t.set('board', 'private', 'vegetable', vegetableSelector.value)
.then(function(){
return t.set('board', 'shared', 'fruit', fruitSelector.value);
})
.then(function(){
t.closePopup();
})
})
================================================
FILE: modal.html
================================================
Tips for using t.modal()
You should only call t.modal() as a response to a user action, such as clicking a button.
Try to use t.popup() when possible instead of t.modal() as it helps to better maintain context.
Try to get your modal to load and render as quickly as possible.
Make sure your section feels at home on the card. It should fit in with the rest of the card and not stick out.
You can specify a height when you claim the section, and also use t.sizeTo() to make sure your section is the perfect height.
Try to keep the height of your sections to a minimum.
It should be obvious to the user what attachments went into your section. They shouldn't be left wondering were one of their attachments disappeared to.