React + kintone UI Componentなプラグイン開発環境

kintoneライクなスタイルシートを利用してプラグインの設定画面を作ってきたけれど,テーブルなどを実装するのが辛いと感じている方は多いのではないでしょうか. React + kintone UI Componentを使えば,複雑なUIも簡単に実装できます.
通常のJSカスタマイズでReactを使いたい方は,こちらの記事などが参考になるかと思います.

開発準備

React + kintone UI Componentで書いたコードを,自動で「コンパイル→プラグインzip化→kintoneにアップロード」する開発環境を整えます.

@kintone/create-pluginのインストール

グローバルに,@kintone/create-pluginをインストールします. インストール済みの場合はスキップしてください.

npm i -g @kintone/create-plugin

プラグイン開発ディレクトリの作成

@kintone/create-pluginのコマンドで,プラグイン開発ディレクトリを作成します.

create-kintone-plugin sample


kintoneプラグインのプロジェクトを作成するために、いくつかの質問に答えてください :)
では、はじめましょう!

  
? プラグインの英語名を入力してください [1-64文字] sample
? プラグインの説明を入力してください [1-200文字] sample
? 日本語をサポートしますか? Yes
? プラグインの日本語名を入力してください [1-64文字] (省略可) 
? プラグインの日本語の説明を入力してください [1-200文字] (省略可) 
? 中国語をサポートしますか? No
? プラグインの英語のWebサイトURLを入力してください (省略可) 
? プラグインの日本語のWebサイトURLを入力してください (省略可) 
? モバイルページをサポートしますか? No
? @kintone/plugin-uploaderを使いますか? Yes

プラグイン開発ルートディレクトリへの移動

作成したディレクトリに移動します.

cd sample

依存パッケージのインストール

依存パッケージをインストールします.

npm i -D webpack webpack-cli babel-loader @babel/core @babel/preset-env @babel/preset-react css-loader style-loader @kintone/webpack-plugin-kintone-plugin

npm i react react-dom @kintone/kintone-ui-component

プラグイン開発ディレクトリ(sample/)内のファイルの書き換え

プラグイン開発ディレクトリ(sample/)内のファイルを書き換えていきます.

「/package.json」の書き換え(scriptという箇所)

・/package.json

{
  ...
  "scripts": {
    "start": "node scripts/npm-start.js",
    "upload": "kintone-plugin-uploader dist/plugin.zip --watch --waiting-dialog-ms 3000",
    "develop": "webpack --mode development --watch",
    "build": "webpack --mode production",
    "lint": "eslint src/*/*"
  },
  ...
}

「/.eslintrc.js」の書き換え

・/.eslintrc.js

'use strict';

module.exports = {
  extends: [
    '@cybozu',
    '@cybozu/eslint-config/globals/kintone',
    '@cybozu/eslint-config/presets/react'
  ]
};

「/webpack.config.js」の新規作成

・/webpack.config.js

const path = require('path');
const KintonePlugin = require('@kintone/webpack-plugin-kintone-plugin');

module.exports = {
  entry: {
    desktop: './src/desktop/index.jsx',
    config: './src/config/index.jsx'
  },
  output: {
    path: path.resolve(__dirname, 'plugin'),
    filename: '[name].js'
  },
  module: {
    rules: [{
      test: /(\.js|\.jsx)$/,
      exclude: /node_modules/,
      use: [{
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env', '@babel/react']
        }
      }]
    }, {
      test: /\.css$/,
      use: [{loader: 'style-loader'}, {loader: 'css-loader'}]
    }]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  plugins: [
    new KintonePlugin({
      manifestJSONPath: './plugin/manifest.json',
      privateKeyPath: './private.ppk',
      pluginZipPath: './dist/plugin.zip'
    })
  ]
};

「/plugin」ディレクトリの新規作成

「/plugin」ディレクトリを新規作成します.

「/plugin/manifest.json」の新規作成

