【Garoon JavaScript API】ワークフロー承認後にスケジュールを登録する

目次

概要

2017年11月のアップデートで追加された ワークフロー申請を承認したときのイベント を使い、ワークフローの申請内容をスケジュールに登録します(予定を登録する部分はGaroon SOAP APIを使用しています)。

前提条件と注意事項

  • このカスタマイズには、クラウド版Garoonまたはパッケージ版Garoon 4.10以降の環境が必要です。
  • ワークフローJavaScriptカスタマイズは、JavaScriptを適用してから申請されたワークフローが対象になります。
    それ以前に申請されたワークフローには適用されません。
  • 代理承認時にカスタマイズが動作しない不具合を確認しています。(2019/2/21追記)

できること

有給休暇、出張など、申請が必要なイベントについてワークフローとスケジュールを連携させることができます。
ワークフローでは、スケジュールへの自動登録機能が搭載されていますが、対象項目の指定等、より柔軟な連携が可能になります。

完成イメージ

Garoonのワークフローにて承認すると、申請内容がスケジュールへ登録される流れになります。

  • 「承認する」ボタンをクリックすると、ワークフローの内容が申請者のスケジュールに登録されます。
  • スケジュールのメモ欄には、承認されたワークフローへのリンクが設定されます。

スケジュール側には特別な設定は不要です。
ワークフローの設定にのみ、JavaScriptファイルを設定しカスタマイズしていきます。

Garoonワークフローの設定手順

ワークフローの項目の内容は、会社によって異なります。
ここでは、サンプルということで、スケジュールの項目をおおむね網羅した申請フォームにJavaScript/CSSカスタマイズを設定する流れを説明します。

1. ワークフローの申請フォームを作成する

まずは以下の項目を配置して、ワークフローの申請フォームを作成していきます。
申請フォームの作成方法については、Garoonヘルプ - 申請フォームの作成の流れ クラウド版 (External link) パッケージ版 (External link) を参照してください。

申請フォームは、スケジュールの項目と対応付けます。それぞれの項目は以下のとおり設定してください。
項目コードは、JavaScriptコード内でそれぞれの項目を指定するための一意の文字列になります。

項目名 項目タイプ 項目コード 必須 備考
開始日時 日付(日付と時刻) From
終了日時 日付(日付と時刻) To
予定メニュー メニュー Menu 出張/休みをメニュー項目とします。
デフォルトの予定メニュー選択肢です。
タイトル 文字列(1行) Title
メモ 文字列(複数行) Memo
公開方法 ラジオボタン Scope 公開/非公開をラジオ項目とします。

上記のとおり設定が完了したら、土台となる申請フォームの作成は完了です。

2. JavaScript/CSSファイルを適用する

申請フォームの作成が完了したので、ここから作成した申請フォームにJavaScriptファイルを適用していきます。

3. 適用ファイルの準備

次のサンプルコードをエディタにコピーして、ファイル名を「wf_to_sch.js」、文字コードを「UTF-8」で保存します。
ファイル名は任意ですが、ファイルの拡張子は「js」にしてください。

  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
/**
 * Garoon JavaScript、SOAP APIを使ったサンプルプログラム
 *
 * 「wf_to_sch.js」ファイル
 *
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
 */
