kintoneでちょっと便利なスクリプトをChrome拡張化してみた

🎄こちらは [kintone Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kintone) 25日目の記事です🎅

# はじめに

みなさん！メリークリスマス！
今年もkintoneアドベントカレンダーにはたくさんの記事が書かれました！ありがとうございます！！
そして僕はいつもいつも学習せず、ぎりぎりの公開ですw

さて、今回の記事なのですが、前々から便利ツール系を作りたいなーとは思っていて、長いこと温めていた **Chrome拡張を使って便利ツールを作る** をやってみました。

といっても中身のネタ自体はいろいろな人のお知恵を拝借して、自分はただそれをChrome拡張にラッピングした感じですw

> Chrome拡張自体は公開しているので良かったらお試しくださいな！
> そしてぜひ自分たちでも便利Chrome拡張を作ってみてください！

https://chrome.google.com/webstore/detail/kntnex/mgmbanjaekigookepocjbhcmkhachinh?hl=ja&authuser=0

# 作ろうと思ったきっかけ

kintoneの対面開発をする中で求められるものの1つに **「スピード」** があります。いかにkintoneの設定をミスなく素早くできるかによって、対面しているお客様の印象がぜんぜん違う！！
（極端な話、もたつくと不安がられスピーディーにできると安心してもらえます ※BB解釈）

で、よく設定するわりにめちゃくちゃめんどくさい設定として **都道府県のドロップダウン** があったりします。そう。47都道府県。
あれを1から設定するってだるいですよね。でも設定して見せると結構お客様の印象いいんですよ。

だから毎回悩ましい。

<br/><br/>

ところが、ありがたいことに @the_red さんがめっちゃ便利なスクリプトを作ってくださってるんですよ！

https://qiita.com/the_red/items/f89c3d021f3064ac9942

これを使えば大量の設定項目を一瞬で設定できる！これは愛用するしか無い！

# スクリプトを愛用してみて

めちゃくちゃ助かってます。助かってはいるのですが、毎回スクリプトを呼び出すのがめんどくさい・・・
自分はChromeの開発者ツールの **snippets** に記録させていますが、それを呼び出すことすらめんどくさい！

https://developer.chrome.com/docs/devtools/javascript/snippets/

さらにいうと、このスクリプトを社内展開してみたところ、意外と利用ニーズがあるという・・

つまり、みんなめんどくさい思いをしている（はず！）
![スクリーンショット 2022-12-25 23.06.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/8ef4b856-146e-c9ec-bfda-d300ceb7c9c7.png)

ということで、Chrome拡張化して **いつでも簡単に呼び出せる** ようにしてみました！

# 作成したスクリプト

上記の都道府県の一括設定スクリプトだけだとなんかつまらないので、あと2つほどスクリプトを仕込んでみました。

- 都道府県を一括設定するスクリプト
- アプリの管理者（最終更新者）が確認できるスクリプト
- プロセス管理の設定情報をコピペできるスクリプト

