IM-FormaDesignerのCSVインポートで件数チェックを実行する

# 対象となる開発形態
- intra-mart Accel Platform
   - IM-FormaDesigner

# やりたいこと
- IM-FormaDesignerのCSVインポートで、指定の件数をオーバーした場合にワーニングメッセージを表示する。

CSVインポートでグリッドテーブルにデータを取り込む時、グリッドテーブルの最大行数をオーバーした場合に最大行数以降を切り捨ててしまうため、ワーニングメッセージを表示したい。

# 実現方法

## 処理の概要
1. ファイル選択ダイアログで選択したCSVファイルのセッションスコープストレージパスを取得する(フォームスクリプト)
2. Ajaxでサーバサイドにパスを渡し、レコード件数を数える(SSJS)
3. 返却された結果からメッセージを表示する(フォームスクリプト)

## 必要になるもの
- レコード件数を数えるSSJS
- SSJSのAjax呼び出し方法を記述するHTMLテンプレート
- Ajaxを実行するFormaアプリケーションフォームのスクリプト

# サンプルソース
今回はCsvImportというIDでFormaアプリケーションを作成したと想定します。

## HTMLテンプレート
`storage\public\storage\%tenant_id%\forma\html_template\template.html`をコピーし、`storage\public\storage\%tenant_id%\forma\html_template\CsvImport\CsvImport.html`を作成します。`CsvImport`はFormaアプリケーションIDです。
HTMLテンプレートの詳細は以下を参照してください。

