【やってみた】kintoneアプリ「値を判定してサブテーブルへ値をコピー」

cybozu developer network の kintone カスタマイズ フォーラム に上がった質問を考えてみました。

* [値を判定してサブテーブルへ値をコピー](https://developer.cybozu.io/hc/ja/community/posts/360047761231)

# アプリの仕様
質問内容を見て下記の様に想定してみました。

## 要求仕様

フィールドの値を判定して、それが0（ゼロ)以外の場合にテーブルに追加したい。

## 要件定義

* 数値フィールドの値を取得する。
* 値がゼロ、未入力の時はテーブルに追加しない。
* 数字以外はkintoneの標準の機能でエラーで保存出来ない。
* 値がゼロ、未入力でも保存は出来る。
* 編集モードで数値が入力されたら、テーブルに追加する。

# 動作画面

## フォーム画面

<img width="1035" alt="スクリーンショット 2019-07-11 20.35.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/982da8bb-f879-d1c4-a9ef-a1accf9e82e5.png">

## 新規入力して保存押下

<img width="626" alt="スクリーンショット 2019-07-11 20.37.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/c6d4d0ae-c024-3cb4-1d99-20ab5c167d33.png">

 <img width="848" alt="スクリーンショット 2019-07-11 20.36.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/0d5e006e-f14c-a155-4ae7-1aeff5d0901e.png">

## 保存後

 <img width="627" alt="スクリーンショット 2019-07-11 20.37.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/1558a7bf-816f-0b54-20d3-622806a5afa9.png">

## 編集入力

<img width="697" alt="スクリーンショット 2019-07-11 20.38.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/5377a034-229c-5183-1206-7782272a56f3.png">

## 編集入力後の保存

<img width="639" alt="スクリーンショット 2019-07-11 20.38.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/29a46535-cf73-fd5a-9bcc-11d19b3a3990.png">

# コードについて

関数のテストコードです。

```test.sample.js
'use strict';
const assert = require('assert');
const commonModule = require('../src/common');

describe('オブジェクト配列をフィルターする', () => {
    let aryFields;
    let recrods;
    const undef = undefined
        aryFields = ['num01', 'num02', 'num03', 'num04']
        recrods = {
            num01: {
                value: "0"
            },
            num02: {
                value: "100"
            },
            num03: {
                value: ""
            },
            num04: {
                value: undef
            }
        }
    })
    it('フィルターした結果の配列が返る', () => {
        assert(commonModule.filterFields(aryFields, recrods).length === 1)
    })
    it('フィルターした結果の配列が返る2', () => {
        assert(commonModule.filterFields(aryFields, recrods)[0] === "num02")
    })

})
```


関数の一部です。

フィールドの値を判別しています。
kintoneのレコードを配列で渡して、判別結果を array.filter で処理しています。

```common.js
/**
* フィールドコードをフィルターして返す
* @param {Array} fieldCodes フィールドコードの配列
* @param {object} records kintoneフィールドのハッシュ
* @return {Array} filters フィルター後のフィールドコードの配列
*　*/
exports.filterFields = (fieldCodes, records) => {
  const filters = fieldCodes.filter(fieldCode => {
    if (records[fieldCode].value) { // null or undefined 以外
      if (parseInt(records[fieldCode].value) !== 0) { // 数値変換した結果が0以外
        return true
      }
    } else {
      return false
    }
  })
  return filters
}

```

メインの処理です。
途中でフィールド情報を取得したり、キャンセル処理をする為に、Promiseで実装しています。
テストコードを書く為に、大した処理もしてませんが処理の大部分を関数としています。

