ワークフロー承認後に掲示板を投稿する

目次

はじめに

ワークフロー申請を承認したときのイベントを使い、ワークフローの申請内容を掲示板に投稿します。
掲示板に投稿する部分はGaroon SOAP APIを使用しています。

前提条件と注意事項

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

できること

ワークフローと掲示板を連携させることにより、投稿前に上司の確認・承認をはさむことができます。
たとえば、人事や総務が全社向けに告知する人事異動情報の掲載など、各種お知らせの掲載の前の上司承認を得るようなケースが想定されます。

完成イメージ

Garoonのワークフローを承認すると、申請内容が掲示板に投稿されます。

  • 「承認する」ボタンをクリックすると、ワークフローの内容が掲示板に投稿されます。

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

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

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

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

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

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

項目名 項目タイプ 項目コード 必須 備考
タイトル 文字列(1行) Subject
掲示期間を指定する チェックボックス SpecifyTerm
掲示期間 日付 BulletinFrom
掲示期間(To) 日付 BulletinTo
本文 文字列(複数行) Body
添付ファイル ファイル添付 Attach 5つまで登録可能とします。
コメントの書き込みを許可する チェックボックス CanFollow

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

Javascript/CSSファイルを適用する

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

適用ファイルの準備

今回はサンプルということで、投稿先のカテゴリを固定しています。まずは投稿先のカテゴリを決定します。
掲示板から、掲載したいカテゴリにアクセスします。URLに含まれるcid(カテゴリID)を確認します。のちほどプログラムの書き換えに使いますので、メモしておきましょう。
例:以下のイメージでは、掲載するカテゴリ「人事部からのお知らせ」にアクセスしています。URLがhttps://{subdomain}.cybozu.com/g/bulletin/index.csp?cid=6のため、カテゴリIDは6です。

次のサンプルコードをエディタにコピーします。
16行目のCATEGORY_IDを先ほどメモしたカテゴリIDに書き換えます。

  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
/**
 * Garoon JavaScript、SOAP APIを使ったサンプルプログラム
 *
 * 「wf_to_bbs.js」ファイル
 *
 * Copyright (c) 2018 Cybozu
 *
 * Licensed under the MIT License
 */

