0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kintone プラグインの作り方:manifest.json・設定画面・署名つき zip まで

0
Posted at

kintone のカスタマイズを JavaScript ファイルのまま配っていると、アプリごとに「どのフィールドを使うか」をコードに書き換える必要があります。プラグインにすると、その設定を管理画面で選べるようになります。

この記事では、日付フィールドの期限切れを赤、期限が近いものを黄色で表示する小さなプラグイン(設定画面つき、JavaScript 約110行)を例に、次の4つを順に見ます。

  1. ファイルの構成と manifest.json
  2. 設定画面:フィールドの一覧を取り、選んだ値を保存する
  3. 動く側(desktop.js):保存した設定を読んで画面に反映する
  4. 署名して zip にする

1. ファイルの構成と manifest.json

expiry-alert/src/
├── manifest.json
├── LICENSE.txt
├── image/icon.png
├── html/config.html
├── css/config.css
├── css/desktop.css
├── js/config.js
└── js/desktop.js

manifest.json は次のとおりです(kintone/expiry-alert/src/manifest.json)。

{
  "manifest_version": 1,
  "version": 1,
  "type": "APP",
  "name": {"ja": "現場の期限アラート", "en": "Site Expiry Alert"},
  "icon": "image/icon.png",
  "homepage_url": {"ja": "https://genbakoe.com/kintone/expiry-alert/", "en": "https://genbakoe.com/kintone/expiry-alert/"},
  "desktop": {"js": ["js/desktop.js"], "css": ["css/desktop.css"]},
  "config": {"html": "html/config.html", "js": ["js/config.js"], "css": ["css/config.css"], "required_params": ["fields"]}
}

(description は省きました)

押さえる所は3つです。

  • desktop は、レコードの一覧や詳細の画面で読み込むファイル。
  • config は、アプリの設定 →「プラグイン」→ 歯車から開く設定画面のファイル。
  • required_params に書いたキーが保存されるまで、kintone はプラグインを「設定が済んでいない」として扱います。ここでは、見張る日付フィールドの一覧 fields を必須にしています。

2. 設定画面:フィールドの一覧を取って選ばせる

設定画面の HTML は、kintone の画面の中に差し込まれる断片です。<html> や <body> は書きません。

<div class="gbk-config">
  <p>期限を見張る日付フィールドを選びます。複数選べます。</p>
  <div id="gbk-fields"></div>
  <label>「期限が近い」とする日数<input id="gbk-days" type="number" min="1" max="365" value="30"></label>
  <p class="gbk-actions"><button type="button" id="gbk-save">保存</button> <button type="button" id="gbk-cancel">キャンセル</button></p>
</div>

チェックボックスは、REST API の「フォームのフィールド一覧の取得」で、そのアプリにある日付と日時のフィールドから作ります(kintone/expiry-alert/src/js/config.js)。

(function(PLUGIN_ID) {
  'use strict';

  var saved = kintone.plugin.app.getConfig(PLUGIN_ID);
  var chosen = saved.fields ? JSON.parse(saved.fields) : [];
  var box = document.getElementById('gbk-fields');
  // (中略)
  kintone.api(kintone.api.url('/k/v1/app/form/fields.json', true), 'GET', {app: kintone.app.getId()}).then(function(response) {
    var found = 0;
    Object.keys(response.properties).forEach(function(code) {
      var field = response.properties[code];
      if (field.type !== 'DATE' && field.type !== 'DATETIME') return;
      found++;
      var label = document.createElement('label');
      var input = document.createElement('input');
      input.type = 'checkbox';
      input.value = code;
      input.checked = chosen.indexOf(code) >= 0;
      label.appendChild(input);
      label.appendChild(document.createTextNode(' ' + field.label + '(' + code + ')'));
      box.appendChild(label);
    });
    if (!found) box.textContent = 'このアプリには、サブテーブルの外に日付フィールドがありません。';
  });
  • ファイル全体を (function(PLUGIN_ID) { ... })(kintone.$PLUGIN_ID); で包みます。kintone.$PLUGIN_ID に、そのプラグインの ID が入っています。
  • kintone.api.url(パス, true) の第2引数を true にすると、ゲストスペースのアプリでは /k/guest/<スペースID>/v1/... の URL を返します。ゲストスペースでも動かしたいときは付けておきます。
  • response.properties の直下に出るのはサブテーブルの外のフィールドだけです。サブテーブルの中は type: 'SUBTABLE' の fields の下にあります。このプラグインは一覧の列に色を付けるので、外のフィールドだけを対象にしています。

保存は kintone.plugin.app.setConfig です。値は文字列しか保存できないので、配列は JSON.stringify で文字列にします。

  document.getElementById('gbk-save').addEventListener('click', function() {
    var fields = Array.prototype.map.call(box.querySelectorAll('input:checked'), function(i) { return i.value; });
    var limit = Number(days.value);
    if (!fields.length) {
      window.alert('日付フィールドを1つ以上選んでください。');
      return;
    }
    if (!(limit >= 1 && limit <= 365)) {
      window.alert('日数は1から365で入れてください。');
      return;
    }
    kintone.plugin.app.setConfig({fields: JSON.stringify(fields), days: String(Math.floor(limit))});
  });

  document.getElementById('gbk-cancel').addEventListener('click', function() {
    window.location.href = '../../' + kintone.app.getId() + '/plugin/';
  });
})(kintone.$PLUGIN_ID);