```main.js
import Common from './common'
import Swal from 'sweetalert2'

/**
 * 値をチェックする対象のフィールドコード配列
 */
const fieldsDiv = [
  {
    fieldCode: 'num01',
  },
  {
    fieldCode: 'num02',
  },
  {
    fieldCode: 'num03',
  },
  {
    fieldCode: 'num04',
  },
  {
    fieldCode: 'num05',
  },
  {
    fieldCode: 'num06',
  }
]

const HANDLE_EVENT = [
  'app.record.create.submit',
  'app.record.edit.submit',
  'mobile.app.record.create.submit',
  'mobile.app.record.edit.submit'
]
/** @type { array } HANDLE_EVENT */
kintone.events.on(HANDLE_EVENT, async (event) => {
  const record = event.record
  // console.log(record)

  // データ取得対象のフィールドコードの配列をセットする
  const aryFields = Common.destructFields(fieldsDiv, 'fieldCode')

  // 0と空白以外のフィールドコードを絞り込む
  const filterFields = Common.filterFields(aryFields, record)

  // フィールド情報取得
  let kintoneConfig = {}
  await kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', {"app": kintone.app.getId()}).then( (resp) => {
    console.log(resp);
    kintoneConfig = resp.properties
  }), (err) => {
    console.log(err)
  }   

  // kintoneのテーブルレコードを生成
  const kintoneTableRows = filterFields.map(field => {
    return {
      "num": {"type": "NUMBER", "value": record[field].value},
      "category": {"type": "SINGLE_LINE_TEXT", "value": kintoneConfig[field].label},
    }
  })
  const kintoneTable = Common.createTableRows(kintoneTableRows, 'Table')

  return Swal.fire({
    title: 'info',
    text: "レコード保存の際に値をテーブルにセットします",
    type: 'info',
    showCancelButton: true,
    confirmButtonText: 'OK'
  }).then((result) => {
    if (result.value) { // OKボタン押下
      return Swal.fire({
        title: '保存',
        text: '値をテーブルにセットしました',
        type: 'success',
        confirmButtonText: 'OK'
      }).then(() => {
        record.Table = kintoneTable.Table
        return event  
      })
    } else { // 処理をキャンセルした
      console.log('do cancel')
      return Swal.fire('処理をキャンセルします').then(() => {
        console.log('cancel done')
        return false
      })
    }
  }).catch((error) => {
    event.error = 'レコード保存時にエラーが発生しました'
    return Swal.fire('エラーが発生しました' + error)
  })
});

```

# 環境設定

* Babel7 + Webpack4 でコンパイル、バンドルしています。
* Vue.js のテンプレートを利用してプロジェクトフォルダを生成しています。
* テストは Mocha + power-assert を利用しました。

`$ vue init webpack-simple プロジェクト名 `

```package.json
{
  "name": "app285",
  "description": "A Vue.js project",
  "version": "1.0.0",
  "author": "Kazuhiro Yoshida <sy250f@gmail.com>",
  "license": "MIT",
  "private": true,
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
    "test": "mocha --require intelli-espower-loader -w"
  },
  "dependencies": {
    "vue": "^2.5.11"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ],
  "devDependencies": {
    "@kintone/dts-gen": "^1.0.3",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.6.0",
    "babel-preset-stage-3": "^6.24.1",
    "cross-env": "^5.0.5",
    "css-loader": "^0.28.7",
    "file-loader": "^1.1.4",
    "intelli-espower-loader": "^1.0.1",
    "mocha": "^6.1.4",
    "node-sass": "^4.5.3",
    "power-assert": "^1.6.1",
    "sass-loader": "^6.0.6",
    "typescript": "^3.5.2",
    "vue-loader": "^13.0.5",
    "vue-template-compiler": "^2.4.4",
    "webpack": "^3.6.0",
    "webpack-dev-server": "^2.9.1"
  }
}
```

```webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['babel-polyfill','./src/main.js'],
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'build.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ],
      },
      {
        test: /\.scss$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'sass-loader'
        ],
      },
      {
        test: /\.sass$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'sass-loader?indentedSyntax'
        ],
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
            // Since sass-loader (weirdly) has SCSS as its default parse mode, we map
            // the "scss" and "sass" values for the lang attribute to the right configs here.
            // other preprocessors should work out of the box, no loader config like this necessary.
            'scss': [
              'vue-style-loader',
              'css-loader',
              'sass-loader'
            ],
            'sass': [
              'vue-style-loader',
              'css-loader',
              'sass-loader?indentedSyntax'
            ]
          }
          // other vue-loader options go here
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file-loader',
        options: {
          name: '[name].[ext]?[hash]'
        }
      }
    ]
  },
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    },
    extensions: ['*', '.js', '.vue', '.json']
  },
  devServer: {
    historyApiFallback: true,
    noInfo: true,
    overlay: true
  },
  performance: {
    hints: false
  },
  devtool: '#eval-source-map'
}

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  // http://vue-loader.vuejs.org/en/workflow/production.html
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}
```


# まとめ

* フォーム未入力時の値判別がなかなかに難しかったです。
* JSのテストを初めて書いてみました。 power-assert はエラー内容が分かりやすくて助かった。テストは書くべきですね。
* Vue.js の テンプレートが環境構築に便利でした。


# 参考サイト

* [（小技）undefined と 空文字のスマートな if 文判定
](https://developer.cybozu.io/hc/ja/articles/201718170)
* [https://developer.cybozu.io/hc/ja/community/posts/115017919303](https://developer.cybozu.io/hc/ja/articles/201718170)
* [環境構築弱者でも簡単に始められるテスト駆動開発〜mocha + power-assert でJavaScriptのテストを書く〜](https://ky-yk-d.hatenablog.com/entry/2018/06/23/130754)
* [ES2015以降のJavaScriptでObjectのkeyに変数を使う](https://qiita.com/kmagai/items/95481a3b9fd97e4616c9)