【ワンランク上のPower Apps開発】 CSVインポート その③ Automateで解析・Appsで登録編 &プログレス表示

Power Apps Power Automate 技術ブログ

はじめに

Power Appsでのファイルインポートのパターンおよび実装サンプルを3回に渡ってご紹介しています。
その①は、ファイルの種類および解析・取り込みのパターンのご紹介
その②は、CSVインポート、解析・取り込みをAutomateで行う実装サンプル
その③は、CSVインポート、解析をAutomate、取り込みをPower Appsで実装し、進捗確認プログレスバーを表示する実装サンプル

となります。

今回は、その③となります。

ちなみに今回本機能を追加した自社アプリはこちらで以前ご紹介しております。アプリの各種実装ポイントを掲載しております。
【ワンランク上のPower Apps 開発】人材スキル管理アプリを作成・運用・改修 | 株式会社エヌサーフ (nsurf.co.jp)

実装の概要

前回はPowerAppsからAutomateへCSVファイルを渡してAutomateで解析(JSON化)、そのままAutomateで登録する内容でしたが、今回は、解析までをAutomateに任せて、Appsへ返却してPatchして更新する内容となります。

その①でも書いてますが、解析したJSONをAppsに返却する方式の場合、プレミアム版なら応答アクションでコレクションにそのまま入れて使えますので一番いいんですが、通常ライセンス版だと文字列としてしか返せないため、従来はIsMatchなどでの解析のカスタム実装が必要でした。が、最近JSONParse関数が実験的機能でリリースされたのでそれだと結構いい感じに使えます。

当初はその②の状態からAppsでPatchに変更しただけだったのですが、作った後にどうせならとプログレスバーと実行ログ一覧画面も追加しました。今回のような時間がかかる処理はローディングはもちろんですが、進捗を表示する機能もあったほうがいいですよね。見た目的にも普通のWebアプリのようにPowerAppsでも簡単に実装できる!ってところをお見せします。

この辺の、作っていっているうちに思いついて、それをサクッと機能追加できるのもPowerAppsのとても良いところだと思います。(進捗プログレス部分、ログ一覧部分の追加調整は1、2時間程度で完成)

実装サンプル

今回作成したサンプル実装です。前回のものからプログレスバー&ログ表示を追加してます。

実装概要

今回は下のような実装サンプルを作成します。
また、Appsのファイルアップ部分とフローでの解析部分はその②のJSON解析部分までを使った内容です。ので、詳細はそっちを見ていただき、本記事では割愛します。★は前回からの変更点です。

  • ファイルの種類:CSV(UTF-8)
  • ファイルのアップ:PowerAppsの添付ファイルコントロールを使ってアップし、Automateへ渡す(PowerAppsV2トリガー)
  • ファイルの解析:Automateでカスタム実装(ループなし)
  • Appsへデータ返却:解析結果をJSON文字列でAppsへ返却する ★
  • データの登録:AppsでSPOへ登録(Patchで追加OR更新) ★
  • 進捗表示:Patch中の状態を表示する ★
  • 結果表示:結果の一覧を表示する ★

① PowerApps上でのファイルアップロード用の実装概要

アプリへのファイルアップロードはSPOの添付ファイルコントロール部分のみ使用してダイアログを作ります。指定したファイルをPowerAppsV2トリガーを使ってAutomateへ渡します。
image.png

内容についてはその② CSVインポート、解析・取り込みをAutomateで行う実装サンプルをご確認ください。※インポートボタンのOnSelectの内容のみ後の工程で変更となります

② PowerAutomateの実装概要

フロー側の実装概要です。
image.png
image.png
その②のフローのうち、JSON解析までおんなじです。
違いとしては返却部分で、成功時はmsgとしてJSON化したやつを返却(文字列となる)してます。エラー時は前と同じでファイルエラーだよと返却してます。
ちなみに待ち時間は本来不要ですが、返却が早すぎたのでデモ用に1秒ウェイトを入れてます。

こちらも該当部分の内容についてはその②をご確認ください。

③ PowerAppsへ返却後の実装概要(プログレスバー対応なし)