(() => {
  'use strict';
  const myJQuery = jQuery.noConflict(true);
  (($) => {

    /**
     * 共通SOAPコンテンツ
     * ${XXXX}の箇所は実施処理等に合わせて置換して使用
     */
    const SOAP_TEMPLATE =
        '<?xml version="1.0" encoding="UTF-8"?>' +
        '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">' +
          '<soap:Header>' +
            '<Action>${ACTION}</Action>' +
            '<Timestamp>' +
              '<Created>${CREATED}</Created>' +
              '<Expires>2037-08-12T14:45:00Z</Expires>' +
            '</Timestamp>' +
            '<Locale>jp</Locale>' +
          '</soap:Header>' +
          '<soap:Body>' +
            '<${ACTION}>' +
              '<parameters>${PARAMETERS}</parameters>' +
            '</${ACTION}>' +
          '</soap:Body>' +
          '</soap:Envelope>';

    /**
     * スケジュール登録パラメータテンプレート
     * ${XXXX}の箇所は入力値等で置換して使用
     */
    const SCH_ADD_TEMPLATE =
        '<request_token>${REQUEST_TOKEN}</request_token>' +
        '<schedule_event xmlns="" id="dummy" event_type="normal" pubic_type="${SCOPE}" version="dummy" ' +
          'plan="${MENU}" detail="${TITLE}" description="${MEMO}" ' +
          'timezone="Asia/Tokyo" end_timezone="Asia/Tokyo" allday="false" start_only="false">' +
          '<members>' +
            '<member>' +
              '<user id="${USER_ID}"></user>' +
            '</member>' +
          '</members>' +
          '<when>' +
            '<datetime start="${START_TIME}" end="${END_TIME}"></datetime>' +
          '</when>' +
        '</schedule_event>';

    // 文字列をHTMLエスケープ
    const escapeHtml = (str) => {
      return str
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
    };

    // エラーハンドリング関数
    const handleError = (error, customMessage) => {
      const errorMessage = customMessage || 'エラーが発生しました';
      console.error(errorMessage, error);
      alert(`ワークフローの承認は正常に完了しましたが、スケジュールへの登録に失敗しました。\n${errorMessage}\n詳細: ${error}`);
    };

    // リクエストトークン取得
    const getRequestToken = () => {
      const defer = $.Deferred();

      // リクエストトークンの取得
      let tokenRequest = SOAP_TEMPLATE;
      tokenRequest = tokenRequest.replace('${PARAMETERS}', '');
      tokenRequest = tokenRequest.split('${ACTION}').join('UtilGetRequestToken');
      tokenRequest = tokenRequest.replace('${CREATED}', luxon.DateTime.utc().startOf('second').toISO({suppressMilliseconds: true}));
      $.ajax({
        type: 'post',
        url: '/g/util_api/util/api.csp',
        cache: false,
        async: false,
        data: tokenRequest
      })
        .then((respForToken) => {
          defer.resolve($(respForToken).find('request_token').text());
        })
        .catch((e) => {
          handleError(e.statusText, 'リクエストトークン取得時にエラーが発生しました');
          defer.reject(e.statusText);
        });

      return defer.promise();
    };

    // 指定のユーザ名に対応するユーザIDを取得する
    const getApplicantId = (userNm) => {
      const defer = $.Deferred();

      // 申請者情報(申請者のID)の取得
      let applicantRequest = SOAP_TEMPLATE;
      applicantRequest = applicantRequest.replace('${PARAMETERS}', '<login_name>' + userNm + '</login_name>');
      applicantRequest = applicantRequest.split('${ACTION}').join('BaseGetUsersByLoginName');
      applicantRequest = applicantRequest.replace(
        '${CREATED}', luxon.DateTime.utc().startOf('second').toISO({suppressMilliseconds: true}));

      $.ajax({
        type: 'post',
        url: '/g/cbpapi/base/api.csp',
        cache: false,
        async: false,
        data: applicantRequest
      })
        .then((respForApplicant) => {
          defer.resolve($(respForApplicant).find('user').attr('key'));
        })
        .catch((e) => {
          handleError(e.statusText, 'ユーザー情報の取得時にエラーが発生しました');
          defer.reject(e.statusText);
        });

      return defer.promise();
    };

    // ワークフロー承認イベントで起動する
    // 申請内容をスケジュールに登録する
    garoon.events.on('workflow.request.approve.submit.success', (event) => {
      // 申請内容を取得する
      const request = event.request;

      return getRequestToken().then((requestToken) => {
        // 申請者のユーザ名をSOAPで処理できるID形式に変換
        return getApplicantId(request.applicant.code).then((applicantId) => {
          // スケジュールSOAP API固有のパラメータを設定
          let schAddParam = SCH_ADD_TEMPLATE;

          schAddParam = schAddParam.replace('${REQUEST_TOKEN}', escapeHtml(requestToken));
          schAddParam = schAddParam.replace('${MENU}', request.items.Menu.value); // 予定メニュー
          schAddParam = schAddParam.replace('${TITLE}', request.items.Title.value); // タイトル

          // メモ欄には申請へのURLを付加
          const url = location.protocol + '//' + location.hostname +
                    '/g/workflow/view.csp?pid=' + request.id +
                    '&amp;fid=' + request.folders[request.folders.length - 1].id + '&#xA;';
          // 改行の変換
          schAddParam = schAddParam.replace('${MEMO}', url + request.items.Memo.value.split('\n').join('&#xA;'));

          const startTime = luxon.DateTime.fromISO(request.items.From.value.date + 'T' + request.items.From.value.time + ':00')
            .toUTC().startOf('second').toISO({suppressMilliseconds: true});
          schAddParam = schAddParam.replace(
            '${START_TIME}', startTime); // 開始日時UTCに変換
          const endTime = luxon.DateTime.fromISO(request.items.To.value.date + 'T' + request.items.To.value.time + ':00')
            .toUTC().startOf('second').toISO({suppressMilliseconds: true});
          schAddParam = schAddParam.replace(
            '${END_TIME}', endTime); // 終了日時UTCに変換

          schAddParam = schAddParam.replace('${USER_ID}', applicantId); // 申請者

          // 公開方法は、公開⇒public、非公開⇒privateに変換
          let scope;
          if (request.items.Scope.value === '公開') {
            scope = 'public';
          } else if (request.items.Scope.value === '非公開') {
            scope = 'private';
          }

          schAddParam = schAddParam.replace('${SCOPE}', scope);

          let schAddRequest = SOAP_TEMPLATE;
          // SOAPパラメータを完成させる
          schAddRequest = schAddRequest.replace('${PARAMETERS}', schAddParam);
          // 実行処理を指定
          schAddRequest = schAddRequest.split('${ACTION}').join('ScheduleAddEvents');
          schAddRequest = schAddRequest.replace(
            '${CREATED}', luxon.DateTime.utc().startOf('second').toISO({suppressMilliseconds: true}));

          // スケジュール登録の実行
          $.ajax({
            type: 'post',
            url: '/g/cbpapi/schedule/api.csp',
            cache: false,
            async: false,
            data: schAddRequest
          })
            .catch((e) => {
              handleError(e.statusText, 'スケジュール登録実行時にエラーが発生しました');
              defer.reject(e.statusText);
            });
        });
      });
    });
  })(myJQuery);
})();

