Asanaブックマークレットのご紹介

プロジェクトのすべてのタスクを完了・未完了にする、プロジェクトのセクションをすべて展開する・折りたたむ、タスクのコメントをすべて展開する、といった一括操作をワンクリックで行う方法をご紹介します。

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

0. ブックマークレットとは

ブックマークレットとは、JavaScriptのコードをブラウザーのブックマークに登録して、クリックにより実行できるようにしたものです。JavaScriptコードを「javascript:(function() {」と「})();」で挟むことでブックマークが可能になります。
Asanaを含むWebサイトのフロントエンドは、HTML(部品を表すマークアップ言語)、CSS(レイアウトやデザインを指定する言語)、JavaScript(プログラミング言語)という3つの言語で主に構成されています。
HTMLはタグで囲まれた部品(ノード)が階層構造になっており、JavaScriptから操作することができます。これはDOM(ドキュメントオブジェクトモデル)と呼ばれる仕組みです。

1. ブックマークレットの追加方法

Asana bookmarklets にアクセスし、青字のリンクをブックマークバーにドラッグ&ドロップして追加してください。
横幅を取らないように、スクリーンショットのようにブックマークレット用のフォルダーを作成してそこに追加していくのがおすすめです。ブックマークバーに直接追加する場合は、名前を編集して絵文字だけにするなど、短くするのがおすすめです。

手作業で登録する場合は、以下のことを行います。

  1. 任意のページをブックマークに登録します。
  2. 登録時に「その他…」をクリックし、保存場所を選択し、名前とURLを編集します。
  3. この投稿の下にあるブックマークのタイトルを「名前」に、javascript:で始まるコードを「URL」に、それぞれ入力します。

ブックマークレットのリストをセクション分けするには、ブックマークのセパレーターツールがおすすめです。Drag meの部分をブックマークにドラッグするだけでセパレーター(横線)を挿入できます。

Asanaのタブを開いた状態で、登録したブックマークレットをクリックすると、自動操作を行えます。

2. 実際のブックマークレット

**Asana bookmarklets**に記載されているブックマークレットのソースコードと説明です。

2-1. 一つのタスクに対する操作

2-1-1. :speech_balloon: ストーリーのスッキリ表示

以下の「コメントの展開」と「作業間リンクの非表示」を両方行います。

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: コメントの展開

タスクストーリーの「その他 7 件のコメント」や「もっと見る」をすべてクリックします。

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. ↔ 作業間リンクの非表示

タスクストーリーの「 <ユーザー名> が次のタスク内でこのタスクをメンションしました: <タスクのリンク>」を非表示にします。参照タスクなど、多数のタスクからリンクされているためコメントや添付ファイルが見づらい場合に便利です。

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

2-1-4. ⫚ 完了したサブタスクの非表示

右側のタスク詳細ウィンドウで、完了したサブタスクをすべて非表示にします。ブックマークレットをもう一度クリックすると、サブタスクを再表示できます。
サブタスク数が多い場合は、Asana Load MoreというChrome拡張機能をインストールするのもおすすめです。

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: タスクの全幅表示

タスクを全画面 (Tab+X) 表示にしたとき、横幅は最大 764px に制限されています。このブックマークレットを使用すると、画面幅を最大に使って本当にフルスクリーン表示することができます。
また、タスク一覧の右側にタスクの詳細が表示される場合、画面幅の75%まで拡張します。

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 = '100%';
		}
	}
	const detailsOverlay = document.querySelector('.FullWidthPageStructureWithDetailsOverlay-detailsOverlay');
	if (detailsOverlay) {
		if (!detailsOverlay.style.maxWidth) {
			detailsOverlay.style.maxWidth = '75%';
			detailsOverlay.style.width = '75%';
		} else {
			detailsOverlay.style.maxWidth = '700px';
			detailsOverlay.style.width = '55%';
		}
	}
})();

