[kintone×gas]スプレッドシートに入力した情報からフィールドを登録する_4
前回まで
↓の内容をやった。記事はこちら
・ダイアログに入力したデータを受け取れることを確認
・スプレッドシートの定義ファイル作成
・処理の大まかな流れを作成
リクエストボディを作る関数の実装
前回のコードの中で、getRequestBody 関数の中身を書いていく。
// リクエストボディを作成する getRequestBody 関数に必要な情報を渡す。
const [fieldRegistrationBody, layoutBody] = getRequestBody(appId, values, definitions);
const getRequestBody = (appId, values, definitions) => {
const COLUMNS = definitions.COLUMNS;
const FIELD_TYPES = definitions.FIELD_TYPES;
// フィールド登録用のリクエストボディ
const fieldRegistrationBody = {
"app": appId,
"properties": {}
}
// フィールドレイアウト更新用のリクエストボディ
const layoutBody = {
"app": appId,
"layout": []
}
// 行番号(A列)の値で昇順にソート
values.sort((a, b) => {
return a[0] - b[0];
});
// サブテーブルの行番号とフィールドコードを代入する変数
const subtableRowNoObj = {};
// サブテーブル以外の行番号を代入する変数
const normalFieldRowNo = [];
// サブテーブル以外のフィールド情報を代入する変数
const normalFieldValues = [];
values.forEach(e => {
// SSに入力されたフィールドコード(C列)を代入
const fieldCode = e[COLUMNS.fieldCode];
// SSに入力されたフィールドタイプ(D列)の情報を英語に変換して代入
const fieldType = FIELD_TYPES[e[COLUMNS.fieldType]];
if(fieldType === FIELD_TYPES.サブテーブル) {
// サブテーブルの行番号が重複していないかチェック
if(Object.keys(subtableRowNoObj).includes(String(e[COLUMNS.row]))) {
throw new Error(`フィールドコード:${fieldCode}の行番号が重複しています。サブテーブルは行番号の重複ができません。`);
}
// フィールド登録用リクエストボディにサブテーブルの情報を追加
fieldRegistrationBody.properties[fieldCode] = {
"type": fieldType,
"code": fieldCode,
"label": e[COLUMNS.fieldLabel],
"noLabel": e[COLUMNS.noLabel] === "" || e[COLUMNS.noLabel] === "false" ? false : true,
"fields": {} // fieldsにはサブテーブル内のフィールド情報を追加していく
}
// サブテーブルの行番号とフィールドコードを代入
subtableRowNoObj[String(e[COLUMNS.row])] = fieldCode;
// レイアウト更新用リクエストボディにサブテーブルの情報を追加
layoutBody.layout[e[COLUMNS.row] - 1] = {
"type": FIELD_TYPES.サブテーブル,
"code": fieldCode,
"fields": [] // fieldsにはサブテーブル内のフィールド情報を追加していく
}
} else {
// サブテーブル以外のフィールドの処理
// classに定義した選択肢が必要か判定する関数でチェック。booleanで返却
const needChoices = definitions.checkNeedOpitons(fieldType);
// 選択肢が必要なフィールドでSSに入力がなければエラーを返す
if (needChoices && e[COLUMNS.choices] === "") throw new Error(`フィールドコード:${fieldCode}は選択肢の入力が必要です`);
// リンクフィールドでリンクタイプの指定がなければエラーを返す
if (fieldType === FIELD_TYPES.リンク && e[COLUMNS.linkType] === "") throw new Error(`フィールドコード:${fieldCode}ははリンクタイプの指定が必要です。`);
if (fieldType === FIELD_TYPES.計算 && e[COLUMNS.formula] === "") throw new Error(`フィールドコード:${fieldCode}は計算式が必要です。`);
// サブテーブル以外のフィールドの情報を配列に追加
normalFieldValues.push(e);
// サブテーブル以外のフィールドの行番号を配列に追加。レイアウト用のリクエストボディ作成に利用
normalFieldRowNo.push(String(e[COLUMNS.row]));
}
});
// サブテーブル以外フィールドの行番号の重複を排除
const uniqueNormalFieldRowNo = Array.from(new Set(normalFieldRowNo));
// サブテーブルの行番号を配列で代入
const subTblRowNo = Object.keys(subtableRowNoObj);
// uniqueNormalFieldRowNo からサブテーブルと同じ行番号を排除する
const rowNoWithoutSubTbl = uniqueNormalFieldRowNo.filter(i => subTblRowNo.indexOf(i) === -1);
// サブテーブルに含まれないフィールドのレイアウト用リクエストボディ
rowNoWithoutSubTbl.forEach(e => {
layoutBody.layout[e - 1] = {
"type": "ROW",
"fields": []
}
});
// 通常フィールドのリクエストボディを作成
normalFieldValues.forEach(e => {
const fieldCode = e[COLUMNS.fieldCode];
const fieldType = FIELD_TYPES[e[COLUMNS.fieldType]];
const requestBody = {
"type": fieldType,
"code": fieldCode,
"label": e[COLUMNS.fieldLabel],
"required": (e[COLUMNS.isRequired] === "" || e[COLUMNS.isRequired] === "false") ? false : true,
"unique": e[COLUMNS.isUnique] === "" || e[COLUMNS.isUnique] === "false" ? false : true,
"noLabel": e[COLUMNS.noLabel] === "" || e[COLUMNS.noLabel] === "false" ? false : true,
};
// 選択肢が必要なフィールドかチェック
const needChoices = definitions.checkNeedOpitons(FIELD_TYPES[e[COLUMNS.fieldType]]);
if (needChoices && e[COLUMNS.choices] !== "") {
// 入力された選択肢をカンマ区切りで分割
const options = e[COLUMNS.choices].split(",");
const optionsObj = {};
for (let i = 0; i < options.length; i++) {
optionsObj[options[i]] = {
"label": options[i],
"index": i
}
}
// リクエストボディに選択肢を追加
requestBody.options = optionsObj;
}
// リンクフィールドの場合リンクのタイプをリクエストボディに追加
if (fieldType === FIELD_TYPES.リンク) requestBody.protocol = e[COLUMNS.linkType];
// 計算フィールドの場合計算式をリクエストボディに追加
if (fieldType === FIELD_TYPES.計算) requestBody.expression = e[COLUMNS.formula];
// 行番号がサブテーブルの行番号と同じか判定
const subTblIndex = subTblRowNo.find(x => x === String(e[COLUMNS.row]));
if (!subTblIndex) {
fieldRegistrationBody.properties[fieldCode] = requestBody;
} else {
// サブテーブルの中にフィールドの場合サブテーブル配下にリクエストボディを追加
fieldRegistrationBody.properties[subtableRowNoObj[subTblIndex]].fields[fieldCode] = requestBody;
}
// レイアウトのリクエスボディにフィールド情報を追加
layoutBody.layout[e[COLUMNS.row] - 1].fields.push({
"type": fieldType,
"code": fieldCode
});
});
return [fieldRegistrationBody, layoutBody];
}
選択肢が必要なフィールドタイプかチェックする関数を Definitions.gs に追加
this.checkNeedOpitons = (fieldType) => {
const needChoices = [
this.FIELD_TYPES.チェックボックス,
this.FIELD_TYPES.複数選択,
this.FIELD_TYPES.ドロップダウン,
this.FIELD_TYPES.ラジオボタン,
];
return needChoices.includes(fieldType);
}
コード全体像
ファイル構成
・index.html
・main.gs
・Definitions.gs
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<form id="form" onsubmit="formSubmit(this)">
<p>
<label for="domain">
◆サブドメイン ※https://〇〇.cybozu.com の〇〇だけ入力してください。<br>
</label>
<input type="text" name="domain" id="domain" required>
</p>
<p>
<label for="token">
◆APIトークン ※アプリ管理権限のAPIトークンが必要です<br>
</label>
<input type="text" name="token" id="token" style="width: 400px" required>
</p>
<p>
<label for="app-id">
◆アプリID<br>
</label>
<input type="number" name="appId" id="appId" style="width: 50px" required>
</p>
<input type="submit" value="登録" />
</form>
<!-- 実行中... を表示させるためのタグ -->
<div id="info"></div>
<script>
const formSubmit = (formObj) => {
const el = document.getElementById("info").appendChild(document.createElement("p"));
el.textContent = "実行中...";
google.script.run.withSuccessHandler(() => {
google.script.host.close();
}).withFailureHandler((err) => {
alert(err);
}).main(formObj);
}
</script>
</body>
</html>
class Definitions {
constructor () {
this.COLUMNS = {
"row": 0,
"fieldLabel": 1,
"fieldCode": 2,
"fieldType": 3,
"noLabel": 4,
"isRequired": 5,
"isUnique": 6,
"choices": 7,
"linkType": 8,
"formula": 9
}
this.FIELD_TYPES = {
"文字列1行": "SINGLE_LINE_TEXT",
"文字列複数行": "MULTI_LINE_TEXT",
"リッチエディタ": "RICH_TEXT",
"リンク": "LINK",
"数値": "NUMBER",
"計算": "CALC",
"チェックボックス": "CHECK_BOX",
"ドロップダウン": "DROP_DOWN",
"ラジオボタン": "RADIO_BUTTON",
"複数選択": "MULTI_SELECT",
"日付": "DATE",
"時刻": "TIME",
"日時": "DATETIME",
"添付ファイル": "FILE",
"ユーザー選択": "USER_SELECT",
"グループ選択": "GROUP_SELECT",
"組織選択": "ORGANIZATION_SELECT",
"サブテーブル": "SUBTABLE",
"レコード番号": "RECORD_NUMBER",
"作成者": "CREATOR",
"更新者": "MODIFIER",
"作成日時": "CREATED_TIME",
"更新日時": "UPDATED_TIME"
}
this.checkNeedOpitons = (fieldType) => {
const needChoices = [
this.FIELD_TYPES.チェックボックス,
this.FIELD_TYPES.複数選択,
this.FIELD_TYPES.ドロップダウン,
this.FIELD_TYPES.ラジオボタン,
];
return needChoices.includes(fieldType);
}
}
}
/**
* @param {Object} formObj
* @param {string} formObj.domain
* @param {string} formObj.token
* @param {number} formObj.appId
*/
const main = (data) => {
try{
const definitions = new Definitions();
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
const lastRow = sheet.getLastRow();
// スプレッドシートに入力された情報を全て取得
const values = sheet.getRange(2, 1, lastRow - 1, Object.keys(definitions.COLUMNS).length).getValues();
// フォームに入力された情報
const apiToken = data.token;
const appId = data.appId;
const domain = data.domain;
// リクエストボディを作成する getRequestBody 関数に必要な情報を渡す。
const [fieldRegistrationBody, layoutBody] = getRequestBody(appId, values, definitions);
const options = {
"method": "post",
"contentType": "application/json",
"muteHttpExceptions" : true,
"headers": {
"X-Cybozu-API-Token": apiToken
},
"payload": JSON.stringify(fieldRegistrationBody)
};
// フィールド登録APIリクエストを送る
const fetchFieldsUrl = `https://${domain}.cybozu.com/k/v1/preview/app/form/fields.json`;
const resp1 = UrlFetchApp.fetch(fetchFieldsUrl,options);
// リクエストが失敗したらエラーメッセージを投げる
if (resp1.getResponseCode() >= 400) throw new Error(resp1.getContentText());
// アプリに登録されたフィールドの位置を変更するリクエストなのでputで送る
options.method = "put";
options.payload = JSON.stringify(layoutBody);
// フィールドレイアウト更新APIにリクエストを送る
const fetchLayoutUrl = `https://${domain}.cybozu.com/k/v1/preview/app/form/layout.json`;
const resp2 = UrlFetchApp.fetch(fetchLayoutUrl,options);
// リクエストが失敗したらエラーメッセージを投げる
if (resp2.getResponseCode() >= 400) throw new Error(resp2.getContentText());
// リクエストが成功したらmsgBoxにsuccessMsgを表示して終了
const successMsg = `処理が完了しました。設定画面で結果を確認して下さい。https://${domain}.cybozu.com/k/admin/app/flow?app=${appId}#section=form`;
Browser.msgBox(successMsg);
} catch (err) {
Browser.msgBox(err)
console.error(err);
}
}
const getRequestBody = (appId, values, definitions) => {
const COLUMNS = definitions.COLUMNS;
const FIELD_TYPES = definitions.FIELD_TYPES;
// フィールド登録用のリクエストボディ
const fieldRegistrationBody = {
"app": appId,
"properties": {}
}
// フィールドレイアウト更新用のリクエストボディ
const layoutBody = {
"app": appId,
"layout": []
}
// 行番号(A列)の値で昇順にソート
values.sort((a, b) => {
return a[0] - b[0];
});
// サブテーブルの行番号とフィールドコードを代入する変数
const subtableRowNoObj = {};
// サブテーブル以外の行番号を代入する変数
const normalFieldRowNo = [];
// サブテーブル以外のフィールド情報を代入する変数
const normalFieldValues = [];
values.forEach(e => {
// SSに入力されたフィールドコード(C列)を代入
const fieldCode = e[COLUMNS.fieldCode];
// SSに入力されたフィールドタイプ(D列)の情報を英語に変換して代入
const fieldType = FIELD_TYPES[e[COLUMNS.fieldType]];
if(fieldType === FIELD_TYPES.サブテーブル) {
// サブテーブルの行番号が重複していないかチェック
if(Object.keys(subtableRowNoObj).includes(String(e[COLUMNS.row]))) {
throw new Error(`フィールドコード:${fieldCode}の行番号が重複しています。サブテーブルは行番号の重複ができません。`);
}
// フィールド登録用リクエストボディにサブテーブルの情報を追加
fieldRegistrationBody.properties[fieldCode] = {
"type": fieldType,
"code": fieldCode,
"label": e[COLUMNS.fieldLabel],
"noLabel": e[COLUMNS.noLabel] === "" || e[COLUMNS.noLabel] === "false" ? false : true,
"fields": {} // fieldsにはサブテーブル内のフィールド情報を追加していく
}
// サブテーブルの行番号とフィールドコードを代入
subtableRowNoObj[String(e[COLUMNS.row])] = fieldCode;
// レイアウト更新用リクエストボディにサブテーブルの情報を追加
layoutBody.layout[e[COLUMNS.row] - 1] = {
"type": FIELD_TYPES.サブテーブル,
"code": fieldCode,
"fields": [] // fieldsにはサブテーブル内のフィールド情報を追加していく
}
} else {
// サブテーブル以外のフィールドの処理
// classに定義した選択肢が必要か判定する関数でチェック。booleanで返却
const needChoices = definitions.checkNeedOpitons(fieldType);
// 選択肢が必要なフィールドでSSに入力がなければエラーを返す
if (needChoices && e[COLUMNS.choices] === "") throw new Error(`フィールドコード:${fieldCode}は選択肢の入力が必要です`);
// リンクフィールドでリンクタイプの指定がなければエラーを返す
if (fieldType === FIELD_TYPES.リンク && e[COLUMNS.linkType] === "") throw new Error(`フィールドコード:${fieldCode}ははリンクタイプの指定が必要です。`);
if (fieldType === FIELD_TYPES.計算 && e[COLUMNS.formula] === "") throw new Error(`フィールドコード:${fieldCode}は計算式が必要です。`);
// サブテーブル以外のフィールドの情報を配列に追加
normalFieldValues.push(e);
// サブテーブル以外のフィールドの行番号を配列に追加。レイアウト用のリクエストボディ作成に利用
normalFieldRowNo.push(String(e[COLUMNS.row]));
}
});
// サブテーブル以外フィールドの行番号の重複を排除
const uniqueNormalFieldRowNo = Array.from(new Set(normalFieldRowNo));
// サブテーブルの行番号を配列で代入
const subTblRowNo = Object.keys(subtableRowNoObj);
// uniqueNormalFieldRowNo からサブテーブルと同じ行番号を排除する
const rowNoWithoutSubTbl = uniqueNormalFieldRowNo.filter(i => subTblRowNo.indexOf(i) === -1);
// サブテーブルに含まれないフィールドのレイアウト用リクエストボディ
rowNoWithoutSubTbl.forEach(e => {
layoutBody.layout[e - 1] = {
"type": "ROW",
"fields": []
}
});
// 通常フィールドのリクエストボディを作成
normalFieldValues.forEach(e => {
const fieldCode = e[COLUMNS.fieldCode];
const fieldType = FIELD_TYPES[e[COLUMNS.fieldType]];
const requestBody = {
"type": fieldType,
"code": fieldCode,
"label": e[COLUMNS.fieldLabel],
"required": (e[COLUMNS.isRequired] === "" || e[COLUMNS.isRequired] === "false") ? false : true,
"unique": e[COLUMNS.isUnique] === "" || e[COLUMNS.isUnique] === "false" ? false : true,
"noLabel": e[COLUMNS.noLabel] === "" || e[COLUMNS.noLabel] === "false" ? false : true,
};
// 選択肢が必要なフィールドかチェック
const needChoices = definitions.checkNeedOpitons(FIELD_TYPES[e[COLUMNS.fieldType]]);
if (needChoices && e[COLUMNS.choices] !== "") {
// 入力された選択肢をカンマ区切りで分割
const options = e[COLUMNS.choices].split(",");
const optionsObj = {};
for (let i = 0; i < options.length; i++) {
optionsObj[options[i]] = {
"label": options[i],
"index": i
}
}
// リクエストボディに選択肢を追加
requestBody.options = optionsObj;
}
// リンクフィールドの場合リンクのタイプをリクエストボディに追加
if (fieldType === FIELD_TYPES.リンク) requestBody.protocol = e[COLUMNS.linkType];
// 計算フィールドの場合計算式をリクエストボディに追加
if (fieldType === FIELD_TYPES.計算) requestBody.expression = e[COLUMNS.formula];
// 行番号がサブテーブルの行番号と同じか判定
const subTblIndex = subTblRowNo.find(x => x === String(e[COLUMNS.row]));
if (!subTblIndex) {
fieldRegistrationBody.properties[fieldCode] = requestBody;
} else {
// サブテーブルの中にフィールドの場合サブテーブル配下にリクエストボディを追加
fieldRegistrationBody.properties[subtableRowNoObj[subTblIndex]].fields[fieldCode] = requestBody;
}
// レイアウトのリクエスボディにフィールド情報を追加
layoutBody.layout[e[COLUMNS.row] - 1].fields.push({
"type": fieldType,
"code": fieldCode
});
});
return [fieldRegistrationBody, layoutBody];
}
動かしてみる
このデータが
ちゃんと反映できた。
まとめ
リクエストボディ作る関数長すぎたな...
あと作ってて思ったけどAPIトークンの認証じゃなくてパスワード認証のほうが良かったかもなー、気が向いたら修正する。