・/plugin/manifest.json

{
  "manifest_version": 1,
  "version": 1,
  "type": "APP",
  "desktop": {
    "js": [
      "desktop.js"
    ]
  },
  "icon": "icon.png",
  "config": {
    "html": "config.html",
    "js": [
      "config.js"
    ]
  },
  "name": {
    "en": "sample"
  },
  "description": {
    "en": "sample"
  }
}

「/plugin/config.html」の新規作成

・/plugin/config.html

<div id="config-root"></div>

アイコンファイルの移動

アイコンファイルの「/src/image/icon.png」を「/plugin/icon.png」への移動します.

「/src」ディレクトリの中身の削除

残った「/src」ディレクトリの中身を削除します.

「/src/config」ディレクトリの新規作成

「/src/config」ディレクトリを新規作成します.

「/src/config/index.jsx」の新規作成

「/src/config/index.jsx」を新規作成します.

「/src/desktop」ディレクトリの新規作成

「/src/desktop」ディレクトリを新規作成します.

「/src/desktop/index.jsx」の新規作成

「/src/desktop/index.jsx」を新規作成します.

よく使うコマンド

npm run build

製品版「/dist/plugin.zip」を作成します.

npm run upload

kintoneに「/dist/plugin.zip」をアップロードします.

npm start

ファイルを監視し,自動で「コンパイル→プラグインzip化→kintoneにアップロード」します.
正常に動作しない場合は,一度「npm run build」を実行した後,「npm start」を実行してください.

サンプル

プラグイン設定画面にテーブルを実装します. 入力したデータを,一覧画面のメニュー部のドロップダウンの選択肢に利用します.

・/src/config/index.jsx

import React from 'react';
import {render} from 'react-dom';
import {Table, Text, Button} from '@kintone/kintone-ui-component';

(PLUGIN_ID => {
  class App extends React.Component {
    constructor(props) {
      super(props);
      this.defaultRowData = {text: ''};
      this.state = {
        data: props.data ? JSON.parse(props.data) : [this.defaultRowData]
      };
    }
    render() {
      return (
        <div>
          <Table
            columns={[{
              header: 'Text',
              cell: ({rowIndex, onCellChange}) =>(
                <Text
                  value={this.state.data[rowIndex].text}
                  onChange={value => onCellChange(value, this.state.data, rowIndex, 'text')}
                />
              )
            }]}
            data={this.state.data}
            defaultRowData={this.defaultRowData}
            onRowAdd={({data}) => {
              this.setState({data});
            }}
            onRowRemove={({data}) => {
              this.setState({data});
            }}
            onCellChange={({data}) => {
              this.setState({data});
            }}
          />
          <Button
            text="submit"
            onClick={() => {
              kintone.plugin.app.setConfig({
                data: JSON.stringify(this.state.data)
              });
            }}
          />
        </div>
      );
    }
  }

  render(
    <App data={kintone.plugin.app.getConfig(PLUGIN_ID).data} />,
    document.getElementById('config-root')
  );
})(kintone.$PLUGIN_ID);

・/src/desktop/index.jsx

import React from 'react';
import {render} from 'react-dom';
import {Dropdown} from '@kintone/kintone-ui-component';

(PLUGIN_ID => {
  class App extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        items: [{
          label: '-----',
          value: ''
        }].concat((props.data ? JSON.parse(props.data) : []).map(({text}) => ({
          label: text,
          value: text
        }))),
        value: ''
      };
    }
    render() {
      return (
        <Dropdown
          items={this.state.items}
          value={this.state.value}
          onChange={(value) => {
            this.setState({value});
          }}
        />
      );
    }
  }

  kintone.events.on(['app.record.index.show'], () => {
    render(
      <App data={kintone.plugin.app.getConfig(PLUGIN_ID).data} />,
      kintone.app.getHeaderMenuSpaceElement()
    );
  });
})(kintone.$PLUGIN_ID);

参考ページ