Tampermonkeyを利用してSalesforceのUIをカスタマイズする

Tampermonkeyを利用してSalesforceのUIをカスタマイズする方法について解説します。

はじめに

Salesforceの開発を行っている際に、若干不便を感じたり、ここがこうならいいのに...と感じたりすることがあります。そういったものは、検索すればブラウザの拡張機能で賄えたりすることもあるのですが、ニッチな部分だとなかなかイメージに合うものがなかったりします。
そんなSalesforceの「かゆいところ」を自力でカスタマイズするためのChrome拡張機能をご紹介します。完成したコードも載せていますので、興味がある方は試してみてください。

使用するChrome拡張

使用するのは、「Tampermonkey」という特定のサイトについて自作のスクリプトを適用できる便利なChrome拡張です。こちらを利用してSalesforceを自分好みのUIにカスタマイズしていきます。
Tampermonkeyのダウンロード、詳細については以下のページをご確認ください。

やりたいこと

今回はリリース時に使用する「送信変更セット」の画面を使いやすくするスクリプトを実装します。
やりたいことは、以下2点です。
1.表示項目をフィルタリングしたい。
2.フィルタリングされた項目すべてを一括チェックしたい。

1は普段ブラウザで検索して対応することは可能ですが、似たような名前の項目が多い場合などは、余計な項目が出ない方が見やすくミスが発生しにくくなると思います。
こちらは以下のjQuery quicksearch plug-inをTampermonkey上で適用し実装します。
2の一括チェックについては、当初は考えていませんでしたが、1のフィルタをかけた状態で全チェックすると、隠れているすべての項目もチェックされてしまうことが発覚して急遽追加しました。

実装イメージ

以下のような動作の実装を目指します。

完成イメージ

Tampermonkeyの設定

まず作成したスクリプトをどのサイトに対して有効にするかを設定する必要があります。以下の「設定」画面から設定も可能ですが、今回はスクリプト部分に直接記載する方法を選びました。
「@include 」に続けて適用したいサイトURLを指定することで、そのサイトだけにスクリプトを動作させることができます。
今回は変更セット送信画面なので、実際にページを開いてみて以下のように設定しました。
※Lightning Experienceの画面では別のURLが表示されていたため、一旦Classicに切り替えて確認して設定したところどちらの画面でもうまく動作しました。
// ==UserScript==
// @name         送信変更セット画面のカスタマイズ
// @include https://*.salesforce.com/*AddToPackageFromChangeMgmtUi*
// ==/UserScript==
UserScriptヘッダ部分
上記を「エディター」のスクリプト部分に設定して保存すると、設定画面では以下のように見えます。もちろんこの画面で設定しても構いません。

includeURLの追加

完成したスクリプト

適用サイトの設定が終わったらいよいよスクリプトの実装をJavascriptで行います。完成したスクリプトは以下の通りです。コピペで動作すると思いますが、ご利用の際は自己責任でお願いいたします。
// ==UserScript==
// @name         送信変更セット画面のカスタマイズ
// @include https://*.salesforce.com/*AddToPackageFromChangeMgmtUi*
// ==/UserScript==

try{
    setTimeout(main,0);
}catch(exception){
    console.log('tempermonkeyカスタムスクリプトでエラー');
}

/**
* メイン処理
*/
function main(){
    // ライブラリのロード
    loadLiblaries()
    .then(function(){
        // 各種カスタマイズスクリプトの適用
        applyScripts();

    }).catch(function(err){
        console.error(err);
    });
}

/**
 * 各種カスタマイズスクリプトの適用
 */
function applyScripts(){
    // jquery quick search適用処理
    applyJqueryQuickSearch();
    // 全選択チェックボックス処理上書き
    overwriteAllSelectCheckBox();
}

/**
* 各種ライブラリ読み込み処理
*/
function loadLiblaries(){
    // jquery読み込み用要素作成
    const JQUERY_JS = createSrcElement('js',"//code.jquery.com/jquery-2.2.4.min.js");
    // jqueryquicksearch読み込み用要素作成
    const QUICKSEARCH_JS = createSrcElement('js',"//cdnjs.cloudflare.com/ajax/libs/jquery.quicksearch/2.3.1/jquery.quicksearch.min.js");

    // jqueryロード後、他依存ライブラリのロード
    return loadSrcElement('jquery',JQUERY_JS)
    .then(function(res){
        jQuery.noConflict();

        return Promise.all([
            // jquery-quicksearchライブラリ追加
            loadSrcElement('QUICKSEARCH_JS',QUICKSEARCH_JS)
            // ※追加する場合この下に
        ]);
    })
}