ポイント

  • request.approve.submit.successイベントに実装することにより、承認が行われた後に起動する処理を作成できます。

4. JavaScript/CSSファイルとして使用するファイルのおよびリンクの追加

  1. 「申請フォーム情報」部分の右端にある「JavaScript / CSSによるカスタマイズ」をクリックします。


    ワークフローのカスタマイズが許可されていない場合、申請フォームの詳細画面に「JavaScript / CSSによるカスタマイズ」というリンクが表示されません。
    詳細は、 ワークフローのカスタマイズを許可する (External link) を参照してください。

  2. [カスタマイズ]項目に「適用する」を選択します。wf_to_sch.jsが使用するjQuery、Luxon、および作成した「wf_to_sch.js」ファイルを追加し、「設定する」をクリックします。

本カスタマイズでは、 Cybozu CDN の以下のライブラリーを使用します。

  • jQuery
    https://js.cybozu.com/jquery/3.6.4/jquery.min.js
  • Luxon
    https://js.cybozu.com/luxon/3.3.0/luxon.min.js
caution
警告

jQuery、Luxonはwf_to_sch.jsより上位に登録してください。

以上ですべての設定は完了です。最初にお見せした完成イメージのとおり、動けば成功です。

おわりに

Garoon APIのカスタマイズサンプル ワークフローとスケジュールとの連携方法を紹介しました。
ワークフローの承認実行のタイミングでGaroon内の別アプリにデータを登録することが簡単にできます。

information

このTipsは、2024年9月版Garoonで動作を確認しています。