GaroonワークフローREST APIを使って承認済みワークフローをkintoneに登録しよう

目次

はじめに

今回は、外部システム= kintoneとして、承認された申請データをREST APIを使ってkintoneアプリに登録するサンプルを紹介します。

完成イメージ

kintoneカスタマイズを行い、承認済み物品購入ワークフローアプリで次のことを行います。

  • レコード一覧画面の[承認済み申請データを取得]ボタンをクリックすると、承認済みのGaroonの「物品購入申請」ワークフローの申請データをアプリに登録します。

  • レコード詳細画面の[添付ファイルを取得]ボタンをクリックすると、申請データに添付された添付ファイルをkintoneへ登録します。

GaroonワークフローのREST APIは、「cybozu.com共通管理者」または「ワークフローのアプリケーション管理者権限」をもつユーザーのみ実行できます。
データ取得のためのボタンは、権限に応じた表示制御を行っていません。
権限のないユーザーがボタンをクリックすると、エラーメッセージが表示されます。

Garoon / kintoneの設定

Garoonワークフローの設定

ワークフローの申請フォームを作成します。

手順

  1. 以下のリンクを右クリックし、保存してください。
    sample_form.xml
  2. ダウンロードしたサンプルフォームをGaroon申請フォームに読み込みます。
    読み込み方法は、 XMLファイルでの申請フォームの管理 (External link) をご参考ください。
  3. 読み込んだ申請フォームの詳細画面を開き、経路情報を設定します。
    詳細は、 経路情報の設定 (External link) をご参考ください。
  4. 作成した申請フォームをユーザーが使用できるよう、申請フォームを有効にします。

作成した申請フォームの申請フォームIDはカスタマイズで利用するので、メモしておいてください。
申請フォームIDは、「申請フォームの詳細」画面のURLのクエリパラメーターfid=xxxx部分です。
例:https://YOUR_GAROON_DOMAIN.cybozu.com/g/workflow/system/form_view.csp?cid=-1&fid=66の場合、申請フォームIDは66です。

申請フォームの項目

サンプルフォームにおける申請フォームの項目は、次のようになっています。

項目名 項目タイプ 項目コード 備考
標題 文字列(1行) subject 「必須項目にする」をチェックします。
■ 1 文字列(1行) goods1_name 「必須項目にする」をチェックします。
入力欄の前/後の文字:「品名:」を「前に配置」
型番1 文字列(1行) goods1_model 「必須項目にする」をチェックします。
入力欄の前/後の文字:「型番:」を「前に配置」
右隣への配置:配置する
数量1 数値 goods1_quantity 「必須項目にする」をチェックします。
入力欄の前/後の文字:「数量:」を「前に配置」
右隣への配置:配置する
単価1 数値 goods1_unitPrice 「必須項目にする」をチェックします。
入力欄の前/後の文字:「単価:¥」を「前に配置」
右隣への配置:配置する
金額1 自動計算 goods1_price 計算内容:「四則演算」にチェック
「数量1」「×」「単価1」
入力欄の前/後の文字:「金額:¥」を「前に配置」
右隣への配置:配置する
■ 2 文字列(1行) goods2_name 入力欄の前/後の文字:「品名:」を「前に配置」
型番2 文字列(1行) goods2_model 入力欄の前/後の文字:「型番:」を「前に配置」
右隣への配置:配置する
数量2 数値 goods2_quantity 入力欄の前/後の文字:「数量:」を「前に配置」
右隣への配置:配置する
単価2 数値 goods2_unitPrice 入力欄の前/後の文字:「単価:¥」を「前に配置」
右隣への配置:配置する
金額2 自動計算 goods2_price 計算内容:「四則演算」にチェック
「数量2」「×」「単価2」
入力欄の前/後の文字:「金額:¥」を「前に配置」
右隣への配置:配置する
■ 3 文字列(1行) goods3_name 入力欄の前/後の文字:「品名:」を「前に配置」
型番3 文字列(1行) goods3_model 入力欄の前/後の文字:「型番:」を「前に配置」
右隣への配置:配置する
数量3 数値 goods3_quantity 入力欄の前/後の文字:「数量:」を「前に配置」
右隣への配置:配置する
単価3 数値 goods3_unitPrice 入力欄の前/後の文字:「単価:¥」を「前に配置」
右隣への配置:配置する
金額3 自動計算 goods3_price 計算内容:「四則演算」にチェック
「数量2」「×」「単価2」
入力欄の前/後の文字:「金額:¥」を「前に配置」
右隣への配置:配置する
合計金額 自動計算 totalPrice 計算内容:「合計」にチェック
「金額1」「金額2」「金額2」を追加
入力欄の前/後の文字:「¥」を「前に配置」
希望納品日 日付 dueDeliveryDate 表示形式:「日付のみ」にチェック
初期値:「入力時の現在日付にする」にチェック
添付ファイル ファイル添付 attachments

