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

目次

はじめに

ワークフロー申請を承認したときのイベント を使い、ワークフローの申請内容を掲示板に投稿します。
掲示板に投稿する部分は 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 で動作を確認しています。