[Formaでのスクリプト開発生産性向上](https://dev.intra-mart.jp/forma_2/)

コピーしたHTMLを開き、以下の`<imart type="jsspRpc">`タグを追加します。場所はどこでもいいですが、`%HEADER%`の下辺りがいいのかな？

```html
<imart type="jsspRpc" name="csvImportAjax" page="CsvImport/csvImportAjax" />
```

`name`属性にはFormaアプリケーションフォームから呼び出す時に指定する名前、`page`属性にはSSJSのパスを指定します。
`<imart type="jsspRpc">`タグの詳細は以下を参照してしてください。

[スクリプト開発向けタグライブラリ - jsspRpc](https://www.intra-mart.jp/apidoc/iap/apilist-jssp-tagdoc/doc/imart_tag_api/jsspRpc/index.html)

このテンプレートHTMLをベースにFormaアプリケーションフォームが読み込まれるため、フォーム側のスクリプトから`name`属性で指定した名前を使ってSSJS処理を呼び出すことができるようになります。

## Formaアプリケーションフォームスクリプト
Formaアプリケーションフォーム側で、SSJSを呼び出すスクリプトを記述します。
CSVインポートで選択したファイルはintra-martのセッションスコープストレージに格納されるため、CSJS側で読み込むことができません。
なので、ストレージの情報をDOMから取得してSSJSに渡し、サーバ側でファイルの読み込みを行おうということです。

### サンプルソース
スクリプトアイテムを画面上に配置するか、アクション設定-初期表示イベントでカスタムスクリプトを追加し、以下のスクリプトを貼ります。

```js
// CSV取込件数チェック
// MutationObserverオブジェクトを生成
let observer = new MutationObserver(function(r, o) {
   // 追加されたノードがspan.download-file-nameの場合
   let fileNameElm = $('span.download-file-name');
   if (fileNameElm.length > 0) {
      let fileNames = [];

      // ファイルインポートアイテムの<script>要素からセッションスコープストレージパスを取得
      let storagePath = 
          $('div[item_type="product_80_fileImport"] script')
          .text()
          .match(/var sessionScopeStoragePath = '(.*?)';/)[1];
      for (let i = 0; fileNameElm.length > i; i++) { 
         // SessionScopeStorageのファイル名を取得
         fileNames[i] = storagePath + '/' + $(fileNameElm[i]).data('physical_file_name');
      }

      // Ajax処理
      let oResult = csvImportAjax.countCsvRecords(fileNames);
      if (oResult.countOver) {
         // 最大件数をオーバーした場合、確定ボタン押下時にワーニングメッセージを表示
         $('button#importOk').on('click', function() {
            imuiShowWarningMessage(oResult.message);
         });
      }
   }
});
// 監視を開始
observer.observe($('body')[0], {childList: true, subtree: true});
```

#### セッションスコープストレージのファイル名
CSVファイルが選択されると、ファイル選択ダイアログのリストに行エレメントが追加されます。
この内、`download-file-name`クラスを付与された`span`要素のカスタムデータ属性`physical_file_name`に、セッションスコープストレージ上のファイル名が格納されています。
セッションスコープストレージ上のファイル名はintra-mart側で一意の値が付与されるため、選択したファイル名をそのまま使うことはできません。

```js
$(fileNameElm[i]).data('physical_file_name');
```

この要素を`mutationObserver`で監視し、追加されたらファイル名を取得してSSJS側にAjaxで渡します。

#### セッションスコープストレージのファイルパス
セッションスコープストレージのファイルパスは、どうやらDOM要素の中にはないようです……。唯一あったのが、ファイルインポートアイテムのスクリプト要素の中。
なので、仕方なくスクリプト内部のファイルパスを正規表現で抜き出すという力技を使いました。

```js
let storagePath = 
    $('div[item_type="product_80_fileImport"] script')
    .text()
    .match(/var sessionScopeStoragePath = '(.*?)';/)[1];
```

#### SSJS呼び出し
HTMLテンプレートで指定した名前を使って、レコード件数チェックのSSJSをAjaxで呼出しています。`countCsvRecords`は、jsファイル内で定義するfunction名です。
ファイルは複数登録が可能なため、全ファイルの合計で件数をチェックする必要があります。そのため、渡している引数はファイルパスの配列です。

```js
let oResult = csvImportAjax.countCsvRecords(fileNames);
```

#### ワーニングメッセージ表示
最大件数をオーバーした時のワーニングメッセージ表示タイミングは、ファイル選択ダイアログの登録ボタン押下時としました。
Ajax処理はファイルリストの要素追加時に実行されてしまうため、Ajaxの実行結果が最大件数をオーバーしていたら確定ボタンの`click`イベントにメッセージ表示処理を定義しています。

```js
if (oResult.countOver) {
   // 最大件数をオーバーした場合、確定ボタン押下時にワーニングメッセージを表示
   $('button#importOk').on('click', function() {
      imuiShowWarningMessage(oResult.message);
   });
}
```

## サーバサイドJavaScript
### サンプルソース

```csvImportAjax.js
function countCsvRecords(fileNames) {
	// 最大件数
	const CSV_MAX_RECORD = 100;

	// 行数
	let count = 0;
	for (let i = 0; fileNames.length > i; i++) {
		// セッションスコープストレージ
		let storage = new SessionScopeStorage(fileNames[i]);

		// テキストの読み込み
		storage.openAsText(function(reader, error) {
			// ファイル読み込みエラー
			if (error != null) {
				return {
					'error': Constant.ACC_ERROR_FLAG_FAIL, // エラーフラグ
					'count': -1, // 件数
					'countOver': false, // 結果フラグ
					'message': error.message // メッセージ
				}
			}

			reader.eachLine(function(line, index) {
				// 空行または空白のみの行とヘッダ行は除外
				if (index > 1 && line.replace(/[\s|　]/g, '') !== "") {
					count++;
				}
			});
		});
	}

	if (count > CSV_MAX_RECORD) {
		return {
			'error': false, // エラーフラグ
			'count': count, // 件数
			'countOver': true, // 結果フラグ
			'message': 'CSVレコード数が' + CSV_MAX_RECORD + '件以上あります。<br />残りのレコードは読み込まれません。' // メッセージ
		}
	} else {
		return {
			'error': false, // エラーフラグ
			'count': count, // 件数
			'countOver': false, // 結果フラグ
			'message': '' // メッセージ
		}
	}
}
```

Formaアプリケーションフォームのスクリプトから渡されたセッションスコープストレージのファイルパス配列を順次読み込み、件数をカウントします。
件数が最大件数をオーバーしたら、結果フラグ`true`とメッセージをクライアント側に返却しています。
`SessionScopeStorage`APIの詳細は以下を参照してください。

[スクリプト開発向けim-BizAPI - SessionScopeStorageオブジェクト](https://www.intra-mart.jp/apidoc/iap/apilist-ssjs/doc/platform/SessionScopeStorage/index.html)