コルネの進捗や備忘録が記されたなにか

進捗や成果物や備忘録てきななにかを雑に更新していきます。

Power Automateを利用してSharePointリストのテナント間移行を行う

はじめに

SharePointリストのテナント間移行を行いたい人なんて少数かもしれませんが、この記事では以下のやり方も纏めてあります。

  • Power Automateを用いて特定のSPOリストからフィールド情報を取得する
  • Power Automateを用いてリストを作成する
  • Power Automateを用いて特定のリストにフィールドを追加する
  • Power Automateのアクションをコピペする(おまけ)

PnPとか使ってスクリプト書く方法もありますが、テナントによっては管理者に許可されていない場合もありますからね。

ざっくり構成

こんな感じで

  1. Tenant A(移行元)でリストのフィールド情報をPower Automateを利用して取得
  2. Tenant B(移行先)のPower Automateに 1. で取得した情報を設定
  3. Tenant Bにリストを作成

を行います。

手順2は手動ですね。

フローを作成する

リストの情報を取得する

フローの全体イメージはこんな感じです。

こちらは移行元のテナント(Tenant A)で作成して実行します。

アクションを順にみていきましょう。

リストが存在するサイトのアドレスでは取得したいリストが存在するアドレスのリンクを設定します。

リスト名では取得したいリストの表示名を設定します。

SharePoint に HTTP 要求を送信しますで上記変数で設定した情報をもとにリストのフィールドを取得します。
今回の設定では、デフォルトフィールドは取得しないように省いています。
ユーザが作成したフィールドだけが欲しいですからね。

もしすべてのフィールドを取得したければ、filterを外してください。

このアクションの出力結果として、以下のようなbodyが取得できます。

欲しいのはd.resultsなので、結果を加工して作成に格納します。
この結果は配列で取得されるのでarray関数を利用しています。

array(body('SharePoint_に_HTTP_要求を送信します')?['d']?['results'])

これで対象リストのフィールド情報が取得できました。

「作成」で得られた出力はこの後使いますので控えておいてください。

リストを作成してフィールドを作成する

フローの全体イメージはこんな感じです。

こちらは移行先のテナント(Tenant B)で作成して実行します。

まずはリスト作成に必要な情報だけみていきましょう。

リストを作成するサイトのアドレスにはリストを作成したいサイトのアドレスを設定します。

リスト名には作成したいリストの内部名(表示名にも利用します)を設定します。

残り二つの変数は、フィールド作成の時に利用するものですので、後述します。

SharePoint に HTTP 要求を送信します(リストの作成)で先ほど設定した2つの変数をもとにリストを作成します。

説明を変えたい場合は、"Description"を変更してください。

リストを作成している箇所は以上です。

続いて先ほど作成したリストに先ほど取得したフィールド情報をもとにフィールドを作成していきます。

ListFieldObjに先ほど移行元で取得したフィールド情報をコピペします。

booleanは条件分岐のtrue or falseの判定に利用したいだけですので、trueとだけ設定しておきます。

続いてリストのフィールド作成を実行していくわけですが、フィールド作成は1フィールドずつしか行えません(多分。知らんけど。)

試しにArray情報を渡してみたらObject情報を渡せ。とのエラーで怒られました。

よって先ほど設定したArray変数からObject単位で情報を取得してアクションを実行していこうと思います。

これを実現するためにApply to eachを利用しています。

まず最初のJSON の解析ではApply to eachのitems()つまり、1フィールドの情報に対して解析を行っています。

ここは、フィールドの型などによってサンプルから生成したスキーマが異なり、そのまま利用するとエラーになってしまうので、地味にめんどくさかったですね。

続いて、JSON の解析(metadata)では__metadataの内容のうちtypeだけが欲しかったので、解析を行っています。
'body('JSON_の解析')?['__metadata']?['type']でもよかったかもですね。(試してはないです)

いよいよフィールドの作成を行っていくのですが、フィールドの型が選択肢か否か。でPOSTする際のBodyが変わってきます。

今ブログを書いているときに思いましたが、型がSP.FieldChoiceどうか?で判断すればよかったかな?とは思いましたが、今回作成したフローでは選択肢が設定されているかどうか?で判断しています。

empty(body('JSON_の解析')?['Choices'])選択肢が空であれば、Choicesを利用しません。

まずがChoicesを利用しない(Bodyに設定しない)パターンです。

SharePoint に HTTP 要求を送信します(フィールドの作成)では選択肢を作成しない場合のフィールドの作成を行っています。
リストは先ほど作成したリストを指定しています。

続いてChoicesを利用する(Bodyに設定する)パターンです。

Choiceで得られた結果は以下のようになっています。

"Choices": {
    "__metadata": {
        "type": "Collection(Edm.String)"
    },
    "results": [
        "選択肢1",
        "選択肢2",
        "選択肢3"
    ]
}

このうち欲しいのはresultsの結果ですので、JSON の解析(Choices)で抜け出せるようにしてあげます。

SharePoint に HTTP 要求を送信します(フィールドの作成(Choicesあり))では選択肢を作成する場合のフィールドの作成を行っています。
選択肢には先ほどの解析で得られた選択肢を設定します。

リストは先ほど作成したリストを指定しています。

以上でTenant A(移行元)からTenant B(移行先)へSPOリストを移行させる仕組みを作成することができました。

とはいえ色々な型やフィールドの設定などは未検証なため正しく動かない部分があるかもです。

もしそのような場合は、どのようなフィールド設定がエラーとなったか?エラー内容はどのようなものか?を教えていただけると助かります。

フローコピペ用コード

リスト情報を取得する

リストが存在するサイトのアドレス

リスト名

SharePoint に HTTP 要求を送信します

作成

リストを作成してフィールドを作成する

リストを作成するサイトのアドレス

リスト名

ListFieldObj

boolean

SharePoint に HTTP 要求を送信します(リストの作成)

Apply to each

なにこのコード?どうやって使うの?

いずれかのコードをクリップボードにコピーした状態でフローを開いて、

「自分のクリップボード > Ctrl + V」をすると、アクションが選択肢に追加されますので、それを選択すると、フロー内にアクションが追加されます。
便利

ここには自分のテナント内でコピーしたアクションであればCtrl + Vをしなくとも、自動でアクションが溜まっていきます。
もちろんログアウト行ったりするとリセットされます。