入力の確かめ(1つ以上選んだか、日数が1〜365か)は保存の前に済ませます。動く側では「設定は正しい」と思って書けるので、コードが短くなります。

3. 動く側:設定を読んで一覧に色を付ける

desktop.js の頭で設定を読みます(kintone/expiry-alert/src/js/desktop.js)。

  var config = kintone.plugin.app.getConfig(PLUGIN_ID);
  if (!config.fields) return;
  var fields = JSON.parse(config.fields);
  var limit = Number(config.days) || 30;

設定がまだなら何もしません。プラグインを入れただけで画面が壊れることを避けます。

一覧の画面では、app.record.index.show で、選んだフィールドのセルを kintone.app.getFieldElements で取ります。返る配列の順番は event.records と同じなので、添字で対応させます。

  kintone.events.on('app.record.index.show', function(event) {
    if (event.viewType !== 'list') return event;
    var expired = 0;
    var soon = 0;
    fields.forEach(function(code) {
      var cells = kintone.app.getFieldElements(code);
      if (!cells) return;
      event.records.forEach(function(record, index) {
        if (!record[code]) return;
        var state = mark(cells[index], daysLeft(record[code].value));
        if (state === 1) expired++;
        if (state === 2) soon++;
      });
    });
    // (中略:ヘッダーに件数を出す)
    return event;
  });
  • カレンダー表示やカスタマイズビューではセルがないので、event.viewType !== 'list' で抜けます。
  • そのフィールドが一覧の列に出ていなければ getFieldElements は null を返すので、そこでも抜けます。

日付の残り日数の数え方には、1つ落とし穴があります。

  // 残り日数。日付だけの値は端末の暦のその日として数える
  function daysLeft(value) {
    if (!value) return null;
    var date = /^\d{4}-\d{2}-\d{2}$/.test(value) ? new Date(value + 'T00:00:00') : new Date(value);
    var time = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
    if (isNaN(time)) return null;
    return Math.round((time - startOfToday()) / DAY);
  }

日付フィールドの値は "2026-12-01" の形です。これをそのまま new Date("2026-12-01") に渡すと、JavaScript は UTC の0時 として読みます。日本時間では同じ日の9時なので気づきにくいのですが、UTC より遅れた時間帯の端末では前の日になります。T00:00:00 を付けると、端末の時間帯の0時として読まれます。日時フィールド(UTC の ...Z で届く)は、そのまま new Date に渡してから、端末の暦の日に丸めています。

4. 署名して zip にする

kintone に読み込ませるには、ソースを zip にし、RSA の鍵で署名した形にします。公式のツール(@kintone/plugin-packer)もありますが、ここでは同じことを Python で書いた例を見ます(tools/kintone_pack.py)。

def main(name):
    src = ROOT / "kintone" / name / "src"
    # (中略)
    contents = zip_bytes((str(p.relative_to(src)), p.read_bytes()) for p in sorted(src.rglob("*")) if p.is_file() and p.name != ".DS_Store")
    key = load_key(name)
    public_der = key.public_key().public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
    signature = key.sign(contents, padding.PKCS1v15(), hashes.SHA1())
    out = ROOT / "static" / ("genbakoe-%s.zip" % name)
    out.write_bytes(zip_bytes([("contents.zip", contents), ("PUBKEY", public_der), ("SIGNATURE", signature)]))

できる zip の中身は3つです。

ファイル 中身
contents.zip src/ 以下をまとめたもの
PUBKEY 公開鍵(DER)
SIGNATURE contents.zip の署名

プラグイン ID は公開鍵から決まります。

def plugin_id(public_der):
    digest = hashlib.sha256(public_der).hexdigest()[:32]
    return "".join(chr(ord("a") + int(c, 16)) for c in digest)

公開鍵の SHA-256 の先頭32桁の16進を、0〜f → a〜p に置き換えた32文字です。鍵を変えると ID が変わり、別のプラグインとして扱われます。 すでに入れている所で「更新」できなくなるので、秘密鍵はリポジトリの外に置いて、なくさないようにします。このスクリプトも鍵を ~/.config/genbakoe/ に置き、Git に入れていません。

zip の中のファイルの日時を (2026, 1, 1, 0, 0, 0) に揃えているのは、中身が同じなら同じ zip になるようにするためです。

まとめ

  • manifest.json の required_params で「設定が済むまで動かない」を kintone に任せる。
  • 設定画面ではフィールドの一覧を REST API で取り、setConfig には文字列で渡す。
  • 日付だけの値は T00:00:00 を付けて端末の暦の日として読む。
  • 署名の鍵がプラグイン ID を決める。鍵はなくさない。

ソースと配布先

この記事のコードは、建設向けの無料の kintone プラグイン「現場の期限アラート」の一部です。

※ kintone はサイボウズ株式会社の登録商標です。このプラグインと記事は、サイボウズ株式会社の公式のものではありません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?