Introduction to Asana bookmarklets

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.

Screen Recording 2022-07-25 at 16.21.00 (1)

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:

  1. Add an arbitrary webpage as a bookmark.
  2. When adding, Click “More…”, select the folder, and edit the name and URL.
  3. 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. :speech_balloon: Clean up story

Do both of “Expand comments” and “Hide connected work links” below.

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());
	document.querySelectorAll('.BacklinkMiniStory').forEach(line => {line.parentNode.style.display = 'none';});
})();

2-1-2. :arrow_up_down: 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

Hide all “<user_name> mentioned this task in another task: <task_link>” in task stories. This is useful when a task is referenced by too many tasks, which makes other comments and attachments hard to find.

javascript:(function() {
	document.querySelectorAll('.BacklinkMiniStory').forEach(line => {line.parentNode.style.display = 'none';});
})();

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-2. Operations to a list of tasks (e.g. in a project)

:exclamation: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. :arrow_forward:︎ 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 in the list view, by clicking the small :arrow_forward: symbols indicating the existence of subtasks. If there are many subtasks, the bookmarklet clicks each “Load more subtasks” link and it takes some time.

javascript:(function() {
	const firstSubtaskButton = document.querySelector('.ProjectSpreadsheetGridRow-subtaskToggleButton');
	const firstTriangleClassName = firstSubtaskButton.firstElementChild.classList.contains('DownTriangleIcon')? 'DownTriangleIcon': 'RightTriangleIcon';

	const taskPlaceholderHTMLCollection = document.getElementsByClassName('SpreadsheetTaskRowScrollPlaceholder');
	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) {
			document.querySelectorAll('.ProjectSpreadsheetGridRow-subtaskToggleButton').forEach(function (buttonIcon) {
				if (buttonIcon.firstElementChild.classList.contains(firstTriangleClassName)) buttonIcon.click();
			});
			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();
					}
				}, 100);
			}, 200);
		}
	}, 100);
})();

2-2-3. :white_check_mark: 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('.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('.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. :ballot_box_with_check: 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('.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('.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);
	}
})();

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

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.

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

This topic was originally posted in Japanese: Asanaブックマークレットのご紹介

9 Likes

This is super cool.
Going to look at this as part of managed bookmarks for our PMO team.

1 Like

Really great implementation and documentation, @ShunS! Bravo!!

Recommended!

Working well for me.

Thanks,

Larry

1 Like

@ShunS ,

Great job!

It is a pleasure working with you, and I am grateful for the solution you provided me.
I hope this helps others and that Asana eventually uses this as inspiration to implement natively in the system for everyone to use.

Thank you :orange_heart:

2 Likes

Super tips, @ShunS! :star_struck: Thanks for putting this together!

1 Like

Yes, bravo, @ShunS , really nice!!

1 Like

Amazing tips! Thank you so much for sharing, @ShunS! :clap:

1 Like

these are so helpful and working great for me! thank you!

1 Like

Super cool. A lecture on Asana Customization

1 Like

Thank you for the feedback :slight_smile:

Update:

Please keep requests and ideas coming :bulb:

1 Like

This is amazing!
I only wish there was some way to use these in the desktop app… :frowning:

1 Like