目次:
-
0. ブックマークレットとは
1. ブックマークレットの追加方法
2. 実際のブックマークレット
2-1. 一つのタスクに対する操作
2-1-1. ストーリーのスッキリ表示
2-1-2. コメントの展開
2-1-3. ↔ 作業間リンクの非表示
2-1-4. ⫚ 完了したサブタスクの非表示
2-1-5. タスクとメッセージの全幅表示
2-2. プロジェクトなどタスクリストに対する操作
2-2-1. ︎ セクションの表示切り替え
2-2-2. ▷ サブタスクの表示切り替え
2-2-3. すべてのタスクの完了
2-2-4. すべてのタスクの未完了
2-2-5. マイタスクのみ
2-2-6. カレンダータスクの一括展開
2-3. その他の操作
2-3-1. ☰ サイドバーのサイズ変更
2-3-2. タイトルとURLのコピー
3. 工夫が必要だったところ
3-1. プレースホルダータスク
3-2. 負荷の軽減
4. 他の方法との比較
4-1. JavaScriptを使用する方法との比較
4-2. JavaScriptとCSSの比較
5. 規約等の考慮
5-1. リバースエンジニアリング
5-2. デザインの変更可能性
5-3. 免責
6. 謝辞
プロジェクトのすべてのタスクを完了・未完了にする、プロジェクトのセクションをすべて展開する・折りたたむ、タスクのコメントをすべて展開する、といった一括操作をワンクリックで行う方法をご紹介します。
0. ブックマークレットとは
ブックマークレットとは、JavaScriptのコードをブラウザーのブックマークに登録して、クリックにより実行できるようにしたものです。JavaScriptコードを「javascript:(function() {
」と「})();
」で挟むことでブックマークが可能になります。
Asanaを含むWebサイトのフロントエンドは、HTML (部品を表すマークアップ言語)、CSS (レイアウトやデザインを指定する言語)、JavaScript (プログラミング言語) という3つの言語で主に構成されています。
HTMLはタグで囲まれた部品 (ノード) が階層構造になっており、JavaScriptから操作することができます。これはDOM (ドキュメントオブジェクトモデル) と呼ばれる仕組みです。
1. ブックマークレットの追加方法
Asana bookmarklets にアクセスし、青字のリンクをブックマークバーにドラッグ&ドロップして追加してください。
横幅を取らないように、スクリーンショットのようにブックマークレット用のフォルダーを作成してそこに追加していくのがおすすめです。ブックマークバーに直接追加する場合は、名前を編集して絵文字だけにするなど、短くするのがおすすめです。
手作業で登録する場合は、以下のことを行います。
- 任意のページをブックマークに登録します。
- 登録時に「その他…」をクリックし、保存場所を選択し、名前とURLを編集します。
- この投稿の下にあるブックマークのタイトルを「名前」に、
javascript:
で始まるコードを「URL」に、それぞれ入力します。
ブックマークレットのリストをセクション分けするには、ブックマークのセパレーターツールがおすすめです。Drag meの部分をブックマークにドラッグするだけでセパレーター (横線) を挿入できます。
Asanaのタブを開いた状態で、登録したブックマークレットをクリックすると、自動操作を行えます。
2. 実際のブックマークレット
Asana bookmarklets に記載されているブックマークレットのソースコードと説明です。
2-1. 一つのタスクに対する操作
2-1-1. ストーリーのスッキリ表示
以下の「コメントの展開」と「作業間リンクの非表示」を両方行います。
2-1-2. コメントの展開
タスクストーリーの「その他 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. ↔ 作業間リンクの非表示
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. タスクとメッセージの全幅表示
Asanaのアップデートのおかげで、この機能は半分不要になりました。
タスクを全画面 (Tab+X) 表示にしたとき、「広いビュー」「狭いビュー」の切り替えができるようになりました🙌
タスク一覧の右側にタスクの詳細が表示されるとき、タスクの幅をドラッグで変更できるようになりました🙌
このブックマークレットをクリックすると:
- 「狭いビュー」も全幅表示になります。
- メッセージを全画面表示するとき、画面幅いっぱいに表示します。
- ブックマークレットをもう一度クリックすると、元の幅で表示します。
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. プロジェクトなどタスクリストに対する操作
注意: タスクが多数あり一画面に表示しきれていない場合は、以下のブックマークレットを実行する前に、Asana画面を一番下までスクロールし、すべてのタスクを読み込んでください。
2-2-1. ︎ セクションの表示切り替え
リストビューでセクションの展開・折りたたみを切り替えます。
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 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. すべてのタスクの完了
リストビューに表示されているすべての未完了タスクを完了にします。多数ある場合は一度タスクの表示を消したあと、50個ずつクリックしていきます。
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 = '処理しています';
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 = `処理しています (${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. すべてのタスクの未完了
リストビューに表示されているすべての完了タスクを未完了に戻します。多数ある場合は一度タスクの表示を消したあと、50個ずつクリックしていきます。
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 = '処理しています';
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 = `処理しています (${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. マイタスクのみ
2-2-6. カレンダータスクの一括展開
カレンダーの月ビューで、「その他 x 件」をすべて開きます。
画面にロードされている日のタスクだけ展開されるので、上下にスクロールすると折り畳まれたままの日が存在します。
javascript:(function() {
document.querySelectorAll('.CalendarDay-xMoreText').forEach(buttonDiv => buttonDiv.click());
})();
2-3. その他の操作
2-3-1. ☰ サイドバーのサイズ変更
2-3-2. タイトルとURLのコピー
ウェブページのリンクをAsanaに貼るとき、以下のように四度手間になっていませんか?
- タイトルをコピー
- タイトルをペースト
- URLをコピー
- URLをペースト
この二ステップで手間を省きましょう!
- ブックマークレットをクリック
- リンクをペースト
Asana以外にリンクを貼るときにも使えます。
ブックマークレットをクリックした直後にカーソルを動かすと、URLしかコピーできない場合があります。クリックが完了するまで、気持ち長めにカーソルを止めておくのがコツです。
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. 工夫が必要だったところ
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要素のデザインを常時変更しておける
- 特定の変化が起きた場合、または特定の操作を行った場合に動的に対応することはできない
利用方法: StylishやUser 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を書ける方は、ぜひご自分でもブックマークレットを作ってみてください。