Table of contents:
-
0. What are bookmarklets?
1. Adding bookmarklets
2. Actual bookmarklets
2-1. Operations to one task
2-1-1. Clean up story
2-1-2. Expand comments
2-1-3. ↔ Hide connected work links
2-1-4. ⫚ Hide completed subtasks
2-1-5. View task/messages in full width
2-2. Operations to a list of tasks (e.g. in a project)
2-2-1. ︎ Toggle sections
2-2-2. ▷ Toggle subtasks
2-2-3. Complete all tasks
2-2-4. Mark all tasks incomplete
2-2-5. Just My Tasks
2-2-6. Expand calendar tasks
2-3. Other operations
2-3-1. ☰ Resize sidebar
2-3-2. Copy title and URL
3. Challanges
3-1. Placeholder tasks
3-2. Mitigating loads
4. Comparison with other methods
4-1. Comparison with other JavaScript methods
4-2. Comparing JavaScript and CSS
5. Considerations
5-1. Reverse engineering
5-2. Possible design changes
5-3. Disclaimer
6. Special thanks
Complete/uncomplete all tasks in a project, expand/collapse all sections in a project, expand all comments in a task – all of these bulk actions can be done with just one click.
0. What are bookmarklets?
Bookmarklets are JavaScript code snippets that are added as browser bookmarks to be executed by clicking them. We can bookmark JavaScript snippets by enclosing them with javascript:(function() {
and })();
.
The frontend of any website including Asana is mainly made up of three languages: HTML (a markup language to build parts), CSS (a language to specify the layout and design), and JavaScript (a programming language).
Tag pairs in HTML build structured units (nodes), which can be operated by JavaScript. This interaction is called DOM (document object model).
1. Adding bookmarklets
Visit Asana bookmarklets and drag the blue links to the browser bookmark bar to add them.
I’d recommend creating a folder so that the bookmarklets don’t take up much width in the bookmark bar. Or, if you are adding them directly on the bookmark bar, I’d recommend editing the name to keep only the emoji.
If you manually add the bookmarklets, follow the below steps:
- Add an arbitrary webpage as a bookmark.
- When adding, Click “More…”, select the folder, and edit the name and URL.
- Add the bookmarklet titles to “Name” and the code starting with
javascript:
to “URL”.
If you want to create a section in the list of bookmarklets, I’d recommend using Chrome Bookmarks Separator.
You can run the automatic actions by clicking the registered bookmarklets on an Asana tab.
2. Actual bookmarklets
The source code and explanation of the bookmarklets listed in Asana bookmarklets.
2-1. Operations to one task
2-1-1. Clean up story
Do both of “Expand comments” and “Hide connected work links” below.
2-1-2. Expand comments
Click all of “7 more comments” and “See More” in task stories.
javascript: (function() {
const expandLink = document.querySelector('.TaskStoryFeed-expandLink');
if (expandLink && expandLink.textContent.match(/\d/)) expandLink.click();
document.querySelectorAll('.TruncatedRichText-expand').forEach(link => link.click());
document.querySelectorAll('.TaskStoryFeed-expandMiniStoriesLink').forEach(link => link.click());
})();
2-1-3. ↔ Hide connected work links
2-1-4. ⫚ Hide completed subtasks
Hide all completed subtasks in the right task details pane. Click the bookmarklet again to redisplay them.
If you have many subtasks, I’d also recommend using Asana Load More Chrome extension.
javascript: (function() {
const completedSubtaskRows = document.querySelectorAll('.SubtaskTaskRow--completed');
completedSubtaskRows.forEach(row => {
row.parentNode.parentNode.style.display = row.parentNode.parentNode.style.display? '': 'none';
});
})();
2-1-5. View task/messages in full width
I created this bookmarklet because
there weren’t “Wide view” and “Narrow view” when we opened a task in fullscreen mode (Tab+X)
and the task width on the right pane wasn’t adjustable by dragging.
Now that the view options are available, the boorkmarklet does:
- Display “Narrow view” in full width too.
- Display full-screen messages in full-width.
- Revert the display width to the original by clicking the bookmarklet again.
javascript: (function() {
const focusModePane = document.querySelector('.Pane.FocusModePage-taskPane');
if (focusModePane) {
if (focusModePane.style.flexBasis == 'auto') {
focusModePane.style.flexBasis = '';
focusModePane.style.width = '';
} else {
focusModePane.style.flexBasis = 'auto';
focusModePane.style.width = '95%';
}
}
const singleConversationPageCards = document.querySelectorAll('.SingleConversationPage-card');
const projectConversationsConversationCards = document.querySelectorAll('.ProjectConversations-conversationCard');
const conversationCards = [...singleConversationPageCards, ...projectConversationsConversationCards];
if (!conversationCards.length) return;
if (conversationCards[0].style.width == '95%') {
conversationCards.forEach(card => card.style.width = '585px');
} else {
conversationCards.forEach(card => card.style.width = '95%');
}
})();
2-2. Operations to a list of tasks (e.g. in a project)
Note: If there are many tasks and some are not displayed in the window, scroll Asana tab to the bottom to load all tasks before running these bookmarklets.
2-2-1. ︎ Toggle sections
Toggle expansion/collapse of sections in the list view.
javascript: (function() {
const firstButtonIcon = document.querySelector('.TaskGroupHeader-toggleButton .Icon');
if (!firstButtonIcon) return;
const firstTriangleClassName = firstButtonIcon.classList.contains('DownTriangleIcon')? 'DownTriangleIcon': 'RightTriangleIcon';
document.querySelectorAll(`.TaskGroupHeader-toggleButton .${firstTriangleClassName}`).forEach(buttonIcon => buttonIcon.parentNode.click());
})();
2-2-2. ▷ Toggle subtasks
Toggle expansion/collapse of subtasks, by clicking the small symbols indicating the existence of subtasks.
- List views: The list needs to be sorted by none. The task details pane on the right needs to be closed. If there are many tasks with subtasks, it takes some time to reload tasks and click each “Load more subtasks” link.
- Timeline views: This works only partially. Only visible subtasks are toggled.
javascript:(function() {
const firstTimelineSubtaskToggl = document.querySelector('.TaskTimelineTasks-taskAndSubtasksToggleContainer .MiniIcon');
if (firstTimelineSubtaskToggl) {
const firstCaretClassName = firstTimelineSubtaskToggl.classList.contains('DownCaretMiniIcon')? 'DownCaretMiniIcon': 'RightCaretMiniIcon';
document.querySelectorAll(`.MiniIcon.${firstCaretClassName}`).forEach(caretIcon => caretIcon.parentNode.click());
}
const triangleDivClassName = '.ProjectSpreadsheetGridRow-subtaskToggleButton';
const firstListSubtaskButton = document.querySelector(triangleDivClassName);
if (!firstListSubtaskButton) return;
const firstTriangleClassName = firstListSubtaskButton.firstElementChild.classList.contains('DownTriangleIcon')? 'DownTriangleIcon': 'RightTriangleIcon';
const taskPlaceholderHTMLCollection = document.getElementsByClassName('SpreadsheetTaskRowScrollPlaceholder');
if (taskPlaceholderHTMLCollection.length == 0) {
const arrayToggleButtons = Array.from(document.querySelectorAll(triangleDivClassName));
for (var i = 0; i < arrayToggleButtons.length; i++) {
const arrayToggleButton = arrayToggleButtons[i];
if (arrayToggleButton.firstElementChild.classList.contains(firstTriangleClassName)) {
setTimeout(() => {
arrayToggleButton.click();
}, 50 * i);
}
}
return;
}
const taskGroup = document.querySelector('.TaskGroup');
const buttonAtTheBottom = document.querySelector('.SpreadsheetPotGridContents-addSectionButton');
setTimeout(function () {
if (buttonAtTheBottom) buttonAtTheBottom.scrollIntoView();
}, 30);
setTimeout(function () {
taskGroup.style.display = 'none';
}, 60);
let monitorTaskStructure = setInterval(() => {
if (taskPlaceholderHTMLCollection.length == 0) {
const arrayToggleButtons = Array.from(document.querySelectorAll(triangleDivClassName));
for (var i = 0; i < arrayToggleButtons.length; i++) {
const arrayToggleButton = arrayToggleButtons[i];
if (arrayToggleButton.firstElementChild.classList.contains(firstTriangleClassName)) {
setTimeout(() => {
arrayToggleButton.click();
}, 5 * i);
}
}
taskGroup.style.display = '';
clearInterval(monitorTaskStructure);
const loadMoreLinkHTMLCollection = document.getElementsByClassName('SpreadsheetTaskList-showMoreLink');
setTimeout(() => {
let clickingLoadMoreLinks = setInterval(function() {
if (!loadMoreLinkHTMLCollection.length) {
clearInterval(clickingLoadMoreLinks);
} else {
loadMoreLinkHTMLCollection[0].scrollIntoView();
loadMoreLinkHTMLCollection[0].click();
}
}, 500);
}, 200);
}
}, 100);
})();
2-2-3. Complete all tasks
Mark all visible incomplete tasks in the list view as complete. If there are many tasks, the bookmarklet temporarily hides all tasks and clicks 50 tasks at a time.
javascript: (function() {
const taskPlaceholderHTMLCollection = document.getElementsByClassName('SpreadsheetTaskRowScrollPlaceholder');
if (!taskPlaceholderHTMLCollection.length) {
document.querySelectorAll('.SpreadsheetGridTaskNameAndDetailsCellGroup-completionStatus .TaskRowCompletionStatus-taskCompletionIcon--incomplete').forEach(incompleteIcon => incompleteIcon.parentNode.click());
} else {
const taskGroup = document.querySelector('.TaskGroup');
const buttonAtTheBottom = document.querySelector('.SpreadsheetPotGridContents-addSectionButton');
setTimeout(function () {if (buttonAtTheBottom) buttonAtTheBottom.scrollIntoView();}, 30);
setTimeout(function () {
taskGroup.style.display = 'none';
const progressIndicator = document.createElement('span');
progressIndicator.setAttribute('id', 'progressIndicator');
progressIndicator.textContent = 'Processing';
taskGroup.parentNode.appendChild(progressIndicator);
}, 60);
let monitorTaskStructure = setInterval(() => {
if (taskPlaceholderHTMLCollection.length == 0) {
clearInterval(monitorTaskStructure);
const progressIndicator = document.querySelector('#progressIndicator');
const allTasks = Array.from(document.querySelectorAll('.SpreadsheetGridTaskNameAndDetailsCellGroup-completionStatus .TaskRowCompletionStatus-taskCompletionIcon--incomplete'));
const numProcesses = Math.floor(allTasks.length / 50) + 1;
let counter = 0;
let loopTasks = setInterval(() => {
progressIndicator.textContent = `Processing (${counter}/${numProcesses}) `;
for (let i = 50 * counter; i < Math.min(allTasks.length, 50 * (counter + 1)); i++) {
allTasks[i].parentNode.click();
if (i == allTasks.length - 1) {
clearInterval(loopTasks);
progressIndicator.remove();
taskGroup.style.display = '';
}
}
counter += 1;
}, 500);
}
}, 100);
}
})();
2-2-4. Mark all tasks incomplete
Mark all visible complete tasks in the list view as incomplete again. If there are many tasks, the bookmarklet temporarily hides all tasks and clicks 50 tasks at a time.
javascript: (function() {
const taskPlaceholderHTMLCollection = document.getElementsByClassName('SpreadsheetTaskRowScrollPlaceholder');
if (!taskPlaceholderHTMLCollection.length) {
document.querySelectorAll('.SpreadsheetGridTaskNameAndDetailsCellGroup-completionStatus .TaskRowCompletionStatus-taskCompletionIcon--complete').forEach(incompleteIcon => incompleteIcon.parentNode.click());
} else {
const taskGroup = document.querySelector('.TaskGroup');
const buttonAtTheBottom = document.querySelector('.SpreadsheetPotGridContents-addSectionButton');
setTimeout(function () {if (buttonAtTheBottom) buttonAtTheBottom.scrollIntoView();}, 30);
setTimeout(function () {
taskGroup.style.display = 'none';
const progressIndicator = document.createElement('span');
progressIndicator.setAttribute('id', 'progressIndicator');
progressIndicator.textContent = 'Processing';
taskGroup.parentNode.appendChild(progressIndicator);
}, 60);
let monitorTaskStructure = setInterval(() => {
if (taskPlaceholderHTMLCollection.length == 0) {
clearInterval(monitorTaskStructure);
const progressIndicator = document.querySelector('#progressIndicator');
const allTasks = Array.from(document.querySelectorAll('.SpreadsheetGridTaskNameAndDetailsCellGroup-completionStatus .TaskRowCompletionStatus-taskCompletionIcon--complete'));
const numProcesses = Math.floor(allTasks.length / 50) + 1;
let counter = 0;
let loopTasks = setInterval(() => {
progressIndicator.textContent = `Processing (${counter}/${numProcesses}) `;
for (let i = 50 * counter; i < Math.min(allTasks.length, 50 * (counter + 1)); i++) {
allTasks[i].parentNode.click();
if (i == allTasks.length - 1) {
clearInterval(loopTasks);
progressIndicator.remove();
taskGroup.style.display = '';
}
}
counter += 1;
}, 500);
}
}, 100);
}
})();
2-2-5. Just My Tasks
2-2-6. Expand calendar tasks
Open all “x more” links in calendar month view.
It only expands days loaded in the screen, so the collapsed days still remains when you scroll up and down.
javascript: (function() {
document.querySelectorAll('.CalendarDay-xMoreText').forEach(buttonDiv => buttonDiv.click());
})();
2-3. Other operations
2-3-1. ☰ Resize sidebar
2-3-2. Copy title and URL
Don’t you take four steps when copying & pasting links into Asana?
- Copy title
- Paste title
- Copy URL
- Paste URL
Let’s save time by just two steps!
- Click the bookmarklet
- Paste the link
It can be used in any rich text environment, not only Asana.
When you move the cursor away quickly after clicking the bookmarklet, you might only get the URL. The tip is to pause the cursor a bit longer until the copy is finished.
javascript: (function() {
const url = window.location.href;
const title = document.title;
const link = `<a href="${url}">${title}</a>`;
const blobHtml = new Blob([link], {type: 'text/html'});
const blobText = new Blob([url], {type: 'text/plain'});
const data = [new ClipboardItem({
['text/plain']: blobText,
['text/html']: blobHtml
})];
navigator.clipboard.write(data);
})();
3. Challanges
3-1. Placeholder tasks
When a project has many tasks, Asana converts some of far-from-view-area tasks into simplified placeholders with only task names in order to save resources (e.g. memory and energy consumption). When we scroll near the placeholder tasks, the data is loaded again.
Example: When we scroll from the bottom of the project, the tasks near the top are displayed name-only for a short period of time. Less than a second later, the tasks are fully displayed.
If we target all tasks as in section 2-2, we temporarily hide all tasks to make Asana believe that there is enough space to display all tasks. After Asana rerenders all placeholder tasks as full tasks, the automation runs, and we undo the hiding after that.
3-2. Mitigating loads
If we do too much at one time, we burden the Asana server. When using Asana API, or when we make bulk actions on multi-selected tasks, there are limits of 50 tasks or so. So, I decided to process 50 tasks at a time in the “Complete/uncomplete all tasks” bookmarklets listed above.
4. Comparison with other methods
4-1. Comparison with other JavaScript methods
Let’s compare different methods to automate things with Asana. The methods on the left are more complicated and powerful, while the ones on the right are simpler. Bookmarklets is a pretty easy way because we need to write only 1/100 or 1/10 of code lines compared with developing a Chrome extension. However, we still need to write some length of code if we consider the challenges discussed in section 3.
Methods: | OAuth | Personal Access Token (PAT) | Chrome extensions | Bookmarklets | Browser address bar |
---|---|---|---|---|---|
Examples: | Log into Asana Academy with Asana account | Asana’s official extension | Listed on this topic | Get the list of workspaces | |
Authentication: | Set up a server | Hard-code PAT or save it to environment variables | Use browser cookie showing user login | Cookie (ditto) | Cookie (ditto) |
Access: | Grant access | Use PAT | Install from Chrome Web Store | Add to bookmark | Enter URL into address bar |
Can do: | Operations through API | Operations through API | Operations through API+DOM | Operations through DOM | GET operations through API (getting information) |
Is good at: | Large scale tools | Writing scripts for personal use | Distribution | Easy creating and distribution | Easiest use |
Cannot do: | Operations through DOM | Operations through DOM | (Some things, of course) | Operating invisible tasks | POST/PUT/DELETE operations (editing information) |
How to manage large data: | Pagination | Pagination | Pagination | Placeholder tasks | Pagination (cannot be automated) |
Example: Complete all tasks in a project
A bookmarklet is a simpler and faster approach.
- With API, we first send a GET request to the endpoint
/projects/<project_gid>/tasks
to fetch tasks in the project. Then we loop through the tasks and send PUT requests to complete each task. We need to make two rounds of API calls for getting and updating information. - With bookmarklets, we directly loop through the displayed tasks and click them.
4-2. Comparing JavaScript and CSS
We can use CSS to customize Asana’s design. The major differences of CSS are that:
- No need to authenticate because it doesn’t interact with API
- Can “always” change the design of HTML elements
- Cannot react dynamically when certain changes happen or the user does a specific operation
How to use CSS customization: Install Chrome extensions like Stylish or User CSS. You can either use custom CSS settings created by other users from libraries like userstyles.org or create your own CSS rules.
Example: Change sidebar width
- The bookmarklets 2-3-1 mentioned above expands/shrinks the sidebar when the button is clicked
- If you want the sidebar width to be “always” changed, then it’s more useful to customize CSS
5. Considerations
5-1. Reverse engineering
Asana’s API Terms prohibits reverse engineering. However, identifying DOM elements with right-clicking and selecting “Inspect” is generally possible, and I don’t believe it accesses Asana’s secrets in any way.
5-2. Possible design changes
Asana can at any time change IDs and class names which we use to identify HTML elements in the Asana tool. Some bookmarklets above stop working in such cases. I’ll update the code if I notice the changes, but I’d appreciate it if you’d let me know in a reply to this topic.
5-3. Disclaimer
I developed and tested the bookmarklets with care, but I can’t be responsible for any issues resulting from them.
6. Special thanks
- I initially got the idea of utilizing bookmarklets by reading "COLLAPSE (or re-expand) ALL SECTIONS" button needed - #4 by Tony_Tsui and thought it’s an easy and good tool. "COLLAPSE (or re-expand) ALL SECTIONS" button needed - #47 by Skyler has bookmarklets curated by another user.
- Forum Leader @Bastien_Siebman and Ambassador @Itay_Kurgan kindly helped me test some of the code in real environments and gave me valuable insights, especially the ones mentioned in section 3. Challenges.
- Thank you for reading this topic. Give me a comment if you want some other features. I’d also encourage writing your own bookmarklets if you know JavaScript