ここからが今回の本題となります。
まずは、インポートボタンの処理ですが、まずは以下のように変更となります。

With({csv:First(DataCardValue.Attachments)}
    //ファイルチェック
    ,If(!EndsWith(csv.Name,".csv") && !EndsWith(csv.Name,".CSV")
        ,Notify("CSVファイルを選択してください。",NotificationType.Error);
        ,
        //ファイルインポートフロー実行
        UpdateContext({isLoading:true});
        UpdateContext({flowresult:
            人材スキルCSVインポート2.Run(      
             {  name: csv.Name,
                contentBytes: csv.Value                
            }
        )
        });

        //結果判定
        If(IsBlankOrError(flowresult) || !flowresult.result,
        Notify("インポートに失敗しました。 " & If(!IsBlankOrError(flowresult), flowresult.msg),NotificationType.Error);
        ,
        //JSON解析 委任警告回避のためLookUPで使うメールアドレスのみ先にテキスト化
        ClearCollect(CSVcol,AddColumns(Table(ParseJSON(flowresult.msg)),"mail",(Text(Value.メールアドレス)))); 
        Clear(pathcCol);
        //更新処理
        ForAll(CSVcol,
            With({current:LookUp(人材スキルリスト,メールアドレス = mail).メールアドレス},
                IfError(Patch(人材スキルリスト,
                    //メールアドレス有無で新規、更新を切り分け
                    If(IsBlank(current),Defaults(人材スキルリスト),LookUp(人材スキルリスト,メールアドレス = current)
                    ),
                    {
                        氏名:Text(Value.氏名)
                        ,メールアドレス:mail
                        ,区分:{Value:Text(Value.区分)}
                        ,'所属/部署':{Value:Text(Value.'所属/部署')}
                        ,'スキル(言語)':ForAll(Split(Text(Value.'スキル(言語)'),";") ,{Value:Result})
                        ,'スキル(環境)':ForAll(Split(Text(Value.'スキル(環境)'),";") ,{Value:Result})
                        ,保有資格:ForAll(Split(Text(Value.保有資格),";") ,{Value:Result})
                        ,'スキル(工程)':ForAll(Split(Text(Value.'スキル(工程)'),";") ,{Value:Result})
                        ,'スキル(ポスト)':ForAll(Split(Text(Value.'スキル(ポスト)'),";") ,{Value:Result})
                        ,勤務先:Text(Value.勤務先)
                        ,プロフィール備考:Text(Value.プロフィール備考)
                        ,コメント:Text(Value.コメント)
                        ,稼働:If(Text(Value.稼働)="はい",true,false)
                        ,開始時期:DateValue(Text(Value.開始時期))
                        ,最寄り駅:Text(Value.最寄り駅)
                        ,スキルシート:Text(Value.スキルシート)
                    };
                );
                //結果格納
                Collect(pathcCol,{type:If(IsBlank(current),"新規","更新"),mail:mail,msg:""});
                //失敗時
                ,UpdateIf(pathcCol,mail=mail,{type:"失敗",mail:mail,msg:FirstError.Message})
                );                
            );             
        );
        If(IsBlank(LookUp(pathcCol,type="失敗")),
            Notify(Concatenate("インポートが完了しました。 新規:",CountRows(Filter(pathcCol,type="新規")),"件 更新:" ,CountRows(Filter(pathcCol,type="更新")),"件"), NotificationType.Success);
            ,
            Notify(Concatenate("インポートの一部に失敗しました。 新規:",CountRows(Filter(pathcCol,type="新規")),"件 更新:" ,CountRows(Filter(pathcCol,type="更新")),"件 失敗:",CountRows(Filter(pathcCol,type="失敗")),"件"),NotificationType.Error)
        );

        ResetForm(FormImport); 
        UpdateContext({ImportShow:false,isLoading:false});
        );        
     );    
);
  • ポイントを以下に記載します。
     ・Withを使って何度も同じことは書かないようにしてます
     ・拡張子でCSVか判定してCSV以外はエラーNotifyで終了
     ・ローディングを表示 →終わったら非表示
     ・インポートフローを呼び出し(ここはフロー未完成の場合はエラーになるので本来はフローを用意しておく) 
     └ファイルをフローへ渡す(Withで指定した添付ファイルコントロールのFirstの名前とValue)
      ※PowerAppsV2でのファイル渡しの書き方はググると出てきます
     └フローの結果を変数に入れる(フローからの戻り値はresult:True/False、msg:エラー時はファイルエラーメッセージ、清光寺はJSON文字列)
     ・IsBlankOrErrorまたはresultがFalseの場合は失敗Notify出す(かつメッセージもあれば出す)

