56 |
57 |
58 |
--------------------------------------------------------------------------------
/artifacts/MinprivilegeSecRole_1_0_0_2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/artifacts/MinprivilegeSecRole_1_0_0_2.zip
--------------------------------------------------------------------------------
/cards.md:
--------------------------------------------------------------------------------
1 | # Cards
2 |
3 | References for Cards in Power Platform.
4 |
5 | **In this article**
6 |
7 | - [Cards](#cards)
8 | - [Navigate and Slect a Record](#navigate-and-select-a-record)
9 |
10 | ## Navigate and Select a Record
11 |
12 | Using a Collection (array) for a data set, a repeating row to display the records on the card, and an Index (integer) to select a record.
13 |
14 | Note, there are more elegant ways to display and select these, but this was a fun activity to familiarize myself with some of the nuances when working with the Cards preview functionality.
15 |
16 | ``` JSON
17 | {
18 | "name": "DataverseCard",
19 | "description": "DataverseCard",
20 | "author": "System Administrator",
21 | "screens": {
22 | "main": {
23 | "template": {
24 | "type": "AdaptiveCard",
25 | "body": [
26 | {
27 | "type": "TextBlock",
28 | "size": "Medium",
29 | "weight": "bolder",
30 | "text": "Review Your Accounts",
31 | "id": "textLabel1"
32 | },
33 | {
34 | "type": "TextBlock",
35 | "id": "textLabel6",
36 | "wrap": true,
37 | "$data": "=varAllAccounts",
38 | "weight": "=If(ThisItem.Account=Index(varAllAccounts,ActiveRow).Account,\\"bolder\\",\\"default\\")",
39 | "text": "=If(ThisItem.Account=Index(varAllAccounts,ActiveRow).Account,\\"->\\"&ThisItem.'Account Name',ThisItem.'Account Name')",
40 | "isSubtle": false
41 | }
42 | ],
43 | "actions": [
44 | {
45 | "type": "Action.Execute",
46 | "id": "button2",
47 | "title": "<",
48 | "associatedInputs": "auto",
49 | "verb": "onSelect_Button4"
50 | },
51 | {
52 | "type": "Action.Execute",
53 | "id": "viewDetails",
54 | "title": "View Details",
55 | "associatedInputs": "auto",
56 | "verb": "onSelect_Button1"
57 | },
58 | {
59 | "type": "Action.Execute",
60 | "id": "button3",
61 | "title": ">",
62 | "associatedInputs": "auto",
63 | "verb": "onSelect_Button5"
64 | }
65 | ],
66 | "$schema": "",
67 | "version": "1.4"
68 | },
69 | "verbs": {
70 | "onSelect_Button5": "If(ActiveRow1, Set(ActiveRow,ActiveRow-1))"
74 | }
75 | },
76 | "DetailsScreen": {
77 | "template": {
78 | "type": "AdaptiveCard",
79 | "body": [
80 | {
81 | "type": "TextBlock",
82 | "size": "Medium",
83 | "weight": "bolder",
84 | "text": "=\\"Account: \\"&Index(varAllAccounts,ActiveRow).'Account Name'",
85 | "id": "textLabel1"
86 | },
87 | {
88 | "type": "TextBlock",
89 | "text": "=\\"Account: \\"&Index(varAllAccounts,ActiveRow).Description",
90 | "wrap": true,
91 | "id": "textLabel2"
92 | },
93 | {
94 | "type": "Input.Text",
95 | "id": "textInput1",
96 | "label": "Account Description",
97 | "text": "",
98 | "placeholder": "only add something here if you want to update the value"
99 | }
100 | ],
101 | "actions": [
102 | {
103 | "type": "Action.Execute",
104 | "id": "button1",
105 | "title": "Back",
106 | "associatedInputs": "auto",
107 | "verb": "onSelect_Button2"
108 | },
109 | {
110 | "type": "Action.Execute",
111 | "id": "Button3",
112 | "title": "Update Account Description",
113 | "associatedInputs": "auto",
114 | "verb": "onSelect_Button3"
115 | }
116 | ],
117 | "$schema": "",
118 | "version": "1.4"
119 | },
120 | "verbs": {
121 | "onSelect_Button3": "Patch(Accounts, Index(varAllAccounts,ActiveRow), {Description:textInput1})",
122 | "onSelect_Button2": "Back()"
123 | }
124 | }
125 | },
126 | "sampleData": {
127 | "main": {},
128 | "DetailsScreen": {}
129 | },
130 | "connections": {
131 | "redacted_account": {
132 | "apiName": "shared_commondataserviceforapps",
133 | "name": "shared-commondataser-500a8cee-a057-418d-a9e4-ba66f0141153",
134 | "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps",
135 | "apiDisplayName": "Microsoft Dataverse",
136 | "tablePluralName": "Accounts",
137 | "logicalName": "account",
138 | "environment": "redactedguid",
139 | "hasParameters": true
140 | }
141 | },
142 | "variables": {
143 | "varAllAccounts": {
144 | "type": "object",
145 | "title": "",
146 | "description": "",
147 | "default": "=Accounts",
148 | "x-ms-cardapp": {
149 | "scope": "session"
150 | },
151 | "loadPowerFx": true
152 | },
153 | "ActiveRow": {
154 | "type": "integer",
155 | "title": "",
156 | "description": "",
157 | "default": 1,
158 | "x-ms-cardapp": {
159 | "scope": "session"
160 | }
161 | }
162 | },
163 | "flows": {},
164 | "locale": "en-US"
165 | }
166 |
167 | ```
168 |
--------------------------------------------------------------------------------
/cli.md:
--------------------------------------------------------------------------------
1 | # Power Platform CLI
2 |
3 | The CLI is not a typical Citizen Developer topic, nor do they have a direct need for it. That said, I am right on the line of Citizen Dev and Pro Dev (Pro Dev Light? Deputized Citizen Dev?) and have found ways to make the Power Platform CLI useful for my builds.
4 |
5 | Typically, I use this to grab point-in-time updates of the configuration I do via the Power Apps maker portal, so they can be part of a larger process and provide better visibility for if/when IT wants to take over (or at least have more oversight of what I'm doing).
6 |
7 | FWIW I keep most of these in a PowerShell file in VSCode and run them from the integrated console. There are definitely other ways to do it but that's what works for me (as a basic PowerShell / CLI user). In case you want that file, let me know via a GitHub issue and I'll upload it ;)
8 |
9 | **In this article**
10 |
11 | - [Power Platform CLI](#power-platform-cli)
12 | - [Install Power Platform CLI and update to latest version](#install-power-platform-cli-and-update-to-latest-version)
13 | - [Authenticate and connect to Dataverse organization](#authenticate-and-connect-to-dataverse-organization)
14 | - [Clone a solution definition to the current folder](#clone-a-solution-definition-to-the-current-folder)
15 | - [Export a solution from the current environment](#export-a-solution-from-the-current-environment)
16 | - [Export Managed and Unmanaged solutions at the current point in time](#export-managed-and-unmanaged-solutions-at-the-current-point-in-time)
17 | - [Export solution and unpack so it is usable in source control](#export-solution-and-unpack-so-it-is-usable-in-source-control)
18 | - [Links](#links)
19 |
20 | ## Install Power Platform CLI and update to latest version
21 |
22 | I installed from VS Code by going to the Extensions marketplace and installing **Power Platform Tools** (the name has changed from the docs below) from the Microsoft publisher.
23 |
24 | After installing, I still needed to run the following command to get the latest and greatest
25 |
26 | ```
27 | pac install latest
28 | ```
29 |
30 | Official documentation: https://docs.microsoft.com/en-us/powerapps/developer/data-platform/powerapps-cli#install-microsoft-power-platform-cli
31 |
32 | ## Authenticate and connect to Dataverse organization
33 |
34 | This will get you started by connecting to a specific Environment. An Office 365 login window will be displayed, which allows login with a username/password and two-factor if needed.
35 |
36 |
37 | Format:
38 |
39 | `pac auth create --kind DATAVERSE --url https://url --name displayname`
40 |
41 | Here's an example (not a real URL):
42 |
43 | ```
44 | pac auth create --kind DATAVERSE --url https://org12345.crm.dynamics.com/ --name FancyEnvName
45 | ```
46 |
47 | The environment will be added to your to your list of connection profiles, which you can view with the command
48 |
49 | ```
50 | pac auth list
51 | ```
52 |
53 | this will return something like this:
54 |
55 | ```
56 | Profiles (* indicates active):
57 | [1] * ADMIN https://service.powerapps.com/ : matt@example.org Public
58 | [2] * DATAVERSE AnchorheadDev https://orge1111111.crm.dynamics.com/ : matt@example.org Public
59 | ```
60 |
61 | In the above example, the number in brackets `[1]` is the *ID* for the profile.
62 |
63 | If you have more than one connection profile, you can use this to validate the environment before you interact with the API
64 |
65 | ```
66 | pac org who
67 | ```
68 |
69 | Which will return something like this (though these are fake IDs and URLs):
70 |
71 | ```
72 | Connected to...Development
73 | Organization Information
74 | Org ID: 741af709-54fc-4470-887b-2cd73552f83a
75 | Unique Name: unq12345243wsdfgwq23tg22v2v2v2
76 | Friendly Name: Development
77 | Org URL: https://org11111.crm.dynamics.com/
78 | User ID: matt@example.com (f123456c-311c-ec11-b3e4-001d3a9d24n4)
79 | Environment ID: 11f8c533-c9ee-4e61-be7b-d04c3126b1c2
80 | ```
81 |
82 | To change to another connection profile, use the `pac auth list` command to view your list, then change by using:
83 |
84 | ```
85 | pac auth select --index 1
86 | ```
87 |
88 | The above example will change to the connection profile with the ID of 1
89 |
90 | ## Clone a solution definition to the current folder
91 |
92 | This will drop the solution components into a subfolder structure which can be used to track metadata changes to Dataverse, Model-Driven Power Apps, and other "solution aware" components in source control systems.
93 |
94 | ```
95 | pac solution clone --name MyFancySolution
96 | ```
97 |
98 | ## Export a solution from the current environment
99 |
100 | I use this to pull Solution file packages (.zip) from Dev environments.
101 |
102 | Unmanaged solution:
103 |
104 | ```
105 | pac solution export --path c:\mypath\solutionName.zip --name solutionName
106 | ```
107 |
108 | Managed solution:
109 |
110 | ```
111 | pac solution export --path c:\mypath\solutionName_managed.zip --name solutionName --managed true
112 | ```
113 |
114 | Note: This will only work for Unmanaged solutions, since Managed solutions are protected from direct export.
115 |
116 | ## Export Managed and Unmanaged solutions at the current point in time
117 |
118 | As I hit key milestones in my builds, I like to export a physical copy of both my managed and unmanaged solutions. To do so I use the following PowerShell script to get it all in one fell swoop. Before running the script, I enter the variable values, then I execute the script.
119 |
120 |
121 | ``` powershell
122 | # Set these before running the script, I put the solution files in the solution folder
123 |
124 | $solutionDir = "c:\mypath\solutions\"
125 | $solutionVersion = "1.0.0.0"
126 | $solutionName = "myFancySolution"
127 |
128 | # The following commands execute the export using the variables above
129 |
130 | pac solution export --path ${solutionDir}${solutionName}-${solutionVersion}.zip --name $solutionName --managed false
131 | pac solution export --path ${solutionDir}${solutionName}-${solutionVersion}_managed.zip --name $solutionName --managed true
132 | ```
133 |
134 | Regarding the `version` variable: All of the version validation is stored inside of the package...this is just a label.
135 |
136 | I'm sure there's a more elegant way to set the version number based on the solution version in Dataverse...I'm welcome to that feedback via a GitHub issue or Pull Request :)
137 |
138 | ## Export solution and unpack so it is usable in source control
139 |
140 | ***Need to update this based on the `-processCanvasApps` parameter for `pac solution unpack` command***
141 |
142 | The .zip file format of the solution is useful for citizen developers to import packages to other enviornments, however from a source control perspective it's more valuable to unpack that .zip file so individual changes can be reviewed/understood later. CLI offers us this capability. I'll start with my script, then describe the different steps.
143 |
144 | ``` powershell
145 |
146 | $srcDir = "C:\mypath\"
147 | $solutionName = "myFancySolution"
148 |
149 | pac solution export --path ${srcDir}tmp\${solutionName}.zip --name $solutionName --managed false
150 | pac solution unpack --zipfile ${srcDir}tmp\${solutionName}.zip --folder ${srcDir}\${solutionName} --allowWrite true
151 |
152 | Get-ChildItem -Path ${srcDir}${solutionName}\CanvasApps\ -Filter *.msapp |
153 |
154 | Foreach-Object {
155 |
156 | $msappFile = $_.Name
157 | pac canvas unpack --msapp ${srcDir}${solutionName}\CanvasApps\${msappFile} --sources ${srcDir}\${solutionName}-UnpackedCanvas\$msappFile
158 |
159 |
160 | }
161 |
162 | Remove-Item -path ${srcDir}tmp\${solutionName}.zip
163 |
164 | ```
165 |
166 | Before running, I set the srcDir and solutionName variables based on what I'm going to export.
167 |
168 | The first chunk exports the solution, then unpacks the contents of the .zip to a /source sub-folder named after the solution.
169 |
170 | ```powershell
171 | pac solution export --path ${srcDir}tmp\${solutionName}.zip --name $solutionName --managed false
172 | pac solution unpack --zipfile ${srcDir}tmp\${solutionName}.zip --folder ${srcDir}\${solutionName} --allowWrite true
173 | ```
174 |
175 | This next bit looks for any canvas apps or canvas pages (.msapp) and unpacks those to the /source folder, with each component having its own subfolder.
176 |
177 | ```powershell
178 | Get-ChildItem -Path ${srcDir}${solutionName}\CanvasApps\ -Filter *.msapp |
179 |
180 | Foreach-Object {
181 |
182 | $msappFile = $_.Name
183 | pac canvas unpack --msapp ${srcDir}${solutionName}\CanvasApps\${msappFile} --sources ${srcDir}\${solutionName}-UnpackedCanvas\$msappFile
184 |
185 |
186 | }
187 | ```
188 |
189 | Then I remove the .zip file that I unpacked (it's nice to tidy up after yourself).
190 |
191 | In the future, to take advantage of change tracking, it's necessary to delete the items in the source folder before unpacking/overwriting. For this, I use the following:
192 |
193 | ```powershell
194 | $srcDir = "C:\mypath\"
195 | $solutionName = "myFancySolution"
196 |
197 | pac solution export --path ${srcDir}tmp\${solutionName}.zip --name $solutionName --managed false
198 | Remove-Item -path ${srcDir}\${solutionName} -Recurse
199 | pac solution unpack --zipfile ${srcDir}tmp\${solutionName}.zip --folder ${srcDir}\${solutionName} --allowWrite true
200 | Remove-Item -path ${srcDir}tmp\${solutionName}.zip
201 |
202 | Get-ChildItem -Path ${srcDir}${solutionName}\CanvasApps\ -Filter *.msapp |
203 |
204 | Foreach-Object {
205 |
206 | $msappFile = $_.Name
207 | Remove-Item -path ${srcDir}${solutionName}-UnpackedCanvas\${msappFile} -Recurse
208 | pac canvas unpack --msapp ${srcDir}${solutionName}\CanvasApps\${msappFile} --sources ${srcDir}\${solutionName}-UnpackedCanvas\$msappFile
209 |
210 |
211 | }
212 | ```
213 |
214 | Ideally this script should be enhanced to remove any canvas source. I have an idea for this but haven't had the time to implement/test it, but when I do you can expect an update.
215 |
216 | ## Links
217 |
218 | [Power Plaform CLI Documentation](https://docs.microsoft.com/en-us/powerapps/developer/data-platform/powerapps-cli)
219 |
--------------------------------------------------------------------------------
/connectors.md:
--------------------------------------------------------------------------------
1 | # Connectors
2 |
3 | References for Power Platform connectors
4 |
5 | **In this article**
6 |
7 | - [Connectors](#connectors)
8 | - [Connector Authentication Patterns](#connector-authentication-patterns)
9 | - [Links](#links)
10 |
11 | ## Connector Authentication Patterns
12 |
13 | Power Apps and Power Automate authenticate with connectors to create a connection environment. It is that environment that contains the specific configuration information necessary for the app or flow to talk to the connector API that is used in each interaction. Connectors could choose to use no authentication, basic authentication, API key authentication or OAuth 2.0. The most common are OAuth and API Key.
14 |
15 | OAuth if you aren't familiar with it is an authorization framework that allows external applications to obtain controlled access to a target service. Many APIs support it including Dataverse, Facebook and Twitter to name a few. The goal of authentication is to allow the user to sign in to a familiar login dialog, consent to the application using the service, and then setup to allow tokens to be acquired. It is the tokens that are used on each request to prove who the user is and their right to use the API. In the Power Apps and Power Automate usage, a Consent Server is involved that helps manage the tokens and their lifecycle including storing the renewal token in the Consent Server and handling the refresh cycle. The following is a step by step look at what happens when you authenticate a connection using OAuth.
16 |
17 | > 
18 |
19 |
20 | The API Key is a little less complex as it typically involves the API assigning a key that is passed on each request. That key is provided when the connection is established for the connector and is stored in the environment with the other connection information in a secure way. An example of an API Key authentication connector is the Azure Storage Blob. As you can see below it wants the Storage Account Name as well as the Access Key.
21 |
22 | > 
23 |
24 |
25 | When on-premises gateways are involved the process is even a little more complex. The following diagrams what happens when you establish a connection with the gateway data source.
26 |
27 | > 
28 |
29 | Source:
30 |
31 | ## Links
32 |
33 | [Connectors Documentation on docs.microsoft.com](https://docs.microsoft.com/en-us/connectors/)
34 | '
35 | > Effective November 2020:
36 | > - Common Data Service (CDS) has been renamed to Microsoft Dataverse. [Learn more](https://aka.ms/PAuAppBlog)
37 | > - Some terminology in Microsoft Dataverse has been updated. For example, *entity* is now *table* and *field* is now *column*. [Learn more](https://go.microsoft.com/fwlink/?linkid=2147247)
38 |
--------------------------------------------------------------------------------
/dataverse-api-101/img/auth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/dataverse-api-101/img/auth.png
--------------------------------------------------------------------------------
/dataverse-api-101/img/bearer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/dataverse-api-101/img/bearer.png
--------------------------------------------------------------------------------
/dataverse-api-101/img/cookie-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/dataverse-api-101/img/cookie-header.png
--------------------------------------------------------------------------------
/dataverse-api-101/img/cookie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/dataverse-api-101/img/cookie.png
--------------------------------------------------------------------------------
/dataverse-api-101/img/post.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/dataverse-api-101/img/post.png
--------------------------------------------------------------------------------
/dataverse-api-101/readme.md:
--------------------------------------------------------------------------------
1 | # A 101 Guide to the Dataverse Web API
2 | Dataverse is low-code/no-code friendly data service for storing both structured and unstructured data behind a role-based security model. Dataverse is a pillar of Microsoft's [Power Platform](https://powerplatform.microsoft.com/) and the backbone of Microsoft's [Dynamics 365](https://dynamics.microsoft.com/) first party applications. Dataverse is designed to be as easy-to-use and robust for small workloads while also being scalable and performant enough to handle enterprise workloads on a massive scale.
3 |
4 | In addition to being used with it's numerous integrations with other Microsoft and 3rd party products, Dataverse also supports direct interfacing via the [Dataverse Web API](https://docs.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api), a [RESTful API](https://en.wikipedia.org/wiki/Representational_state_transfer). **This article is intended to get you started with using the Dataverse Web API**. By the end of the article, you will be prepared to start querying the Dataverse API!
5 |
6 | ## Medium
7 | Since the Dataverse API is web-based, all requests must go through the Hypertext Transfer Protocol (HTTP). This is just a fancy way of saying each request to the API will look similar to a URL, like what you'd see in your browser.
8 |
9 | We have to be able to customize components of each HTTP request: URL, request headers, body, etc. Because of this, we will be using [Postman](https://www.postman.com/), a free application for customizing these components of an HTTP request, sending to the API, and receiving the response.
10 |
11 | You can download Postman here: [Postman Download](https://www.postman.com/downloads/)
12 |
13 | ## Authentication
14 | Authentication will likely be the most challenging aspect of this article. There are two ways that I will document how you can authenticate; the first is the "correct" way of doing things, and the second is a faster, easier to understand way.
15 |
16 | When calling the Dataverse API, you don't directly provide your credentials (username/password) each time you make a single request. Instead, you authenticate with the API once, an *Access Token* is provided to you, and you then provide this *Access Token* to the API each time you make a request. Keep in mind that this token *does* expire however (usually in 60 minutes from the time it was received), so after the expiration time, you'll have to do it again to get a new token!
17 |
18 | ### Authenticating via an Access Token Request
19 | One can make an HTTP POST request with the following characteristics to receive an access token:
20 | - HTTP Method: `POST`
21 | - URL: `https://login.windows.net/common/oauth2/token`
22 | - Body: `x-www-form-urlencoded` format with the following values:
23 | - `grant_type`: `password`
24 | - `client_id`: `51f81489-12ee-4a9e-aaae-a2591f45987d`
25 | - `resource`: *(the URL of your Dataverse environment)*
26 | - `username`: *(your username)*
27 | - `password`: *(your password)*
28 |
29 | After constructing the request as specified above, hit send! If all goes well, you'll receive a response like the following:
30 | ```
31 | {
32 | "token_type": "Bearer",
33 | "scope": "user_impersonation",
34 | "expires_in": "4817",
35 | "ext_expires_in": "4817",
36 | "expires_on": "1655742922",
37 | "not_before": "1655737804",
38 | "resource": "https://org9422ae7b.crm.dynamics.com/",
39 | "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImpTMVhvMU9XRGpfNTJ2YndHTmd2UU8yVnpNYyIsImtpZCI6ImpTMVhvMU9XRGpfNTJ2YndHTmd2UU8yVnpNYyJ9.eyJhdWQiOiJodHRwczovL29yZzk0NDJhZTdiLmNybS5keW5hbWljcy5jb20vIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvMWU4NWYyM2YtYzBhZi00YmNlLWJiOTYtOTIwMTRkM2MxMzU5LyIsImlhdCI6MTY1NTczNzgwNCwibmJmIjoxNjU1NzM3ODA0LCJleHAiOjE2NTU3NDI5MjIsImFjciI6IjEiLCJhaW8iOiJFMlpnWUFqOCtZZWhjTVZoNXI0NHVZYWNpeFVHRTFOZDJJUnlXUXNxR2RiNVBabmI5Z3NBIiwiYW1yIjpbInB3ZCJdLCJhcHBpZCI6IjUxZjgxNDg5LTEyZWUtNGE5ZS1hYWFlLWEyNTkxZjQ1OTg3ZCIsImFwcGlkYWNyIjoiMCIsImZhbWlseV9uYW1lIjoiQWRtaW5pc3RyYXRvciIsImdpdmVuX25hbWUiOiJTeXN0ZW0iLCJpcGFkZHIiOiI0Ny4yMDIuMTIuMjgiLCJuYW1lIjoiU3lzdGVtIEFkbWluaXN0cmF0b3IiLCJvaWQiOiIzNTAzYzUzMy1kZmI5LTRlNDktYmNhZC00N2VkYzgzYzA2NTUiLCJwdWlkIjoiMTAwMzIwMDEzQzIxQTM5MyIsInJoIjoiMC5BWHdBUF9LRkhxX0F6a3U3bHBJQlRUd1RXUWNBQUFBQUFBQUF3QUFBQUFBQUFBQjhBQUUuIiwic2NwIjoidXNlcl9pbXBlcnNvbmF0aW9uIiwic3ViIjoiQkpPeTdRcFJRYk1TTTVjLS1zZlh5aVEyU2t3ZThqdkdzWHNiR0ZuWXlsWSIsInRpZCI6fjFlODVmMjNmLWMwYWYtNGJjZS1iYjk2LTkyMDE0ZDNjMTM1OSIsInVuaXF1ZV9uYW1lIjoiYWRtaW5ARDM2NURlbW9UUzkwOTE5Ni5vbm1pY3Jvc29mdC5jb20iLCJ1cG4iOiJhZG1pbkBEMzY1RGVtb1RTOTA5MTk2Lm9ubWljcm9zb2Z0LQNvbSIsInV0aSI6Ii1oX1BmdWVySDAydlMzWkRDeEZ6QVEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbIjYyZTkwMzk0LTY5ZjUtNDIzNy05MTkwLTAxMjE3NzE0NWUxMCIsImI3OWZiZjRkLTNlZjktNDY4OS04MTQzLTc2YjE5NGU4NTUwOSJdfQ.B7-xfXv5UwPiYyrmbhpRlEOqOSs7aOranemqzFrAJx3h52D_tB2RjRj4rizuhKQTZ4XtyDcNZYMCmj57G64h2ICFXopm3dfpMmZ_hZ6qHD7zwRrQK2oQHUCpxtuyeT17ssLorFuWxZYfQYMxhjQYqu7WFWRX42IMBYeUqV7zGnnJmtMN1piaeasaSENVlo5IAxVyPfQyWv1roc-IjqNhxBB8Egnl3cez8sFDXY5JwwAcxpBRzHcF3HzLsQYqQ28bloUE44Za4JTR0tGushPl34AKdMblZYmCgysZiMH1Gyw8rYyJD2NeImV6Mo0cNvEnkS5tMzhDfMglHCSq92mjttQ",
40 | "refresh_token": "0.AXwAP_KFHq_Azku7lpIBTTwTWYkU-FHuEp5Kqq6iWR9FmH18AAE.AgABAAEAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P-jUK3eXBOWMvudf1yc-687D2O37fykNKvvoBi5VlWc0NLQCqKg-goiG6RGG66CFhyrBYHBscQN6tybyDbb3m9toQ7boa3gKqwqWYA2jUoQ1JFPr8SH1mebCy5RUPXCeGFrksWe1iFJfr-SFCWLh2wpBSRmiX0yZ02nbsDQNXZdNln7FVEcNvGzbFyzlqi-gIS-9FP6rSx82jcs1GJV2OxUkYIE0Bv6x2oPTtZYU78LuERPyxYQ4AxjQmpvzdWpVqidQ5UptW-QSgIpKq_jUFRUas9YZhJNCPNxN5BIUIVLMKFwxRWT3GA5GunjBK6BeUWRr_nI1wU_6G-A_bzxzbXCq5hZB1SsxrEQQDTaWvN8cjG5PPTSnXI6phmi38c-BI_zV2QxXuejPi4FPzWrSaWoru6R3HR7jBCMb6Amueyqks7Zuipbq3GkH-WWxQe26Y0echvDHm2_MkVaxDulQfesR25PFjvkzvFdOy9emLZFIOCmrNJK54gOnhcbn_kdDWQNEwzkOAtO1c6WK6mRKAkXnfObmTc0QsVkkC7YxY704B35ys-i8cYFJM69MDRcWbRg6M-Z4-cR3GCAsGi9_ZcdMXxnClQ-DefVYuc_dY-pWsH62DT92LF048ho1Nh2i3qhxcUWKGxk8ODukHeUNs5Z3504h7T1ssCwZQa65AERRT9ote2Rgi1U0NlOsz0STV9NGzktZK0iiDzLTtzCuEOw_JSr"
41 | }
42 | ```
43 | Altogether, your request and response should look like this:
44 |
45 | 
46 |
47 | The `access_token` property (beginning with "eyJ" and ending with "ttQ") is what we're after. We will supply this to all future requests.
48 |
49 | We must supply this value as a *Bearer Token*, a specific format for providing an access token via an API. Open a new request in Postman. Go to the *Authorization* tab and change the *Type* dropdown to *Bearer Token*. Paste the value of the `access_token` into the `token` field:
50 |
51 | 
52 |
53 | ### Using Cookies from your Browser
54 | When you are logged into a part of the Power Platform - say Power Apps, for example, a cookie will be added to your browsing session so you don't have to re-authenticate on *every* page! We are actually able to "intercept" this cookie from the web browser and use this in our requests.
55 |
56 | To get this cookie, we first need to know the URL of your Dataverse environment. From the Power Platform Admin Center, open your environment. Your will find this URL listed as the **Environment URL**. For example, mine looks like this: https://org56f8d45d.crm.dynamics.com/.
57 |
58 | Append `/api/data/v9.0/` to this environment URL, so it looks like this: `https://org56f8d45d.crm.dynamics.com/api/data/v9.0/`. From your authenticated browser session, navigate to this URL. You should receive a list of tables that exist in this environment.
59 |
60 | Open your browser's developer tools (I am using Microsoft Edge) with Ctrl+Shift+I. With the developer tools open, refresh the webpage. You should see a network log labeled `v9.0`. Click on this. In the *headers* tab (opened by default), scroll down to find the `cookie` header. Copy the value of this header; this is what we will be using!
61 |
62 | 
63 |
64 | In each request we make via Postman, we must provide this value in the headers, labeled as `cookie`. Open a new request in Postman. Add this value to the headers with the label `cookie`, like below:
65 |
66 | 
67 |
68 | ## The OData Protocol
69 | The Dataverse API follows the standards described by the [Open Data Protocol ("OData")](https://www.odata.org/). OData is a set of best practices for building and consuming RESTful API's. Breaking this down, this means that the Dataverse API follows a standard that makes querying easy and understandable, with a format of which is almost universally understood. Some of the features of OData include:
70 | - `$filter`: filtering results
71 | - `$select`: selecting only specific fields to be returned
72 | - `$orderby`: sorting the returned results
73 | - `$count`: receiving a count of the queried records
74 | - `$top`: limiting the number of records returned
75 | Each of the parameters above can be passed into the query URL in a read request to the Dataverse API. For more information about OData: [OData queries](https://docs.microsoft.com/en-us/odata/concepts/queryoptions-overview).
76 |
77 | ## Dataverse Read Operation
78 | Now that we have a request in Postman equipped with the necessary data to authenticate, we can begin making calls!
79 |
80 | ### List Tables
81 | Firstly, make a `GET` request (the drop down near the URL in Postman) to the following URL: `https://org9442ae7b.crm.dynamics.com/api/data/v9.0` (obviously replacing `org9442ae7b` with your org name).
82 |
83 | If successful, you will receive back a list of tables in your Dataverse instance, in JavaScript Object Notation (JSON) format. It will look like this (this is truncated, but you get the point):
84 | ```
85 | {
86 | "@odata.context": "https://org9442ae7b.crm.dynamics.com/api/data/v9.0/$metadata",
87 | "value": [
88 | {
89 | "name": "accounts",
90 | "kind": "EntitySet",
91 | "url": "accounts"
92 | },
93 | {
94 | "name": "contacts",
95 | "kind": "EntitySet",
96 | "url": "contacts"
97 | },
98 | {
99 | "name": "phonecalls",
100 | "kind": "EntitySet",
101 | "url": "phonecalls"
102 | },
103 | ...
104 | ```
105 | In each object in the `value` property, we can see the name of the table, the type, and the `url` to access the data in the table.
106 |
107 | ### Read records from a table
108 | To get a list of records (data) that exist in each table, simply append one of the `url` properties to the URL that we used above to list the tables. For example, to get contacts: `https://org9442ae7b.crm.dynamics.com/api/data/v9.0/contacts`.
109 |
110 | The response will return **every field** of **every record** in the contacts table. To limit the number of responses we get to 5, you can append `$top=5` to the URL as a parameter, like so: `https://org9442ae7b.crm.dynamics.com/api/data/v9.0/contacts?$top=5`.
111 |
112 | You just appended an OData parameter to your URL! By stacking the OData parameters mentioned earlier, you can create more complex queries.
113 |
114 | ### Read a single record
115 | If we know the GUID ("Global Unique Identifier", a primary key) of a single record in a table, you can request data for *only* that record by including it in parentheses. For example, requesting a single record with ID `8e27ee97-cbe1-ec11-bb3d-00224803b8c6` from the contacts table: `https://org9442ae7b.crm.dynamics.com/api/data/v9.0/contacts(8e27ee97-cbe1-ec11-bb3d-00224803b8c6)`. You will see that the response does not return an *array* of objects, but rather a single object.
116 |
117 | ## Dataverse Write Operation
118 | In addition to reading from Dataverse, we can also use the Web API for *writing* to Dataverse.
119 |
120 | ### Create a New Record
121 | Make a new request in Postman. Add the necessary authentication data, as described above in the **Authenticate** section. Set the HTTP method to `POST` and set your target URL to `https://org9442ae7b.crm.dynamics.com/api/data/v9.0/accounts` (obviously replacing `org9442ae7b` with your org name). This means our intention is to **create** a new **account** record.
122 |
123 | Navigate to the *Body* column. Select *raw* as the body type, and *JSON* as the format. The Dataverse Web API accepts the data of the record in JSON format. To set the name of the account we would like to save, set the body to the following:
124 | ```
125 | {
126 | "name":"Stark Industries"
127 | }
128 | ```
129 | Hit send! If you receive a `204 No Content` response, you have succeeded. Pull up the list of accounts in a model-driven app and now you should see your new account has been made.
130 | 
131 |
132 | ### Update a Record
133 | As per the OData standard, we can only update a single record at a time via the Dataverse Web API. To do this, we *must* know the GUID (primary key) value of the record we wish to update. For example, changing the name of the account record with ID `a33dd8e6-b6f0-ec11-bb3d-000d3a357ea4`:
134 | - Set the HTTP method to `PATCH`
135 | - Set the URL to `https://org9442ae7b.crm.dynamics.com/api/data/v9.0/accounts(a33dd8e6-b6f0-ec11-bb3d-000d3a357ea4)` (obviously replacing `org9442ae7b` with your org name and `a33dd8e6-b6f0-ec11-bb3d-000d3a357ea4` with a GUID of a record in your account table)
136 | - Similar to above, set the *Body* of your request to *raw* and the format to *JSON* with the following content:
137 | ```
138 | {
139 | "name":"Tesla Inc"
140 | }
141 | ```
142 | After sending this request, you will again receive a `204 No Content` response if the update was successful. If you make a read call for the accounts table using the methods outlined earlier, you should see your change reflected.
143 |
144 | ## Delete a Record
145 | So far we've seen how to **create** a record, **read** a record, and **update** a record. Finally, we will explore how you can **delete** a record. Just like with update requests, the OData standard does not allow for deletion of multiple records in a single request. Thus, again, we must know the GUID of the record we would like to delete. For example, deletion of the account record with ID `a33dd8e6-b6f0-ec11-bb3d-000d3a357ea4`:
146 | - Set the HTTP method to `DELETE`
147 | - Set the URL to `https://org9442ae7b.crm.dynamics.com/api/data/v9.0/accounts(a33dd8e6-b6f0-ec11-bb3d-000d3a357ea4)` (obviously replacing `org9442ae7b` with your org name and `a33dd8e6-b6f0-ec11-bb3d-000d3a357ea4` with a GUID of a record in your account table)
148 | - Note that this is the same URL that we used above for the update. The *only* difference between the update and delete operation is the HTTP method used.
149 |
150 | Unlike the update operation, we obviously will not need to provide content in the request Body because this is merely a delete operation - no data should be changed. Be sure you have *none* selected under the *Body* tab.
151 |
152 | After specifying the above in your request, press send. If you receive a `204 No Content` response, this means your delete request was successful. The targeted record with the ID that you specified is now deleted.
153 |
154 | ## Summary
155 | There is *a lot* more to what the Dataverse Web API can provide, but I hope this tutorial was enough to get you started with using, but more importantly **understanding** the Dataverse Web API. The Dataverse Web API follows the OData standard, and thus, the more you learn about the OData protocol, the more capable you will be of working with the Dataverse Web API.
156 |
157 | Written by Tim Hanewich, Business Applications Technical Specialist @ Microsoft (alias TIMH)
--------------------------------------------------------------------------------
/dataverse.md:
--------------------------------------------------------------------------------
1 | # Dataverse
2 |
3 | Details and reminders for working with Dataverse as a platform component, data store, the transaction pipeline, and custom APIs exposed through Dataverse.
4 |
5 | **In this article**
6 |
7 | - [Dataverse](#dataverse)
8 | - [Security Roles](#security-roles)
9 | - [Minimum privilege security role](#minimum-privilege-security-role)
10 | - [Standard APIs](#standard-apis)
11 | - [Custom APIs](#custom-apis)
12 | - [Dataverse Healthcare APIs](#dataverse-healthcare-apis)
13 | - [Dataverse Offline Mode](#dataverse-offline-mode-for-canvas)
14 | - [Links](#links)
15 |
16 | ## Security roles
17 |
18 | ### Minimum privilege security role
19 |
20 | When creating a net new security role, the best practice is to start with the "minimum privilege security role" and making a copy, rather than starting from a truly blank role.
21 |
22 | Slightly more nuanced approach is that every user must have the minimum privileges, but additional roles can be created from blank, which can be layered as needed.
23 |
24 | The base role can be downloaded here: [Official download link](https://download.microsoft.com/download/6/5/5/6552A30E-05F4-45F0-AEE3-9BB01E13118A/MinprivilegeSecRole_1_0_0_2.zip) [Local copy in this repo from Feb 2023](./artifacts/MinprivilegeSecRole_1_0_0_2.zip)
25 |
26 | To use: Download the solution, import to your dev environment, then select Copy Role.
27 |
28 | The full details can be found here: [MS Learn](https://learn.microsoft.com/en-us/power-platform/admin/database-security#minimum-privileges-to-run-an-app)
29 |
30 | ## Standard APIs
31 |
32 | [Dataverse API 101](/dataverse-api-101/readme.md)
33 |
34 | ## Custom APIs
35 |
36 | Developers can create their own custom APIs to expose through Dataverse. Some solutions from Microsoft make use of this in order to offer scenario-specific functionality.
37 |
38 | ### Dataverse Healthcare APIs
39 |
40 | | API | Action | URI | Purpose |
41 | |-----------------|--------|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
42 | | Upsert Bundle | POST | [Organization URI]/api/data/v9.1/msind_UpsertBundle | Accepts a FHIR bundle, insert/updates values in one or more Dataverse tables based on the mapping template. |
43 | | Retrieve Bundle | POST | [Organization URI]/api/data/v9.1/msind_RetrieveBundle | Produces a FHIR bundle for the selected resource, based on the values stored in Dataverse and the mapping template. |
44 |
45 | Blog and video for how to get started: [Getting started with the Dataverse Healthcare API](https://techcommunity.microsoft.com/t5/healthcare-and-life-sciences/getting-started-with-the-dataverse-healthcare-api/ba-p/3713587)
46 |
47 | Github Gists for how to get started (referenced in the blog/video): [Dataverse Healthcare API samples](https://gist.github.com/mathyousee/3678a14fe5599cb9526428b9e1a6ed24)
48 |
49 | ## Dataverse offline mode for Canvas
50 |
51 | ### Power Fx for Dataverse offline mode
52 |
53 | **Power Fx send notification based on ConnectionSync value**
54 |
55 | ``` Power Fx
56 | Switch(
57 | Connection.Sync,
58 | ConnectionSync.Connected,
59 | Notify("Your device is connected to the network, and your app is ready to work offline.",NotificationType.Success),
60 | ConnectionSync.ConnectedWithWarning,
61 | Notify(Connection.LastSyncMessage,NotificationType.Warning),
62 | ConnectionSync.ConnectedPendingUpsync,
63 | Notify("Some data on your device must be synchronized with the server.",NotificationType.Warning),
64 | ConnectionSync.ConnectedError,
65 | Notify(Connection.LastSyncMessage,NotificationType.Error),
66 | ConnectionSync.ConnectedRefresh,
67 | Notify("Your app is currently synchronizing data with the server.",NotificationType.Information),
68 | ConnectionSync.NotConnected,
69 | Notify("Your device is not connected to the network, but you can keep using this app.",NotificationType.Success),
70 | ConnectionSync.NotConnectedWithWarning,
71 | Notify(Connection.LastSyncMessage, NotificationType.Warning),
72 | ConnectionSync.NotConnectedPendingUpsync,
73 | Notify("Some data on your device must be synchronized with the server. Reconnect to the network to synchronize.", NotificationType.Warning),
74 | ConnectionSync.NotConnectedSyncError,
75 | Notify(Connection.LastSyncMessage,NotificationType.Error)
76 | )
77 | ```
78 |
79 | ## Links
80 |
81 | > Effective November 2020:
82 | > - Common Data Service (CDS) has been renamed to Microsoft Dataverse. [Learn more](https://aka.ms/PAuAppBlog)
83 | > - Some terminology in Microsoft Dataverse has been updated. For example, *entity* is now *table* and *field* is now *column*. [Learn more](https://go.microsoft.com/fwlink/?linkid=2147247)
84 |
--------------------------------------------------------------------------------
/odata.md:
--------------------------------------------------------------------------------
1 | # Odata expressions
2 |
3 | References for Odata filters
4 |
5 | **In this article**
6 |
7 | - [Odata expressions](#odata-expressions)
8 | - [Filter results](#filter-results)
9 | - [Standard filter operators](#standard-filter-operators)
10 | - [Standard query functions](#standard-query-functions)
11 | - [Checking for null or non-null values](#checking-for-null-or-non-null-values)
12 | - [Dataverse Web API query functions](#dataverse-web-api-query-functions)
13 | - [Use Lambda operators](#use-lambda-operators)
14 | - [`any` operator](#any-operator)
15 | - [Aggregate and Grouping results](#aggregate-and-grouping-results)
16 | - [Videos](#videos)
17 | - [Links](#links)
18 |
19 | ## Filter results
20 |
21 | Use the `$filter` system query option to set criteria for which entities will be returned.
22 |
23 |
24 |
25 | ### Standard filter operators
26 |
27 | The Web API supports the standard OData filter operators listed in the following table.
28 |
29 | |Operator|Description|Odata Filter field example|
30 | |--------------|-----------------|-------------|
31 | |**Comparison Operators**|||
32 | |`eq`|Equal|`revenue eq 100000`|
33 | |`ne`|Not Equal|`revenue ne 100000`|
34 | |`gt`|Greater than|`revenue gt 100000`|
35 | |`ge`|Greater than or equal|`revenue ge 100000`|
36 | |`lt`|Less than|`revenue lt 100000` `enddate lt utcnow()` (comparisons work with dates)|
37 | |`le`|Less than or equal|`revenue le 100000`|
38 | |**Logical Operators**|||
39 | |`and`|Logical and|`revenue lt 100000 and revenue gt 2000`|
40 | |`or`|Logical or|`contains(name,'(sample)') or contains(name,'test')`|
41 | |`not`|Logical negation|`not contains(name,'sample')`|
42 | |**Grouping Operators**|||
43 | |`( )`|Precedence grouping|`(contains(name,'sample') or contains(name,'test')) and revenue gt 5000`|
44 |
45 | > [!NOTE]
46 | > This is a sub-set of the [11.2.5.1.1 Built-in Filter Operations](https://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part1-protocol/odata-v4.0-errata02-os-part1-protocol-complete.html). Arithmetic operators and the comparison has operator are not supported in the Web API.
47 |
48 |
49 |
50 | ### Standard query functions
51 |
52 | The Web API supports these standard OData string query functions:
53 |
54 | |Function|Odata Filter field example|
55 | |--------------|-------------|
56 | |`contains`|`contains(name,'(sample)')`|
57 | |`endswith`|`endswith(name,'Inc.')`|
58 | |`startswith`|`startswith(name,'a')`|
59 |
60 | > [!NOTE]
61 | > This is a sub-set of the [11.2.5.1.2 Built-in Query Functions](https://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part1-protocol/odata-v4.0-errata02-os-part1-protocol-complete.html). `Date`, `Math`, `Type`, `Geo` and other string functions aren’t supported in the web API.
62 |
63 | ### Checking for null or non-null values
64 |
65 | In the Odata filter, there's a nuance to filtering on null values. The "null" expression from the assistant is not needed. Instead, just use plain text in the odata query.
66 |
67 | The following example will include rows where *ctd_myfield* contains data
68 |
69 | ```
70 | ctd_myfield ne null
71 | ```
72 |
73 | ### Dataverse Web API query functions
74 |
75 | Dataverse provides a number of special functions that accept parameters, return Boolean values, and can be used as filter criteria in a query. See for a list of these functions. The following is an example of the searching for accounts with a number of employees between 5 and 2000.
76 |
77 | ```http
78 | GET [Organization URI]/api/data/v9.1/accounts?$select=name,numberofemployees
79 | &$filter=Microsoft.Dynamics.CRM.Between(PropertyName='numberofemployees',PropertyValues=["5","2000"])
80 | ```
81 |
82 | More information: [Compose a query with functions](use-web-api-functions.md#bkmk_composeQueryWithFunctions).
83 |
84 |
85 |
86 | ### Use Lambda operators
87 |
88 | The Web API allows you to use two lambda operators, which are `any` and `all` to evaluate a Boolean expression on a collection.
89 |
90 |
91 |
92 | ### `any` operator
93 |
94 | The `any` operator returns `true` if the Boolean expression applied is `true` for any member of the collection, otherwise it returns `false`. The `any` operator without an argument returns `true` if the collection is not empty.
95 |
96 | ## Aggregate and Grouping results
97 |
98 | By using `$apply` you can aggregate and group your data dynamically. Possible use cases with `$apply`:
99 |
100 | |Use Case|Example|
101 | |--------------|-------------|
102 | |List of unique statuses in the query|`accounts?$apply=groupby((statuscode))`|
103 | |Aggregate sum of the estimated value|`opportunities?$apply=aggregate(estimatedvalue with sum as total)`|
104 | |Average size of the deal based on estimated value and status|`opportunities?$apply=groupby((statuscode),aggregate(estimatedvalue with average as averagevalue)`|
105 | |Sum of estimated value based on status|`opportunities?$apply=groupby((statuscode),aggregate(estimatedvalue with sum as total))`|
106 | |Total opportunity revenue by Account name|`opportunities?$apply=groupby((parentaccountid/name),aggregate(estimatedvalue with sum as total))`|
107 | |Primary contact names for accounts in 'WA'|`accounts?$apply=filter(address1_stateorprovince eq 'WA')/groupby((primarycontactid/fullname))`|
108 | |Last created record date and time|`accounts?$apply=aggregate(createdon with max as lastCreate)`|
109 | |First created record date and time|`accounts?$apply=aggregate(createdon with min as firstCreate)`|
110 |
111 | The aggregate functions are limited to a collection of 50,000 records. Further information around using aggregate functionality with Dataverse can be found here: [Use FetchXML to construct a query](../use-fetchxml-construct-query.md).
112 |
113 | Additional details on OData data aggregation can be found here: [OData Extension for Data Aggregation Version 4.0](https://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs01/odata-data-aggregation-ext-v4.0-cs01.html). Note that Dataverse supports only a sub-set of these aggregate methods.
114 |
115 | ## Videos
116 |
117 | [Odata introduction - Jon Levesque and Ahmad Najjar on YouTube](https://youtu.be/Kj8M_hXWc88?list=PLN-cZRQeAiDWT0J1NW9sBDEXX3jkKkyjP)
118 |
119 | - What is Odata
120 | - What things do you use it for
121 | - How does this apply to Flow?
122 | - Operators ([29:26](https://youtu.be/Kj8M_hXWc88?list=PLN-cZRQeAiDWT0J1NW9sBDEXX3jkKkyjP&t=1766))
123 | - Functions ([30:05](https://youtu.be/Kj8M_hXWc88?list=PLN-cZRQeAiDWT0J1NW9sBDEXX3jkKkyjP&t=1805))
124 |
125 | ## Links
126 |
127 | [Odata website](https://www.odata.org/)
128 |
129 | [Web API Documentation on docs](https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/webapi/query-data-web-api)
130 |
131 | > Effective November 2020:
132 | > - Common Data Service (CDS) has been renamed to Microsoft Dataverse. [Learn more](https://aka.ms/PAuAppBlog)
133 | > - Some terminology in Microsoft Dataverse has been updated. For example, *entity* is now *table* and *field* is now *column*. [Learn more](https://go.microsoft.com/fwlink/?linkid=2147247)
134 |
--------------------------------------------------------------------------------
/power-apps.md:
--------------------------------------------------------------------------------
1 | # Power Apps
2 |
3 | References I want to remember for later.
4 |
5 | All Power Fx snippets have been separated out into the [power-fx.md](/power-fx.md) file
6 |
7 | **In this article**
8 |
9 | - [Power Apps](#power-apps)
10 | - [Embed canvas Power App in an iframe (without header)](#embed-canvas-power-app-in-an-iframe-without-header)
11 | - [Links](#links)
12 |
13 | ## Embed canvas Power App in an iframe (without header)
14 |
15 | I'll just leave this here...nothing special to it but I have checked this page for the format half a dozen times...time to add it!
16 |
17 | ```
18 | https://[applinkurl]?source=iframe
19 | ```
20 |
21 | ## Adding a canvas Power App to model-driven Power App navigation
22 |
23 | This can be done with a URL, however that opens the link in a new window. To have the app open in the main panel of a model-driven app, the technique is to use a Web Resource for the navigation item. The code of that web resource generally looks like this:
24 |
25 | ``` html
26 |
27 |
28 | Embedded App
29 |
42 |
43 |
44 |
45 |
46 |
47 | ```
48 |
49 | Source: [https://docs.microsoft.com/en-us/powerapps/maker/canvas-apps/embed-apps-dev](https://docs.microsoft.com/en-us/powerapps/maker/canvas-apps/embed-apps-dev)
50 |
51 | ## Links
52 |
53 | [Northwind Traders Sample Data](https://docs.microsoft.com/en-us/powerapps/maker/canvas-apps/northwind-install)
54 |
--------------------------------------------------------------------------------
/power-automate.md:
--------------------------------------------------------------------------------
1 | # Power Automate
2 |
3 | Little snippets, functions, and code that I want to remember later.
4 |
5 | **In this article**
6 |
7 | - [Power Automate](#power-automate)
8 | - [Format utcNow date results](#format-utcnow-date-results)
9 | - [Query Dataverse User based on O365 user ID](#query-dataverse-user-based-on-o365-user-id)
10 | - [Get first record from a Dataverse List Records action](#get-first-record-from-a-dataverse-list-records-action)
11 | - [Did the body output contain data?](#did-the-body-output-contain-data)
12 | - [Get primary entity and related record information in Dataverse flow step](#get-primary-entity-and-related-record-information-in-dataverse-flow-step)
13 | - [Relate Records action in Dataverse (Current Environment) connector](#relate-records-action-in-dataverse-current-environment-connector)
14 | - [Lookup relationships when using Create New Record action in Dataverse (Current Environment) connector](#lookup-relationships-when-using-create-new-record-action-in-dataverse-current-environment-connector)
15 | - [Plural names for Dataverse tables ending in -y](#plural-names-for-dataverse-tables-ending-in--y)
16 | - [Setting a Case regarding a Patient via the Dataverse Create Record action](#setting-a-case-regarding-a-patient-via-the-dataverse-create-record-action)
17 | - [First item from Sub-array](#first-item-from-sub-array)
18 | - [Updating list of required fields in original JSON](#updating-list-of-required-fields-in-original-json)
19 | - [Expand query for Dataverse connector, find related lookup record](#expand-query-for-dataverse-connector-find-related-lookup-record)
20 | - [Expand query for Dataverse connector, find related many records](#expand-query-for-dataverse-connector-find-related-many-records)
21 | - [Use display name of Choice field returned from Dataverse record](#use-display-name-of-choice-field-returned-from-dataverse-record)
22 | - [Parse JSON](#parse-json)
23 | - [Links](#links)
24 |
25 | ## Format utcNow date results
26 |
27 | YYYY-MM-dd
28 |
29 | ```
30 | formatDateTime(utcNow(), 'yyyy-MM-dd')
31 | ```
32 |
33 | ## Query Dataverse User based on O365 user ID
34 |
35 | Here's the situation: you are using Power Automate to do something that involves a User record in Dataverse (like assign a record), but all you know is their O365 profile. The pattern is to
36 |
37 | 1. [trigger/action that includes an O365 user record, such as [Get User Profile](https://docs.microsoft.com/en-us/Connectors/office365users/#get-user-profile-(v2)) ]
38 | 2. Query **User** entity, filter query is:
39 |
40 | ```
41 | azureactivedirectoryobjectid eq '[O365id]'
42 | ```
43 |
44 | ## Get first record from a Dataverse List Records action
45 |
46 | Next up is a scope that includes list and compose actions. A list users action will get us the value for systemuserid. This is a user’s id/GUID in Dataverse. A filter query is used to narrow the results down to just a single record when azureactivedirectoryobjectid matches id from the Get my profile‘s output. Normally Flow will throw in an apply to each loop, when dynamic content from a list records action is used. To get around this, a compose action with a first expression is used. There is only a single record that we’re after so this trick works well. The expression used is:
47 |
48 | ```
49 | first(body('List_Users_to_get_GUID')?['value']). systemuserid
50 | ```
51 |
52 | ## Did the body output contain data?
53 |
54 | Next, a condition is used to check whether or not any RAs are returned or not. If a user doesn’t have any RAs that meet our criteria, the Flow is cancelled as succeeded. If RAs are found, we’re jumping into the Flow’s first apply to each loop to iterate through the previous list records action for RAs. Imagine if those first expressions were not used in the previous steps. We’d have a pretty loopy Flow by now. The empty expression used in the Does List my RAs return values condition is:
55 |
56 | ```
57 | empty(outputs('List_my_Resource_Assignments')?['body/value'])
58 | ```
59 |
60 | ## Get primary entity and related record information in Dataverse flow step
61 |
62 | Use the "Expand Query" property in the Dataverse connector
63 |
64 | Expand N:1
65 |
66 | ```
67 | primarycontactid($select=contactid,fullname)
68 | ```
69 |
70 | Expand 1:N
71 |
72 | ```
73 | account_tasks($select=name)
74 | ```
75 |
76 | ## Relate Records action in Dataverse (Current Environment) connector
77 |
78 | This works with both one to many and many to many relationships when working in the Dataverse.
79 |
80 | ```
81 | [Environment URL]/api/data/v9.0/[Entity Schema Name]([GUID for the target record you’re associating])
82 | ```
83 |
84 | Example URL payload in the attribute, using an environment "myorg" and relationship called "ctd_contact_game". The Contact is specified in a different attribute and the game GUID is "00000000-0000-0000-0000-000000000001":
85 |
86 | ```
87 | https://myorg.crm.dynamics.com/api/data/v9.0/ctd_contact_game(00000000-0000-0000-0000-000000000001)
88 | ```
89 |
90 | Action documentation from docs.microsoft.com -
91 |
92 | Web API documentation from docs.microsoft.com -
93 |
94 | ## Lookup relationships when using Create New Record action in Dataverse (Current Environment) connector
95 |
96 | Couldn't find the syntax for how to relate to a record in a standard lookup for a new record. I didn't want to have to create the record, then relate the records in a separate step (stubbornness?). The value format for the lookup field is:
97 |
98 | ```
99 | enityschemaname(recordGUID)
100 | ```
101 |
102 |
103 | So in my particular example, I had a lookup from *Linked Activity* (Many) to *Application* (One).
104 |
105 | Getting this value in a single Flow action proved difficult, though. I struggled to get the @odata.editLink attribute as part of a First() formula, so I used Data Operations steps to clean up the record:
106 |
107 | - Data Operation: Parse JSON - technically this is optional, but I like doing this so I can more easily snag out specific attributes
108 | - Data Operation: Select - all I did here was rename the @odata.editLink field to be editLink
109 | - Data Operation: Compose - I used a first() formula to grab the output of the Select action above
110 |
111 | I was left with:
112 |
113 | ```
114 | ctd_applications(deaef762-d4f7-1a11-a815-000d3a8d1000)
115 | ```
116 |
117 | ...which I could use to fill in the lookup field.
118 |
119 | For an activity regarding field, include a forward slash in front of the schema name. For example with a Contact record:
120 |
121 | ```
122 | /contacts(recordGUID)
123 | ```
124 |
125 | ### Plural names for Dataverse tables ending in -y
126 |
127 | I have found that there is some spelling logic used behind the scenes when creating a custom table that ends in **y**.
128 |
129 | A table with the table name of **Stay** and the plural name of **Stays**, resulted in a database schema name of `ctd_stay` had the plural schema name of `ctd_staies`. This was definitely unexpected, but fortunately I was able to track it down with some troubleshooting.
130 |
131 | So in that instance, referencing the related record used the following:
132 |
133 | ```
134 | ctd_staies(recordGUID)
135 | ```
136 |
137 | ### Setting a Case regarding a Patient via the Dataverse Create Record action
138 |
139 | I found that when working when the Microsoft Cloud for Healthcare data model, there are some additional relationships to set the Patient associated to a Case record.
140 |
141 | Out of the box, the action shows a required field of *Patient (Contact)*. Note, there is an additional field in the action for *Patient (Contact)* which also needs to be populated.
142 |
143 | The format for these are:
144 |
145 | | Create Case column | Odata value |
146 | |----------------------|-----------------------|
147 | | * Patient (Contacts) | /contacts(recordGUID) |
148 | | Patient (Contacts) | contacts(recordGUID) |
149 |
150 | ## First item from Sub-array
151 |
152 | I faced a challenge with an array, where we were selecting fields to return in a flat table. One of the fields was an array, so we looked for an easy way to return just the first value from that sub-array.
153 |
154 | When using the **Data Operations - Select** action, this expression will return the first value from a sub-array
155 |
156 | ```
157 | @item()?['subArrayName'][0]
158 | ```
159 |
160 | This is used as the value in an individual row of the Select action. The sub-array in this use case was a single field. I haven't yet tested but if the sub-array had multiple fields I assume a single field's value could be isoloated with:
161 |
162 | ```
163 | @item()?['subArrayName'][0]. columnmane
164 | ```
165 |
166 | ## Updating list of required fields in original JSON
167 |
168 | If you try to Parse JSON and a required field is null, the Flow will fail when run. In the Parse JSON step, modify the *required* fields by updating the *required* array in the **Schema** attribute
169 |
170 | Here's what it looks like if you still have some required fields:
171 | ```
172 | "required": [
173 | "name",
174 | "id"
175 | ]
176 | ```
177 |
178 | Here's what it looks like if you want no required fields (provided as an FYI but don't abuse this!):
179 | ```
180 | "required": []
181 | ```
182 |
183 | A Type mismatch will also cause a failure, so consider updating the output schema like the *id* attribute below if the source format is not strict:
184 |
185 | ```
186 | "name": {string},
187 | "id": {},
188 | "description": {string}
189 | ```
190 | More on this -
191 |
192 | ## Expand query for Dataverse connector, find related lookup record
193 |
194 | Expand lookup fields to get the fields you actually care about. For example, if you list Contacts and want the Parent Account name, you could add this to the expand query field:
195 |
196 | ``` odata
197 | parentcustomerid_account($select=name)
198 | ```
199 |
200 | Then let's say, perhaps you listed multiple Contacts (and their related account names), but later in the flow you just want to grab the first one. You can grab the related account name for the first Contact record with an expression like this:
201 |
202 | ``` odata
203 | first(outputs('List_rows')?['body/value'])?['parentcustomerid_account']?['name']
204 | ```
205 |
206 | Note, that the "first" is just the row, then outside (after) the first() function. Also, the expanded value is an object and you need to get the specific field, even though you only expanded one field.
207 |
208 | Note, a single property didn't need to be selected, in fact all of the related fields could be expanded by just calling the relationship name *without* using the `$select` system query option. It's also important in this scenario to omit the parenthesis
209 |
210 | ``` odata
211 | parentcustomerid_account
212 | ```
213 |
214 | Multiple levels of $expand lookup are possible, but limit of 10 $expand (up or down) per query). If doing this to traverse levels, the `$expand=relationshipname` is inside of the first relationship name (in parenthesis).
215 |
216 | More details are here: [MS Docs](https://docs.microsoft.com/en-us/power-automate/dataverse/list-rows)
217 |
218 | ## Expand query for Dataverse connector, find related many records
219 |
220 | This is very similar to expanding a lookup record. Here's an example of getting the Contacts associated with an Account:
221 |
222 | ```odata
223 | contact_customer_accounts
224 | ```
225 |
226 | You can select individual columns when expanding many records as well. The following example gets the *fullname* column for all Contacts related to an Account.
227 |
228 | ```odata
229 | contact_customer_accounts($select=fullname)
230 | ```
231 |
232 | Even though it's only a single column that's been selected, it's returned as an object with some additional fields (such as *contactid* in the example above).
233 |
234 | https://docs.microsoft.com/en-us/powerapps/developer/data-platform/webapi/retrieve-related-entities-query
235 |
236 | ## Use display name of Choice field returned from Dataverse record
237 |
238 | The example below gets the localized Stage Category label returned in the body of a previously called GetItem action called *Get_Process_Stage_Details*.
239 |
240 | ```
241 | outputs('Get_Process_Stage_Details')?['body/stagecategory@OData.Community.Display.V1.FormattedValue']
242 | ```
243 |
244 | ## Parse JSON
245 |
246 | Mini Tutorial (YouTube video)
247 |
248 | Thesis on Parse JSON action in Power Automate -
249 |
250 | ## Links
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 | [Logic Apps Functions Reference](https://learn.microsoft.com/en-us/azure/logic-apps/workflow-definition-language-functions-reference)
261 |
262 |
263 |
264 | > Effective November 2020:
265 | > - Common Data Service (CDS) has been renamed to Microsoft Dataverse. [Learn more](https://aka.ms/PAuAppBlog)
266 | > - Some terminology in Microsoft Dataverse has been updated. For example, *entity* is now *table* and *field* is now *column*. [Learn more](https://go.microsoft.com/fwlink/?linkid=2147247)
267 |
--------------------------------------------------------------------------------
/power-fx.md:
--------------------------------------------------------------------------------
1 | # Power Fx
2 |
3 | Little snippets, functions, and code that I want to remember later.
4 |
5 | **In this article**
6 |
7 | - [Power Fx](#power-fx)
8 | - [Format currency](#format-currency)
9 | - [Format with a custom date format](#format-with-a-custom-date-format)
10 | - [Quotation marks in text](#quotation-marks-in-text)
11 | - [Create a Collection by hand](#create-a-collection-by-hand)
12 | - [Collect response values from a Flow](#collect-response-values-from-a-flow)
13 | - [Add Columns to a Collection using a LookUp to another table](#add-columns-to-a-collection-using-a-lookup-to-another-table)
14 | - [Updating the user defined function](#updating-the-user-defined-function)
15 | - [Simple theming](#simple-theming)
16 | - [Relate and Unrelate N:N Dataverse records](#relate-and-unrelate-nn-dataverse-entities)
17 | - [Navigation Commands](#navigation-commands)
18 | - [Navigate to Page from Grid or Record](#navigate-to-page-from-grid-or-record)
19 | - [Handle command navigation parameter in Canvas Page](#handle-command-navigation-parameter-in-canvas-page)
20 | - [Patch Opportunity Customer](#patch-opportunity-customer)
21 | - [Validate email format has been followed](#validate-email-format-has-been-followed)
22 | - [Strip non-numeric characters from a string](#strip-non-numeric-characters-from-a-string)
23 | - [Teams URL-based deep links](#teams-url-based-deep-links)
24 | - [Truncate long labels](#truncate-long-label)
25 | - [Set multiple default items in a ComboBox](#set-multiple-default-items-in-a-combobox)
26 | - [Links](#links)
27 |
28 | ## Format currency
29 |
30 | EN-US $2,000.00
31 |
32 | ```
33 | Text( currencyfield, "[$-en-US]$ #,###.00" )
34 | ```
35 |
36 | ## Format with a custom date format
37 |
38 | The key to this one is to pass in a legit date/datetime value. If the source field provides the field as a *Date*, you're good to go. Use the DateValue() or DateTimeValue() function as needed to adjust the source from *Text* to *Date*
39 |
40 | ```
41 | Text(date or datetime field, "[$-en-US] yyyy-mm-dd hh:mm:ss.fff AM/PM" )
42 |
43 | (adjust the yyyy-mm-dd... part as appropriate)
44 | ```
45 |
46 | More comprehensive details on [docs.microsoft.com](https://docs.microsoft.com/en-us/powerapps/maker/canvas-apps/show-text-dates-times)
47 |
48 | ## Quotation marks in text
49 |
50 | ```
51 | "I would like ""this"" to be in quotes." // Reads: I would like "this" to be in quotes.
52 | ```
53 |
54 | ## Create a Collection by hand
55 |
56 | When I have an app where I want to reference specific fields, but I haven't attached to a data source, I like to use a Collection as a placeholder. This allows me to use similar data to get things looking right.
57 |
58 | The following example is from an app where I later populated a URL list based on a search query. Simple 2-column blank table:
59 |
60 | ```
61 | ClearCollect(flowSiteURL,{url:"",name:""});
62 | ```
63 |
64 | The format with sample data and additional rows can look like this:
65 |
66 | ```
67 | ClearCollect(flowSiteURL,{url:"https://make.powerapps.com",name:"Power Platform Maker Portal"},{url:"https://connectingthedata.com",name:"Connecting the Data website"});
68 | ```
69 |
70 | ## Collect response values from a Flow
71 |
72 | At the end of a Power Automate Flow, you can return data to the app. When calling the Flow, call it as the table value for a Collection.
73 |
74 | ```
75 | ClearCollect(varFlowResponse,FlowName.Run())
76 | ```
77 |
78 | This won't behave correctly if you modify the Response format in the Flow after it's been added to the Power App. The collection will become a single value of *True* even though the full payload is there. The fix for this is to remove all references to the Flow, save/close the Power App, then open and re-add the Flow.
79 |
80 | ## Add Columns to a Collection using a LookUp to another table
81 |
82 | In the following, I'm creating a local Collection, which includes a few columns from a related table.
83 |
84 | I've already pulled the related table into a local collection *LocalFoods* so the LookUps don't get chatty with network calls to the data source.
85 |
86 | ```
87 | ClearCollect(LocalInventory,
88 | AddColumns(Filter(Inventory,Gone=false),
89 | "FoodName", LookUp(LocalFoods,UPC = ParentUPC).Title,
90 | "FoodSize", LookUp(LocalFoods,UPC = ParentUPC).Size,
91 | "FoodSearchTags", LookUp(LocalFoods,UPC = ParentUPC).SearchTags,
92 | "FoodUnits", LookUp(LocalFoods,UPC = ParentUPC).UnitsInPackage
93 | )
94 | );
95 | ```
96 |
97 | Things to remember: for the LookUp, the first parameter is the "parent" table, the second parameter starts with he column I'm comparing in the second table...this gets the record. Then I returmn the specific **.Field** as the result for the column.
98 |
99 | ```
100 | LookUp(LocalFoods,UPC = ParentUPC).Title
101 | ^ ^ ^ ^
102 | | | \ `----.
103 | [Parent Table] [Parent Field] [Match value] [Returned Field]
104 | ```
105 |
106 | ### Updating the user defined function
107 |
108 | In the future, if you need to update the formulas, simply change the *fncMyFunction.OnChange* Action property. All of the rest of the references can stay as they are.
109 |
110 | Credit: [Code re-usability in Power Apps](https://powerusers.microsoft.com/t5/News-Announcements/Code-re-usability-in-PowerApps-using-User-Defined-functions/ba-p/672998#)
111 |
112 | ## Simple theming
113 |
114 | The [Power Platform Creator Kit](https://docs.microsoft.com/en-us/power-platform/guidance/creator-kit/overview) includes a theme designer (which is verrrrrry similar to this [Fluent UI Theme Designer](https://fluentuipr.z22.web.core.windows.net/heads/master/theming-designer/index.html)).
115 |
116 | To use these in the app, I set a variable with multiple properties:
117 |
118 | ```
119 | Set(AppTheme,
120 | { palette:
121 | {
122 | themePrimary: "#0078d4",
123 | themeLighterAlt: "#eff6fc",
124 | themeLighter: "#deecf9",
125 | themeLight: "#c7e0f4",
126 | themeTertiary: "#71afe5",
127 | themeSecondary: "#2b88d8",
128 | themeDarkAlt: "#106ebe",
129 | themeDark: "#005a9e",
130 | themeDarker: "#004578",
131 | neutralLighterAlt: "#faf9f8",
132 | neutralLighter: "#f3f2f1",
133 | neutralLight: "#edebe9",
134 | neutralQuaternaryAlt: "#e1dfdd",
135 | neutralQuaternary: "#d0d0d0",
136 | neutralTertiaryAlt: "#c8c6c4",
137 | neutralTertiary: "#a19f9d",
138 | neutralSecondary: "#605e5c",
139 | neutralPrimaryAlt: "#3b3a39",
140 | neutralPrimary: "#323130",
141 | neutralDark: "#201f1e",
142 | black: "#000000",
143 | white: "#ffffff"
144 | }
145 | }
146 | )
147 | ```
148 |
149 | Then instead of specifying specific colors in my app, I'll reference AppTheme.pallete.themePrimary or AppTheme.pallete.neutralPrimary instead of the RBGA value (or color name). Also, this works with the Template apps.
150 |
151 | Then, later, if I want to modify the colors in my app later, I can update this one variable and it's immediately reflected throughout the app.
152 |
153 | Those who have followed this resource probably saw a variant on this with far fewer colors. While this is definitely a lot more to choose from, it's not really so complex. I explored some more of the Fluent UI theme designer web app "semantic slots" detail and came up with my most important subset of colors:
154 |
155 | - **themePrimary** - Used for links, button backgrounds, icons, and headers
156 | - **neutralPrimary** - used for primary text
157 | - **white** - used for backgrounds, button text, (note this is note necessarily white, but instead is the "Background Color" in the theme designer, but black always seems to be Black)
158 | - **neutralLight** - used for many element borders, also menu/list item backgrounds on hover
159 | - **themeDark** - used for button hover/press colors
160 | - **neutralTertiary** - used for many disabled elements. Note, this is an oversimplification but close enough for my apps
161 |
162 | I use the same approach for fonts, though admittedly I'm still trying to find the sweet spot of how much to define/automate this way. Note, I haven't updated the AppTheme variable to include fonts, but the template apps aren't expecting it either.
163 |
164 | ```
165 |
166 | Set(MyFont,
167 | {
168 | FaceHeader: Font.'Lato Black',
169 | FaceBody: Font.'Lato Light',
170 | SizeH1: 28,
171 | SizeH2: 24,
172 | SizeLarge: 16,
173 | SizeStandard: 14,
174 | SizeTiny: 12
175 | }
176 | )
177 |
178 | ```
179 |
180 | This approach is also useful for other general properties that need to be set for various controls. I borrowed the target list from the Center of Excellence theme editor.
181 |
182 | ```
183 |
184 | Set(myProperties,
185 | {
186 | TextBoxHeight: 80,
187 | RadiusValue: 5,
188 | PaddingValue: 8,
189 | BorderThicknessValue: 0,
190 | FocusedBorderThicknessValue: 0
191 | }
192 | )
193 |
194 | ```
195 |
196 | To save time on setting these properties, I try to use copy/paste of a similar control that I've already treated (in favor of adding a new one from the control library). That said, if I have multiple components to update at once, I'll multi-select these from the navigation view, then use the formula bar to choose he property (such as Font) then set all of the selected components to MyFont.FaceBody.
197 |
198 | ## Relate and Unrelate N:N Dataverse entities
199 |
200 | The following example assumes you are tracking volunteers and the organizations with which they volunteer. Any given volunteer (Contact) can volunteer with multiple organizations (Accounts) and the Dataverse N:N relationship has been created between the Account and Contact to represent this.
201 |
202 | The following sample relates a volunteer contact (selected in the gal_Contacts gallery) with an Account (selected in the gal_Account gallery).
203 |
204 | ```
205 | Relate(gal_Account.Volunteers,gal_Contacts.Selected)
206 | ```
207 |
208 | The format of the function is to identify a record in a table then the relationship name via dot notation (.Volunteers) as the first parameter, then the related record as the second parameter.
209 |
210 | ## Navigation Commands
211 |
212 | JavaScript is supported as well, so there are some functions included below as well.
213 |
214 | ### Navigate to Page from Grid or Record
215 |
216 | There's probably a more elegant way to use a single JavaScript function, but hey, it works :)
217 |
218 | The following must be uploaded as a JavaScript web resource. It hard codes the page name for simple build, but I'm sure there's something more elegant that someone can come up with to pass it in as a parameter to the function.
219 |
220 | ``` js
221 | function openFullPageFromGrid(selectedItems)
222 | {
223 | let selectedItem = selectedItems[0];
224 |
225 | if (selectedItem) {
226 | let pageInput = {
227 | pageType: "custom",
228 | name: "ctd_custompagelogicalname_35171",
229 | entityName: selectedItem.TypeName,
230 | recordId: selectedItem.Id,
231 | };
232 | let navigationOptions = {
233 | target: 1
234 | };
235 | Xrm.Navigation.navigateTo(pageInput, navigationOptions)
236 | .then(
237 | function () {
238 | // Handle success
239 | }
240 | ).catch(
241 | function (error) {
242 | // Handle error
243 | }
244 | );
245 | }
246 | }
247 |
248 | function openFullPageFromItem(selectedItem)
249 | {
250 | // Inline Page
251 | var pageInput = {
252 | pageType: "custom",
253 | name: "ctd_custompagelogicalname_35171",
254 | entityName: "ctd_table",
255 | recordId: selectedItem,
256 | };
257 | var navigationOptions = {
258 | target: 1
259 | };
260 | Xrm.Navigation.navigateTo(pageInput, navigationOptions)
261 | .then(
262 | function () {
263 | // Called when page opens
264 | }
265 | ).catch(
266 | function (error) {
267 | // Handle error
268 | }
269 | );
270 |
271 | }
272 | ```
273 |
274 | Calling from the **Main grid**:
275 |
276 | - Open the Navigation editor
277 | - *Make sure the Power Fx formula for **OnSelect** is cleared out*
278 | - Choose action type of JavaScript
279 | - Set the appropriate library
280 | - Set the function name `openFullPageFromGrid`
281 | - Add a parameter: *SelectedControlSelectedItemRecord*
282 |
283 | Note, I'm also including a rule to only show the button when a single record is selected.
284 |
285 | ``` PowerFx
286 | CountRows(Self.Selected.AllItems) = 1
287 | ```
288 |
289 | Calling from the **Record form**:
290 |
291 | - Open the Navigation editor
292 | - *Make sure the Power Fx formula for **OnSelect** is cleared out*
293 | - Choose action type of JavaScript
294 | - Set the appropriate library
295 | - Set the function name `openFullPageFromItem`
296 | - Add a parameter: *FirstPrimaryItemId*
297 |
298 | ### Handle command navigation parameter in Canvas Page
299 |
300 | The parameter that gets passed to the page has a slightly different format, it may include curly braces {}, so when consuming the parameter, make sure to strip out the braces in most cases. Example formula is below.
301 |
302 | ```
303 | Set(RecordItem,
304 | If(IsBlank(Param("recordId")),
305 | First('Table'),
306 | LookUp('Table', 'RecordId' = GUID(Substitute(Substitute(Param("recordId"),"{",""),"}",""))))
307 | )
308 | ```
309 |
310 | ## Patch Opportunity Customer
311 |
312 | I can never seem to remember the Patch() command when setting an Opportunity Potential Customer in Dynamics 365 Sales. The bare bones are:
313 |
314 | ```
315 | Patch(Opportunities,Defaults(Opportunities),{
316 | Topic: "Test",
317 | _customerid_value: varMyCustomerRecord
318 | })
319 | ```
320 |
321 | Where `varMyCustomerRecord` is a variable set to the *Record* for an Account or Contact. `Defaults(Opportunities)` is used to create a new Opportunity, but this could reference an existing Opportunity *Record*.
322 |
323 | ## Validate email format has been followed
324 |
325 | Check to make sure the proper name@domain.extension format has been followed. This is more precise than other approaches that rely on the "IsMatch(myFancyText,Email)" which can provide false positives.
326 |
327 | ``` PowerFx
328 | IsMatch(myFancyText,"^([a-zA-Z0-9_.-])+@(([a-zA-Z0-9-])+.)+([a-zA-Z0-9]{2,4})+$")
329 | ```
330 |
331 | The example below is from a button, if the input_email control has an email address, then collect that email address, otherwise send up a notification.
332 |
333 | ``` PowerFx
334 | If(
335 | IsMatch(input_email_1.Text,"^([a-zA-Z0-9_.-])+@(([a-zA-Z0-9-])+.)+([a-zA-Z0-9]{2,4})+$"),
336 | Collect(col_invitees,{channel:"email",value:input_email_1.Text});Reset(input_email_1),
337 | Notify("Please use a valid email address",NotificationType.Error)
338 | )
339 | ```
340 |
341 | ## Strip non-numeric characters from a string
342 |
343 | I had a requirement to take a parameter of a phone number, but the phone number may have unexpected formatting (some may have parenthesis, dashes, spaces...nothing consistent).
344 |
345 | I used the MatchAll() function to grab all of the digits from the string (relying on a regular expression for matching), then wrapped with the Concat() function to squish them all of the "FullMatch"es back together.
346 |
347 | ``` PowerFx
348 | Concat(MatchAll(myFancyEmail.Text,"\d"),FullMatch)
349 | ```
350 |
351 | In this case, the value of "abc(800) 555-1234" returns "8005551234", which I could use consistently through the app.
352 |
353 | More on matching functions can be found here: https://learn.microsoft.com/en-us/power-platform/power-fx/reference/function-ismatch and I like
354 |
355 | ## Teams URL-based deep links
356 |
357 | I have several apps where I launch unscheduled (ad-hoc) Teams voice calls.
358 |
359 | To dial out to a traditional phone number, I use the following format to prepend the phone number.
360 |
361 | ``` PowerFx
362 | Launch("https://teams.microsoft.com/l/call/0/0?users=4:"&varFancyPhoneNumber)
363 | ```
364 |
365 | Note, the important thing is the `4:` before the phone number.
366 |
367 | [More examples on Microsoft Learn](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/deep-links?tabs=teamsjs-v2)
368 |
369 | ## URL parameter consumption formula
370 |
371 | Upon learning about the upcoming feature of *Named formulas* in a Power App, there are some opportunities to standardize some common operations. One of those is passing a record into a Canvas App.
372 |
373 | ``` PowerFx
374 | varMyRecord=If(
375 | IsBlank(Param("RecordId")),
376 | LookUp('Contacts','Contact'=GUID("b57974f4-b403-ed21-82e4-000d3a997ecc")),
377 | LookUp('Contacts','Contact'=GUID(Param("RecordId")))
378 | );
379 | ```
380 |
381 | The upshot of this approach is that this can be used with a Debug flag in the app, to add in a check for the If statement.
382 |
383 | For simplicity, I am assuming a valid ID is passed if Param() is not blank...but error handling should be considered in how your app should behave if a bad GUID was passed.
384 |
385 | ## Truncate long label
386 |
387 | In a label field, sometimes I like using a flexible height, but other times I just want to cut off the text in a way that shows there's "more".
388 |
389 | ``` PowerFx
390 | If(Len(ThisItem.Name)>25,Left(ThisItem.Name,25)&"...",ThisItem.Name)
391 | ```
392 |
393 | The example above is used for a Label control inside of a Gallery.
394 |
395 | - I set the above in a *Label.Text* property, then typically also add the full **ThisItem.Name** in the *Label.Tooltip* property.
396 | - I'm using *25* for the length, however this should be based on your label size.
397 | - The inclusion of the "..." is a hint to the user that the full value is not visible. Note: the ellipsis adds 3 characters, so make sure to trim enough characters that the ellipsis dont' cause overflow.
398 | - Remember the impact of variable width fonts... *llllll* has a different width than *WWWWWW* even though they're both 6 characters.
399 |
400 | ## Set multiple default items in a ComboBox
401 |
402 | Set the ComboBox.DefaultSelectedItems property to a Table() value. Something like this:
403 |
404 | ``` Power Fx
405 | Table(
406 | LookUp(Users,User=GUID("b83c01bc-7cc2-ed11-83af-000d3a1a02cb")),
407 | LookUp(Users,User=GUID("511d59f1-5de8-ed11-a6c6-000d3a1a02cb"))
408 | )
409 | ```
410 |
411 | This will ensure that the full record is selected, which becomes important when referencing field values in other formulas.
412 |
413 | ## Links
414 |
415 | [Power Fx overview](https://docs.microsoft.com/en-us/power-platform/power-fx/overview)
416 |
417 | [Canvas Power Apps formula reference](https://docs.microsoft.com/en-us/powerapps/maker/canvas-apps/formula-reference)
418 |
419 | [Regular Expression Builder](https://regexr.com) - this is the online tool I use to hack out the regex expressions I can't easily find in a web search.
420 |
--------------------------------------------------------------------------------
/power-pages.md:
--------------------------------------------------------------------------------
1 | # Power Pages
2 |
3 | ## Track the authenticated portal contact who creates/modifies a record
4 |
5 | As of this writing, this is not explicitly tracked by default. The following method creates the relationship between the creating/modifying contact associated with a Dataverse record:
6 |
7 | [Community Blog: Power Pages: What Contact Created or Modified This Dataverse Row?](https://community.dynamics.com/365/b/crminthefield/posts/power-pages-what-contact-created-modified-this-record)
8 |
9 | ## Lightweight internal web apps
10 |
11 | Placeholder for internal-facing lightweight web apps.
12 |
13 |
--------------------------------------------------------------------------------
/power-virtual-agents.md:
--------------------------------------------------------------------------------
1 | # Power Virtual Agents
2 |
3 | Little snippets, functions, and code that I want to remember later.
4 |
5 | **In this article**
6 |
7 | - [Power Virtual Agents](#power-virtual-agents)
8 | - [Default values for variables in PVA](#default-values-for-variables-in-pva)
9 | - [Links](#links)
10 |
11 | ## Default values for variables in PVA
12 |
13 | The basic pattern as of today is to create a Power Automate flow with Response values for each of the variables to set.
14 |
15 | ## Links
16 |
17 |
18 |
--------------------------------------------------------------------------------
/sample-code/ppcoe-capacity-extension/CoECapacityExtension_2_1_0_3.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/sample-code/ppcoe-capacity-extension/CoECapacityExtension_2_1_0_3.zip
--------------------------------------------------------------------------------
/sample-code/ppcoe-capacity-extension/CoECapacityExtension_2_1_0_3_managed.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/sample-code/ppcoe-capacity-extension/CoECapacityExtension_2_1_0_3_managed.zip
--------------------------------------------------------------------------------
/sample-code/ppcoe-capacity-extension/README.md:
--------------------------------------------------------------------------------
1 | # Deprecated - Center of Excellence - Capacity Extension solution
2 |
3 | 
4 |
5 | ## Deprecation announcement
6 |
7 | The Center of Excellence Starter Kit has been updated to include this functinoality out of the box!! No need to install this additional solution, but I'll keep it here for posterity.
8 |
9 | ## Summary
10 |
11 | Extension to the Power Platform Center of Excellence Starter Kit. Track database storage targets and actual consumption.
12 |
13 | - Track targets/actual on a per-environment basis
14 | - Environment records updated with current DB consumption once daily
15 | - Grid-view reporting to show environments over 80% of target
16 | - Includes charts to show consumption.
17 |
18 | > **Note:** This sample is one of a collection of Power Platform samples.
19 | > [Check out the larger list here](../../README.md#Sample-Solutions).
20 |
21 | ## Deployment
22 |
23 | ### Prerequisites
24 |
25 | - Environment with **Center of Excellence – Core Components** v1.45 or greater deployed
26 |
27 | ### Solution Deployment
28 |
29 | - Import the solution via the Power Apps maker portal -> Solutions area
30 | - Either:
31 | - Managed: CoECapacityExtension_x_x_x_x.zip
32 | - Unmanaged: CoECapacityExtension_x_x_x_x.zip
33 | - Modify the **Power Platform Admin View** app
34 | - Add the following Environment components
35 | - Form: **main form**
36 | - Views: all views with “capacity” in the title
37 | - Chart: **Environment Data Storage**
38 | - Deselect the original form from showing the app
39 | - Form: **Main Environment Form**
40 | - Save and publish the app
41 |
42 | ### Update data
43 |
44 | - Log into the **Power Platform Admin View** app
45 | - Navigate to the Environments -> **Active Environments with Capacity** view
46 | - Save the data to CRM and wait for the import to refresh.
47 |
48 | ## Release History
49 |
50 | ### 2.1.0
51 |
52 | - Environment table
53 | - Added *Rated Consumption* column
54 | - Adjusted consumed percentage to use *Rated Consumption* column
55 | - Added *Rated Consumption* to main form
56 | - Added *Rated Consumption* to capacity views
57 | - Filtered capacity views to include flag for *hasCds=True*
58 |
59 | ### 2.0.0
60 |
61 | - Added *Log environment DB consumption* cloud Flow
62 | - Daily recurring trigger
63 | - Queries Power Platform Admin API, version 2020-10-01 with *Capacity* data preview
64 | - Updates static value in Environment record
65 |
--------------------------------------------------------------------------------
/sample-code/ppcoe-capacity-extension/ppcoe-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/sample-code/ppcoe-capacity-extension/ppcoe-screenshot.png
--------------------------------------------------------------------------------
/sample-code/ppcoe-capacity-extension/solution-archive/CoECapacityExtension_1_0_0_1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/sample-code/ppcoe-capacity-extension/solution-archive/CoECapacityExtension_1_0_0_1.zip
--------------------------------------------------------------------------------
/sample-code/ppcoe-capacity-extension/solution-archive/CoECapacityExtension_1_0_0_1_managed.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/sample-code/ppcoe-capacity-extension/solution-archive/CoECapacityExtension_1_0_0_1_managed.zip
--------------------------------------------------------------------------------
/sample-code/ppcoe-capacity-extension/solution-archive/CoECapacityExtension_2_0_0_2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/sample-code/ppcoe-capacity-extension/solution-archive/CoECapacityExtension_2_0_0_2.zip
--------------------------------------------------------------------------------
/sample-code/ppcoe-capacity-extension/solution-archive/CoECapacityExtension_2_0_0_2_managed.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathyousee/power-platform-snippets/696c5eb9dfda56d74bf84dd5552cf8b7716205f5/sample-code/ppcoe-capacity-extension/solution-archive/CoECapacityExtension_2_0_0_2_managed.zip
--------------------------------------------------------------------------------