kintoneアプリの作成とカスタマイズの適用

手順1. 「承認済み購入申請データ」アプリの作成

新規にアプリを作成し(参考: アプリをはじめから作成する (External link) )、次のようにフィールドを追加します。

フィールド名 フィールドタイプ フィールドコード/要素ID 備考
スペース attachBtn JSカスタマイズで、「添付ファイルを取得する」ボタンを設置します
申請番号 文字列(1行) requestNumber 「必須入力」にチェックします。
申請データの「申請番号」に対応します。
標題 文字列(1行) subject 「必須入力」にチェックする
申請データの「標題」に対応します。
作成日 日時 createdAt 「レコード登録時の日時を初期値にする」のチェックを外します。
申請データの「作成日時」に対応します。
ステータス 文字列(1行) status 申請データの「ステータス名」に対応します。
申請者コード 文字列(1行) applicantCode 申請者の「ログイン名」に対応します。
申請者名 文字列(1行) applicantName 申請者の「表示名」に対応します。
商品テーブル goods 申請データに入力された■ 1〜■ 3の「品名」「型番」「数量」「単価」「金額」を行として追加します。
設定の内容は後述します。
合計金額 数値 totalPrice 申請データの「合計金額」に対応します。
希望納品日 日付 dueDeliveryDate 「レコード登録時の日付を初期値にする」のチェックを外す。
申請データの「希望納品日」に対応します。
添付ファイルテーブル attachments 設定の内容は後述します。

商品テーブルの内容は、次のように設定します。

フィールド名 フィールドタイプ フィールドコード 備考
品名 文字列(1行) goodsName 申請データの「品名」に対応します。
型番 文字列(1行) goodsModel 申請データの「型番」に対応します。
数量 数値 goodsQuantity 申請データの「数量」に対応します。
単価 数値 goodsUnitPrice 申請データの「単価」に対応します。
金額 数値 goodsPrice 申請データの「金額」に対応します。

添付ファイルテーブルの内容は、次のように設定します。

フィールド名 フィールドタイプ フィールドコード 備考
ファイルID 文字列(1行) fileId 申請データの「添付ファイル」のidに対応します。
ファイル名 文字列(1行) fileName 申請データの「添付ファイル」のnameに対応します。
Content-Type 文字列(1行) fileContentType 申請データの「添付ファイル」のcontentTypeに対応します。
添付ファイル 添付ファイル fileAttachment 申請データの「添付ファイル」のファイルに対応します。
手順2. カスタマイズの適用

カスタマイズを適用します。

  1. アプリの設定画面を開き、[設定]タブを選択します。

  2. カスタマイズ/サービス連携の[JavaScript / CSSでカスタマイズ]を選択します。

  3. カスタマイズを設定します。次のように設定します。
    「PC用のJavaScriptファイル」に、リンクまたはファイルを追加します。

    • kintone UI Component v1
      https://unpkg.com/kintone-ui-component@1.16.0/umd/kuc.min.js
    • customize.js
      詳細は、後述の サンプルコード を参照してください。
  4. 左上の[保存]ボタンをクリックして、カスタマイズを適用します。

動作確認

  1. サンプルデータを登録します。Garoonで今回作成した「物品購入申請」ワークフローの申請データを作成し、「承認済み」または「完了」までステータスを進めます。
  2. ワークフローに対する管理者権限をもつユーザーで、「承認済み購入申請データ」アプリを開きます。
  3. レコード一覧画面で、[承認済み申請データを取得]ボタンをクリックし、申請データが登録されることを確認します。
  4. ファイルを添付した申請データのレコードを開き、[添付ファイルを取得]ボタンをクリックし、添付ファイルが登録されることを確認します。

サンプルコード

customize.js

承認済み物品購入ワークフローアプリから、承認済みのGaroonの「物品購入申請」ワークフローの申請データをアプリに登録します。 その後、申請データに添付されている添付ファイルをkintoneに登録します。