(($) => {
  'use strict';

  // 投稿先掲示板カテゴリID
  const CATEGORY_ID = 1;

  /**
   * 共通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}' +
            '</${ACTION}>' +
          '</soap:Body>' +
        '</soap:Envelope>';

  /**
   * 掲示板登録パラメータ
   * ${XXXX}の箇所は入力値等で置換して使用
   *
   * ※ 投稿先は、15行目CATEGORY_ID変数で指定したカテゴリに固定。
   * ※ 掲載期間は、未指定時Attributeを出力しないため、Attribute毎置換する
   */
  const BBS_ADD_TEMPLATE =
        '<parameters>' +
          '<request_token>${REQUEST_TOKEN}</request_token>' +
          '<create_topic xmlns="">' +
            '<topic xmlns="" id="dummy" version="dummy" subject="${SUBJECT}" ' +
              '${START_DATETIME} ${END_DATETIME} can_follow="${CAN_FOLLOW}" category_id="' + CATEGORY_ID + '">' +
              '<content body="${BODY}">' +
                '${FILE_DEFS}' +
              '</content>' +
            '</topic>' +
            '${FILE_CONTENTS}' +
          '</create_topic>' +
        '</parameters>';

  /**
   * ファイル定義
   * ${XXXX}の箇所は入力値等で置換して使用
   */
  const FILE_DEF_TEMPLATE =
        '<file id="${INDEX}" name="${FILE_NAME}" mime_type="${FILE_MIME_TYPE}"></file>';

  /**
   * ファイルコンテンツ
   * ${XXXX}の箇所は入力値等で置換して使用
   */
  const FILE_CONTENT_TEMPLATE =
        '<file xmlns="" id="${INDEX}">' +
          '<content xmlns="">${FILE_CONTENT}</content>' +
        '</file>';

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

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

    // リクエストトークンの取得
    let request = SOAP_TEMPLATE;
    request = request.replace('${PARAMETERS}', '<parameters></parameters>');
    request = request.split('${ACTION}').join('UtilGetRequestToken');
    request = request.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: request
    })
      .then((response) => {
        defer.resolve($(response).find('request_token').text());
      });
    // 本来はエラー処理を実施
    return defer.promise();
  };

  // 指定した申請情報から添付ファイルの情報を取得
  const getRequestFiles = function(requestId) {
    const defer = $.Deferred();
    let request = SOAP_TEMPLATE;
    request = request.replace('${PARAMETERS}',
      '<parameters><application_id>' + requestId + '</application_id></parameters>');
    request = request.split('${ACTION}').join('WorkflowGetReceivedApplicationsById');
    request = request.replace(
      '${CREATED}', luxon.DateTime.utc().startOf('second').toISO({suppressMilliseconds: true}));
    $.ajax({
      type: 'post',
      url: '/g/cbpapi/workflow/api.csp',
      cache: false,
      async: false,
      data: request
    })
      .then((response) => {
        defer.resolve($(response).find('file'));
      });
    // 本来はエラー処理を実施
    return defer.promise();
  };

  // 指定IDのファイルを取得
  const downloadFile = function(fileId) {
    const defer = $.Deferred();

    let request = SOAP_TEMPLATE;
    request = request.replace('${PARAMETERS}',
      '<parameters file_id="' + fileId + '"></parameters>');
    request = request.split('${ACTION}').join('WorkflowFileDownload');
    request = request.replace(
      '${CREATED}', luxon.DateTime.utc().startOf('second').toISO({suppressMilliseconds: true}));
    $.ajax({
      type: 'post',
      url: '/g/cbpapi/workflow/api.csp',
      cache: false,
      async: false,
      data: request
    })
      .then((response) => {
        defer.resolve($(response).find('content').text());
      });
    // 本来はエラー処理を実施
    return defer.promise();
  };

  // 申請に登録された全ファイルを取得
  const downloadFiles = function(files, index) {
    let optIndex = index || 0;
    // すべてのファイルコンテンツを取得し終えた場合終了
    if (files.length === optIndex) {
      const defer = $.Deferred();
      defer.resolve(files);
      return defer.promise();
    }

    return downloadFile(files[optIndex].id).then((content) => {
      files[optIndex].content = content;
      optIndex++;
      return downloadFiles(files, optIndex);
    });
  };

  garoon.events.on('workflow.request.approve.submit.success', (event) => {
    const request = event.request;
    return getRequestToken().then((requestToken) => {
      const requestId = request.id;
      // 申請 ID から添付されたファイル情報を取得
      return getRequestFiles(requestId).then(($requestFiles) => {
        const files = [];
        $requestFiles.each((_index, file) => {
          files.push(
            {
              id: $(file).attr('file_id'),
              name: $(file).attr('name'),
              contentType: $(file).attr('mime_type')
            }
          );
        });
        // 添付ファイルのbase64Binary表現を取得
        return downloadFiles(files);
      }).then((files) => {
        // ファイルパラメータの構築
        const fileDefs = [];
        const fileContents = [];
        for (let i = 0; i < files.length; i++) {
          let fileDef = FILE_DEF_TEMPLATE;
          fileDef = fileDef.replace('${INDEX}', i + 1);
          fileDef = fileDef.replace('${FILE_NAME}', files[i].name);
          fileDef = fileDef.replace('${FILE_MIME_TYPE}', files[i].contentType);
          fileDefs.push(fileDef);

          let fileContent = FILE_CONTENT_TEMPLATE;
          fileContent = fileContent.replace('${INDEX}', i + 1);
          fileContent = fileContent.replace('${FILE_CONTENT}', files[i].content);
          fileContents.push(fileContent);
        }

        // 掲示板パラメータの構築
        let bbsAddParam = BBS_ADD_TEMPLATE;
        bbsAddParam = bbsAddParam.replace('${REQUEST_TOKEN}', escapeHtml(requestToken));
        bbsAddParam = bbsAddParam.replace('${SUBJECT}',
          request.items.Subject.value + '(申請者:' + request.applicant.name + ')');
        bbsAddParam = bbsAddParam.replace('${BODY}', request.items.Body.value.split('\n').join('&#xA;'));
        bbsAddParam = bbsAddParam.replace('${CAN_FOLLOW}', request.items.CanFollow.value);
        bbsAddParam = bbsAddParam.replace('${FILE_DEFS}', fileDefs.join(''));
        bbsAddParam = bbsAddParam.replace('${FILE_CONTENTS}', fileContents.join(''));

        // 掲示期間の指定
        let startDatetime = '';
        let endDatetime = '';
        if (request.items.SpecifyTerm.value) {
          startDatetime = 'start_datetime="' + request.items.BulletinFrom.value + '"';
          endDatetime = 'end_datetime="' + request.items.BulletinTo.value + '"';
        }
        bbsAddParam = bbsAddParam.replace('${START_DATETIME}', startDatetime);
        bbsAddParam = bbsAddParam.replace('${END_DATETIME}', endDatetime);

        // SOAPパラメータを構築
        let bbsAddRequest = SOAP_TEMPLATE;
        bbsAddRequest = bbsAddRequest.replace('${PARAMETERS}', bbsAddParam);
        bbsAddRequest = bbsAddRequest.split('${ACTION}').join('BulletinCreateTopics');
        bbsAddRequest = bbsAddRequest.replace(
          '${CREATED}', luxon.DateTime.utc().startOf('second').toISO({suppressMilliseconds: true}));

        // 掲示板登録実行
        $.ajax({
          type: 'post',
          url: '/g/cbpapi/bulletin/api.csp?',
          cache: false,
          async: false,
          data: bbsAddRequest
        });
        // 本来はエラー処理を実施
      });
    });
  });
})(jQuery.noConflict(true));

ファイル名を「wf_to_bbs.js」、文字コードを「UTF-8」で保存します。
ファイル名は任意ですが、ファイルの拡張子は「js」にしてください。

ポイント

  • workflow.request.approve.submit.successイベントに実装することにより、承認が行われた後に起動する処理を作成できます。
  • ワークフローに登録した添付ファイルを、5つまで掲示板にコピー投稿できます。
  • 掲示期間を指定することにより、期間を限定した投稿、予約投稿も可能です。
  • 投稿者は申請者ではなく、承認者になります。申請者名義での投稿はできないため、タイトルに申請者名を明記しています。

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

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

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

  2. [カスタマイズ]項目に「適用する」を選択します。wf_to_bbs.jsが使用するjQuery、Luxon、および作成した「wf_to_bbs.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

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

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

おわりに

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

更新履歴

  • 2022年2月1日
    添付ファイルのIDを取得する処理を、workflow.request.approve.submit.successイベントのワークフローオブジェクトを使う方法から、 SOAP API(受信した申請を取得する)を使う方法に変更
information

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