2-2. プロジェクトなどタスクリストに対する操作

:exclamation:注意: タスクが多数あり一画面に表示しきれていない場合は、以下のブックマークレットを実行する前に、Asana画面を一番下までスクロールし、すべてのタスクを読み込んでください。

2-2-1. :arrow_forward:︎ セクションの表示切り替え

リストビューでセクションの展開・折りたたみを切り替えます。

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. ▷ サブタスクの表示切り替え

サブタスクの小さい▶︎をクリックし、サブタスクの展開・折りたたみを切り替えます。

  • リストビュー: まず「ソート: なし」に設定します。サブタスクのあるタスクが多数ある場合にはタスクをリロードし、「他のサブタスクを読み込む」を順にクリックするため時間がかかります。
  • ボードビュー: ボードビューに表示されているサブタスクのみ表示を切り替えられます。画面外のタスクには影響しません。
javascript:(function() {
	const boardSubtaskToggleButtons = document.querySelectorAll('.SubtaskCountToggleButton');
	if (boardSubtaskToggleButtons.length) {
		boardSubtaskToggleButtons.forEach(button => button.click());
		return;
	}

	const firstListSubtaskButton = document.querySelector('.ProjectSpreadsheetGridRow-subtaskToggleButton');
	if (!firstListSubtaskButton) return;
	const firstTriangleClassName = firstListSubtaskButton.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: すべてのタスクの完了

リストビューに表示されているすべての未完了タスクを完了にします。多数ある場合は一度タスクの表示を消したあと、50個ずつクリックしていきます。

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 = '処理しています';
			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 = `処理しています (${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: すべてのタスクの未完了

リストビューに表示されているすべての完了タスクを未完了に戻します。多数ある場合は一度タスクの表示を消したあと、50個ずつクリックしていきます。

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 = '処理しています';
			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 = `処理しています (${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: マイタスクのみ

プロジェクトなどで「マイタスクのみ」フィルターを適用します。

javascript:(function() {
    const filterMenu = [...document.querySelectorAll('.PageToolbarStructure-rightChildren div')]
        .find((node) => node.innerText.startsWith('Filter'));
    if (!filterMenu) return;
    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);
})();

2-3. その他の操作

2-3-1. ☰ サイドバーのサイズ変更

サイドバーの幅を倍の480pxに変更して、プロジェクト等の名前が長い場合に読みやすくします。ブックマークレットをもう一度クリックすると、デフォルトの幅に戻ります。
この投稿を見たユーザーが共有してくださったブックマークレット SideBar Adjust Size / Width - #14 by Ian_Houser をベースにしています!

javascript:(function() {
	const widerWidth = '480px';
	const asanaSidebar = document.querySelector('.AsanaMain-sidebar');
	if (!asanaSidebar) return; 

	var newStyle = document.querySelector('#asanaSidebarBookmarkletStyle');
	if (!newStyle) {	
		newStyle = document.createElement('style');
		newStyle.id = 'asanaSidebarBookmarkletStyle';
		document.head.appendChild(newStyle);
	}

	if (asanaSidebar.style.width == widerWidth) {
		asanaSidebar.style.width = '240px';
		newStyle.innerText = '';
	} else {
		asanaSidebar.style.width = widerWidth;
		newStyle.innerText = `.AsanaMain-sidebar.AsanaMain-sidebar--isCollapsed {margin-left: -${widerWidth} !important}`;
	}
})();

3. 工夫が必要だったところ

3-1. プレースホルダータスク

プロジェクトに多数のタスクがあるとき、Asanaは表示範囲から遠く離れたタスクを「タスク名だけ」のプレースホルダーの状態に簡略化してリソース(メモリやエネルギー使用量)を節約しています。その近くにスクロールすると、再度データが読み込まれます。

例: プロジェクトの一番下からスクロールしていくと、上の方のタスクが一瞬「タスク名のみ」の状態で表示され、コンマ数秒後にフル表示されます。

セクション2-2のように「すべてのタスク」を対象にする場合は、一度すべてのタスクを非表示にして、Asanaに「すべてのタスクを表示するだけの十分なスペースがあるんだ」と信じ込ませます。Asanaがすべてのプレースホルダーを完全なタスクの形に書き直したとき、処理を実行し、非表示を解除します。

3-2. 負荷の軽減

一度に行う処理が多すぎると、Asanaのサーバーに負荷がかかりすぎてしまいます。APIを使用するときも、タスクを複数選択して一括操作するときも、おおよそ数十タスクごとに処理を行うので、上記「すべてのタスクの完了・未完了」では、50個ずつ処理するようにしました。

4. 他の方法との比較

4-1. JavaScriptを使用する方法との比較

Asanaで自動化を行う方法を比較してみました。左にあるほど複雑・強力な方法であり、右に行くほど簡単な方法です。ブックマークレットはChrome拡張機能を作る1/100〜1/10くらいのコード行数で済むので、かなり気軽な方法です。セクション3の問題を考慮すると少々長くなります。

方法: OAuth 個人アクセストークン(PAT) Chrome拡張 ブックマークレット アドレスバー
例: Asana AcademyにAsanaアカウントでログイン Asana公式拡張機能 このトピックに記載 ワークスペースの一覧を取得
認証方法: サーバーを立てる PATをハードコードまたは環境変数に保存する ログインしていることを示すブラウザーのCookie Cookie(同左) Cookie(同左)
利用方法: アクセスを認可 PATを利用 Chrome Web Storeからインストール ブックマークに追加 アドレスバーにURLを入力
できること: APIを通じた操作 APIを通じた操作 APIを通じた操作+DOMの操作 DOMの操作 APIを通じたGET操作(情報の取得)
得意なこと: 大規模なツールに最適 自分用にスクリプトを書くときに便利 配布が楽 作成・配布が楽 最も気軽
できないこと: DOMの操作 DOMの操作 (少しはあります) 表示されていないタスクの操作 POST/PUT/DELETE操作(情報の編集)
データが多い場合: ページネーション ページネーション ページネーション プレースホルダータスク ページネーション(自動化できない)

例: プロジェクトのタスクをすべて完了にする
この場合、ブックマークレットを使った方がシンプルな方法で高速に処理が行えます。

  • APIを利用する方法では、/projects/<project_gid>/tasks エンドポイントへのGETリクエストでタスクを取得し、forループで各タスクを完了するPUTリクエストを書きます。タスクの取得と更新で2回APIコールを行う必要があります。
  • ブックマークレットでは、画面に表示されているタスクを直接ループしてクリックします。

4-2. JavaScriptとCSSの比較

CSSを利用してデザインをカスタマイズすることもできます。JavaScriptとの大きな違いは以下のとおりです。

  • APIを使用しないので認証不要
  • HTML要素のデザインを常時変更しておける
  • 特定の変化が起きた場合、または特定の操作を行った場合に動的に対応することはできない
    利用方法: StylishUser CSSなどのChrome拡張機能をインストールします。userstyles.orgなどから別のユーザーが作成したCSSを利用することも、自分でCSSをカスタマイズすることもできます。

例: サイドバーの幅を変更する

  • 上記2-3-1のブックマークレットを使用した方法では、ボタンを押したときにサイドバーの拡大・縮小が行えます。
  • サイドバーの幅を常に変更しておきたい場合は、CSSをカスタマイズする方法が適しています。

5. 規約等の考慮

5-1. リバースエンジニアリング

AsanaのAPI規約でリバースエンジニアリングは禁止されています。ただし、右クリック > 「要素を検証」でDOM要素を特定する行為は一般に簡単に行えるものであり、Asanaの機密には何ら関わらない方法だと考えています。

5-2. デザインの変更可能性

AsanaツールのHTML要素を特定するためのIDやクラス名は、Asanaがいつでも自由に変更できます。その場合には上記のブックマークレットが機能しなくなります。気付き次第アップデートしますが、問題にお気づきの方がいらっしゃれば下の「返信」でお知らせください。

5-3. 免責

ブックマークレットの作成には細心の注意を払い、テストをおこなっていますが、それに基づくいかなる問題への責任も開発者は負えません。

6. 謝辞

  • ブックマークレットのアイデアは元々 "COLLAPSE (or re-expand) ALL SECTIONS" button needed - #4 by Tony_Tsui を見たときに便利で簡単に応用できる方法だと思いました。"COLLAPSE (or re-expand) ALL SECTIONS" button needed - #47 by Skyler には別のユーザーがまとめたブックマークレットもあります。
  • フォーラムリーダーのBastienさんとアンバサダーのItayさんに実際に試していただき、上記「工夫が必要だったところ」の課題に気づくことができました。
  • お読みいただきありがとうございます。他に欲しい機能などあれば、コメントをお願いいたします。JavaScriptを書ける方は、ぜひご自分でもブックマークレットを作ってみてください。
5 Likes

@ShunS さん、素晴らしい機能を構築いただきありがとうございます :clap:
非エンジニアの私でも、JavaScriptのコードをブックマークするだけという簡単な手順で実現できました。素晴らしいですね、感動しております :smiling_face_with_three_hearts:
コメントやサブタスクの一括展開機能や、作業間リンクが多い場合はストーリーのスッキリ表示を、私はとても活用できそうです :rocket:
この機能、いろんな方の要望を満たしそうですね。ぜひ使ってみた方はコメントいただけると嬉しいです。

2 Likes

お褒めいただきありがとうございます :slight_smile:

完了したサブタスクを、タスク内冒頭のリストでも非表示にしたい を読み、いくつかアップデートしました。

他にもご要望やアイデアがあれば、どしどしお寄せください :bulb:

3 Likes

アップデート:

他の人もブックマークレットを作っていて、さらに便利にAsanaを使えるようになるので嬉しいです✨

2 Likes

アップデート:

2 Likes

@ShunS さん、コードはさっぱり分からない私でも、リンクをブックマークバーにドラッグ&ドロップするだけで、簡単にできました :raised_hands:
:speech_balloon: ストーリーのスッキリ表示」と「 :desktop_computer: タスクの全幅表示」がワンクリックで出来て、さらに快適に作業できるようになりました :tada:

:musical_note: Vivaldi Browserでも使えると、うれしいです :musical_note:

1 Like

@Hiroko_Sakamoto さん、ブックマークレットお使いいただきありがとうございます🙌

Vivaldiで使う方法を試してみました。
まず、設定 > Address Bar > Security Featuresで、Strip JavaScript from Pasted Textのチェックを外す必要がありそうです。

  1. ブックマークレットをドラッグして追加すると、リンクが about:blank#blocked になります
  2. ブックマークレットを右クリックして Copy Link Address します
  3. about:blank#blocked になっているところにJavaScriptコードを貼り付けます
  4. ブックマークのアイコンが地球儀からコードファイルに変わり、登録できたことを確認できます

登録の手間が増えますが、これで、Asanaページを開いているとき、ブックマークレットをクリック(2回?)すると、無事に実行されます。

ChromeとVivaldiでCSSの扱いが違うようで、「2-1-5. :desktop_computer: タスクの全幅表示」などの挙動が違うようです。ただし、Asanaの対応外のブラウザーを完全にはサポートできませんので、ご容赦ください!

1 Like

アップデート:

1 Like

@ShunS さん、お忙しいところ Vivaldi Browser で使う方法まで、ありがとうございます :notes: ぜひ試してみます :exclamation:

2 Likes

アップデート:

@Tomohiro_Oka さん、本日のAUAでご提案いただきありがとうございました🙌

2 Likes