プログラムの以下の部分を利用環境に合わせて書き換えてください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/*
 * Garoon Workflow x kintone sample program
 * Copyright (c) 2019 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

(function() {
  'use strict';

  // Garoon物品購入申請ワークフローのID
  const GAROON_FORM_ID = 1;

  // Garoon申請データの商品テーブルの行数
  const NUMBER_OF_GAROON_WORKFLOW_ITEMS = 3;

  // kintone UI Componentの読み込み
  const Kuc = Kucs['1.16.0'];

  /*
   * Garoonから申請データを取得してkintoneに登録する
   */
  const registerWorkFlow = async function(notify) {
    try {
      const workflows = await fetchGaroonWorkFlow();
      if (workflows.requests.length === 0) {
        notify.text = '新規の承認済みワークフローはありませんでした。';
        notify.type = 'success';
        notify.open();
      } else {
        // kintoneに登録されていない申請データに絞り込む
        const workflof = await filterNewWorkflow(workflows.requests);

        if (workflof.length === 0) {
          notify.text = '新規の承認済みワークフローはありませんでした。';
          notify.type = 'success';
          notify.open();
        } else {
          const resp = await insertKintoneRecords(workflof);
          notify.text = '承認済みワークフローをアプリに登録しました。画面を更新してください。';
          notify.type = 'success';
          notify.open();
        }
      }
    } catch (err) {
      notify.text = err.message || err;
      notify.type = err.type || 'error';
      notify.open();
    }
  };

  /*
   * Garoonから申請データを取得する
   */
  const fetchGaroonWorkFlow = async function() {
    try {
      const response = await fetch(`/g/api/v1/workflow/admin/requests?form=${GAROON_FORM_ID}&status=APPROVED,COMPLETED`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'X-Requested-With': 'XMLHttpRequest',
        }
      });
      if (!response.ok) {
        throw new Error();
      }
      const data = await response.json();
      return data;
    } catch (error) {
      throw new Error('Garoonワークフローの取得に失敗しました。');
    }
  };

  /*
   * kintoneに登録されていない申請データに絞り込む
   */
  const filterNewWorkflow = async (allWorkflows) => {
    const params = {
      app: kintone.app.getId(),
      fields: ['requestNumber']
    };
    // kintoneから既存のレコードを取得する
    const resp = await kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', params);

    const registerdWorkflows = resp.records.map((record) => record.requestNumber.value);
    const newWorkflows = allWorkflows.filter((workflow) => !registerdWorkflows.includes(workflow.number));
    return newWorkflows;
  };

  /*
   * 申請データをkintoneにレコード登録する
   */
  const insertKintoneRecords = async function(workflows) {
    const records = [];
    // Garoonの申請データをkintoneレコードフォーマットに整形する
    workflows.forEach((workflow) => {
      records.push(formatKintoneRecord(workflow));
    });
    const params = {
      app: kintone.app.getId(),
      records: records
    };
    const resp = await kintone.api(kintone.api.url('/k/v1/records.json', true), 'POST', params);
    return resp;
  };

  /*
   * 添付ファイルをkintoneに登録する
   */
  const registerAttachments = function(notify, recordId, attachmentTable) {
    // Garoonから取得した添付ファイルをkintoneに登録し、ファイルキーを取得する
    // ファイルが複数あるので、再帰的にuploadToKintoneFromGaroonを呼び出す
    const uploadToKintoneFromGaroon = function(opt_index, opt_fileKeys) {
      const index = opt_index || 0;
      const row = attachmentTable[index].value;
      const fileKeys = opt_fileKeys || {};
      // Garoonから添付ファイルを取得する
      return getAttachmentFromGaroon(row.fileId.value).then((file) => {
        // kintoneにファイルをアップロード
        return uploadFileToKintone(file);
      }).then((fileKey) => {
        fileKeys[row.fileId.value] = fileKey;
        if (attachmentTable.length > index + 1) {
          return uploadToKintoneFromGaroon(index + 1, fileKeys);
        }
        return fileKeys;
      }).catch((err) => {
        return kintone.Promise.reject(err);
      });
    };

    /*
    * Garoonから添付ファイルを取得する
    */
    const getAttachmentFromGaroon = async function(fileId) {
      try {
        const response = await fetch(`/g/api/v1/workflow/admin/files/${fileId}`, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
          }
        });
        if (!response.ok) {
          throw new Error('Garoonから添付ファイルの取得に失敗しました');
        }
        const data = await response.json();
        return data;
      } catch (error) {
        throw new Error('Garoonワークフローの取得に失敗しました。');
      }
    };

    // Garoonから添付ファイルを取得してkintoneのレコードに登録する
    uploadToKintoneFromGaroon().then((fileKeys) => {
      attachmentTable.forEach((row) => {
        const fileId = row.value.fileId.value;
        row.value.fileAttachment.value = [{fileKey: fileKeys[fileId]}];
      });
      const params = {
        app: kintone.app.getId(),
        id: recordId,
        record: {attachments: {value: attachmentTable}}
      };
      // kintoneのレコードにファイルキーを紐付ける
      return kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', params);
    }).then((resp) => {
      notify.text = '添付ファイルを取得しました。画面を更新してください。';
      notify.type = 'success';
      notify.open();
    }).catch((err) => {
      notify.text = err.message || err;
      notify.type = err.type || 'error';
      notify.open();
    });
  };

  /*
   * ファイルをkintoneに登録する
   */
  const uploadFileToKintone = async function(file) {
    // Garoonから取得したファイルはBase64エンコードされているので、Blobデータに変換する
    const decodedData = atob(file.content);
    const bufferArray = new Uint8Array(decodedData.length);
    for (let i = 0; i < decodedData.length; i++) {
      bufferArray[i] = decodedData.charCodeAt(i);
    }
    const byteArray = new Uint8Array(bufferArray);
    const blob = new Blob([byteArray], {type: file.contentType});

    // multipart/form-data形式で送信するため、FormDataを利用する
    const formData = new FormData();
    formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
    formData.append('file', blob, file.name);

    const resp = await fetch('/k/v1/file.json', {
      method: 'POST',
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
      },
      body: formData,
    });
    const respData = await resp.json();
    return respData.fileKey;
  };

  /*
   * Garoonの申請データをkintoneのレコード形式に整形する
   */
  const formatKintoneRecord = function(workflow) {
    const items = workflow.items;
    const record = {
      requestNumber: {value: workflow.number},
      subject: {value: items.subject.value},
      createdAt: {value: workflow.createdAt},
      status: {value: workflow.status.name},
      applicantCode: {value: workflow.applicant.code},
      applicantName: {value: workflow.applicant.name},
      totalPrice: {value: items.totalPrice.value},
      dueDeliveryDate: {value: items.dueDeliveryDate.value},
      goods: {value: []},
      attachments: {value: []}
    };
    // 申請データの商品テーブル
    for (let i = 1; i <= NUMBER_OF_GAROON_WORKFLOW_ITEMS; i++) {
      if (items['goods' + i + '_name'] && items['goods' + i + '_name'].value) {
        record.goods.value.push({
          value: {
            goodsName: {value: items['goods' + i + '_name'].value},
            goodsModel: {value: items['goods' + i + '_model'].value},
            goodsQuantity: {value: items['goods' + i + '_quantity'].value},
            goodsUnitPrice: {value: items['goods' + i + '_unitPrice'].value},
            goodsPrice: {value: items['goods' + i + '_price'].value}
          }
        });
      }
    }
    // 申請データの添付ファイル
    const attachmentFiled = items.attachments.value;
    if (attachmentFiled.length > 0) {
      attachmentFiled.forEach((attachment) => {
        record.attachments.value.push({
          value: {
            fileId: {value: attachment.id},
            fileName: {value: attachment.name},
            fileContentType: {value: attachment.contentType}
          }
        });
      });
    }
    return record;
  };

  // レコード一覧画面 表示イベント
  kintone.events.on('app.record.index.show', (event) => {
    const notify = new Kuc.Notification({
      text: 'エラーが発生しました',
      type: 'danger',
      className: 'options-class',
      duration: 2000,
      container: document.body
    });
    const getWorkflowBtn = new Kuc.Button({
      text: '承認済み申請データを取得',
      type: 'submit'
    });

    // ヘッダーメニュー下に「承認済み申請データを取得」ボタンを追加
    if (document.querySelectorAll('.kuc-btn').length) {
      // すでに追加済みなら追加しない
      return event;
    }
    kintone.app.getHeaderMenuSpaceElement().appendChild(getWorkflowBtn);

    // 「承認済み申請データを取得」ボタンが押されたら、Garoonから申請コードを取得する
    getWorkflowBtn.addEventListener('click', () => {
      registerWorkFlow(notify);
    });
    return event;
  });

  // レコード詳細画面 表示イベント
  kintone.events.on('app.record.detail.show', (event) => {
    const notify = new Kuc.Notification({
      text: 'エラーが発生しました',
      type: 'danger',
      className: 'options-class',
      duration: 2000,
      container: document.body
    });

    const getWorkflowBtn = new Kuc.Button({
      text: '添付ファイルを取得',
      type: 'submit'
    });
    // 添付ファイルテーブル
    const attachmentTable = event.record.attachments.value;

    // スペースフィールドに「添付ファイルを取得」ボタンを追加
    if (document.querySelectorAll('.kuc-btn').length) {
      // すでに追加済みなら追加しない
      return event;
    }
    kintone.app.record.getSpaceElement('attachBtn').appendChild(getWorkflowBtn);
    // 添付ファイルテーブルが0件ならボタンを非活性化
    if (attachmentTable.length === 0) {
      getWorkflowBtn.disable();
      return event;
    }

    // 「添付ファイルを取得」ボタンが押されたら、kintoneに添付ファイルを登録する
    getWorkflowBtn.addEventListener('click', () => {
      registerAttachments(notify, event.recordId, attachmentTable);
    });
    return event;
  });

})();