アプリの管理者のやつは[cybozu developer network](https://developer.cybozu.io/hc/ja/articles/212141066)にあるやつです(これもよく使う！)

<blockquote class="twitter-tweet"><p lang="zxx" dir="ltr"><a href="https://t.co/h76nAbkalF">pic.twitter.com/h76nAbkalF</a></p>&mdash; BB@サブ (@BB_File) <a href="https://twitter.com/BB_File/status/1607030463088201728?ref_src=twsrc%5Etfw">December 25, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 構成

Chrome拡張のファイル構成はこんな感じ。

```
├── icons
│   └── icon.png
├── manifest.json
└── src
    └── js
        ├── checkAppAdmin.js
        ├── copyStatus.js
        ├── pasteStatus.js
        ├── public
        │   ├── background.js
        │   └── content_scripts.js
        └── setPrefecture.js
```

## manifest.json

Chrome拡張の設定ファイルですね。これは意外とシンプルにまとまりました。（manifest v3対応版です）

```json:manifest.json
{
  "name": "KNTNEX",
  "version": "1.0.0",
  "manifest_version": 3,
  "description": "kintoneであると便利なスクリプトをまとめました",
  "permissions": [
    "contextMenus"
  ],
  "icons": {
    "16": "icons/icon.png",
    "48": "icons/icon.png",
    "128": "icons/icon.png"
  },
  "web_accessible_resources": [
    {
      "matches": ["https://*.cybozu.com/*"],
      "resources": [
        "src/js/setPrefecture.js",
        "src/js/checkAppAdmin.js",
        "src/js/copyStatus.js",
        "src/js/pasteStatus.js"
      ]
    }
  ],
  "content_scripts": [
    {
      "matches": ["https://*.cybozu.com/*"],
      "js": ["src/js/public/content_scripts.js"]
    }
  ],
  "background": {
    "service_worker": "./src/js/public/background.js",
    "type": "module"
  }
}
```

今回、右クリックのChrome拡張メニューを使うので、permissionsに `contextMenus` を追加しています！

## background.js / content_script.js

一番こだわった&作るのが大変だった部分です。
右クリックのメニューでこんな感じに階層構造で各スクリプトを呼び出せるようにしたく、そのためにbackground.jsとcontent_script.jsをちょっとアレンジしました。
![スクリーンショット 2022-12-25 23.22.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/3ac20306-43ac-6970-5858-e0385e6f6327.png)

```javascript:background.js
// 右クリックメニュー
const contextMenus = async () => {
  await chrome.contextMenus.removeAll();

  // 都道府県
  chrome.contextMenus.create({
    id: "kntnex_prefecture",
    title: "都道府県の挿入",
    contexts: ["all"],
  });

  // アプリ管理者の確認
  chrome.contextMenus.create({
    id: "kntnex_app_manager",
    title: "アプリ管理者の確認",
    contexts: ["all"],
  });

  // プロセス管理のコピペ
  const kntnex_process = chrome.contextMenus.create({
    id: "kntnex_process",
    title: "プロセス管理",
    contexts: ["all"],
  });
  chrome.contextMenus.create({
    id: "kntnex_process_copy",
    parentId: kntnex_process,
    title: "プロセス管理の設定コピー",
    contexts: ["all"],
  });
  chrome.contextMenus.create({
    id: "kntnex_process_paste",
    parentId: kntnex_process,
    title: "プロセス管理の設定ペースト",
    contexts: ["all"],
  });
};

chrome.runtime.onInstalled.addListener(contextMenus);
chrome.runtime.onStartup.addListener(contextMenus);
chrome.contextMenus.onClicked.addListener((info, tab) => {
  chrome.tabs.sendMessage(tab.id, {
    action: info.menuItemId,
  });
});
```

メニューの階層構造はドキュメント通りに記載したらOKで、parentを指定する感じです。
ただ、今回のメイン処理となる **kintone上でスクリプトを動かす** をするためにはちょっと一工夫が必要です。

### ポイント1：background.js（ブラウザの裏側）とcontent_script.js（ブラウザの表側）の連携
Chrome拡張のドキュメントだったり他のブログ等を見ると、このbackground.jsの `chrome.contextMenus.onClicked.addListener` 部分で、実際に動かすプログラムを指定していることが多いのですが、でもそれだとbackground側（documentなどの画面側ではなくブラウザ自体の裏側っぽい方）で動くことになるので、kintone上にあるkintoneオブジェクトなどが使えず今回困るんですよね。（kintone.apiとかそのまま使いたい）

なので、background側からフロント側（content_script.js）へ情報を渡す必要があり、background.jsからは`chrome.tabs.sendMessage` を使って投げ、
content_script.js側で`chrome.runtime.onMessage.addListener` を使って受け取る仕組みにしています。
（この仕組みを考えるのにまぁまぁ時間がかかった・・・）

```javascript:content_script.js
// 右クリックメニューの結果受取
chrome.runtime.onMessage.addListener(async (request, sender) => {
  console.log(request);
  if (request.action === "kntnex-process-copy") {
    window.alert("エラーが発生しました。");
    return;
  }

  let script;
  switch (request.action) {
    case "kntnex_prefecture":
      script = document.createElement("script");
      script.src = chrome.runtime.getURL("src/js/setPrefecture.js");
      document.head.appendChild(script);
      break;
    case "kntnex_app_manager":
      script = document.createElement("script");
      script.src = chrome.runtime.getURL("src/js/checkAppAdmin.js");
      document.head.appendChild(script);
      break;
    case "kntnex_process_copy":
      script = document.createElement("script");
      script.src = chrome.runtime.getURL("src/js/copyStatus.js");
      document.head.appendChild(script);
      break;
    case "kntnex_process_paste":
      script = document.createElement("script");
      script.src = chrome.runtime.getURL("src/js/pasteStatus.js");
      document.head.appendChild(script);
      break;
  }
});
```

あとは受け取った変数に右クリックメニューのIDが格納されているのでそれで分岐処理をする感じです。
でも、ここでも少しテクニックが必要になり・・・

### ポイント2：各スクリプトファイルの呼び出し

今回、3つのスクリプト（コピペを **コピ** と **ペ** に分けたら計4つ）を用意するのですが、1つのファイルにベタ書きだと管理が大変なので、4ファイルにJavaScriptファイルを分けて、それぞれ呼び出すようにしたかったんです。でもそれも簡単ではなかったという。

まず、`require`や`import`はブラウザ側でエラーになり、webpackをする手があるのですがそれ以外に何かないものかと調べていたところ、`scriptタグとして各JavaScriptを埋め込む`方法がChrome拡張ではできるらしく、その方法にしてみました。（webpackを使わなかったのはただ環境設定がめんどくさかっただけですがw）

それぞれのJavaScriptファイルを `chrome.runtime.getURL`でURL化してscriptタグに埋め込むことで、それぞれのJavaScriptが動くようになっています。

## 都道府県を一括設定するスクリプト

redさんの記事の内容をコピペしただけっす。

```javascript:setPrefecture.js
(() => {
  const thisUrl = location.href;
  if (!thisUrl.includes("/k/admin")) {
    window.alert("この拡張はフォーム設定画面でのみ有効です");
    return;
  }

  const ITEMS = [
    "北海道",
    "青森県",
    "岩手県",
    "宮城県",
    "秋田県",
    "山形県",
    "福島県",
    "茨城県",
    "栃木県",
    "群馬県",
    "埼玉県",
    "千葉県",
    "東京都",
    "神奈川県",
    "新潟県",
    "富山県",
    "石川県",
    "福井県",
    "山梨県",
    "長野県",
    "岐阜県",
    "静岡県",
    "愛知県",
    "三重県",
    "滋賀県",
    "京都府",
    "大阪府",
    "兵庫県",
    "奈良県",
    "和歌山県",
    "鳥取県",
    "島根県",
    "岡山県",
    "広島県",
    "山口県",
    "徳島県",
    "香川県",
    "愛媛県",
    "高知県",
    "福岡県",
    "佐賀県",
    "長崎県",
    "熊本県",
    "大分県",
    "宮崎県",
    "鹿児島県",
    "沖縄県",
  ];

  const addButton = ".treeeditor-node-item-add-cybozu";
  const textField = ".treeeditor-nodes-cybozu .input-text-cybozu";
  for (const value of ITEMS) {
    const allButtons = document.querySelectorAll(addButton);
    const length = allButtons.length;
    if (!allButtons[length - 1]) {
      window.alert("この画面では都道府県の設定はできません。");
      return;
    }
    allButtons[length - 1].click();
    document.querySelectorAll(textField)[length].value = value;
  }
})();
```

![スクリーンショット 2022-12-25 23.41.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/937cbd07-ed06-3b98-72a2-9a7aaa4b0110.png)

ドロップダウン以外にチェックボックス、ラジオボタン、複数選択でも使えます！

## アプリの管理者（最終更新者）が確認できるスクリプト

devnetの記事のやつをコピペしただけっす。

```javascript:checkAppAdmin.js
(async () => {
  try {
    const appId = window.prompt(
      "最終更新者を調べたいアプリのIDを記入してください。※空で実行した場合は画面上のアプリになります。",
    );
    if (!appId && (!kintone || !kintone.app.getId())) {
      window.alert("この拡張はアプリの画面でのみ有効です。");
      return;
    }
    const url = kintone.api.url("/k/v1/app", true);
    const app = appId || kintone.app.getId();
    const resp = await kintone.api(url, "GET", { id: app });
    console.log(resp);
    window.alert(
      `【アプリID:${resp.appId} / アプリ名:${resp.name}】の最終更新者: ${resp.modifier.name}`,
    );
  } catch (err) {
    const errMsg = err.message || err;
    window.alert(`【エラーが発生しました】${errMsg}`);
    console.error(err);
  }
})();
```
![スクリーンショット 2022-12-25 23.46.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/3df4dd63-9085-e197-bb0d-cf1122b9e917.png)
![スクリーンショット 2022-12-25 23.46.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/4fea8d10-b454-15ec-a243-da750a730c2b.png)

## プロセス管理の設定情報をコピペできるスクリプト

これは特に元ネタはなく、案件中に同じプロセス管理の設定を既存アプリに反映したいことがあり、手入力するのがめんどくさすぎて作りました。

`copyStatus.js` ではプロセス管理の設定情報をAPIで取得して、 `local.storage` に保存しています。

```javascript:copyStatus.js
(async () => {
  try {
    const thisUrl = location.href;
    if (
      !thisUrl.includes("/k/admin/app/status") &&
      (!kintone || !kintone.app.getId())
    ) {
      window.alert("この拡張はアプリの画面でのみ有効です。");
      return;
    }
    const appUrl = kintone.api.url("/k/v1/app", true);
    const statusUrl = kintone.api.url("/k/v1/preview/app/status.json", true);

    const app = kintone.app.getId() || thisUrl.split("app=")[1];

    // アプリ情報を取得
    const resp = await kintone.api(appUrl, "GET", { id: app });
    // アプリのプロセス設定を取得
    const res = await kintone.api(statusUrl, "GET", {
      app,
    });
    delete res.revision;
    console.log(res);
    localStorage.setItem("kntnex-process", JSON.stringify(res));
    window.alert(
      `【アプリID:${resp.appId} / アプリ名:${resp.name}】のプロセス設定をコピーしました。`,
    );
  } catch (err) {
    const errMsg = err.message || err;
    window.alert(`【エラーが発生しました】${errMsg}`);
    console.error(err);
  }
})();
```

`pasteStatus.js` では `local.storage` に設定があれば、それをAPIで反映させています。

（取得もですが）反映は `preview` を使っているので、間違えたとしてもアプリを反映しなければOK!（設定画面ですでに設定中だったときはすみません。無理っす・・）


# おわりに

なんとか25日中には公開できたーーーー（というこのコメントを書いているのが25日の23:55）
便利ツールがあるとQOLが爆上がりするので今後も便利ツールを作っていきたい！

それでは！≧(+･` ཀ･´)≦