上記までは基本前回同様です。違いはフローからの戻りがJSON文字列となっているかの点くらいです。

以降、JSONParseについてです。

0.まず、アプリの機能で「ParseJSON関数と型のないオブジェクト」をオンにする必要があります。
image.png
→ParseJSON関数の使い方については公式ページや公式ブログに載っていますのでこちらご参照ください。
https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-parsejson
https://powerapps.microsoft.com/en-us/blog/power-fx-introducing-parsejson/

1.まずはParseJSON関数でフローからの戻り値を囲みます。そしてテーブル化します。
 この段階で型指定のないテーブルができます。Value.列名で値が取れます。
 ※この段階で各列をForAllで型指定することも可能ですが、後でPatch時にやるので重複しないようここではそのまま

本来はこれでOK Table(ParseJSON(flowresult.msg)

2.今回の実装ではメールアドレスが存在するかしないかで追加OR更新の判定をしています。
  そのため、後の処理でリストからLookupしているんですが、Lookup文内で列の型指定をした場合、委任対象外の警告がでます。。
なので、この段階でメールアドレスのみ型指定して追加で持たせます。結果こうなります(今回の場合の例)

ClearCollect(CSVcol,AddColumns(Table(ParseJSON(flowresult.msg)),"mail",(Text(Value.メールアドレス)))); 

3.続いてpatch部分です。以下までは、
 ・ patchcol→カウント用のレコード(結果ログとしても利用)をクリア
 ・ForAllでCSVcolを回す
 ・追加OR更新用の判定にLookupする
 ・IfErrorで囲む ★機能で数式レベルのエラー管理をオンにしておいてください。
  →以降の部分でエラー時の処理を記載してます
 ・Patch文の中で存在結果をもとに新規OR更新を切り分け 
※このPatchの第2パラメータで分岐すれば同じ内容を2つ書かないで済むので良いです。
※ForAll内では変数設定は出来ないのでCollectで追加する実装をします。

        Clear(pathcCol);
        //更新処理
        ForAll(CSVcol,
            With({current:LookUp(人材スキルリスト,メールアドレス = mail).メールアドレス},
                IfError(Patch(人材スキルリスト,
                    //メールアドレス有無で新規、更新を切り分け
                    If(IsBlank(current),Defaults(人材スキルリスト),LookUp(人材スキルリスト,メールアドレス = current)
                    ),
・・・・

4.Patchの更新情報の部分は以下のような感じになります。SPOの列と対応するCSVテーブルの列を型指定しながらはめます。
  列:型指定(CSVのValue.列名)が基本です。
  選択肢(単一)は{Value:型指定(CSVのValue.列名},
選択肢(複数)はForAllで分割してはめてます。True,Falseや時間も以下のような感じで変換
 ※本来はNULL時の対応も必要かも?

※記載用に重複した列種別は省略して記載してます。
                 {                        
                        氏名:Text(Value.氏名)
                        ,区分:{Value:Text(Value.区分)}
                        ,'スキル(言語)':ForAll(Split(Text(Value.'スキル(言語)'),";") ,{Value:Result})
                        ,稼働:If(Text(Value.稼働)="はい",true,false)
                        ,開始時期:DateValue(Text(Value.開始時期))
                    };

5.Patch後にpatchcolに新規か更新か、メールを入れます。
  次の処理はIfErrorで失敗した際の処理となり、この場合は上記を失敗、取れた最初のエラー情報で更新します。

 //結果格納
                Collect(pathcCol,{type:If(IsBlank(current),"新規","更新"),mail:mail,msg:""});
                //失敗時
                ,UpdateIf(pathcCol,mail=mail,{type:"失敗",mail:mail,msg:FirstError.Message})
                );                
            );             
        );

6.最後は前と同様で結果をNotifyで出してます。件数はpatchColから取ります。
 ※Automateの場合は最後にリフレッシュ必要でしたが、Patchの場合は不要なのでRefleshは消してます。

今回は面倒なのでやってませんが、さらに細かいデータチェックなども上記のどこかでやってあげることも可能です。

④ プログレスバー・結果ギャラリーを追加!

上記まででAutomate版同様の動作・表示となります。
こちらに以下の要素を追加
※正直プログレスバーはなくても、ローディングと進捗の件数表示だけでも十分事足りるかとは思います。が、せっかくなので。

  • 上部に処理概要メッセージ(ファイルチェック中/インポート中)
  • ファイルチェック中(フローから戻ってくるまで)はローディング
    image.png
  • インポート中はプログレスバーを表示(ここもローディングのままのパターンも良いと思います)
  • 全体の件数や対応、種別を表示
    image.png
  • インポート完了時にファイルアイコンを表示
    image.png
  • ファイルアイコンクリックで結果ギャラリーを表示、エラー時はメッセージも表示
    image.png

という感じです。画面周りの表示制御などは割愛しますが、以下記載する修正後の実装の中で、

  • 適宜変数を調整してメッセージ、取り込み時、取り込み後、完了時の表示制御をしてます。
  • 件数はCsvCol(CSV件数)、pathcCol(更新件数と結果)の件数を表示
  • 結果ギャラリーはpathcColに入れたメール、種類、エラー時メッセージを出す、追加で氏名を入れている。
  • プログレスバーはHTMLテキストコントロールでHTML標準のプログレスタグを利用
     ※いまいちCSS調整とか効かない感じでしたが以下でいい感じだったのでそのまま利用。PowerAppsで自作してもいいかもですね。
<HTMLテキストコントロール:プログレスバー>
Concatenate("<div style=""text-align: center""><progress  value='" , CountRows(pathcCol) ,"' max='", CountRows(CSVcol) ,"' style=""height: 40px; Width:80% ""  ></progress></div> ")
  • 最後に一応修正版のコードを載せておきます。変更点は上記参考ください。

最終系サンプルコード

※patchColに氏名を追加した部分でPatch外からだとなぜかText(Value.氏名)が効かなかったので、CSVcol As csvで明示的に名前つけるように調整してます。

With({csv:First(DataCardValue.Attachments)}
    //ファイルチェック
    ,If(!EndsWith(csv.Name,".csv") && !EndsWith(csv.Name,".CSV")
        ,Notify("CSVファイルを選択してください。",NotificationType.Error);
        ,
        //ファイルインポートフロー実行
        UpdateContext({importStatusShow:true,statusMsg:"ファイルチェック中・・"});
        UpdateContext({flowresult:
            人材スキルCSVインポート2.Run(      
             {  name: csv.Name,
                contentBytes: csv.Value                
            }
        )
        });

        //結果判定
        If(IsBlankOrError(flowresult) || !flowresult.result,
        Notify("インポートに失敗しました。 " & If(!IsBlankOrError(flowresult), flowresult.msg),NotificationType.Error);
        ,
        UpdateContext({countMsgShow:true,statusMsg:"インポート中・・"});
        //JSON解析 委任警告回避のためLookUPで使うメールアドレスのみ先にテキスト化
        ClearCollect(CSVcol,AddColumns(Table(ParseJSON(flowresult.msg)),"mail",(Text(Value.メールアドレス)))); 
        Clear(pathcCol);
        //更新処理
        ForAll(CSVcol As csv,
            With({current:LookUp(人材スキルリスト,メールアドレス = csv.mail).メールアドレス},
                IfError(Patch(人材スキルリスト,
                    //メールアドレス有無で新規、更新を切り分け
                    If(IsBlank(current),Defaults(人材スキルリスト),LookUp(人材スキルリスト,メールアドレス = current)
                    ),
                    {
                        氏名:Text(csv.Value.氏名)
                        ,メールアドレス:csv.mail
                        ,区分:{Value:Text(csv.Value.区分)}
                        ,'所属/部署':{Value:Text(csv.Value.'所属/部署')}
                        ,'スキル(言語)':ForAll(Split(Text(csv.Value.'スキル(言語)'),";") ,{Value:Result})
                        ,'スキル(環境)':ForAll(Split(Text(csv.Value.'スキル(環境)'),";") ,{Value:Result})
                        ,保有資格:ForAll(Split(Text(csv.Value.保有資格),";") ,{Value:Result})
                        ,'スキル(工程)':ForAll(Split(Text(csv.Value.'スキル(工程)'),";") ,{Value:Result})
                        ,'スキル(ポスト)':ForAll(Split(Text(csv.Value.'スキル(ポスト)'),";") ,{Value:Result})
                        ,勤務先:Text(csv.Value.勤務先)
                        ,プロフィール備考:Text(csv.Value.プロフィール備考)
                        ,コメント:Text(csv.Value.コメント)
                        ,稼働:If(Text(csv.Value.稼働)="はい",true,false)
                        ,開始時期:DateValue(Text(csv.Value.開始時期))
                        ,最寄り駅:Text(csv.Value.最寄り駅)
                        ,スキルシート:Text(csv.Value.スキルシート)
                    };
                );
                //結果格納
                Collect(pathcCol,{type:If(IsBlank(current),"新規","更新"),mail:csv.mail, name:Text(csv.Value.氏名), msg:""});
                //失敗時
                ,UpdateIf(pathcCol,mail=csv.mail,{type:"失敗",msg:FirstError.Message})
                );                
            );             
        );
        If(IsBlank(LookUp(pathcCol,type="失敗")),
            Notify(Concatenate("インポートが完了しました。 新規:",CountRows(Filter(pathcCol,type="新規")),"件 更新:" ,CountRows(Filter(pathcCol,type="更新")),"件"), NotificationType.Success);
            UpdateContext({statusMsg:"インポートが完了しました!",logShow:true});
            ,
            Notify(Concatenate("インポートの一部に失敗しました。 新規:",CountRows(Filter(pathcCol,type="新規")),"件 更新:" ,CountRows(Filter(pathcCol,type="更新")),"件 失敗:",CountRows(Filter(pathcCol,type="失敗")),"件"),NotificationType.Error);
            UpdateContext({statusMsg:"インポートの一部に失敗しました",logShow:true});
        );

        ResetForm(FormImport); 

        );        
     );    
);

おわりに

今回までで3回にわたってPowerAppsでのCSVインポートのサンプル実装をご紹介しました。
自社アプリ用の実装なのでそのままの活用は難しいと思いますが、ポイント的に参考になる部分などあれば、上記サンプルをベースにお試しください。(サンプルなので不備もあるかと思いますのでご容赦ください)

その① ファイルの種類および解析・取り込みのパターンのご紹介
その② CSVインポート、解析・取り込みをAutomateで行う実装サンプル
その③ CSVインポート、解析をAutomate、取り込みをPower Appsで実装し、進捗確認プログレスバーを表示する実装サンプル

【お知らせ】 Power Platform コンサルティングサービスのご提供を開始いたしました。

本サービスはPower Platform(主にPower Apps、Power Automate)をご利用のお客様において、開発時や運用時のお困りごとに対し、チャットやTV会議を用いてのQA対応やアドバイス、サンプルコードのご提供などの業務サポートを行い、お客様のDX化推進業務を強くサポートするサービスとなります。

導入事例


今回ご紹介したアプリや本ブログにご興味をお持ちになられましたら、技術支援や同様のカスタマイズ開発など、各種ご支援させていただきますので、お気軽に「お問い合わせフォーム」よりお問い合わせください。
今後も自社で開発したお役立ちアプリや技術支援を行ったアプリのご紹介など、定期的に更新を行ってまいります。

関連記事