Introduction to Asana bookmarklets

Table of contents:

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.

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

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. :desktop_computer: 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)

: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, by clicking the small :arrow_forward: 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. :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('.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. :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('.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. :bust_in_silhouette: Just My Tasks

2-2-6. :spiral_calendar: 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. :link: Copy title and URL

Don’t you take four steps when copying & pasting links into Asana?

  1. Copy title
  2. Paste title
  3. Copy URL
  4. Paste URL

Let’s save time by just two steps!

  1. Click the bookmarklet
  2. 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

19 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

2 Likes

@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

Update:

It’s great to see new bookmarklets created by other users and helping teams :slight_smile:

2 Likes

I just stumbled upon this… this is gold :coin: great stuff @ShunS :slight_smile:

2 Likes

Update:

3 Likes

A bookmarklet for showing “Just my tasks” would be great to add. I made one a few months back, but recent changes to the UI broke it. If I get it working again, can we include it in your collection?

Here was the previous version’s code (it looks like the easily identifiable classnames are now gone… so this might be more brittle going forward):

javascript: (() => {
    const filterMenu = document.querySelector('.FilterMenu');
    filterMenu.click();
    setTimeout(() => {
        const filterMenuContents = document.querySelector('.FilterMenuContents');
        if (filterMenuContents) {
            const removeButton = filterMenuContents.querySelector('.RemoveButton');
            removeButton.click();
        } else {
            const justMyTasks = document.getElementById('view_options_filter_Just my tasks');
            if (justMyTasks) {
                justMyTasks.click();
            }
        }
    }, 250);
})()

Here is a fixed version (works on the project view, only if there isn’t a task open)

javascript: (() => {
    const filterMenu = [...document.querySelectorAll('.PageToolbarStructure-rightChildren div')]
        .find((node) => node.innerText.startsWith('Filter'));
    filterMenu.click();
    setTimeout(() => {
        const filterMenuContents = document.querySelector('.FilterMenuContents');
        if (filterMenuContents) {
            const removeButton = filterMenuContents.querySelector('.RemoveButton');
            removeButton.click();
        } else {
            const justMyTasks = document.getElementById('view_options_filter_Just my tasks');
            if (justMyTasks) {
                justMyTasks.click();
            }
        }
    }, 250);
})()

1 Like

Hi @John_Pope , thank you for the offer :slight_smile:
I confirm it’s working:

Clicking the boommark folder > bookmarklet is almost the same effort as using the official feature, but I see the benefit in being able to undo the filter by the click of the bookmarklet.

I’ll add yours as “2-2-5. :bust_in_silhouette: Just My Tasks:raised_hands:
Please forgive me for making a little changes to fit my style.

1 Like

Thanks! I usually just have this one outside of any bookmark folders since I use it everyday. I appreciate you including this in your collection as it has been a great resource for Asana bookmarklets.

Update:

3 Likes

Very nice, @ShunS; thanks!!!

Larry

1 Like