Garoon のポートレットに kintone の一覧を表示する

経緯

ことの発端は自分が過去に書いた "Garoonでkintoneのデータを表形式で表示してみよう! | キントマニア | kintone活用ブログ" なのですが、最近でも時折掘り返されてるようだったので、ちょっとだけモダンに書き換えました。

"Garoonにkintoneアプリを表示する方法を比較しました(HTML vs iframe)" という記事にもサンプルコードあるのですが、これも 2018 年製で jQuery が使われていて、これも先の記事と五十歩百歩のようでした 😅 また、この記事では iframe を用いた kintone の一覧を表示する方法も紹介されてますが、ヘッダ等を除いて一覧だけ表示させたいという方もいらっしゃることでしょうし。やっていきましょう。

アプローチ

自分は普段は React/TypeScript 使いなのですが、今回はビルドなしでとりあえずすぐ試せるように、Lodash の HTML テンプレートエンジン機能を使いつつ、JavaScript の書き方だけモダンにした感じです。 あとは、kintone-rest-api-client も当時はなかったので、kintone の API 操作は今回これに任せました。

コーディング

出来上がりイメージ

カレンダーの下に見えている kintone の一覧画面っぽい HTML ポートレットを今回やっていきます。

JavaScript (customize.js)

(async () => {
  const APP_ID = 547; // アプリ ID
  const ORDERBY_FIELD = '$id'; // ソートに使うフィールド
  const AVAILABLE_FIELD_CODES = [ // 表示するフィールドのフィールドコード
    '会社名',
    '部署名',
    '担当者名',
    '住所'
  ];

  const client = new KintoneRestAPIClient({});
  
  // レコードとフィールド情報を取得して、表示用データを作成
  const buildData = async () => {
    const orderBy = `${ORDERBY_FIELD} desc`;
    // kintone の API 操作は rest client を利用 https://github.com/kintone/js-sdk/tree/master/packages/rest-api-client#readme
    const [records, {properties}] = await Promise.all([
      client.record.getAllRecordsWithOffset({ app: APP_ID, orderBy }),
      client.app.getFormFields({ app: APP_ID })
    ]);
    // console.log(records, properties);

    // table のタイトルを作成
    const titles = AVAILABLE_FIELD_CODES
      .map(code => {
        // フィールド情報を使って、フィールドのコードからラベルを抽出
        const prop = Object.entries(properties).find(([_code]) => code === _code);
        if (prop) {
          const [_, {label}] = prop;
          return label;
        }
        return undefined;
      })
      .filter(label => label !== undefined);
    // table のセルデータを作成
    const rows = records.map(record => {
      // 行(レコード)毎に対象フィールドの値を抽出
      const row = AVAILABLE_FIELD_CODES
        .map(code => {
          const field = Object.entries(record).find(([_code]) => code === _code);
          if (field) {
            const [_, {type, value}] = field;
            let newValue;
            // フィールドタイプによって値の表示方法を切り替えるようにする https://cybozu.dev/ja/kintone/docs/overview/field-types/
            // 対応するフィールドタイプを増やすためにはここを調整
            switch (type) {
              default:
                newValue = value;
            };
            return newValue;
          }
          return undefined;
        })
        .filter(value => value !== undefined);
      return {row, id: record.$id.value};
    });
    // console.log(titles);
    // console.log(rows);
    return {titles, rows};
  };

  // Lodash のテンプレートエンジン機能を使って HTML を作成
  const createTableHTML = ({titles, rows}) => {
    // https://lodash.com/docs/4.17.15#template
    const templateString = `
    <table class="kintone-table">
      <thead class="kintone-table-thead">
        <tr>
          <th class="kintone-table-th"><div></div></th>
          <% titles.forEach((title) => { %>
            <th class="kintone-table-th"><div><%- title %></div></th>
          <% }); %>
        </tr>
      </thead>
      <tbody class="kintone-table-tbody">
        <% rows.forEach(({row, id}) => { %>
          <tr class="kintone-table-tr">
            <th class="kintone-table-th">
              <a href="${`/k/${APP_ID}/show#record=<%= id %>`}">
                <span class="kintone-table-record-detail-icon"></span>
              </a>
            </th>
            <% row.forEach((value) => { %>
              <td class="kintone-table-td">
                <div>
                  <%= value %>
                </div>
              </td>
            <% }); %>
          </tr>
        <% }); %>
      </tbody>
    </table>`;
    return _.template(templateString)({titles, rows});
  };

  // レンダリング
  const renderTable = async () => {
    const rootElement = document.querySelector('#root');
    const data = await buildData();
    console.log(data);
    rootElement.innerHTML = createTableHTML(data); // insert HTML to containerElement (div#root)
  };

  // レンダリングを実行
  await renderTable();

})();

CSS (customize.css)

.kintone-table {
  margin: 0;
  max-height: 500px;
  font-size: 14px;
  line-height: 1.5;
  border-spacing: 0;
  border-collapse: separate;
  border-bottom: 1px solid #e3e7e8;
  border-left: 1px solid #e3e7e8;
}

.kintone-table-thead {
  height: 54px;
}

.kintone-table-thead tr:first-of-type th {
  border-top: 1px solid #e3e7e8;
}

.kintone-table-tr {
  height: 54px;
}

.kintone-table-tr:nth-of-type(2n) {
  background-color: #f5f5f5;
}

.kintone-table-tr:nth-of-type(2n+1) td,
.kintone-table-tr:nth-of-type(2n+1) th {
  background-color: #f5f5f5;
}

.kintone-table-tr:nth-of-type(2n) td,
.kintone-table-tr:nth-of-type(2n) th {
  background-color: #fff;
}

.kintone-table-th {
  position: sticky;
  top: 0;
  left: 0;
  max-width: 652px;
  padding: 0 8px;
  font-weight: 400;
  letter-spacing: 0.1em;
  background: #fff;
  border-right: 1px solid #e3e7e8;
  border-bottom: 1px solid #e3e7e8;
}

.kintone-table-th div {
  position: relative;
  padding: 10px 5px 10px 0;
}

.kintone-table-tbody tr:nth-of-type(2n+1) th {
  border-left: 1px solid #e3e7e8;
}

.kintone-table-td {
  max-width: 652px;
  min-height: 50px;
  padding: 5px;
  background: #fff;
  border-right: 1px solid #e3e7e8;
  border-bottom: 1px solid #e3e7e8;
}

.kintone-table-td a {
  position: relative;
  padding: 9px 12px;
}

.kintone-table-tr:nth-child(odd) {
  background-color: #f5f5f5;
}

.kintone-table-record-detail-icon{
  display: inline-block;
  margin-top: 7px;
  width: 12px;
  height: 16px;
  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAQCAYAAAAiYZ4HAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NDkxMSwgMjAxMy8xMC8yOS0xMTo0NzoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUxRkEyQjBBMEJDOTExRTQ5MUVFOTIwNzZCRTVEQkEwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjI1MzVEOTgyMEJERjExRTQ5MUVFOTIwNzZCRTVEQkEwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NTFGQTJCMDgwQkM5MTFFNDkxRUU5MjA3NkJFNURCQTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NTFGQTJCMDkwQkM5MTFFNDkxRUU5MjA3NkJFNURCQTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5St7+KAAAAvUlEQVR42uySMQrCUBBEZ/9PExOxtFEv4QEk2HgHD+FxUoiVJ7DwANYewUovICFGoiS7bhBFJD+Y3oFthn3DwC6N48ORgB6+ZK0gTezSWOz9jmyYcat8o8sjPIG6YSJZgGT7CjI6GVwSZAZ01tSpgmvSdA9NssivZbGzdztgwUSE5k6AS4IfyixJ85W5hLGmaz71PXcb7csSDbtBxOC331ipggqWTyszaKk/8CsQtNgPPD3Lqe69HYdMHgIMACT+N6kpJL2vAAAAAElFTkSuQmCC) no-repeat center center;
}

適用方法

JavaScript 側は kintone-rest-api-client と Lodash の minimized ファイルを事前に読み込んで、今回の customize.js をセットします。CSS 側はテーブルのスタイリングをそれっぽく行うための customize.css を同じくセットします。

所感

  • HTML テンプレートと JSX 近いけど、慣れもあってか JSX の方が書きやすいw
  • 頑張りポイントはロジックよりスタイル調整

免責

紹介したコードの内容・利用に関する責任は一切負いません。 また、こちらは個人の意見で、所属する企業や団体は関係ありません。

おまけ

ChatGPT に今回の JavaScriptCSS を TypeScript ベースの React 1 ファイルに書き換えてもらったら、こうなりました。スタイルとかだいぶ削られた感じですが、粗方それっぽい感じで、悪くないかなぁと思います。

import { useState, useEffect } from 'react';
import { css } from '@emotion/react';
import { KintoneRestAPIClient } from '@kintone/rest-api-client';

const client = new KintoneRestAPIClient({});

type FieldCode = '会社名' | '部署名' | '担当者名' | '住所';

type RecordData = {
  id: number;
  row: (string | undefined)[];
};

type TableData = {
  titles: string[];
  rows: RecordData[];
};

const AVAILABLE_FIELD_CODES: FieldCode[] = ['会社名', '部署名', '担当者名', '住所'];

const Table = () => {
  const [tableData, setTableData] = useState<TableData>({ titles: [], rows: [] });

  useEffect(() => {
    const fetchData = async () => {
      const orderBy = '$id desc';
      const [records, { properties }] = await Promise.all([
        client.record.getAllRecordsWithOffset({ app: 547, orderBy }),
        client.app.getFormFields({ app: 547 }),
      ]);

      const titles = AVAILABLE_FIELD_CODES.map((code) => {
        const prop = Object.entries(properties).find(([fieldCode]) => fieldCode === code);
        return prop ? prop[1].label : undefined;
      }).filter(Boolean);

      const rows = records.map((record) => {
        const row = AVAILABLE_FIELD_CODES.map((code) => {
          const field = Object.entries(record).find(([fieldCode]) => fieldCode === code);
          return field ? field[1].value : undefined;
        }).filter(Boolean);

        return { id: record.$id.value, row };
      });

      setTableData({ titles, rows });
    };

    fetchData();
  }, []);

  return (
    <table css={tableStyle}>
      <thead>
        <tr>
          <th></th>
          {tableData.titles.map((title) => (
            <th key={title}>{title}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {tableData.rows.map(({ id, row }) => (
          <tr key={id}>
            <td>
              <a href={`/k/547/show#record=${id}`}>
                <span css={recordDetailIconStyle} />
              </a>
            </td>
            {row.map((value, index) => (
              <td key={index}>{value}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

const tableStyle = css`
  margin: 0;
  max-height: 500px;
  font-size: 14px;
  line-height: 1.5;
  border-spacing: 0;
  border-collapse: separate;
  border-bottom: 1px solid #e3e7e8;
  border-left: 1px solid #e3e7e8;
`;

const recordDetailIconStyle = css`
  display: inline-block;
  width: 16px;
  height: 16px;
  margin: 0;
  padding: 0;
  background: url('/k/static/resources/records_show.png') no-repeat center;
  vertical-align: middle;
`;

kintone hack & show+case unlimited 2022 - エントリーから発表まで

 今年 2022 年、久しぶりに kintone hack & show+case unlimited にエントリーして、本戦グランプリも頂いたので、グランプリエントリ、残しておきたいと思います。

 主にの次の 2 点について書かせてもらっています。

  • 昨年までの hack から変更された show+case unlimited という新しいイベントタイトルへの自分の解釈
  • 自分の発表内容を "シン・kintone 検索カスタマイズ" にした経緯

 主語が "自分" だらけの内容ですが、興味を持って頂いた方、発表をご覧頂いて "なんで、いつものようなテクノロジーに振った内容じゃなかったの?" と思われた方に読んで頂けると嬉しいです。2、3 回に分けても良さようなくらいの長文なので、3 時のおやつのお供にでもお読み頂ければ幸いです🙇

 ちなみに、予選と本戦のスライドは公開済みですが、本戦はここに貼り付けておきます。

 また、技術的な話はこちらにエントリ書きましたので、よろしければ合わせて是非。

当初のエントリー理由

 主にこれらが重なってエントリーに至りました。

  • 去年初めて出番なしの Cybozu Days に参加して少々手持ち無沙汰だったので、今年は出番作ろうかなと思ったから(今年も去年も出張とかではなくてただの帰省だったので)
  • いずれ機会が来たら、普段の活動の延長のような王道なテーマを引っ提げてまた出てみようかなぁと思っていたから(今年変更されたイベントタイトルにも合いそうだったから)
  • そして、その時には JavaScript カスタマイズ(kintone 内)だけで完結する内容で挑んでみたいと思っていたから
  • hack に何回か出させてもらっていて、2018 チャンプになって、最後が 2020 で予選敗退で終わってるのも何だかなぁと思っていたから😅

今年やって来た hack のコンセプト変更

イベントコンセプトの変更は近いと思っていた

 これだからこのイベントが盛り上がると言ってもいい、このイベントを象徴するキーワードである "ここまでできる kintone""カスタマイズ頂上決戦" は外せないだろうなぁと個人的に思いつつ、大きく次の 2 点から hack のコンセプトの変更あるいは修正は近いだろうなぁと思っていました。あるいは、一旦イベントの開催が中断になるか。

  • kintone 自体も大きく機能や API が増えていないこと
  • 一般論として、関連テクノロジーの進化が緩やかなフェーズに入ってきたこと

 また、これらに引っ張られるかのように、hack 予選での発表も過去の発表・アイディアと近いものが出てくるようになり、周回傾向に入ってきてたのは否めないと思います。新しいアイディアの捻出が難しくなってきているというところでしょうか(ただ、何だかんだテクノロジーは進化していくので・・・😁)。

そして、自分が思ったより早く、今年運営が動いてきました。

今年の予選の告知が例年より遅かったので、ひょっとしたら今年か、とは途中から思いましたが。

運営が動いてきた

 予選の名称は kintone hack を受け継ぎ、本戦は kintone show+case unlimited と改称されました。その他わかりやすい変更ポイントとしては、予選審査基準と本戦オーディエンスのターゲットだったでしょうか、予選エントリーページを見た限り。

予選エントリーページから幾つか関連する部分のキャプチャを貼っておきます。

予選審査基準

この "審査員が本戦に出て欲しいと思ったか?" というのは、最後何でもアリ感があって、個人的には面白くて好きですね😆 色々審査項目を詳細に詰めたりしても、今度はそれを忠実にこなすために審査員に求められるスキルが上がったり、審査員選定が難しくなるでしょうし。まぁ、ズルい感じ?しますが、結局響くかどうか、ということですかね。

変更ポイント
本戦について(ページの漢字間違ってますね)

イベントタイトルの変更は都合よく解釈する

 自分は今中の人なので、社内の情報を漁れば変更意図等を汲み取ることが出来たかもしれませんが、エントリーする可能性があったことと、エントリーしたとして特に漁っても結局自分がやることや結果は変わらないような気がしていて、・・・エントリーするとすれば自分なりに都合よく解釈してテーマ選定するのが良さそうと思ってました😁

新しいイベントタイトルの解釈とテーマ選定

イベントタイトルの解釈

 show+case unlimited をまず解釈します。出すものとして unlimited show case で考えるといいかなと思いました。

 unlimited は "ここまで出来る kintone" がそのまま生きてる感じがします。幕張に舞台を移した最初の kintone hack では "どこまでも拡張していける"、"無限の可能性" という煽り言葉が入った口上でスピーカー達を呼び込んでますし。ここは大きく解釈し直す必要なさそうです。

 show case は文字通り自分のカスタマイズの中でもショーケースに入れて見せられるもの、そして勿論その頂上決戦に出して恥じないカスタマイズ、という感じでしょうか。ただ、hack という言葉からの変更はここに掛かってるので、もう少し考えてみます。"hackという、少しギークなイメージの言葉" という説明があるのと、"親しみ" という言葉がサイトで使われていたので、最終的により広く分かりやすく届けられる発表としてまとまると良いかなと思いました。

 ここまで来て、やっとテーマ選定です。

テーマ選定

 自分のこれまで kintone hack では、新しいテクノロジーを kintone と掛け合わせて、未来の kintone ユースケースを見せるようなテーマ選定をしてきていていました。この線で先の解釈にマッチできるテーマ候補もありました。これまでの自分の発表を見て頂いたことがある方で、それを見たいと言ってくださる方もいらっしゃるかなぁとも思いました(というか、結果的に発表後にまぁ予想通り言われたわけですが😅)。

 しかし、今回はエントリー理由の通り、王道テーマを JavaScript だけでやって、山下実践的なことも出来るよ、というのもお見せしたいなという気持ちがあって、シン・kintone 検索カスタマイズ を選択しました。"より広く分かりやすく届けられる発表としてまとまる" という自分の解釈にも合うかなと。言わずもがな、kintone に限らず検索って Web サービスのユーザーにとって馴染み深く、kintone でも検索がストレスになる体験をされてる方を沢山見てきていたので。

発表の組み立て

 テーマ選定と同時に発表の組み立ても考えました。(これは少々謎のままですが)予選には hack の名前を残しているので、予選に技術的なポイントの説明を詰め込むことで hack な雰囲気を出つつ、本戦は中身を見せることにフォーカスして show case 感を出そうかなと考えました。

 unlimited については、鉄板でありながら未解決とも言える課題に解決策のひとつを導くことでひとつ限界を超えた感を出せればと思いました。言われて気付けば仕組みは難しくない、ありそうでなかった、というのは今回のポイントですし、解決手段としてもオシャレかなぁとか思ったりしまして。

発表を終えて

やっぱり期待を外した?

 「やまりゅう、テーマ選定手抜いてきた😒」、「やまりゅうといえば、・・・新しいもの、とりあえず凄いもの見せてくれる、というのを今回も期待してた😞」といった旨のフィードバックを沢山頂きました😅

 前言通り、テーマ選定の段階で想定はしてまし、そういう期待があったとしても、あえて外しにいくのもいいかなと思ったのですが、「おめでとう🏆😃」と競るレベルで翌日これらを言われました。"グランプリだし、いいじゃん!🤭" と同時に "期待されてることがあるって、ありがたいことだなぁ🥹" と内心素直に喜んでいました。ありがとうございます!🙇

 ちなみに、今回のアイディアはシンプルですが、書いたコードの量は自分の hack の中では 1、2 を争います😂(なので、手は抜くどころか結構かかってまっせ👍)

 来年エキシビジョン枠、あるのかな、出るのかな。

 出ることになれば、やはりテクノロジーに振り切った kintone カスタマイズ、show case に入れますか。

でも、楽しんで頂けたかな

 発表内容については「あれ、使いたい」「売って」「標準機能にしてほしい」「予選のスライド見て納得」「確かに、言われてみれば、ありそうでなかった」等言って頂けましたし、Twitter も狙い通りの反応も頂けてるので、何だかんだ楽しんで頂けたかなと思っています😁

まとめ

 kintone hack から kintone show+case unlimited というイベント名の変更はあったけど、自分がやることは変わらない(変える必要はない)と、思いました。一方で、今回のようなテーマ選定を自分がする後押しにはなっていたと。

 ただ、周囲の期待はもっとテックっぽい感じに振ったものだったようでした 😅 来年出るなら、やっぱりそっち方面が良いのかな(アイディアは今年もあったし、来年も多分ある😁)、というお話でした。

最後に

 最後までお読み頂き、ありがとうございます。

 また、エントリーと発表に際して、応援してくださった皆さまに感謝します。ありがとうございました。

免責

 こちらは個人の意見で、所属する企業や団体は関係ありません。

kintone hack & show+case unlimited 2022 - シン・kintone 検索カスタマイズの技術面の説明

 kintone hack & show+case unlimited 2022 にて発表させてもらった シン・kintone 検索カスタマイズ の技術的な説明エントリです。

 ソースコードプラグインを公開してよ、という声が聞こえてきそうですが、何だかんだのコード量でメンテナンスやサポートが必要な内容になったので、ひとまずポイントを共有させてもらおうと思います。

 機能やスタイルを絞れば実装も難しくないので、参考にして頂ければ幸いです。

最大のポイント - IndexedDB の活用

 IndexedDB を知っていたことと、これを検索に使ってみようというアイディアが今回の hack ポイントです。

 IndexedDB API - Web APIs | MDN

 ブラウザにデータを保存する方法のひとつで、JavaScript でハンドリングできるので、kintone との相性が良いです。

 IndexedDB に事前取り込みしておくことによって、"高速な検索が可能になる"、"API のコール数を圧倒的に減らすことができる" 等のメリットがあります。セキュリティに関する考察含めて kintone hack 予選スライドで説明していますので、詳細はこちらをご覧ください。

その他の技術スタックや利用したライブラリ等

package.jsondependenciesdevDependencies を載せておきます。

"devDependencies": {
  "@cybozu/eslint-config": "^17.0.3",
  "@jgoz/esbuild-plugin-typecheck": "^2.0.0",
  "@kintone/dts-gen": "^6.1.10",
  "@types/autosuggest-highlight": "^3.2.0",
  "@types/lodash.debounce": "^4.0.7",
  "@types/luxon": "^3.0.1",
  "@types/node": "^18.8.2",
  "@types/react": "^18.0.21",
  "@types/react-dom": "^18.0.6",
  "esbuild": "^0.15.10",
  "eslint": "^8.24.0",
  "prettier": "^2.7.1",
  "typescript": "^4.8.4"
},
"dependencies": {
  "@emotion/react": "^11.10.4",
  "@kintone/rest-api-client": "^3.1.12",
  "autosuggest-highlight": "^3.3.4",
  "lodash.debounce": "^4.0.8",
  "luxon": "^3.0.4",
  "react": "^18.2.0",
  "react-dom": "^18.2.0"
}

簡単に用途等を説明します。

React, TypeScript

 まず、今回ベースに React & TypeScript を使いました。スタイルは Emotion 経由であてました。

 トランスパイルやファイルバンドルのためのビルダーは esbuild を使ってみました。型チェックには esbuild-plugin-typecheck というプラグインがあったので、これを使いました。

@kintone/rest-api-client

 実は今回は内包されている型定義を使うためだけに利用しています。Chrome Extension での利用も想定したいたのと、API の呼び出しは GET メソッドしか使わないので、必要な API 呼び出しメソッドは自前で fetch ベースで準備し直しました。あとは、rest-api-client がカバーしてない User API を使う可能性もあったためです。

lodash.debounce

 検索キーワード入力に合わせた検索ロジックの発火を、文字の入力単位ではなく、単語単位で起こす役割を担ってくれます。

autosuggest-highlight

 検索データに対してハイライトする部分をこれで実現しています。キーワードと一致した部分を今回は太字にして、どの部分がマッチしたのか分かりやすくしています。こんな感じです👇

ESLint, Prettier 関連

 文字通りです。@cybozu/eslint-config をベースに prettier 含めて設定しました。

ビルドの実行ファイル

import { build } from 'esbuild';
import { typecheckPlugin } from '@jgoz/esbuild-plugin-typecheck';

await build({
  entryPoints: ['./src/ts/customize.tsx'],
  outfile: './dist/js/customize.js',
  plugins: [typecheckPlugin()],
  bundle: true,
  minify: false,
  target: 'es2020',
  jsx: "automatic",
  jsxImportSource: "@emotion/react",
  watch: {
    onRebuild(error, result) {
      if (error) {
        console.error('watch build failed:', error);
      } else {
        console.log('watch build succeeded:', result);
      }
    },
  }
}).then(result => {
  console.log('watching...')
}).catch(() => process.exit(1));

コードの説明

 ポイントになる部分のコードをピックアップして説明したいと思います。

IndexedDB からのレコード取得

 IndexedDB のレコード取得は、カーソルスタイルです。順次レコード取得をして、途中で中断するようなことはできません。IDBCursor - Web APIs | MDN のページを見ながら、最終的に次のようなメソッドを準備しました。

 カーソルごとにレコードを都度 callback で取得できるようにしつつ、全件取得の結果を Promise 経由でメソッドの返り値とするようにしました。

export const getStoreRecordsWithCursor = <T extends IDBRecord>(
  db: IDBDatabase,
  storeName: string,
  direction: IDBCursorDirection,
  callback?: (arg: any) => any
) => {
  const transaction = db.transaction(storeName, 'readonly');
  const store = transaction.objectStore(storeName);
  const request = store.openCursor(null, direction);
  let results: T[] = [];
  return new Promise<T[]>(resolve => {
    request.onsuccess = event => {
      const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
      if (cursor) {
        Promise.resolve()
          .then(() => {
            if (callback !== undefined) {
              return Promise.resolve(callback(cursor)).then(result => {
                if (result) {
                  results = [...results, result];
                }
                return results;
              });
            }
            results = [...results, cursor.value];
            return results;
          })
          .then(() => cursor.continue());
      } else {
        resolve(results);
      }
    };
  });
};

全件取得からのフィルタリング

 検索条件にマッチしたレコードを抽出する部分です。

 先程の getStoreRecordsWithCursor を使います。カーソルでレコードを都度取得する方法をとりました。

 matchingMethod は次の 3 つのメソッド名そのものです。

  • includes: 中間一致
  • startsWith: 前方一致
  • endsWith: 後方一致
type MatchingMethod = 'includes' | 'startsWith' | 'endsWith';

const defaultFilter = (
  input: string,  // 検索キーワード
  record: IDBRecord,  // カーソルで取得した IndexedDB レコード
  searchableFields: string[],  // 検索対象フィールドのフィールドコード(配列)
  matchingMethod: MatchingMethod  // 検索時の照合方法
) => {
  const codes = searchableFields.filter(code => {
    const value = record[code];
    return typeof value === 'string' && value?.toLowerCase()[matchingMethod](input?.toLowerCase());
  });
  return codes.length > 0 ? { ...record, $label: codes } : null;
};

export const searchRecords = (
  db: IDBDatabase,
  input: string,
  searchableFields: string[],
  matchingMethod: MatchingMethod,
  callback?: (arg: any) => void
) => {
  return getStoreRecordsWithCursor(db, RECORDS_OBJECT_NAME, 'prev', (cursor: IDBCursorWithValue) => {
    const { value: record } = cursor;
    const ret = defaultFilter(input, record, searchableFields, matchingMethod);

    if (callback && ret) {
      callback(ret);
    }
    return ret;
  }).then(records => {
    return records;
  });
};

debounce 処理

 検索時にはキーボードで入力した 1 文字単位(タイプした単位)で React の onChange に引っかかってしまいますが、検索(レコード取得と絞込)は入力された単語単位でメソッドを呼び出したいところです(ボタンを押して検索を実行させる、という動きにはしたくないのは勿論のこと)。

 そこで登場するのが、debouncethrottle による処理です。lodash にこれらがあって、今回は debounce がマッチしたので、採用しました。

const [results, setResults] = React.useState<IDBRecord[]>([]);

// 盛大に中略

const debouncedSearchRecords = debounce(value => {
  searchRecords(inputValue, value, searchableFields, filterTypeIndex, matchingMethod).then(records => {
    setResults(records);
  });
}, 700);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const { value } = e.target;
  debouncedSearchRecords(value);
};

まとめ

 ポイントの部分は書き出せたと思うので、今回はこのくらいにさせて頂きたいと思います。どなたかの参考になれば、幸いです。

最後に

 最後までお読み頂き、ありがとうございます。

免責

 こちらは個人の意見で、所属する企業や団体は関係ありません。

VSCode Remote Containers で kintone のカスタマイズ開発環境を整える

(数年ぶりにこちらに記事書きます)

 

 ここ数年の kintone カスタマイズ開発にはエディタとして VSCode、開発環境として Docker を使っていたのですが、昨年リリースされた VSCode Remote Containers の Extension がこれにまたマッチしていい感じだったので、kintone カスタマイズ用に整理した kintone-customizationGitHub に上げてみました。

 

github.com

 

VSCode Remote Containers とは 

 まず VSCode Remote Containers のおさらいです。Docker コンテナに VSCode からアクセスできる VSCode Extension です。設定等の情報は公式で詳しい情報を確認できます。

 Docker コンテナの volume マウントでファイルがローカルと同期しており、ファイル操作はローカルで作業するのと Docker コンテナ中で作業するのには変わらず、実行できるコマンド等が ローカルか Docker コンテナ中かで異なってくるといった感じです。新たに準備するのではなくて既存のコンテナに対しても VSCode からアクセスして開発を進めるといったことも可能です。

https://code.visualstudio.com/assets/docs/remote/containers/architecture-containers.png

(公式ページにあるイメージです)

 新しく始める時の設定方法は 3 通りあるのですが、.devcontainer というディレクトリ中に、Dockerfile と devcontainer.json を作るのが基本になります。それぞれ、ベースになる Docker コンテナの設定と VSCode Remote Containers を通してアクセスするための設定ファイルです。今回もこの方法で必要ファイルを揃えました。

 

kintone-customization での始め方 

 手順は次のとおりです。

  1. ローカル PC に Docker をインストールする(バージョン等の条件はこちら
  2. VSCodeVSCode Remote Containers をインストールする(こちらからもインストールできます)
  3. kintone-customization をダウンロードする
  4. VSCode の左下のクイックアクションをクリックする

    https://code.visualstudio.com/assets/docs/remote/common/remote-dev-status-bar.png

    (緑の「> <」みたいなところです)
  5. Remote-Containers: Open Folder in Container を選択し、ダウンロードした kintone-customization フォルダ( .devcontainer を含む階層)で Open する

    f:id:yamaryu0508:20200331055243j:plain

    f:id:yamaryu0508:20200331055303j:plain

  6. コンテナが起動し、コンテナ中で作業する(初めての時にはイメージのビルドが走るので少し時間がかかりますが、一度イメージができてしまうと次からはすぐに立ち上がります)

    https://code.visualstudio.com/assets/docs/remote/containers/dev-container-progress.png

 

Docker コンテナが立ち上がると、VSCode で kintone-customization の中身が volume マウントされて左の File Explorer に現れ、Terminal も コンテナ中で実行できるようになりまます。

f:id:yamaryu0508:20200331014726j:plain

閉じるときは、始めた時に押した左下の緑ボタンから Close Remote Connection を選択します。

f:id:yamaryu0508:20200331055329j:plain

 

kintone-customization の中身 

 今回の kintone-customization の基本部分はこんな感じです。

  • Dockerfile: Amazon Linux 2 をベースに Node.js と Python 3 が実行できるイメージ
  • devcontainer.json:  8443, 8080, 443, 80 ポートを開放、この Remote Container でのみ利用する VSCode Extension をインストール

kintone のカスタマイズと言えば JavaScript ですので Node.js、AWS Lambda を使ったバッチ処理を開発するケースも想定して Python がベースで入るようにしました。また、開発用のローカル Devserver のためにポートを幾つか開放しています。そして、本コンテナアクセス時のみ利用する VSCode Extension が幾つか入るようになっています(詳しくは GitHub やソースをご覧ください)

 最後に、今回サンプルとして最上位階層に汎用的な package.json を置き、ES5、ES6、TypeScript の 3 種類で sample-project を今回入れてみたので、フォルダ階層や Webpack の設定等参考にして頂ければ幸いです。

 

 Docker 自体をちょっとした検証からガッツリ開発・サービス提供に利用されている方も多いと思いますが、VSCode を開発に利用されている方はその恩恵にもあずかれるので一度試されてみてはいかがでしょうか。

Atom用kintone-JavaScriptスニペット(Beta版)

ちょっと空いてのブログ更新となりましたが、Atom用kintone-JavaScriptスニペット(Beta版)をGistで公開しました。

 

f:id:yamaryu0508:20150426021639j:plain

 

スニペットというよりタブ補完に近いので「autocomplete-plus」と「autocomplete-snippets」のパッケージ追加が前提になります。

 

Beta版と言いつつ、kintone JavaScript APIと共に、イベント名も全て記載しています。あと幾らか個人的によく使う関数を記載しています。よく覚えていない関数のために都度ドキュメントを見る必要性はなくなり、多少は効率上がったかなぁと個人的には思っています。※Gistがうまく表示されない時にはこちらから直接ご覧ください。

 

gist.github.com

お試し頂いた方は、フィードバック頂けると嬉しいですm(__)m

Raspberry Piの設定【I2Cの有効化】

 Raspberry Pi 2が登場して、ほぼ同時期にアップデートされたRASPBIAN(2015-02-16版)ですが、こちらのOSでのI2Cの設定方法が以前の方法からDEVICE TREE(1, 2)というものを利用する方法に変更になりましたので、まとめておきたいと思います。こちらの「手順2 I2Cの設定」の置き換えでもあります。

 

手順 I2Cの設定

 /boot/config.txt/etc/modulesが対象ファイルで、いずれも書き足し(コメントアウト等はなし)ですので、以下のように一挙に設定します。はじめに、sudo suしておきましょう。

$ sudo su
# echo dtparam=i2c_arm=on >> /boot/config.txt
# echo i2c-dev >> /etc/modules
# reboot

最後に再起動して設定は終了です。その他の設定は、現状以前と同様に行えます。

kintoneは“技術者を育てる”開発プラットフォームだ

 先日ITproに掲載された「kintoneは“働くママによりそう”開発プラットフォームだ」を読んで、ふと自分なりのタイトルがふと頭をよぎりました。FBのシェアで少し触れていますが、「技術者を育てる」というのもkintoneの枕詞として「働くママによりそう」「開発者とユーザーを繋ぐ」に加わっても良いんじゃないかなぁと思ったのです。「技術者自身が楽しく作り、kintoneを通じて成長していける」、そしてこれがまた先の2要素にもフィードバックされると考えます。私自身はkintoneに出会ってちょうど2年、kintoneエバンジェリストにご指名を頂いてからもうすぐ1年目ということで、これまでの自身の取組みと共に、こんなタイトル付けができると感じた理由を今回「技術的な側面を中心」にまとめたいと思います。

 

kintoneとは 

 まずはkintoneのおさらいです。私なりには「kintone Café 福岡 Vol.1」で大方まとめさせてもらっていましたが、大きく認識も変わっていませんので大事なところを掻い摘んで振り返りたいと思います。

 いわゆる概要はこうですね。「チームを強くするツール」を提供するサイボウズさんが提供する「データ」「ワークフロー」「コミュニケーション」を構成要素とする、誰でも簡単に業務アプリが作れてしまう「ファストシステム(Web−DB)」です。

 

そして、もたらされるものは、(ここも大きく変わっておらず)「ユーザ」、「プロバイダ(営業・技術)」の側面で次のように考えています。タイトルにも拘りがあって、それぞれが「kintoneを使って嬉しく、楽しいこと」です!「開発者とユーザーを繋ぐ」ことが出来る所以の一端でもあると思います。

 

自分なりにまとめると、・・・kintoneは簡単なアプリ作成からはじまり、「データ」、「ワークフロー」、「コミュニケーション」という機能の疎密も絶妙にコントロール出来ますし、カスタマイズ機能も強力です。一定の制約はあるにしても手直しにも強いので、案件クロージング・アプリ提供・享受にかかるコストはやはり他の様々な方法と比較しても低く、「ICTを使った業務改善」には最強・最短のツールだと思います。

 

kintoneは“技術者を育てる”開発プラットフォーム【考察・まとめ】

 自身の振返りは長くなるので、先にタイトルたる所以の考察とまとめです。

 kintoneはもともと優れた設計のREST APIやパワフルなJavaScriptカスタマイズ機能を有し、今日ではkintone Caféやcybozu.com developer networkといったコミュニティも充実してきたことで学習コストも低くなってきました。kintoneを通じて案件に対応しようとする時に技術者自身も楽しく作りながら、それに伴う成長の実感を得やすい開発プラットフォームになっています。「(技術者も)ユーザーの反応をファストに体現でき、納得・満足してもらえて」というサイクルを生み出しやすいというのが本質だと思いますが、技術マターで少し項目を挙げます。

  • REST API 今では当たり前のREST/JSONという形式のAPIを提供するサービスの中でも優れた設計・バリエーションを有するので、簡単なリクエストからスタートしても使いこなそうとする過程でデータの取扱、RESTfulサービス周辺の技術的な知識・勘が養われる。JSONによるテキストベースのデータだけでなくファイルも取扱の対象となっている。勿論全知全能ではなくとも、全体としてv1のままここまで進化できているのも類を見ない

  • JavaScript/CSS データやデータアクセス権限等を扱うREST APIだけでなく、その気になれば画面もパワフルにカスタマイズできるJavaScriptCSSを読み込めるため、特にJSそのものや周辺ライブラリに触れるきっかけになり、kintoneに適用して試すという使い方もできる。kintone.proxy()というJSから外部サービスにAPIリクエストできるメソッドも更に可能性を広げてくれる

  • 短いアップデートサイクル kintoneが1〜2ヶ月に1回というハイペースでアップデート・進化していくので、それをフォローするだけでもアイディア・技術の幅を広げることができる。また、今まで出来なかったことが次のアップデートで出来るようになるかもという期待・ワクワク感、出来るようになるはずという安心感が常にある
  • 連携サービス kintone連携サービスまで手を伸ばすことで、周辺技術習得のきっかけが得られる。サードパーティが提供する連携サービスを使いこなすことも含まれるが、連携プログラムを別クラウドに設置する必要があればAWS等で構築したり、CMS連携の際にはMovableTypeWordPressの知識を要したり、directのようなチャットと便利に連携させたい場合にはNode.jsやHubotに触れたりといったこと具合になる

f:id:yamaryu0508:20150214165403j:plain

  • kintone Café まだkintoneに触れたことの無い方から、より高度なカスタマイズを行いたいと考えているプロフェッショナルの方まで幅広い層を対象に、 楽しく学び・教え合うことで、kintoneの魅力や活用法をみんなで共有するための勉強会コミュニティ(以上、kintone Café 理念)- 「kintone Café」(kintone Café運営事務局)の全国拡大に伴って、kintoneを学びながら、情報交換できる場が身近になってきた。2013年12月の「kintone Café at 札幌 Vol.1」を起源として、昨年2014年は各地で23回の開催回数を刻んでいる

f:id:yamaryu0508:20150213233623p:plain

  • cybozu.com developer network 開発者(技術者)同士を繋ぐコミュニティ「cybozu.com developer network」(サイボウズ運営)を通じてkintone界隈の開発者間でコミュニケーションし、疑問や課題を解決するためのヒントや具体策を得ることができる。開設半年で1,000IDを超え、2/27(金)には同コミュニティのリアル版?である「kintone devCamp Vol.1」の開催も予定されている

f:id:yamaryu0508:20150213233858p:plain

 

後に触れますが、私は技術面で「REST API」と「JavaScript」はkintoneを通して学ばせてもらいました。また、いまだにDBのコマンドは打ったことはありませんが、これらの要素は私にファストに大きな成長をもたらしてくれたと思います。そして、ユーザーと対面しながらのモノづくりの楽しさをkintoneの醍醐味として覚えました。

 いかがでしょうか?kintoneを通じて、技術者としての成長がユーザーの課題解決に近づくことに繋がり、・・・「開発者とユーザーを繋がり」、「働くママによりそう」ことにも繋がってきそうなイメージできそうでしょうか?いきなり「働くママによりそう」ところまでは想像が追いつかないかもしれませんが、「開発者とユーザーを繋ぐ」まではイメージしてもらえるのではないでしょうか?実は私自身が実感として語れる部分も「開発者とユーザーを繋ぐ」までで、「働くママによりそう」は今まさにジョイゾーメンバーが新たな切り口で実践中です。

 私の説明がイマイチということもあるかもしれませんが、kintoneは自分で触って、好きになっていくツールでもあると思いますので、興味を持って頂いた方は是非イジって実感してもらえればと思います。

 

kintoneは“技術者を育てる”開発プラットフォーム【自身の振返り】

 私はタイトル通り自身がkintoneを通じて技術的にも成長させてもらった存在だと思っています。また表現を増やすなら「kintoneは最高の教材」でもありました。

 ということで、自分のkintoneへの取組みの振返りたいと思います。

 

 kintoneに出会ったのはニシム電子工業サイボウズシルバーパートナー)時代M2Mに関わり出して3年目の2013年の2月のこと。M2M(Machine to machine)で連携できるサービスを探していた先輩がkintoneを見つけてきました。私自身の最初の印象は「NAS+ExcelGoogleスプレッドシートとかで十分じゃないですか?」でした(^^; まずは、kintoneのパートナー認定資格であるCD(Cloud Developer)の講習会に行きました。APIはもとよりDBはおろかプログラムも学生時代研究用のC言語Javaをちょっとかじった程度から7年経っているという状況からのスタートで、PHPkintoneのAPIをリクエストするということを通して「REST API」を学び、kintone上の画面をカスタマイズするということを通して「JavaScript」、「CSS」を学んでいきました。kintoneのREST APIは当時ドキュメントが申込制でググって情報が出てくるというものでもなかったので、はじめ苦労しました。ただ、いまだアプリを作るのに、このように多少のコードは書くようになりましたが、DBのコマンドを打ったことはありません。kintoneがあったからです。

 

kViewerTimelineカスタマイズで、「kintoneへの取組み振返り」)

 

しかし、kintone最初の案件がいきなり太陽光とある意味得意分野で、しかもM2Mに発展(クラウドWatch)し、M2M2kintoneな案件となれば、熊本や愛媛の農場に行ったり、宮崎や鹿児島のインターネットも届かなさそうな現場に足を運んでは、客先で作ったアプリを要望が出たその場で修正したり、カスタマイズコードを復路で修正したりということをやっていました。

 

 振り返れば、・・・まさしくkintoneを通じた「技術者とユーザーの繋がり」を楽しみながら、kintoneを便利に使って「お客さまに価値提供する(課題解決にあたる)」、「技術的に必要な機能を追加する」プロセスによる多くの学びと成長を実感し始めた時だったのだと思います。ちなみに、一応企画と営業の部門にいましたので、カスタマイズのやりすぎ感が否めませんが、kintoneであれば営業でも技術スタッフの帯同をお願いして要件を詰めるといった作業をスキップできるというのは体現できたのではないかと思います。

 

 そして、kintone等を扱う開発者コミュニティサイト「cybozu.com developer network」の開設(2014年4月)と同時に選出されたkintoneエバンジェリストにご指名を頂いたことをきっかけに自身でもkintoneの勉強会コミュニティであるkintone Caféの福岡版として「kintone Café 福岡」のVol.1〜3を主催し、各地のkintone Café沖縄のkintothon等遠方のkintoneイベントにも度々顔を出すようになりました。普段の福岡・九州という自身の枠の中での活動が、こういったコミュニティを通じて一気に刺激と広がりを帯び出した時です。

 

 このコミュニティを通じたご縁で、私はニシム電子工業から九州電力の電力保安通信(法律で義務付けられている電力会社専用の社内通信網)を司る電子通信部門に復帰して3ヶ月後、・・・「全てのサイボウズプロダクトに精通する」四宮さんが代表を務めるジョイゾーサイボウズSIパートナー)へと転職します。四宮さんは東京から福岡までわざわざ話に来てくれましたが、「Enjoy IT, Enjoy LIFE」というのスローガン、「システム39」という新しいSI・サービスの形を体現されているところに勝手に共感していましたので、あまり多くの説明を求め合うこともなく決断していました。これは「kintone転職」ですね^^ 「光ファイバ心線貸しの事業移管」といった重要ミッションへの取り組みの最中「送別会」ではなく「壮行会」として送り出してくれた九州電力電子通信部門の仲間への感謝も絶えないところです。SIerとして、kintoneを通じて「自身もユーザーさんもみんなハッピー」を共有できるような取組みをしたい、と上京し丁度3ヶ月が経ったところです。これからもkintoneがファストに進化していくように私もここまでに述べたコミュニティと共にまだまだ成長していきます!

 

 結びにかかりますが、・・・kintoneへの取組みによる成長、そのポイントは「優れた機能を備えたサービス」の上に形成された「コミュニティ」、「繋がり」、「楽しさとその共有」のサイクルに入り込むことだとまとめながら感じました。読んで頂いた方の中で、「kintone触って業務改善に挑んでみよう!そして、そこにある楽しさを感じ共有してみたい」と思ってもらえる方がいらっしゃれば幸いですし、実際に触れてみて、コミュニティに加わってもらい、どこかのCaféでお会いできれば最高だなと思っています!

 

p.s. こういうまとめでは良いことばかりだけでなく、残念なところにも触れた方が良いと思いますが、kintoneの場合はだいたいアップデートとコミュニティによってフォローされることがわかっていて、気にしたことありませんので、無しで(笑)