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

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


アプリの仕様

質問内容を見て下記の様に想定してみました。


要求仕様

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


要件定義


  • 数値フィールドの値を取得する。

  • 値がゼロ、未入力の時はテーブルに追加しない。

  • 数字以外はkintoneの標準の機能でエラーで保存出来ない。

  • 値がゼロ、未入力でも保存は出来る。

  • 編集モードで数値が入力されたら、テーブルに追加する。


動作画面


フォーム画面

スクリーンショット 2019-07-11 20.35.11.png


新規入力して保存押下

スクリーンショット 2019-07-11 20.37.08.png

スクリーンショット 2019-07-11 20.36.51.png


保存後

スクリーンショット 2019-07-11 20.37.26.png


編集入力

スクリーンショット 2019-07-11 20.38.05.png


編集入力後の保存

スクリーンショット 2019-07-11 20.38.35.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 の テンプレートが環境構築に便利でした。


参考サイト