ただこの機能、プレビューなだけあって「条件」アクション内や「スイッチ」アクション内などにはこの方法でアクションを追加することはできない。などの制限があるようです。
残念

おわりに

短時間で作ったフローなのでちょっと作り雑だったり検証不足だったりする箇所があります。
お気づきの点があればご指摘いただければー

それでは

Power AppsのIndex関数について + 宣伝

はじめに

Power Appsに最近インデックス関数が追加されましたのでその紹介 + 宣伝(こっちがほぼメイン)を行います。

Index関数って?

最近Power Appsの関数に新しくIndex関数が追加されたことを皆さんはご存知でしょうか?

こいつを使うと、なんとテーブル内のN番目のレコードを取得することができます。

今までは Last(FirstN([テーブル], N) みたいなちょっとめんどくさいやり方でN番目のレコードを取得していましたが、Index関数を使うと Index([テーブル], N) だけでN番目のレコードを取得することができるようになりました!

便利ですね!

皆さんも今度からはN番目のレコードを取得したい。なんてときはIndex関数を使うようにしましょう。

ちなみに5/8(日)現在では日本語の公式リファレンスは更新されていませんでした。。。

docs.microsoft.com

英語で見ましょう。
docs.microsoft.com

宣伝

こちらのIndex関数についてゆっくりボイスで解説した動画を出しました!

youtu.be

この娘は新しいうちの娘の「夢乃 るぅ」です。
かわいい(自画自賛)

今後、「るぅの一口講座」シリーズは定期的にアップできるように頑張りたいと思っておりますので、皆さま励みになりますので応援の程よろしくお願いいたします!

不定期でPower Automateのフローを実行する

はじめに

特定の感覚ではなく、不定期でPower Autometのフローを実行したいなー。。。と思うときありますよね?

あるんですよ。

今回はそういったロジックの作成方法について纏めます。

仕組み

構成図

この仕組みを実現するために、下記のような構成を作成します。

f:id:koruneko:20220326174405p:plain

ロジック

不定期とはいえなにかトリガーがあり、そのトリガーの情報でいつフローを実行すればよいのか?が判断できるかと思います。
そのトリガーに今回はPower Appsを用いています。

SPOへのアイテム登録は必須ではないです。
今回例で作成するアプリでたまたま利用するので入っているだけです。

このロジックの肝となるのはOutlook予定表です。

Power Automateでフローを作成する際、なんらかのトリガーを設定する必要があります。
そのトリガーを疑似的に不定期で実行させるための要がOutlook予定表になります。

Power Automateには「予定しているイベントが間もなく開始されるとき」というトリガーが存在します。

f:id:koruneko:20220326180606p:plain

これを使い不定期な予定を作成し、それをトリガーにフローを実行するというわけです。

仕組みさえわかってしまえばそこまで難しいことはないですね。

Power Appsで予定を作成する

Power AppsでOutlook予定表を作成するには、「Office365Outlook」コネクタの Office365Outlook.V4CalendarPostItem を利用します。

こちらのアクションの詳細は V4CalendarPostItem をご確認ください。

今回サンプルとして、食料品管理アプリを用意しました。
ここで登録した食料品の消費期限/賞味期限が近付いてきたら通知するようなアプリを作成します。

f:id:koruneko:20220326182106p:plain

SPOリストをデータソースとしているので、そこへの登録が完了したら予定表を作成するようにします。

また、どうせならSPOリストのアイテムと予定表でどれが紐づいているのかがわかるようにしたいと思います。

そこで、SPOリストのフィールドには予定表のリンクを記載するためのフィールドを作成しておきます。

f:id:koruneko:20220326182523p:plain

予定表の方には、本文にSPOリストのアイテムへのリンクを記載するようにします。

以上を式にするとこのようになります。

Form.OnSuccess

Patch(
    食料品管理,
    LookUp(食料品管理, ID = Self.LastSubmit.ID),
    {
        Outlookのスケジュールリンク:
        Office365Outlook.V4CalendarPostItem(
            LookUp(Office365Outlook.CalendarGetTables().value, DisplayName = "食料品管理", Name),
            Self.LastSubmit.品名,
            Self.LastSubmit.'消費期限/賞味期限',
            Self.LastSubmit.'消費期限/賞味期限',
            "(UTC+09:00) Osaka, Sapporo, Tokyo",
            {
                body: 
                    "<a href=
                        'https://XXXX.sharepoint.com/sites/XXXX/Lists/GroceryManagement/DispForm.aspx?ID=" & Self.LastSubmit.ID & "'>
                        SharePointでアイテムを開く
                    </a>",
                reminderMinutesBeforeStart: 30,
                isReminderOn: true,
                showAs: "free"
            }
        ).webLink
    }
);

予定表には事前に「食料品管理」という予定を作成しておいています。

f:id:koruneko:20220326182818p:plain

こちらの予定表に予定が作成されていきます。

こちらの予定を作成したことで、なにか予定がある。と思われるのも嫌なので showAsfree に設定することで「空き時間」に設定しています。

bodyに設定している代替テキストは各自の環境に合わせて適宜設定してください。

また、SPOリストのハイパーリンクフィールドですが、こちら現時点ではPower Appsから登録するときに代替テキストを設定することができないので、そこは諦めてください。
もしどうしても代替テキストを設定したい場合は、Power Automateを使うとかしてください。

Power Appsからも設定できるようになるといいですね。

これで予定表を作成すると、このような登録が行われます。

f:id:koruneko:20220326183451p:plain

また、リストにはこのような登録がなされます。

f:id:koruneko:20220326183555p:plain

不定期で実行するフローを作成する

先ほども説明したので簡単に記載しますが、トリガーには先ほど作成された予定を用います。

これには「予定しているイベントが間もなく開始されるとき(V3)」というトリガーを利用します。

f:id:koruneko:20220326180606p:plain

カレンダーIDには「食料品管理」を選択します。
ルックアヘッド タイムという英語をカタカナにしただけのところには、予定開始の何分前をトリガーにフローを実行するか?を設定します。

例えば、予定表に登録されたとある予定の開始時間が「2022/3/26 18:00」だったとします。
スクショのように'5'を設定していた場合、このフローが実行されるのは「2022/3/26 17:55」です。

f:id:koruneko:20220326183854p:plain

残りのアクションは各自実行したいフローを好きに設定するだけでOKです。

今回のサンプルでは、自身の携帯に通知するぐらいでいいのでこんな感じになりました。

f:id:koruneko:20220326184409p:plain

おわりに

不定期にPower Automateのフローを実行させる。ということをPower Automateだけで考えると難しくなってしまうかもですが、他のサービスとうまく組み合わせることによって簡単に実装することができました。

今回記載したPower Appsの式ですが、実際運用するうえでは以下のことが考慮されていないです。

  • 予定を削除したいとき
  • 予定を変更したいとき

なのでもう少し改良が必要です。

Power Appsでラベルにテーブルの情報を表示させる(ユーザ/グループ列)

はじめに

今回はこちらの質問に回答します。

f:id:koruneko:20220313172614p:plain

ユーザ情報を表示する

今回は以下のようなリストを用意しました。

f:id:koruneko:20220313174753p:plain

こちらの「Users」列が「ユーザーとグループ列」になっています。

f:id:koruneko:20220313174603p:plain

このリストをもとにアプリを作成していきます。

BrowseScreenといわれていることから、0からアプリを作成しているのではなく、リストからアプリを作成されているかと思いますので、リストからのアプリ作成をベースに説明します。

ラベルにユーザ情報を表示させる

まず、ラベル内でUsers情報を表示させようとすると、以下のようなエラーが発生するかと思います。

f:id:koruneko:20220313175149p:plain

これは、エラーでも記載されている通りラベルにはText値が必要なのに対し、現在設定されている ThisItem.Users はテーブル型なのが原因です。

なのでテーブル型の情報を加工してテキスト型にしてあげます。

今回欲しい情報がユーザ/グループの表示名だと仮定し、これらを表示させるようにします。
この表示名は ThisItem.Users.DisplayName で取得できます。
ただ、これもテーブル型ですので、このままでは使えません。

f:id:koruneko:20220313175653p:plain

そこでConcat関数を用いて、テーブル内の特定のフィールドに設定されている値を結合させて1つの文字列に設定してあげます。

例えば、表示名を「; 」で区切って表示させようとすると以下のような式になります。

Label.Text

Concat(ThisItem.Users, DisplayName & "; ")

これにより、下記の表示になります。

f:id:koruneko:20220313180122p:plain

DetailScreenと同じような表示にする

DetailScreenでこの「Users」フィールドを表示した場合、設定されているユーザ/グループが多いと以下のような表示になります。

f:id:koruneko:20220313180439p:plain

⇓ 「すべて表示」選択

f:id:koruneko:20220313180452p:plain

これと同じ表示をBrowseScreenでも行ってみます。

まずこちらですが画面から、ラベルではなくコンボボックスが利用されている。表示モードは「ビュー」である。ということが確認できるかと思います。

f:id:koruneko:20220313181359p:plain

なのでBrowseScreenに、コンボボックスをビューモードで追加します。

残り設定が必要な箇所は ItemsDefaultSelectedItemsDisplayFields です。(座標やサイズはイイ感じに設定しておいてください。)

Items にはコンボボックスをユーザが操作したときの選択肢を設定します。
今回このコンボボックスはビューモード固定なので、直接は関係ないですし、最悪設定していなくていいですが、後述するフィールドの設定でここ設定されていないとGUIでの設定ができないので、設定することによってデメリットが発生するわけでもないですし、(本来設定されてるべき項目なので)設定しておきましょう。

特定リストのフィールドの選択肢はChoices関数で取得可能です。

ComboBox.Items

Choices(UserLists.Users)

DefaultSelectedItems では、デフォルトで設定されている値を設定します。
よく間違えて Default に設定されて、期待通りの動作をしない!
という声をききますが、ドキュメントの通り設定すべきは DefaultSelectedItems なので気を付けましょう。

docs.microsoft.com

今回表示させていのはユーザ/グループ情報なので以下を設定します。

ComboBox.DefaultSelectedItems

ThisItem.Users

恐らくここまで設定が完了すると「i:0#.f~」や「c:0o.c~」のような文字列で始まるClaims情報が画面に表示されているのではないかな?と思います。

今回表示させたいのは「DisplayName」(表示名)ですので最後にこちらを変更します。

「フィールド」の「編集」を選択して「主要なテキスト」で「DisplayName」を選択してください。

f:id:koruneko:20220313184039p:plain

これで表示名が表示されるようになりました!

f:id:koruneko:20220313184210p:plain

おわりに

以上、BrowseScreenでユーザ/グループ情報を表示させる方法でした。

Power Appsはローコードですが、とはいえデータ型は意識して開発を進めた方がいいと思います。

他質問などある方いらっしゃいましたらお気軽にどうぞー

marshmallow-qa.com

Plannerの内容をPower Automateで通知する

はじめに

私のマシュマロにて以下のような質問をいただいたので今回はこちらの回答 + α について纏めたいと思います。

f:id:koruneko:20220227153013p:plain

関連記事

私が以前書いた記事で「Power Automate でPlanner の情報を取得する」というものがあります。

koruneko.hatenablog.com

この記事では

  • タスクの内容
  • タスクのメモ
  • タスクのコメント

を取得する方法について記載されています。
今回の記事ではそこまで掘り下げては記載しないので、気になる方は上記記事をご覧になってください。

フローを作成する

スケジュールを設定する

質問者さんは、「毎日9時に実行」という設定はできているようですが、こちらの設定方法も記載しておきます。

「毎日9時に実行」なので「スケジュール」コネクタをトリガーに設定します。
また、フローを実行したいのは平日かな?と思いましたので、月~金のみ実行するようにします。

上記を設定すると以下のようになります。

f:id:koruneko:20220227155016p:plain

プレビューでいつ実行されるのか記載してくれているのでわかりやすいですね。

Plannerの情報を取得する

続いてPlannerのタスクを一覧で取得します。

ちなみに今回取得するプランには以下のようなタスクが登録されています。

f:id:koruneko:20220227155424p:plain

ここに登録されているタスクを取得するにはPlannerコネクタの「タスクを一覧表示します」を使用します。

取得したい対象の対象のプランを選択するだけです。

f:id:koruneko:20220227155612p:plain

このアクションにより以下のような情報が取得できます。

key 説明
@@odata.etag 識別子
planId 対象のタスクが登録されているプランのID
bucketId 対象のタスクが登録されているバケットのID
title 対象のタスクのタイトル
orderHint リストビューでこのタイプの項目を順番に並べるためのヒント(参考)
assigneePriority リストビューでこのタイプの項目を順番に並べるためのヒント(参考)
percentComplete タスクの完了の割合(100に設定すると完成とみなされる)
startDateTime タスクの開始日時 ※UTC
createdDateTime タスクが作成された日時 ※UTC
dueDateTime タスクの期限日時 ※UTC
hasDescription タスクに詳細が設定されている場合はtrueそうでない場合はfalse
previewType タスクに表示されるプレビューの種類
使用可能な値: automatic, noPreview checklist, description, reference
referenceCount タスクに存在している外部参照の数
checklistItemCount タスクに存在するチェックリストの数
activeChecklistItemCount チェックリストの項目数
値がfalseである場合は、不完全な項目が存在している
id 対象のタスクのID
createdBy 対象のタスクを作成したユーザのID
appliedCategories 対象のタスクが適用されているカテゴリ(詳細は「適用されるカテゴリ参照」)
assignments タスクが割り当てられている担当者
_assignments ????

参考文献
Planner - Connectors | Microsoft Docs
plannerTask のリソースの種類 - Microsoft Graph v1.0 | Microsoft Docs

_assignmentsがいまいちわからなかった。
なんだこいつ

これでタスク一覧が取得できました。
だいたい欲しい情報はこれで取得できたかと思います。

より細かい情報が欲しい場合は、「タスクの詳細を取得する」アクションで取得を行ってください。
使い方については冒頭で紹介した記事を参照してください。

条件を設定する

Apply to eachを使用するやり方

まず最初に紹介するのは、おそらく一番簡単な方法で「条件」アクションを使用する方法です。

こちらはプログラムが少しわかる方向けに説明するとif分岐ですね。

条件にて条件式を記載して、はい(true)の場合 / いいえ(false)の場合の処理をそれぞれ記載します。

今回は、

  • 期限
  • タイトル

の2つを条件に設定します。
また、質問者さんの書き方からしてこれら2つの条件すべてに一致した場合~だと思いますので、条件はAndとします。

実際に設定すると以下のようになります。

f:id:koruneko:20220227173341p:plain

Apply to eachは「タスクを一覧表示します」の値を設定した際に自動で作成されます。
これは「タスクを一覧表示します」で取得した結果がアレイ型であるからです。

Apply to eachはループ処理だと思ってください。

期日の条件は1段目で、条件は適当に期日が本日以前だった場合。としています。
その際の式は以下です。

左辺

formatDateTime(addHours(items('Apply_to_each')?['dueDateTime'], 9) 'yyyy/MM/dd')

右辺

formatDateTime(addHours(utcNow(), 9) 'yyyy/MM/dd')

リファレンスにも合った通り、時間はUTCでとれますのでJSTに変換して計算しています。
また、みたいのは日にちのみで時間はみなくともよいので、formatDateTime 関数で時間のフォーマットをしています。

その他の時間フォーマットはこちらをご参照ください。
koruneko.hatenablog.com

タイトルの条件は2段目で、右辺の文字に一致する場合。としています。

もし部分一致にしたい場合はindexOf 関数を利用します。

この関数は第一引数に設定した特定の文字列内に、第二引数で設定した特定の文字列が何番目の文字列で登場するか?を返します。
このカウントは0から始まり、また見つからなかった場合は-1を返します。

なので以下のように記載します。

左辺

indexOf(items('Apply_to_each')?['title'], 'todo')

条件

次の値より大きい

右辺

-1

なおindexOf 関数は大文字小文字を区別しないので注意してください。

Apply to eachを使用しないやり方

次にApply to eachを(極力)使用しないやり方です。
私はこちらを推奨し、またApply to eachを使用しないでいいやり方がある場合は極力そちらを推奨します。(もちろんメンバのレベルに合わせた運用にすることも大事です。)

なぜわざわざApply to eachを使用しないことを推奨するかというと、性能の問題です。
詳しくはHiroさんのこちらの記事が参考になると思います。

mofumofupower.hatenablog.com

また、極力と記載したのは後続処理でどうしても使わなくてはならない場面が出てくるからです。

さて、Apply to eachを使わないやり方ですが、これには「アレイのフィルター処理」を利用します。

翻訳がいまいちいけてないですが「差出人」には処理を行う配列を設定します。
今回ですと「タスクを一覧表示します」で得られた結果ですね。

差出人

@outputs('タスクを一覧表示します')?['body/value']

余談ですが、この「差出人」は英語ですと「From」です。

次にフィルタ条件を2段目に記載するわけですが、ここで先ほどと同じ条件で結果をフィルタするには大きく2パターン存在します。

1つ目は無理やりこの関数バーに書く方法です。
デフォルトの基本モードでは、複雑な式を記載することができないので、「詳細モードで編集」で式を記載します。

先ほどの条件に記載した式たちを1つの式に纏めると以下のようになります。

@and(
   equals(item()?['title'], 'ToDo'), 
   lessOrEquals(addHours(formatDateTime(item()?['dueDateTime'], 9) 'yyyy/MM/dd'), formatDateTime(addHours(utcNow(), 9), 'yyyy/MM/dd'))
)

この式を設定した状態で基本モードに戻すことはできません。

f:id:koruneko:20220227233555p:plain

また、このやり方ですと式を記載している途中にオートコンプリートも表示されませんし(作成難易度が高い)、設定されている式を確認するのがちょっと見ずらいので運用面であまりよくありません。

ではどうするか?というと「アレイのフィルター処理」を2つに分けます。

具体的には以下のようにします。

f:id:koruneko:20220227235252p:plain

このようにそれぞれのフィルタ条件ごとにアクションを追加します。

「アレイのフィルター処理(期日のフィルタ)」では「アレイのフィルター処理(タスク名のフィルタ)」で得られた結果を設定しています。

差出人

@body('アレイのフィルター処理(タスク名のフィルタ)')

フィルタ条件の箇所に記載している式は「条件」で設定した式とほぼ同じ(items('Apply_to_each')item()になっている)ぐらいですので、説明は割愛します。

これでフィルタの条件が整いました。

いったんこの記事では一応質問者さんが知りたかった?タイトルと期日でのフィルタができるようになったのでこれで一区切りにします。

おわりに

これで一区切りにする。
とは言いましたが、フローとしてはまだ完成していないので、また続きを書いていきたいと思います。

質問者さんの文面では、「特定の担当者に」と記載されているので固定のユーザ(マネージャー?)の可能性もありますが、せっかくですのでタスクに割り当てられたユーザに対してメールを送るようにしたいと思っています。

タスクに割り当てられたユーザは"assignments"に設定されていました。
ただ、取得結果をみるに、

{
  "XXX-XXX-XXXX": {
    "@odata.type": "#microsoft.graph.plannerAssignment",
    "assignedDateTime": "2022-02-27T03:46:06.6898942Z",
    "orderHint": "",
    "assignedBy": {
      "user": {
        "displayName": null,
        "id": ""
      }
    }
  },
  "YYY-YYY-YYYY": {
    "@odata.type": "#microsoft.graph.plannerAssignment",
    "assignedDateTime": "2022-02-27T15:31:41.0967091Z",
    "orderHint": "",
    "assignedBy": {
      "user": {
        "displayName": null,
        "id": ""
      }
    }
  }
}

のように
キー値 = ID
で記載されているんですよね。。。
くそですね

これどうやって取得すればいいんだ。。。 またなにか良い案思いついたら記事にします。

ちなみに今後の説明では最後に説明した、アレイのフィルタを2つ使うやり方で実施していきます。

Power AppsでSPOリストに委任問題をどうにかして回避する

はじめに

2/19(土)にこちらの勉強会に参加させていただきました。
Power Apps オンライン勉強会 ~つまづきポイント対策会~ - connpass

そのときにSPOリストでの委任問題について触れられていましたので、SPOリストでの委任問題の回避方法を纏めようと思います。

使用するデータ

今回の検証には以下のようなSPOリストを利用します。

f:id:koruneko:20220219175932p:plain f:id:koruneko:20220219180034p:plain f:id:koruneko:20220219180845p:plain

アイテム数が2,500件もあるユーザ情報が格納されたリストです。
なお、ここで表示されているアイテムは自動生成されたテストデータであり、実在の人物とは一切関係がないのでご注意ください。

委任問題とは?

そもそも委任問題ってなんぞや?という方もおられると思いますので簡単に触れておきます。

SPOリストでの委任問題とは、Power AppsからSPOリストのデータを取得する際に複雑な式をSPOリストに対して渡した際に発生する警告です。
この警告が発生すると、数式が正常に動作せず期待通りの結果を得られない可能性があります。

より詳しく知りたい方は、公式docsの委任問題に関する記載を参照してください。
docs.microsoft.com

例えばSearch関数をSPOリストからアイテムを取得しようとした際などに発生します。

f:id:koruneko:20220219181420p:plain

困りますね。。。

回避方法

データをコレクションに格納する

一番簡単なやり方?ですかね。

SPOリストのアイテムをコレクションに格納して、そのコレクションに対してフィルタなどを行う方法です。

// アイテムをコレクションに格納 
ClearCollect(
    SPOData,
    委任問題
)
// 特定のアイテムを取得
Search(SPOData, NameTxt.Text, "Name")

これで委任問題はでなくなりました。
f:id:koruneko:20220219182812p:plain

ただ1点問題があります。
このやり方ですとデータ行の制限事項にもろに引っかかってしまいます。

まず、Power Appsでは「データ行の制限」という制限があります。
f:id:koruneko:20220219185306p:plain

これは、
設定 > 全般 > データ行の制限
から確認することができ、デフォルトでは500件に設定されています。

これによりなにが起きるか?というと、例えば上でSPOリストの値をコレクションに格納しましたが、ここで取得できる件数に影響がでます。

CountRows(SPOData)で実際に取得できた件数を確認してみましょう。

その結果としては以下のように500という数値が得られるかと思います。
f:id:koruneko:20220219185627p:plain

次に「データ行の制限」を1000に変更して、コレクションを再取得してみましょう。
f:id:koruneko:20220219185759p:plain

そうすると、今度は1,000件のアイテムが取得できたかと思います。
f:id:koruneko:20220219185832p:plain

続いて、Search関数を設定しているわけですので、テキストボックスに値を入力してどのようなアイテムが取得できるのか確認してみましょう。

Search関数は完全一致ではなく、部分一致での検索が可能なので「菊」という値で部分一致検索をしてみます。

ちなみにExcelで元データに対してフィルタを行った場合は、20レコード取得することができました。
f:id:koruneko:20220219190549p:plain

これをPower Appsで実行すると以下のような結果になります。
f:id:koruneko:20220219190540p:plain

8件しか取得できてないですね。

これはそもそもPower Apps側でコレクションしたアイテムが1,000件しかなく、そのアイテムに対してフィルタ処理を行っているので結果に差異が生まれてしまっているのです。

つまり取得できなかった1,500件のアイテムが完全に抜け落ちてしまっている。ということになります。

では、「データ行の制限」の値を9999999とかに設定すれば、実質制限なしでできるのでは?と考えてしまうかもしれませんが、そう甘くはありません。

ここに設定できる数は2,000までです。
f:id:koruneko:20220219190027p:plain

なので今回の検証のような2,500件ものアイテムがあるようなリストですと、正確なアイテム操作ができないのです。

また、Power Appsにデータを一度すべて持ってしまいますので、スペックの低いPCやネットワークが弱い環境で実施すると、アプリの性能に影響が出てしまうので注意が必要です。

纏めると、コレクション格納により委任問題を回避する際は、

  • データ行の制限に気を付ける
  • アプリの性能が落ちてしまう可能性があることに留意する

という問題に注意する必要があります。

With関数を利用する

次に試すのはWith関数を利用する方法です。

Docsの引用になりますが、With関数を使用すると、自己完結型で理解しやすく、宣言式のコンテキストで使用できるため、コンテキスト変数やグローバル変数よりも使用が推奨されているようです。

フィルタを行う式には以下のように記載します。

With(
    {tmpData:委任問題},
    Search(tmpData, NameTxt.Text, "Name")
)

次に先ほどと同様にこの式で「菊」という文字でフィルタしてみます。
(データ行の制限は先ほど最後に試した1,000で行っています。今後も1,000で実施します。)
その結果として、先ほどと同じ8件のデータを取得することができました。
f:id:koruneko:20220220235937p:plain

これは何故か?というとWith関数もコレクションした場合と同じで

  1. アイテムをPower Apps側に取得
  2. 「1」で取得したアイテムに対してフィルタ処理

という順序になるので、同様のデータ行の制限が課せられてしまうのです。

ただ、With関数はグルーバル変数やコンテキスト変数よりも推奨されているようですので、リストのアイテムを保持してもここでしか使わない。(他のところでの利用予定はない)という場合はWith関数を利用するようにしたほうがパフォーマンス面を考慮するとよいかもしれませんね。
* パフォーマンスの検証はしてないです。

ForAll関数を利用する

次に試すのはForAll関数を利用する方法です。

ForAll関数では、簡単にいいますと第一引数に設定したテーブルに対してループ処理のようなことを行うことができます。

以下のようにフィルタします。

Filter(
    ForAll(
        委任問題,
        If(
            NameTxt.Text in ThisRecord.Name,
            ThisRecord
        )
    ),
    // 条件に合致していないレコードはBlankなのでIsBlank関数で弾く
    !IsBlank(ThisRecord)
)

こちらもこれまで同様、ForAllの第一引数でまずはアイテムを取得してその後その取得したアイテムに対して処理をする。
という流れになるので、データ行の制限に引っかかってしまいます。

AddColumns関数を利用する

次はAddColumns関数を利用する方法です。

やり方としては、表示 / 非表示 を判断する列を追加してフィルタを行う方法です。

式にすると以下のようになります。

Filter(
    AddColumns(
        委任問題,
        "isView",
        NameTxt.Text in Name
    ),
    isView
)

こちらもまずAddColumnsで取得できる件数がデータ行の制限に引っかかってしまいますので、厳密なフィルタ処理はできません。

おわりに

色々やり方まとめましたが、SPOリストの委任問題を解決する際はデータ行の制限にも気を付ける必要があることが分かってもらえたかと思います。

ただ、こんなにたくさんのデータ量を使用するなら素直にDataverseや他DBの利用を検討するようにしたほうが運用や開発面では楽になるかもしれません。
そこはプロジェクトの予算などと応相談ですね。

Power Apps からSPOリストの日付フィールドを空で更新する

はじめに

お久しぶりのブログ更新になります。
最近やりたいことが渋滞して逆になにも進められていないという悪循環に陥っております。

近況報告はこの程度にしておきまして、今回はPower Apps からSPOリストの値を空(Blank)で更新する方法を纏めたいと思います。

最近Power Apps からSPOリストの日付フィールドを空で更新したい!!と思って試してみたところ、空の値をSubmitFormPatchでPower Apps からSPOリストに渡してみたところ、なんと特になにもエラーがでていないにも関わらず値が前回のまま更新されずに残ってしまう。
という事象が発生してしまいました。

この事象の解決方法と、なぜこれで解決できるのか?について備忘録を今回は記載します。

とりあえず解決方法だけ知りたい方向け

以下設定を行うだけで今回の問題は解決できます。

設定 > 実験段階 > 数式レベルのエラー管理
がデフォルトですと「オフ」になっているので「オン」に設定します。

f:id:koruneko:20220122221053p:plain

これだけで、今回の問題は解決します。

発生した事象

今回の問題は「アイテムを空の値で更新できない」というものですので、まずは適当なデータを作成します。

  1. 適当なSPOリストを用意して、そのリストをもとにアプリを作成します。
    f:id:koruneko:20220122221601p:plain
  2. 適当なデータを作成します。
    f:id:koruneko:20220122221748p:plain
  3. 無事アイテムを登録することができました!
    f:id:koruneko:20220122222018p:plain
  4. ではいよいよ本題です。
    こちらのアイテムを空のデータで更新したいと思います。
    Power Apps でこちらのアイテムを選択してDispScreen1 へ遷移し、IconEdit1 を選択してEditScreen1 へ遷移します。
    f:id:koruneko:20220122222435p:plain
    f:id:koruneko:20220122222452p:plain
  5. 必須項目(Title)以外の入力項目を空にします。
    値が更新されたことをわかりやすくするためにTitleは連番を付与しておきます。
    f:id:koruneko:20220122222820p:plain
  6. これでSubmitForm を行うと...一部空になってない!!
    f:id:koruneko:20220122222852p:plain

1行テキストと複数行テキストの項目は空白になっていますが、他項目は空白になっていませんね。

次に空白に設定する方法を変えてみます。
先ほどは入力項目の値を手で消して空にしましたが、今度はBlank 関数を用いて空白の値をセットさせてあげようと思います。

適当にReset ボタンを用意して、 OnSelect に以下を設定します。

Button1.OnSelect

UpdateContext({isReset:true});

次に入力項目の Default を以下のように設定します。

XXX_DataCard[N]

If(
    !isReset,
    ThisItem.XXX,
    Blank()
)

XXX は各項目名になります。

これでReset ボタンが押されたときに空白の値が各項目に設定されるようになりました。

f:id:koruneko:20220124003353p:plain

この状態でSubmitForm してみます。
その結果は以下になります。
先ほどは空が入っていた1行テキストと複数行テキストについても空が設定されていないですね。

f:id:koruneko:20220124003438p:plain

これは 空文字を設定しているのか空白値を設定しているのか の違いです。

前者の入力項目を消して設定した方法で設定されたのが空文字で、 Blank() を用いて設定した後者は空白値を設定していることになります。

ちなみにことらPatch 関数でアイテムの更新を行った場合も同様の結果となります。

気になった方は試してみてください。

なぜ空白を登録できないのか

ではなぜ空白を登録できないのでしょうか?

これはBkank 関数の説明をしているdocs の注意書きをみると理由が記載されていました。

docs.microsoft.com

注意

切り替えの期間にいます。 これまでは、空白 を使用してエラーをレポートし、エラーから有効な "値なし" を区別することができませんでした。 このため、現時点で、空白 値の格納は、ローカル コレクションでのみサポートされています。 設定 > 今後の機能 > 実験的数式レベルのエラー管理実験機能 をオンにしておくと、他のデータソースに 空白 の値を保存することができます。 この機能を完了し、およびエラーからの 空白 値の適切な分離を完了することに積極的に取り組んでいます。

どうやら現時点では空白値の設定はローカルコレクションでのみしかサポートされていないようですね。

空白値を設定するには

これを回避するためには
設定 > 実験段階 > 数式レベルのエラー管理
を「オン」に設定するよう記載されているので「オン」にして試してみます。

f:id:koruneko:20220122221053p:plain

まずは入力項目を手動で空にした場合です。

f:id:koruneko:20220124004707p:plain

この状態でSubmitForm を行います。

f:id:koruneko:20220124004742p:plain

無事値のクリアができました!

次にBlank 関数で空白値を設定します。

f:id:koruneko:20220124004901p:plain

この場合も同様に値のクリアができるようになりました!

f:id:koruneko:20220124004940p:plain

おわりに

Power Apps からSPOリストに空白値を設定する方法を纏めてみました。

空白値を設定したい。というケースは少なくないと思いますので早くこの設定がデフォルトで実装されるようになるといいですね。

私の所感ですが、こちらの機能は実験段階の機能にあるとはいえあのdocs の記載方法てきに今回紹介した機能がまったくなくなるということはないのではないかな?と思っています。

機能のロールアウトステージに関してはこちら。

docs.microsoft.com

とはいえ、実験的な機能は、「まだこの機能に依存しないでください」と明言されている機能なので実務では使えない。使いたくない。とい場合があるかと思います。

そのような場合は項目が「テキスト」であれば空文字を、
「選択肢」であれば以下のように空文字を選択したような状態にするなどすれば回避できます。

If(
    !isReset,
    ThisItem.Choice,
    {
        Claims: "i:0#.f|membership|" & Lower(User().Email),
        Value: ""
    }
)

ただし、「数値」や「日付」は上記のような回避策がないので、0を設定する(日付項目に0などの数値を設定すると「1970/1/1」が設定されます。)などして運用で回避するしか現時点ではなさそうです。

Power Automate でソートする(時間編)

はじめに

Power Automate でループなしでソートする方法を纏めました。

koruneko.hatenablog.com

しかし、この方法では日付のソートはできません。
range 関数の制限に引っかかってしまうからです。

f:id:koruneko:20211129003006p:plain

そこで、この記事では日付フィールドを昇順 / 降順で取得する方法を記載しておこうと思います。

ただし、SPO リストから取得する際限定になります。

SPOリストから項目を取得する

取得したデータをソートするというのは難しいことが分かったので、データを取得するときにソートした結果を取得するようにしたいと思います。

これを実現するためには"SharePoint にHTTP 要求を送信します"を利用します。

こちらを利用するとSharePoint に対してREST API を送信することができます。
このAPI を利用することで、項目の取得 & フィールドのソートを行いたいと思います。

今回行うのは項目の取得なので"方法"には GET を利用します。

REST API (URI) では以下を投げます。

_api/web/lists/getbytitle('[リスト名]')/items?$select=Title,Modified&$orderby=Modified asc

_api/web/lists/getbytitle('[リスト名]')/items では[リスト名]のリストから項目を取得しています。

select=Title,Modified ではTitle とModified フィールドから項目を取得しています。

orderby=Modified asc では、Modified を昇順で並び変えています。
もし、降順にしたければ orderby=Modified desc とします。

また、取得する件数を絞りたければ &$top=5 を式の最後に追記します。
この例だと先頭5件を取得する。となります。

先頭5件だけ。とやったうえでModifiedで降順ソートすると、変更日時が5番目に新しいものを last 関数で簡単に取得できますね。

おわりに

このやり方を応用して、取得したデータを一度SPO リストに一時的に格納してそのデータをソートさせて取得する。というやり方をすれば一応時間フィールドのソートもできそうですね。

ループするよりもアクション数自体は少なくなりそうですが、実行時間や一時格納用リスト用意しなくてはならない点を考慮するとちょっと微妙な気もしますが。。。

Power Automate でループなしでデータのソートを行う

はじめに

この記事ではPower Automate で取得したデータをループなしでソート、また、N番目のデータをループなしで取得する方法を纏めています。

Power Automate ではJSON 形式での取得データが行われます。
その取得したデータをソートしたりN番目のデータを取得したい。といった場合があるかと思います。

ただし、現在Power Automate の関数ではいまだにSort 関数のようなものが提供されていません。

docs.microsoft.com

そこで今回は自力でPower Automate でソートを行ってみようと思います。
ただし、Apply to each などでのループ処理は行いません。

ソートアルゴリズムアルゴリズムと要素数によっては計算量が膨大になってしまうからです。
詳しく知りたい方は、「ソートアルゴリズム 計算量」などで調べてみてください。

データ操作

データを昇順ソートさせる

まずは元となるデータを用意します。

"作成"アクションでJSON データを用意します。

f:id:koruneko:20211128034435p:plain

入力

[
  {
    "Name": "Tom",
    "Score": 50,
    "Class": "A"
  },
  {
    "Name": "Jun",
    "Score": 75,
    "Class": "A"
  },
  {
    "Name": "Kei",
    "Score": 64,
    "Class": "A"
  },
  {
    "Name": "Riko",
    "Score": 89,
    "Class": "B"
  },
  {
    "Name": "Hana",
    "Score": 34,
    "Class": "B"
  },
  {
    "Name": "Miyu",
    "Score": 75,
    "Class": "B"
  }
]

上記データの Score でソートを行いたいと思います。

データ操作を行うにあたり、データがオブジェクトリストとなっているとデータ操作ができない(後述のやり方では対象データにアクセスできない)のでソートを行いたいフィールドのみ抽出してあげます。

"選択"アクションを使って Score だけを抽出します。

f:id:koruneko:20211128035416p:plain

開始

@{outputs('作成')}

マップ

@item()?['Score']

これにより以下のような結果が得られます。(* 未加工入力の例です。)

{
    "body": [
        50,
        75,
        64,
        89,
        34,
        75
    ]
}

こちらを昇順ソートさせます。

これには"アレイのフィルター処理"を利用します。

f:id:koruneko:20211128040827p:plain

差出人(from)

range(min(body('Score')), add(sub(max(body('Score')), min(body('Score'))), 1))

条件(where)

contains(body('Score'), item())

"差出人"の訳どうにかならないですかね。元(英語)だと"From"です。

ロジックについて解説します。

fromにはソート順を定義しています。
range 関数range(A, B) のようにすると、整数Aから始まる整数Bまでの配列を返します。

開始の数はソート対象のフィールドの最小値であり、終値はソート対象の最大値であるので、min 関数max 関数を利用します。
ただし、range 関数はBまでの値を返すのではなく、Aから始まるB個の増分1で連続する数値を返す。という点に注意が必要です。

よって終値は"最大値 - 最小値 + 1"とする必要があります。

これにより以下のようなfrom を得ることができます。

[
    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
]

これで数値が昇順で設定された配列を作成することができました。

あとはフィルタ条件では、ソートを行いたいフィールドと作成された配列の共通部を順に設定していけばよいです。

ソート対象は先ほど"選択"アクションで作成した結果を利用します。
body('Score')

条件は右辺の値を含む左辺の結果が欲しいので"次の値を含む"を利用します。

含む値は、fromの結果を順に取得してもらいたいので、 item() を利用します。

これにより、以下のようにソートされた結果が得られます。

{
    "body": [
        34,
        50,
        64,
        75,
        89
    ]
}

気が付いた方もいらっしゃるかもしれませんが、この方法ではソート対象フィールドに重複した値がある場合は想定している結果が得られない可能性があります。

また、ソート対象フィールドに負の数値がある場合や小数点を含む場合は適宜、正の整数に変換する操作が必要があることに注意してください。

また、このやり方では降順ソートができません。。。
なにかいい方法ないかな

N番目の情報の取得

続いて先ほどソートを行った結果に基づいてN番目のデータを取得してみたいと思います。

この処理にも"アレイのフィルター処理"を利用します。

f:id:koruneko:20211128050020p:plain

From

outputs('作成')

Where

equals(item()?['Score'], body('昇順ソート')?[3])

フィルタ操作は元データに対して行うのでFrom には上記のように設定しています。

検索条件では、元データのソートを行いたいフィールドと昇順ソートした結果のN番目の値が一致した場合。としています。
なお、こちらの配列は0から始まるので、3と設定した場合は4番目に小さい値が存在するデータが取得されることに注意してください。

もしN番目に小さい値ではなくN番目に大きい値を取得したい場合は

equals(item()?['Score'], body('昇順ソート')?[sub(length(body('昇順ソート')), 1)])

という風にソート結果の"配列数 - (N + 1)"としてください。
+ 1 しているのは、0から始まっているからですね。

これにより以下のような結果が取得できます。

[
  {
    "Name": "Riko",
    "Score": 89,
    "Class": "B"
  }
]

データが重複していた場合は以下のように重複結果をきちんと取得できます。

{
    "body": [
        {
            "Name": "Jun",
            "Score": 75,
            "Class": "A"
        },
        {
            "Name": "Miyu",
            "Score": 75,
            "Class": "B"
        }
    ]
}

おわりに

以上。Power Automate でのループなしでソートとN番目のデータの取得方法でした。

工夫次第でこんなこともできるわけではありますが、標準でソート関数が実装されると嬉しいですね。

Power Apps のIdeas を使ってみた

はじめに

この記事はPower Apps のIdeas 機能を試してみた記事になります。
ちなみに試してみた環境は日本環境です。

Power Apps Ideas ってなに?という方に簡単に説明すると自然言語入力によってPower Apps 側がPower Fx を自動で生成してくれる機能です。

f:id:koruneko:20211119003442g:plain

詳細はこちらをご覧ください。

powerapps.microsoft.com

ちなみに2021/11/18現在私が試した限りではGif のようなことはできませんでした!
残念

イデアを試してみた

Ideas (日本語だと"アイデア"と表記されているので以下アイデア)は、ツリービューのコントロールの右にある三点リーダ、もしくはプロパティペインから開くことができます。

f:id:koruneko:20211119013706p:plain

f:id:koruneko:20211119013733p:plain

ちなみにこちらのアイデアですが、現時点ではギャラリーのラベルやデータテーブルの列などのテキストを含むラベルにしか対応してないようです。

f:id:koruneko:20211119014016p:plain

余談ですが、"テキストを含む何かベルのもの"となっており翻訳がミスってますね。
まぁ、まだプレビューなので。。。

ということでGallery のラベルに対してアイデアを試してみます。

今回は CustomGallerySample をデータソースに使用したGallery で試してみます。
ギャラリー > 縦 から追加しました。

f:id:koruneko:20211119014631p:plain

'Title1' のアイデアを開くとこのようになっています。

f:id:koruneko:20211119014728p:plain

これらのTitle に設定されたテキストの数値を "Lorem ipsum 001" のように0埋めして3桁表示されるようにしたいと思います。

データのところの表記を "Lorem ipsum 001" に書き換えてEnter を押してあげます。
するとこのように数式のサンプルが表示されます。

f:id:koruneko:20211119015110p:plain

なんか複雑な式が提案されてますね。
こちらをクリックしてみます。

するとラベルの Text にここで提案されていた式が反映されます。

f:id:koruneko:20211119015230p:plain

Gallery を確認してみると、数値が0埋めされて3桁表示されています。

ちなみに今回提案された式はこちら(わかりやすく整形しています。)

Concatenate(
    Mid(
        Left(
            ThisItem.SampleHeading,
            Last(
                MatchAll(
                    ThisItem.SampleHeading,
                    "\p{Zs}+"
                )
            ).StartMatch - 1
        ),
        Match(
            ThisItem.SampleHeading,
            "[\p{Lu}\p{Ll}]+"
        ).StartMatch
    ),
    Concatenate(
        " ",
        Text(
            Value(
                Mid(
                    Left(
                        ThisItem.SampleHeading,
                        Match(
                            ThisItem.SampleHeading,
                            "[0-9]+"
                        ).StartMatch + Len(
                            Match(
                                ThisItem.SampleHeading,
                                "[0-9]+"
                            ).FullMatch
                        ) - 1
                    ),
                    Match(
                        ThisItem.SampleHeading,
                        "[0-9]+"
                    ).StartMatch
                )
            ),
            "000"
        )
    )
)

正規表現使っているのはいいのですが、Unicode 文字プロパティ使われていますね。

おわりに

以上、今回は簡単に使ってみた内容をメモ書き程度に残してみました。

まだまだGif のような動作の実現はできませんでしたが、今後更にアップデートされたらまた纏めたいと思います。


スポンサードリンク