取得する件数が多くなると、申請データのレコードを登録するまでに時間がかかります。
処理が終わるまでは Spinner (External link) を表示するなど工夫してください。

サンプルコードの解説

今回のサンプルコードのポイントです。

  • 56 - 73行目
    Garoonワークフローの 申請データを取得する APIを利用して、申請データを取得します。
    申請フォームIDの指定や申請データのステータスで絞り込みしたいので、URLのクエリパラメーターとして指定します。
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const fetchGaroonWorkFlow = async function() {
  try {
    const response = await fetch(`/g/api/v1/workflow/admin/requests?form=${GAROON_FORM_ID}&status=APPROVED,COMPLETED`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
      }
    });
    if (!response.ok) {
      throw new Error();
    }
    const data = await response.json();
    return data;
  } catch (error) {
    throw new Error('Garoonワークフローの取得に失敗しました。');
  }
};
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
const getAttachmentFromGaroon = async function(fileId) {
  try {
    const response = await fetch(`/g/api/v1/workflow/admin/files/${fileId}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
      }
    });
    if (!response.ok) {
      throw new Error('Garoonから添付ファイルの取得に失敗しました');
    }
    const data = await response.json();
    return data;
  } catch (error) {
    throw new Error('Garoonワークフローの取得に失敗しました。');
  }
};
  • 183 - 190行目
    Garoonワークフローの申請データのファイル取得APIを利用して取得したファイルの内容は、Base64エンコードされています。
    kintoneにアップロードできるようにBlob形式に変換します。
183
184
185
186
187
188
189
190
// Garoonから取得したファイルはBase64エンコードされているので、Blobデータに変換する
const decodedData = atob(file.content);
const bufferArray = new Uint8Array(decodedData.length);
for (let i = 0; i < decodedData.length; i++) {
  bufferArray[i] = decodedData.charCodeAt(i);
}
const byteArray = new Uint8Array(bufferArray);
const blob = new Blob([byteArray], {type: file.contentType});
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// multipart/form-data形式で送信するため、FormDataを利用する
const formData = new FormData();
formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
formData.append('file', blob, file.name);

const resp = await fetch('/k/v1/file.json', {
  method: 'POST',
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
  },
  body: formData,
});
const respData = await resp.json();
return respData.fileKey;
  • 269-274行目、300-304行目
    kintone UI Componentのボタンパーツを画面に配置します。ただし、すでにボタンが存在する場合は配置しません。
    また、レコード詳細画面の添付ファイルテーブルに行がない場合は、「添付ファイルを取得」ボタンを非活性にしています。
269
270
271
272
273
274
// ヘッダーメニュー下に「承認済み申請データを取得」ボタンを追加
if (document.querySelectorAll('.kuc-btn').length) {
  // すでに追加済みなら追加しない
  return event;
}
kintone.app.getHeaderMenuSpaceElement().appendChild(getWorkflowBtn);

おわりに

ワークフローREST APIが追加されたことで、Garoonワークフローの外部システムとの連携を楽に実装できるようになりました。
今回承認経路は扱っていませんが、申請データの取得APIでは申請データの承認・閲覧などの経路情報も取得できるので、「誰が承認or差し戻したか?」などの情報も連携できます。
なお、この記事では次のAPIを利用しています。

information

このTipsは、2024年2月版Garoonおよびkintone UI Component v1で動作を確認しています。