/**
* ライブラリ要素作成
*/
function createSrcElement(strType,url){
    let element;
    if(strType == 'js'){
        element = document.createElement("script");
        element.src = url;

    }else if(strType == 'css'){
        element = document.createElement("link");
        element.rel = "stylesheet";
        element.href = url;
    }
    return element;
}

/**
* ライブラリを非同期で読み込む
*/
function loadSrcElement(srcName,src) {
    return new Promise(function(resolve, reject) {
        // 要素追加
        document.body.appendChild(src);
        // ロード後の処理
        src.onload = function (event) {
            resolve(srcName + ' の読み込み成功');
        }
        // エラー時の処理
        src.onerror = function (event) {
            reject(srcName + ' の読み込み失敗');
        }
    });
}

/**
 * jQuery quick search適用処理
 */
 function applyJqueryQuickSearch(){
    //テーブルを検索して検索欄を挿入
    const tableSelector = 'table.list:visible'; // テーブルセレクタ
    const rowsSelector = 'tbody tr:not(.headerRow)'; // フィルタ対象セレクタ
    const searchEleStr = '<div class="jqs-div"><input type="search" class="jqueryQuickSearch"></div>'; // 検索欄作成用文字列

    // jQueryuQuickSearch適用
    const $table = jQuery(tableSelector);
    $table.addClass('jqueryQuickSearch'); // 他のテーブルと区別するためクラス追加
    $table.before(searchEleStr); // テーブル要素の直前に検索欄追加

    // jqueryquicksearch適用
    jQuery('input.jqueryQuickSearch').quicksearch('table.jqueryQuickSearch tbody tr:not(.headerRow)', {
        'delay':300
        ,'stripeRows':['odd','even']
        ,'loader':'span.loading'
        ,'noResults':'tr#noresults'
        ,'bind':'keyup click change'
    });
}

/**
 * 全チェックオン/全チェックオフの処理を「非表示のチェックリストは処理対象外とする」ように上書きする
 */
function overwriteAllSelectCheckBox(){
    // チェックボックス全チェックの処理が存在するかチェックし、あれば上書き
    if(typeof SelectAllOrNoneByCheckbox == "function"){
        SelectAllOrNoneByCheckbox = function(){
            const thisEle = event.target;
            const isChecked = jQuery(thisEle).prop('checked');
            jQuery(thisEle).closest('table').find('td.actionColumn input[type="checkbox"]:visible').prop('checked',isChecked);
        }
    }
}
送信変更セット画面のカスタマイズ.js

振り返り

いざ動かしてみると、変更セット作成の作業はかなり楽になりました。
例えば、特定のオブジェクトのカスタム項目を全部追加する場合は、対象オブジェクトで検索>一括チェックですべての項目を変更セットに追加できますので、生産性アップに貢献してくれそうです。

ただし実装は予想外のことが多く、かなりつまずきながらの開発となりました。

■ライブラリが読み込めない問題

ライブラリをどうやって読み込むか?という部分でいきなりつまずきました。
Tampermonkeyはスクリプトを追加で読み込ませる拡張機能なので、ライブラリを記載する設定箇所がなく、いろいろと調べた結果スクリプトタグを文字列から生成してスクリプトで追加する方法に落ち着きました。

■読み込んだライブラリが使えない問題

次に読み込んだライブラリの機能を使用できない問題が発生しました。
ライブラリ読み込み用ソースの追加後、そのまま処理を書くとライブラリの読み込みが完了する前に呼び出す動きとなり、エラーとなってしまいました。

そのためPromiseでDOMの生成を待ち、処理後に各スクリプトを差し込む動きに変更することで、やっと期待した動作をしてくれるようになりました。このあたり普段の業務では使用することが少ないので、今回じっくり使ってみて理解が深まりました。実装に当たっては以下のページなどを参考にしました。

■全チェック時の動作が想定外

考えてみれば当然のことですが、もともとjQuery quicksearch plug-inの適用など想定されていないため、ライブラリの適用だけでは、フィルタをかけて全チェックした際に、フィルタリングを無視してすべて項目にチェックがつく動きになってしまいました。
そのため標準の全チェック処理をソースから特定して自作処理で上書きするという力技で期待通り動作するように修正しています。

今後やりたいこと

つまずきつつもかなり自由度高くカスタマイズが可能だとわかりましたので、次は以下のようなカスタマイズができないものかと考えています。※現時点でできるかどうかは不明です。
・項目作成など特定の処理に対してショートカットキーを設定する
・選択リストをコンボボックス化する

そのうち公式で対応されて不要になってしまったり、UIが変わって使えなくなってしまったりする部分も出てくると思いますが、快適な開発環境づくりには便利な拡張機能だと思いますので、積極的に使っていきたいと思います。